diff --git a/.github/ISSUE_TEMPLATE/release-checklist.md b/.github/ISSUE_TEMPLATE/release-checklist.md index 598d41dfd..d53c1e820 100644 --- a/.github/ISSUE_TEMPLATE/release-checklist.md +++ b/.github/ISSUE_TEMPLATE/release-checklist.md @@ -7,11 +7,18 @@ assignees: '' --- +**QA** +- [ ] Play through tutorials and verify that they are working. +- [ ] Do a smoketest on all game modes +- [ ] Smoketest server hosting (dedicated and client) +- [ ] Install Trick or Trauma and check you can start a round with no obvious issues/errors (to make sure we didn't unintentionally break compatibility with older mods). +- [ ] Play through one single player campaign round. +- [ ] Do a smoketest in a language other than English. + **Code:** - [ ] Build and upload dedicated server - [ ] Verify that Vanilla content package hashes match between Windows/Mac/Linux - [ ] Run "checkmissingloca" command to make sure localization files are up-to-date. -- [ ] Install Trick or Trauma and check you can start a round with no obvious issues/errors (to make sure we didn't unintentionally break compatibility with older mods). - [ ] Prepare new main menu content (changelog) - [ ] Prepare public github repo for pushing the new changes diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 02fe9bec7..17c4300d4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -261,7 +261,7 @@ namespace Barotrauma if (targetPos == Vector2.Zero) { Vector2 moveInput = Vector2.Zero; - if (allowMove) + if (allowMove && !Freeze) { if (GUI.KeyboardDispatcher.Subscriber == null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs index a16f76d39..dd836a3c7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs @@ -1,6 +1,5 @@ using Microsoft.Xna.Framework; using FarseerPhysics; -using System.Linq; namespace Barotrauma { @@ -64,6 +63,27 @@ namespace Barotrauma GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 60 + offset), $"ACTIVE OBJECTIVE: {activeObjective.DebugTag} ({activeObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } } + for (int i = 0; i < ObjectiveManager.Objectives.Count; i++) + { + var objective = ObjectiveManager.Objectives[i]; + int offsetMultiplier; + if (ObjectiveManager.CurrentOrder == null) + { + if (i == 0) + { + continue; + } + else + { + offsetMultiplier = i - 1; + } + } + else + { + offsetMultiplier = i + 1; + } + GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(120, offsetMultiplier * 18 + 100), $"{objective.DebugTag} ({objective.Priority.FormatZeroDecimal()})", Color.White, Color.Black * 0.5f); + } } if (steeringManager is IndoorsSteeringManager pathSteering) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index aa19f80c1..618176093 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -409,11 +409,6 @@ namespace Barotrauma emitter.Emit(1.0f, limb.WorldPosition, character.CurrentHull, amountMultiplier: gibParticleAmount); } - - if (!string.IsNullOrEmpty(character.BloodDecalName)) - { - character.CurrentHull?.AddDecal(character.BloodDecalName, limb.WorldPosition, MathHelper.Clamp(limb.Mass, 0.5f, 2.0f)); - } } if (playSound) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 743e8511c..ec57f76ff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -97,8 +97,6 @@ namespace Barotrauma set { chromaticAberrationStrength = MathHelper.Clamp(value, 0.0f, 100.0f); } } - public string BloodDecalName => Params.BloodDecal; - private readonly List bloodEmitters = new List(); public IEnumerable BloodEmitters { @@ -617,7 +615,7 @@ namespace Barotrauma partial void SetOrderProjSpecific(Order order, string orderOption) { - GameMain.GameSession?.CrewManager?.DisplayCharacterOrder(this, order, orderOption); + GameMain.GameSession?.CrewManager?.AddCurrentOrderIcon(this, order, orderOption); } public static void AddAllToGUIUpdateList() @@ -786,7 +784,7 @@ namespace Barotrauma } if (CampaignInteractionType != CampaignMode.InteractionType.None && AllowCustomInteract) { - var iconStyle = GUI.Style.GetComponentStyle("CampaignInteractionIcon." + CampaignInteractionType); + var iconStyle = GUI.Style.GetComponentStyle("CampaignInteractionBubble." + CampaignInteractionType); if (iconStyle != null) { Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.WorldPosition ?? WorldPosition + Vector2.UnitY * 100.0f; @@ -825,15 +823,19 @@ namespace Barotrauma /// Creates a progress bar that's "linked" to the specified object (or updates an existing one if there's one already linked to the object) /// The progress bar will automatically fade out after 1 sec if the method hasn't been called during that time /// - public HUDProgressBar UpdateHUDProgressBar(object linkedObject, Vector2 worldPosition, float progress, Color emptyColor, Color fullColor) + public HUDProgressBar UpdateHUDProgressBar(object linkedObject, Vector2 worldPosition, float progress, Color emptyColor, Color fullColor, string textTag = "") { - if (controlled != this) return null; + if (controlled != this) { return null; } if (!hudProgressBars.TryGetValue(linkedObject, out HUDProgressBar progressBar)) { - progressBar = new HUDProgressBar(worldPosition, Submarine, emptyColor, fullColor); + progressBar = new HUDProgressBar(worldPosition, Submarine, emptyColor, fullColor, textTag); hudProgressBars.Add(linkedObject, progressBar); } + else + { + progressBar.TextTag = textTag; + } progressBar.WorldPosition = worldPosition; progressBar.FadeTimer = Math.Max(progressBar.FadeTimer, 1.0f); @@ -859,7 +861,7 @@ namespace Barotrauma } var selectedSound = matchingSounds.GetRandom(); if (selectedSound?.Sound == null) { return; } - soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, AnimController.WorldPosition, selectedSound.Volume, selectedSound.Range, CurrentHull); + soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, AnimController.WorldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: CurrentHull); soundTimer = soundInterval; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 97af07d7e..2b6d18c7e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -52,7 +52,6 @@ namespace Barotrauma return character?.Inventory != null && character.AllowInput && - !character.LockHands && (controller?.User != character || !controller.HideHUD) && !IsCampaignInterfaceOpen && !ConversationAction.FadeScreenToBlack; @@ -227,7 +226,7 @@ namespace Barotrauma Color.Lerp(GUI.Style.Red, GUI.Style.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha); } - if (!character.IsIncapacitated && character.Stun <= 0.0f && !IsCampaignInterfaceOpen) + if (!character.IsIncapacitated && character.Stun <= 0.0f && !IsCampaignInterfaceOpen && (!character.IsKeyDown(InputType.Aim) || character.SelectedItems.Any(it => it?.GetComponent() == null))) { if (character.FocusedCharacter != null && character.FocusedCharacter.CanBeSelected) { @@ -307,6 +306,14 @@ namespace Barotrauma { progressBar.Draw(spriteBatch, cam); } + + foreach (Character npc in Character.CharacterList) + { + if (npc.CampaignInteractionType == CampaignMode.InteractionType.None || npc.Submarine != character.Submarine || npc.IsDead || npc.IsIncapacitated) { continue; } + + var iconStyle = GUI.Style.GetComponentStyle("CampaignInteractionIcon." + npc.CampaignInteractionType); + GUI.DrawIndicator(spriteBatch, npc.WorldPosition, cam, 500.0f, iconStyle.GetDefaultSprite(), iconStyle.Color); + } } if (character.SelectedConstruction != null && @@ -355,7 +362,7 @@ namespace Barotrauma } if (ShouldDrawInventory(character)) { - character.Inventory.Locked = LockInventory(character); + character.Inventory.Locked = character == Character.Controlled && LockInventory(character); character.Inventory.DrawOwn(spriteBatch); character.Inventory.CurrentLayout = CharacterHealth.OpenHealthWindow == null && character.SelectedCharacter == null ? CharacterInventory.Layout.Default : @@ -369,14 +376,10 @@ namespace Barotrauma { if (character.SelectedCharacter.CanInventoryBeAccessed) { - ///character.Inventory.CurrentLayout = Alignment.Left; + character.SelectedCharacter.Inventory.Locked = false; character.SelectedCharacter.Inventory.CurrentLayout = CharacterInventory.Layout.Left; character.SelectedCharacter.Inventory.DrawOwn(spriteBatch); } - else - { - //character.Inventory.CurrentLayout = (CharacterHealth.OpenHealthWindow == null) ? Alignment.Center : Alignment.Left; - } if (CharacterHealth.OpenHealthWindow == character.SelectedCharacter.CharacterHealth) { character.SelectedCharacter.CharacterHealth.Alignment = Alignment.Left; @@ -450,7 +453,6 @@ namespace Barotrauma private static bool LockInventory(Character character) { if (character?.Inventory == null || !character.AllowInput || character.LockHands || IsCampaignInterfaceOpen) { return true; } - return character.ShouldLockHud(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 59190cd54..b1bca5ab8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -14,7 +14,8 @@ namespace Barotrauma { if (this != Controlled) { - if (GameMain.Client.EndCinematic != null) // Freezes the characters during the ending cinematic + if (GameMain.Client.EndCinematic != null && + GameMain.Client.EndCinematic.Running) // Freezes the characters during the ending cinematic { AnimController.Frozen = true; memState.Clear(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/HUDProgressBar.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/HUDProgressBar.cs index 66978830d..0ebc33f9b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/HUDProgressBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/HUDProgressBar.cs @@ -38,25 +38,43 @@ namespace Barotrauma public Vector2 Size; - private Submarine parentSub; + private readonly Submarine parentSub; + public string Text + { + get; + private set; + } - public HUDProgressBar(Vector2 worldPosition, Submarine parentSubmarine = null) - : this(worldPosition, parentSubmarine, GUI.Style.Red, GUI.Style.Green) + private string textTag; + public string TextTag + { + get { return textTag; } + set + { + if (textTag == value) { return; } + textTag = value; + Text = string.IsNullOrEmpty(textTag) ? string.Empty : TextManager.Get(textTag); + } + } + + public HUDProgressBar(Vector2 worldPosition, string textTag, Submarine parentSubmarine = null) + : this(worldPosition, parentSubmarine, GUI.Style.Red, GUI.Style.Green, textTag) { } - public HUDProgressBar(Vector2 worldPosition, Submarine parentSubmarine, Color emptyColor, Color fullColor) + public HUDProgressBar(Vector2 worldPosition, Submarine parentSubmarine, Color emptyColor, Color fullColor, string textTag) { this.emptyColor = emptyColor; this.fullColor = fullColor; - parentSub = parentSubmarine; - WorldPosition = worldPosition; - Size = new Vector2(100.0f, 20.0f); - FadeTimer = 1.0f; + if (!string.IsNullOrEmpty(textTag)) + { + textTag = textTag; + Text = TextManager.Get(textTag); + } } public void Update(float deltatime) @@ -76,12 +94,21 @@ namespace Barotrauma } pos = cam.WorldToScreen(pos); - + Color color = Color.Lerp(emptyColor, fullColor, progress); GUI.DrawProgressBar(spriteBatch, new Vector2(pos.X, -pos.Y), - Size, progress, - Color.Lerp(emptyColor, fullColor, progress) * a, + Size, progress, + color * a, Color.White * a * 0.8f); + + if (!string.IsNullOrEmpty(Text)) + { + Vector2 textSize = GUI.SmallFont.MeasureString(Text); + Vector2 textPos = new Vector2(pos.X + (Size.X - textSize.X) / 2, pos.Y - textSize.Y * 1.2f); + GUI.DrawString(spriteBatch, textPos - Vector2.One, Text, Color.Black * a, font: GUI.SmallFont); + GUI.DrawString(spriteBatch, textPos, Text, Color.White * a, font: GUI.SmallFont); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 336f42425..6bdbfd16d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -36,7 +36,7 @@ namespace Barotrauma } } - private GUIButton suicideButton; + public GUIButton SuicideButton { get; private set; } // healthbars private GUIProgressBar healthBar; @@ -247,22 +247,6 @@ namespace Barotrauma private GUIFrame healthBarHolder; - private Point healthBarOffset - { - get - { - return new Point(Math.Max(2, GUI.IntScaleCeiling(1.5f)), Math.Min(GUI.IntScaleFloor(18f), 19)); - } - } - - private Point healthBarSize - { - get - { - return new Point(healthBarHolder.Rect.Width - Math.Min(GUI.IntScale(45f), 47), GUI.IntScale(15f)); - } - } - partial void InitProjSpecific(XElement element, Character character) { DisplayedVitality = MaxVitality; @@ -290,29 +274,22 @@ namespace Barotrauma healthBarHolder.RectTransform.NonScaledSize = HUDLayoutSettings.HealthBarArea.Size; healthBarHolder.RectTransform.RelativeOffset = Vector2.Zero; - GUIFrame healthBarBG = new GUIFrame(new RectTransform(Vector2.One, healthBarHolder.RectTransform), style: "CharacterHealthBarBG") - { - CanBeFocused = false - }; - - healthBarShadow = new GUIProgressBar(new RectTransform(healthBarSize, healthBarHolder.RectTransform, Anchor.BottomRight), - barSize: 1.0f, color: Color.Green, style: horizontal ? "CharacterHealthBarSlider" : "GUIProgressBarVertical", showFrame: false) + healthBarShadow = new GUIProgressBar(new RectTransform(Vector2.One, healthBarHolder.RectTransform, Anchor.BottomRight), + barSize: 1.0f, color: Color.Green, style: horizontal ? "CharacterHealthBar" : "GUIProgressBarVertical", showFrame: false) { IsHorizontal = horizontal }; healthBarShadow.Visible = false; healthShadowSize = 1.0f; - healthBar = new GUIProgressBar(new RectTransform(healthBarSize, healthBarHolder.RectTransform, Anchor.BottomRight), - barSize: 1.0f, color: GUI.Style.HealthBarColorHigh, style: horizontal ? "CharacterHealthBarSlider" : "GUIProgressBarVertical", showFrame: false) + healthBar = new GUIProgressBar(new RectTransform(Vector2.One, healthBarHolder.RectTransform, Anchor.BottomRight), + barSize: 1.0f, color: GUI.Style.HealthBarColorHigh, style: horizontal ? "CharacterHealthBar" : "GUIProgressBarVertical") { HoverCursor = CursorState.Hand, Enabled = true, IsHorizontal = horizontal }; - healthBar.RectTransform.AbsoluteOffset = healthBarShadow.RectTransform.AbsoluteOffset = healthBarOffset; - healthInterfaceFrame = new GUIFrame(new RectTransform(new Vector2(0.7f, 0.55f), GUI.Canvas, anchor: Anchor.Center, scaleBasis: ScaleBasis.Smallest), style: "ItemUI"); var healthInterfaceLayout = new GUILayoutGroup(new RectTransform(Vector2.One / 1.05f, healthInterfaceFrame.RectTransform, anchor: Anchor.Center), true); @@ -519,7 +496,7 @@ namespace Barotrauma UpdateAlignment(); - suicideButton = new GUIButton(new RectTransform(new Vector2(0.1f, 0.02f), GUI.Canvas, Anchor.TopCenter) + SuicideButton = new GUIButton(new RectTransform(new Vector2(0.1f, 0.02f), GUI.Canvas, Anchor.TopCenter) { MinSize = new Point(150, 20), RelativeOffset = new Vector2(0.0f, 0.01f) }, @@ -546,7 +523,7 @@ namespace Barotrauma return true; } }; - suicideButton.TextBlock.AutoScaleHorizontal = true; + SuicideButton.TextBlock.AutoScaleHorizontal = true; if (element != null) { @@ -591,9 +568,6 @@ namespace Barotrauma healthBarHolder.RectTransform.NonScaledSize = HUDLayoutSettings.HealthBarArea.Size; healthBarHolder.RectTransform.RelativeOffset = Vector2.Zero; - healthBar.RectTransform.NonScaledSize = healthBarShadow.RectTransform.NonScaledSize = healthBarSize; - healthBar.RectTransform.AbsoluteOffset = healthBarShadow.RectTransform.AbsoluteOffset = healthBarOffset; - switch (alignment) { case Alignment.Left: @@ -943,23 +917,7 @@ namespace Barotrauma healthBar.State = GUIComponent.ComponentState.None; } - suicideButton.Visible = Character == Character.Controlled && !Character.IsDead && Character.IsIncapacitated; - - if (GameMain.GameSession?.Campaign is { } campaign) - { - RectTransform endRoundButton = campaign?.EndRoundButton.RectTransform; - if (endRoundButton != null) - { - if (suicideButton.Visible) - { - endRoundButton.ScreenSpaceOffset = new Point(0, suicideButton.Rect.Height); - } - else if (endRoundButton.ScreenSpaceOffset != Point.Zero) - { - endRoundButton.ScreenSpaceOffset = Point.Zero; - } - } - } + SuicideButton.Visible = Character == Character.Controlled && !Character.IsDead && Character.IsIncapacitated; cprButton.Visible = Character == Character.Controlled?.SelectedCharacter @@ -992,9 +950,9 @@ namespace Barotrauma { healthBarHolder.AddToGUIUpdateList(); } - if (suicideButton.Visible && Character == Character.Controlled) + if (SuicideButton.Visible && Character == Character.Controlled) { - suicideButton.AddToGUIUpdateList(); + SuicideButton.AddToGUIUpdateList(); } if (cprButton != null && cprButton.Visible) { @@ -1907,6 +1865,8 @@ namespace Barotrauma private readonly List> newAfflictions = new List>(); private readonly List> newLimbAfflictions = new List>(); + private readonly List> newPeriodicEffects = new List>(); + public void ClientRead(IReadMessage inc) { newAfflictions.Clear(); @@ -1920,9 +1880,20 @@ namespace Barotrauma DebugConsole.ThrowError("Error while reading character health data: affliction with the uint ID " + afflictionID + " not found."); //read the 8 bytes for affliction strength anyway to prevent messing up reading rest of the message _ = inc.ReadRangedSingle(0.0f, 100.0f, 8); + int _periodicAfflictionCount = inc.ReadByte(); + for (int j = 0; j < _periodicAfflictionCount; j++) + { + _ = inc.ReadByte(); + } continue; } float afflictionStrength = inc.ReadRangedSingle(0.0f, afflictionPrefab.MaxStrength, 8); + int periodicAfflictionCount = inc.ReadByte(); + for (int j = 0; j < periodicAfflictionCount; j++) + { + float periodicAfflictionTimer = inc.ReadRangedSingle(afflictionPrefab.PeriodicEffects[j].MinInterval, afflictionPrefab.PeriodicEffects[j].MaxInterval, 8); + newPeriodicEffects.Add(new Pair(afflictionPrefab.PeriodicEffects[j], periodicAfflictionTimer)); + } newAfflictions.Add(new Pair(afflictionPrefab, afflictionStrength)); } @@ -1940,12 +1911,26 @@ namespace Barotrauma Affliction existingAffliction = afflictions.Find(a => a.Prefab == newAffliction.First); if (existingAffliction == null) { - afflictions.Add(newAffliction.First.Instantiate(newAffliction.Second)); + existingAffliction = newAffliction.First.Instantiate(newAffliction.Second); + afflictions.Add(existingAffliction); } - else + existingAffliction.SetStrength(newAffliction.Second); + if (existingAffliction == stunAffliction) { - existingAffliction.Strength = newAffliction.Second; - if (existingAffliction == stunAffliction) Character.SetStun(existingAffliction.Strength, true, true); + Character.SetStun(existingAffliction.Strength, true, true); + } + foreach (var periodicEffect in newPeriodicEffects) + { + if (!existingAffliction.Prefab.PeriodicEffects.Contains(periodicEffect.First)) { continue; } + //timer has wrapped around, apply the effect + if (periodicEffect.Second - existingAffliction.PeriodicEffectTimers[periodicEffect.First] > periodicEffect.First.MinInterval / 2) + { + existingAffliction.PeriodicEffectTimers[periodicEffect.First] = periodicEffect.Second; + foreach (StatusEffect effect in periodicEffect.First.StatusEffects) + { + existingAffliction.ApplyStatusEffect(effect, deltaTime: 1.0f, this, targetLimb: null); + } + } } } @@ -1961,9 +1946,20 @@ namespace Barotrauma DebugConsole.ThrowError("Error while reading character health data: affliction with the uint ID " + afflictionID + " not found."); //read the 8 bytes for affliction strength anyway to prevent messing up reading rest of the message _ = inc.ReadRangedSingle(0.0f, 100.0f, 8); + int _periodicAfflictionCount = inc.ReadByte(); + for (int j = 0; j < _periodicAfflictionCount; j++) + { + _ = inc.ReadByte(); + } continue; } float afflictionStrength = inc.ReadRangedSingle(0.0f, afflictionPrefab.MaxStrength, 8); + int periodicAfflictionCount = inc.ReadByte(); + for (int j = 0; j < periodicAfflictionCount; j++) + { + float periodicAfflictionTimer = inc.ReadRangedSingle(afflictionPrefab.PeriodicEffects[j].MinInterval, afflictionPrefab.PeriodicEffects[j].MaxInterval, 8); + newPeriodicEffects.Add(new Pair(afflictionPrefab.PeriodicEffects[j], periodicAfflictionTimer)); + } newLimbAfflictions.Add(new Triplet(limbHealths[limbIndex], afflictionPrefab, afflictionStrength)); } @@ -1980,15 +1976,28 @@ namespace Barotrauma foreach (Triplet newAffliction in newLimbAfflictions) { - if (newAffliction.First != limbHealth) continue; + if (newAffliction.First != limbHealth) { continue; } Affliction existingAffliction = limbHealth.Afflictions.Find(a => a.Prefab == newAffliction.Second); if (existingAffliction == null) { - limbHealth.Afflictions.Add(newAffliction.Second.Instantiate(newAffliction.Third)); + existingAffliction = newAffliction.Second.Instantiate(newAffliction.Third); + limbHealth.Afflictions.Add(existingAffliction); } - else + existingAffliction.SetStrength(newAffliction.Third); + + foreach (var periodicEffect in newPeriodicEffects) { - existingAffliction.Strength = newAffliction.Third; + if (!existingAffliction.Prefab.PeriodicEffects.Contains(periodicEffect.First)) { continue; } + //timer has wrapped around, apply the effect + if (periodicEffect.Second - existingAffliction.PeriodicEffectTimers[periodicEffect.First] > periodicEffect.First.MinInterval / 2) + { + existingAffliction.PeriodicEffectTimers[periodicEffect.First] = periodicEffect.Second; + foreach (StatusEffect effect in periodicEffect.First.StatusEffects) + { + Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == limbHealths.IndexOf(newAffliction.First)); + existingAffliction.ApplyStatusEffect(effect, deltaTime: 1.0f, this, targetLimb: targetLimb); + } + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 30f07e97d..c258b3591 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -446,7 +446,7 @@ namespace Barotrauma { if (affliction is AfflictionBleeding) { - bleedingDamage += affliction.GetVitalityDecrease(character.CharacterHealth); + bleedingDamage += affliction.GetVitalityDecrease(null); } } } @@ -455,7 +455,7 @@ namespace Barotrauma { if (affliction.Prefab.AfflictionType == "damage") { - damage += affliction.GetVitalityDecrease(character.CharacterHealth); + damage += affliction.GetVitalityDecrease(null); } } float damageMultiplier = 1; @@ -488,7 +488,7 @@ namespace Barotrauma } // spawn damage particles - float damageParticleAmount = Math.Min(damage / 10, 1.0f) * damageMultiplier; + float damageParticleAmount = Math.Min(damage / 5, 1.0f) * damageMultiplier; if (damageParticleAmount > 0.001f) { foreach (ParticleEmitter emitter in character.DamageEmitters) @@ -510,11 +510,6 @@ namespace Barotrauma if (!inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } emitter.Emit(1.0f, WorldPosition, character.CurrentHull, sizeMultiplier: bloodParticleSize, amountMultiplier: bloodParticleAmount); } - - if (bloodParticleAmount > 0 && character.CurrentHull != null && !string.IsNullOrEmpty(character.BloodDecalName)) - { - character.CurrentHull.AddDecal(character.BloodDecalName, WorldPosition, MathHelper.Clamp(bloodParticleSize, 0.5f, 1.0f)); - } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index a64888009..20fa2e1ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -1086,17 +1086,6 @@ namespace Barotrauma AssignRelayToServer("water|editwater", false); AssignRelayToServer("fire|editfire", false); - commands.Add(new Command("togglecampaignteleport", "togglecampaignteleport: Toggle on/off teleportation between campaign locations by double clicking on the campaign map.", (string[] args) => - { - if (GameMain.GameSession?.Campaign == null) - { - ThrowError("No campaign active."); - return; - } - GameMain.GameSession.Map.AllowDebugTeleport = !GameMain.GameSession.Map.AllowDebugTeleport; - NewMessage((GameMain.GameSession.Map.AllowDebugTeleport ? "Enabled" : "Disabled") + " teleportation on the campaign map.", Color.White); - }, isCheat: true)); - commands.Add(new Command("mute", "mute [name]: Prevent the client from speaking to anyone through the voice chat. Using this command requires a permission from the server host.", null, () => @@ -1136,7 +1125,7 @@ namespace Barotrauma { foreach (var ingredient in fabricationRecipe.RequiredItems) { - int? ingredientPrice = ingredient.ItemPrefab.GetMinPrice(); + int? ingredientPrice = ingredient.ItemPrefabs.Min(ip => ip.GetMinPrice()); if (ingredientPrice.HasValue) { if (!fabricationCost.HasValue) { fabricationCost = 0; } @@ -1163,14 +1152,14 @@ namespace Barotrauma if (fabricationRecipe != null) { - var ingredient = fabricationRecipe.RequiredItems.Find(r => r.ItemPrefab == targetItem); + var ingredient = fabricationRecipe.RequiredItems.Find(r => r.ItemPrefabs.Contains(targetItem)); if (ingredient == null) { NewMessage("Deconstructing \"" + itemPrefab.Name + "\" produces \"" + deconstructItem.ItemIdentifier + "\", which isn't required in the fabrication recipe of the item.", Color.Red); } else if (ingredient.UseCondition && ingredient.MinCondition < deconstructItem.OutCondition) { - NewMessage($"Deconstructing \"{itemPrefab.Name}\" produces more \"{deconstructItem.ItemIdentifier}\", than what's required to fabricate the item (required: {ingredient.ItemPrefab.Name} {(int)(ingredient.MinCondition * 100)}%, output: {deconstructItem.ItemIdentifier} {(int)(deconstructItem.OutCondition * 100)}%)", Color.Red); + NewMessage($"Deconstructing \"{itemPrefab.Name}\" produces more \"{deconstructItem.ItemIdentifier}\", than what's required to fabricate the item (required: {targetItem.Name} {(int)(ingredient.MinCondition * 100)}%, output: {deconstructItem.ItemIdentifier} {(int)(deconstructItem.OutCondition * 100)}%)", Color.Red); } } } @@ -1389,6 +1378,39 @@ namespace Barotrauma ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); })); #if DEBUG + commands.Add(new Command("setplanthealth", "setplanthealth [value]: Sets the health of the selected plant in sub editor.", (string[] args) => + { + if (1 > args.Length || Screen.Selected != GameMain.SubEditorScreen) { return; } + + string arg = args[0]; + + if (!float.TryParse(arg, out float value)) + { + ThrowError($"{arg} is not a valid value."); + return; + } + + foreach (MapEntity me in MapEntity.SelectedList) + { + if (me is Item it) + { + if (it.GetComponent() is { } planter) + { + foreach (Growable seed in planter.GrowableSeeds.Where(s => s != null)) + { + NewMessage($"Set the health of {seed.Name} to {value} (from {seed.Health})"); + seed.Health = value; + } + } + else if (it.GetComponent() is { } seed) + { + NewMessage($"Set the health of {seed.Name} to {value} (from {seed.Health})"); + seed.Health = value; + } + } + } + })); + commands.Add(new Command("printreceivertransfers", "", (string[] args) => { GameMain.Client.PrintReceiverTransters(); @@ -2012,7 +2034,7 @@ namespace Barotrauma return; } - GameMain.Config.SelectCorePackage(GameMain.Config.SelectedContentPackages.First(cp => cp.CorePackage), true); + GameMain.Config.SelectCorePackage(GameMain.Config.CurrentCorePackage, true); })); commands.Add(new Command("ingamemodswap", "", (string[] args) => @@ -2041,7 +2063,7 @@ namespace Barotrauma } ShowQuestionPrompt("Permission to grant to client " + args[0] + "?", (perm) => { - GameMain.Client?.SendConsoleCommand("giveperm " + args[0] + " " + perm); + GameMain.Client?.SendConsoleCommand("giveperm \"" + args[0] + "\" " + perm); }, args, 1); } ); @@ -2060,7 +2082,7 @@ namespace Barotrauma ShowQuestionPrompt("Permission to revoke from client " + args[0] + "?", (perm) => { - GameMain.Client?.SendConsoleCommand("revokeperm " + args[0] + " " + perm); + GameMain.Client?.SendConsoleCommand("revokeperm \"" + args[0] + "\" " + perm); }, args, 1); } ); @@ -2078,7 +2100,7 @@ namespace Barotrauma } ShowQuestionPrompt("Rank to grant to client " + args[0] + "?", (rank) => { - GameMain.Client?.SendConsoleCommand("giverank " + args[0] + " " + rank); + GameMain.Client?.SendConsoleCommand("giverank \"" + args[0] + "\" " + rank); }, args, 1); } ); @@ -2091,7 +2113,7 @@ namespace Barotrauma ShowQuestionPrompt("Console command permissions to grant to client " + args[0] + "? You may enter multiple commands separated with a space or use \"all\" to give the permission to use all console commands.", (commandNames) => { - GameMain.Client?.SendConsoleCommand("givecommandperm " + args[0] + " " + commandNames); + GameMain.Client?.SendConsoleCommand("givecommandperm \"" + args[0] + "\" " + commandNames); }, args, 1); } ); @@ -2104,7 +2126,7 @@ namespace Barotrauma ShowQuestionPrompt("Console command permissions to revoke from client " + args[0] + "? You may enter multiple commands separated with a space or use \"all\" to revoke the permission to use any console commands.", (commandNames) => { - GameMain.Client?.SendConsoleCommand("revokecommandperm " + args[0] + " " + commandNames); + GameMain.Client?.SendConsoleCommand("revokecommandperm \"" + args[0] + "\" " + commandNames); }, args, 1); } ); @@ -2505,7 +2527,7 @@ namespace Barotrauma } }, isCheat: true)); - commands.Add(new Command("spawnsub", "spawnsub [subname]: Spawn a submarine at the position of the cursor", (string[] args) => + commands.Add(new Command("spawnsub", "spawnsub [subname] [is thalamus]: Spawn a submarine at the position of the cursor", (string[] args) => { if (GameMain.NetworkMember != null) { @@ -2528,6 +2550,25 @@ namespace Barotrauma { Submarine spawnedSub = Submarine.Load(subInfo, false); spawnedSub.SetPosition(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition)); + if (subInfo.Type == SubmarineType.Wreck) + { + spawnedSub.MakeWreck(); + if (args.Length > 1 && bool.TryParse(args[1], out bool isThalamus)) + { + if (isThalamus) + { + spawnedSub.CreateWreckAI(); + } + else + { + spawnedSub.DisableWreckAI(); + } + } + else + { + spawnedSub.DisableWreckAI(); + } + } } } catch (Exception e) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Decals/Decal.cs b/Barotrauma/BarotraumaClient/ClientSource/Decals/Decal.cs new file mode 100644 index 000000000..8025acb22 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Decals/Decal.cs @@ -0,0 +1,33 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Linq; + +namespace Barotrauma +{ + partial class Decal + { + + public void Draw(SpriteBatch spriteBatch, Hull hull, float depth) + { + Vector2 drawPos = position + hull.Rect.Location.ToVector2(); + if (hull.Submarine != null) { drawPos += hull.Submarine.DrawPosition; } + drawPos.Y = -drawPos.Y; + + spriteBatch.Draw(Sprite.Texture, drawPos, clippedSourceRect, Color * GetAlpha(), 0, Vector2.Zero, Scale, SpriteEffects.None, depth); + + if (GameMain.DebugDraw && affectedSections != null && affectedSections.Count > 0) + { + Vector2 drawOffset = hull.Submarine == null ? Vector2.Zero : hull.Submarine.DrawPosition; + Point sectionSize = affectedSections.First().Rect.Size; + Rectangle drawPositionRect = new Rectangle((int)(drawOffset.X + hull.Rect.X), (int)(drawOffset.Y + hull.Rect.Y), sectionSize.X, sectionSize.Y); + + foreach (var section in affectedSections) + { + // Draw colors + GUI.DrawRectangle(spriteBatch, new Vector2(drawPositionRect.X + section.Rect.X, -(drawPositionRect.Y + section.Rect.Y)), new Vector2(sectionSize.X, sectionSize.Y), Color.Red, false, 0.0f, (int)Math.Max(1.5f / Screen.Selected.Cam.Zoom, 1.0f)); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index 6d943d132..9c2c4795a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -67,7 +67,7 @@ namespace Barotrauma GUIListBox conversationList = lastMessageBox.FindChild("conversationlist", true) as GUIListBox; Debug.Assert(conversationList != null); - + // gray out the last text block if (conversationList.Content.Children.LastOrDefault() is GUILayoutGroup lastElement) { @@ -132,17 +132,19 @@ namespace Barotrauma }; shadow.SetAsFirstChild(); - void RecalculateLastMessage(GUIListBox conversationList, bool append) + static void RecalculateLastMessage(GUIListBox conversationList, bool append) { if (conversationList.Content.Children.LastOrDefault() is GUILayoutGroup lastElement) { GUILayoutGroup textLayout = lastElement.GetChild(); - if (lastElement.Rect.Size.Y < textLayout.Rect.Size.Y && !append) - { - lastElement.RectTransform.MinSize = textLayout.Rect.Size; - } + if (textLayout != null) { + if (lastElement.Rect.Size.Y < textLayout.Rect.Size.Y && !append) + { + lastElement.RectTransform.MinSize = textLayout.Rect.Size; + } + int textHeight = textLayout.Children.Sum(c => c.Rect.Height); textLayout.RectTransform.MaxSize = new Point(lastElement.RectTransform.MaxSize.X, textHeight); textLayout.Recalculate(); @@ -323,7 +325,10 @@ namespace Barotrauma } textContent.RectTransform.MinSize = new Point(0, textContent.Children.Sum(c => c.Rect.Height + textContent.AbsoluteSpacing) + GUI.IntScale(16)); - // content.RectTransform.MinSize = new Point(0, textContent.Rect.Height); + + // Recalculate the text size as it is scaled up and no longer matching the text height due to the textContent's minSize increasing + textBlock.CalculateHeightFromText(); + //content.RectTransform.MinSize = new Point(0, textContent.Rect.Height); return buttons; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index f23070044..cbd48fd79 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -17,10 +17,11 @@ namespace Barotrauma private float intensityGraphUpdateInterval; private float lastIntensityUpdate; - private Event? pinnedEvent; private Vector2 pinnedPosition = new Vector2(256, 128); private bool isDragging; + public Event? PinnedEvent { get; set; } + public void DebugDraw(SpriteBatch spriteBatch) { foreach (Event ev in activeEvents) @@ -104,12 +105,21 @@ namespace Barotrauma GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), "New event (ID " + eventSet.DebugIdentifier + ") after: ", Color.Orange * 0.8f, null, 0, GUI.SmallFont); y += 12; - if ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) && - roundDuration < eventSet.MinMissionTime) + if (eventSet.PerWreck) + { + GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), " submarine near the wreck", Color.Orange * 0.8f, null, 0, GUI.SmallFont); + y += 12; + } + if (eventSet.PerRuin) + { + GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), " submarine near the ruins", Color.Orange * 0.8f, null, 0, GUI.SmallFont); + y += 12; + } + if (roundDuration < eventSet.MinMissionTime) { GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), " " + (int) (eventSet.MinDistanceTraveled * 100.0f) + "% travelled (current: " + (int) (distanceTraveled * 100.0f) + " %)", - Color.Orange * 0.8f, null, 0, GUI.SmallFont); + ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) ? Color.Lerp(GUI.Style.Yellow, GUI.Style.Red, eventSet.MinDistanceTraveled - distanceTraveled) : GUI.Style.Green) * 0.8f, null, 0, GUI.SmallFont); y += 12; } @@ -125,7 +135,7 @@ namespace Barotrauma { GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), " " + (int) (eventSet.MinMissionTime - roundDuration) + " s", - Color.Orange * 0.8f, null, 0, GUI.SmallFont); + Color.Lerp(GUI.Style.Yellow, GUI.Style.Red, (eventSet.MinMissionTime - roundDuration)), null, 0, GUI.SmallFont); } y += 15; @@ -143,25 +153,25 @@ namespace Barotrauma Rectangle outlineRect = new Rectangle(rect.Location, rect.Size); outlineRect.Inflate(4, 4); - if (pinnedEvent == ev) { GUI.DrawRectangle(spriteBatch, outlineRect, Color.White); } + if (PinnedEvent == ev) { GUI.DrawRectangle(spriteBatch, outlineRect, Color.White); } if (rect.Contains(PlayerInput.MousePosition)) { GUI.MouseCursor = CursorState.Hand; GUI.DrawRectangle(spriteBatch, outlineRect, Color.White); - if (ev != pinnedEvent) + if (ev != PinnedEvent) { DrawEvent(spriteBatch, ev, rect); } else if (PlayerInput.SecondaryMouseButtonHeld() || PlayerInput.SecondaryMouseButtonDown()) { - pinnedEvent = null; + PinnedEvent = null; } if (PlayerInput.PrimaryMouseButtonHeld() || PlayerInput.PrimaryMouseButtonDown()) { - pinnedEvent = ev; + PinnedEvent = ev; } } @@ -171,9 +181,9 @@ namespace Barotrauma public void DrawPinnedEvent(SpriteBatch spriteBatch) { - if (pinnedEvent != null) + if (PinnedEvent != null) { - Rectangle rect = DrawEvent(spriteBatch, pinnedEvent, null); + Rectangle rect = DrawEvent(spriteBatch, PinnedEvent, null); if (rect != Rectangle.Empty) { @@ -187,7 +197,7 @@ namespace Barotrauma if (PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.SecondaryMouseButtonHeld()) { - pinnedEvent = null; + PinnedEvent = null; isDragging = false; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index 6724f3ab0..31a5b9add 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -2,11 +2,9 @@ using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; -using Microsoft.Xna.Framework.Input; namespace Barotrauma { @@ -14,7 +12,7 @@ namespace Barotrauma { public const string RadioChatString = "r; "; - private GUIListBox chatBox; + private readonly GUIListBox chatBox; private Point screenResolution; public readonly ChatManager ChatManager = new ChatManager(); @@ -37,10 +35,17 @@ namespace Barotrauma private float prevUIScale; + private readonly GUIFrame channelSettingsFrame; + private readonly GUITextBox channelText; + private readonly GUILayoutGroup channelPickerContent; + private readonly GUIButton memButton; + private WifiComponent prevRadio; + + private bool channelMemPending; + //individual message texts that pop up when the chatbox is hidden const float PopupMessageDuration = 5.0f; - private float popupMessageTimer; - private Queue popupMessages = new Queue(); + private readonly List popupMessages = new List(); public GUITextBox.OnEnterHandler OnEnterMessage { @@ -67,9 +72,9 @@ namespace Barotrauma } } - private GUIButton showNewMessagesButton; + private readonly GUIButton showNewMessagesButton; - private GUIFrame hideableElements; + private readonly GUIFrame hideableElements; public const int ToggleButtonWidthRaw = 30; private int popupMessageOffset; @@ -88,6 +93,133 @@ namespace Barotrauma var chatBoxHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.875f), hideableElements.RectTransform), style: "ChatBox"); chatBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.95f), chatBoxHolder.RectTransform, Anchor.CenterRight), style: null); + // channel settings ----------------------------------------------------------------------------- + + channelSettingsFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), chatBoxHolder.RectTransform, Anchor.TopCenter, Pivot.BottomCenter) { MinSize = new Point(0, 25) }, style: "GUIFrameBottom"); + var channelSettingsContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), channelSettingsFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + RelativeSpacing = 0.01f + }; + + var buttonLeft = new GUIButton(new RectTransform(new Vector2(0.1f, 0.8f), channelSettingsContent.RectTransform), style: "DeviceButton") + { + OnClicked = (btn, userdata) => + { + if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) + { + SetChannel(radio.Channel - 1, setText: true); + GUI.PlayUISound(GUISoundType.PopupMenu); + } + return true; + } + }; + var arrowIcon = new GUIImage(new RectTransform(new Vector2(0.4f), buttonLeft.RectTransform, Anchor.Center), style: "GUIButtonHorizontalArrow", scaleToFit: true) + { + Color = new Color(51, 59, 46), + SpriteEffects = Microsoft.Xna.Framework.Graphics.SpriteEffects.FlipHorizontally + }; + arrowIcon.HoverColor = arrowIcon.PressedColor = arrowIcon.PressedColor = arrowIcon.Color; + + channelText = new GUITextBox(new RectTransform(new Vector2(0.25f, 0.8f), channelSettingsContent.RectTransform), style: "DigitalFrameLight", textAlignment: Alignment.Center, font: GUI.DigitalFont) + { + textFilterFunction = text => + { + var str = new string(text.Where(c => char.IsNumber(c)).ToArray()); + if (str.Length > 4) { str = str.Substring(0, 4); } + return str; + }, + OnEnterPressed = (tb, text) => + { + tb.Deselect(); + return true; + } + }; + Vector2 textSize = channelText.Font.MeasureString("0000"); + channelText.TextBlock.ToolTip = TextManager.Get("currentradiochannel"); + channelText.TextBlock.TextScale = Math.Min(channelText.Rect.Height / textSize.Y * 0.9f, 1.0f); + channelText.OnDeselected += (sender, key) => + { + int.TryParse(channelText.Text, out int newChannel); + SetChannel(newChannel, setText: true); + GUI.PlayUISound(GUISoundType.PopupMenu); + }; + + var buttonRight = new GUIButton(new RectTransform(new Vector2(0.1f, 0.8f), channelSettingsContent.RectTransform), style: "DeviceButton") + { + OnClicked = (btn, userdata) => + { + if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) + { + SetChannel(radio.Channel + 1, setText: true); + GUI.PlayUISound(GUISoundType.PopupMenu); + } + return true; + } + }; + arrowIcon = new GUIImage(new RectTransform(new Vector2(0.4f), buttonRight.RectTransform, Anchor.Center), style: "GUIButtonHorizontalArrow", scaleToFit: true) + { + Color = new Color(51, 59, 46) + }; + arrowIcon.HoverColor = arrowIcon.PressedColor = arrowIcon.PressedColor = arrowIcon.Color; + + var channelPicker = new GUIFrame(new RectTransform(new Vector2(0.4f, 0.6f), channelSettingsContent.RectTransform), "InnerFrame"); + channelPickerContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), channelPicker.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true + }; + for (int i = 0; i < 10; i++) + { + new GUIButton(new RectTransform(new Vector2(0.1f, 1.0f), channelPickerContent.RectTransform), i.ToString(), style: "GUITextBlock") + { + TextColor = new Color(51, 59, 46), + SelectedTextColor = GUI.Style.Green, + UserData = i, + OnClicked = (btn, userdata) => + { + if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) + { + int index = (int)userdata; + if (channelMemPending) + { + int.TryParse(channelText.Text, out int newChannel); + radio.SetChannelMemory(index, newChannel); + btn.ToolTip = TextManager.GetWithVariables("radiochannelpreset", + new string[] { "[index]", "[channel]" }, + new string[] { index.ToString(), radio.GetChannelMemory(index).ToString() }); + channelMemPending = false; + channelPickerContent.Children.First().CanBeFocused = true; + memButton.Enabled = true; + channelPickerContent.Flash(GUI.Style.Green); + channelText.Flash(GUI.Style.Green); + } + SetChannel(radio.GetChannelMemory(index), setText: true); + GUI.PlayUISound(GUISoundType.PopupMenu); + } + return true; + } + }; + } + + memButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.9f), channelSettingsContent.RectTransform), style: "DeviceButton", text: TextManager.Get("saveradiochannelbutton")) + { + ToolTip = TextManager.Get("saveradiochannelbuttontooltip"), + OnClicked = (btn, userdata) => + { + channelMemPending = true; + //don't allow storing a value in the first preset + channelPickerContent.Children.First().CanBeFocused = false; + foreach (GUIComponent channelButton in channelPickerContent.Children) + { + channelButton.Selected = false; + } + btn.Enabled = false; + return true; + } + }; + + // --------------------------------------------------------------------------------------------- + InputBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.125f), hideableElements.RectTransform, Anchor.BottomLeft), style: "ChatTextBox") { @@ -148,7 +280,7 @@ namespace Barotrauma } } - Color textColor = textBox.Color; + Color textColor; switch (command) { case "r": @@ -235,7 +367,7 @@ namespace Barotrauma }, Text = senderName }; - + senderNameBlock.RectTransform.NonScaledSize = senderNameBlock.TextBlock.TextSize.ToPoint(); senderNameBlock.TextBlock.OverrideTextColor(senderColor); if (senderNameBlock.UserData != null) @@ -244,7 +376,7 @@ namespace Barotrauma } } - var msgText =new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), msgHolder.RectTransform) + var msgText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), msgHolder.RectTransform) { AbsoluteOffset = new Point((int)(10 * GUI.Scale), senderNameTimestamp == null ? 0 : senderNameTimestamp.Rect.Height) }, displayedText, textColor: message.Color, font: GUI.SmallFont, textAlignment: Alignment.TopLeft, style: null, wrap: true, color: ((chatBox.Content.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f) @@ -296,7 +428,7 @@ namespace Barotrauma { var popupMsg = new GUIFrame(new RectTransform(Vector2.One, GUIFrame.RectTransform), style: "GUIToolTip") { - Visible = false, + UserData = 0.0f, CanBeFocused = false }; var content = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), popupMsg.RectTransform, Anchor.Center)); @@ -322,10 +454,10 @@ namespace Barotrauma popupMsg.RectTransform.Resize(new Point((int)(textWidth / content.RectTransform.RelativeSize.X) , (int)((senderTextSize.Y + msgSize.Y) / content.RectTransform.RelativeSize.Y)), resizeChildren: true); popupMsg.RectTransform.IsFixedSize = true; content.Recalculate(); - popupMessages.Enqueue(popupMsg); + popupMessages.Add(popupMsg); } - if ((prevSize == 1.0f && chatBox.BarScroll == 0.0f) || (prevSize < 1.0f && chatBox.BarScroll == 1.0f)) chatBox.BarScroll = 1.0f; + if ((prevSize == 1.0f && chatBox.BarScroll == 0.0f) || (prevSize < 1.0f && chatBox.BarScroll == 1.0f)) { chatBox.BarScroll = 1.0f; } GUISoundType soundType = GUISoundType.ChatMessage; if (message.Type == ChatMessageType.Radio) @@ -372,7 +504,7 @@ namespace Barotrauma GUIFrame.RectTransform.NonScaledSize -= new Point(toggleButtonWidth, 0); GUIFrame.RectTransform.AbsoluteOffset += new Point(toggleButtonWidth, 0); - popupMessageOffset = GameMain.GameSession.CrewManager.ReportButtonFrame.Rect.Width + GUIFrame.Rect.Width + (int)(20 * GUI.Scale); + popupMessageOffset = GameMain.GameSession.CrewManager.ReportButtonFrame.Rect.Width + GUIFrame.Rect.Width + (int)(35 * GUI.Scale); } public void Update(float deltaTime) @@ -394,22 +526,93 @@ namespace Barotrauma ToggleButton.RectTransform.AbsoluteOffset = new Point(GUIFrame.Rect.Right, GUIFrame.Rect.Y + HUDLayoutSettings.ChatBoxArea.Height - ToggleButton.Rect.Height); } + if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) + { + if (prevRadio != radio) + { + foreach (GUIComponent presetButton in channelPickerContent.Children) + { + int index = (int)presetButton.UserData; + presetButton.ToolTip = TextManager.GetWithVariables("radiochannelpreset", + new string[] { "[index]", "[channel]" }, + new string[] { index.ToString(), radio.GetChannelMemory(index).ToString() }); + } + SetChannel(radio.Channel, setText: true); + prevRadio = radio; + } + if (channelMemPending) + { + if (channelPickerContent.FlashTimer <= 0) + { + channelPickerContent.Flash(GUI.Style.Green, flashRectInflate: new Vector2(GUI.Scale * 5.0f)); + } + if (PlayerInput.PrimaryMouseButtonClicked() && !GUI.IsMouseOn(channelPickerContent)) + { + channelPickerContent.Children.First().CanBeFocused = true; + channelMemPending = false; + memButton.Enabled = true; + SetChannel(radio.Channel, setText: true); + } + } + channelSettingsFrame.Visible = true; + } + else + { + channelSettingsFrame.Visible = false; + channelPickerContent.Children.First().CanBeFocused = true; + channelMemPending = false; + memButton.Enabled = true; + } + if (ToggleOpen) { openState += deltaTime * 5.0f; //delete all popup messages when the chatbox is open - while (popupMessages.Count > 0) + foreach (var popupMsg in popupMessages) { - var popupMsg = popupMessages.Dequeue(); popupMsg.Parent.RemoveChild(popupMsg); } + popupMessages.Clear(); } else { openState -= deltaTime * 5.0f; + int yOffset = 0; + foreach (var popupMsg in popupMessages) + { + float msgTimer = (float)popupMsg.UserData; + + int targetYOffset = (int)MathHelper.Lerp(popupMsg.RectTransform.ScreenSpaceOffset.Y, yOffset, deltaTime * 10.0f); + + if (popupMsg == popupMessages.First()) + { + popupMsg.UserData = msgTimer + deltaTime * Math.Max(popupMessages.Count / 2, 1); + if (msgTimer > PopupMessageDuration) + { + //move the message out of the screen and delete it + popupMsg.RectTransform.ScreenSpaceOffset = + new Point((int)MathHelper.SmoothStep(popupMessageOffset, 10, (msgTimer - PopupMessageDuration) * 5.0f), targetYOffset); + if (msgTimer > PopupMessageDuration + 0.2f) + { + popupMsg.Parent?.RemoveChild(popupMsg); + } + } + } + if (msgTimer < PopupMessageDuration) + { + if (popupMsg != popupMessages.First()) { popupMsg.UserData = Math.Min(msgTimer + deltaTime, 1.0f); } + //move the message on the screen + popupMsg.RectTransform.ScreenSpaceOffset = new Point( + (int)MathHelper.SmoothStep(0, popupMessageOffset, msgTimer * 5.0f), targetYOffset); + } + yOffset += popupMsg.Rect.Height + GUI.IntScale(10); + } + + popupMessages.RemoveAll(p => p.Parent == null); + //make the first popup message visible - var popupMsg = popupMessages.Count > 0 ? popupMessages.Peek() : null; + /*var popupMsg = popupMessages.Count > 0 ? popupMessages.Peek() : null; if (popupMsg != null) { popupMsg.Visible = true; @@ -433,7 +636,7 @@ namespace Barotrauma popupMsg.RectTransform.ScreenSpaceOffset = new Point( (int)MathHelper.SmoothStep(0, popupMessageOffset, popupMessageTimer * 5.0f), 0); } - } + }*/ } openState = MathHelper.Clamp(openState, 0.0f, 1.0f); int hiddenBoxOffset = -(GUIFrame.Rect.Width); @@ -441,5 +644,27 @@ namespace Barotrauma new Point((int)MathHelper.SmoothStep(hiddenBoxOffset, 0, openState), 0); hideableElements.Visible = openState > 0.0f; } + + private void SetChannel(int channel, bool setText) + { + if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) + { + radio.Channel = channel; + if (setText) + { + string text = radio.Channel.ToString().PadLeft(4, '0'); + if (channelText.Text != text) { channelText.Text = text; } + + } + if (!channelMemPending) + { + foreach (GUIComponent channelButton in channelPickerContent.Children) + { + int buttonIndex = (int)channelButton.UserData; + channelButton.Selected = radio.GetChannelMemory(buttonIndex) == channel; + } + } + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index a1201fd35..c77f2b94e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -323,7 +323,7 @@ namespace Barotrauma if (Screen.Selected == GameMain.GameScreen) { - yOffset = -HUDLayoutSettings.ChatBoxArea.Height; + yOffset = (int)(-HUDLayoutSettings.ChatBoxArea.Height * 1.2f); watermarkRect.Y += yOffset; } @@ -609,7 +609,7 @@ namespace Barotrauma if (Character.Controlled?.Inventory != null) { - if (!Character.Controlled.LockHands && Character.Controlled.Stun < 0.1f && !Character.Controlled.IsDead) + if (Character.Controlled.Stun < 0.1f && !Character.Controlled.IsDead) { Inventory.DrawFront(spriteBatch); } @@ -1267,7 +1267,7 @@ namespace Barotrauma Vector2 diff = worldPosition - cam.WorldViewCenter; float dist = diff.Length(); - float symbolScale = 64.0f / sprite.size.X; + float symbolScale = Math.Min(64.0f / sprite.size.X, 1.0f); if (dist > hideDist) { @@ -2089,8 +2089,8 @@ namespace Barotrauma Stretch = true, RelativeSpacing = 0.05f }; - - var button = new GUIButton(new RectTransform(new Vector2(0.1f, 0.1f), pauseMenuInner.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point((int)(15 * GUI.Scale)) }, + + new GUIButton(new RectTransform(new Vector2(0.1f, 0.1f), pauseMenuInner.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point((int)(15 * GUI.Scale)) }, "", style: "GUIBugButton") { IgnoreLayoutGroups = true, @@ -2098,12 +2098,12 @@ namespace Barotrauma OnClicked = (btn, userdata) => { GameMain.Instance.ShowBugReporter(); return true; } }; - button = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuResume")) + new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuResume")) { OnClicked = TogglePauseMenu }; - button = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuSettings")) + new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuSettings")) { OnClicked = (btn, userData) => { @@ -2117,8 +2117,8 @@ namespace Barotrauma { if (GameMain.GameSession.GameMode is SinglePlayerCampaign spMode) { - button = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuRetry")); - button.OnClicked += (btn, userData) => + var retryButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuRetry")); + retryButton.OnClicked += (btn, userData) => { var msgBox = new GUIMessageBox("", TextManager.Get("PauseMenuRetryVerification"), new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) { @@ -2131,6 +2131,7 @@ namespace Barotrauma GUIMessageBox.MessageBoxes.Remove(GameMain.GameSession.RoundSummary.Frame); } + GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction"); TogglePauseMenu(btn, userData); GameMain.GameSession.LoadPreviousSave(); return true; @@ -2144,24 +2145,57 @@ namespace Barotrauma }; return true; }; - button = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuSaveQuit")) + var saveAndQuitButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuSaveQuit")) { UserData = "save" }; - button.OnClicked += QuitClicked; - button.OnClicked += TogglePauseMenu; + saveAndQuitButton.OnClicked += (btn, userdata) => + { + //Only allow saving mid-round in outpost levels. Quitting in the middle of a mission reset progress to the start of the round. + if (GameMain.GameSession == null) + { + pauseMenuOpen = false; + + } + else if (GameMain.GameSession?.Campaign == null || Level.IsLoadedOutpost) + { + pauseMenuOpen = false; + GameMain.QuitToMainMenu(save: true); + } + else + { + var msgBox = new GUIMessageBox("", TextManager.Get("PauseMenuSaveAndQuitVerification", fallBackTag: "pausemenuquitverification"), new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) + { + UserData = "verificationprompt" + }; + msgBox.Buttons[0].OnClicked = (_, userdata) => + { + pauseMenuOpen = false; + GameMain.QuitToMainMenu(save: false); + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked = (_, userdata) => + { + pauseMenuOpen = false; + msgBox.Close(); + return true; + }; + } + return true; + }; } else if (GameMain.GameSession.GameMode is TestGameMode) { - button = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), text: TextManager.Get("PauseMenuReturnToEditor")) + new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), text: TextManager.Get("PauseMenuReturnToEditor")) { OnClicked = (btn, userdata) => { GameMain.GameSession.EndRound(""); + pauseMenuOpen = false; return true; } }; - button.OnClicked += TogglePauseMenu; } else if (!GameMain.GameSession.GameMode.IsSinglePlayer && GameMain.Client != null && GameMain.Client.HasPermission(ClientPermissions.ManageRound)) { @@ -2181,7 +2215,7 @@ namespace Barotrauma }; msgBox.Buttons[0].OnClicked = (_, __) => { - TogglePauseMenu(btn, userdata); + pauseMenuOpen = false; GameMain.Client.RequestRoundEnd(); return true; }; @@ -2190,7 +2224,7 @@ namespace Barotrauma } else { - TogglePauseMenu(btn, userdata); + pauseMenuOpen = false; GameMain.Client.RequestRoundEnd(); } return true; @@ -2199,10 +2233,9 @@ namespace Barotrauma } } - button = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuQuit")); - button.OnClicked += (btn, userData) => + var quitButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuQuit")); + quitButton.OnClicked += (btn, userData) => { - var quitButton = button; if (GameMain.GameSession != null || (Screen.Selected is CharacterEditorScreen || Screen.Selected is SubEditorScreen)) { string text = GameMain.GameSession == null ? "PauseMenuQuitVerificationEditor" : "PauseMenuQuitVerification"; @@ -2212,21 +2245,21 @@ namespace Barotrauma }; msgBox.Buttons[0].OnClicked = (yesBtn, userdata) => { - QuitClicked(quitButton, quitButton.UserData); + GameMain.QuitToMainMenu(save: false); pauseMenuOpen = false; return true; }; msgBox.Buttons[0].OnClicked += msgBox.Close; msgBox.Buttons[1].OnClicked = (_, userdata) => { - TogglePauseMenu(btn, userData); + pauseMenuOpen = false; msgBox.Close(); return true; }; } else { - QuitClicked(quitButton, quitButton.UserData); + GameMain.QuitToMainMenu(save: false); pauseMenuOpen = false; } return true; @@ -2242,12 +2275,6 @@ namespace Barotrauma return true; } - public static bool QuitClicked(GUIButton button, object obj) - { - GameMain.QuitToMainMenu(button.UserData as string == "save"); - return true; - } - /// /// Displays a message at the center of the screen, automatically preventing overlapping with other centered messages. TODO: Allow to show messages at the middle of the screen (instead of the top center). /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs index 878dca710..21acaa8f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs @@ -104,6 +104,12 @@ namespace Barotrauma set { textBlock.HoverTextColor = value; } } + public Color SelectedTextColor + { + get { return textBlock.SelectedTextColor; } + set { textBlock.SelectedTextColor = value; } + } + public override float FlashTimer { get { return Frame.FlashTimer; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index e944fe410..31233c164 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -756,9 +756,9 @@ namespace Barotrauma flashColor = (color == null) ? GUI.Style.Red : (Color)color; } - public void FadeOut(float duration, bool removeAfter) + public void FadeOut(float duration, bool removeAfter, float wait = 0.0f) { - CoroutineManager.StartCoroutine(LerpAlpha(0.0f, duration, removeAfter)); + CoroutineManager.StartCoroutine(LerpAlpha(0.0f, duration, removeAfter, wait)); } public void FadeIn(float wait, float duration) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs index 1a2ce4052..086cc25e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs @@ -239,7 +239,7 @@ namespace Barotrauma { activeTextureLoads.Add(Sprite.FullPath); } - Sprite.EnsureLazyLoaded(); + await Sprite.LazyLoadAsync(); } finally { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 9fd0993dc..7b6af817b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -446,7 +446,7 @@ namespace Barotrauma //dragging if (CanDragElements && draggedElement != null) { - if (!PlayerInput.LeftButtonHeld()) + if (!PlayerInput.PrimaryMouseButtonHeld()) { OnRearranged?.Invoke(this, draggedElement.UserData); draggedElement = null; @@ -533,7 +533,7 @@ namespace Barotrauma } } - if (CanDragElements && PlayerInput.LeftButtonDown() && GUI.MouseOn == child) + if (CanDragElements && PlayerInput.PrimaryMouseButtonDown() && GUI.MouseOn == child) { draggedElement = child; draggedReferenceRectangle = child.Rect; @@ -970,7 +970,6 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } - var children = Content.Children; int lastVisible = 0; int i = 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index df67d6172..e6bda9377 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -180,13 +180,13 @@ namespace Barotrauma Icon = new GUIImage(new RectTransform(new Vector2(0.2f, 0.95f), horizontalLayoutGroup.RectTransform), iconStyle, scaleToFit: true); } - Content = new GUILayoutGroup(new RectTransform(new Vector2(icon != null ? 0.65f : 0.85f, 1.0f), horizontalLayoutGroup.RectTransform)); + Content = new GUILayoutGroup(new RectTransform(new Vector2(Icon != null ? 0.65f : 0.85f, 1.0f), horizontalLayoutGroup.RectTransform)); var buttonContainer = new GUIFrame(new RectTransform(new Vector2(0.15f, 1.0f), horizontalLayoutGroup.RectTransform), style: null); Buttons = new List(1) { - new GUIButton(new RectTransform(new Vector2(0.5f, 0.5f), buttonContainer.RectTransform, Anchor.Center), - style: "GUIButtonHorizontalArrow") + new GUIButton(new RectTransform(new Vector2(0.3f, 0.5f), buttonContainer.RectTransform, Anchor.Center), + style: "UIToggleButton") { OnClicked = Close } @@ -221,7 +221,7 @@ namespace Barotrauma Content.RectTransform.NonScaledSize = new Point(Content.Rect.Width, height); } - Buttons[0].RectTransform.MaxSize = new Point(Math.Min(Buttons[0].Rect.Width, Buttons[0].Rect.Height)); + Buttons[0].RectTransform.MaxSize = new Point((int)(0.4f * Buttons[0].Rect.Y), Buttons[0].Rect.Y); } MessageBoxes.Add(this); @@ -271,99 +271,92 @@ namespace Barotrauma protected override void Update(float deltaTime) { - if (type == Type.InGame) + if (type != Type.InGame) { return; } + + Vector2 initialPos = new Vector2(0.0f, GameMain.GraphicsHeight); + Vector2 defaultPos = new Vector2(0.0f, HUDLayoutSettings.InventoryAreaLower.Y - InnerFrame.Rect.Height - 20 * GUI.Scale); + Vector2 endPos = new Vector2(GameMain.GraphicsWidth, defaultPos.Y); + + if (!closing) { - Vector2 initialPos = new Vector2(0.0f, GameMain.GraphicsHeight); - Vector2 defaultPos = new Vector2(0.0f, HUDLayoutSettings.InventoryAreaLower.Y - InnerFrame.Rect.Height - 20 * GUI.Scale); - Vector2 endPos = new Vector2(GameMain.GraphicsWidth, defaultPos.Y); - - /*for (int i = MessageBoxes.IndexOf(this); i >= 0; i--) + Point step = Vector2.SmoothStep(initialPos, defaultPos, openState).ToPoint(); + InnerFrame.RectTransform.AbsoluteOffset = step; + if (BackgroundIcon != null) { - if (MessageBoxes[i] is GUIMessageBox otherMsgBox && otherMsgBox != this && otherMsgBox.type == type && !otherMsgBox.closing) + BackgroundIcon.RectTransform.AbsoluteOffset = new Point(InnerFrame.Rect.Location.X - (int) (BackgroundIcon.Rect.Size.X / 1.25f), (int)defaultPos.Y - BackgroundIcon.Rect.Size.Y / 2); + if (!MathUtils.NearlyEqual(openState, 1.0f)) { - defaultPos = new Vector2( - Math.Max(otherMsgBox.InnerFrame.RectTransform.AbsoluteOffset.X + 10 * GUI.Scale, defaultPos.X), - Math.Max(otherMsgBox.InnerFrame.RectTransform.AbsoluteOffset.Y + 10 * GUI.Scale, defaultPos.Y)); - } - }*/ - - if (!closing) - { - Point step = Vector2.SmoothStep(initialPos, defaultPos, openState).ToPoint(); - InnerFrame.RectTransform.AbsoluteOffset = step; - if (BackgroundIcon != null) - { - BackgroundIcon.RectTransform.AbsoluteOffset = new Point(InnerFrame.Rect.Location.X - (int) (BackgroundIcon.Rect.Size.X / 1.25f), (int)defaultPos.Y - BackgroundIcon.Rect.Size.Y / 2); - if (!MathUtils.NearlyEqual(openState, 1.0f)) - { - BackgroundIcon.Color = ToolBox.GradientLerp(openState, Color.Transparent, Color.White); - } - } - openState = Math.Min(openState + deltaTime * 2.0f, 1.0f); - - if (GUI.MouseOn != InnerFrame && !InnerFrame.IsParentOf(GUI.MouseOn) && AutoClose) - { - inGameCloseTimer += deltaTime; - } - - if (inGameCloseTimer >= inGameCloseTime) - { - Close(); + BackgroundIcon.Color = ToolBox.GradientLerp(openState, Color.Transparent, Color.White); } } - else + if (!(Screen.Selected is RoundSummaryScreen) && !MessageBoxes.Any(mb => mb.UserData is RoundSummary)) + { + openState = Math.Min(openState + deltaTime * 2.0f, 1.0f); + } + + if (GUI.MouseOn != InnerFrame && !InnerFrame.IsParentOf(GUI.MouseOn) && AutoClose) + { + inGameCloseTimer += deltaTime; + } + + if (inGameCloseTimer >= inGameCloseTime) + { + Close(); + } + } + else + { + openState += deltaTime * 2.0f; + Point step = Vector2.SmoothStep(defaultPos, endPos, openState - 1.0f).ToPoint(); + InnerFrame.RectTransform.AbsoluteOffset = step; + if (BackgroundIcon != null) + { + BackgroundIcon.Color *= 0.9f; + } + if (openState >= 2.0f) + { + if (Parent != null) { Parent.RemoveChild(this); } + if (MessageBoxes.Contains(this)) { MessageBoxes.Remove(this); } + } + } + + if (newBackgroundIcon != null) + { + if (!iconSwitching) { - openState += deltaTime * 2.0f; - Point step = Vector2.SmoothStep(defaultPos, endPos, openState - 1.0f).ToPoint(); - InnerFrame.RectTransform.AbsoluteOffset = step; if (BackgroundIcon != null) { BackgroundIcon.Color *= 0.9f; - } - if (openState >= 2.0f) - { - if (Parent != null) { Parent.RemoveChild(this); } - if (MessageBoxes.Contains(this)) { MessageBoxes.Remove(this); } - } - } - - if (newBackgroundIcon != null) - { - if (!iconSwitching) - { - if (BackgroundIcon != null) - { - BackgroundIcon.Color *= 0.9f; - if (BackgroundIcon.Color.A == 0) - { - BackgroundIcon = null; - iconSwitching = true; - RemoveChild(BackgroundIcon); - } - } - else + if (BackgroundIcon.Color.A == 0) { + BackgroundIcon = null; iconSwitching = true; + RemoveChild(BackgroundIcon); } - iconState = 0; } else { - newBackgroundIcon.SetAsFirstChild(); - newBackgroundIcon.RectTransform.AbsoluteOffset = new Point(InnerFrame.Rect.Location.X - (int) (newBackgroundIcon.Rect.Size.X / 1.25f), (int)defaultPos.Y - newBackgroundIcon.Rect.Size.Y / 2); - newBackgroundIcon.Color = ToolBox.GradientLerp(iconState, Color.Transparent, Color.White); - if (newBackgroundIcon.Color.A == 255) - { - BackgroundIcon = newBackgroundIcon; - BackgroundIcon.SetAsFirstChild(); - newBackgroundIcon = null; - iconSwitching = false; - } - - iconState = Math.Min(iconState + deltaTime * 2.0f, 1.0f); + iconSwitching = true; } + iconState = 0; + } + else + { + newBackgroundIcon.SetAsFirstChild(); + newBackgroundIcon.RectTransform.AbsoluteOffset = new Point(InnerFrame.Rect.Location.X - (int) (newBackgroundIcon.Rect.Size.X / 1.25f), (int)defaultPos.Y - newBackgroundIcon.Rect.Size.Y / 2); + newBackgroundIcon.Color = ToolBox.GradientLerp(iconState, Color.Transparent, Color.White); + if (newBackgroundIcon.Color.A == 255) + { + BackgroundIcon = newBackgroundIcon; + BackgroundIcon.SetAsFirstChild(); + newBackgroundIcon = null; + iconSwitching = false; + } + + iconState = Math.Min(iconState + deltaTime * 2.0f, 1.0f); } } + } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index 95f2e3a74..46cb7d19c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -380,16 +380,18 @@ namespace Barotrauma private void ClampIntValue() { - if (MinValueInt != null) + if (MinValueInt != null && intValue < MinValueInt.Value) { intValue = Math.Max(intValue, MinValueInt.Value); - minusButton.Enabled = intValue > MinValueInt; + UpdateText(); } - if (MaxValueInt != null) + if (MaxValueInt != null && intValue > MaxValueInt.Value) { intValue = Math.Min(intValue, MaxValueInt.Value); - plusButton.Enabled = intValue < MaxValueInt; + UpdateText(); } + plusButton.Enabled = intValue < MaxValueInt; + minusButton.Enabled = intValue > MinValueInt; } private void UpdateText() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index 9a050d022..e1cc01fa6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -462,7 +462,7 @@ namespace Barotrauma protected List> GetAllPositions() { - float halfHeight = Font.MeasureString("T").Y * 0.5f; + float halfHeight = Font.MeasureString("T").Y * 0.5f * textScale; string textDrawn = Censor ? CensoredText : WrappedText; var positions = new List>(); if (textDrawn.Contains("\n")) @@ -474,10 +474,10 @@ namespace Barotrauma { string line = lines[i]; totalIndex += line.Length; - float totalTextHeight = Font.MeasureString(textDrawn.Substring(0, totalIndex)).Y; + float totalTextHeight = Font.MeasureString(textDrawn.Substring(0, totalIndex)).Y * textScale; for (int j = 0; j <= line.Length; j++) { - Vector2 lineTextSize = Font.MeasureString(line.Substring(0, j)); + Vector2 lineTextSize = Font.MeasureString(line.Substring(0, j)) * textScale; Vector2 indexPos = new Vector2(lineTextSize.X + Padding.X, totalTextHeight + Padding.Y - halfHeight); //DebugConsole.NewMessage($"index: {index}, pos: {indexPos}", Color.AliceBlue); positions.Add(new Tuple(indexPos, index + j)); @@ -490,8 +490,8 @@ namespace Barotrauma textDrawn = Censor ? CensoredText : Text; for (int i = 0; i <= Text.Length; i++) { - Vector2 textSize = Font.MeasureString(textDrawn.Substring(0, i)); - Vector2 indexPos = new Vector2(textSize.X + Padding.X, textSize.Y + Padding.Y - halfHeight) + TextPos - Origin; + Vector2 textSize = Font.MeasureString(textDrawn.Substring(0, i)) * textScale; + Vector2 indexPos = new Vector2(textSize.X + Padding.X, textSize.Y + Padding.Y - halfHeight) + TextPos - Origin * textScale; //DebugConsole.NewMessage($"index: {i}, pos: {indexPos}", Color.WhiteSmoke); positions.Add(new Tuple(indexPos, i)); } @@ -508,7 +508,7 @@ namespace Barotrauma { var positions = GetAllPositions(); if (positions.Count == 0) { return 0; } - float halfHeight = Font.MeasureString("T").Y * 0.5f; + float halfHeight = Font.MeasureString("T").Y * 0.5f * textScale; var currPosition = positions[0]; @@ -522,7 +522,8 @@ namespace Barotrauma float diffY = Math.Abs(p1.Item1.Y - pos.Y) - Math.Abs(p2.Item1.Y - pos.Y); if (diffY < -3.0f) { - currPosition = p1; continue; + currPosition = p1; + continue; } else if (diffY > 3.0f) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 30c29ef1c..ee12d4702 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -325,7 +325,7 @@ namespace Barotrauma } else { - while (ClampText && textBlock.Text.Length>0 && Font.MeasureString(textBlock.Text).X > (int)(textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z)) + while (ClampText && textBlock.Text.Length > 0 && Font.MeasureString(textBlock.Text).X * TextBlock.TextScale > (int)(textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z)) { textBlock.Text = textBlock.Text.Substring(0, textBlock.Text.Length - 1); } @@ -354,10 +354,10 @@ namespace Barotrauma { int diff = totalIndex - CaretIndex; int index = currentLineLength - diff; - Vector2 lineTextSize = Font.MeasureString(lines[i].Substring(0, index)); - Vector2 lastLineSize = Font.MeasureString(lines[i]); - float totalTextHeight = Font.MeasureString(textDrawn.Substring(0, totalIndex)).Y; - caretPos = new Vector2(lineTextSize.X, totalTextHeight - lastLineSize.Y) + textBlock.TextPos - textBlock.Origin; + Vector2 lineTextSize = Font.MeasureString(lines[i].Substring(0, index)) * TextBlock.TextScale; + Vector2 lastLineSize = Font.MeasureString(lines[i]) * TextBlock.TextScale; + float totalTextHeight = Font.MeasureString(textDrawn.Substring(0, totalIndex)).Y * TextBlock.TextScale; + caretPos = new Vector2(lineTextSize.X, totalTextHeight - lastLineSize.Y) + textBlock.TextPos - textBlock.Origin * TextBlock.TextScale; break; } } @@ -366,8 +366,8 @@ namespace Barotrauma { CaretIndex = Math.Min(CaretIndex, textDrawn.Length); textDrawn = Censor ? textBlock.CensoredText : textBlock.Text; - Vector2 textSize = Font.MeasureString(textDrawn.Substring(0, CaretIndex)); - caretPos = new Vector2(textSize.X, 0) + textBlock.TextPos - textBlock.Origin; + Vector2 textSize = Font.MeasureString(textDrawn.Substring(0, CaretIndex)) * TextBlock.TextScale; + caretPos = new Vector2(textSize.X, 0) + textBlock.TextPos - textBlock.Origin * TextBlock.TextScale; } caretPosDirty = false; } @@ -506,7 +506,7 @@ namespace Barotrauma { GUI.DrawLine(spriteBatch, new Vector2(Rect.X + (int)caretPos.X + 2, Rect.Y + caretPos.Y + 3), - new Vector2(Rect.X + (int)caretPos.X + 2, Rect.Y + caretPos.Y + Font.MeasureString("I").Y - 3), + new Vector2(Rect.X + (int)caretPos.X + 2, Rect.Y + caretPos.Y + Font.MeasureString("I").Y * textBlock.TextScale - 3), CaretColor ?? textBlock.TextColor * (textBlock.TextColor.A / 255.0f)); } if (selectedCharacters > 0) @@ -544,7 +544,7 @@ namespace Barotrauma : selectionEndIndex < totalIndex && selectionStartIndex > previousCharacters; if (containsSelection) { - Vector2 currentLineSize = Font.MeasureString(currentLine); + Vector2 currentLineSize = Font.MeasureString(currentLine) * TextBlock.TextScale; if ((IsLeftToRight && selectionStartIndex < previousCharacters && selectionEndIndex > totalIndex) || !IsLeftToRight && selectionEndIndex < previousCharacters && selectionStartIndex > totalIndex) { @@ -560,7 +560,7 @@ namespace Barotrauma int startIndex = selectFromTheBeginning ? 0 : Math.Abs(selectionStartIndex - previousCharacters); int endIndex = Math.Abs(selectionEndIndex - previousCharacters); int characters = Math.Min(endIndex - startIndex, currentLineLength - startIndex); - Vector2 selectedTextSize = Font.MeasureString(currentLine.Substring(startIndex, characters)); + Vector2 selectedTextSize = Font.MeasureString(currentLine.Substring(startIndex, characters)) * TextBlock.TextScale; Vector2 topLeft = selectFromTheBeginning ? new Vector2(offset.X, offset.Y + currentLineSize.Y * i) : new Vector2(selectionStartPos.X, offset.Y + currentLineSize.Y * i); @@ -573,7 +573,7 @@ namespace Barotrauma int startIndex = selectFromTheBeginning ? currentLineLength : Math.Abs(selectionStartIndex - previousCharacters); int endIndex = selectFromTheStart ? 0 : Math.Abs(selectionEndIndex - previousCharacters); int characters = Math.Min(Math.Abs(endIndex - startIndex), currentLineLength); - Vector2 selectedTextSize = Font.MeasureString(currentLine.Substring(endIndex, characters)); + Vector2 selectedTextSize = Font.MeasureString(currentLine.Substring(endIndex, characters)) * TextBlock.TextScale; Vector2 topLeft = selectFromTheBeginning ? new Vector2(offset.X + currentLineSize.X - selectedTextSize.X, offset.Y + currentLineSize.Y * i) : new Vector2(selectionStartPos.X - selectedTextSize.X, offset.Y + currentLineSize.Y * i); @@ -612,7 +612,7 @@ namespace Barotrauma OnTextChanged?.Invoke(this, Text); if (textBlock.OverflowClipActive && wasOverflowClipActive && !MathUtils.NearlyEqual(textBlock.TextPos, textPos)) { - textBlock.TextPos = textPos + Vector2.UnitX * Font.MeasureString(input).X; + textBlock.TextPos = textPos + Vector2.UnitX * Font.MeasureString(input).X * TextBlock.TextScale; } } } @@ -714,8 +714,8 @@ namespace Barotrauma { InitSelectionStart(); } - float lineHeight = Font.MeasureString("T").Y; - int newIndex = textBlock.GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y-lineHeight)); + float lineHeight = Font.MeasureString("T").Y * TextBlock.TextScale; + int newIndex = textBlock.GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y - lineHeight)); CaretIndex = newIndex; caretTimer = 0; HandleSelection(); @@ -725,8 +725,8 @@ namespace Barotrauma { InitSelectionStart(); } - lineHeight = Font.MeasureString("T").Y; - newIndex = textBlock.GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y+lineHeight)); + lineHeight = Font.MeasureString("T").Y * TextBlock.TextScale; + newIndex = textBlock.GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y + lineHeight)); CaretIndex = newIndex; caretTimer = 0; HandleSelection(); @@ -865,18 +865,18 @@ namespace Barotrauma { string textDrawn = Censor ? textBlock.CensoredText : textBlock.WrappedText; InitSelectionStart(); - selectionEndIndex = CaretIndex; + selectionEndIndex = Math.Min(CaretIndex, textDrawn.Length); selectionEndPos = caretPos; selectedCharacters = Math.Abs(selectionStartIndex - selectionEndIndex); if (IsLeftToRight) { selectedText = Text.Substring(selectionStartIndex, selectedCharacters); - selectionRectSize = Font.MeasureString(textDrawn.Substring(selectionStartIndex, selectedCharacters)); + selectionRectSize = Font.MeasureString(textDrawn.Substring(selectionStartIndex, selectedCharacters)) * TextBlock.TextScale; } else { - selectedText = Text.Substring(selectionEndIndex, selectedCharacters); - selectionRectSize = Font.MeasureString(textDrawn.Substring(selectionEndIndex, selectedCharacters)); + selectedText = Text.Substring(selectionEndIndex, Math.Min(selectedCharacters, textDrawn.Length - selectionEndIndex)); + selectionRectSize = Font.MeasureString(textDrawn.Substring(selectionEndIndex, selectedCharacters)) * TextBlock.TextScale; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 2d5cf7b96..b82efcead 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -68,6 +68,10 @@ namespace Barotrauma CreateUI(); campaignUI.Campaign.Map.OnLocationChanged += UpdateLocation; + if (CurrentLocation?.Reputation != null) + { + CurrentLocation.Reputation.OnReputationValueChanged += Refresh; + } campaignUI.Campaign.CargoManager.OnItemsInBuyCrateChanged += RefreshBuying; campaignUI.Campaign.CargoManager.OnPurchasedItemsChanged += RefreshBuying; campaignUI.Campaign.CargoManager.OnItemsInSellCrateChanged += RefreshSelling; @@ -378,9 +382,13 @@ namespace Barotrauma { Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), totalContainer.RectTransform), TextManager.Get("campaignstore.total"), font: GUI.Font); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), totalContainer.RectTransform), TextManager.Get("campaignstore.total"), font: GUI.Font) + { + CanBeFocused = false + }; shoppingCrateTotal = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), totalContainer.RectTransform), "", font: GUI.SubHeadingFont, textAlignment: Alignment.Right) { + CanBeFocused = false, TextScale = 1.1f }; @@ -412,11 +420,20 @@ namespace Barotrauma { if (prevLocation == newLocation) { return; } + if (prevLocation?.Reputation != null) + { + prevLocation.Reputation.OnReputationValueChanged -= Refresh; + } + foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { if (itemPrefab.CanBeBoughtAtLocation(CurrentLocation, out PriceInfo _)) { ChangeStoreTab(StoreTab.Buy); + if (newLocation?.Reputation != null) + { + newLocation.Reputation.OnReputationValueChanged += Refresh; + } return; } } @@ -717,9 +734,15 @@ namespace Barotrauma private GUIComponent CreateItemFrame(PurchasedItem pi, PriceInfo priceInfo, GUIListBox listBox, bool forceDisable = false) { + var tooltip = pi.ItemPrefab.Name; + if (!string.IsNullOrWhiteSpace(pi.ItemPrefab.Description)) + { + tooltip += "\n" + pi.ItemPrefab.Description; + } + GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, (int)(GUI.yScale * 60)), parent: listBox.Content.RectTransform), style: "ListBoxElement") { - ToolTip = pi.ItemPrefab.Description, + ToolTip = tooltip, UserData = pi }; @@ -740,6 +763,7 @@ namespace Barotrauma iconRelativeWidth = (0.9f * mainGroup.Rect.Height) / mainGroup.Rect.Width; GUIImage img = new GUIImage(new RectTransform(new Vector2(iconRelativeWidth, 0.9f), mainGroup.RectTransform), itemIcon, scaleToFit: true) { + CanBeFocused = false, Color = (itemIcon == pi.ItemPrefab.InventoryIcon ? pi.ItemPrefab.InventoryIconColor : pi.ItemPrefab.SpriteColor) * (forceDisable ? 0.5f : 1.0f), UserData = "icon" }; @@ -748,8 +772,8 @@ namespace Barotrauma GUILayoutGroup nameAndQuantityGroup = new GUILayoutGroup(new RectTransform(new Vector2(nameAndIconRelativeWidth - iconRelativeWidth, 1.0f), mainGroup.RectTransform)) { - Stretch = true, - ToolTip = pi.ItemPrefab.Description + CanBeFocused = false, + Stretch = true }; GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), nameAndQuantityGroup.RectTransform), pi.ItemPrefab.Name, font: GUI.SubHeadingFont, textAlignment: Alignment.BottomLeft) @@ -801,8 +825,8 @@ namespace Barotrauma var priceBlock = new GUITextBlock(new RectTransform(new Vector2(priceAndButtonRelativeWidth - buttonRelativeWidth, 1.0f), mainGroup.RectTransform), "", font: GUI.SubHeadingFont, textAlignment: Alignment.Right) { + CanBeFocused = false, TextColor = Color.White * (forceDisable ? 0.5f : 1.0f), - ToolTip = pi.ItemPrefab.Description, UserData = "price" }; if(listBox == storeSellList || listBox == shoppingCrateSellList) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index e98683d2a..6078eb9c0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -788,17 +788,12 @@ namespace Barotrauma string msg = ChatMessage.GetTimeStamp() + message.TextWithSender; storedMessages.Add(new Pair(msg, message.ChangeType)); - if (GameSession.IsTabMenuOpen) + if (GameSession.IsTabMenuOpen && selectedTab == InfoFrameTab.Crew) { TabMenu instance = GameSession.TabMenuInstance; instance.AddLineToLog(msg, message.ChangeType); - - // Update crew - if (selectedTab == InfoFrameTab.Crew) - { - instance.RemoveCurrentElements(); - instance.CreateMultiPlayerList(true); - } + instance.RemoveCurrentElements(); + instance.CreateMultiPlayerList(true); } } @@ -835,14 +830,15 @@ namespace Barotrauma break; } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), logList.Content.RectTransform), line, wrap: true, font: GUI.SmallFont) + if (logList != null) { - TextColor = textColor, - CanBeFocused = false, - UserData = line - }.CalculateHeightFromText(); - - //if ((prevSize == 1.0f && listBox.BarScroll == 0.0f) || (prevSize < 1.0f && listBox.BarScroll == 1.0f)) listBox.BarScroll = 1.0f; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), logList.Content.RectTransform), line, wrap: true, font: GUI.SmallFont) + { + TextColor = textColor, + CanBeFocused = false, + UserData = line + }.CalculateHeightFromText(); + } } private void CreateMissionInfo(GUIFrame infoFrame) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index a8a473e59..97a598368 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -56,11 +56,6 @@ namespace Barotrauma public static Thread MainThread { get; private set; } - public static IEnumerable SelectedPackages - { - get { return Config?.SelectedContentPackages; } - } - private static ContentPackage vanillaContent; public static ContentPackage VanillaContent { @@ -69,7 +64,7 @@ namespace Barotrauma if (vanillaContent == null) { // TODO: Dynamic method for defining and finding the vanilla content package. - vanillaContent = ContentPackage.List.SingleOrDefault(cp => Path.GetFileName(cp.Path).Equals("vanilla 0.9.xml", StringComparison.OrdinalIgnoreCase)); + vanillaContent = ContentPackage.CorePackages.SingleOrDefault(cp => Path.GetFileName(cp.Path).Equals("vanilla 0.9.xml", StringComparison.OrdinalIgnoreCase)); } return vanillaContent; } @@ -454,40 +449,31 @@ namespace Barotrauma } } - GUI.Init(Window, Config.SelectedContentPackages, GraphicsDevice); + GUI.Init(Window, Config.AllEnabledPackages, GraphicsDevice); DebugConsole.Init(); if (Config.AutoUpdateWorkshopItems) { - bool waitingForWorkshopUpdates = true; - bool result = false; + Config.WaitingForAutoUpdate = true; TaskPool.Add("AutoUpdateWorkshopItemsAsync", SteamManager.AutoUpdateWorkshopItemsAsync(), (task) => { - result = ((Task)task).Result; - waitingForWorkshopUpdates = false; + bool result = ((Task)task).Result; + + Config.WaitingForAutoUpdate = false; }); - while (waitingForWorkshopUpdates) { yield return CoroutineStatus.Running; } - - if (result) - { - CrossThread.RequestExecutionOnMainThread(() => - { - ContentPackage.LoadAll(); - Config.ReloadContentPackages(); - }); - } + while (Config.WaitingForAutoUpdate) { yield return CoroutineStatus.Running; } } - if (SelectedPackages.None()) + if (Config.AllEnabledPackages.None()) { DebugConsole.Log("No content packages selected"); } else { - DebugConsole.Log("Selected content packages: " + string.Join(", ", SelectedPackages.Select(cp => cp.Name))); + DebugConsole.Log("Selected content packages: " + string.Join(", ", Config.AllEnabledPackages.Select(cp => cp.Name))); } #if DEBUG @@ -542,6 +528,7 @@ namespace Barotrauma OutpostGenerationParams.LoadPresets(); WreckAIConfig.LoadAll(); EventSet.LoadPrefabs(); + ItemPrefab.LoadAll(GetFilesOfType(ContentType.Item)); AfflictionPrefab.LoadAll(GetFilesOfType(ContentType.Afflictions)); SkillSettings.Load(GetFilesOfType(ContentType.SkillSettings)); Order.Init(); @@ -550,10 +537,6 @@ namespace Barotrauma yield return CoroutineStatus.Running; StructurePrefab.LoadAll(GetFilesOfType(ContentType.Structure)); - TitleScreen.LoadState = 53.0f; - yield return CoroutineStatus.Running; - - ItemPrefab.LoadAll(GetFilesOfType(ContentType.Item)); TitleScreen.LoadState = 55.0f; yield return CoroutineStatus.Running; @@ -628,8 +611,6 @@ namespace Barotrauma LocationType.Init(); MainMenuScreen.Select(); - CheckContentPackage(); - foreach (string steamError in SteamManager.InitializationErrors) { new GUIMessageBox(TextManager.Get("Error"), TextManager.Get(steamError)); @@ -645,29 +626,6 @@ namespace Barotrauma } - private void CheckContentPackage() - { - foreach (ContentPackage contentPackage in Config.SelectedContentPackages) - { - var exePaths = contentPackage.GetFilesOfType(ContentType.Executable); - if (exePaths.Any() && AppDomain.CurrentDomain.FriendlyName != Path.GetFileNameWithoutExtension(exePaths.First())) - { - var msgBox = new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariables("IncorrectExe", - new string[2] { "[selectedpackage]", "[exename]" }, new string[2] { contentPackage.Name, exePaths.First() }), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - msgBox.Buttons[0].OnClicked += (_, userdata) => - { - string fullPath = Path.GetFullPath(exePaths.First()); - ToolBox.OpenFileWithShell(fullPath); - Exit(); - return true; - }; - msgBox.Buttons[1].OnClicked = msgBox.Close; - break; - } - } - } - /// /// UnloadContent will be called once per game and is the place to unload /// all content. @@ -690,11 +648,11 @@ namespace Barotrauma { if (searchAllContentPackages) { - return ContentPackage.GetFilesOfType(ContentPackage.List, type); + return ContentPackage.GetFilesOfType(ContentPackage.AllPackages, type); } else { - return ContentPackage.GetFilesOfType(SelectedPackages, type); + return ContentPackage.GetFilesOfType(Config.AllEnabledPackages, type); } } @@ -938,6 +896,7 @@ namespace Barotrauma if (GameSession?.GameMode != null && GameSession.GameMode.Paused) { Paused = true; + GameSession.GameMode.UpdateWhilePaused((float)Timing.Step); } #if !DEBUG @@ -960,7 +919,6 @@ namespace Barotrauma DebugConsole.AddToGUIUpdateList(); DebugConsole.Update((float)Timing.Step); - Paused = Paused || (DebugConsole.IsOpen && (NetworkMember == null || !NetworkMember.GameStarted)); if (!Paused) { @@ -1088,11 +1046,11 @@ namespace Barotrauma } // Update store stock when saving and quitting in an outpost (normally updated when CampaignMode.End() is called) - if (GameSession?.Campaign is SinglePlayerCampaign campaign && Level.IsLoadedOutpost && campaign.Map?.CurrentLocation != null && campaign.CargoManager != null) + if (GameSession?.Campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedOutpost && spCampaign.Map?.CurrentLocation != null && spCampaign.CargoManager != null) { - campaign.Map.CurrentLocation.AddToStock(campaign.CargoManager.SoldItems); - campaign.CargoManager.ClearSoldItemsProjSpecific(); - campaign.Map.CurrentLocation.RemoveFromStock(campaign.CargoManager.PurchasedItems); + spCampaign.Map.CurrentLocation.AddToStock(spCampaign.CargoManager.SoldItems); + spCampaign.CargoManager.ClearSoldItemsProjSpecific(); + spCampaign.Map.CurrentLocation.RemoveFromStock(spCampaign.CargoManager.PurchasedItems); } SaveUtil.SaveGame(GameSession.SavePath); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index 6138a1f0b..f2ee9ebca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -10,8 +10,17 @@ namespace Barotrauma { public enum SellStatus { + /// + /// Entity sold in SP. Or, entity sold by client and confirmed by server in MP. + /// Confirmed, + /// + /// Entity sold by client in MP. Client has received at least one update from server after selling, but this entity wasn't yet confirmed. + /// Unconfirmed, + /// + /// Entity sold by client in MP. Client hasn't yet received an update from server after selling. + /// Local } @@ -36,28 +45,41 @@ namespace Barotrauma // Only consider items which have been: // a) sold in singleplayer or confirmed by server (SellStatus.Confirmed); or - // b) sold locally in multiplier (SellStatus.Local), but the client has not received a campaing state update yet after selling them + // b) sold locally in multiplayer (SellStatus.Local), but the client has not received a campaing state update yet after selling them var soldEntities = SoldEntities.Where(se => se.Status != SoldEntity.SellStatus.Unconfirmed); var sellables = Item.ItemList.FindAll(i => i?.Prefab != null && !i.Removed && i.GetRootInventoryOwner() == character && !i.SpawnedInOutpost && (i.ContainedItems == null || i.ContainedItems.None() || i.ContainedItems.All(ci => soldEntities.Any(se => se.Item == ci))) && - i.IsFullCondition && soldEntities.None(se => se.Item == i)); + i.Condition >= 0.9f * i.MaxCondition && soldEntities.None(se => se.Item == i)); - // Prevent selling things like battery cells from headsets and oxygen tanks from diving masks - var slots = new List() { InvSlotType.Head, InvSlotType.OuterClothes, InvSlotType.Headset }; + // Prevent selling items in equipment slots + var slots = new List() { InvSlotType.Head, InvSlotType.InnerClothes, InvSlotType.OuterClothes, InvSlotType.Headset, InvSlotType.Card }; foreach (InvSlotType slot in slots) { var index = character.Inventory.FindLimbSlot(slot); - if (character.Inventory.Items[index] is Item item && item.ContainedItems != null) + if (character.Inventory.Items[index] is Item item) + { + // Don't prevent selling of items which can only be put in equipment slots (like diving suits) + if (item.AllowedSlots.Contains(InvSlotType.Any)) + { + sellables.Remove(item); + } + } + } + + // Prevent selling items contained in certain equipped items (like battery cell in equipped headset or oxygen tank in equipped diving mask) + slots = new List() { InvSlotType.Head, InvSlotType.OuterClothes, InvSlotType.Headset }; + foreach (InvSlotType slot in slots) + { + var index = character.Inventory.FindLimbSlot(slot); + if (character.Inventory.Items[index] is Item item && + item.ContainedItems != null && item.AllowedSlots.Contains(InvSlotType.Any)) { foreach (Item containedItem in item.ContainedItems) { - if (containedItem != null) - { - sellables.Remove(containedItem); - } + sellables.Remove(containedItem); } } } @@ -123,7 +145,7 @@ namespace Barotrauma var itemValue = GetSellValueAtCurrentLocation(item.ItemPrefab, quantity: item.Quantity); // check if the store can afford the item - if (location.StoreCurrentBalance < itemValue) { continue; } + if (Location.StoreCurrentBalance < itemValue) { continue; } var matchingItems = itemsInInventory.FindAll(i => i.Prefab == item.ItemPrefab); if (matchingItems.Count <= item.Quantity) @@ -147,7 +169,7 @@ namespace Barotrauma } // Exchange money - campaign.Map.CurrentLocation.StoreCurrentBalance -= itemValue; + Location.StoreCurrentBalance -= itemValue; campaign.Money += itemValue; // Remove from the sell crate diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 3a9977d69..81061b83b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -15,11 +15,6 @@ namespace Barotrauma { partial class CrewManager { - /// - /// How long the previously selected character waits doing nothing when switching to another character. Only affects idling. - /// - const float CharacterWaitOnSwitch = 10.0f; - private Point screenResolution; #region UI @@ -63,6 +58,8 @@ namespace Barotrauma private Sprite jobIndicatorBackground, previousOrderArrow, cancelIcon; + private const int MaxOrderIcons = 3; + #endregion #region Constructors @@ -363,9 +360,10 @@ namespace Barotrauma UserData = character }; - // "Padding" to prevent member-specific command button from overlapping job indicator var commandButtonAbsoluteHeight = Math.Min(40.0f, 0.67f * background.Rect.Height); var paddingRelativeWidth = 0.35f * commandButtonAbsoluteHeight / background.Rect.Width; + + // "Padding" to prevent member-specific command button from overlapping job indicator new GUIFrame(new RectTransform(new Vector2(paddingRelativeWidth, 1.0f), layoutGroup.RectTransform), style: null); var jobIconBackground = new GUIImage( @@ -392,7 +390,16 @@ namespace Barotrauma }; } - var nameRelativeWidth = 1.0f - paddingRelativeWidth - 3.7f * iconRelativeWidth; + var nameRelativeWidth = 1.0f + // Start padding + - paddingRelativeWidth + // 5 icons (job, 3 orders, sound) + - (5 * 0.8f * iconRelativeWidth) + // Vertical line + - (0.1f * iconRelativeWidth) + // Spacing + - (7 * layoutGroup.RelativeSpacing); + var font = layoutGroup.Rect.Width < 150 ? GUI.SmallFont : GUI.Font; var nameBlock = new GUITextBlock( new RectTransform( @@ -417,6 +424,7 @@ namespace Barotrauma { UserData = character }; + // Only create a tooltip if the name doesn't fit the name block if (nameBlock.Text.EndsWith("...")) { @@ -432,6 +440,7 @@ namespace Barotrauma }}; } } + if (IsSinglePlayer) { characterButton.OnClicked = CharacterClicked; @@ -443,7 +452,7 @@ namespace Barotrauma } new GUIImage( - new RectTransform(new Vector2(0.5f * iconRelativeWidth, 0.5f), layoutGroup.RectTransform), + new RectTransform(new Vector2(0.1f * iconRelativeWidth, 0.5f), layoutGroup.RectTransform), style: "VerticalLine") { CanBeFocused = false @@ -697,7 +706,7 @@ namespace Barotrauma /// /// Displays the specified order in the crew UI next to the character. /// - public void DisplayCharacterOrder(Character character, Order order, string option) + public void AddCurrentOrderIcon(Character character, Order order, string option) { if (character == null) { return; } @@ -706,34 +715,44 @@ namespace Barotrauma if (characterFrame == null) { return; } GUILayoutGroup layoutGroup = (GUILayoutGroup)characterFrame.FindChild(c => c is GUILayoutGroup); - var currentOrderComponent = GetCurrentOrderComponent(layoutGroup); - if (order != null) + // Get the OrderInfo from the current order icon + var currentOrderIcon = GetCurrentOrderIcon(layoutGroup); + OrderInfo? currentOrderInfo = null; + if (currentOrderIcon?.UserData is OrderInfo) { - var prevOrderComponent = GetPreviousOrderComponent(layoutGroup); - if (currentOrderComponent?.UserData is OrderInfo currentOrderInfo) - { - if (order.Identifier == currentOrderInfo.Order.Identifier && - option == currentOrderInfo.OrderOption && - order.TargetEntity == currentOrderInfo.Order.TargetEntity) { return; } + currentOrderInfo = (OrderInfo)currentOrderIcon.UserData; + // No need to recreate icons if the current order matches the new order + if (currentOrderInfo.Value.MatchesOrder(order, option)) { return; } + } - layoutGroup.RemoveChild(prevOrderComponent); - DisplayPreviousCharacterOrder(character, layoutGroup, currentOrderInfo); - } - else if (order.Identifier != dismissedOrderPrefab.Identifier && - prevOrderComponent?.UserData is OrderInfo prevOrderInfo && - order.Identifier == prevOrderInfo.Order.Identifier && - option == prevOrderInfo.OrderOption && - order.TargetEntity == prevOrderInfo.Order.TargetEntity) + // Remove the current order icon + layoutGroup.RemoveChild(currentOrderIcon); + + // Remove a previous order icon if it matches the new order + // We don't want the same order as both a current order and a previous order + foreach (GUIComponent icon in GetPreviousOrderIcons(layoutGroup)) + { + if (icon?.UserData is OrderInfo info && info.MatchesOrder(order, option)) { - layoutGroup.RemoveChild(prevOrderComponent); + layoutGroup.RemoveChild(icon); + break; } } - layoutGroup.RemoveChild(currentOrderComponent); + // Create a new previous order icon from the current order icon's OrderInfo + if (currentOrderInfo.HasValue) + { + AddPreviousOrderIcon(character, layoutGroup, currentOrderInfo.Value); + } if (order == null || order.Identifier == dismissedOrderPrefab.Identifier) { return; } + if (GetPreviousOrderIcons(layoutGroup).Count() >= MaxOrderIcons) + { + RemoveLastPreviousOrderIcon(layoutGroup); + } + var orderFrame = new GUIButton( new RectTransform( layoutGroup.GetChildByUserData("job").RectTransform.RelativeSize, @@ -748,25 +767,31 @@ namespace Barotrauma return true; } }; + CreateNodeIcon(orderFrame.RectTransform, order.SymbolSprite, order.Color, tooltip: order.Name); - new GUIImage( - new RectTransform(Vector2.One, orderFrame.RectTransform), - cancelIcon, - scaleToFit: true) + + new GUIImage(new RectTransform(Vector2.One, orderFrame.RectTransform), cancelIcon, scaleToFit: true) { CanBeFocused = false, UserData = "cancel", Visible = false }; + orderFrame.RectTransform.RepositionChildInHierarchy(4); - characterFrame.SetAsFirstChild(); } - private void DisplayPreviousCharacterOrder(Character character, GUILayoutGroup characterComponent, OrderInfo orderInfo) + private void AddPreviousOrderIcon(Character character, GUILayoutGroup characterComponent, OrderInfo orderInfo) { if (orderInfo.Order == null || orderInfo.Order.Identifier == dismissedOrderPrefab.Identifier) { return; } + var maxPreviousOrderIcons = GetCurrentOrderIcon(characterComponent) != null ? MaxOrderIcons - 1 : MaxOrderIcons; + if (GetPreviousOrderIcons(characterComponent).Count() >= maxPreviousOrderIcons) + { + RemoveLastPreviousOrderIcon(characterComponent); + } + var previousOrderInfo = new OrderInfo(orderInfo); + var prevOrderFrame = new GUIButton( new RectTransform( characterComponent.GetChildByUserData("job").RectTransform.RelativeSize, @@ -786,17 +811,20 @@ namespace Barotrauma var prevOrderIconFrame = new GUIFrame( new RectTransform(new Vector2(0.8f), prevOrderFrame.RectTransform, anchor: Anchor.BottomLeft), style: null); + CreateNodeIcon( prevOrderIconFrame.RectTransform, previousOrderInfo.Order.SymbolSprite, previousOrderInfo.Order.Color, tooltip: previousOrderInfo.Order.Name); + foreach (GUIComponent c in prevOrderIconFrame.Children) { c.HoverColor = c.Color; c.PressedColor = c.Color; c.SelectedColor = c.Color; } + new GUIImage( new RectTransform(new Vector2(0.8f), prevOrderFrame.RectTransform, anchor: Anchor.TopRight), previousOrderArrow, @@ -804,19 +832,63 @@ namespace Barotrauma { CanBeFocused = false }; - prevOrderFrame.RectTransform.RepositionChildInHierarchy(GetCurrentOrderComponent(characterComponent) != null ? 5 : 4); + + var positionInHierarchy = GetCurrentOrderIcon(characterComponent) != null ? 5 : 4; + prevOrderFrame.RectTransform.RepositionChildInHierarchy(positionInHierarchy); } - private GUIComponent GetCurrentOrderComponent(GUILayoutGroup characterComponent) + private void AddOldPreviousOrderIcons(Character character, GUILayoutGroup oldCharacterComponent) { - return characterComponent?.FindChild(c => c?.UserData is OrderInfo orderInfo && orderInfo.ComponentIdentifier == "currentorder"); + var prevOrderIcons = GetPreviousOrderIcons(oldCharacterComponent).ToList(); + if (prevOrderIcons.None()) { return; } + if (prevOrderIcons.Count() > 1) + { + prevOrderIcons.Sort((x, y) => oldCharacterComponent.GetChildIndex(x).CompareTo(oldCharacterComponent.GetChildIndex(y))); + prevOrderIcons.Reverse(); + } + if (crewList.Content.Children.FirstOrDefault(c => c?.UserData == character)?.GetChild() is GUILayoutGroup newCharacterComponent) + { + foreach (GUIComponent icon in prevOrderIcons) + { + if (icon.UserData is OrderInfo orderInfo) + { + AddPreviousOrderIcon(character, newCharacterComponent, orderInfo); + } + } + } } - private GUIComponent GetPreviousOrderComponent(GUILayoutGroup characterComponent) + private void RemoveLastPreviousOrderIcon(GUILayoutGroup characterComponent) { - return characterComponent?.FindChild(c => c?.UserData is OrderInfo orderInfo && orderInfo.ComponentIdentifier == "previousorder"); + var prevOrderIcons = GetPreviousOrderIcons(characterComponent); + if (prevOrderIcons.None()) { return; } + if (prevOrderIcons.Count() == 1) + { + characterComponent.RemoveChild(prevOrderIcons.First()); + } + else + { + int highestIndex = 0; + GUIComponent oldestPreviousOrderIcon = null; + foreach (GUIComponent icon in prevOrderIcons) + { + int i = characterComponent.GetChildIndex(icon); + if (i > highestIndex || oldestPreviousOrderIcon == null) + { + highestIndex = i; + oldestPreviousOrderIcon = icon; + } + } + characterComponent.RemoveChild(oldestPreviousOrderIcon); + } } + private GUIComponent GetCurrentOrderIcon(GUILayoutGroup characterComponent) => + characterComponent?.FindChild(c => c?.UserData is OrderInfo orderInfo && orderInfo.ComponentIdentifier == "currentorder"); + + private IEnumerable GetPreviousOrderIcons(GUILayoutGroup characterComponent) => + characterComponent?.FindChildren(c => c?.UserData is OrderInfo orderInfo && orderInfo.ComponentIdentifier == "previousorder"); + #endregion #region Updating and drawing the UI @@ -854,7 +926,7 @@ namespace Barotrauma { if (IsSinglePlayer || client == null || (GameMain.NetworkMember?.ConnectedClients?.All(match => match != client) ?? true)) { return; } - contextMenu = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.12f), GUI.Canvas) { ScreenSpaceOffset = mousePos }, style: "GUIToolTip") { UserData = client }; + contextMenu = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.15f), GUI.Canvas) { ScreenSpaceOffset = mousePos }, style: "GUIToolTip") { UserData = client }; var nameLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.2f), contextMenu.RectTransform), client.Name, font: GUI.SubHeadingFont) { @@ -864,7 +936,9 @@ namespace Barotrauma var optionsList = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), contextMenu.RectTransform, Anchor.BottomLeft), style: null) { - Padding = new Vector4(4, 0, 4, 4) + Padding = new Vector4(4, 0, 4, 4), + AutoHideScrollBar = false, + ScrollBarVisible = false }; bool hasSteam = client.SteamID > 0 && SteamManager.IsInitialized, @@ -886,6 +960,13 @@ namespace Barotrauma UserData = "steam" }; + new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get("moderationmenu.userdetails"), font: GUI.SmallFont) + { + Padding = new Vector4(4), + Enabled = true, + UserData = "user" + }; + new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get("permissions"), font: GUI.SmallFont) { Padding = new Vector4(4), @@ -971,6 +1052,9 @@ namespace Barotrauma case "ban": GameMain.Client?.CreateKickReasonPrompt(client.Name, true); break; + case "user": + GameMain.NetLobbyScreen?.SelectPlayer(client); + break; } contextMenu = null; return true; @@ -983,7 +1067,11 @@ namespace Barotrauma { if (client == null ) { return; } - subContextMenu = new GUIListBox(new RectTransform(new Vector2(0.1f, 0.1f), GUI.Canvas) { ScreenSpaceOffset = pos }, style: "GUIToolTip"); + subContextMenu = new GUIListBox(new RectTransform(new Vector2(0.1f, 0.1f), GUI.Canvas) { ScreenSpaceOffset = pos }, style: "GUIToolTip") + { + AutoHideScrollBar = false, + ScrollBarVisible = false + }; foreach (var rank in PermissionPreset.List) { @@ -1073,11 +1161,7 @@ namespace Barotrauma { if (!(c.UserData is Character character) || character.IsDead || character.Removed) { continue; } AddCharacter(character); - if (GetPreviousOrderComponent(c.GetChild())?.UserData is OrderInfo prevInfo && - crewList.Content.Children.FirstOrDefault(c => c?.UserData == character)?.GetChild() is GUILayoutGroup newLayoutGroup) - { - DisplayPreviousCharacterOrder(character, newLayoutGroup, prevInfo); - } + AddOldPreviousOrderIcons(character, c.GetChild()); } } @@ -1112,9 +1196,10 @@ namespace Barotrauma if (!AllowCharacterSwitch) { return; } //make the previously selected character wait in place for some time //(so they don't immediately start idling and walking away from their station) - if (Character.Controlled?.AIController?.ObjectiveManager != null) + var aiController = Character.Controlled?.AIController; + if (aiController != null) { - Character.Controlled.AIController.ObjectiveManager.WaitTimer = CharacterWaitOnSwitch; + aiController.Reset(); } DisableCommandUI(); Character.Controlled = character; @@ -1419,7 +1504,7 @@ namespace Barotrauma } if (child.FindChild(c => c is GUILayoutGroup) is GUILayoutGroup layoutGroup) { - if (GetCurrentOrderComponent(layoutGroup) is GUIComponent orderButton && + if (GetCurrentOrderIcon(layoutGroup) is GUIComponent orderButton && orderButton.GetChildByUserData("colorsource") is GUIComponent orderIcon && orderButton.GetChildByUserData("cancel") is GUIComponent cancelIcon) { @@ -1504,8 +1589,8 @@ namespace Barotrauma private Hull hullContext; private bool isContextual; private readonly List contextualOrders = new List(); - private Point shorcutCenterNodeOffset; - private const int maxShorcutNodeCount = 4; + private Point shortcutCenterNodeOffset; + private const int maxShortcutNodeCount = 4; private bool WasCommandInterfaceDisabledThisUpdate { get; set; } private bool CanIssueOrders @@ -1688,7 +1773,7 @@ namespace Barotrauma returnNodeMargin = returnNodeSize.X * 0.5f; nodeDistance = (int)(150 * GUI.Scale); - shorcutCenterNodeOffset = new Point(0, (int)(1.25f * nodeDistance)); + shortcutCenterNodeOffset = new Point(0, (int)(1.25f * nodeDistance)); } private List GetAvailableCategories() @@ -1972,7 +2057,7 @@ namespace Barotrauma shortcutNodes.Clear(); - if (shortcutNodes.Count < maxShorcutNodeCount && sub.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent() is Reactor reactor) + if (shortcutNodes.Count < maxShortcutNodeCount && sub.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent() is Reactor reactor) { var reactorOutput = -reactor.CurrPowerConsumption; // If player is not an engineer AND the reactor is not powered up AND nobody is using the reactor @@ -1990,7 +2075,7 @@ namespace Barotrauma // TODO: Reconsider the conditions as bot captain can have the nav term selected without operating it // If player is not a captain AND nobody is using the nav terminal AND the nav terminal is powered up // --> Create shortcut node for Steer order - if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info?.Job?.Prefab != JobPrefab.Get("captain")) && + if (shortcutNodes.Count < maxShortcutNodeCount && (Character.Controlled == null || Character.Controlled.Info?.Job?.Prefab != JobPrefab.Get("captain")) && sub.GetItems(false).Find(i => i.HasTag("navterminal") && !i.NonInteractable) is Item nav && characters.None(c => c.SelectedConstruction == nav) && nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) { @@ -2000,7 +2085,7 @@ namespace Barotrauma // If player is not a security officer AND invaders are reported // --> Create shorcut node for Fight Intruders order - if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info?.Job?.Prefab != JobPrefab.Get("securityofficer")) && + if (shortcutNodes.Count < maxShortcutNodeCount && (Character.Controlled == null || Character.Controlled.Info?.Job?.Prefab != JobPrefab.Get("securityofficer")) && (Order.GetPrefab("reportintruders") is Order reportIntruders && ActiveOrders.Any(o => o.First.Prefab == reportIntruders))) { shortcutNodes.Add( @@ -2009,7 +2094,7 @@ namespace Barotrauma // If player is not a mechanic AND a breach has been reported // --> Create shorcut node for Fix Leaks order - if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info?.Job?.Prefab != JobPrefab.Get("mechanic")) && + if (shortcutNodes.Count < maxShortcutNodeCount && (Character.Controlled == null || Character.Controlled.Info?.Job?.Prefab != JobPrefab.Get("mechanic")) && (Order.GetPrefab("reportbreach") is Order reportBreach && ActiveOrders.Any(o => o.First.Prefab == reportBreach))) { shortcutNodes.Add( @@ -2018,7 +2103,7 @@ namespace Barotrauma // If player is not an engineer AND broken devices have been reported // --> Create shortcut node for Repair Damaged Systems order - if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info?.Job?.Prefab != JobPrefab.Get("engineer")) && + if (shortcutNodes.Count < maxShortcutNodeCount && (Character.Controlled == null || Character.Controlled.Info?.Job?.Prefab != JobPrefab.Get("engineer")) && (Order.GetPrefab("reportbrokendevices") is Order reportBrokenDevices && ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices))) { shortcutNodes.Add( @@ -2027,13 +2112,13 @@ namespace Barotrauma // If fire is reported // --> Create shortcut node for Extinguish Fires order - if (shortcutNodes.Count < maxShorcutNodeCount && ActiveOrders.Any(o=> o.First.Prefab == Order.GetPrefab("reportfire"))) + if (shortcutNodes.Count < maxShortcutNodeCount && ActiveOrders.Any(o=> o.First.Prefab == Order.GetPrefab("reportfire"))) { shortcutNodes.Add( CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("extinguishfires"), -1)); } - if (shortcutNodes.Count < maxShorcutNodeCount && characterContext?.Info?.Job?.Prefab?.AppropriateOrders != null) + if (shortcutNodes.Count < maxShortcutNodeCount && characterContext?.Info?.Job?.Prefab?.AppropriateOrders != null) { foreach (string orderIdentifier in characterContext.Info.Job.Prefab.AppropriateOrders) { @@ -2046,7 +2131,7 @@ namespace Barotrauma { shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, orderPrefab, -1)); } - if (shortcutNodes.Count >= maxShorcutNodeCount) { break; } + if (shortcutNodes.Count >= maxShortcutNodeCount) { break; } } } } @@ -2063,7 +2148,7 @@ namespace Barotrauma c.PressedColor = c.Color; c.SelectedColor = c.Color; } - shortcutCenterNode.RectTransform.MoveOverTime(shorcutCenterNodeOffset, CommandNodeAnimDuration); + shortcutCenterNode.RectTransform.MoveOverTime(shortcutCenterNodeOffset, CommandNodeAnimDuration); var nodeCountForCalculations = shortcutNodes.Count * 2 + 2; var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, 0.75f * nodeDistance, nodeCountForCalculations); @@ -2071,7 +2156,7 @@ namespace Barotrauma for (int i = 0; i < shortcutNodes.Count; i++) { shortcutNodes[i].RectTransform.Parent = commandFrame.RectTransform; - shortcutNodes[i].RectTransform.MoveOverTime(shorcutCenterNodeOffset + offsets[firstOffsetIndex - i].ToPoint(), CommandNodeAnimDuration); + shortcutNodes[i].RectTransform.MoveOverTime(shortcutCenterNodeOffset + offsets[firstOffsetIndex - i].ToPoint(), CommandNodeAnimDuration); } } @@ -2100,6 +2185,8 @@ namespace Barotrauma { if (contextualOrders.None()) { + string orderIdentifier; + // Check if targeting an item or a hull if (itemContext != null && !itemContext.NonInteractable) { @@ -2114,7 +2201,7 @@ namespace Barotrauma } // If targeting a periscope connected to a turret, show the 'operateweapons' order - var orderIdentifier = "operateweapons"; + orderIdentifier = "operateweapons"; var operateWeaponsPrefab = Order.GetPrefab(orderIdentifier); if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && itemContext.Components.Any(c => c is Controller)) { @@ -2123,9 +2210,9 @@ namespace Barotrauma if (turret != null) { contextualOrders.Add(new Order(operateWeaponsPrefab, turret.Item, turret, Character.Controlled)); } } - // If targeting a repairable item, show the 'repairsystems' order + // If targeting a repairable item with condition below the repair threshold, show the 'repairsystems' order orderIdentifier = "repairsystems"; - if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && itemContext.Repairables.Any()) + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && itemContext.Repairables.Any(r => itemContext.ConditionPercentage < r.RepairThreshold)) { contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), itemContext, null, Character.Controlled)); if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical")))) @@ -2138,13 +2225,6 @@ namespace Barotrauma } } - // Always show the 'wait' order if there are other crew members alive - orderIdentifier = "wait"; - if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && characters.Any(c => c != Character.Controlled)) - { - contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), itemContext, null, Character.Controlled)); - } - // Remove the 'pumpwater' order if the target pump is auto-controlled (as it will immediately overwrite the work done by the bot) orderIdentifier = "pumpwater"; if (contextualOrders.FirstOrDefault(o => o.Identifier.Equals(orderIdentifier)) is Order o && @@ -2152,20 +2232,34 @@ namespace Barotrauma { if (pump.IsAutoControlled) { contextualOrders.Remove(o); } } + + if (contextualOrders.None()) + { + // If there are other crew members alive and there are no other contextual orders available, show the 'wait' order + orderIdentifier = "wait"; + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && characters.Any(c => c != Character.Controlled)) + { + contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), itemContext, null, Character.Controlled)); + } + } } else if(hullContext != null) { contextualOrders.Add(new Order(Order.GetPrefab("fixleaks"), hullContext, null, Character.Controlled)); } - // Show the 'follow' and 'dismissed' orders if there are other crew members alive if (characters.Any(c => c != Character.Controlled)) { - var orderIdentifier = "follow"; - if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier))) + // Show 'follow' order only when there are no orders of other categories available + if (contextualOrders.None(o => o.Category != OrderCategory.Movement)) { - contextualOrders.Add(Order.GetPrefab(orderIdentifier)); + orderIdentifier = "follow"; + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier))) + { + contextualOrders.Add(Order.GetPrefab(orderIdentifier)); + } } + // Show 'dismissed' order only when there are crew members with active orders orderIdentifier = "dismissed"; if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && @@ -2192,7 +2286,7 @@ namespace Barotrauma if (Order.PrefabList.Any(o => item.HasTag(o.ItemIdentifiers))) { return true; } if (Order.PrefabList.Any(o => o.ItemComponentType != null && item.Components.Any(c => c?.GetType() == o.ItemComponentType))) { return true; } - if (item.Repairables.Any()) { return true; } + if (item.Repairables.Any(r => item.ConditionPercentage < r.RepairThreshold)) { return true; } var operateWeaponsPrefab = Order.GetPrefab("operateweapons"); return item.Components.Any(c => c is Controller) && (item.GetConnectedComponents().Any(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier)) || @@ -2209,8 +2303,30 @@ namespace Barotrauma node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); - if (checkIfOrderCanBeHeard && !disableNode) { disableNode = !CanSomeoneHearCharacter(); } - var mustSetOptionOrTarget = order.HasOptions || (order.MustSetTarget && itemContext == null); + if (checkIfOrderCanBeHeard && !disableNode) + { + disableNode = !CanSomeoneHearCharacter(); + } + + var mustSetOptionOrTarget = order.HasOptions; + Item orderTargetEntity = null; + + // If the order doesn't have options, but must set a target, + // we have to check if there's only one possible target available + // so we know to directly target that with the order + if (!mustSetOptionOrTarget && order.MustSetTarget && itemContext == null) + { + var matchingItems = order.GetMatchingItems(GetTargetSubmarine(), true); + if (matchingItems.Count > 1) + { + mustSetOptionOrTarget = true; + } + else + { + orderTargetEntity = matchingItems.FirstOrDefault(); + } + } + node.OnClicked = (button, userData) => { if (disableNode || !CanIssueOrders) { return false; } @@ -2224,7 +2340,11 @@ namespace Barotrauma NavigateForward(button, userData); } else - { + { + if (orderTargetEntity != null) + { + o = new Order(o.Prefab, orderTargetEntity, orderTargetEntity.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType), orderGiver: order.OrderGiver); + } SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o), o, null, Character.Controlled); DisableCommandUI(); } @@ -2382,6 +2502,10 @@ namespace Barotrauma Sprite icon = null; order.MinimapIcons?.TryGetValue(item.Prefab.Identifier, out icon); + if (item.Prefab.MinimapIcon != null) + { + icon = item.Prefab.MinimapIcon; + } var colorMultiplier = characters.Any(c => c.CurrentOrder != null && c.CurrentOrder.Identifier == userData.Item1.Identifier && c.CurrentOrder.TargetEntity == userData.Item1.TargetEntity) ? 0.5f : 1f; @@ -2601,7 +2725,7 @@ namespace Barotrauma var availableNodePositions = 20; var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, 2.7f * this.nodeDistance, availableNodePositions, firstAngle: MathHelper.ToRadians(-90f - ((extraOptionCharacters.Count - 1) * 0.5f * (360f / availableNodePositions)))); - for (int i = 0; i < extraOptionCharacters.Count; i++) + for (int i = 0; i < extraOptionCharacters.Count && i < availableNodePositions; i++) { CreateAssignmentNode(userData as Tuple, extraOptionCharacters[i], offsets[i].ToPoint(), -1, nameLabelScale: 1.15f); } @@ -2786,7 +2910,7 @@ namespace Barotrauma { bearing = GetBearing( centerNode.RectTransform.AnimTargetPos.ToVector2(), - shorcutCenterNodeOffset.ToVector2()); + shortcutCenterNodeOffset.ToVector2()); } return nodeCount % 2 > 0 ? MathHelper.ToRadians(bearing + 360.0f / nodeCount / 2) : diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 71de2e8f2..ef2571f4b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -1,4 +1,5 @@ using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -30,11 +31,6 @@ namespace Barotrauma protected set; } - public override bool Paused - { - get { return ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition"); } - } - private bool showCampaignUI; private bool wasChatBoxOpen; public bool ShowCampaignUI @@ -165,7 +161,6 @@ namespace Barotrauma } break; case TransitionType.LeaveLocation: - // not sure why this can happen at an outpost but it apparently can in multiplayer buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; break; @@ -195,14 +190,26 @@ namespace Barotrauma if (endRoundButton.Visible) { + if (!AllowedToEndRound()) { buttonText = TextManager.Get("map"); } endRoundButton.Text = ToolBox.LimitString(buttonText, endRoundButton.Font, endRoundButton.Rect.Width - 5); if (endRoundButton.Text != buttonText) { endRoundButton.ToolTip = buttonText; } - endRoundButton.Enabled = AllowedToEndRound(); + if (Character.Controlled?.ViewTarget is Item item) + { + Turret turret = item.GetComponent(); + endRoundButton.RectTransform.ScreenSpaceOffset = turret == null ? Point.Zero : new Point(0, (int)(turret.UIElementHeight * 1.25f)); + } + else if (Character.Controlled?.CharacterHealth?.SuicideButton?.Visible ?? false) + { + endRoundButton.RectTransform.ScreenSpaceOffset = new Point(0, Character.Controlled.CharacterHealth.SuicideButton.Rect.Height); + } + else + { + endRoundButton.RectTransform.ScreenSpaceOffset = Point.Zero; + } } - endRoundButton.DrawManually(spriteBatch); } @@ -216,7 +223,14 @@ namespace Barotrauma { await Task.Yield(); Rand.ThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId; - GameMain.GameSession.StartRound(newLevel, mirrorLevel: mirror); + try + { + GameMain.GameSession.StartRound(newLevel, mirrorLevel: mirror); + } + catch (Exception e) + { + roundSummaryScreen.LoadException = e; + } Rand.ThreadId = 0; }); TaskPool.Add("AsyncCampaignStartRound", loadTask, (t) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 9f509c46a..1ada97e3b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -3,7 +3,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -13,6 +13,11 @@ namespace Barotrauma { public bool SuppressStateSending = false; + public override bool Paused + { + get { return ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition"); } + } + private UInt16 pendingSaveID = 1; public UInt16 PendingSaveID { @@ -100,7 +105,7 @@ namespace Barotrauma }; int buttonHeight = (int)(GUI.Scale * 40); - int buttonWidth = GUI.IntScale(200); + int buttonWidth = GUI.IntScale(450); endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle((GameMain.GraphicsWidth / 2) - (buttonWidth / 2), HUDLayoutSettings.ButtonAreaTop.Center.Y - (buttonHeight / 2), buttonWidth, buttonHeight), GUICanvas.Instance), TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") @@ -209,6 +214,7 @@ namespace Barotrauma Character.Controlled = null; prevControlled.ClearInputs(); } + GameMain.GameScreen.Cam.Freeze = true; overlayTextColor = Color.Lerp(Color.Transparent, Color.White, (timer - 1.0f) / fadeInDuration); timer = Math.Min(timer + CoroutineManager.DeltaTime, textDuration); yield return CoroutineStatus.Running; @@ -357,7 +363,7 @@ namespace Barotrauma base.Update(deltaTime); - if (PlayerInput.RightButtonClicked() || + if (PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) { ShowCampaignUI = false; @@ -403,6 +409,14 @@ namespace Barotrauma if (CampaignUI == null) { InitCampaignUI(); } } + else + { + var transitionType = GetAvailableTransition(out _, out _); + if (transitionType == TransitionType.None && CampaignUI?.SelectedTab == InteractionType.Map) + { + ShowCampaignUI = false; + } + } } public override void End(TransitionType transitionType = TransitionType.None) { @@ -528,6 +542,7 @@ namespace Barotrauma UInt16 currentLocIndex = msg.ReadUInt16(); UInt16 selectedLocIndex = msg.ReadUInt16(); byte selectedMissionIndex = msg.ReadByte(); + bool allowDebugTeleport = msg.ReadBoolean(); float? reputation = null; if (msg.ReadBoolean()) { reputation = msg.ReadSingle(); } @@ -640,6 +655,7 @@ namespace Barotrauma campaign.Map.SetLocation(currentLocIndex == UInt16.MaxValue ? -1 : currentLocIndex); campaign.Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex); campaign.Map.SelectMission(selectedMissionIndex); + campaign.Map.AllowDebugTeleport = allowDebugTeleport; campaign.CargoManager.SetItemsInBuyCrate(buyCrateItems); campaign.CargoManager.SetPurchasedItems(purchasedItems); campaign.CargoManager.SetSoldItems(soldItems); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 7662c9ebf..0df8733dc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -11,6 +11,35 @@ namespace Barotrauma { class SinglePlayerCampaign : CampaignMode { + public override bool Paused + { + get { return ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition") || ShowCampaignUI && CampaignUI.SelectedTab == InteractionType.Map; } + } + + public override void UpdateWhilePaused(float deltaTime) + { + if (CoroutineManager.IsCoroutineRunning("LevelTransition") || CoroutineManager.IsCoroutineRunning("SubmarineTransition") || gameOver) { return; } + + if (PlayerInput.RightButtonClicked() || + PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) + { + ShowCampaignUI = false; + if (GUIMessageBox.VisibleBox?.UserData is RoundSummary roundSummary && + roundSummary.ContinueButton != null && + roundSummary.ContinueButton.Visible) + { + GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); + } + } + + if (CrewManager.ChatBox != null) + { + CrewManager.ChatBox.Update(deltaTime); + } + + CrewManager.UpdateReports(); + } + private float endTimer; private bool savedOnStart; @@ -124,7 +153,7 @@ namespace Barotrauma private void InitUI() { int buttonHeight = (int)(GUI.Scale * 40); - int buttonWidth = GUI.IntScale(200); + int buttonWidth = GUI.IntScale(450); endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle((GameMain.GraphicsWidth / 2) - (buttonWidth / 2), HUDLayoutSettings.ButtonAreaTop.Center.Y - (buttonHeight / 2), buttonWidth, buttonHeight), GUICanvas.Instance), TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") @@ -357,10 +386,11 @@ namespace Barotrauma break; case TransitionType.ProgressToNextLocation: Map.MoveToNextLocation(); - Map.ProgressWorld(); break; } + Map.ProgressWorld(transitionType, (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime)); + var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, transitionType == TransitionType.LeaveLocation ? Alignment.BottomCenter : Alignment.Center, fadeOut: false, @@ -383,6 +413,8 @@ namespace Barotrauma //-------------------------------------- + bool save = false; + if (success) { if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub)) @@ -398,21 +430,33 @@ namespace Barotrauma } GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + + if (PendingSubmarineSwitch != null) + { + SubmarineInfo previousSub = GameMain.GameSession.SubmarineInfo; + GameMain.GameSession.SubmarineInfo = PendingSubmarineSwitch; + PendingSubmarineSwitch = null; + + for (int i = 0; i < GameMain.GameSession.OwnedSubmarines.Count; i++) + { + if (GameMain.GameSession.OwnedSubmarines[i].Name == previousSub.Name) + { + GameMain.GameSession.OwnedSubmarines[i] = previousSub; + break; + } + } + } + SaveUtil.SaveGame(GameMain.GameSession.SavePath); } else { + PendingSubmarineSwitch = null; EnableRoundSummaryGameOverState(); } //-------------------------------------- - if (PendingSubmarineSwitch != null) - { - GameMain.GameSession.SubmarineInfo = PendingSubmarineSwitch; - PendingSubmarineSwitch = null; - } - SelectSummaryScreen(roundSummary, newLevel, mirror, () => { GameMain.GameScreen.Select(); @@ -473,7 +517,7 @@ namespace Barotrauma base.Update(deltaTime); - if (PlayerInput.RightButtonClicked() || + if (PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) { ShowCampaignUI = false; @@ -581,7 +625,7 @@ namespace Barotrauma } else if (transitionType == TransitionType.ProgressToNextEmptyLocation) { - Map.SetLocation(Map.Locations.IndexOf(Level.Loaded.EndLocation)); + Map.SetLocation(Map.Locations.IndexOf(Level.Loaded.EndLocation ?? Map.CurrentLocation)); } var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs index 27aeb3551..795c18836 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs @@ -1,5 +1,9 @@ using Microsoft.Xna.Framework; using System; +using System.Collections.Generic; +using System.Linq; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; namespace Barotrauma { @@ -7,6 +11,17 @@ namespace Barotrauma { public Action OnRoundEnd; + public bool SpawnOutpost; + + public OutpostGenerationParams OutpostParams; + public LocationType OutpostType; + + public EventPrefab TriggeredEvent; + + private List scriptedEvent; + + private GUIButton createEventButton; + public TestGameMode(GameModePreset preset) : base(preset) { foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) @@ -24,11 +39,96 @@ namespace Barotrauma base.Start(); CrewManager.InitSinglePlayerRound(); + + if (SpawnOutpost) + { + GenerateOutpost(Submarine.MainSub); + } + + if (TriggeredEvent != null) + { + scriptedEvent = new List { TriggeredEvent.CreateInstance() }; + GameMain.GameSession.EventManager.PinnedEvent = scriptedEvent.Last(); + + createEventButton = new GUIButton(new RectTransform(new Point(128, 64), GUI.Canvas, Anchor.TopCenter) { ScreenSpaceOffset = new Point(0, 32) }, TextManager.Get("create")) + { + OnClicked = delegate + { + scriptedEvent.Add(TriggeredEvent.CreateInstance()); + GameMain.GameSession.EventManager.PinnedEvent = scriptedEvent.Last(); + return true; + } + }; + } } - + + public override void AddToGUIUpdateList() + { + base.AddToGUIUpdateList(); + createEventButton?.AddToGUIUpdateList(); + } + public override void End(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) { + GameMain.GameSession.EventManager.PinnedEvent = null; OnRoundEnd?.Invoke(); } + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + + if (scriptedEvent != null) + { + foreach (Event sEvent in scriptedEvent.Where(sEvent => !sEvent.IsFinished)) + { + sEvent.Update(deltaTime); + } + } + } + + private void GenerateOutpost(Submarine submarine) + { + Submarine outpost = OutpostGenerator.Generate(OutpostParams ?? OutpostGenerationParams.Params.GetRandom(), OutpostType ?? LocationType.List.GetRandom()); + outpost.SetPosition(Vector2.Zero); + + float closestDistance = 0.0f; + DockingPort myPort = null, outPostPort = null; + foreach (DockingPort port in DockingPort.List) + { + if (port.IsHorizontal || port.Docked) { continue; } + if (port.Item.Submarine == outpost) + { + outPostPort = port; + continue; + } + if (port.Item.Submarine != submarine) { continue; } + + //the submarine port has to be at the top of the sub + if (port.Item.WorldPosition.Y < submarine.WorldPosition.Y) { continue; } + + float dist = Vector2.DistanceSquared(port.Item.WorldPosition, outpost.WorldPosition); + if ((myPort == null || dist < closestDistance || port.MainDockingPort) && !(myPort?.MainDockingPort ?? false)) + { + myPort = port; + closestDistance = dist; + } + } + + if (myPort != null && outPostPort != null) + { + Vector2 portDiff = myPort.Item.WorldPosition - submarine.WorldPosition; + Vector2 spawnPos = (outPostPort.Item.WorldPosition - portDiff) - Vector2.UnitY * outPostPort.DockedDistance; + + submarine.SetPosition(spawnPos); + myPort.Dock(outPostPort); + myPort.Lock(true); + } + + if (Character.Controlled != null) + { + Character.Controlled.TeleportTo(outpost.GetWaypoints(false).GetRandom(point => point.SpawnType == SpawnType.Human).WorldPosition); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs index ebfda0359..67077870c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs @@ -293,7 +293,9 @@ namespace Barotrauma.Tutorials // Room 4 do { yield return null; } while (!officer_somethingBigSensor.MotionDetected); - TriggerTutorialSegment(3); // Arm railgun + GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Officer.Radio.SomethingBig"), ChatMessageType.Radio, null); + yield return new WaitForSeconds(2f, false); + TriggerTutorialSegment(3); // Arm coilgun do { SetHighlight(officer_coilgunLoader.Item, officer_coilgunLoader.Inventory.Items[0] == null || officer_coilgunLoader.Inventory.Items[0].Condition == 0); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index ad7843fc9..135e61b32 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -1,4 +1,5 @@ using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Globalization; @@ -373,15 +374,6 @@ namespace Barotrauma } } - if (!(gameSession.GameMode is CampaignMode)) - { - var shadow = new GUIFrame(new RectTransform(new Point((int)(totalWidth * 1.2f), GameMain.GraphicsHeight * 2), background.RectTransform, Anchor.Center), style: "OuterGlow") - { - Color = Color.Black - }; - shadow.RectTransform.SetAsFirstChild(); - } - Frame = background; return background; } @@ -511,7 +503,7 @@ namespace Barotrauma Character character = characterInfo.Character; if (character == null || character.IsDead) { - if (characterInfo.IsNewHire) + if (character == null && characterInfo.IsNewHire) { statusText = TextManager.Get("CampaignCrew.NewHire"); statusColor = GUI.Style.Blue; @@ -616,21 +608,8 @@ namespace Barotrauma sliderHolder.RectTransform.MaxSize = new Point(int.MaxValue, GUI.IntScale(25.0f)); factionTextContent.Recalculate(); - new GUICustomComponent(new RectTransform(new Vector2(0.8f, 1.0f), sliderHolder.RectTransform), onDraw: (sb, customComponent) => - { - GUI.DrawRectangle(sb, customComponent.Rect, GUI.Style.ColorInventoryBackground, isFilled: true); - if (normalizedReputation < 0.5f) - { - int barWidth = (int)((0.5f - normalizedReputation) * customComponent.Rect.Width); - GUI.DrawRectangle(sb, new Rectangle(customComponent.Rect.Center.X - barWidth, customComponent.Rect.Y, barWidth, customComponent.Rect.Height), GUI.Style.Red, isFilled: true); - } - else if (normalizedReputation > 0.5f) - { - int barWidth = (int)((normalizedReputation - 0.5f) * customComponent.Rect.Width); - GUI.DrawRectangle(sb, new Rectangle(customComponent.Rect.Center.X, customComponent.Rect.Y, barWidth, customComponent.Rect.Height), GUI.Style.Green, isFilled: true); - } - GUI.DrawLine(sb, new Vector2(customComponent.Rect.Center.X, customComponent.Rect.Y - 2), new Vector2(customComponent.Rect.Center.X, customComponent.Rect.Bottom + 2), factionDescription.TextColor, width: 1); - }); + new GUICustomComponent(new RectTransform(new Vector2(0.8f, 1.0f), sliderHolder.RectTransform), + onDraw: (sb, customComponent) => DrawReputationBar(sb, customComponent.Rect, normalizedReputation)); string reputationText = ((int)Math.Round(reputation)).ToString(); int reputationChange = (int)Math.Round( reputation - initialReputation); @@ -650,5 +629,21 @@ namespace Barotrauma textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont); } } + + public static void DrawReputationBar(SpriteBatch sb, Rectangle rect, float normalizedReputation) + { + GUI.DrawRectangle(sb, rect, GUI.Style.ColorInventoryBackground, isFilled: true); + if (normalizedReputation < 0.5f) + { + int barWidth = (int)((0.5f - normalizedReputation) * rect.Width); + GUI.DrawRectangle(sb, new Rectangle(rect.Center.X - barWidth, rect.Y, barWidth, rect.Height), GUI.Style.Red, isFilled: true); + } + else if (normalizedReputation > 0.5f) + { + int barWidth = (int)((normalizedReputation - 0.5f) * rect.Width); + GUI.DrawRectangle(sb, new Rectangle(rect.Center.X, rect.Y, barWidth, rect.Height), GUI.Style.Green, isFilled: true); + } + GUI.DrawLine(sb, new Vector2(rect.Center.X, rect.Y - 2), new Vector2(rect.Center.X, rect.Bottom + 2), GUI.Style.TextColor); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index bb1f6e4af..4e7cf35f6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -65,6 +65,10 @@ namespace Barotrauma keyMapping[(int)InputType.Voice] = new KeyOrMouse(Keys.V); keyMapping[(int)InputType.LocalVoice] = new KeyOrMouse(Keys.B); keyMapping[(int)InputType.Command] = new KeyOrMouse(MouseButton.MiddleMouse); +#if DEBUG + keyMapping[(int)InputType.PreviousFireMode] = new KeyOrMouse(MouseButton.MouseWheelDown); + keyMapping[(int)InputType.NextFireMode] = new KeyOrMouse(MouseButton.MouseWheelUp); +#endif if (Language == "French") { @@ -314,7 +318,7 @@ namespace Barotrauma var corePackageDropdown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), leftPanel.RectTransform)) { - ButtonEnabled = ContentPackage.List.Count(cp => cp.CorePackage) > 1 + ButtonEnabled = ContentPackage.CorePackages.Count > 1 }; var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), leftPanel.RectTransform), isHorizontal: true) @@ -342,7 +346,7 @@ namespace Barotrauma ScrollBarVisible = true }; - foreach (ContentPackage contentPackage in ContentPackage.List.Where(cp => cp.CorePackage)) + foreach (ContentPackage contentPackage in ContentPackage.CorePackages) { var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), corePackageDropdown.ListBox.Content.RectTransform), style: "ListBoxElement") { @@ -358,7 +362,7 @@ namespace Barotrauma TextManager.GetWithVariables(contentPackage.GameVersion <= new Version(0, 0, 0, 0) ? "IncompatibleContentPackageUnknownVersion" : "IncompatibleContentPackage", new string[3] { "[packagename]", "[packageversion]", "[gameversion]" }, new string[3] { contentPackage.Name, contentPackage.GameVersion.ToString(), GameMain.Version.ToString() }); } - else if (contentPackage.CorePackage && !contentPackage.ContainsRequiredCorePackageFiles(out List missingContentTypes)) + else if (!contentPackage.ContainsRequiredCorePackageFiles(out List missingContentTypes)) { frame.UserData = null; text.TextColor = GUI.Style.Red * 0.6f; @@ -374,7 +378,7 @@ namespace Barotrauma "\n" + string.Join("\n", contentPackage.ErrorMessages); } - if (SelectedContentPackages.Contains(contentPackage)) + if (contentPackage == CurrentCorePackage) { corePackageDropdown.Select(corePackageDropdown.ListBox.Content.GetChildIndex(frame)); } @@ -382,10 +386,7 @@ namespace Barotrauma corePackageDropdown.OnSelected = SelectCorePackage; corePackageDropdown.ListBox.CanBeFocused = CanHotswapPackages(true); - foreach (ContentPackage contentPackage in ContentPackage.List - .Where(cp => !cp.CorePackage) - .OrderBy(cp => !SelectedContentPackages.Contains(cp)) - .ThenBy(cp => -SelectedContentPackages.IndexOf(cp))) + foreach (ContentPackage contentPackage in ContentPackage.RegularPackages) { var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, tickBoxScale.Y), contentPackageList.Content.RectTransform), style: "ListBoxElement") { @@ -407,7 +408,7 @@ namespace Barotrauma style: "GUITickBox") { UserData = contentPackage, - Selected = SelectedContentPackages.Contains(contentPackage), + Selected = EnabledRegularPackages.Contains(contentPackage), OnSelected = SelectContentPackage, Enabled = CanHotswapPackages(false) }; @@ -1595,10 +1596,10 @@ namespace Barotrauma if (userData is ContentPackage contentPackage) { - if (!SelectedContentPackages.Contains(contentPackage)) { return; } + if (!EnabledRegularPackages.Contains(contentPackage)) { return; } } - ReorderSelectedContentPackages(cp => -listBox.Content.GetChildIndex(listBox.Content.GetChildByUserData(cp))); + ContentPackage.SortContentPackages(cp => listBox.Content.GetChildIndex(listBox.Content.GetChildByUserData(cp)), true); UnsavedSettings = true; } @@ -1609,18 +1610,13 @@ namespace Barotrauma var contentPackage = tickBox.UserData as ContentPackage; - ContentPackage.List = ContentPackage.List - .OrderByDescending(p => p.CorePackage) - .ThenBy(cp => -contentPackageList.Content.GetChildIndex(contentPackageList.Content.GetChildByUserData(cp))) - .ToList(); - if (tickBox.Selected) { - SelectContentPackage(contentPackage); + EnableRegularPackage(contentPackage); } else { - DeselectContentPackage(contentPackage); + DisableRegularPackage(contentPackage); } UnsavedSettings = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 1859241ea..c54105e9e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -717,7 +717,7 @@ namespace Barotrauma { slot.QuickUseButtonToolTip = quickUseAction == QuickUseAction.None ? "" : TextManager.GetWithVariable("QuickUseAction." + quickUseAction.ToString(), "[equippeditem]", character.SelectedItems.FirstOrDefault(i => i != null)?.Name); - if (PlayerInput.PrimaryMouseButtonDown()) slot.EquipButtonState = GUIComponent.ComponentState.Pressed; + if (PlayerInput.PrimaryMouseButtonDown()) { slot.EquipButtonState = GUIComponent.ComponentState.Pressed; } if (PlayerInput.PrimaryMouseButtonClicked()) { QuickUseItem(item, allowEquip: true, allowInventorySwap: false, allowApplyTreatment: false); @@ -937,28 +937,52 @@ namespace Barotrauma switch (quickUseAction) { case QuickUseAction.Equip: - //attempt to put in a free slot first - for (int i = capacity - 1; i >= 0; i--) + if (string.IsNullOrEmpty(item.Prefab.EquipConfirmationText) || character != Character.Controlled) { - if (Items[i] != null) { continue; } - if (SlotTypes[i] == InvSlotType.Any || !item.AllowedSlots.Any(a => a.HasFlag(SlotTypes[i]))) { continue; } - success = TryPutItem(item, i, true, false, Character.Controlled, true); - if (success) { break; } + Equip(); + } + else + { + if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "equipconfirmation")) { return; } + var equipConfirmation = new GUIMessageBox(string.Empty, TextManager.Get(item.Prefab.EquipConfirmationText), + new string[] { TextManager.Get("yes"), TextManager.Get("no") }) + { + UserData = "equipconfirmation" + }; + equipConfirmation.Buttons[0].OnClicked = (btn, userdata) => + { + Equip(); + equipConfirmation.Close(); + return true; + }; + equipConfirmation.Buttons[1].OnClicked = equipConfirmation.Close; } - if (!success) + void Equip() { + //attempt to put in a free slot first for (int i = capacity - 1; i >= 0; i--) { + if (Items[i] != null) { continue; } if (SlotTypes[i] == InvSlotType.Any || !item.AllowedSlots.Any(a => a.HasFlag(SlotTypes[i]))) { continue; } - // something else already equipped in a hand slot, attempt to unequip it so items aren't unnecessarily swapped to it - if (Items[i] != null && Items[i].AllowedSlots.Contains(InvSlotType.Any) && (SlotTypes[i] == InvSlotType.LeftHand || SlotTypes[i] == InvSlotType.RightHand)) - { - TryPutItem(Items[i], Character.Controlled, new List() { InvSlotType.Any }, true); - } success = TryPutItem(item, i, true, false, Character.Controlled, true); if (success) { break; } } + + if (!success) + { + for (int i = capacity - 1; i >= 0; i--) + { + if (SlotTypes[i] == InvSlotType.Any || !item.AllowedSlots.Any(a => a.HasFlag(SlotTypes[i]))) { continue; } + // something else already equipped in a hand slot, attempt to unequip it so items aren't unnecessarily swapped to it + if (Items[i] != null && Items[i].AllowedSlots.Contains(InvSlotType.Any) && (SlotTypes[i] == InvSlotType.LeftHand || SlotTypes[i] == InvSlotType.RightHand)) + { + TryPutItem(Items[i], Character.Controlled, new List() { InvSlotType.Any }, true); + } + success = TryPutItem(item, i, true, false, Character.Controlled, true); + if (success) { break; } + } + } } break; case QuickUseAction.Unequip: @@ -1046,8 +1070,8 @@ namespace Barotrauma public void DrawOwn(SpriteBatch spriteBatch) { - if (!AccessibleWhenAlive && !character.IsDead) return; - if (slots == null) CreateSlots(); + if (!AccessibleWhenAlive && !character.IsDead) { return; } + if (slots == null) { CreateSlots(); } if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || prevUIScale != UIScale || @@ -1070,7 +1094,7 @@ namespace Barotrauma for (int i = 0; i < capacity; i++) { - if (HideSlot(i)) continue; + if (HideSlot(i)) { continue; } Rectangle interactRect = slots[i].InteractRect; interactRect.Location += slots[i].DrawOffset.ToPoint(); @@ -1086,9 +1110,13 @@ namespace Barotrauma } InventorySlot highlightedQuickUseSlot = null; + Rectangle inventoryArea = Rectangle.Empty; + for (int i = 0; i < capacity; i++) { - if (HideSlot(i)) continue; + if (HideSlot(i)) { continue; } + + inventoryArea = inventoryArea == Rectangle.Empty ? slots[i].InteractRect : Rectangle.Union(inventoryArea, slots[i].InteractRect); if (Items[i] == null || (draggingItem == Items[i] && !slots[i].InteractRect.Contains(PlayerInput.MousePosition)) || @@ -1102,7 +1130,7 @@ namespace Barotrauma } continue; } - if (draggingItem == Items[i] && !slots[i].IsHighlighted) continue; + if (draggingItem == Items[i] && !slots[i].IsHighlighted) { continue; } //draw hand icons if the item is equipped in a hand slot if (IsInLimbSlot(Items[i], InvSlotType.LeftHand)) @@ -1169,7 +1197,17 @@ namespace Barotrauma } } - if (highlightedQuickUseSlot != null && !string.IsNullOrEmpty(highlightedQuickUseSlot.QuickUseButtonToolTip)) + if (Locked) + { + GUI.DrawRectangle(spriteBatch, inventoryArea, new Color(30,30,30,100), isFilled: true); + var lockIcon = GUI.Style.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)) + { + GUIComponent.DrawToolTip(spriteBatch, TextManager.Get("handcuffed"), new Rectangle(inventoryArea.Center - new Point(inventoryArea.Height / 2), new Point(inventoryArea.Height))); + } + } + else if (highlightedQuickUseSlot != null && !string.IsNullOrEmpty(highlightedQuickUseSlot.QuickUseButtonToolTip)) { GUIComponent.DrawToolTip(spriteBatch, highlightedQuickUseSlot.QuickUseButtonToolTip, highlightedQuickUseSlot.EquipButtonRect); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index 3b6fcbe42..c10bbb748 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -68,29 +68,52 @@ namespace Barotrauma.Items.Components if (Window.Height > 0 && Window.Width > 0) { - rect.Height = -(int)(Window.Y * item.Scale); - - rect.Y += (int)(doorRect.Height * openState); - rect.Height = Math.Max(rect.Height - (rect.Y - doorRect.Y), 0); - rect.Y = Math.Min(doorRect.Y, rect.Y); - - if (convexHull2 != null) + if (IsHorizontal) { - Rectangle rect2 = doorRect; - rect2.Y += (int)(Window.Y * item.Scale - Window.Height * item.Scale); - - rect2.Y += (int)(doorRect.Height * openState); - rect2.Y = Math.Min(doorRect.Y, rect2.Y); - rect2.Height = rect2.Y - (doorRect.Y - (int)(doorRect.Height * (1.0f - openState))); - - if (rect2.Height == 0) + rect.Width = (int)(Window.X * item.Scale); + rect.X -= (int)(doorRect.Width * openState); + rect.Width = Math.Max(rect.Width - (doorRect.X - rect.X), 0); + rect.X = Math.Max(doorRect.X, rect.X); + if (convexHull2 != null) { - convexHull2.Enabled = false; + Rectangle rect2 = doorRect; + rect2.X += (int)(Window.Right * item.Scale); + rect2.X -= (int)(doorRect.Width * openState); + rect2.X = Math.Max(doorRect.X, rect2.X); + rect2.Width = doorRect.Right - (int)(doorRect.Width * openState) - rect2.X; + if (rect2.Width == 0) + { + convexHull2.Enabled = false; + } + else + { + convexHull2.Enabled = true; + convexHull2.SetVertices(GetConvexHullCorners(rect2)); + } } - else + } + else + { + rect.Height = -(int)(Window.Y * item.Scale); + rect.Y += (int)(doorRect.Height * openState); + rect.Height = Math.Max(rect.Height - (rect.Y - doorRect.Y), 0); + rect.Y = Math.Min(doorRect.Y, rect.Y); + if (convexHull2 != null) { - convexHull2.Enabled = true; - convexHull2.SetVertices(GetConvexHullCorners(rect2)); + Rectangle rect2 = doorRect; + rect2.Y += (int)(Window.Y * item.Scale - Window.Height * item.Scale); + rect2.Y += (int)(doorRect.Height * openState); + rect2.Y = Math.Min(doorRect.Y, rect2.Y); + rect2.Height = rect2.Y - (doorRect.Y - (int)(doorRect.Height * (1.0f - openState))); + if (rect2.Height == 0) + { + convexHull2.Enabled = false; + } + else + { + convexHull2.Enabled = true; + convexHull2.SetVertices(GetConvexHullCorners(rect2)); + } } } } @@ -251,8 +274,9 @@ namespace Barotrauma.Items.Components bool open = msg.ReadBoolean(); bool broken = msg.ReadBoolean(); bool forcedOpen = msg.ReadBoolean(); + bool isStuck = msg.ReadBoolean(); SetState(open, isNetworkMessage: true, sendNetworkMessage: false, forcedOpen: forcedOpen); - Stuck = msg.ReadRangedSingle(0.0f, 100.0f, 8); + stuck = msg.ReadRangedSingle(0.0f, 100.0f, 8); UInt16 lastUserID = msg.ReadUInt16(); Character user = lastUserID == 0 ? null : Entity.FindEntityByID(lastUserID) as Character; if (user != lastUser) @@ -260,7 +284,7 @@ namespace Barotrauma.Items.Components lastUser = user; toggleCooldownTimer = ToggleCoolDown; } - + this.isStuck = isStuck; if (isStuck) { OpenState = 0.0f; } IsBroken = broken; PredictedState = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs new file mode 100644 index 000000000..c83c16458 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs @@ -0,0 +1,360 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma.Items.Components +{ + internal partial class VineTile + { + public void Draw(SpriteBatch spriteBatch, Vector2 position, float depth, float leafDepth) + { + Vector2 pos = position + Position; + pos.Y = -pos.Y; + + VineSprite vineSprite = Parent.VineSprites[Type]; + Color color = Parent.Decayed ? Parent.DeadTint : Parent.VineTint; + + float layer1 = depth + 0.01f, // flowers + layer2 = depth + 0.02f, // decay atlas + layer3 = depth + 0.03f; // branches and leaves + + float scale = Parent.VineScale * VineStep; + + if (Parent.VineAtlas != null) + { + spriteBatch.Draw(Parent.VineAtlas.Texture, pos + offset, vineSprite.SourceRect, color, 0f, vineSprite.AbsoluteOrigin, scale, SpriteEffects.None, layer3); + } + + if (Parent.DecayAtlas != null) + { + spriteBatch.Draw(Parent.DecayAtlas.Texture, pos, vineSprite.SourceRect, HealthColor, 0f, vineSprite.AbsoluteOrigin, scale, SpriteEffects.None, layer2); + } + + if (FlowerConfig.Variant >= 0 && !Parent.Decayed) + { + Sprite flowerSprite = Parent.FlowerSprites[FlowerConfig.Variant]; + flowerSprite.Draw(spriteBatch, pos, Parent.FlowerTint, flowerSprite.Origin, scale: Parent.BaseFlowerScale * FlowerConfig.Scale * FlowerStep, rotate: FlowerConfig.Rotation, depth: layer1); + } + + if (LeafConfig.Variant >= 0) + { + Sprite leafSprite = Parent.LeafSprites[LeafConfig.Variant]; + leafSprite.Draw(spriteBatch, pos, Parent.Decayed ? Parent.DeadTint : Parent.LeafTint, leafSprite.Origin, scale: Parent.BaseLeafScale * LeafConfig.Scale * FlowerStep, rotate: LeafConfig.Rotation, depth: layer3 + leafDepth); + } + } + } + + internal class VineSprite + { + [Serialize("0,0,0,0", false)] + public Rectangle SourceRect { get; private set; } + + [Serialize("0.5,0.5", false)] + public Vector2 Origin { get; private set; } + + public Vector2 AbsoluteOrigin; + + public VineSprite(XElement element) + { + SerializableProperty.DeserializeProperties(this, element); + AbsoluteOrigin = new Vector2(SourceRect.Width * Origin.X, SourceRect.Height * Origin.Y); + } + } + + internal partial class Growable + { + public readonly Dictionary VineSprites = new Dictionary(); + public readonly List FlowerSprites = new List(); + public readonly List LeafSprites = new List(); + + public Sprite? VineAtlas, DecayAtlas; + + protected override void RemoveComponentSpecific() + { + VineAtlas?.Remove(); + DecayAtlas?.Remove(); + + foreach (Sprite sprite in FlowerSprites) + { + sprite.Remove(); + } + + foreach (Sprite sprite in LeafSprites) + { + sprite.Remove(); + } + } + + public void Draw(SpriteBatch spriteBatch, Planter planter, Vector2 offset, float depth) + { + const float zStep = 0.0001f; + float leafDepth = 0f; + + foreach (VineTile vine in Vines) + { + leafDepth += zStep; + vine.Draw(spriteBatch, planter.Item.DrawPosition + offset, depth, leafDepth); + } + + if (GameMain.DebugDraw) + { + foreach (Rectangle rect in FailedRectangles) + { + Rectangle wRect = rect; + wRect.Y = -wRect.Y; + wRect.Y -= wRect.Height; + GUI.DrawRectangle(spriteBatch, wRect, Color.Red); + } + } + } + + partial void LoadVines(XElement element) + { + string? vineAtlasPath = element.GetAttributeString("vineatlas", null); + string? decayAtlasPath = element.GetAttributeString("decayatlas", null); + + if (vineAtlasPath != null) + { + VineAtlas = new Sprite(vineAtlasPath, Rectangle.Empty); + } + + if (decayAtlasPath != null) + { + DecayAtlas = new Sprite(decayAtlasPath, Rectangle.Empty); + } + + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "vinesprite": + var tileType = subElement.GetAttributeString("type", null); + VineTileType type = Enum.Parse(tileType); + VineSprites.Add(type, new VineSprite(subElement)); + break; + case "flowersprite": + FlowerSprites.Add(new Sprite(subElement)); + break; + case "leafsprite": + LeafSprites.Add(new Sprite(subElement)); + break; + } + + flowerVariants = FlowerSprites.Count; + leafVariants = LeafSprites.Count; + } + + foreach (VineTileType type in Enum.GetValues(typeof(VineTileType))) + { + if (!VineSprites.ContainsKey(type)) + { + DebugConsole.ThrowError($"Vine sprite missing from {item.prefab.Identifier}: {type}"); + } + } + } + + private readonly object mutex = new object(); + + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + Health = msg.ReadRangedSingle(0, MaxHealth, 8); + int startOffset = msg.ReadRangedInteger(-1, MaximumVines); + if (startOffset > -1) + { + int vineCount = msg.ReadRangedInteger(0, VineChunkSize); + List tiles = new List(); + for (int i = 0; i < vineCount; i++) + { + VineTileType vineType = (VineTileType) msg.ReadRangedInteger(0b0000, 0b1111); + int flowerConfig = msg.ReadRangedInteger(0, 0xFFF); + int leafConfig = msg.ReadRangedInteger(0, 0xFFF); + sbyte posX = (sbyte) msg.ReadByte(), posY = (sbyte) msg.ReadByte(); + Vector2 pos = new Vector2(posX * VineTile.Size, posY * VineTile.Size); + + tiles.Add(new VineTile(this, pos, vineType, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafConfig))); + } + + // is this even needed?? + lock (mutex) + { + for (var i = 0; i < vineCount; i++) + { + int index = i + startOffset; + if (index >= Vines.Count) + { + Vines.Add(tiles[i]); + continue; + } + + VineTile oldVine = Vines[index]; + VineTile newVine = tiles[i]; + newVine.GrowthStep = oldVine.GrowthStep; + Vines[index] = newVine; + } + } + } + + UpdateBranchHealth(); + ResetPlanterSize(); + } + + private void ResetPlanterSize() + { + if (item.ParentInventory is ItemInventory itemInventory && itemInventory.Owner is Item parentItem) + { + if (parentItem.GetComponent() is { } planter) + { + planter.Item.ResetCachedVisibleSize(); + } + } + } + +#if DEBUG + private int seed; + + // Huge bowl of spaghetti + public void CreateDebugHUD(Planter planter, PlantSlot slot) + { + Vector2 relativeSize = new Vector2(0.3f, 0.6f); + GUIMessageBox msgBox = new GUIMessageBox(item.Name, "", new[] { TextManager.Get("applysettingsbutton") }, relativeSize); + + GUILayoutGroup content = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.85f), msgBox.Content.RectTransform)) { Stretch = true }; + GUINumberInput seedInput = CreateIntEntry("Random Seed", seed, content.RectTransform); + GUINumberInput vineTileSizeInput = CreateIntEntry("Vine Tile Size (Global)", VineTile.Size, content.RectTransform); + GUINumberInput[] leafScaleRangeInput = CreateMinMaxEntry("Leaf Scale Range (Global)", new []{ MinLeafScale, MaxLeafScale }, 1.5f, content.RectTransform); + GUINumberInput[] flowerScaleRangeInput = CreateMinMaxEntry("Flower Scale Range (Global)", new []{ MinFlowerScale, MaxFlowerScale }, 1.5f, content.RectTransform); + GUINumberInput vineCountInput = CreateIntEntry("Vine Count", MaximumVines, content.RectTransform); + GUINumberInput vineScaleInput = CreateFloatEntry("Vine Scale", VineScale, content.RectTransform); + GUINumberInput flowerInput = CreateIntEntry("Flower Quantity", FlowerQuantity, content.RectTransform); + GUINumberInput flowerScaleInput = CreateFloatEntry("Flower Scale", BaseFlowerScale, content.RectTransform); + GUINumberInput leafScaleInput = CreateFloatEntry("Leaf Scale", BaseLeafScale, content.RectTransform); + GUINumberInput leafProbabilityInput = CreateFloatEntry("Leaf Probability", LeafProbability, content.RectTransform); + GUINumberInput[] leafTintInputs = CreateMinMaxEntry("Leaf Tint", new []{ LeafTint.R / 255f, LeafTint.G / 255f, LeafTint.B / 255f }, 1.0f, content.RectTransform); + GUINumberInput[] flowerTintInputs = CreateMinMaxEntry("Flower Tint", new []{ FlowerTint.R / 255f, FlowerTint.G / 255f, FlowerTint.B / 255f }, 1.0f, content.RectTransform); + GUINumberInput[] vineTintInputs = CreateMinMaxEntry("Branch Tint", new []{ VineTint.R / 255f, VineTint.G / 255f, VineTint.B / 255f }, 1.0f, content.RectTransform); + + // Apply + msgBox.Buttons[0].OnClicked = (button, o) => + { + seed = seedInput.IntValue; + MaximumVines = vineCountInput.IntValue; + FlowerQuantity = flowerInput.IntValue; + BaseFlowerScale = flowerScaleInput.FloatValue; + VineScale = vineScaleInput.FloatValue; + BaseLeafScale = leafScaleInput.FloatValue; + LeafProbability = leafProbabilityInput.FloatValue; + VineTile.Size = vineTileSizeInput.IntValue; + + MinFlowerScale = flowerScaleRangeInput[0].FloatValue; + MaxFlowerScale = flowerScaleRangeInput[1].FloatValue; + MinLeafScale = leafScaleRangeInput[0].FloatValue; + MaxLeafScale = leafScaleRangeInput[1].FloatValue; + + LeafTint = new Color(leafTintInputs[0].FloatValue, leafTintInputs[1].FloatValue, leafTintInputs[2].FloatValue); + FlowerTint = new Color(flowerTintInputs[0].FloatValue, flowerTintInputs[1].FloatValue, flowerTintInputs[2].FloatValue); + VineTint = new Color(vineTintInputs[0].FloatValue, vineTintInputs[1].FloatValue, vineTintInputs[2].FloatValue); + + if (FlowerQuantity >= MaximumVines - 1) + { + vineCountInput.Flash(Color.Red); + flowerInput.Flash(Color.Red); + return false; + } + + if (MinFlowerScale > MaxFlowerScale) + { + foreach (GUINumberInput input in flowerScaleRangeInput) + { + input.Flash(Color.Red); + } + + return false; + } + + if (MinLeafScale > MaxLeafScale) + { + foreach (GUINumberInput input in leafScaleRangeInput) + { + input.Flash(Color.Red); + } + + return false; + } + + msgBox.Close(); + + Random random = new Random(seed); + Random flowerRandom = new Random(seed); + Vines.Clear(); + GenerateFlowerTiles(flowerRandom); + GenerateStem(); + + Decayed = false; + FullyGrown = false; + while (MaximumVines > Vines.Count) + { + if (!CanGrowMore()) + { + Decayed = true; + break; + } + + TryGenerateBranches(planter, slot, random, flowerRandom); + } + + if (!Decayed) + { + FullyGrown = true; + } + + foreach (VineTile vineTile in Vines) + { + vineTile.GrowthStep = 2.0f; + } + + return true; + }; + } + + private static GUINumberInput CreateIntEntry(string label, int defaultValue, RectTransform parent) + { + GUILayoutGroup layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.08f), parent), isHorizontal: true); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), layout.RectTransform), label); + GUINumberInput input = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), layout.RectTransform), GUINumberInput.NumberType.Int) { IntValue = defaultValue }; + return input; + } + + private static GUINumberInput CreateFloatEntry(string label, float defaultValue, RectTransform parent) + { + GUILayoutGroup layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.08f), parent), isHorizontal: true); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), layout.RectTransform), label); + GUINumberInput input = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), layout.RectTransform), GUINumberInput.NumberType.Float) { FloatValue = defaultValue, DecimalsToDisplay = 2 }; + return input; + } + + private static GUINumberInput[] CreateMinMaxEntry(string label, float[] values, float max, RectTransform parent, float min = 0f) + { + GUILayoutGroup layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.08f), parent), isHorizontal: true); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), layout.RectTransform), label); + GUINumberInput[] inputs = new GUINumberInput[values.Length]; + for (var i = 0; i < values.Length; i++) + { + float value = values[i]; + GUINumberInput input = new GUINumberInput(new RectTransform(new Vector2(0.5f / values.Length, 1f), layout.RectTransform), GUINumberInput.NumberType.Float) + { + FloatValue = value, DecimalsToDisplay = 2, + MinValueFloat = min, + MaxValueFloat = max + }; + inputs[i] = input; + } + + return inputs; + } +#endif + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index b5b9fd2f7..ebe4c63e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -12,11 +12,11 @@ namespace Barotrauma.Items.Components { partial class RangedWeapon : ItemComponent { - private Sprite crosshairSprite, crosshairPointerSprite; + protected Sprite crosshairSprite, crosshairPointerSprite; - private Vector2 crosshairPos, crosshairPointerPos; + protected Vector2 crosshairPos, crosshairPointerPos; - private float currentCrossHairScale, currentCrossHairPointerScale; + protected float currentCrossHairScale, currentCrossHairPointerScale; private readonly List particleEmitters = new List(); @@ -86,7 +86,6 @@ namespace Barotrauma.Items.Components public override void DrawHUD(SpriteBatch spriteBatch, Character character) { - if (crosshairSprite == null) { return; } if (character == null || !character.IsKeyDown(InputType.Aim)) { return; } //camera focused on some other item/device, don't draw the crosshair diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs new file mode 100644 index 000000000..620434040 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs @@ -0,0 +1,338 @@ +using Barotrauma.Networking; +using Barotrauma.Particles; +using FarseerPhysics; +using FarseerPhysics.Dynamics; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + + partial class Sprayer : RangedWeapon, IDrawableComponent + { +#if DEBUG + private Vector2 debugRayStartPos, debugRayEndPos; +#endif + + public Vector2 DrawSize + { + get { return Vector2.Zero; } + } + + private readonly List particleEmitters = new List(); + private Hull targetHull; + + private Vector2 rayStartWorldPosition; + + private Color color; + + partial void InitProjSpecific(XElement element) + { + currentCrossHairPointerScale = element.GetAttributeFloat("crosshairscale", 0.1f); + + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "particleemitter": + particleEmitters.Add(new ParticleEmitter(subElement)); + break; + } + } + } + + private readonly List targetSections = new List(); + + // 0 = 1x1, 1 = 2x2, 2 = 3x3 + private int spraySetting = 0; + private readonly Point[] sprayArray = new Point[8]; + + public override void UpdateHUD(Character character, float deltaTime, Camera cam) + { + if (character == null || !character.IsKeyDown(InputType.Aim)) return; + +#if DEBUG + if (PlayerInput.KeyHit(InputType.PreviousFireMode)) +#else + if (PlayerInput.MouseWheelDownClicked()) +#endif + { + + if (spraySetting > 0) + { + spraySetting--; + } + else + { + spraySetting = 2; + } + + targetSections.Clear(); + } + +#if DEBUG + if (PlayerInput.KeyHit(InputType.NextFireMode)) +#else + if (PlayerInput.MouseWheelUpClicked()) +#endif + { + if (spraySetting < 2) + { + spraySetting++; + } + else + { + spraySetting = 0; + } + + targetSections.Clear(); + } + + crosshairPointerPos = PlayerInput.MousePosition; + + Vector2 rayStart; + Vector2 sourcePos = character?.AnimController == null ? item.SimPosition : character.AnimController.AimSourceSimPos; + Vector2 barrelPos = item.SimPosition + TransformedBarrelPos; + //make sure there's no obstacles between the base of the item (or the shoulder of the character) and the end of the barrel + if (Submarine.PickBody(sourcePos, barrelPos, collisionCategory: Physics.CollisionItem | Physics.CollisionItemBlocking | Physics.CollisionWall) == null) + { + //no obstacles -> we start the raycast at the end of the barrel + rayStart = ConvertUnits.ToSimUnits(item.WorldPosition) + TransformedBarrelPos; + } + else + { + targetHull = null; + targetSections.Clear(); + return; + } + + Vector2 pos = character.CursorWorldPosition; + Vector2 rayEnd = ConvertUnits.ToSimUnits(pos); + rayStartWorldPosition = ConvertUnits.ToDisplayUnits(rayStart); + + if (Vector2.Distance(rayStartWorldPosition, pos) > Range) + { + targetHull = null; + targetSections.Clear(); + return; + } + +#if DEBUG + debugRayStartPos = ConvertUnits.ToDisplayUnits(rayStart); + debugRayEndPos = ConvertUnits.ToDisplayUnits(rayEnd); +#endif + + Submarine parentSub = character.Submarine ?? item.Submarine; + if (parentSub != null) + { + rayStart -= parentSub.SimPosition; + rayEnd -= parentSub.SimPosition; + } + + var obstacles = Submarine.PickBodies(rayStart, rayEnd, collisionCategory: Physics.CollisionItem | Physics.CollisionItemBlocking | Physics.CollisionWall); + foreach (var body in obstacles) + { + if (body.UserData is Item item) + { + var door = item.GetComponent(); + if (door != null && door.IsOpen || door.IsBroken) continue; + } + + targetHull = null; + targetSections.Clear(); + return; + } + + targetHull = Hull.GetCleanTarget(pos); + if (targetHull == null) + { + targetSections.Clear(); + return; + } + + BackgroundSection mousedOverSection = targetHull.GetBackgroundSection(pos); + + if (mousedOverSection == null) + { + targetSections.Clear(); + return; + } + + // No need to refresh + if (targetSections.Count > 0 && mousedOverSection == targetSections[0]) + { + return; + } + + targetSections.Clear(); + + targetSections.Add(mousedOverSection); + int mousedOverIndex = mousedOverSection.Index; + + // Start with 2x2 + if (spraySetting > 0) + { + sprayArray[0].X = mousedOverIndex + 1; + sprayArray[0].Y = mousedOverSection.RowIndex; + + sprayArray[1].X = mousedOverIndex + targetHull.xBackgroundMax; + sprayArray[1].Y = mousedOverSection.RowIndex + 1; + + sprayArray[2].X = sprayArray[1].X + 1; + sprayArray[2].Y = sprayArray[1].Y; + + for (int i = 0; i < 3; i++) + { + if (targetHull.DoesSectionMatch(sprayArray[i].X, sprayArray[i].Y)) + { + targetSections.Add(targetHull.BackgroundSections[sprayArray[i].X]); + } + } + + // Add more if it's 3x3 + if (spraySetting == 2) + { + sprayArray[3].X = mousedOverIndex - 1; + sprayArray[3].Y = mousedOverSection.RowIndex; + + sprayArray[4].X = sprayArray[1].X - 1; + sprayArray[4].Y = sprayArray[1].Y; + + sprayArray[5].X = sprayArray[3].X - targetHull.xBackgroundMax; + sprayArray[5].Y = sprayArray[3].Y - 1; + + sprayArray[6].X = sprayArray[5].X + 1; + sprayArray[6].Y = sprayArray[5].Y; + + sprayArray[7].X = sprayArray[6].X + 1; + sprayArray[7].Y = sprayArray[6].Y; + + for (int i = 3; i < sprayArray.Length; i++) + { + if (targetHull.DoesSectionMatch(sprayArray[i].X, sprayArray[i].Y)) + { + targetSections.Add(targetHull.BackgroundSections[sprayArray[i].X]); + } + } + } + } + } + + public override void DrawHUD(SpriteBatch spriteBatch, Character character) + { + if (character == null || !character.IsKeyDown(InputType.Aim)) { return; } + GUI.HideCursor = targetSections.Count > 0; + } + + public override bool Use(float deltaTime, Character character = null) + { + if (character == null) { return false; } + if (character == Character.Controlled) + { + if (targetSections.Count == 0) { return false; } + Spray(deltaTime); + 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; + } + } + + public void Spray(float deltaTime) + { + if (targetSections.Count == 0) { return; } + + Item liquidItem = liquidContainer?.Inventory.Items[0]; + 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) + { + for (int i = 0; i < targetSections.Count; i++) + { + targetHull.SetSectionColorOrStrength(targetSections[i], color, sizeAdjustedSprayStrength * deltaTime, true, false); + } + } + else + { + for (int i = 0; i < targetSections.Count; i++) + { + targetHull.CleanSection(targetSections[i], -sizeAdjustedSprayStrength * deltaTime, true); + } + } + + 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); + + foreach (ParticleEmitter particleEmitter in particleEmitters) + { + float particleAngle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); + float particleRange = particleEmitter.Prefab.VelocityMax * particleEmitter.Prefab.ParticlePrefab.LifeTime; + particleEmitter.Emit( + deltaTime, particleStartPos, + item.CurrentHull, particleAngle, particleEmitter.Prefab.CopyEntityAngle ? -particleAngle : 0, velocityMultiplier: dist / particleRange * 1.5f, + colorMultiplier: new Color(color.R, color.G, color.B, (byte)255)); + } + } + + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + { +#if DEBUG + if (GameMain.DebugDraw && Character.Controlled != null && Character.Controlled.IsKeyDown(InputType.Aim)) + { + GUI.DrawLine(spriteBatch, + new Vector2(debugRayStartPos.X, -debugRayStartPos.Y), + new Vector2(debugRayEndPos.X, -debugRayEndPos.Y), + Color.Yellow); + } +#endif + if (Character.Controlled == null || !Character.Controlled.HasEquippedItem(item) || !Character.Controlled.IsKeyDown(InputType.Aim) || targetHull == null || targetSections.Count == 0) return; + + Vector2 drawOffset = targetHull.Submarine == null ? Vector2.Zero : targetHull.Submarine.DrawPosition; + Point sectionSize = targetSections[0].Rect.Size; + Rectangle drawPositionRect = new Rectangle((int)(drawOffset.X + targetHull.Rect.X), (int)(drawOffset.Y + targetHull.Rect.Y), sectionSize.X, sectionSize.Y); + + if (crosshairSprite == null && crosshairPointerSprite == null) + { + for (int i = 0; i < targetSections.Count; i++) + { + GUI.DrawRectangle(spriteBatch, new Vector2(drawPositionRect.X + targetSections[i].Rect.X, -(drawPositionRect.Y + targetSections[i].Rect.Y)), new Vector2(sectionSize.X, sectionSize.Y), Color.White, false, 0.0f, 1); + } + } + else if (targetSections.Count > 0) + { + Vector2 drawPos = Vector2.Zero; + for (int i = 0; i < targetSections.Count; i++) + { + drawPos += new Vector2(drawPositionRect.X + targetSections[i].Rect.X + sectionSize.X / 2, -(drawPositionRect.Y + targetSections[i].Rect.Y - sectionSize.Y / 2)); + } + drawPos /= targetSections.Count; + crosshairSprite?.Draw(spriteBatch, drawPos, scale: sectionSize.X * 3 / crosshairSprite.size.X); + crosshairPointerSprite?.Draw(spriteBatch, drawPos, scale: sectionSize.X * (spraySetting + 1) / crosshairPointerSprite.size.X); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 7b8235322..516e6dfc4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -256,6 +256,7 @@ namespace Barotrauma.Items.Components loopingSoundChannel = loopingSound.RoundSound.Sound.Play( new Vector3(item.WorldPosition, 0.0f), 0.01f, + loopingSound.RoundSound.GetRandomFrequencyMultiplier(), SoundPlayer.ShouldMuffleSound(Character.Controlled, item.WorldPosition, loopingSound.Range, Character.Controlled?.CurrentHull)); loopingSoundChannel.Looping = true; //TODO: tweak @@ -326,7 +327,7 @@ namespace Barotrauma.Items.Components { float volume = GetSoundVolume(itemSound); if (volume <= 0.0001f) { return; } - var channel = SoundPlayer.PlaySound(itemSound.RoundSound.Sound, position, volume, itemSound.Range, item.CurrentHull); + var channel = SoundPlayer.PlaySound(itemSound.RoundSound.Sound, position, volume, itemSound.Range, itemSound.RoundSound.GetRandomFrequencyMultiplier(), item.CurrentHull); if (channel != null) { playingOneshotSoundChannels.Add(channel); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 274220734..f961eba32 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -172,6 +172,8 @@ namespace Barotrauma.Items.Components { Vector2 transformedItemPos = ItemPos * item.Scale; Vector2 transformedItemInterval = ItemInterval * item.Scale; + Vector2 transformedItemIntervalHorizontal = new Vector2(transformedItemInterval.X, 0.0f); + Vector2 transformedItemIntervalVertical = new Vector2(0.0f, transformedItemInterval.Y); if (item.body == null) { @@ -180,15 +182,26 @@ namespace Barotrauma.Items.Components transformedItemPos.X = -transformedItemPos.X; transformedItemPos.X += item.Rect.Width; transformedItemInterval.X = -transformedItemInterval.X; + transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X; } if (item.FlippedY) { transformedItemPos.Y = -transformedItemPos.Y; transformedItemPos.Y -= item.Rect.Height; transformedItemInterval.Y = -transformedItemInterval.Y; + transformedItemIntervalVertical.Y = -transformedItemIntervalVertical.Y; } transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y); if (item.Submarine != null) { transformedItemPos += item.Submarine.DrawPosition; } + + if (Math.Abs(item.Rotation) > 0.01f) + { + Matrix transform = Matrix.CreateRotationZ(MathHelper.ToRadians(-item.Rotation)); + transformedItemPos = Vector2.Transform(transformedItemPos - item.DrawPosition, transform) + item.DrawPosition; + transformedItemInterval = Vector2.Transform(transformedItemInterval, transform); + transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); + transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform); + } } else { @@ -197,9 +210,12 @@ namespace Barotrauma.Items.Components { transformedItemPos.X = -transformedItemPos.X; transformedItemInterval.X = -transformedItemInterval.X; + transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X; } + transformedItemPos = Vector2.Transform(transformedItemPos, transform); transformedItemInterval = Vector2.Transform(transformedItemInterval, transform); + transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); transformedItemPos += item.DrawPosition; } @@ -207,8 +223,14 @@ namespace Barotrauma.Items.Components Vector2 currentItemPos = transformedItemPos; SpriteEffects spriteEffects = SpriteEffects.None; - if ((item.body != null && item.body.Dir == -1) || item.FlippedX) { spriteEffects |= SpriteEffects.FlipHorizontally; } - if (item.FlippedY) { spriteEffects |= SpriteEffects.FlipVertically; } + 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; + } int i = 0; foreach (Item containedItem in Inventory.Items) @@ -233,7 +255,7 @@ namespace Barotrauma.Items.Components new Vector2(currentItemPos.X, -currentItemPos.Y), containedItem.GetSpriteColor(), origin, - -(containedItem.body == null ? 0.0f : containedItem.body.DrawRotation), + -(containedItem.body == null ? 0.0f : containedItem.body.DrawRotation + MathHelper.ToRadians(-item.Rotation)), containedItem.Scale, spriteEffects, depth: containedSpriteDepth); @@ -248,11 +270,11 @@ namespace Barotrauma.Items.Components if (Math.Abs(ItemInterval.X) > 0.001f && Math.Abs(ItemInterval.Y) > 0.001f) { //interval set on both axes -> use a grid layout - currentItemPos.X += transformedItemInterval.X; + currentItemPos += transformedItemIntervalHorizontal; if (i % ItemsPerRow == 0) { - currentItemPos.X = transformedItemPos.X; - currentItemPos.Y += transformedItemInterval.Y; + currentItemPos = transformedItemPos; + currentItemPos += transformedItemIntervalVertical * (i / ItemsPerRow); } } else @@ -264,6 +286,7 @@ namespace Barotrauma.Items.Components public override void UpdateHUD(Character character, float deltaTime, Camera cam) { + if (item.NonInteractable) { return; } if (Inventory.RectTransform != null) { guiCustomComponent.RectTransform.Parent = Inventory.RectTransform; @@ -272,7 +295,7 @@ namespace Barotrauma.Items.Components //if the item is in the character's inventory, no need to update the item's inventory //because the player can see it by hovering the cursor over the item guiCustomComponent.Visible = item.ParentInventory?.Owner != character && DrawInventory; - if (!guiCustomComponent.Visible) return; + if (!guiCustomComponent.Visible) { return; } Inventory.Update(deltaTime, cam); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index 75e2f6704..2dd59dedd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Text; @@ -225,5 +226,10 @@ namespace Barotrauma.Items.Components textBlock.TextOffset = drawPos - textBlock.Rect.Location.ToVector2() + new Vector2(scrollAmount + scrollPadding, 0.0f); textBlock.DrawManually(spriteBatch); } + + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + Text = msg.ReadString(); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 823cdd6ea..7c761645a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -49,8 +49,8 @@ namespace Barotrauma.Items.Components if (light.LightSprite != null && (item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn) { Vector2 origin = light.LightSprite.Origin; - if (light.LightSpriteEffect == SpriteEffects.FlipHorizontally) { origin.X = light.LightSprite.SourceRect.Width - origin.X; } - if (light.LightSpriteEffect == SpriteEffects.FlipVertically) { origin.Y = light.LightSprite.SourceRect.Height - origin.Y; } + if ((light.LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) { origin.X = light.LightSprite.SourceRect.Width - origin.X; } + if ((light.LightSpriteEffect & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically) { origin.Y = light.LightSprite.SourceRect.Height - origin.Y; } light.LightSprite.Draw(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), lightColor * lightBrightness, origin, -light.Rotation, item.Scale, light.LightSpriteEffect, item.SpriteDepth - 0.0001f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs index 6240e6ddf..0ce476ce6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs @@ -52,7 +52,9 @@ namespace Barotrauma.Items.Components RelativeOffset = new Vector2(0.05f, 0) }, TextManager.Get("PumpAutoControl", fallBackTag: "ReactorAutoControl"), font: GUI.SubHeadingFont, style: "IndicatorLightYellow") { - CanBeFocused = false + Selected = false, + Enabled = false, + ToolTip = TextManager.Get("AutoControlTip") }; powerIndicator.TextBlock.Wrap = autoControlIndicator.TextBlock.Wrap = true; powerIndicator.TextBlock.OverrideTextColor(GUI.Style.TextColor); @@ -75,6 +77,7 @@ namespace Barotrauma.Items.Components if (Math.Abs(newTargetForce - targetForce) < 0.01) { return false; } targetForce = newTargetForce; + User = Character.Controlled; if (GameMain.Client != null) { @@ -145,7 +148,7 @@ namespace Barotrauma.Items.Components propellerSprite.Draw(spriteBatch, (int)Math.Floor(spriteIndex), drawPos, Color.White, propellerSprite.Origin, 0.0f, Vector2.One); } - if (editing) + if (editing && !GUI.DisableHUD) { Vector2 drawPos = item.DrawPosition; drawPos += PropellerPos; @@ -164,11 +167,16 @@ namespace Barotrauma.Items.Components { if (correctionTimer > 0.0f) { - StartDelayedCorrection(type, msg.ExtractBits(5), sendingTime); + StartDelayedCorrection(type, msg.ExtractBits(5 + 16), sendingTime); return; } targetForce = msg.ReadRangedInteger(-10, 10) * 10.0f; + UInt16 userID = msg.ReadUInt16(); + if (userID != Entity.NullEntityID) + { + User = Entity.FindEntityByID(userID) as Character; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 3fb2886d0..8e036e5a0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -100,7 +100,7 @@ namespace Barotrauma.Items.Components OnSelected = (component, userdata) => { selectedItem = userdata as FabricationRecipe; - if (selectedItem != null) SelectItem(Character.Controlled, selectedItem); + if (selectedItem != null) { SelectItem(Character.Controlled, selectedItem); } return true; } }; @@ -292,7 +292,7 @@ namespace Barotrauma.Items.Components foreach (Item item in inputContainer.Inventory.Items) { if (item == null) { continue; } - missingItems.Remove(missingItems.FirstOrDefault(mi => mi.ItemPrefab == item.prefab)); + missingItems.Remove(missingItems.FirstOrDefault(mi => mi.ItemPrefabs.Contains(item.prefab))); } var availableIngredients = GetAvailableIngredients(); @@ -328,12 +328,12 @@ namespace Barotrauma.Items.Components if (slotIndex >= inputContainer.Capacity) { break; } - var itemIcon = requiredItem.ItemPrefab.InventoryIcon ?? requiredItem.ItemPrefab.sprite; + var itemIcon = requiredItem.ItemPrefabs.First().InventoryIcon ?? requiredItem.ItemPrefabs.First().sprite; Rectangle slotRect = inputContainer.Inventory.slots[slotIndex].Rect; itemIcon.Draw( spriteBatch, slotRect.Center.ToVector2(), - color: requiredItem.ItemPrefab.InventoryIconColor * 0.3f, + color: requiredItem.ItemPrefabs.First().InventoryIconColor * 0.3f, scale: Math.Min(slotRect.Width / itemIcon.size.X, slotRect.Height / itemIcon.size.Y)); if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f) @@ -346,14 +346,16 @@ namespace Barotrauma.Items.Components if (slotRect.Contains(PlayerInput.MousePosition)) { - string toolTipText = requiredItem.ItemPrefab.Name; + var suitableIngredients = requiredItem.ItemPrefabs.Select(ip => ip.Name); + string toolTipText = string.Join(", ", suitableIngredients.Count() > 3 ? suitableIngredients.SkipLast(suitableIngredients.Count() - 3) : suitableIngredients); + if (suitableIngredients.Count() > 3) { toolTipText += "..."; } if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f) { toolTipText += " " + (int)Math.Round(requiredItem.MinCondition * 100) + "%"; } - if (!string.IsNullOrEmpty(requiredItem.ItemPrefab.Description)) + if (!string.IsNullOrEmpty(requiredItem.ItemPrefabs.First().Description)) { - toolTipText += '\n' + requiredItem.ItemPrefab.Description; + toolTipText += '\n' + requiredItem.ItemPrefabs.First().Description; } tooltip = new Pair(slotRect, toolTipText); } @@ -378,10 +380,11 @@ namespace Barotrauma.Items.Components if (fabricatedItem != null) { + float clampedProgressState = Math.Clamp(progressState, 0f, 1f); GUI.DrawRectangle(spriteBatch, new Rectangle( - slotRect.X, slotRect.Y + (int)(slotRect.Height * (1.0f - progressState)), - slotRect.Width, (int)(slotRect.Height * progressState)), + slotRect.X, slotRect.Y + (int)(slotRect.Height * (1.0f - clampedProgressState)), + slotRect.Width, (int)(slotRect.Height * clampedProgressState)), GUI.Style.Green * 0.5f, isFilled: true); } @@ -429,8 +432,10 @@ namespace Barotrauma.Items.Components return true; } - private bool SelectItem(Character user, FabricationRecipe selectedItem) + private bool SelectItem(Character user, FabricationRecipe selectedItem, float? overrideRequiredTime = null) { + this.selectedItem = selectedItem; + selectedItemFrame.ClearChildren(); selectedItemReqsFrame.ClearChildren(); @@ -496,7 +501,8 @@ namespace Barotrauma.Items.Components float degreeOfSuccess = user == null ? 0.0f : FabricationDegreeOfSuccess(user, selectedItem.RequiredSkills); if (degreeOfSuccess > 0.5f) { degreeOfSuccess = 1.0f; } - float requiredTime = user == null ? selectedItem.RequiredTime : GetRequiredTime(selectedItem, user); + float requiredTime = overrideRequiredTime ?? + (user == null ? selectedItem.RequiredTime : GetRequiredTime(selectedItem, user)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), TextManager.Get("FabricatorRequiredTime") , textColor: ToolBox.GradientLerp(degreeOfSuccess, GUI.Style.Red, Color.Yellow, GUI.Style.Green), font: GUI.SubHeadingFont) @@ -605,7 +611,7 @@ namespace Barotrauma.Items.Components State = newState; timeUntilReady = newTimeUntilReady; - if (newState == FabricatorState.Stopped || itemIndex == -1 || user == null) + if (newState == FabricatorState.Stopped || itemIndex == -1) { CancelFabricating(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index 2632cc2cf..4b38ab997 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -81,7 +81,9 @@ namespace Barotrauma.Items.Components autoControlIndicator = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), rightArea.RectTransform, Anchor.TopLeft), TextManager.Get("PumpAutoControl", fallBackTag: "ReactorAutoControl"), font: GUI.SubHeadingFont, style: "IndicatorLightYellow") { - CanBeFocused = false + Selected = false, + Enabled = false, + ToolTip = TextManager.Get("AutoControlTip") }; autoControlIndicator.TextBlock.AutoScaleHorizontal = true; autoControlIndicator.TextBlock.OverrideTextColor(GUI.Style.TextColor); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index 2ddb5a88f..7aa373710 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs @@ -131,17 +131,23 @@ namespace Barotrauma.Items.Components criticalHeatWarning = new GUITickBox(new RectTransform(new Vector2(0.33f, 1.0f), topLeftArea.RectTransform) { MaxSize = maxIndicatorSize }, TextManager.Get("ReactorWarningCriticalTemp"), font: GUI.SubHeadingFont, style: "IndicatorLightRed") { - CanBeFocused = false + Selected = false, + Enabled = false, + ToolTip = TextManager.Get("ReactorHeatTip") }; lowTemperatureWarning = new GUITickBox(new RectTransform(new Vector2(0.33f, 1.0f), topLeftArea.RectTransform) { MaxSize = maxIndicatorSize }, TextManager.Get("ReactorWarningCriticalLowTemp"), font: GUI.SubHeadingFont, style: "IndicatorLightRed") { - CanBeFocused = false + Selected = false, + Enabled = false, + ToolTip = TextManager.Get("ReactorTempTip") }; criticalOutputWarning = new GUITickBox(new RectTransform(new Vector2(0.33f, 1.0f), topLeftArea.RectTransform) { MaxSize = maxIndicatorSize }, TextManager.Get("ReactorWarningCriticalOutput"), font: GUI.SubHeadingFont, style: "IndicatorLightRed") { - CanBeFocused = false + Selected = false, + Enabled = false, + ToolTip = TextManager.Get("ReactorOutputTip") }; List indicatorLights = new List() { criticalHeatWarning, lowTemperatureWarning, criticalOutputWarning }; indicatorLights.ForEach(l => l.TextBlock.OverrideTextColor(GUI.Style.TextColor)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index fa5065625..89ef58d14 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; -using Barotrauma.Extensions; namespace Barotrauma.Items.Components { @@ -20,7 +19,7 @@ namespace Barotrauma.Items.Components private PathFinder pathFinder; - private bool dynamicDockingIndicator = true; + private readonly bool dynamicDockingIndicator = true; private bool unsentChanges; private float networkUpdateTimer; @@ -378,7 +377,6 @@ namespace Barotrauma.Items.Components float edgeDist = Rand.Range(0.0f, 1.0f); Vector2 blipPos = trigger.WorldPosition + Rand.Vector(trigger.ColliderRadius * edgeDist); Vector2 blipVel = flow; - if (trigger.ForceFalloff) flow *= (1.0f - edgeDist); //go through other triggers in range and add the flows of the ones that the blip is inside foreach (KeyValuePair triggerFlow2 in levelTriggerFlows) @@ -387,7 +385,7 @@ namespace Barotrauma.Items.Components if (trigger2 != trigger && Vector2.DistanceSquared(blipPos, trigger2.WorldPosition) < trigger2.ColliderRadius * trigger2.ColliderRadius) { Vector2 trigger2flow = triggerFlow2.Value; - if (trigger2.ForceFalloff) trigger2flow *= (1.0f - Vector2.Distance(blipPos, trigger2.WorldPosition) / trigger2.ColliderRadius); + if (trigger2.ForceFalloff) trigger2flow *= 1.0f - Vector2.Distance(blipPos, trigger2.WorldPosition) / trigger2.ColliderRadius; blipVel += trigger2flow; } } @@ -507,7 +505,7 @@ namespace Barotrauma.Items.Components float passivePingRadius = (float)(Timing.TotalTime % 1.0f); if (passivePingRadius > 0.0f) { - disruptedDirections.Clear(); + if (activePingsCount == 0) { disruptedDirections.Clear(); } foreach (AITarget t in AITarget.List) { if (t.Entity is Character c && c.Params.HideInSonar) { continue; } @@ -581,14 +579,14 @@ namespace Barotrauma.Items.Components } } - if (currentMode == Mode.Active && currentPingIndex != -1) + if (currentPingIndex != -1) { var activePing = activePings[currentPingIndex]; if (activePing.IsDirectional && directionalPingCircle != null) { directionalPingCircle.Draw(spriteBatch, center, Color.White * (1.0f - activePing.State), - rotate: MathUtils.VectorToAngle(activePing.Direction), - scale: (DisplayRadius / directionalPingCircle.size.X) * activePing.State); + rotate: MathUtils.VectorToAngle(activePing.Direction), + scale: DisplayRadius / directionalPingCircle.size.X * activePing.State); } else { @@ -611,13 +609,13 @@ namespace Barotrauma.Items.Components if (sonarBlips.Count > 0) { - zoomSqrt = (float)Math.Sqrt(zoom); + float blipScale = 0.08f * (float)Math.Sqrt(zoom) * (rect.Width / 700.0f); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive); foreach (SonarBlip sonarBlip in sonarBlips) { - DrawBlip(spriteBatch, sonarBlip, transducerCenter, center, sonarBlip.FadeTimer / 2.0f * signalStrength); + DrawBlip(spriteBatch, sonarBlip, transducerCenter, center, sonarBlip.FadeTimer / 2.0f * signalStrength, blipScale); } spriteBatch.End(); @@ -1297,7 +1295,7 @@ namespace Barotrauma.Items.Components return true; } - private void DrawBlip(SpriteBatch spriteBatch, SonarBlip blip, Vector2 transducerPos, Vector2 center, float strength) + private void DrawBlip(SpriteBatch spriteBatch, SonarBlip blip, Vector2 transducerPos, Vector2 center, float strength, float blipScale) { strength = MathHelper.Clamp(strength, 0.0f, 1.0f); @@ -1306,8 +1304,8 @@ namespace Barotrauma.Items.Components Vector2 pos = (blip.Position - transducerPos) * displayScale * zoom; pos.Y = -pos.Y; - if (Rand.Range(0.5f, 2.0f) < distort) pos.X = -pos.X; - if (Rand.Range(0.5f, 2.0f) < distort) pos.Y = -pos.Y; + if (Rand.Range(0.5f, 2.0f) < distort) { pos.X = -pos.X; } + if (Rand.Range(0.5f, 2.0f) < distort) { pos.Y = -pos.Y; } float posDistSqr = pos.LengthSquared(); if (posDistSqr > DisplayRadius * DisplayRadius) @@ -1324,15 +1322,15 @@ namespace Barotrauma.Items.Components Vector2 dir = pos / (float)Math.Sqrt(posDistSqr); Vector2 normal = new Vector2(dir.Y, -dir.X); - float scale = (strength + 3.0f) * blip.Scale * zoomSqrt; + float scale = (strength + 3.0f) * blip.Scale * blipScale; Color color = ToolBox.GradientLerp(strength, blipColorGradient[blip.BlipType]); sonarBlip.Draw(spriteBatch, center + pos, color, sonarBlip.Origin, blip.Rotation ?? MathUtils.VectorToAngle(pos), - blip.Size * scale * 0.04f, SpriteEffects.None, 0); + blip.Size * scale * 0.5f, SpriteEffects.None, 0); pos += Rand.Range(0.0f, 1.0f) * dir + Rand.Range(-scale, scale) * normal; - sonarBlip.Draw(spriteBatch, center + pos, color * 0.5f, sonarBlip.Origin, 0, scale * 0.08f, SpriteEffects.None, 0); + sonarBlip.Draw(spriteBatch, center + pos, color * 0.5f, sonarBlip.Origin, 0, scale, SpriteEffects.None, 0); } private void DrawMarker(SpriteBatch spriteBatch, string label, string iconIdentifier, object targetIdentifier, Vector2 worldPosition, Vector2 transducerPosition, float scale, Vector2 center, float radius) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index f52417a50..0d93814ad 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -22,6 +22,7 @@ namespace Barotrauma.Items.Components LevelEnd, LevelStart }; + private GUITickBox maintainPosTickBox, levelEndTickBox, levelStartTickBox; private GUIComponent statusContainer, dockingContainer, controlContainer; @@ -349,20 +350,34 @@ namespace Barotrauma.Items.Components { OnClicked = (btn, userdata) => { - - if (GameMain.GameSession?.Campaign != null) + if (GameMain.GameSession?.Campaign is CampaignMode campaign) { if (Level.IsLoadedOutpost && DockingSources.Any(d => d.Docked && (d.DockingTarget?.Item.Submarine?.Info?.IsOutpost ?? false))) { - GameMain.GameSession.Campaign.CampaignUI.SelectTab(CampaignMode.InteractionType.Map); - GameMain.GameSession.Campaign.ShowCampaignUI = true; + // Undocking from an outpost + campaign.CampaignUI.SelectTab(CampaignMode.InteractionType.Map); + campaign.ShowCampaignUI = true; return false; } else if (!Level.IsLoadedOutpost && DockingModeEnabled && ActiveDockingSource != null && - !ActiveDockingSource.Docked && (DockingTarget?.Item?.Submarine?.Info.IsOutpost ?? false)) + !ActiveDockingSource.Docked && DockingTarget?.Item?.Submarine == Level.Loaded.StartOutpost && (DockingTarget?.Item?.Submarine?.Info.IsOutpost ?? false)) { - enterOutpostPrompt = new GUIMessageBox("", TextManager.GetWithVariable("campaignenteroutpostprompt", "[locationname]", DockingTarget.Item.Submarine.Info.Name), new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + // Docking to an outpost + var subsToLeaveBehind = campaign.GetSubsToLeaveBehind(Item.Submarine); + if (subsToLeaveBehind.Any()) + { + enterOutpostPrompt = new GUIMessageBox( + TextManager.GetWithVariable("enterlocation", "[locationname]", DockingTarget.Item.Submarine.Info.Name), + TextManager.Get(subsToLeaveBehind.Count == 1 ? "LeaveSubBehind" : "LeaveSubsBehind"), + new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + } + else + { + enterOutpostPrompt = new GUIMessageBox("", + TextManager.GetWithVariable("campaignenteroutpostprompt", "[locationname]", DockingTarget.Item.Submarine.Info.Name), + new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + } enterOutpostPrompt.Buttons[0].OnClicked += (btn, userdata) => { SendDockingSignal(); @@ -780,10 +795,16 @@ namespace Barotrauma.Items.Components } checkConnectedPortsTimer = CheckConnectedPortsInterval; } + else + { + checkConnectedPortsTimer -= deltaTime; + } - float closestDist = DockingAssistThreshold * DockingAssistThreshold; DockingModeEnabled = false; - + + if (connectedPorts.None()) { return; } + + float closestDist = DockingAssistThreshold * DockingAssistThreshold; foreach (DockingPort sourcePort in connectedPorts) { if (sourcePort.Docked || sourcePort.Item.Submarine == null) { continue; } @@ -795,15 +816,14 @@ namespace Barotrauma.Items.Components { if (targetPort.Docked || targetPort.Item.Submarine == null) { continue; } if (targetPort.Item.Submarine == controlledSub || targetPort.IsHorizontal != sourcePort.IsHorizontal) { continue; } + if (targetPort.Item.Submarine.DockedTo?.Contains(sourcePort.Item.Submarine) ?? false) { continue; } if (Level.Loaded != null && targetPort.Item.Submarine.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } - - int targetDir = targetPort.GetDir(); - - if (sourceDir == targetDir) { continue; } + if (sourceDir == targetPort.GetDir()) { continue; } float dist = Vector2.DistanceSquared(sourcePort.Item.WorldPosition, targetPort.Item.WorldPosition); if (dist < closestDist) { + closestDist = dist; DockingModeEnabled = true; ActiveDockingSource = sourcePort; DockingTarget = targetPort; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs new file mode 100644 index 000000000..de9dc05fe --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma.Items.Components +{ + internal partial class Planter + { + public Vector2 DrawSize => CalculateSize(); + + private Vector2 CalculateSize() + { + if (GrowableSeeds.All(s => s == null)) { return Vector2.Zero; } + + Point pos = item.DrawPosition.ToPoint(); + Rectangle rect = new Rectangle(pos, Point.Zero); + + for (int i = 0; i < GrowableSeeds.Length; i++) + { + Growable seed = GrowableSeeds[i]; + PlantSlot slot = PlantSlots.ContainsKey(i) ? PlantSlots[i] : NullSlot; + if (seed == null) { continue; } + + foreach (VineTile vine in seed.Vines) + { + Rectangle worldRect = vine.Rect; + worldRect.Location += slot.Offset.ToPoint(); + worldRect.Location += pos; + rect = Rectangle.Union(rect, worldRect); + } + } + + Vector2 result = new Vector2(MaxDistance(pos.X, rect.Left, rect.Right) * 2, MaxDistance(pos.Y, rect.Top, rect.Bottom) * 2); + return result; + + static float MaxDistance(float origin, float x, float y) + { + return Math.Max(Math.Abs(origin - x), Math.Abs(origin - y)); + } + } + + private LightComponent lightComponent; + + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + { + for (var i = 0; i < GrowableSeeds.Length; i++) + { + Growable growable = GrowableSeeds[i]; + PlantSlot slot = PlantSlots.ContainsKey(i) ? PlantSlots[i] : NullSlot; + growable?.Draw(spriteBatch, this, slot.Offset, itemDepth); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs index 634e10fcd..e34555c26 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs @@ -29,6 +29,7 @@ namespace Barotrauma.Items.Components private List> particleEmitterHitItem = new List>(); private float prevProgressBarState; + private Item prevProgressBarTarget = null; partial void InitProjSpecific(XElement element) { @@ -65,7 +66,7 @@ namespace Barotrauma.Items.Components { foreach (ParticleEmitter particleEmitter in particleEmitters) { - float particleAngle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); + float particleAngle = item.body.Rotation + MathHelper.ToRadians(BarrelRotation) + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); particleEmitter.Emit( deltaTime, ConvertUnits.ToDisplayUnits(raystart), item.CurrentHull, particleAngle, particleEmitter.Prefab.CopyEntityAngle ? -particleAngle : 0); @@ -92,7 +93,7 @@ namespace Barotrauma.Items.Components if (targetStructure.Submarine != null) particlePos += targetStructure.Submarine.DrawPosition; foreach (var emitter in particleEmitterHitStructure) { - float particleAngle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); + float particleAngle = item.body.Rotation + MathHelper.ToRadians(BarrelRotation) + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); emitter.Emit(deltaTime, particlePos, item.CurrentHull, particleAngle + MathHelper.Pi, -particleAngle + MathHelper.Pi); } } @@ -103,7 +104,7 @@ namespace Barotrauma.Items.Components if (targetCharacter.Submarine != null) particlePos += targetCharacter.Submarine.DrawPosition; foreach (var emitter in particleEmitterHitCharacter) { - float particleAngle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); + float particleAngle = item.body.Rotation + MathHelper.ToRadians(BarrelRotation) + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); emitter.Emit(deltaTime, particlePos, item.CurrentHull, particleAngle + MathHelper.Pi, -particleAngle + MathHelper.Pi); } } @@ -111,7 +112,7 @@ namespace Barotrauma.Items.Components partial void FixItemProjSpecific(Character user, float deltaTime, Item targetItem) { float progressBarState = targetItem.ConditionPercentage / 100.0f; - if (!MathUtils.NearlyEqual(progressBarState, prevProgressBarState)) + if (!MathUtils.NearlyEqual(progressBarState, prevProgressBarState) || prevProgressBarTarget != targetItem) { var door = targetItem.GetComponent(); if (door == null || door.Stuck <= 0) @@ -121,18 +122,20 @@ namespace Barotrauma.Items.Components targetItem, progressBarPos, progressBarState, - GUI.Style.Red, GUI.Style.Green); + GUI.Style.Red, GUI.Style.Green, + progressBarState < prevProgressBarState ? "progressbar.cutting" : ""); if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } } prevProgressBarState = progressBarState; + prevProgressBarTarget = targetItem; } Vector2 particlePos = ConvertUnits.ToDisplayUnits(pickedPosition); if (targetItem.Submarine != null) particlePos += targetItem.Submarine.DrawPosition; foreach (var emitter in particleEmitterHitItem) { - if (!emitter.First.MatchesItem(targetItem)) continue; - float particleAngle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); + if (!emitter.First.MatchesItem(targetItem)) { continue; } + float particleAngle = item.body.Rotation + MathHelper.ToRadians(BarrelRotation) + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); emitter.Second.Emit(deltaTime, particlePos, item.CurrentHull, particleAngle + MathHelper.Pi, -particleAngle + MathHelper.Pi); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index a6378276e..03488b972 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -192,6 +192,7 @@ namespace Barotrauma.Items.Components foreach (Wire wire in panel.DisconnectedWires) { if (wire == DraggingConnected && mouseInRect) { continue; } + if (wire.HiddenInGame && Screen.Selected == GameMain.GameScreen) { continue; } Connection recipient = wire.OtherConnection(null); string label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})"; @@ -238,6 +239,7 @@ namespace Barotrauma.Items.Components for (int i = 0; i < MaxLinked; i++) { if (wires[i] == null || wires[i].Hidden || (DraggingConnected == wires[i] && (mouseIn || Screen.Selected == GameMain.SubEditorScreen))) { continue; } + if (wires[i].HiddenInGame && Screen.Selected == GameMain.GameScreen) { continue; } Connection recipient = wires[i].OtherConnection(this); string label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})"; @@ -289,7 +291,7 @@ namespace Barotrauma.Items.Components flashColor * (float)Math.Sin(FlashTimer % flashCycleDuration / flashCycleDuration * MathHelper.Pi * 0.8f), scale: connectorSpriteScale); } - if (Wires.Any(w => w != null && w != DraggingConnected && !w.Hidden)) + if (Wires.Any(w => w != null && w != DraggingConnected && !w.Hidden && (!w.HiddenInGame || Screen.Selected != GameMain.GameScreen))) { int screwIndex = (int)Math.Floor(position.Y / 30.0f) % screwSprites.Count; screwSprites[screwIndex].Draw(spriteBatch, position, scale: connectorSpriteScale); @@ -325,7 +327,7 @@ namespace Barotrauma.Items.Components canDrag && ((PlayerInput.MousePosition.X > Math.Min(start.X, end.X) && PlayerInput.MousePosition.X < Math.Max(start.X, end.X) && - MathUtils.LineToPointDistance(start, end, PlayerInput.MousePosition) < 6) || + MathUtils.LineToPointDistanceSquared(start, end, PlayerInput.MousePosition) < 36) || Vector2.Distance(end, PlayerInput.MousePosition) < 20.0f || new Rectangle((start.X < end.X) ? textX - 100 : textX, (int)start.Y - 5, 100, 14).Contains(PlayerInput.MousePosition)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MemoryComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MemoryComponent.cs new file mode 100644 index 000000000..68e7884e9 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MemoryComponent.cs @@ -0,0 +1,12 @@ +using Barotrauma.Networking; + +namespace Barotrauma.Items.Components +{ + partial class MemoryComponent : ItemComponent + { + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + Value = msg.ReadString(); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index d61908320..47e50417d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -165,6 +165,12 @@ namespace Barotrauma.Items.Components } else { + if (!string.IsNullOrEmpty(target.customInteractHUDText) && target.AllowCustomInteract) + { + texts.Add(target.customInteractHUDText); + textColors.Add(GUI.Style.Green); + } + if (target.IsUnconscious) { texts.Add(TextManager.Get("Unconscious")); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index bbf51b8e4..48a36b102 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -17,6 +17,17 @@ namespace Barotrauma.Items.Components private GUIProgressBar powerIndicator; + public int UIElementHeight + { + get + { + int height = 0; + if (ShowChargeIndicator) { height += powerIndicator.Rect.Height; } + if (ShowProjectileIndicator) { height += (int)(Inventory.SlotSpriteSmall.size.Y * Inventory.UIScale) + 5; } + return height; + } + } + private float recoilTimer; private float RetractionTime => Math.Max(Reload * RetractionDurationMultiplier, RecoilTime); @@ -235,12 +246,11 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { - Vector2 drawPos = new Vector2(item.Rect.X + transformedBarrelPos.X, item.Rect.Y - transformedBarrelPos.Y); - if (item.Submarine != null) + if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation)) { - drawPos += item.Submarine.DrawPosition; + UpdateTransformedBarrelPos(); } - drawPos.Y = -drawPos.Y; + Vector2 drawPos = GetDrawPos(); float recoilOffset = 0.0f; if (Math.Abs(RecoilDistance) > 0.0f && recoilTimer > 0.0f) @@ -275,7 +285,7 @@ namespace Barotrauma.Items.Components rotation + MathHelper.PiOver2, item.Scale, SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth)); - if (!editing) { return; } + if (!editing || GUI.DisableHUD) { return; } float widgetRadius = 60.0f; @@ -310,7 +320,7 @@ namespace Barotrauma.Items.Components }; widget.MouseHeld += (deltaTime) => { - minRotation = GetRotationAngle(drawPos); + minRotation = GetRotationAngle(GetDrawPos()); if (minRotation > maxRotation) { float temp = minRotation; @@ -332,7 +342,7 @@ namespace Barotrauma.Items.Components widget.PreDraw += (sprtBtch, deltaTime) => { widget.tooltip = "Min: " + (int)MathHelper.ToDegrees(minRotation); - widget.DrawPos = drawPos + new Vector2((float)Math.Cos(minRotation), (float)Math.Sin(minRotation)) * widgetRadius; + widget.DrawPos = GetDrawPos() + new Vector2((float)Math.Cos(minRotation), (float)Math.Sin(minRotation)) * widgetRadius; widget.Update(deltaTime); }; }); @@ -351,7 +361,7 @@ namespace Barotrauma.Items.Components }; widget.MouseHeld += (deltaTime) => { - maxRotation = GetRotationAngle(drawPos); + maxRotation = GetRotationAngle(GetDrawPos()); if (minRotation > maxRotation) { float temp = minRotation; @@ -373,12 +383,20 @@ namespace Barotrauma.Items.Components widget.PreDraw += (sprtBtch, deltaTime) => { widget.tooltip = "Max: " + (int)MathHelper.ToDegrees(maxRotation); - widget.DrawPos = drawPos + new Vector2((float)Math.Cos(maxRotation), (float)Math.Sin(maxRotation)) * widgetRadius; + widget.DrawPos = GetDrawPos() + new Vector2((float)Math.Cos(maxRotation), (float)Math.Sin(maxRotation)) * widgetRadius; widget.Update(deltaTime); }; }); minRotationWidget.Draw(spriteBatch, (float)Timing.Step); maxRotationWidget.Draw(spriteBatch, (float)Timing.Step); + + Vector2 GetDrawPos() + { + Vector2 drawPos = new Vector2(item.Rect.X + transformedBarrelPos.X, item.Rect.Y - transformedBarrelPos.Y); + if (item.Submarine != null) { drawPos += item.Submarine.DrawPosition; } + drawPos.Y = -drawPos.Y; + return drawPos; + } } private Widget GetWidget(string id, SpriteBatch spriteBatch, int size = 5, Action initMethod = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 2a6e27003..8a0e39f1e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -715,7 +715,12 @@ namespace Barotrauma /// public static bool IsMouseOnInventory(bool ignoreDraggedItem = false) { - var isSubEditor = Screen.Selected is SubEditorScreen editor && !editor.WiringMode; + if (GameMain.GameSession?.Campaign != null && + (GameMain.GameSession.Campaign.ShowCampaignUI || GameMain.GameSession.Campaign.ForceMapUI)) + { + return false; + } + if (Character.Controlled == null) { return false; } if (!ignoreDraggedItem) @@ -723,6 +728,8 @@ namespace Barotrauma if (draggingItem != null || DraggingInventory != null) { return true; } } + var isSubEditor = Screen.Selected is SubEditorScreen editor && !editor.WiringMode; + if (Character.Controlled.Inventory != null && !isSubEditor) { var inv = Character.Controlled.Inventory; @@ -840,7 +847,8 @@ namespace Barotrauma foreach (var ic in character.SelectedConstruction.ActiveHUDs) { var itemContainer = ic as ItemContainer; - if (itemContainer?.Inventory?.slots == null) continue; + if (itemContainer?.Inventory?.slots == null) { continue; } + if (ic.Item.NonInteractable) { continue; } foreach (var slot in itemContainer.Inventory.slots) { @@ -1127,7 +1135,6 @@ namespace Barotrauma return hoverArea; } - public static void DrawFront(SpriteBatch spriteBatch) { if (GUI.PauseMenuOpen || GUI.SettingsMenuOpen) { return; } @@ -1202,6 +1209,7 @@ namespace Barotrauma } Color slotColor = Color.White; + if ((inventory?.Owner as Item)?.NonInteractable ?? false) { slotColor = Color.Gray; } var itemContainer = item?.GetComponent(); if (itemContainer != null && (itemContainer.InventoryTopSprite != null || itemContainer.InventoryBottomSprite != null)) { @@ -1470,18 +1478,19 @@ namespace Barotrauma { if (receivedItemIDs[i] == 0 || (Entity.FindEntityByID(receivedItemIDs[i]) as Item != Items[i])) { - if (Items[i] != null) Items[i].Drop(null); + Items[i]?.Drop(null); System.Diagnostics.Debug.Assert(Items[i] == null); } } - for (int i = 0; i < capacity; i++) + //iterate backwards to get the item to the Any slots first + for (int i = capacity - 1; i >= 0; i--) { if (receivedItemIDs[i] > 0) { if (!(Entity.FindEntityByID(receivedItemIDs[i]) is Item item) || Items[i] == item) { continue; } - TryPutItem(item, i, true, true, null, false); + TryPutItem(item, i, false, false, null, false); for (int j = 0; j < capacity; j++) { if (Items[j] == item && receivedItemIDs[j] != item.ID) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 83694b914..e17e0b092 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -92,8 +92,6 @@ namespace Barotrauma return parentInventory == null && (body == null || body.Enabled) && ShowItems; } } - - public float SpriteRotation; public Color GetSpriteColor() { @@ -297,7 +295,7 @@ namespace Barotrauma foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState) * Scale; + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, -rotationRad) * Scale; decorativeSprite.Sprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X + offset.X - rect.Width / 2, -(DrawPosition.Y + offset.Y + rect.Height / 2)), size, color: color, @@ -307,15 +305,24 @@ namespace Barotrauma } else { - activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, SpriteRotation + rotation, Scale, activeSprite.effects, depth); - fadeInBrokenSprite?.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, SpriteRotation + rotation, Scale, activeSprite.effects, depth - 0.000001f); + Vector2 origin = activeSprite.Origin; + if ((activeSprite.effects & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) + { + origin.X = activeSprite.SourceRect.Width - origin.X; + } + if ((activeSprite.effects & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically) + { + origin.Y = activeSprite.SourceRect.Height - origin.Y; + } + activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, origin, rotationRad, Scale, activeSprite.effects, depth); + fadeInBrokenSprite?.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, origin, rotationRad, Scale, activeSprite.effects, depth - 0.000001f); foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } float rot = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState) * Scale; + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, -rotationRad) * Scale; decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, - SpriteRotation + rotation + rot, decorativeSprite.Scale * Scale, activeSprite.effects, + rotationRad + rot, decorativeSprite.Scale * Scale, activeSprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); } } @@ -358,7 +365,7 @@ namespace Barotrauma { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState) * Scale; + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, -rotationRad) * Scale; var ca = (float)Math.Cos(-body.Rotation); var sa = (float)Math.Sin(-body.Rotation); @@ -378,7 +385,7 @@ namespace Barotrauma { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); - var (xOff, yOff) = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState) * Scale; + var (xOff, yOff) = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, -rotationRad) * Scale; decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + xOff, -(DrawPosition.Y + yOff)), color, rotation, decorativeSprite.Scale * Scale, activeSprite.effects, @@ -437,7 +444,7 @@ namespace Barotrauma } } - if (!ShowLinks) return; + if (!ShowLinks || GUI.DisableHUD) { return; } foreach (MapEntity e in linkedTo) { @@ -455,21 +462,6 @@ namespace Barotrauma } } - private void DrawDecorativeSprite(SpriteBatch spriteBatch, DecorativeSprite decorativeSprite, Color color, float depth) - { - if (!spriteAnimState[decorativeSprite].IsActive) { return; } - float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState) * Scale; - - var ca = (float)Math.Cos(-body.Rotation); - var sa = (float)Math.Sin(-body.Rotation); - Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); - - decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + transformedOffset.X, -(DrawPosition.Y + transformedOffset.Y)), color, - -body.Rotation + rotation, decorativeSprite.Scale * Scale, activeSprite.effects, - depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); - } - partial void OnCollisionProjSpecific(float impact) { if (impact > 1.0f && @@ -512,7 +504,7 @@ namespace Barotrauma } if (Screen.Selected != GameMain.SubEditorScreen) { return; } - + if (Character.Controlled == null) { activeHUDs.Clear(); } if (!Linkable) { return; } @@ -1154,7 +1146,6 @@ namespace Barotrauma editingHUDRefreshPending = true; break; case NetEntityEvent.Type.Upgrade: - { string identifier = msg.ReadString(); byte level = msg.ReadByte(); if (UpgradePrefab.Find(identifier) is { } upgradePrefab) @@ -1174,8 +1165,7 @@ namespace Barotrauma AddUpgrade(upgrade, false); } - break; - } + break; case NetEntityEvent.Type.Invalid: break; } @@ -1391,7 +1381,7 @@ namespace Barotrauma if (itemPrefab == null) { string errorMsg = "Failed to spawn item, prefab not found (name: " + (itemName ?? "null") + ", identifier: " + (itemIdentifier ?? "null") + ")"; - errorMsg += "\n" + string.Join(", ", GameMain.Config.SelectedContentPackages.Select(cp => cp.Name)); + errorMsg += "\n" + string.Join(", ", GameMain.Config.AllEnabledPackages.Select(cp => cp.Name)); GameAnalyticsManager.AddErrorEventOnce("Item.ReadSpawnData:PrefabNotFound" + (itemName ?? "null") + (itemIdentifier ?? "null"), GameAnalyticsSDK.Net.EGAErrorSeverity.Critical, errorMsg); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index f45406199..3e701f41e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -55,6 +55,7 @@ namespace Barotrauma public List ContainedSprites = new List(); public Dictionary> DecorativeSpriteGroups = new Dictionary>(); public Sprite InventoryIcon; + public Sprite MinimapIcon; //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", false)] @@ -90,6 +91,7 @@ namespace Barotrauma }; item.SetTransform(ConvertUnits.ToSimUnits(Submarine.MainSub == null ? item.Position : item.Position - Submarine.MainSub.Position), 0.0f); item.FindHull(); + item.Submarine = Submarine.MainSub; if (PlayerInput.IsShiftDown()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs index 91110bbce..7cd74456b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs @@ -14,7 +14,7 @@ namespace Barotrauma Vector2.Zero, 0.0f, hull); } - hull = hull ?? Hull.FindHull(worldPosition, useWorldCoordinates: true); + hull ??= Hull.FindHull(worldPosition, useWorldCoordinates: true); bool underwater = hull == null || worldPosition.Y < hull.WorldSurface; if (underwater && underwaterBubble) @@ -44,7 +44,7 @@ namespace Barotrauma } if (smoke) { - var smokeParticle = GameMain.ParticleManager.CreateParticle(Rand.Range(0.0f, 1.0f) < 0.5f ? "explosionsmoke" : "smoke", + GameMain.ParticleManager.CreateParticle(Rand.Range(0.0f, 1.0f) < 0.5f ? "explosionsmoke" : "smoke", ClampParticlePos(worldPosition + Rand.Vector((float)System.Math.Sqrt(Rand.Range(0.0f, attack.Range))), hull), Rand.Vector(Rand.Range(0.0f, particleSpeed)), 0.0f, hull); } @@ -75,11 +75,6 @@ namespace Barotrauma } } - if (hull != null && !string.IsNullOrWhiteSpace(decal) && decalSize > 0.0f) - { - hull.AddDecal(decal, worldPosition, decalSize); - } - if (flash) { float displayRange = flashRange.HasValue ? flashRange.Value : attack.Range; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs index c007fc16f..558615ef5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs @@ -12,35 +12,10 @@ namespace Barotrauma partial void UpdateProjSpecific(float growModifier) { EmitParticles(size, WorldPosition, hull, growModifier, OnChangeHull); - + lightSource.Color = new Color(1.0f, 0.45f, 0.3f) * Rand.Range(0.8f, 1.0f); if (Math.Abs((lightSource.Range * 0.2f) - Math.Max(size.X, size.Y)) > 1.0f) lightSource.Range = Math.Max(size.X, size.Y) * 5.0f; - if (Vector2.DistanceSquared(lightSource.Position,position) > 5.0f) lightSource.Position = position + Vector2.UnitY * 30.0f; - - if (size.X > 256.0f) - { - if (burnDecals.Count == 0) - { - var newDecal = hull.AddDecal("burnt", WorldPosition + size/2); - if (newDecal != null) burnDecals.Add(newDecal); - } - else if (WorldPosition.X < burnDecals[0].WorldPosition.X - 256.0f) - { - var newDecal = hull.AddDecal("burnt", WorldPosition); - if (newDecal != null) burnDecals.Insert(0, newDecal); - } - else if (WorldPosition.X + size.X > burnDecals[burnDecals.Count-1].WorldPosition.X + 256.0f) - { - var newDecal = hull.AddDecal("burnt", WorldPosition + Vector2.UnitX * size.X); - if (newDecal != null) burnDecals.Add(newDecal); - } - } - - foreach (Decal d in burnDecals) - { - //prevent the decals from fading out as long as the firesource is alive - d.FadeTimer = Math.Min(d.FadeTimer, d.FadeInTime); - } + if (Vector2.DistanceSquared(lightSource.Position, position) > 5.0f) lightSource.Position = position + Vector2.UnitY * 30.0f; } public static void EmitParticles(Vector2 size, Vector2 worldPosition, Hull hull, float growModifier, Particle.OnChangeHullHandler onChangeHull = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index bc9545749..31171fff0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -1,6 +1,4 @@ using Barotrauma.Networking; -using Barotrauma.Particles; -using Barotrauma.Sounds; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; @@ -12,16 +10,10 @@ namespace Barotrauma { partial class Hull : MapEntity, ISerializableEntity, IServerSerializable, IClientSerializable { - public const int MaxDecalsPerHull = 10; - - private readonly List decals = new List(); - private float serverUpdateDelay; private float remoteWaterVolume, remoteOxygenPercentage; private List remoteFireSources; - - private bool networkUpdatePending; - private float networkUpdateTimer; + private readonly List remoteBackgroundSections = new List(); private double lastAmbientLightEditTime; @@ -49,11 +41,14 @@ namespace Barotrauma } } + private float paintAmount = 0.0f; + private float minimumPaintAmountToDraw; + public override bool IsVisible(Rectangle worldView) { if (Screen.Selected != GameMain.SubEditorScreen && !GameMain.DebugDraw) { - if (decals.Count == 0) { return false; } + if (decals.Count == 0 && paintAmount < minimumPaintAmountToDraw) { return false; } Rectangle worldRect = WorldRect; if (worldRect.X > worldView.Right || worldRect.Right < worldView.X) { return false; } @@ -70,20 +65,6 @@ namespace Barotrauma !Submarine.RectContains(MathUtils.ExpandRect(WorldRect, -8), position)); } - public Decal AddDecal(string decalName, Vector2 worldPosition, float scale = 1.0f) - { - if (decals.Count >= MaxDecalsPerHull) return null; - - var decal = GameMain.DecalManager.CreateDecal(decalName, scale, worldPosition, this); - - if (decal != null) - { - decals.Add(decal); - } - - return decal; - } - private GUIComponent CreateEditingHUD(bool inGame = false) { editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.25f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) { UserData = this }; @@ -101,39 +82,32 @@ namespace Barotrauma { editingHUD = CreateEditingHUD(Screen.Selected != GameMain.SubEditorScreen); } - - if (!PlayerInput.KeyDown(Keys.Space)) return; + + if (!PlayerInput.KeyDown(Keys.Space)) { return; } bool lClick = PlayerInput.PrimaryMouseButtonClicked(); bool rClick = PlayerInput.SecondaryMouseButtonClicked(); - if (!lClick && !rClick) return; + if (!lClick && !rClick) { return; } Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - if (lClick) + foreach (MapEntity entity in mapEntityList) { - foreach (MapEntity entity in mapEntityList) + if (entity == this || !entity.IsHighlighted) { continue; } + if (!entity.IsMouseOn(position)) { continue; } + if (entity.linkedTo != null && entity.linkedTo.Contains(this)) { if (entity == this || !entity.IsHighlighted) continue; if (!entity.IsMouseOn(position)) continue; - if (entity.Linkable && entity.linkedTo != null) + if (entity.Linkable && entity.linkedTo != null && !entity.linkedTo.Contains(this)) { entity.linkedTo.Add(this); linkedTo.Add(entity); } } - } - else - { - foreach (MapEntity entity in mapEntityList) + else if (entity.Linkable && entity.linkedTo != null) { - if (entity == this || !entity.IsHighlighted) continue; - if (!entity.IsMouseOn(position)) continue; - if (entity.linkedTo != null && entity.linkedTo.Contains(this)) - { - entity.linkedTo.Remove(this); - linkedTo.Remove(entity); - - } + entity.linkedTo.Add(this); + linkedTo.Add(entity); } } } @@ -151,7 +125,15 @@ namespace Barotrauma networkUpdateTimer += deltaTime; if (networkUpdateTimer > 0.2f) { - GameMain.NetworkMember?.CreateEntityEvent(this); + if (!pendingSectionUpdates.Any()) + { + GameMain.NetworkMember?.CreateEntityEvent(this); + } + foreach (int pendingSectionUpdate in pendingSectionUpdates) + { + GameMain.NetworkMember?.CreateEntityEvent(this, new object[] { pendingSectionUpdate }); + } + pendingSectionUpdates.Clear(); networkUpdatePending = false; networkUpdateTimer = 0.0f; } @@ -189,14 +171,7 @@ namespace Barotrauma } } } - - foreach (Decal decal in decals) - { - decal.Update(deltaTime); - } - - decals.RemoveAll(d => d.FadeTimer >= d.LifeTime); - + if (waterVolume < 1.0f) return; for (int i = 1; i < waveY.Length - 1; i++) { @@ -324,6 +299,35 @@ namespace Barotrauma } } + public void DrawSectionColors(SpriteBatch spriteBatch) + { + Vector2 drawOffset = Submarine == null ? Vector2.Zero : Submarine.DrawPosition; + Point sectionSize = BackgroundSections[0].Rect.Size; + Vector2 drawPos = drawOffset + new Vector2(rect.Location.X + sectionSize.X / 2, rect.Location.Y - sectionSize.Y / 2); + + for (int i = 0; i < BackgroundSections.Count; i++) + { + BackgroundSection section = BackgroundSections[i]; + + if (section.ColorStrength < 0.01f || section.Color.A < 1) { continue; } + + if (GameMain.DecalManager.GrimeSprites.Count == 0) + { + GUI.DrawRectangle(spriteBatch, + new Vector2(drawOffset.X + rect.X + section.Rect.X, -(drawOffset.Y + rect.Y + section.Rect.Y)), + new Vector2(sectionSize.X, sectionSize.Y), + section.GetStrengthAdjustedColor(), true, 0.0f, (int)Math.Max(1.5f / Screen.Selected.Cam.Zoom, 1.0f)); + } + else + { + Vector2 sectionPos = new Vector2(drawPos.X + section.Rect.Location.X, -(drawPos.Y + section.Rect.Location.Y)); + Vector2 randomOffset = new Vector2(section.Noise.X - 0.5f, section.Noise.Y - 0.5f) * 15.0f; + var sprite = GameMain.DecalManager.GrimeSprites[i % GameMain.DecalManager.GrimeSprites.Count]; + sprite.Draw(spriteBatch, sectionPos + randomOffset, section.GetStrengthAdjustedColor(), scale: 1.25f); + } + } + } + public static void UpdateVertices(Camera cam, WaterRenderer renderer) { foreach (EntityGrid entityGrid in EntityGrids) @@ -526,22 +530,38 @@ namespace Barotrauma public void ClientWrite(IWriteMessage msg, object[] extraData = null) { - msg.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8); - - msg.Write(FireSources.Count > 0); - if (FireSources.Count > 0) + msg.Write(extraData != null); + if (extraData == null) { - msg.WriteRangedInteger(Math.Min(FireSources.Count, 16), 0, 16); - for (int i = 0; i < Math.Min(FireSources.Count, 16); i++) - { - var fireSource = FireSources[i]; - Vector2 normalizedPos = new Vector2( - (fireSource.Position.X - rect.X) / rect.Width, - (fireSource.Position.Y - (rect.Y - rect.Height)) / rect.Height); + msg.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8); - msg.WriteRangedSingle(MathHelper.Clamp(normalizedPos.X, 0.0f, 1.0f), 0.0f, 1.0f, 8); - msg.WriteRangedSingle(MathHelper.Clamp(normalizedPos.Y, 0.0f, 1.0f), 0.0f, 1.0f, 8); - msg.WriteRangedSingle(MathHelper.Clamp(fireSource.Size.X / rect.Width, 0.0f, 1.0f), 0, 1.0f, 8); + msg.Write(FireSources.Count > 0); + if (FireSources.Count > 0) + { + msg.WriteRangedInteger(Math.Min(FireSources.Count, 16), 0, 16); + for (int i = 0; i < Math.Min(FireSources.Count, 16); i++) + { + var fireSource = FireSources[i]; + Vector2 normalizedPos = new Vector2( + (fireSource.Position.X - rect.X) / rect.Width, + (fireSource.Position.Y - (rect.Y - rect.Height)) / rect.Height); + + msg.WriteRangedSingle(MathHelper.Clamp(normalizedPos.X, 0.0f, 1.0f), 0.0f, 1.0f, 8); + msg.WriteRangedSingle(MathHelper.Clamp(normalizedPos.Y, 0.0f, 1.0f), 0.0f, 1.0f, 8); + msg.WriteRangedSingle(MathHelper.Clamp(fireSource.Size.X / rect.Width, 0.0f, 1.0f), 0, 1.0f, 8); + } + } + } + else + { + int sectorToUpdate = (int)extraData[0]; + int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent; + int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1); + msg.WriteRangedInteger(sectorToUpdate, 0, BackgroundSections.Count - 1); + for (int i = start; i < end; i++) + { + msg.WriteRangedSingle(BackgroundSections[i].ColorStrength, 0.0f, 1.0f, 8); + msg.Write(BackgroundSections[i].Color.PackedValue); } } } @@ -565,6 +585,54 @@ namespace Barotrauma } } + bool hasExtraData = message.ReadBoolean(); + if (hasExtraData) + { + bool hasSectionUpdate = message.ReadBoolean(); + if (hasSectionUpdate) + { + int sectorToUpdate = message.ReadRangedInteger(0, BackgroundSections.Count - 1); + int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent; + int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1); + for (int i = start; i < end; i++) + { + float colorStrength = message.ReadRangedSingle(0.0f, 1.0f, 8); + Color color = new Color(message.ReadUInt32()); + float prevColorStrength = BackgroundSections[i].ColorStrength; + BackgroundSections[i].SetColorStrength(colorStrength); + BackgroundSections[i].SetColor(color); + paintAmount = Math.Max(0, paintAmount + (BackgroundSections[i].ColorStrength - prevColorStrength) / BackgroundSections.Count); + + var remoteBackgroundSection = remoteBackgroundSections.Find(s => s.Index == i); + if (remoteBackgroundSection != null) + { + remoteBackgroundSection.SetColorStrength(colorStrength); + remoteBackgroundSection.SetColor(color); + } + else + { + remoteBackgroundSections.Add(new BackgroundSection(new Rectangle(0, 0, 1, 1), i, colorStrength, color, 0)); + } + } + paintAmount = BackgroundSections.Sum(s => s.ColorStrength); + } + else + { + int decalCount = message.ReadRangedInteger(0, MaxDecalsPerHull); + decals.Clear(); + for (int i = 0; i < decalCount; i++) + { + UInt32 decalId = message.ReadUInt32(); + float normalizedXPos = message.ReadRangedSingle(0.0f, 1.0f, 8); + float normalizedYPos = message.ReadRangedSingle(0.0f, 1.0f, 8); + float decalPosX = MathHelper.Lerp(rect.X, rect.Right, normalizedXPos); + float decalPosY = MathHelper.Lerp(rect.Y - rect.Height, rect.Y, normalizedYPos); + float decalScale = message.ReadRangedSingle(0.0f, 2.0f, 12); + AddDecal(decalId, new Vector2(decalPosX, decalPosY), decalScale, isNetworkEvent: true); + } + } + } + if (serverUpdateDelay > 0.0f) { return; } ApplyRemoteState(); @@ -572,10 +640,14 @@ namespace Barotrauma private void ApplyRemoteState() { - if (remoteFireSources == null) + foreach (BackgroundSection remoteBackgroundSection in remoteBackgroundSections) { - return; + BackgroundSections[remoteBackgroundSection.Index].SetColor(remoteBackgroundSection.Color); + BackgroundSections[remoteBackgroundSection.Index].SetColorStrength(remoteBackgroundSection.ColorStrength); } + remoteBackgroundSections.Clear(); + + if (remoteFireSources == null) { return; } WaterVolume = remoteWaterVolume; OxygenPercentage = remoteOxygenPercentage; @@ -609,7 +681,6 @@ namespace Barotrauma FireSources.RemoveAt(i); } } - remoteFireSources = null; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs index f837b4bad..dab22d962 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs @@ -34,8 +34,8 @@ namespace Barotrauma public const int DefaultBufferSize = 2000; public const int DefaultIndoorsBufferSize = 3000; - public static Vector2 DistortionScale = new Vector2(2f, 2f); - public static Vector2 DistortionStrength = new Vector2(0.25f, 0.25f); + public static Vector2 DistortionScale = new Vector2(2f, 1.5f); + public static Vector2 DistortionStrength = new Vector2(0.01f, 0.33f); public static float BlurAmount = 0.0f; public Vector2 WavePos diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index e4e810725..5c2fa3df5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -2,6 +2,7 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using SharpFont; using System; using System.Collections.Generic; using System.Diagnostics; @@ -87,6 +88,12 @@ namespace Barotrauma.Lights } } + class VectorPair + { + public Vector2? A = null; + public Vector2? B = null; + } + class ConvexHull { public static List HullLists = new List(); @@ -96,6 +103,7 @@ namespace Barotrauma.Lights private readonly Segment[] segments = new Segment[4]; private readonly SegmentPoint[] vertices = new SegmentPoint[4]; private readonly SegmentPoint[] losVertices = new SegmentPoint[4]; + private readonly VectorPair[] losOffsets = new VectorPair[4]; private readonly bool[] backFacing; private readonly bool[] ignoreEdge; @@ -105,6 +113,7 @@ namespace Barotrauma.Lights public VertexPositionColor[] ShadowVertices { get; private set; } public VertexPositionTexture[] PenumbraVertices { get; private set; } public int ShadowVertexCount { get; private set; } + public int PenumbraVertexCount { get; private set; } private readonly HashSet overlappingHulls = new HashSet(); @@ -157,15 +166,24 @@ namespace Barotrauma.Lights ParentEntity = parent; - ShadowVertices = new VertexPositionColor[6 * 2]; - PenumbraVertices = new VertexPositionTexture[6]; + ShadowVertices = new VertexPositionColor[6 * 4]; + PenumbraVertices = new VertexPositionTexture[6 * 4]; backFacing = new bool[4]; ignoreEdge = new bool[4]; - SetVertices(points); - - Enabled = true; + float minX = points[0].X, minY = points[0].Y, maxX = points[0].X, maxY = points[0].Y; + + for (int i = 1; i < vertices.Length; i++) + { + if (points[i].X < minX) minX = points[i].X; + if (points[i].Y < minY) minY = points[i].Y; + + if (points[i].X > maxX) maxX = points[i].X; + if (points[i].Y > minY) maxY = points[i].Y; + } + + BoundingBox = new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY)); isHorizontal = BoundingBox.Width > BoundingBox.Height; if (ParentEntity is Structure structure) @@ -180,6 +198,10 @@ namespace Barotrauma.Lights if (door != null) { isHorizontal = door.IsHorizontal; } } + SetVertices(points); + + Enabled = true; + var chList = HullLists.Find(h => h.Submarine == parent.Submarine); if (chList == null) { @@ -202,11 +224,17 @@ namespace Barotrauma.Lights if (isHorizontal == ch.isHorizontal) { - if (BoundingBox == ch.BoundingBox) { return; } - //hide segments that are roughly at the some position as some other segment (e.g. the ends of two adjacent wall pieces) - float mergeDist = 32; + float mergeDist = 16; float mergeDistSqr = mergeDist * mergeDist; + + Rectangle intersection = Rectangle.Intersect(BoundingBox, ch.BoundingBox); + int intersectionArea = intersection.Width * intersection.Height; + int bboxArea = BoundingBox.Width * BoundingBox.Height; + int otherBboxArea = ch.BoundingBox.Width * ch.BoundingBox.Height; + if (Math.Abs(intersectionArea - bboxArea) < mergeDistSqr) { return; } + if (Math.Abs(intersectionArea - otherBboxArea) < mergeDistSqr) { return; } + for (int i = 0; i < segments.Length; i++) { for (int j = 0; j < ch.segments.Length; j++) @@ -237,21 +265,67 @@ namespace Barotrauma.Lights } } } - else + + for (int i = 0; i < segments.Length; i++) { - //TODO: do something to corner areas where a vertical wall meets a horizontal one + if (ignoreEdge[i]) { continue; } + if (Vector2.DistanceSquared(segments[i].Start.Pos, segments[i].End.Pos) < 1.0f) { continue; } + for (int j = 0; j < ch.segments.Length; j++) + { + if (ch.ignoreEdge[j]) { continue; } + if (Vector2.DistanceSquared(ch.segments[j].Start.Pos, ch.segments[j].End.Pos) < 1.0f) { continue; } + if (IsSegmentAInB(segments[i], ch.segments[j])) + { + ignoreEdge[i] = true; + if (Vector2.DistanceSquared(ch.segments[j].Start.Pos, segments[i].Start.Pos) < 4.0f) + { + ch.ShiftSegmentPoint(j, false, segments[i].End.Pos); + } + else if (Vector2.DistanceSquared(ch.segments[j].Start.Pos, segments[i].End.Pos) < 4.0f) + { + ch.ShiftSegmentPoint(j, false, segments[i].Start.Pos); + } + + if (Vector2.DistanceSquared(ch.segments[j].End.Pos, segments[i].Start.Pos) < 4.0f) + { + ch.ShiftSegmentPoint(j, true, segments[i].End.Pos); + } + else if (Vector2.DistanceSquared(ch.segments[j].End.Pos, segments[i].End.Pos) < 4.0f) + { + ch.ShiftSegmentPoint(j, true, segments[i].Start.Pos); + } + } + else if (IsSegmentAInB(ch.segments[j], segments[i])) + { + ch.ignoreEdge[j] = true; + + if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].Start.Pos) < 4.0f) + { + ShiftSegmentPoint(i, false, ch.segments[j].End.Pos); + } + else if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].End.Pos) < 4.0f) + { + ShiftSegmentPoint(i, false, ch.segments[j].Start.Pos); + } + + if (Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].Start.Pos) < 4.0f) + { + ShiftSegmentPoint(i, true, ch.segments[j].End.Pos); + } + else if (Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].End.Pos) < 4.0f) + { + ShiftSegmentPoint(i, true, ch.segments[j].Start.Pos); + } + } + } } //ignore edges that are inside some other convex hull for (int i = 0; i < vertices.Length; i++) { - if (vertices[i].Pos.X >= ch.BoundingBox.X && vertices[i].Pos.X <= ch.BoundingBox.Right && - vertices[i].Pos.Y >= ch.BoundingBox.Y && vertices[i].Pos.Y <= ch.BoundingBox.Bottom) + if (ch.IsPointInside(vertices[i].Pos)) { - Vector2 p = vertices[(i + 1) % vertices.Length].Pos; - - if (p.X >= ch.BoundingBox.X && p.X <= ch.BoundingBox.Right && - p.Y >= ch.BoundingBox.Y && p.Y <= ch.BoundingBox.Bottom) + if (ch.IsPointInside(vertices[(i + 1) % vertices.Length].Pos)) { ignoreEdge[i] = true; overlappingHulls.Add(ch); @@ -260,6 +334,75 @@ namespace Barotrauma.Lights } } + private void ShiftSegmentPoint(int segmentIndex, bool end, Vector2 newPos) + { + var segment = segments[segmentIndex]; + + losOffsets[segmentIndex] ??= new VectorPair(); + bool flipped = false; + if (Vector2.DistanceSquared(vertices[segmentIndex].Pos, segment.Start.Pos) > Vector2.DistanceSquared(vertices[segmentIndex].Pos, segment.End.Pos)) + { + flipped = true; + } + if (end == !flipped) + { + losOffsets[segmentIndex].B = newPos; + } + else + { + losOffsets[segmentIndex].A = newPos; + } + } + + public bool IsSegmentAInB(Segment a, Segment b) + { + if (Vector2.DistanceSquared(a.Start.Pos, a.End.Pos) > Vector2.DistanceSquared(b.Start.Pos, b.End.Pos)) + { + return false; + } + + Vector2 min = new Vector2(Math.Min(b.Start.Pos.X, b.End.Pos.X), Math.Min(b.Start.Pos.Y, b.End.Pos.Y)); + Vector2 max = new Vector2(Math.Max(b.Start.Pos.X, b.End.Pos.X), Math.Max(b.Start.Pos.Y, b.End.Pos.Y)); + min.X -= 1.0f; min.Y -= 1.0f; + max.X += 1.0f; max.Y += 1.0f; + + if (a.Start.Pos.X < min.X) { return false; } + if (a.Start.Pos.Y < min.Y) { return false; } + if (a.End.Pos.X < min.X) { return false; } + if (a.End.Pos.Y < min.Y) { return false; } + + if (a.Start.Pos.X > max.X) { return false; } + if (a.Start.Pos.Y > max.Y) { return false; } + if (a.End.Pos.X > max.X) { return false; } + if (a.End.Pos.Y > max.Y) { return false; } + + float startDist = MathUtils.LineToPointDistanceSquared(b.Start.Pos, b.End.Pos, a.Start.Pos); + if (startDist > 1.0f) { return false; } + float endDist = MathUtils.LineToPointDistanceSquared(b.Start.Pos, b.End.Pos, a.End.Pos); + if (endDist > 1.0f) { return false; } + return true; + } + + public bool IsPointInside(Vector2 point) + { + if (!BoundingBox.Contains(point)) { return false; } + + Vector2 center = (vertices[0].Pos + vertices[1].Pos + vertices[2].Pos + vertices[3].Pos) * 0.25f; + for (int i=0;i<4;i++) + { + Vector2 segmentVector = vertices[(i + 1) % 4].Pos - vertices[i].Pos; + Vector2 centerToVertex = center - vertices[i].Pos; + Vector2 pointToVertex = point - vertices[i].Pos; + + float dotCenter = Vector2.Dot(centerToVertex, segmentVector); + float dotPoint = Vector2.Dot(pointToVertex, segmentVector); + + if ((dotCenter > 0f && dotPoint < 0f) || (dotCenter < 0f && dotPoint > 0f)) { return false; } + } + + return true; + } + private void MergeSegments(Segment segment1, Segment segment2, bool startPointsMatch) { int startPointIndex = -1, endPointIndex = -1; @@ -344,6 +487,8 @@ namespace Barotrauma.Lights vertices[i].Pos += amount; losVertices[i].Pos += amount; + losOffsets[i] = null; + segments[i].Start.Pos += amount; segments[i].End.Pos += amount; } @@ -406,6 +551,7 @@ namespace Barotrauma.Lights { vertices[i] = new SegmentPoint(points[i], this); losVertices[i] = new SegmentPoint(points[i], this); + losOffsets[i] = null; } for (int i = 0; i < 4; i++) @@ -517,7 +663,7 @@ namespace Barotrauma.Lights } } - public void CalculateShadowVertices(Vector2 lightSourcePos, bool los = true) + public void CalculateLosVertices(Vector2 lightSourcePos) { Vector3 offset = Vector3.Zero; if (ParentEntity != null && ParentEntity.Submarine != null) @@ -527,8 +673,6 @@ namespace Barotrauma.Lights ShadowVertexCount = 0; - var vertices = los ? losVertices : this.vertices; - //compute facing of each edge, using N*L for (int i = 0; i < 4; i++) { @@ -538,8 +682,8 @@ namespace Barotrauma.Lights continue; } - Vector2 firstVertex = vertices[i].Pos; - Vector2 secondVertex = vertices[(i+1) % 4].Pos; + Vector2 firstVertex = losVertices[i].Pos; + Vector2 secondVertex = losVertices[(i+1) % 4].Pos; Vector2 L = lightSourcePos - ((firstVertex + secondVertex) / 2.0f); @@ -547,121 +691,144 @@ namespace Barotrauma.Lights -(secondVertex.Y - firstVertex.Y), secondVertex.X - firstVertex.X); - backFacing[i] = (Vector2.Dot(N, L) < 0) == los; + backFacing[i] = (Vector2.Dot(N, L) < 0); } - //find beginning and ending vertices which - //belong to the shadow - int startingIndex = -1; - int endingIndex = -1; - for (int i = 0; i < 4; i++) + ShadowVertexCount = 0; + for (int i=0;i<4;i++) { - int currentEdge = i; - int nextEdge = (i + 1) % 4; + if (!backFacing[i]) { continue; } + int currentIndex = i; + Vector3 vertexPos0 = new Vector3(losOffsets[currentIndex]?.A ?? losVertices[currentIndex].Pos, 0.0f); + Vector3 vertexPos1 = new Vector3(losOffsets[currentIndex]?.B ?? losVertices[(currentIndex + 1) % 4].Pos, 0.0f); - if (backFacing[currentEdge] && !backFacing[nextEdge]) - endingIndex = nextEdge; + if (Vector3.DistanceSquared(vertexPos0, vertexPos1) < 1.0f) { continue; } - if (!backFacing[currentEdge] && backFacing[nextEdge]) - startingIndex = nextEdge; - } + Vector3 L2P0 = vertexPos0 - new Vector3(lightSourcePos, 0); + L2P0.Normalize(); + Vector3 extruded0 = new Vector3(lightSourcePos, 0) + L2P0 * 9000; - if (startingIndex == -1 || endingIndex == -1) { return; } + Vector3 L2P1 = vertexPos1 - new Vector3(lightSourcePos, 0); + L2P1.Normalize(); + Vector3 extruded1 = new Vector3(lightSourcePos, 0) + L2P1 * 9000; - //nr of vertices that are in the shadow - if (endingIndex > startingIndex) - ShadowVertexCount = endingIndex - startingIndex + 1; - else - ShadowVertexCount = 4 + 1 - startingIndex + endingIndex; - - //shadowVertices = new VertexPositionColor[shadowVertexCount * 2]; - - //create a triangle strip that has the shape of the shadow - int currentIndex = startingIndex; - int svCount = 0; - while (svCount != ShadowVertexCount * 2) - { - Vector3 vertexPos = new Vector3(vertices[currentIndex].Pos, 0.0f); - - int i = los ? svCount : svCount + 1; - int j = los ? svCount + 1 : svCount; - - //one vertex on the hull - ShadowVertices[i] = new VertexPositionColor + ShadowVertices[ShadowVertexCount + 0] = new VertexPositionColor { - Color = los ? Color.Black : Color.Transparent, - Position = vertexPos + offset + Color = Color.Black, + Position = vertexPos1 + offset }; - //one extruded by the light direction - ShadowVertices[j] = new VertexPositionColor + ShadowVertices[ShadowVertexCount + 1] = new VertexPositionColor { - Color = ShadowVertices[i].Color + Color = Color.Black, + Position = vertexPos0 + offset }; - Vector3 L2P = vertexPos - new Vector3(lightSourcePos, 0); - L2P.Normalize(); - - ShadowVertices[j].Position = new Vector3(lightSourcePos, 0) + L2P * 9000 + offset; + ShadowVertices[ShadowVertexCount + 2] = new VertexPositionColor + { + Color = Color.Black, + Position = extruded0 + offset + }; - svCount += 2; - currentIndex = (currentIndex + 1) % 4; + ShadowVertices[ShadowVertexCount + 3] = new VertexPositionColor + { + Color = Color.Black, + Position = vertexPos1 + offset + }; + + ShadowVertices[ShadowVertexCount + 4] = new VertexPositionColor + { + Color = Color.Black, + Position = extruded0 + offset + }; + + ShadowVertices[ShadowVertexCount + 5] = new VertexPositionColor + { + Color = Color.Black, + Position = extruded1 + offset + }; + + ShadowVertexCount += 6; } - if (los) - { - CalculatePenumbraVertices(startingIndex, endingIndex, lightSourcePos, los); - } + CalculateLosPenumbraVertices(lightSourcePos); } - private void CalculatePenumbraVertices(int startingIndex, int endingIndex, Vector2 lightSourcePos, bool los) + private void CalculateLosPenumbraVertices(Vector2 lightSourcePos) { - var vertices = los ? losVertices : this.vertices; - Vector3 offset = Vector3.Zero; if (ParentEntity != null && ParentEntity.Submarine != null) { offset = new Vector3(ParentEntity.Submarine.DrawPosition.X, ParentEntity.Submarine.DrawPosition.Y, 0.0f); } - for (int n = 0; n < 4; n += 3) + PenumbraVertexCount = 0; + for (int i = 0; i < 4; i++) { - Vector3 penumbraStart = new Vector3((n == 0) ? vertices[startingIndex].Pos : vertices[endingIndex].Pos, 0.0f); + int currentIndex = i; + int prevIndex = (i + 3) % 4; + int nextIndex = (i + 1) % 4; + bool disjointed = losOffsets[i]?.A != null; + Vector2 vertexPos0 = losOffsets[currentIndex]?.A ?? losVertices[currentIndex].Pos; + Vector2 vertexPos1 = losOffsets[currentIndex]?.B ?? losVertices[nextIndex].Pos; - PenumbraVertices[n] = new VertexPositionTexture + if (Vector2.DistanceSquared(vertexPos0, vertexPos1) < 1.0f) { continue; } + + if (backFacing[currentIndex] && (disjointed || (!backFacing[prevIndex]))) { - Position = penumbraStart + offset, - TextureCoordinate = new Vector2(0.0f, 1.0f) - }; + Vector3 penumbraStart = new Vector3(vertexPos0, 0.0f); - for (int i = 0; i < 2; i++) - { - PenumbraVertices[n + i + 1] = new VertexPositionTexture(); - Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0); - vertexDir.Normalize(); - - Vector3 normal = (i == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f; - if (n > 0) normal = -normal; - - vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) - normal * 20.0f); - vertexDir.Normalize(); - PenumbraVertices[n + i + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset; - - if (los) + PenumbraVertices[PenumbraVertexCount] = new VertexPositionTexture { - PenumbraVertices[n + i + 1].TextureCoordinate = (i == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f); - } - else + Position = penumbraStart + offset, + TextureCoordinate = new Vector2(0.0f, 1.0f) + }; + + for (int j = 0; j < 2; j++) { - PenumbraVertices[n + i + 1].TextureCoordinate = (i == 0) ? new Vector2(1.0f, 0.0f) : Vector2.Zero; + PenumbraVertices[PenumbraVertexCount + j + 1] = new VertexPositionTexture(); + Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0); + vertexDir.Normalize(); + + Vector3 normal = (j == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f; + + vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) - normal * 20.0f); + vertexDir.Normalize(); + PenumbraVertices[PenumbraVertexCount + j + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset; + + PenumbraVertices[PenumbraVertexCount + j + 1].TextureCoordinate = (j == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f); } + + PenumbraVertexCount += 3; } - if (n > 0) + disjointed = losOffsets[i]?.B != null; + if (backFacing[currentIndex] && (disjointed || (!backFacing[nextIndex]))) { - var temp = PenumbraVertices[4]; - PenumbraVertices[4] = PenumbraVertices[5]; - PenumbraVertices[5] = temp; + Vector3 penumbraStart = new Vector3(vertexPos1, 0.0f); + + PenumbraVertices[PenumbraVertexCount] = new VertexPositionTexture + { + Position = penumbraStart + offset, + TextureCoordinate = new Vector2(0.0f, 1.0f) + }; + + for (int j = 0; j < 2; j++) + { + PenumbraVertices[PenumbraVertexCount + (1 - j) + 1] = new VertexPositionTexture(); + Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0); + vertexDir.Normalize(); + + Vector3 normal = (j == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f; + + vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) + normal * 20.0f); + vertexDir.Normalize(); + PenumbraVertices[PenumbraVertexCount + (1 - j) + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset; + + PenumbraVertices[PenumbraVertexCount + (1 - j) + 1].TextureCoordinate = (j == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f); + } + + PenumbraVertexCount += 3; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 46c22c1ae..576d16170 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -381,7 +381,7 @@ namespace Barotrauma.Lights if (GUI.DisableItemHighlights) { return false; } highlightedEntities.Clear(); - if (Character.Controlled != null) + if (Character.Controlled != null && (!Character.Controlled.IsKeyDown(InputType.Aim) || Character.Controlled.SelectedItems.Any(it => it?.GetComponent() == null))) { if (Character.Controlled.FocusedItem != null) { @@ -532,28 +532,16 @@ namespace Barotrauma.Lights Vector2 relativeLightPos = pos; if (convexHull.ParentEntity?.Submarine != null) relativeLightPos -= convexHull.ParentEntity.Submarine.Position; - convexHull.CalculateShadowVertices(relativeLightPos, true); + convexHull.CalculateLosVertices(relativeLightPos); - //convert triangle strips to a triangle list - for (int i = 0; i < convexHull.ShadowVertexCount * 2 - 2; i++) + for (int i = 0; i < convexHull.ShadowVertexCount; i++) { - if (i % 2 == 0) - { - shadowVerts.Add(convexHull.ShadowVertices[i]); - shadowVerts.Add(convexHull.ShadowVertices[i + 1]); - shadowVerts.Add(convexHull.ShadowVertices[i + 2]); - } - else - { - shadowVerts.Add(convexHull.ShadowVertices[i]); - shadowVerts.Add(convexHull.ShadowVertices[i + 2]); - shadowVerts.Add(convexHull.ShadowVertices[i + 1]); - } + shadowVerts.Add(convexHull.ShadowVertices[i]); } - if (convexHull.ShadowVertexCount > 0) + for (int i = 0; i < convexHull.PenumbraVertexCount; i++) { - penumbraVerts.AddRange(convexHull.PenumbraVertices); + penumbraVerts.Add(convexHull.PenumbraVertices[i]); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 76bcc63ae..43655583c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -140,7 +140,7 @@ namespace Barotrauma.Lights private short[] indices; private List hullsInRange; - + public Texture2D texture; public SpriteEffects LightSpriteEffect; @@ -300,6 +300,14 @@ namespace Barotrauma.Lights } } + public float TextureRange + { + get + { + return lightSourceParams.TextureRange; + } + } + /// /// Background lights are drawn behind submarines and they don't cast shadows. /// @@ -366,7 +374,7 @@ namespace Barotrauma.Lights var fullChList = ConvexHull.HullLists.Find(x => x.Submarine == sub); if (fullChList == null) return; - chList.List = fullChList.List.FindAll(ch => ch.Enabled && MathUtils.CircleIntersectsRectangle(lightPos, Range, ch.BoundingBox)); + chList.List = fullChList.List.FindAll(ch => ch.Enabled && MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, ch.BoundingBox)); NeedsHullCheck = true; } @@ -418,7 +426,7 @@ namespace Barotrauma.Lights subBorders.Location += sub.HiddenSubPosition.ToPoint() - new Point(0, sub.Borders.Height); //only draw if the light overlaps with the sub - if (!MathUtils.CircleIntersectsRectangle(lightPos, Range, subBorders)) + if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, subBorders)) { if (chList.List.Count > 0) NeedsRecalculation = true; chList.List.Clear(); @@ -452,7 +460,7 @@ namespace Barotrauma.Lights subBorders.Location += sub.HiddenSubPosition.ToPoint() - new Point(0, sub.Borders.Height); //don't draw any shadows if the light doesn't overlap with the borders of the sub - if (!MathUtils.CircleIntersectsRectangle(lightPos, Range, subBorders)) + if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, subBorders)) { if (chList.List.Count > 0) NeedsRecalculation = true; chList.List.Clear(); @@ -495,7 +503,7 @@ namespace Barotrauma.Lights { foreach (ConvexHull hull in chList.List) { - if (!chList.IsHidden.Contains(hull)) hulls.Add(hull); + if (!chList.IsHidden.Contains(hull)) { hulls.Add(hull); } } foreach (ConvexHull hull in chList.List) { @@ -503,7 +511,7 @@ namespace Barotrauma.Lights } } - float bounds = Range * 2; + float bounds = TextureRange; //find convexhull segments that are close enough and facing towards the light source List visibleSegments = new List(); List points = new List(); @@ -513,6 +521,49 @@ namespace Barotrauma.Lights hull.GetVisibleSegments(drawPos, visibleSegments, ignoreEdges: false); } + //add a square-shaped boundary to make sure we've got something to construct the triangles from + //even if there aren't enough hull segments around the light source + + //(might be more effective to calculate if we actually need these extra points) + + Vector2 drawOffset = Vector2.Zero; + float boundsExtended = bounds; + if (OverrideLightTexture != null) + { + float cosAngle = (float)Math.Cos(Rotation); + float sinAngle = -(float)Math.Sin(Rotation); + + var overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height); + + Vector2 origin = OverrideLightTexture.Origin; + + origin /= Math.Max(overrideTextureDims.X, overrideTextureDims.Y); + origin -= Vector2.One * 0.5f; + + if (Math.Abs(origin.X) >= 0.45f || Math.Abs(origin.Y) >= 0.45f) + { + boundsExtended += 5.0f; + } + + origin *= TextureRange; + + drawOffset.X = -origin.X * cosAngle - origin.Y * sinAngle; + drawOffset.Y = origin.X * sinAngle + origin.Y * cosAngle; + } + + var boundaryCorners = new SegmentPoint[] { + new SegmentPoint(new Vector2(drawPos.X + drawOffset.X + boundsExtended, drawPos.Y + drawOffset.Y + boundsExtended), null), + new SegmentPoint(new Vector2(drawPos.X + drawOffset.X + boundsExtended, drawPos.Y + drawOffset.Y - boundsExtended), null), + new SegmentPoint(new Vector2(drawPos.X + drawOffset.X - boundsExtended, drawPos.Y + drawOffset.Y - boundsExtended), null), + new SegmentPoint(new Vector2(drawPos.X + drawOffset.X - boundsExtended, drawPos.Y + drawOffset.Y + boundsExtended), null) + }; + + for (int i = 0; i < 4; i++) + { + var s = new Segment(boundaryCorners[i], boundaryCorners[(i + 1) % 4], null); + visibleSegments.Add(s); + } + //Generate new points at the intersections between segments //This is necessary for the light volume to generate properly on some subs for (int i = 0; i < visibleSegments.Count; i++) @@ -532,10 +583,10 @@ namespace Barotrauma.Lights Vector2 p2a = visibleSegments[j].Start.WorldPos; Vector2 p2b = visibleSegments[j].End.WorldPos; - if (Vector2.DistanceSquared(p1a, p2a) < 25.0f || - Vector2.DistanceSquared(p1a, p2b) < 25.0f || - Vector2.DistanceSquared(p1b, p2a) < 25.0f || - Vector2.DistanceSquared(p1b, p2b) < 25.0f) + if (Vector2.DistanceSquared(p1a, p2a) < 5.0f || + Vector2.DistanceSquared(p1a, p2b) < 5.0f || + Vector2.DistanceSquared(p1b, p2a) < 5.0f || + Vector2.DistanceSquared(p1b, p2b) < 5.0f) { continue; } @@ -565,8 +616,8 @@ namespace Barotrauma.Lights mid.Pos -= visibleSegments[i].ConvexHull.ParentEntity.Submarine.DrawPosition; } - if (Vector2.DistanceSquared(start.WorldPos, mid.WorldPos) < 25.0f || - Vector2.DistanceSquared(end.WorldPos, mid.WorldPos) < 25.0f) + if (Vector2.DistanceSquared(start.WorldPos, mid.WorldPos) < 5.0f || + Vector2.DistanceSquared(end.WorldPos, mid.WorldPos) < 5.0f) { continue; } @@ -580,6 +631,7 @@ namespace Barotrauma.Lights { IsHorizontal = visibleSegments[i].IsHorizontal }; + visibleSegments[i] = seg1; visibleSegments.Insert(i + 1, seg2); i--; @@ -588,34 +640,27 @@ namespace Barotrauma.Lights } } - foreach (Segment s in visibleSegments) + //remove segments that fall out of bounds + for (int i = 0; i < visibleSegments.Count; i++) { - points.Add(s.Start); - points.Add(s.End); - if (Math.Abs(s.Start.WorldPos.X - drawPos.X) > bounds) bounds = Math.Abs(s.Start.WorldPos.X - drawPos.X); - if (Math.Abs(s.Start.WorldPos.Y - drawPos.Y) > bounds) bounds = Math.Abs(s.Start.WorldPos.Y - drawPos.Y); - if (Math.Abs(s.End.WorldPos.X - drawPos.X) > bounds) bounds = Math.Abs(s.End.WorldPos.X - drawPos.X); - if (Math.Abs(s.End.WorldPos.Y - drawPos.Y) > bounds) bounds = Math.Abs(s.End.WorldPos.Y - drawPos.Y); + Segment s = visibleSegments[i]; + if (Math.Abs(s.Start.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f || + Math.Abs(s.Start.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f || + Math.Abs(s.End.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f || + Math.Abs(s.End.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f) + { + visibleSegments.RemoveAt(i); + i--; + } + else + { + points.Add(s.Start); + points.Add(s.End); + } } - //add a square-shaped boundary to make sure we've got something to construct the triangles from - //even if there aren't enough hull segments around the light source + visibleSegments = visibleSegments.OrderBy(s => MathUtils.LineToPointDistanceSquared(s.Start.WorldPos, s.End.WorldPos, drawPos)).ToList(); - //(might be more effective to calculate if we actually need these extra points) - var boundaryCorners = new List { - new SegmentPoint(new Vector2(drawPos.X + bounds, drawPos.Y + bounds), null), - new SegmentPoint(new Vector2(drawPos.X + bounds, drawPos.Y - bounds), null), - new SegmentPoint(new Vector2(drawPos.X - bounds, drawPos.Y - bounds), null), - new SegmentPoint(new Vector2(drawPos.X - bounds, drawPos.Y + bounds), null) - }; - - points.AddRange(boundaryCorners); - - for (int i = 0; i < 4; i++) - { - visibleSegments.Add(new Segment(boundaryCorners[i], boundaryCorners[(i + 1) % 4], null)); - } - var compareCCW = new CompareSegmentPointCW(drawPos); try { @@ -635,13 +680,15 @@ namespace Barotrauma.Lights //List> preOutput = new List>(); //remove points that are very close to each other - for (int i = 0; i < points.Count - 1; i++) + for (int i = 0; i < points.Count; i++) { - if (Math.Abs(points[i].WorldPos.X - points[i + 1].WorldPos.X) < 6 && - Math.Abs(points[i].WorldPos.Y - points[i + 1].WorldPos.Y) < 6) + for (int j = Math.Min(i + 4, points.Count-1); j > i; j--) { - points.RemoveAt(i + 1); - i--; + if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < 6 && + Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < 6) + { + points.RemoveAt(j); + } } } @@ -651,16 +698,16 @@ namespace Barotrauma.Lights Vector2 dirNormal = new Vector2(-dir.Y, dir.X) * 3; //do two slightly offset raycasts to hit the segment itself and whatever's behind it - Pair intersection1 = RayCast(drawPos, drawPos + dir * bounds * 2 - dirNormal, visibleSegments); - Pair intersection2 = RayCast(drawPos, drawPos + dir * bounds * 2 + dirNormal, visibleSegments); + Pair intersection1 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 - dirNormal, visibleSegments); + Pair intersection2 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 + dirNormal, visibleSegments); - if (intersection1.First < 0) return new List(); - if (intersection2.First < 0) return new List(); + if (intersection1.First < 0) return null; + if (intersection2.First < 0) return null; Segment seg1 = visibleSegments[intersection1.First]; Segment seg2 = visibleSegments[intersection2.First]; - bool isPoint1 = MathUtils.LineToPointDistance(seg1.Start.WorldPos, seg1.End.WorldPos, p.WorldPos) < 5.0f; - bool isPoint2 = MathUtils.LineToPointDistance(seg2.Start.WorldPos, seg2.End.WorldPos, p.WorldPos) < 5.0f; + bool isPoint1 = MathUtils.LineToPointDistanceSquared(seg1.Start.WorldPos, seg1.End.WorldPos, p.WorldPos) < 25.0f; + bool isPoint2 = MathUtils.LineToPointDistanceSquared(seg2.Start.WorldPos, seg2.End.WorldPos, p.WorldPos) < 25.0f; if (isPoint1 && isPoint2) { @@ -696,11 +743,13 @@ namespace Barotrauma.Lights //remove points that are very close to each other for (int i = 0; i < output.Count - 1; i++) { - if (Math.Abs(output[i].X - output[i + 1].X) < 6 && - Math.Abs(output[i].Y - output[i + 1].Y) < 6) + for (int j = Math.Min(i + 4, output.Count - 1); j > i; j--) { - output.RemoveAt(i + 1); - i--; + if (Math.Abs(output[i].X - output[j].X) < 6 && + Math.Abs(output[i].Y - output[j].Y) < 6) + { + output.RemoveAt(j); + } } } @@ -709,7 +758,6 @@ namespace Barotrauma.Lights private Pair RayCast(Vector2 rayStart, Vector2 rayEnd, List segments) { - float closestDist = float.PositiveInfinity; Vector2? closestIntersection = null; int segment = -1; @@ -724,11 +772,11 @@ namespace Barotrauma.Lights //segment's end position always has a higher or equal y coordinate than the start position //so we can do this comparison and skip segments that are at the wrong side of the ray - if (s.End.WorldPos.Y < s.Start.WorldPos.Y) + /*if (s.End.WorldPos.Y < s.Start.WorldPos.Y) { System.Diagnostics.Debug.Assert(s.End.WorldPos.Y >= s.Start.WorldPos.Y, "LightSource raycast failed. Segment's end positions should never be below the start position. Parent entity: " + (s.ConvexHull?.ParentEntity == null ? "null" : s.ConvexHull.ParentEntity.ToString())); - } + }*/ if (s.Start.WorldPos.Y > maxY || s.End.WorldPos.Y < minY) { continue; } //same for the x-axis if (s.Start.WorldPos.X > s.End.WorldPos.X) @@ -741,18 +789,29 @@ namespace Barotrauma.Lights if (s.End.WorldPos.X < minX) continue; if (s.Start.WorldPos.X > maxX) continue; } - - if (s.IsAxisAligned ? - MathUtils.GetAxisAlignedLineIntersection(rayStart, rayEnd, s.Start.WorldPos, s.End.WorldPos, s.IsHorizontal, out Vector2 intersection) : - MathUtils.GetLineIntersection(rayStart, rayEnd, s.Start.WorldPos, s.End.WorldPos, out intersection)) + + bool intersects; + Vector2 intersection; + if (s.IsAxisAligned) { - float dist = Vector2.DistanceSquared(intersection, rayStart); - if (dist < closestDist) - { - closestDist = dist; - closestIntersection = intersection; - segment = i; - } + intersects = MathUtils.GetAxisAlignedLineIntersection(rayStart, rayEnd, s.Start.WorldPos, s.End.WorldPos, s.IsHorizontal, out intersection); + } + else + { + intersects = MathUtils.GetLineIntersection(rayStart, rayEnd, s.Start.WorldPos, s.End.WorldPos, out intersection); + } + + if (intersects) + { + closestIntersection = intersection; + + rayEnd = intersection; + minX = Math.Min(rayStart.X, rayEnd.X); + maxX = Math.Max(rayStart.X, rayEnd.X); + minY = Math.Min(rayStart.Y, rayEnd.Y); + maxY = Math.Max(rayStart.Y, rayEnd.Y); + + segment = i; } } @@ -797,7 +856,7 @@ namespace Barotrauma.Lights //hacky fix to exc excessively large light volumes (they used to be up to 4x the range of the light if there was nothing to block the rays). //might want to tweak the raycast logic in a way that this isn't necessary - float boundRadius = Range * 1.1f / (1.0f - Math.Max(Math.Abs(uvOffset.X), Math.Abs(uvOffset.Y))); + /*float boundRadius = Range * 1.1f / (1.0f - Math.Max(Math.Abs(uvOffset.X), Math.Abs(uvOffset.Y))); Rectangle boundArea = new Rectangle((int)(drawPos.X - boundRadius), (int)(drawPos.Y + boundRadius), (int)(boundRadius * 2), (int)(boundRadius * 2)); for (int i = 0; i < rayCastHits.Count; i++) { @@ -805,7 +864,7 @@ namespace Barotrauma.Lights { rayCastHits[i] = intersection; } - } + }*/ // Add all the other encounter points as vertices // storing their world position as UV coordinates @@ -818,7 +877,7 @@ namespace Barotrauma.Lights //so we can add new vertices based on these normals Vector2 prevVertex = rayCastHits[i > 0 ? i - 1 : rayCastHits.Count - 1]; Vector2 nextVertex = rayCastHits[i < rayCastHits.Count - 1 ? i + 1 : 0]; - + Vector2 rawDiff = vertex - drawPos; //calculate normal of first segment @@ -836,12 +895,18 @@ namespace Barotrauma.Lights //if the normal is pointing towards the light origin //rather than away from it, invert it if (Vector2.DistanceSquared(nDiff2, rawDiff) > Vector2.DistanceSquared(-nDiff2, rawDiff)) nDiff2 = -nDiff2; - + //add the normals together and use some magic numbers to create //a somewhat useful/good-looking blur - Vector2 nDiff = nDiff1 + nDiff2; - nDiff /= Math.Max(Math.Abs(nDiff.X), Math.Abs(nDiff.Y)); - nDiff *= 50.0f; + Vector2 nDiff = nDiff1 * 40.0f; + if (MathUtils.GetLineIntersection(vertex + (nDiff1 * 40.0f), nextVertex + (nDiff1 * 40.0f), vertex + (nDiff2 * 40.0f), prevVertex + (nDiff2 * 40.0f), true, out Vector2 intersection)) + { + nDiff = intersection - vertex; + if (nDiff.LengthSquared() > 10000.0f) + { + nDiff /= Math.Max(Math.Abs(nDiff.X), Math.Abs(nDiff.Y)); nDiff *= 100.0f; + } + } Vector2 diff = rawDiff; diff /= Range * 2.0f; @@ -948,6 +1013,50 @@ namespace Barotrauma.Lights /// public void DrawSprite(SpriteBatch spriteBatch, Camera cam) { + if (GameMain.DebugDraw) + { + Vector2 drawPos = position; + if (ParentSub != null) + { + drawPos += ParentSub.DrawPosition; + } + drawPos.Y = -drawPos.Y; + + float cosAngle = (float)Math.Cos(Rotation); + float sinAngle = -(float)Math.Sin(Rotation); + + float bounds = TextureRange; + + if (OverrideLightTexture != null) + { + var overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height); + + Vector2 origin = OverrideLightTexture.Origin; + + origin /= Math.Max(overrideTextureDims.X, overrideTextureDims.Y); + origin *= TextureRange; + + drawPos.X += origin.X * sinAngle + origin.Y * cosAngle; + drawPos.Y += origin.X * cosAngle + origin.Y * sinAngle; + } + + //add a square-shaped boundary to make sure we've got something to construct the triangles from + //even if there aren't enough hull segments around the light source + + //(might be more effective to calculate if we actually need these extra points) + var boundaryCorners = new SegmentPoint[] { + new SegmentPoint(new Vector2(drawPos.X + bounds, drawPos.Y + bounds), null), + new SegmentPoint(new Vector2(drawPos.X + bounds, drawPos.Y - bounds), null), + new SegmentPoint(new Vector2(drawPos.X - bounds, drawPos.Y - bounds), null), + new SegmentPoint(new Vector2(drawPos.X - bounds, drawPos.Y + bounds), null) + }; + + for (int i=0;i<4;i++) + { + GUI.DrawLine(spriteBatch, boundaryCorners[i].Pos, boundaryCorners[(i + 1) % 4].Pos, Color.White, 0, 3); + } + } + if (DeformableLightSprite != null) { Vector2 origin = DeformableLightSprite.Origin; @@ -976,11 +1085,11 @@ namespace Barotrauma.Lights if (LightSprite != null) { Vector2 origin = LightSprite.Origin; - if (LightSpriteEffect == SpriteEffects.FlipHorizontally) + if ((LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) { origin.X = LightSprite.SourceRect.Width - origin.X; } - if (LightSpriteEffect == SpriteEffects.FlipVertically) + if ((LightSpriteEffect & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically) { origin.Y = LightSprite.SourceRect.Height - origin.Y; } @@ -1088,7 +1197,7 @@ namespace Barotrauma.Lights PrimitiveType.TriangleList, 0, 0, indexCount / 3 ); } - + public void Reset() { hullsInRange.Clear(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index d36f8cc27..d13f8c34c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -9,8 +9,6 @@ namespace Barotrauma { partial class Map { - public bool AllowDebugTeleport; - class MapAnim { public Location StartLocation; @@ -380,6 +378,7 @@ namespace Barotrauma Location prevLocation = CurrentDisplayLocation; CurrentLocation = HighlightedLocation; Level.Loaded.DebugSetStartLocation(CurrentLocation); + Level.Loaded.DebugSetEndLocation(null); CurrentLocation.Discovered = true; CurrentLocation.CreateStore(); @@ -573,7 +572,7 @@ namespace Barotrauma } } - if (GameMain.DebugDraw && location == HighlightedLocation) + if (GameMain.DebugDraw && location == HighlightedLocation && (!location.Discovered || !location.Type.HasOutpost)) { if (location.Reputation != null) { @@ -589,7 +588,7 @@ namespace Barotrauma Color barColor = ToolBox.GradientLerp(location.Reputation.NormalizedValue, Color.Red, Color.Yellow, Color.LightGreen); GUI.DrawRectangle(spriteBatch, bgRect, Color.Black * 0.8f, isFilled: true); GUI.DrawRectangle(spriteBatch, new Rectangle((int)dPos.X, (int)dPos.Y, (int)(location.Reputation.NormalizedValue * 255), 32), barColor, isFilled: true); - string reputationValue = location.Reputation.Value.ToString(CultureInfo.InvariantCulture); + string reputationValue = ((int)location.Reputation.Value).ToString(); Vector2 repValueSize = GUI.SubHeadingFont.MeasureString(reputationValue); GUI.DrawString(spriteBatch, dPos + (new Vector2(256, 32) / 2) - (repValueSize / 2), reputationValue, Color.White, Color.Black, font: GUI.SubHeadingFont); GUI.DrawRectangle(spriteBatch, new Rectangle((int)dPos.X, (int)dPos.Y, 256, 32), Color.White); @@ -607,12 +606,34 @@ namespace Barotrauma Vector2 nameSize = GUI.LargeFont.MeasureString(HighlightedLocation.Name); Vector2 typeSize = GUI.Font.MeasureString(HighlightedLocation.Type.Name); Vector2 size = new Vector2(Math.Max(nameSize.X, typeSize.X), nameSize.Y + typeSize.Y); + bool showReputation = HighlightedLocation.Discovered && HighlightedLocation.Type.HasOutpost && HighlightedLocation.Reputation != null; + string repLabelText = null, repValueText = null; + Vector2 repLabelSize = Vector2.Zero, repBarSize = Vector2.Zero; + if (showReputation) + { + repLabelText = TextManager.Get("reputation"); + repLabelSize = GUI.Font.MeasureString(repLabelText); + size.X = Math.Max(size.X, repLabelSize.X); + repBarSize = new Vector2(Math.Max(0.75f * size.X, 100), repLabelSize.Y); + size.X = Math.Max(size.X, (4.0f / 3.0f) * repBarSize.X); + size.Y += 2 * repLabelSize.Y + 4 + repBarSize.Y; + repValueText = ((int)HighlightedLocation.Reputation.Value).ToString(); + } GUI.Style.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); - GUI.DrawString(spriteBatch, pos - new Vector2(0.0f, size.Y / 2), - HighlightedLocation.Name, GUI.Style.TextColor * hudVisibility * 1.5f, font: GUI.LargeFont); - GUI.DrawString(spriteBatch, pos + new Vector2(0.0f, size.Y / 2 - GUI.Font.MeasureString(HighlightedLocation.Type.Name).Y), - HighlightedLocation.Type.Name, GUI.Style.TextColor * hudVisibility * 1.5f); + 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, GUI.Style.TextColor * hudVisibility * 1.5f, font: GUI.LargeFont); + topLeftPos += new Vector2(0.0f, nameSize.Y); + GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Type.Name, GUI.Style.TextColor * hudVisibility * 1.5f); + if (showReputation) + { + topLeftPos += new Vector2(0.0f, typeSize.Y + repLabelSize.Y); + GUI.DrawString(spriteBatch, topLeftPos, repLabelText, GUI.Style.TextColor * hudVisibility * 1.5f); + topLeftPos += new Vector2(0.0f, repLabelSize.Y + 4); + 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 + 4, repBarRect.Top), repValueText, GUI.Style.TextColor); + } } if (tooltip != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 5bb384020..90a63e7da 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -159,6 +159,31 @@ namespace Barotrauma if (PlayerInput.IsCtrlDown()) { +#if DEBUG + if (PlayerInput.KeyHit(Keys.D)) + { + bool terminate = false; + foreach (MapEntity entity in selectedList) + { + if (entity is Item item && item.GetComponent() is { } planter) + { + planter.Update(1.0f, cam); + for (var i = 0; i < planter.GrowableSeeds.Length; i++) + { + Growable seed = planter.GrowableSeeds[i]; + PlantSlot slot = planter.PlantSlots.ContainsKey(i) ? planter.PlantSlots[i] : Planter.NullSlot; + if (seed == null) { continue; } + + seed.CreateDebugHUD(planter, slot); + terminate = true; + break; + } + } + + if (terminate) { break; } + } + } +#endif if (PlayerInput.KeyHit(Keys.C)) { Copy(selectedList); @@ -897,8 +922,9 @@ namespace Barotrauma Clone(copiedList); var clones = mapEntityList.Except(prevEntities).ToList(); - var nonWireClones = clones.Where(c => !(c is Item item) || item.GetComponent() == null); + if (!nonWireClones.Any()) { nonWireClones = clones; } + Vector2 center = Vector2.Zero; nonWireClones.ForEach(c => center += c.WorldPosition); center = Submarine.VectorToWorldGrid(center / nonWireClones.Count()); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index fb552b9e6..3d3d46a0b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -185,9 +185,20 @@ namespace Barotrauma public override bool IsVisible(Rectangle worldView) { Rectangle worldRect = WorldRect; + Vector2 worldPos = WorldPosition; - if (worldRect.X > worldView.Right || worldRect.Right < worldView.X) { return false; } - if (worldRect.Y < worldView.Y - worldView.Height || worldRect.Y - worldRect.Height > worldView.Y) { return false; } + Vector2 min = new Vector2(worldRect.X, worldRect.Y - worldRect.Height); + Vector2 max = new Vector2(worldRect.Right, worldRect.Y); + foreach (DecorativeSprite decorativeSprite in Prefab.DecorativeSprites) + { + min.X = Math.Min(worldPos.X - decorativeSprite.Sprite.size.X * decorativeSprite.Sprite.RelativeOrigin.X * decorativeSprite.Scale * Scale, min.X); + max.X = Math.Max(worldPos.X + decorativeSprite.Sprite.size.X * (1.0f - decorativeSprite.Sprite.RelativeOrigin.X) * decorativeSprite.Scale * Scale, max.X); + min.Y = Math.Min(worldPos.Y - decorativeSprite.Sprite.size.Y * (1.0f - decorativeSprite.Sprite.RelativeOrigin.Y) * decorativeSprite.Scale * Scale, min.Y); + max.Y = Math.Max(worldPos.Y + decorativeSprite.Sprite.size.Y * decorativeSprite.Sprite.RelativeOrigin.Y * decorativeSprite.Scale * Scale, max.Y); + } + + if (min.X > worldView.Right || max.X < worldView.X) { return false; } + if ( min.Y > worldView.Y || max.Y < worldView.Y - worldView.Height) { return false; } return true; } @@ -455,8 +466,16 @@ namespace Barotrauma public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { byte sectionCount = msg.ReadByte(); - if (sectionCount != Sections.Length) + + bool invalidMessage = false; + if (type != ServerNetObject.ENTITY_EVENT && type != ServerNetObject.ENTITY_EVENT_INITIAL) { + DebugConsole.NewMessage($"Error while reading a network event for the structure \"{Name} ({ID})\". Invalid event type ({type}).", Color.Red); + return; + } + else if (sectionCount != Sections.Length) + { + invalidMessage = true; string errorMsg = $"Error while reading a network event for the structure \"{Name} ({ID})\". Section count does not match (server: {sectionCount} client: {Sections.Length})"; DebugConsole.NewMessage(errorMsg, Color.Red); GameAnalyticsManager.AddErrorEventOnce("Structure.ClientRead:SectionCountMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); @@ -465,7 +484,7 @@ namespace Barotrauma for (int i = 0; i < sectionCount; i++) { float damage = msg.ReadRangedSingle(0.0f, 1.0f, 8) * MaxHealth; - if (i < Sections.Length) + if (!invalidMessage && i < Sections.Length) { SetDamage(i, damage); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 5a97c0294..ff303859b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -11,6 +11,7 @@ using Barotrauma.IO; using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; +using System.Globalization; namespace Barotrauma { @@ -19,6 +20,7 @@ namespace Barotrauma public Sound Sound; public readonly float Volume; public readonly float Range; + public readonly Vector2 FrequencyMultiplierRange; public readonly bool Stream; public string Filename @@ -32,8 +34,34 @@ namespace Barotrauma Stream = sound.Stream; Range = element.GetAttributeFloat("range", 1000.0f); Volume = element.GetAttributeFloat("volume", 1.0f); + FrequencyMultiplierRange = new Vector2(1.0f); + string freqMultAttr = element.GetAttributeString("frequencymultiplier", element.GetAttributeString("frequency", "1.0")); + if (!freqMultAttr.Contains(',')) + { + if (float.TryParse(freqMultAttr, NumberStyles.Any, CultureInfo.InvariantCulture, out float freqMult)) + { + FrequencyMultiplierRange = new Vector2(freqMult); + } + } + else + { + var freqMult = XMLExtensions.ParseVector2(freqMultAttr, false); + if (freqMult.Y >= 0.25f) + { + FrequencyMultiplierRange = freqMult; + } + } + if (FrequencyMultiplierRange.Y > 4.0f) + { + DebugConsole.ThrowError($"Loaded frequency range exceeds max value: {FrequencyMultiplierRange} (original string was \"{freqMultAttr}\")"); + } sound.IgnoreMuffling = element.GetAttributeBool("dontmuffle", false); } + + public float GetRandomFrequencyMultiplier() + { + return Rand.Range(FrequencyMultiplierRange.X, FrequencyMultiplierRange.Y); + } } partial class Submarine : Entity, IServerSerializable @@ -288,6 +316,27 @@ namespace Barotrauma } } + public static void DrawPaintedColors(SpriteBatch spriteBatch, bool editing = false, Predicate predicate = null) + { + var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.mapEntityList; + + foreach (MapEntity e in entitiesToRender) + { + if (e is Hull hull) + { + if (hull.SupportsPaintedColors) + { + if (predicate != null) + { + if (!predicate(e)) continue; + } + + hull.DrawSectionColors(spriteBatch); + } + } + } + } + public static void DrawBack(SpriteBatch spriteBatch, bool editing = false, Predicate predicate = null) { var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.mapEntityList; @@ -511,6 +560,11 @@ namespace Barotrauma public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { + if (type != ServerNetObject.ENTITY_POSITION) + { + DebugConsole.NewMessage($"Error while reading a network event for the submarine \"{Info.Name} ({ID})\". Invalid event type ({type}).", Color.Red); + } + var posInfo = PhysicsBody.ClientRead(type, msg, sendingTime, parentDebugName: Info.Name); msg.ReadPadBits(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 483989f52..725889e03 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -238,42 +238,7 @@ namespace Barotrauma return true; } - private bool EnterIDCardDesc(GUITextBox textBox, string text) - { - IdCardDesc = text; - textBox.Text = text; - textBox.Color = GUI.Style.Green; - - textBox.Deselect(); - - return true; - } - private bool EnterIDCardTags(GUITextBox textBox, string text) - { - IdCardTags = text.Split(','); - textBox.Text = string.Join(",", IdCardTags); - textBox.Flash(GUI.Style.Green); - textBox.Deselect(); - return true; - } - - private bool EnterTags(GUITextBox textBox, string text) - { - tags = text.Split(',').ToList(); - textBox.Text = string.Join(",", Tags); - textBox.Flash(GUI.Style.Green); - textBox.Deselect(); - return true; - } - - private bool TextBoxChanged(GUITextBox textBox, string text) - { - textBox.Color = GUI.Style.Red; - - return true; - } - - private GUIComponent CreateEditingHUD(bool inGame = false) + private GUIComponent CreateEditingHUD() { int width = 500; int height = spawnType == SpawnType.Path ? 80 : 200; @@ -326,21 +291,48 @@ namespace Barotrauma GUITextBox propertyBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), descText.RectTransform, Anchor.CenterRight), IdCardDesc) { MaxTextLength = 150, - OnEnterPressed = EnterIDCardDesc, ToolTip = TextManager.Get("IDCardDescriptionTooltip") }; - propertyBox.OnTextChanged += TextBoxChanged; + propertyBox.OnTextChanged += (textBox, text) => + { + IdCardDesc = text; + return true; + }; + propertyBox.OnEnterPressed += (textBox, text) => + { + IdCardDesc = text; + textBox.Flash(GUI.Style.Green); + return true; + }; + propertyBox.OnDeselected += (textBox, keys) => + { + IdCardDesc = textBox.Text; + textBox.Flash(GUI.Style.Green); + }; var idCardTagsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("IDCardTags"), font: GUI.SmallFont); propertyBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), idCardTagsText.RectTransform, Anchor.CenterRight), string.Join(", ", idCardTags)) { MaxTextLength = 60, - OnEnterPressed = EnterIDCardTags, ToolTip = TextManager.Get("IDCardTagsTooltip") }; - propertyBox.OnTextChanged += TextBoxChanged; - + propertyBox.OnTextChanged += (textBox, text) => + { + IdCardTags = text.Split(','); + return true; + }; + propertyBox.OnEnterPressed += (textBox, text) => + { + textBox.Text = string.Join(",", IdCardTags); + textBox.Flash(GUI.Style.Green); + return true; + }; + propertyBox.OnDeselected += (textBox, keys) => + { + textBox.Text = string.Join(",", IdCardTags); + textBox.Flash(GUI.Style.Green); + }; var jobsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("SpawnpointJobs"), font: GUI.SmallFont) @@ -368,12 +360,26 @@ namespace Barotrauma propertyBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), tagsText.RectTransform, Anchor.CenterRight), string.Join(", ", tags)) { MaxTextLength = 60, - OnEnterPressed = EnterTags, ToolTip = TextManager.Get("spawnpointtagstooltip") }; - propertyBox.OnTextChanged += TextBoxChanged; + propertyBox.OnTextChanged += (textBox, text) => + { + tags = text.Split(',').ToList(); + return true; + }; + propertyBox.OnEnterPressed += (textBox, text) => + { + textBox.Text = string.Join(",", tags); + textBox.Flash(GUI.Style.Green); + return true; + }; + propertyBox.OnDeselected += (textBox, keys) => + { + textBox.Text = string.Join(",", tags); + textBox.Flash(GUI.Style.Green); + }; } - + PositionEditingHUD(); return editingHUD; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs index 1dffec685..c9444afcb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Steam; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -7,12 +8,13 @@ namespace Barotrauma.Networking { partial class BannedPlayer { - public BannedPlayer(string name, UInt16 uniqueIdentifier, bool isRangeBan, string ip, ulong steamID) + public BannedPlayer(string name, UInt16 uniqueIdentifier, bool isRangeBan, string endPoint, ulong steamID) { this.Name = name; + this.EndPoint = endPoint; this.SteamID = steamID; + ParseEndPointAsSteamId(); this.IsRangeBan = isRangeBan; - this.IP = ip; this.UniqueIdentifier = uniqueIdentifier; } } @@ -66,13 +68,13 @@ namespace Barotrauma.Networking RelativeSpacing = 0.02f }; - string ip = bannedPlayer.IP; - if (localRangeBans.Contains(bannedPlayer.UniqueIdentifier)) ip = ToRange(ip); + string endPoint = bannedPlayer.EndPoint; + if (localRangeBans.Contains(bannedPlayer.UniqueIdentifier)) endPoint = ToRange(endPoint); GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), topArea.RectTransform), - bannedPlayer.Name + " (" + ip + ")"); + bannedPlayer.Name + " (" + endPoint + ")"); textBlock.RectTransform.MinSize = new Point(textBlock.Rect.Width, 0); - if (bannedPlayer.IP.IndexOf(".x") <= -1) + if (bannedPlayer.EndPoint.IndexOf(".x") <= -1) { var rangeBanButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.4f), topArea.RectTransform), TextManager.Get("BanRange"), style: "GUIButtonSmall") @@ -156,19 +158,19 @@ namespace Barotrauma.Networking UInt16 uniqueIdentifier = incMsg.ReadUInt16(); bool isRangeBan = incMsg.ReadBoolean(); incMsg.ReadPadBits(); - string ip = ""; + string endPoint = ""; UInt64 steamID = 0; if (isOwner) { - ip = incMsg.ReadString(); + endPoint = incMsg.ReadString(); steamID = incMsg.ReadUInt64(); } else { - ip = "IP concealed by host"; + endPoint = "Endpoint concealed by host"; steamID = 0; } - bannedPlayers.Add(new BannedPlayer(name, uniqueIdentifier, isRangeBan, ip, steamID)); + bannedPlayers.Add(new BannedPlayer(name, uniqueIdentifier, isRangeBan, endPoint, steamID)); } if (banFrame != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 581a504b7..d1a7c1502 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -520,11 +520,13 @@ namespace Barotrauma.Networking msgBox.Content.Parent.RectTransform.MinSize = new Point(0, (int)(msgBox.Content.RectTransform.MinSize.Y / msgBox.Content.RectTransform.RelativeSize.Y)); var okButton = msgBox.Buttons[0]; + okButton.OnClicked += msgBox.Close; var cancelButton = msgBox.Buttons[1]; + cancelButton.OnClicked += msgBox.Close; okButton.OnClicked += (GUIButton button, object obj) => { - clientPeer.SendPassword(passwordBox.Text); + clientPeer?.SendPassword(passwordBox.Text); requiresPw = false; return true; }; @@ -855,22 +857,6 @@ namespace Barotrauma.Networking traitorResults.Add(new TraitorMissionResult(inc)); } - if (GameMain.GameSession?.GameMode is CampaignMode mpCampaign) - { - if (inc.ReadBoolean()) - { - Dictionary clientUpgrades = UpgradeManager.GetMetadataLevels(mpCampaign.CampaignMetadata); - Dictionary serverUpgrades = new Dictionary(); - - int length = inc.ReadUInt16(); - for (int i = 0; i < length; i++) - { - serverUpgrades.Add(inc.ReadString(), inc.ReadByte()); - } - UpgradeManager.CompareUpgrades(clientUpgrades, serverUpgrades); - } - } - roundInitStatus = RoundInitStatus.Interrupted; CoroutineManager.StartCoroutine(EndGame(endMessage, traitorResults, transitionType), "EndGame"); break; @@ -934,7 +920,7 @@ namespace Barotrauma.Networking private void ReadStartGameFinalize(IReadMessage inc) { - TaskPool.ListTasks(null); + TaskPool.ListTasks(); ushort contentToPreloadCount = inc.ReadUInt16(); List contentToPreload = new List(); for (int i = 0; i < contentToPreloadCount; i++) @@ -1006,8 +992,19 @@ namespace Barotrauma.Networking } - private void OnDisconnect() + private void OnDisconnect(bool disableReconnect) { + CoroutineManager.StopCoroutines("WaitForStartingInfo"); + reconnectBox?.Close(); + reconnectBox = null; + + GameMain.Config.RestoreBackupPackages(); + + GUI.ClearCursorWait(); + + if (disableReconnect) { allowReconnect = false; } + if (!this.allowReconnect) { CancelConnect(); } + if (SteamManager.IsInitialized) { Steamworks.SteamFriends.ClearRichPresence(); @@ -1107,8 +1104,9 @@ namespace Barotrauma.Networking reconnectBox?.Close(); reconnectBox = new GUIMessageBox( - TextManager.Get("ConnectionLost"), - msg, new string[0]); + TextManager.Get("ConnectionLost"), msg, + new string[] { TextManager.Get("Cancel") }); + reconnectBox.Buttons[0].OnClicked += (btn, userdata) => { CancelConnect(); return true; }; connected = false; ConnectToServer(serverEndpoint, serverName); } @@ -1385,6 +1383,7 @@ namespace Barotrauma.Networking if (gameMode == null) { DebugConsole.ThrowError("Game mode \"" + modeIdentifier + "\" not found!"); + roundInitStatus = RoundInitStatus.Interrupted; yield return CoroutineStatus.Failure; } @@ -1415,11 +1414,13 @@ namespace Barotrauma.Networking int missionIndex = inc.ReadInt16(); if (!GameMain.NetLobbyScreen.TrySelectSub(subName, subHash, GameMain.NetLobbyScreen.SubList)) { + roundInitStatus = RoundInitStatus.Interrupted; yield return CoroutineStatus.Success; } if (!GameMain.NetLobbyScreen.TrySelectSub(shuttleName, shuttleHash, GameMain.NetLobbyScreen.ShuttleList.ListBox)) { + roundInitStatus = RoundInitStatus.Interrupted; yield return CoroutineStatus.Success; } @@ -1448,6 +1449,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.Select(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:FailedToSelectSub" + subName, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + roundInitStatus = RoundInitStatus.Interrupted; yield return CoroutineStatus.Failure; } if (GameMain.NetLobbyScreen.SelectedShuttle == null || @@ -1459,6 +1461,7 @@ namespace Barotrauma.Networking string errorMsg = "Failed to select shuttle \"" + shuttleName + "\" (hash: " + shuttleHash + ")."; DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:FailedToSelectShuttle" + shuttleName, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + roundInitStatus = RoundInitStatus.Interrupted; yield return CoroutineStatus.Failure; } @@ -1487,6 +1490,7 @@ namespace Barotrauma.Networking gameStarted = true; DebugConsole.ThrowError(errorMsg); GameMain.NetLobbyScreen.Select(); + roundInitStatus = RoundInitStatus.Interrupted; yield return CoroutineStatus.Failure; } else if (campaign.Map == null) @@ -1495,6 +1499,7 @@ namespace Barotrauma.Networking gameStarted = true; DebugConsole.ThrowError(errorMsg); GameMain.NetLobbyScreen.Select(); + roundInitStatus = RoundInitStatus.Interrupted; yield return CoroutineStatus.Failure; } @@ -1515,6 +1520,11 @@ namespace Barotrauma.Networking } } + if (GameMain.Client?.ServerSettings?.Voting != null) + { + GameMain.Client.ServerSettings.Voting.ResetVotes(GameMain.Client.ConnectedClients); + } + if (loadTask != null) { while (!loadTask.IsCompleted && !loadTask.IsFaulted && !loadTask.IsCanceled) @@ -1626,7 +1636,10 @@ namespace Barotrauma.Networking } } - if (respawnAllowed) { respawnManager = new RespawnManager(this, GameMain.NetLobbyScreen.UsingShuttle ? GameMain.NetLobbyScreen.SelectedShuttle : null); } + if (respawnAllowed) + { + respawnManager = new RespawnManager(this, GameMain.NetLobbyScreen.UsingShuttle && gameMode != GameModePreset.MultiPlayerCampaign ? GameMain.NetLobbyScreen.SelectedShuttle : null); + } gameStarted = true; ServerSettings.ServerDetailsChanged = true; @@ -1645,6 +1658,17 @@ namespace Barotrauma.Networking public IEnumerable EndGame(string endMessage, List traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) { + //round starting up, wait for it to finish + DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 60); + while (TaskPool.IsTaskRunning("AsyncCampaignStartRound")) + { + if (DateTime.Now > timeOut) + { + throw new Exception("Failed to end a round (async campaign round start timed out)."); + } + yield return new WaitForSeconds(1.0f); + } + if (!gameStarted) { GameMain.NetLobbyScreen.Select(); @@ -2086,7 +2110,6 @@ namespace Barotrauma.Networking { while ((objHeader = (ServerNetObject)inc.ReadByte()) != ServerNetObject.END_OF_MESSAGE) { - bool eventReadFailed = false; switch (objHeader) { case ServerNetObject.SYNC_IDS: @@ -2110,7 +2133,13 @@ namespace Barotrauma.Networking int msgEndPos = (int)(inc.BitPosition + msgLength * 8); var entity = Entity.FindEntityByID(id) as IServerSerializable; - if (entity != null) + 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; + } + + if (entity != null && (entity is Item || entity is Character || entity is Submarine)) { entity.ClientRead(objHeader.Value, inc, sendingTime); } @@ -2127,8 +2156,7 @@ namespace Barotrauma.Networking case ServerNetObject.ENTITY_EVENT_INITIAL: if (!entityEventManager.Read(objHeader.Value, inc, sendingTime, entities)) { - eventReadFailed = true; - break; + return; } break; case ServerNetObject.CHAT_MESSAGE: @@ -2138,16 +2166,11 @@ namespace Barotrauma.Networking throw new Exception($"Unknown object header \"{objHeader}\"!)"); } prevBitLength = inc.BitPosition - prevBitPos; - prevByteLength = inc.BytePosition - prevByteLength; + prevByteLength = inc.BytePosition - prevBytePos; prevObjHeader = objHeader; prevBitPos = inc.BitPosition; prevBytePos = inc.BytePosition; - - if (eventReadFailed) - { - break; - } } } @@ -2618,7 +2641,7 @@ namespace Barotrauma.Networking public void VoteForKick(Client votedClient) { if (votedClient == null) { return; } - votedClient.AddKickVote(ConnectedClients.First(c => c.ID == ID)); + votedClient.AddKickVote(ConnectedClients.FirstOrDefault(c => c.ID == myID)); Vote(VoteType.Kick, votedClient); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs index 0e06cf296..fe14cb666 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -164,6 +164,19 @@ namespace Barotrauma.Networking for (int i = 0; i < eventCount; i++) { + //16 = entity ID, 8 = msg length + if (msg.BitPosition + 16 + 8 > msg.LengthBits) + { + string errorMsg = $"Error while reading a message from the server. Entity event data exceeds the size of the buffer (current position: {msg.BitPosition}, length: {msg.LengthBits})."; + errorMsg += "\nPrevious entities:"; + for (int j = entities.Count - 1; j >= 0; j--) + { + errorMsg += "\n" + (entities[j] == null ? "NULL" : entities[j].ToString()); + } + DebugConsole.ThrowError(errorMsg); + return false; + } + UInt16 thisEventID = (UInt16)(firstEventID + (UInt16)i); UInt16 entityID = msg.ReadUInt16(); @@ -176,7 +189,7 @@ namespace Barotrauma.Networking } msg.ReadPadBits(); entities.Add(null); - if (thisEventID == (UInt16)(lastReceivedID + 1)) lastReceivedID++; + if (thisEventID == (UInt16)(lastReceivedID + 1)) { lastReceivedID++; } continue; } @@ -248,10 +261,8 @@ namespace Barotrauma.Networking errorMsg += "\n" + (entities[j] == null ? "NULL" : entities[j].ToString()); } - if (GameSettings.VerboseLogging) - { - DebugConsole.ThrowError("Failed to read event for entity \"" + entity.ToString() + "\"!", e); - } + DebugConsole.ThrowError("Failed to read event for entity \"" + entity.ToString() + "\"!", e); + GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:ReadFailed" + entity.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); msg.BitPosition = (int)(msgPosition + msgLength * 8); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index 6a8e0fe08..38f0f6fb2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -1,13 +1,56 @@ -using System; +using Barotrauma.Extensions; +using Barotrauma.Steam; +using System; using System.Collections.Generic; +using System.Linq; +using System.Reflection; using System.Text; namespace Barotrauma.Networking { abstract class ClientPeer { + protected class ServerContentPackage + { + public string Name; + public string Hash; + public UInt64 WorkshopId; + + public ContentPackage RegularPackage + { + get + { + return ContentPackage.RegularPackages.Find(p => p.MD5hash.Hash.Equals(Hash)); + } + } + + public ContentPackage CorePackage + { + get + { + return ContentPackage.CorePackages.Find(p => p.MD5hash.Hash.Equals(Hash)); + } + } + + public ServerContentPackage(string name, string hash, UInt64 workshopId) + { + Name = name; + Hash = hash; + WorkshopId = workshopId; + } + } + + protected string GetPackageStr(ContentPackage contentPackage) + { + return "\"" + contentPackage.Name + "\" (hash " + contentPackage.MD5hash.ShortHash + ")"; + } + protected string GetPackageStr(ServerContentPackage contentPackage) + { + return "\"" + contentPackage.Name + "\" (hash " + Md5Hash.GetShortHash(contentPackage.Hash) + ")"; + } + public delegate void MessageCallback(IReadMessage message); - public delegate void DisconnectCallback(); + public delegate void DisconnectCallback(bool disableReconnect); public delegate void DisconnectMessageCallback(string message); public delegate void PasswordCallback(int salt, int retries); public delegate void InitializationCompleteCallback(); @@ -25,11 +68,150 @@ namespace Barotrauma.Networking public NetworkConnection ServerConnection { get; protected set; } public abstract void Start(object endPoint, int ownerKey); - public abstract void Close(string msg = null); + public abstract void Close(string msg = null, bool disableReconnect = false); public abstract void Update(float deltaTime); public abstract void Send(IWriteMessage msg, DeliveryMethod deliveryMethod); public abstract void SendPassword(string password); + protected abstract void SendMsgInternal(DeliveryMethod deliveryMethod, IWriteMessage msg); + + protected ConnectionInitialization initializationStep; + protected bool contentPackageOrderReceived; + protected int ownerKey = 0; + protected int passwordSalt; + protected Steamworks.AuthTicket steamAuthTicket; + protected void ReadConnectionInitializationStep(IReadMessage inc) + { + ConnectionInitialization step = (ConnectionInitialization)inc.ReadByte(); + + IWriteMessage outMsg; + + switch (step) + { + case ConnectionInitialization.SteamTicketAndVersion: + if (initializationStep != ConnectionInitialization.SteamTicketAndVersion) { return; } + outMsg = new WriteOnlyMessage(); + outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); + outMsg.Write((byte)ConnectionInitialization.SteamTicketAndVersion); + outMsg.Write(Name); + outMsg.Write(ownerKey); + outMsg.Write(SteamManager.GetSteamID()); + if (steamAuthTicket == null) + { + outMsg.Write((UInt16)0); + } + else + { + outMsg.Write((UInt16)steamAuthTicket.Data.Length); + outMsg.Write(steamAuthTicket.Data, 0, steamAuthTicket.Data.Length); + } + outMsg.Write(GameMain.Version.ToString()); + outMsg.Write(GameMain.Config.Language); + + SendMsgInternal(DeliveryMethod.Reliable, outMsg); + break; + case ConnectionInitialization.ContentPackageOrder: + if (initializationStep == ConnectionInitialization.SteamTicketAndVersion || + initializationStep == ConnectionInitialization.Password) { initializationStep = ConnectionInitialization.ContentPackageOrder; } + if (initializationStep != ConnectionInitialization.ContentPackageOrder) { return; } + outMsg = new WriteOnlyMessage(); + outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); + outMsg.Write((byte)ConnectionInitialization.ContentPackageOrder); + + string serverName = inc.ReadString(); + + UInt32 cpCount = inc.ReadVariableUInt32(); + ServerContentPackage corePackage = null; + List regularPackages = new List(); + List missingPackages = new List(); + for (int i = 0; i < cpCount; i++) + { + string name = inc.ReadString(); + string hash = inc.ReadString(); + UInt64 workshopId = inc.ReadUInt64(); + var pkg = new ServerContentPackage(name, hash, workshopId); + if (pkg.CorePackage != null) + { + corePackage = pkg; + } + else if (pkg.RegularPackage != null) + { + regularPackages.Add(pkg); + } + else + { + missingPackages.Add(pkg); + } + } + + if (missingPackages.Count > 0) + { + var nonDownloadable = missingPackages.Where(p => p.WorkshopId == 0); + + if (nonDownloadable.Any()) + { + string disconnectMsg; + if (nonDownloadable.Count() == 1) + { + disconnectMsg = $"DisconnectMessage.MissingContentPackage~[missingcontentpackage]={GetPackageStr(missingPackages[0])}"; + } + else + { + List packageStrs = new List(); + nonDownloadable.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); + disconnectMsg = $"DisconnectMessage.MissingContentPackages~[missingcontentpackages]={string.Join(", ", packageStrs)}"; + } + Close(disconnectMsg, disableReconnect: true); + } + else + { + Close(disableReconnect: true); + + string missingModNames = "\n- " + string.Join("\n\n- ", missingPackages.Select(p => GetPackageStr(p))) + "\n\n"; + + var msgBox = new GUIMessageBox( + TextManager.Get("WorkshopItemDownloadTitle"), + TextManager.GetWithVariable("WorkshopItemDownloadPrompt", "[items]", missingModNames), + new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + msgBox.Buttons[0].OnClicked = (yesBtn, userdata) => + { + GameMain.ServerListScreen.Select(); + GameMain.ServerListScreen.DownloadWorkshopItems(missingPackages.Select(p => p.WorkshopId), serverName, ServerConnection.EndPointString); + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked = msgBox.Close; + } + + return; + } + + if (!contentPackageOrderReceived) + { + GameMain.Config.SwapPackages(corePackage.CorePackage, regularPackages.Select(p => p.RegularPackage).ToList()); + contentPackageOrderReceived = true; + } + + SendMsgInternal(DeliveryMethod.Reliable, outMsg); + break; + case ConnectionInitialization.Password: + if (initializationStep == ConnectionInitialization.SteamTicketAndVersion) { initializationStep = ConnectionInitialization.Password; } + if (initializationStep != ConnectionInitialization.Password) { return; } + bool incomingSalt = inc.ReadBoolean(); inc.ReadPadBits(); + int retries = 0; + if (incomingSalt) + { + passwordSalt = inc.ReadInt32(); + } + else + { + retries = inc.ReadInt32(); + } + OnRequestPassword?.Invoke(passwordSalt, retries); + break; + } + } + #if DEBUG public abstract void ForceTimeOut(); #endif diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index 58cf2f388..cd1c81315 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -14,11 +14,6 @@ namespace Barotrauma.Networking private NetClient netClient; private NetPeerConfiguration netPeerConfiguration; - private ConnectionInitialization initializationStep; - private bool contentPackageOrderReceived; - private int ownerKey; - private int passwordSalt; - private Steamworks.AuthTicket steamAuthTicket; List incomingLidgrenMessages; public LidgrenClientPeer(string name) @@ -128,7 +123,7 @@ namespace Barotrauma.Networking if (isConnectionInitializationStep && initializationStep != ConnectionInitialization.Success) { - ReadConnectionInitializationStep(inc); + ReadConnectionInitializationStep(new ReadWriteMessage(inc.Data, (int)inc.Position, inc.LengthBits, false)); } else { @@ -158,99 +153,6 @@ namespace Barotrauma.Networking } } - private void ReadConnectionInitializationStep(NetIncomingMessage inc) - { - if (!isActive) { return; } - - ConnectionInitialization step = (ConnectionInitialization)inc.ReadByte(); - //Console.WriteLine(step + " " + initializationStep); - NetOutgoingMessage outMsg; NetSendResult result; - - switch (step) - { - case ConnectionInitialization.SteamTicketAndVersion: - if (initializationStep != ConnectionInitialization.SteamTicketAndVersion) { return; } - outMsg = netClient.CreateMessage(); - outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); - outMsg.Write((byte)ConnectionInitialization.SteamTicketAndVersion); - outMsg.Write(Name); - outMsg.Write(ownerKey); - outMsg.Write(SteamManager.GetSteamID()); - if (steamAuthTicket == null) - { - outMsg.Write((UInt16)0); - } - else - { - outMsg.Write((UInt16)steamAuthTicket.Data.Length); - outMsg.Write(steamAuthTicket.Data, 0, steamAuthTicket.Data.Length); - } - - outMsg.Write(GameMain.Version.ToString()); - - IEnumerable mpContentPackages = GameMain.SelectedPackages.Where(cp => cp.HasMultiplayerIncompatibleContent); - outMsg.WriteVariableInt32(mpContentPackages.Count()); - foreach (ContentPackage contentPackage in mpContentPackages) - { - outMsg.Write(contentPackage.Name); - outMsg.Write(contentPackage.MD5hash.Hash); - } - - result = netClient.SendMessage(outMsg, NetDeliveryMethod.ReliableUnordered); - if (result != NetSendResult.Queued && result != NetSendResult.Sent) - { - DebugConsole.NewMessage("Failed to send "+initializationStep.ToString()+" message to host: " + result); - } - break; - case ConnectionInitialization.ContentPackageOrder: - if (initializationStep == ConnectionInitialization.SteamTicketAndVersion || - initializationStep == ConnectionInitialization.Password) { initializationStep = ConnectionInitialization.ContentPackageOrder; } - if (initializationStep != ConnectionInitialization.ContentPackageOrder) { return; } - outMsg = netClient.CreateMessage(); - outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); - outMsg.Write((byte)ConnectionInitialization.ContentPackageOrder); - - Int32 cpCount = inc.ReadVariableInt32(); - List serverContentPackages = new List(); - for (int i = 0; i < cpCount; i++) - { - string hash = inc.ReadString(); - serverContentPackages.Add(GameMain.Config.SelectedContentPackages.Find(cp => cp.MD5hash.Hash == hash)); - } - - if (!contentPackageOrderReceived) - { - GameMain.Config.ReorderSelectedContentPackages(cp => serverContentPackages.Contains(cp) ? - serverContentPackages.IndexOf(cp) : - serverContentPackages.Count + GameMain.Config.SelectedContentPackages.IndexOf(cp)); - contentPackageOrderReceived = true; - } - - result = netClient.SendMessage(outMsg, NetDeliveryMethod.ReliableUnordered); - if (result != NetSendResult.Queued && result != NetSendResult.Sent) - { - DebugConsole.NewMessage("Failed to send " + initializationStep.ToString() + " message to host: " + result); - } - - break; - case ConnectionInitialization.Password: - if (initializationStep == ConnectionInitialization.SteamTicketAndVersion) { initializationStep = ConnectionInitialization.Password; } - if (initializationStep != ConnectionInitialization.Password) { return; } - bool incomingSalt = inc.ReadBoolean(); inc.ReadPadBits(); - int retries = 0; - if (incomingSalt) - { - passwordSalt = inc.ReadInt32(); - } - else - { - retries = inc.ReadInt32(); - } - OnRequestPassword?.Invoke(passwordSalt, retries); - break; - } - } - public override void SendPassword(string password) { if (!isActive) { return; } @@ -269,7 +171,7 @@ namespace Barotrauma.Networking } } - public override void Close(string msg = null) + public override void Close(string msg = null, bool disableReconnect = false) { if (!isActive) { return; } @@ -278,7 +180,7 @@ namespace Barotrauma.Networking netClient.Shutdown(msg ?? TextManager.Get("Disconnecting")); netClient = null; steamAuthTicket?.Cancel(); steamAuthTicket = null; - OnDisconnect?.Invoke(); + OnDisconnect?.Invoke(disableReconnect); } public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod) @@ -320,6 +222,18 @@ namespace Barotrauma.Networking } } + protected override void SendMsgInternal(DeliveryMethod deliveryMethod, IWriteMessage msg) + { + NetOutgoingMessage lidgrenMsg = netClient.CreateMessage(); + lidgrenMsg.Write(msg.Buffer, 0, msg.LengthBytes); + + NetSendResult result = netClient.SendMessage(lidgrenMsg, NetDeliveryMethod.ReliableUnordered); + if (result != NetSendResult.Queued && result != NetSendResult.Sent) + { + DebugConsole.NewMessage("Failed to send message to host: " + result + "\n" + Environment.StackTrace); + } + } + #if DEBUG public override void ForceTimeOut() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index f2720cc52..cad9bc5ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -12,10 +12,6 @@ namespace Barotrauma.Networking { private bool isActive; private UInt64 hostSteamId; - private ConnectionInitialization initializationStep; - private bool contentPackageOrderReceived; - private int passwordSalt; - private Steamworks.AuthTicket steamAuthTicket; private double timeout; private double heartbeatTimer; private double connectionStatusTimer; @@ -89,6 +85,7 @@ namespace Barotrauma.Networking Steamworks.SteamNetworking.AcceptP2PSessionWithUser(steamId); } else if (initializationStep != ConnectionInitialization.Password && + initializationStep != ConnectionInitialization.ContentPackageOrder && initializationStep != ConnectionInitialization.Success) { DebugConsole.ThrowError($"Connection from incorrect SteamID was rejected: "+ @@ -105,7 +102,7 @@ namespace Barotrauma.Networking OnDisconnectMessageReceived?.Invoke($"SteamP2P connection failed: {error}"); } - private void OnP2PData(ulong steamId, byte[] data, int dataLength, int channel) + private void OnP2PData(ulong steamId, byte[] data, int dataLength) { if (!isActive) { return; } if (steamId != hostSteamId) { return; } @@ -165,6 +162,7 @@ namespace Barotrauma.Networking heartbeatTimer -= deltaTime; if (initializationStep != ConnectionInitialization.Password && + initializationStep != ConnectionInitialization.ContentPackageOrder && initializationStep != ConnectionInitialization.Success) { connectionStatusTimer -= deltaTime; @@ -194,7 +192,7 @@ namespace Barotrauma.Networking var packet = Steamworks.SteamNetworking.ReadP2PPacket(); if (packet.HasValue) { - OnP2PData(packet?.SteamId ?? 0, packet?.Data, packet?.Data.Length ?? 0, 0); + OnP2PData(packet?.SteamId ?? 0, packet?.Data, packet?.Data.Length ?? 0); receivedBytes += packet?.Data.Length ?? 0; } } @@ -249,88 +247,6 @@ namespace Barotrauma.Networking incomingDataMessages.Clear(); } - private void ReadConnectionInitializationStep(IReadMessage inc) - { - if (!isActive) { return; } - - ConnectionInitialization step = (ConnectionInitialization)inc.ReadByte(); - - IWriteMessage outMsg; - - //DebugConsole.NewMessage(step + " " + initializationStep); - switch (step) - { - case ConnectionInitialization.SteamTicketAndVersion: - if (initializationStep != ConnectionInitialization.SteamTicketAndVersion) { return; } - outMsg = new WriteOnlyMessage(); - outMsg.Write((byte)DeliveryMethod.Reliable); - outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); - outMsg.Write((byte)ConnectionInitialization.SteamTicketAndVersion); - outMsg.Write(Name); - outMsg.Write(SteamManager.GetSteamID()); - outMsg.Write((UInt16)steamAuthTicket.Data.Length); - outMsg.Write(steamAuthTicket.Data, 0, steamAuthTicket.Data.Length); - - outMsg.Write(GameMain.Version.ToString()); - - IEnumerable mpContentPackages = GameMain.SelectedPackages.Where(cp => cp.HasMultiplayerIncompatibleContent); - outMsg.WriteVariableUInt32((UInt32)mpContentPackages.Count()); - foreach (ContentPackage contentPackage in mpContentPackages) - { - outMsg.Write(contentPackage.Name); - outMsg.Write(contentPackage.MD5hash.Hash); - } - - heartbeatTimer = 5.0; - Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); - sentBytes += outMsg.LengthBytes; - break; - case ConnectionInitialization.ContentPackageOrder: - if (initializationStep == ConnectionInitialization.SteamTicketAndVersion || - initializationStep == ConnectionInitialization.Password) { initializationStep = ConnectionInitialization.ContentPackageOrder; } - if (initializationStep != ConnectionInitialization.ContentPackageOrder) { return; } - outMsg = new WriteOnlyMessage(); - outMsg.Write((byte)DeliveryMethod.Reliable); - outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); - outMsg.Write((byte)ConnectionInitialization.ContentPackageOrder); - - UInt32 cpCount = inc.ReadVariableUInt32(); - List serverContentPackages = new List(); - for (int i = 0; i < cpCount; i++) - { - string hash = inc.ReadString(); - serverContentPackages.Add(GameMain.Config.SelectedContentPackages.Find(cp => cp.MD5hash.Hash == hash)); - } - - if (!contentPackageOrderReceived) - { - GameMain.Config.ReorderSelectedContentPackages(cp => serverContentPackages.Contains(cp) ? - serverContentPackages.IndexOf(cp) : - serverContentPackages.Count + GameMain.Config.SelectedContentPackages.IndexOf(cp)); - contentPackageOrderReceived = true; - } - - Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); - sentBytes += outMsg.LengthBytes; - break; - case ConnectionInitialization.Password: - if (initializationStep == ConnectionInitialization.SteamTicketAndVersion) { initializationStep = ConnectionInitialization.Password; } - if (initializationStep != ConnectionInitialization.Password) { return; } - bool incomingSalt = inc.ReadBoolean(); inc.ReadPadBits(); - int retries = 0; - if (incomingSalt) - { - passwordSalt = inc.ReadInt32(); - } - else - { - retries = inc.ReadInt32(); - } - OnRequestPassword?.Invoke(passwordSalt, retries); - break; - } - } - public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod) { if (!isActive) { return; } @@ -339,8 +255,7 @@ namespace Barotrauma.Networking buf[0] = (byte)deliveryMethod; byte[] bufAux = new byte[msg.LengthBytes]; - bool isCompressed; int length; - msg.PrepareForSending(ref bufAux, out isCompressed, out length); + msg.PrepareForSending(ref bufAux, out bool isCompressed, out int length); buf[1] = (byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None); @@ -426,7 +341,7 @@ namespace Barotrauma.Networking sentBytes += outMsg.LengthBytes; } - public override void Close(string msg = null) + public override void Close(string msg = null, bool disableReconnect = false) { if (!isActive) { return; } @@ -451,7 +366,7 @@ namespace Barotrauma.Networking steamAuthTicket?.Cancel(); steamAuthTicket = null; hostSteamId = 0; - OnDisconnect?.Invoke(); + OnDisconnect?.Invoke(disableReconnect); } ~SteamP2PClientPeer() @@ -460,6 +375,31 @@ namespace Barotrauma.Networking Close(); } + protected override void SendMsgInternal(DeliveryMethod deliveryMethod, IWriteMessage msg) + { + Steamworks.P2PSend sendType; + switch (deliveryMethod) + { + case DeliveryMethod.Reliable: + case DeliveryMethod.ReliableOrdered: + //the documentation seems to suggest that the Reliable send type + //enforces packet order (TODO: verify) + sendType = Steamworks.P2PSend.Reliable; + break; + default: + sendType = Steamworks.P2PSend.Unreliable; + break; + } + + IWriteMessage msgToSend = new WriteOnlyMessage(); + msgToSend.Write((byte)deliveryMethod); + msgToSend.Write(msg.Buffer, 0, msg.LengthBytes); + + heartbeatTimer = 5.0; + Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, msgToSend.Buffer, msgToSend.LengthBytes, 0, sendType); + sentBytes += msg.LengthBytes; + } + #if DEBUG public override void ForceTimeOut() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index dfecea763..d6f35f5bf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -1,8 +1,6 @@ -using System; +using Barotrauma.Steam; +using System; using System.Collections.Generic; -using System.Net; -using System.Text; -using Barotrauma.Steam; using System.Linq; using System.Threading; @@ -12,7 +10,6 @@ namespace Barotrauma.Networking { private bool isActive; - private ConnectionInitialization initializationStep; private readonly UInt64 selfSteamID; private long sentBytes, receivedBytes; @@ -61,7 +58,7 @@ namespace Barotrauma.Networking initializationStep = ConnectionInitialization.SteamTicketAndVersion; - ServerConnection = new PipeConnection(); + ServerConnection = new PipeConnection(selfSteamID); ServerConnection.Status = NetworkConnectionStatus.Connected; remotePeers = new List(); @@ -161,6 +158,7 @@ namespace Barotrauma.Networking remotePeer.Authenticating = true; authMsg.ReadString(); //skip name + authMsg.ReadInt32(); //skip owner key authMsg.ReadUInt64(); //skip steamid UInt16 ticketLength = authMsg.ReadUInt16(); byte[] ticket = authMsg.ReadBytes(ticketLength); @@ -395,7 +393,7 @@ namespace Barotrauma.Networking return; //owner doesn't send passwords } - public override void Close(string msg = null) + public override void Close(string msg = null, bool disableReconnect = false) { if (!isActive) { return; } @@ -415,7 +413,7 @@ namespace Barotrauma.Networking ChildServerRelay.ClosePipes(); - OnDisconnect?.Invoke(); + OnDisconnect?.Invoke(disableReconnect); SteamManager.LeaveLobby(); Steamworks.SteamNetworking.ResetActions(); @@ -445,6 +443,12 @@ namespace Barotrauma.Networking Close(); } + protected override void SendMsgInternal(DeliveryMethod deliveryMethod, IWriteMessage msg) + { + //not currently used by SteamP2POwnerPeer + throw new NotImplementedException(); + } + #if DEBUG public override void ForceTimeOut() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs index 38e0fe399..e5dfe0417 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs @@ -73,26 +73,25 @@ namespace Barotrauma.Networking get; private set; } = new List(); - public List ContentPackageWorkshopUrls + public List ContentPackageWorkshopIds { get; private set; - } = new List(); + } = new List(); - public bool ContentPackagesMatch(IEnumerable myContentPackages) + public bool ContentPackagesMatch() { + var myContentPackages = ContentPackage.AllPackages; //make sure we have all the packages the server requires - foreach (string hash in ContentPackageHashes) + if (ContentPackageHashes.Count != ContentPackageWorkshopIds.Count) { return false; } + for (int i = 0; i < ContentPackageWorkshopIds.Count; i++) { - if (!myContentPackages.Any(myPackage => myPackage.MD5hash.Hash == hash)) { return false; } - } - - //make sure the server isn't missing any of our packages that cause multiplayer incompatibility - foreach (ContentPackage myPackage in myContentPackages) - { - if (myPackage.HasMultiplayerIncompatibleContent) + string hash = ContentPackageHashes[i]; + UInt64 id = ContentPackageWorkshopIds[i]; + if (!myContentPackages.Any(myPackage => myPackage.MD5hash.Hash == hash)) { - if (!ContentPackageHashes.Any(hash => hash == myPackage.MD5hash.Hash)) { return false; } + if (myContentPackages.Any(p => p.SteamWorkshopId == id)) { return false; } + if (id == 0) { return false; } } } @@ -145,19 +144,22 @@ namespace Barotrauma.Networking new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), previewContainer.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("ServerListVersion"), string.IsNullOrEmpty(GameVersion) ? TextManager.Get("Unknown") : GameVersion)); - PlayStyle playStyle = PlayStyle ?? Networking.PlayStyle.Serious; + bool hidePlaystyleBanner = previewContainer.Rect.Height < 380 || !PlayStyle.HasValue; + if (!hidePlaystyleBanner) + { + PlayStyle playStyle = PlayStyle ?? Networking.PlayStyle.Serious; + Sprite playStyleBannerSprite = ServerListScreen.PlayStyleBanners[(int)playStyle]; + float playStyleBannerAspectRatio = playStyleBannerSprite.SourceRect.Width / playStyleBannerSprite.SourceRect.Height; + var playStyleBanner = new GUIImage(new RectTransform(new Point(previewContainer.Rect.Width, (int)(previewContainer.Rect.Width / playStyleBannerAspectRatio)), previewContainer.RectTransform), + playStyleBannerSprite, null, true); - Sprite playStyleBannerSprite = ServerListScreen.PlayStyleBanners[(int)playStyle]; - float playStyleBannerAspectRatio = playStyleBannerSprite.SourceRect.Width / playStyleBannerSprite.SourceRect.Height; - var playStyleBanner = new GUIImage(new RectTransform(new Point(previewContainer.Rect.Width, (int)(previewContainer.Rect.Width / playStyleBannerAspectRatio)), previewContainer.RectTransform), - playStyleBannerSprite, null, true); - - var playStyleName = new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.0f), playStyleBanner.RectTransform) { RelativeOffset = new Vector2(0.01f, 0.06f) }, - TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"), TextManager.Get("servertag."+ playStyle)), textColor: Color.White, - font: GUI.SmallFont, textAlignment: Alignment.Center, - color: ServerListScreen.PlayStyleColors[(int)playStyle], style: "GUISlopedHeader"); - playStyleName.RectTransform.NonScaledSize = (playStyleName.Font.MeasureString(playStyleName.Text) + new Vector2(20, 5) * GUI.Scale).ToPoint(); - playStyleName.RectTransform.IsFixedSize = true; + var playStyleName = new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.0f), playStyleBanner.RectTransform) { RelativeOffset = new Vector2(0.01f, 0.06f) }, + TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"), TextManager.Get("servertag."+ playStyle)), textColor: Color.White, + font: GUI.SmallFont, textAlignment: Alignment.Center, + color: ServerListScreen.PlayStyleColors[(int)playStyle], style: "GUISlopedHeader"); + playStyleName.RectTransform.NonScaledSize = (playStyleName.Font.MeasureString(playStyleName.Text) + new Vector2(20, 5) * GUI.Scale).ToPoint(); + playStyleName.RectTransform.IsFixedSize = true; + } var content = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), previewContainer.RectTransform)) { @@ -207,8 +209,13 @@ namespace Barotrauma.Networking TextManager.Get(string.IsNullOrEmpty(GameMode) ? "Unknown" : "GameMode." + GameMode, returnNull: true) ?? GameMode, textAlignment: Alignment.Right); - /*var traitors = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), bodyContainer.RectTransform), TextManager.Get("Traitors")); - new GUITextBlock(new RectTransform(Vector2.One, traitors.RectTransform), TextManager.Get(!TraitorsEnabled.HasValue ? "Unknown" : TraitorsEnabled.Value.ToString()), textAlignment: Alignment.Right);*/ + GUITextBlock playStyleText = null; + if (hidePlaystyleBanner && PlayStyle.HasValue) + { + PlayStyle playStyle = PlayStyle.Value; + playStyleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("serverplaystyle")); + new GUITextBlock(new RectTransform(Vector2.One, playStyleText.RectTransform), TextManager.Get("servertag." + playStyle), textAlignment: Alignment.Right); + } var subSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("ServerListSubSelection")); new GUITextBlock(new RectTransform(Vector2.One, subSelection.RectTransform), TextManager.Get(!SubSelectionMode.HasValue ? "Unknown" : SubSelectionMode.Value.ToString()), textAlignment: Alignment.Right); @@ -222,6 +229,10 @@ namespace Barotrauma.Networking { gameMode.Font = subSelection.Font = modeSelection.Font = GUI.SmallFont; gameMode.GetChild().Font = subSelection.GetChild().Font = modeSelection.GetChild().Font = GUI.SmallFont; + if (playStyleText != null) + { + playStyleText.Font = playStyleText.GetChild().Font = GUI.SmallFont; + } } var allowSpectating = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), content.RectTransform), TextManager.Get("ServerListAllowSpectating")) @@ -279,7 +290,6 @@ namespace Barotrauma.Networking } else { - List availableWorkshopUrls = new List(); for (int i = 0; i < ContentPackageNames.Count; i++) { var packageText = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.15f), contentPackageList.Content.RectTransform) { MinSize = new Point(0, 15) }, @@ -289,22 +299,15 @@ namespace Barotrauma.Networking }; if (i < ContentPackageHashes.Count) { - if (GameMain.Config.SelectedContentPackages.Any(cp => cp.MD5hash.Hash == ContentPackageHashes[i])) + if (ContentPackage.AllPackages.Any(cp => cp.MD5hash.Hash == ContentPackageHashes[i])) { packageText.Selected = true; continue; } - //matching content package found, but it hasn't been enabled - if (ContentPackage.List.Any(cp => cp.MD5hash.Hash == ContentPackageHashes[i])) - { - packageText.TextColor = GUI.Style.Orange; - packageText.ToolTip = TextManager.GetWithVariable("ServerListContentPackageNotEnabled", "[contentpackage]", ContentPackageNames[i]); - } //workshop download link found - else if (i < ContentPackageWorkshopUrls.Count && !string.IsNullOrEmpty(ContentPackageWorkshopUrls[i])) + if (i < ContentPackageWorkshopIds.Count && ContentPackageWorkshopIds[i] != 0) { - availableWorkshopUrls.Add(ContentPackageWorkshopUrls[i]); packageText.TextColor = Color.Yellow; packageText.ToolTip = TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", ContentPackageNames[i]); } @@ -316,21 +319,6 @@ namespace Barotrauma.Networking } } } - if (availableWorkshopUrls.Count > 0) - { - var workshopBtn = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), content.RectTransform), TextManager.Get("ServerListSubscribeMissingPackages")) - { - ToolTip = TextManager.Get(SteamManager.IsInitialized ? "ServerListSubscribeMissingPackagesTooltip" : "ServerListSubscribeMissingPackagesTooltipNoSteam"), - Enabled = SteamManager.IsInitialized, - OnClicked = (btn, userdata) => - { - GameMain.SteamWorkshopScreen.SubscribeToPackages(availableWorkshopUrls); - GameMain.SteamWorkshopScreen.Select(); - return true; - } - }; - workshopBtn.TextBlock.AutoScaleHorizontal = true; - } } // ----------------------------------------------------------------------------- @@ -391,7 +379,7 @@ namespace Barotrauma.Networking if (bool.TryParse(element.GetAttributeString("UsingWhiteList", ""), out bool whitelistTemp)) { info.UsingWhiteList = whitelistTemp; } if (Enum.TryParse(element.GetAttributeString("TraitorsEnabled", ""), out YesNoMaybe traitorsTemp)) { info.TraitorsEnabled = traitorsTemp; } if (Enum.TryParse(element.GetAttributeString("SubSelectionMode", ""), out SelectionMode subSelectionTemp)) { info.SubSelectionMode = subSelectionTemp; } - if (Enum.TryParse(element.GetAttributeString("ModeSelectionMode", ""), out SelectionMode modeSelectionTemp)) { info.ModeSelectionMode = subSelectionTemp; } + if (Enum.TryParse(element.GetAttributeString("ModeSelectionMode", ""), out SelectionMode modeSelectionTemp)) { info.ModeSelectionMode = modeSelectionTemp; } if (bool.TryParse(element.GetAttributeString("VoipEnabled", ""), out bool voipTemp)) { info.VoipEnabled = voipTemp; } if (bool.TryParse(element.GetAttributeString("KarmaEnabled", ""), out bool karmaTemp)) { info.KarmaEnabled = karmaTemp; } if (bool.TryParse(element.GetAttributeString("FriendlyFireEnabled", ""), out bool friendlyFireTemp)) { info.FriendlyFireEnabled = friendlyFireTemp; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 54785afba..313e4a394 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -477,10 +477,6 @@ 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)); - var endBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), - TextManager.Get("ServerSettingsEndRoundWhenDestReached")); - GetPropertyData("EndRoundAtLevelEnd").AssignGUIComponent(endBox); - var endVoteBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsEndRoundVoting")); GetPropertyData("AllowEndVoting").AssignGUIComponent(endVoteBox); @@ -569,6 +565,8 @@ namespace Barotrauma.Networking }; slider.OnMoved(slider, slider.BarScroll); + var traitorsMinPlayerCount = CreateLabeledNumberInput(roundsTab, "ServerSettingsTraitorsMinPlayerCount", 1, 16, "ServerSettingsTraitorsMinPlayerCountToolTip"); + GetPropertyData("TraitorsMinPlayerCount").AssignGUIComponent(traitorsMinPlayerCount); var ragdollButtonBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsAllowRagdollButton")); GetPropertyData("AllowRagdollButton").AssignGUIComponent(ragdollButtonBox); @@ -576,53 +574,6 @@ namespace Barotrauma.Networking var disableBotConversationsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsDisableBotConversations")); GetPropertyData("DisableBotConversations").AssignGUIComponent(disableBotConversationsBox); - /*var traitorRatioBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsUseTraitorRatio")); - - CreateLabeledSlider(roundsTab, "", out slider, out sliderLabel); - var traitorRatioSlider = slider; - traitorRatioBox.OnSelected = (GUITickBox) => - { - traitorRatioSlider.OnMoved(traitorRatioSlider, traitorRatioSlider.BarScroll); - return true; - }; - - if (TraitorUseRatio) - { - traitorRatioSlider.Range = new Vector2(0.1f, 1.0f); - } - else - { - traitorRatioSlider.Range = new Vector2(1.0f, maxPlayers); - } - - string traitorRatioLabel = TextManager.Get("ServerSettingsTraitorRatio") + " "; - string traitorCountLabel = TextManager.Get("ServerSettingsTraitorCount") + " "; - - traitorRatioSlider.Range = new Vector2(0.1f, 1.0f); - traitorRatioSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - GUITextBlock traitorText = scrollBar.UserData as GUITextBlock; - if (traitorRatioBox.Selected) - { - scrollBar.Step = 0.01f; - scrollBar.Range = new Vector2(0.1f, 1.0f); - traitorText.Text = traitorRatioLabel + (int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 1.0f) + " %"; - } - else - { - scrollBar.Step = 1f / (maxPlayers - 1); - scrollBar.Range = new Vector2(1.0f, maxPlayers); - traitorText.Text = traitorCountLabel + scrollBar.BarScrollValue; - } - return true; - }; - - GetPropertyData("TraitorUseRatio").AssignGUIComponent(traitorRatioBox); - GetPropertyData("TraitorRatio").AssignGUIComponent(traitorRatioSlider); - - traitorRatioSlider.OnMoved(traitorRatioSlider, traitorRatioSlider.BarScroll); - traitorRatioBox.OnSelected(traitorRatioBox);*/ - var buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), roundsTab.RectTransform), isHorizontal: true) { Stretch = true, @@ -919,7 +870,7 @@ namespace Barotrauma.Networking slider.UserData = label; } - private GUINumberInput CreateLabeledNumberInput(GUIComponent parent, string labelTag, int min, int max) + private GUINumberInput CreateLabeledNumberInput(GUIComponent parent, string labelTag, int min, int max, string toolTipTag = null) { var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), isHorizontal: true) { @@ -928,11 +879,15 @@ namespace Barotrauma.Networking ToolTip = TextManager.Get(labelTag) }; - new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), + var label = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: GUI.SmallFont) { AutoScaleHorizontal = true }; + if (!string.IsNullOrEmpty(toolTipTag)) + { + label.ToolTip = TextManager.Get(toolTipTag); + } var input = new GUINumberInput(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), GUINumberInput.NumberType.Int) { MinValueInt = min, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs index 5ed8d8b6d..a78d7c1d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs @@ -1,22 +1,18 @@ -using Barotrauma.Networking; +using Barotrauma.IO; +using Barotrauma.Networking; using RestSharp; using System; using System.Collections.Generic; -using Barotrauma.IO; using System.Linq; -using System.Threading; using System.Threading.Tasks; -using RestSharp.Contrib; using System.Xml.Linq; using Color = Microsoft.Xna.Framework.Color; -using System.Runtime.InteropServices; -using NLog.Fluent; namespace Barotrauma.Steam { static partial class SteamManager { - private static Dictionary modCopiesInProgress = new Dictionary(); + private static readonly Dictionary modCopiesInProgress = new Dictionary(); private static void InitializeProjectSpecific() { @@ -49,6 +45,9 @@ namespace Barotrauma.Steam } catch (Exception e) { +#if !DEBUG + DebugConsole.ThrowError("SteamManager initialization threw an exception", e); +#endif isInitialized = false; initializationErrors.Add("SteamClientInitFailed"); } @@ -85,28 +84,6 @@ namespace Barotrauma.Steam } } - - private static void UpdateProjectSpecific(float deltaTime) - { - if (ugcSubscriptionTasks != null) - { - var ugcSubscriptionKeys = ugcSubscriptionTasks.Keys.ToList(); - foreach (var key in ugcSubscriptionKeys) - { - var task = ugcSubscriptionTasks[key]; - - if (task.IsCompleted) - { - if (!task.IsCompletedSuccessfully) - { - DebugConsole.ThrowError("Failed to subscribe to a Steam Workshop item with id " + key.ToString() + ": TaskStatus = " + task.Status.ToString()); - } - ugcSubscriptionTasks.Remove(key); - } - } - } - } - public static async Task InitRelayNetworkAccess() { if (!IsInitialized) { return; } @@ -209,13 +186,13 @@ namespace Barotrauma.Steam return; } - var contentPackages = GameMain.Config.SelectedContentPackages.Where(cp => cp.HasMultiplayerIncompatibleContent); + var contentPackages = GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent); currentLobby?.SetData("name", serverSettings.ServerName); currentLobby?.SetData("playercount", (GameMain.Client?.ConnectedClients?.Count ?? 0).ToString()); currentLobby?.SetData("maxplayernum", serverSettings.MaxPlayers.ToString()); //currentLobby?.SetData("hostipaddress", lobbyIP); - string pingLocation = Steamworks.SteamNetworkingUtils.LocalPingLocation.ToString(); + string pingLocation = Steamworks.SteamNetworkingUtils.LocalPingLocation?.ToString(); currentLobby?.SetData("pinglocation", pingLocation ?? ""); currentLobby?.SetData("lobbyowner", SteamIDUInt64ToString(GetSteamID())); currentLobby?.SetData("haspassword", serverSettings.HasPassword.ToString()); @@ -225,7 +202,7 @@ namespace Barotrauma.Steam currentLobby?.SetData("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); currentLobby?.SetData("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.MD5hash.Hash))); - currentLobby?.SetData("contentpackageurl", string.Join(",", contentPackages.Select(cp => cp.SteamWorkshopUrl ?? ""))); + currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.SteamWorkshopId))); currentLobby?.SetData("usingwhitelist", (serverSettings.Whitelist != null && serverSettings.Whitelist.Enabled).ToString()); currentLobby?.SetData("modeselectionmode", serverSettings.ModeSelectionMode.ToString()); currentLobby?.SetData("subselectionmode", serverSettings.SubSelectionMode.ToString()); @@ -280,9 +257,8 @@ namespace Barotrauma.Steam { if (!isInitialized) { return false; } - int doneTasks = 0; - Action taskDone = () => + void taskDone() { doneTasks++; if (doneTasks >= 2) @@ -290,52 +266,64 @@ namespace Barotrauma.Steam serverQueryFinished?.Invoke(); serverQueryFinished = null; } - }; + } //TODO: find a better strategy to fetch all lobbies, this is gonna take forever if we actually have 10000 lobbies Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery().FilterDistanceWorldwide().WithMaxResults(10000); + Steamworks.Dispatch.OnDebugCallback = (callbackType, contents, isServer) => + { + DebugConsole.NewMessage($"{callbackType}: " + contents, Color.Yellow); + }; TaskPool.Add("LobbyQueryRequest", lobbyQuery.RequestAsync(), (t) => { - var lobbies = ((Task)t).Result; - foreach (var lobby in lobbies) - { - if (string.IsNullOrEmpty(lobby.GetData("name"))) { continue; } - - ServerInfo serverInfo = new ServerInfo(); - serverInfo.ServerName = lobby.GetData("name"); - serverInfo.OwnerID = SteamIDStringToUInt64(lobby.GetData("lobbyowner")); - serverInfo.LobbyID = lobby.Id; - bool.TryParse(lobby.GetData("haspassword"), out serverInfo.HasPassword); - serverInfo.PlayerCount = int.TryParse(lobby.GetData("playercount"), out int playerCount) ? playerCount : 0; - serverInfo.MaxPlayers = int.TryParse(lobby.GetData("maxplayernum"), out int maxPlayers) ? maxPlayers : 1; - serverInfo.RespondedToSteamQuery = true; - - AssignLobbyDataToServerInfo(lobby, serverInfo); - - addToServerList(serverInfo); - } - taskDone(); + Steamworks.Dispatch.OnDebugCallback = null; if (t.Status == TaskStatus.Faulted) { TaskPool.PrintTaskExceptions(t, "Failed to retrieve SteamP2P lobbies"); + taskDone(); return; } + var lobbies = ((Task)t).Result; + if (lobbies != null) + { + foreach (var lobby in lobbies) + { + if (string.IsNullOrEmpty(lobby.GetData("name"))) { continue; } + + ServerInfo serverInfo = new ServerInfo(); + serverInfo.ServerName = lobby.GetData("name"); + serverInfo.OwnerID = SteamIDStringToUInt64(lobby.GetData("lobbyowner")); + serverInfo.LobbyID = lobby.Id; + bool.TryParse(lobby.GetData("haspassword"), out serverInfo.HasPassword); + serverInfo.PlayerCount = int.TryParse(lobby.GetData("playercount"), out int playerCount) ? playerCount : 0; + serverInfo.MaxPlayers = int.TryParse(lobby.GetData("maxplayernum"), out int maxPlayers) ? maxPlayers : 1; + serverInfo.RespondedToSteamQuery = true; + + AssignLobbyDataToServerInfo(lobby, serverInfo); + + addToServerList(serverInfo); + } + } + taskDone(); }); Steamworks.ServerList.Internet serverQuery = new Steamworks.ServerList.Internet(); - Action onServer = (Steamworks.Data.ServerInfo info, bool responsive) => + void onServer(Steamworks.Data.ServerInfo info, bool responsive) { if (string.IsNullOrEmpty(info.Name)) { return; } - ServerInfo serverInfo = new ServerInfo(); - serverInfo.ServerName = info.Name; - serverInfo.IP = info.Address.ToString(); - serverInfo.Port = info.ConnectionPort.ToString(); - serverInfo.PlayerCount = info.Players; - serverInfo.MaxPlayers = info.MaxPlayers; - serverInfo.RespondedToSteamQuery = responsive; + ServerInfo serverInfo = new ServerInfo + { + ServerName = info.Name, + HasPassword = info.Passworded, + IP = info.Address.ToString(), + Port = info.ConnectionPort.ToString(), + PlayerCount = info.Players, + MaxPlayers = info.MaxPlayers, + RespondedToSteamQuery = responsive + }; if (responsive) { @@ -344,11 +332,11 @@ namespace Barotrauma.Steam { if (t.Status == TaskStatus.Faulted) { - TaskPool.PrintTaskExceptions(t, "Failed to retrieve rules for "+info.Name); + TaskPool.PrintTaskExceptions(t, "Failed to retrieve rules for " + info.Name); return; } - var rules = ((Task>)t).Result; + var rules = ((Task>)t).Result; AssignServerRulesToServerInfo(rules, serverInfo); CrossThread.RequestExecutionOnMainThread(() => @@ -365,7 +353,7 @@ namespace Barotrauma.Steam }); } - }; + } serverQuery.OnResponsiveServer += (info) => onServer(info, true); serverQuery.OnUnresponsiveServer += (info) => onServer(info, false); @@ -393,11 +381,20 @@ namespace Barotrauma.Steam serverInfo.ContentPackageNames.AddRange(lobby.GetData("contentpackage").Split(',')); serverInfo.ContentPackageHashes.AddRange(lobby.GetData("contentpackagehash").Split(',')); - serverInfo.ContentPackageWorkshopUrls.AddRange(lobby.GetData("contentpackageurl").Split(',')); + + string workshopIdData = lobby.GetData("contentpackageid"); + if (!string.IsNullOrEmpty(workshopIdData)) + { + serverInfo.ContentPackageWorkshopIds.AddRange(ParseWorkshopIds(workshopIdData)); + } + else + { + string[] workshopUrls = lobby.GetData("contentpackageurl").Split(','); + serverInfo.ContentPackageWorkshopIds.AddRange(WorkshopUrlsToIds(workshopUrls)); + } serverInfo.UsingWhiteList = getLobbyBool("usingwhitelist"); - SelectionMode selectionMode; - if (Enum.TryParse(lobby.GetData("modeselectionmode"), out selectionMode)) { serverInfo.ModeSelectionMode = selectionMode; } + if (Enum.TryParse(lobby.GetData("modeselectionmode"), out SelectionMode selectionMode)) { serverInfo.ModeSelectionMode = selectionMode; } if (Enum.TryParse(lobby.GetData("subselectionmode"), out selectionMode)) { serverInfo.SubSelectionMode = selectionMode; } serverInfo.AllowSpectating = getLobbyBool("allowspectating"); @@ -412,11 +409,12 @@ namespace Barotrauma.Steam if (Enum.TryParse(lobby.GetData("playstyle"), out PlayStyle playStyle)) serverInfo.PlayStyle = playStyle; if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || - serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopUrls.Count) + serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopIds.Count) { //invalid contentpackage info serverInfo.ContentPackageNames.Clear(); serverInfo.ContentPackageHashes.Clear(); + serverInfo.ContentPackageWorkshopIds.Clear(); } string pingLocation = lobby.GetData("pinglocation"); @@ -449,10 +447,18 @@ namespace Barotrauma.Steam serverInfo.ContentPackageNames.Clear(); serverInfo.ContentPackageHashes.Clear(); - serverInfo.ContentPackageWorkshopUrls.Clear(); + serverInfo.ContentPackageWorkshopIds.Clear(); if (rules.ContainsKey("contentpackage")) serverInfo.ContentPackageNames.AddRange(rules["contentpackage"].Split(',')); if (rules.ContainsKey("contentpackagehash")) serverInfo.ContentPackageHashes.AddRange(rules["contentpackagehash"].Split(',')); - if (rules.ContainsKey("contentpackageurl")) serverInfo.ContentPackageWorkshopUrls.AddRange(rules["contentpackageurl"].Split(',')); + if (rules.ContainsKey("contentpackageid")) + { + serverInfo.ContentPackageWorkshopIds.AddRange(ParseWorkshopIds(rules["contentpackageid"])); + } + else if (rules.ContainsKey("contentpackageurl")) + { + string[] workshopUrls = rules["contentpackageurl"].Split(','); + serverInfo.ContentPackageWorkshopIds.AddRange(WorkshopUrlsToIds(workshopUrls)); + } if (rules.ContainsKey("usingwhitelist")) serverInfo.UsingWhiteList = rules["usingwhitelist"] == "True"; if (rules.ContainsKey("modeselectionmode")) @@ -479,37 +485,16 @@ namespace Barotrauma.Steam } if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || - serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopUrls.Count) + serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopIds.Count) { //invalid contentpackage info serverInfo.ContentPackageNames.Clear(); serverInfo.ContentPackageHashes.Clear(); + serverInfo.ContentPackageWorkshopIds.Clear(); } } - public static ulong GetWorkshopItemIDFromUrl(string url) - { - try - { - Uri uri = new Uri(url); - string idStr = HttpUtility.ParseQueryString(uri.Query).Get("id"); - if (ulong.TryParse(idStr, out ulong id)) - { - return id; - } - } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to get Workshop item ID from the url \"" + url + "\"!", e); - } - - return 0; - } - - #region Connecting to servers - - //TODO: reimplement server list queries - +#region Connecting to servers private static Steamworks.AuthTicket currentTicket = null; public static Steamworks.AuthTicket GetAuthSessionTicket() { @@ -545,9 +530,9 @@ namespace Barotrauma.Steam Steamworks.SteamUser.EndAuthSession(clientSteamID); } - #endregion +#endregion - #region Workshop +#region Workshop public const string WorkshopItemPreviewImageFolder = "Workshop"; public const string PreviewImageName = "PreviewImage.png"; @@ -620,7 +605,8 @@ namespace Barotrauma.Steam .WithLongDescription(); if (requireTags != null) query.WithTags(requireTags); - TaskPool.Add("GetPopularWorkshopItems", GetWorkshopItemsAsync(query, amount, (item) => !item.IsSubscribed), (task) => { + TaskPool.Add("GetPopularWorkshopItems", GetWorkshopItemsAsync(query, amount, (item) => !item.IsSubscribed), (task) => + { var entries = ((Task>)task).Result; //count the number of each unique tag @@ -669,47 +655,82 @@ namespace Barotrauma.Steam TaskPool.Add("GetPublishedWorkshopItems", GetWorkshopItemsAsync(query), (task) => { onItemsFound?.Invoke(((Task>)task).Result); }); } - private static Dictionary ugcSubscriptionTasks; + private static readonly HashSet pendingWorkshopSubscriptions = new HashSet(); - public static void SubscribeToWorkshopItem(string itemUrl) - { - if (!isInitialized) return; - - ulong id = GetWorkshopItemIDFromUrl(itemUrl); - - SubscribeToWorkshopItem(id); - } - - public static void SubscribeToWorkshopItem(ulong id) + public static void SubscribeToWorkshopItem(ulong id, Action onInstalled = null) { if (!isInitialized) return; if (id == 0) { return; } - if (ugcSubscriptionTasks?.ContainsKey(id) ?? false) { return; } + if (pendingWorkshopSubscriptions.Contains(id)) { return; } - ugcSubscriptionTasks ??= new Dictionary(); - ugcSubscriptionTasks.Add(id, Task.Run(async () => - { - Steamworks.Ugc.Item? item = await Steamworks.SteamUGC.QueryFileAsync(id); + pendingWorkshopSubscriptions.Add(id); + TaskPool.Add( + $"SubscribeToWorkshopItem({id})", + Task.Run(async () => + { + Steamworks.Ugc.Item? item = await Steamworks.SteamUGC.QueryFileAsync(id); - if (!item.HasValue) - { - DebugConsole.ThrowError("Failed to find a Steam Workshop item with the ID " + id.ToString() + "."); - return; - } + if (!item.HasValue) + { + DebugConsole.ThrowError($"Failed to find a Steam Workshop item with the ID {id}."); + return null; + } - bool subscribed = await item?.Subscribe(); - if (!subscribed) + if (!(item?.IsSubscribed ?? false)) + { + bool subscribed = await item?.Subscribe(); + if (!subscribed) + { + DebugConsole.ThrowError($"Failed to subscribe to Steam Workshop item with the ID {id}."); + return null; + } + } + + return item; + }), + (t) => { - DebugConsole.ThrowError("Failed to subscribe to Steam Workshop item with the ID " + id.ToString() + "."); - } - bool downloading = item?.Download() ?? false; - if (!downloading) - { - DebugConsole.ThrowError("Failed to start downloading Steam Workshop item with the ID " + id.ToString() + "."); - } - })); + bool shouldCleanup = true; + if (t.IsFaulted) + { + TaskPool.PrintTaskExceptions(t, $"Workshop subscription task {id} faulted"); + } + else + { + var item = ((Task)t).Result; + if (item != null) + { + if (item?.IsInstalled ?? false) + { + onInstalled?.Invoke(); + } + else + { + void _onInstalled() + { + onInstalled?.Invoke(); + pendingWorkshopSubscriptions.Remove(id); + } + bool downloading = item?.Download(_onInstalled) ?? false; + if (!downloading) + { + DebugConsole.ThrowError($"Failed to start downloading Steam Workshop item with the ID {id}."); + } + else + { + shouldCleanup = false; + } + } + } + + if (shouldCleanup) + { + pendingWorkshopSubscriptions.Remove(id); + } + } + }); } public static void CreateWorkshopItemStaging(ContentPackage contentPackage, out Steamworks.Ugc.Editor? itemEditor) @@ -790,9 +811,9 @@ namespace Barotrauma.Steam itemEditor = itemEditor?.WithPrivateVisibility(); } - if (!CheckWorkshopItemEnabled(existingItem)) + if (!CheckWorkshopItemInstalled(existingItem)) { - if (!EnableWorkShopItem(existingItem, out string errorMsg)) + if (!InstallWorkshopItem(existingItem, out string errorMsg)) { DebugConsole.NewMessage(errorMsg, Color.Red); new GUIMessageBox( @@ -804,9 +825,9 @@ namespace Barotrauma.Steam } } - ContentPackage tempContentPackage = new ContentPackage(Path.Combine(existingItem?.Directory, MetadataFileName)) { SteamWorkshopUrl = existingItem.Value.Url }; + ContentPackage tempContentPackage = new ContentPackage(Path.Combine(existingItem?.Directory, MetadataFileName)) { SteamWorkshopId = existingItem.Value.Id }; string installedContentPackagePath = Path.GetFullPath(GetWorkshopItemContentPackagePath(tempContentPackage)); - contentPackage = ContentPackage.List.Find(cp => Path.GetFullPath(cp.Path) == installedContentPackagePath); + contentPackage = ContentPackage.AllPackages.FirstOrDefault(cp => Path.GetFullPath(cp.Path) == installedContentPackagePath); itemEditor = itemEditor?.WithContent(Path.GetDirectoryName(installedContentPackagePath)); @@ -923,7 +944,7 @@ namespace Barotrauma.Steam workshopPublishStatus.Result = task.Result; DebugConsole.NewMessage("Published workshop item " + item?.Title + " successfully.", Microsoft.Xna.Framework.Color.LightGreen); - contentPackage.SteamWorkshopUrl = $"http://steamcommunity.com/sharedfiles/filedetails/?source=Facepunch.Steamworks&id={task.Result.FileId.Value}"; + contentPackage.SteamWorkshopId = task.Result.FileId.Value; //NOTE: This sets InstallTime one hour into the future to guarantee //that the published content package won't be autoupdated incorrectly. //Change if it causes issues. @@ -937,9 +958,9 @@ namespace Barotrauma.Steam } /// - /// Enables a workshop item by moving it to the game folder. + /// Installs a workshop item by moving it to the game folder. /// - public static bool EnableWorkShopItem(Steamworks.Ugc.Item? item, out string errorMsg, bool selectContentPackage = false, bool suppressInstallNotif = false) + public static bool InstallWorkshopItem(Steamworks.Ugc.Item? item, out string errorMsg, bool enableContentPackage = false, bool suppressInstallNotif = false) { if (!(item?.IsInstalled ?? false)) { @@ -959,11 +980,11 @@ namespace Barotrauma.Steam ContentPackage contentPackage = new ContentPackage(metaDataFilePath) { - SteamWorkshopUrl = item?.Url + SteamWorkshopId = item?.Id ?? 0 }; string newContentPackagePath = GetWorkshopItemContentPackagePath(contentPackage); - List existingPackages = ContentPackage.List.Where(cp => cp.Path.CleanUpPath() == newContentPackagePath.CleanUpPath()).ToList(); + List existingPackages = ContentPackage.AllPackages.Where(cp => cp.Path.CleanUpPath() == newContentPackagePath.CleanUpPath()).ToList(); if (existingPackages.Any()) { if (item?.Owner.Id != Steamworks.SteamClient.SteamId) @@ -975,7 +996,7 @@ namespace Barotrauma.Steam } else { - RemoveMods(cp => !string.IsNullOrWhiteSpace(cp.SteamWorkshopUrl) && cp.SteamWorkshopUrl == contentPackage.SteamWorkshopUrl, + RemoveMods(cp => cp.SteamWorkshopId != 0 && cp.SteamWorkshopId == contentPackage.SteamWorkshopId, false); } } @@ -1024,7 +1045,7 @@ namespace Barotrauma.Steam var newPackage = new ContentPackage(cp.Path, newContentPackagePath) { - SteamWorkshopUrl = item?.Url, + SteamWorkshopId = item?.Id ?? 0, InstallTime = item?.Updated > item?.Created ? item?.Updated : item?.Created }; @@ -1045,17 +1066,17 @@ namespace Barotrauma.Steam Directory.CreateDirectory(Path.GetDirectoryName(newContentPackagePath)); } newPackage.Save(newContentPackagePath); - ContentPackage.List.Add(newPackage); + ContentPackage.AddPackage(newPackage); - if (selectContentPackage) + if (enableContentPackage) { - if (newPackage.CorePackage) + if (newPackage.IsCorePackage) { GameMain.Config.SelectCorePackage(newPackage); } else { - GameMain.Config.SelectContentPackage(newPackage); + GameMain.Config.EnableRegularPackage(newPackage); } GameMain.Config.SaveNewPlayerConfig(); @@ -1213,40 +1234,21 @@ namespace Barotrauma.Steam return ""; } - private static bool CheckFileEquality(string filePath1, string filePath2) - { - if (filePath1 == filePath2) - { - return true; - } - - using (FileStream fs1 = File.OpenRead(filePath1)) - using (FileStream fs2 = File.OpenRead(filePath2)) - { - Md5Hash hash1 = new Md5Hash(fs1); - Md5Hash hash2 = new Md5Hash(fs2); - return hash1.Hash == hash2.Hash; - } - } - private static void RemoveMods(Func predicate, bool delete = true) { - var toRemove = ContentPackage.List.Where(predicate).ToList(); - var packagesToDeselect = GameMain.Config.SelectedContentPackages.Where(p => toRemove.Contains(p)).ToList(); + var toRemoveCore = ContentPackage.CorePackages.Where(predicate).ToList(); + if (toRemoveCore.Contains(GameMain.Config.CurrentCorePackage)) { GameMain.Config.AutoSelectCorePackage(toRemoveCore); } + + var toRemoveRegular = ContentPackage.RegularPackages.Where(predicate).ToList(); + var packagesToDeselect = GameMain.Config.EnabledRegularPackages.Where(p => toRemoveRegular.Contains(p)).ToList(); foreach (var cp in packagesToDeselect) { - if (cp.CorePackage) - { - GameMain.Config.AutoSelectCorePackage(toRemove); - } - else - { - GameMain.Config.DeselectContentPackage(cp); - } + GameMain.Config.DisableRegularPackage(cp); } if (delete) { + var toRemove = toRemoveCore.Concat(toRemoveRegular); foreach (var cp in toRemove) { try @@ -1258,22 +1260,19 @@ namespace Barotrauma.Steam { DebugConsole.ThrowError($"An error occurred while attempting to delete {Path.GetDirectoryName(cp.Path)}", e); } + ContentPackage.RemovePackage(cp); } } - ContentPackage.List.RemoveAll(cp => toRemove.Contains(cp)); - GameMain.Config.SelectedContentPackages.RemoveAll(cp => !ContentPackage.List.Contains(cp)); - - ContentPackage.SortContentPackages(); GameMain.Config.SaveNewPlayerConfig(); GameMain.Config.WarnIfContentPackageSelectionDirty(); } /// - /// Disables a workshop item by removing the files from the game folder. + /// Uninstalls a workshop item by removing the files from the game folder. /// - public static bool DisableWorkShopItem(Steamworks.Ugc.Item? item, bool noLog, out string errorMsg) + public static bool UninstallWorkshopItem(Steamworks.Ugc.Item? item, bool noLog, out string errorMsg) { errorMsg = null; if (!(item?.IsInstalled ?? false)) @@ -1288,13 +1287,13 @@ namespace Barotrauma.Steam ContentPackage contentPackage = new ContentPackage(Path.Combine(item?.Directory, MetadataFileName)) { - SteamWorkshopUrl = item?.Url + SteamWorkshopId = item?.Id ?? 0 }; GameMain.Config.SuppressModFolderWatcher = true; try { - RemoveMods(cp => !string.IsNullOrWhiteSpace(cp.SteamWorkshopUrl) && cp.SteamWorkshopUrl == contentPackage.SteamWorkshopUrl); + RemoveMods(cp => cp.SteamWorkshopId != 0 && cp.SteamWorkshopId == contentPackage.SteamWorkshopId); } catch (Exception e) { @@ -1330,7 +1329,7 @@ namespace Barotrauma.Steam return contentPackage.IsCompatible(); } - public static bool CheckWorkshopItemEnabled(Steamworks.Ugc.Item? item) + public static bool CheckWorkshopItemInstalled(Steamworks.Ugc.Item? item) { if (!(item?.IsInstalled ?? false)) { return false; } @@ -1359,7 +1358,7 @@ namespace Barotrauma.Steam string errorMessage = "Metadata file for the Workshop item \"" + item?.Title + "\" not found. Could not combine path (" + (item?.Directory ?? "directory name empty") + ")."; DebugConsole.ThrowError(errorMessage); - GameAnalyticsManager.AddErrorEventOnce("SteamManager.CheckWorkshopItemEnabled:PathCombineException" + item?.Title, + GameAnalyticsManager.AddErrorEventOnce("SteamManager.CheckWorkshopItemInstalled:PathCombineException" + item?.Title, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMessage); return false; @@ -1373,12 +1372,12 @@ namespace Barotrauma.Steam ContentPackage contentPackage = new ContentPackage(metaDataPath) { - SteamWorkshopUrl = item?.Url + SteamWorkshopId = item?.Id ?? 0 }; //make sure the contentpackage file is present if (!File.Exists(GetWorkshopItemContentPackagePath(contentPackage)) || - !ContentPackage.List.Any(cp => cp.SteamWorkshopUrl == contentPackage.SteamWorkshopUrl || - (string.IsNullOrWhiteSpace(cp.SteamWorkshopUrl) && cp.Name == contentPackage.Name))) + !ContentPackage.AllPackages.Any(cp => cp.SteamWorkshopId == contentPackage.SteamWorkshopId || + (cp.SteamWorkshopId == 0 && cp.Name == contentPackage.Name))) { return false; } @@ -1399,9 +1398,9 @@ namespace Barotrauma.Steam ContentPackage steamPackage = new ContentPackage(metaDataPath) { - SteamWorkshopUrl = item?.Url + SteamWorkshopId = item?.Id ?? 0 }; - ContentPackage myPackage = ContentPackage.List.Find(cp => cp.SteamWorkshopUrl == steamPackage.SteamWorkshopUrl); + ContentPackage myPackage = ContentPackage.AllPackages.FirstOrDefault(cp => cp.SteamWorkshopId == steamPackage.SteamWorkshopId); if (myPackage?.InstallTime == null) { @@ -1427,7 +1426,7 @@ namespace Barotrauma.Steam GameMain.Config.SuppressModFolderWatcher = true; //remove mods that the player is no longer subscribed to - RemoveMods(cp => !string.IsNullOrWhiteSpace(cp.SteamWorkshopUrl) && !items.Any(it => it.Id == GetWorkshopItemIDFromUrl(cp.SteamWorkshopUrl))); + RemoveMods(cp => cp.SteamWorkshopId != 0 && !items.Any(it => it.Id == cp.SteamWorkshopId)); GameMain.Config.SuppressModFolderWatcher = false; @@ -1441,9 +1440,9 @@ namespace Barotrauma.Steam bool installedSuccessfully = false; string errorMsg; - if (!CheckWorkshopItemEnabled(item)) + if (!CheckWorkshopItemInstalled(item)) { - installedSuccessfully = EnableWorkShopItem(item, out errorMsg); + installedSuccessfully = InstallWorkshopItem(item, out errorMsg); } else if (!CheckWorkshopItemUpToDate(item)) { @@ -1529,12 +1528,12 @@ namespace Barotrauma.Steam { errorMsg = ""; if (!(item?.IsInstalled ?? false)) { return false; } - bool reenable = GameMain.Config.SelectedContentPackages.Any(p => !string.IsNullOrEmpty(p.SteamWorkshopUrl) && GetWorkshopItemIDFromUrl(p.SteamWorkshopUrl) == item?.Id); + bool reenable = GameMain.Config.AllEnabledPackages.Any(p => p.SteamWorkshopId != 0 && p.SteamWorkshopId == item?.Id); if (item?.Owner.Id != Steamworks.SteamClient.SteamId) { - if (!DisableWorkShopItem(item, false, out errorMsg)) { return false; } + if (!UninstallWorkshopItem(item, false, out errorMsg)) { return false; } } - if (!EnableWorkShopItem(item, errorMsg: out errorMsg, selectContentPackage: reenable)) { return false; } + if (!InstallWorkshopItem(item, errorMsg: out errorMsg, enableContentPackage: reenable)) { return false; } return true; } @@ -1543,7 +1542,7 @@ namespace Barotrauma.Steam string packageName = contentPackage.Name.Trim(); packageName = ToolBox.RemoveInvalidFileNameChars(packageName); while (packageName.Last() == '.') { packageName = packageName.Substring(0, packageName.Length-1); } - //packageName = packageName + "_" + GetWorkshopItemIDFromUrl(contentPackage.SteamWorkshopUrl); + //packageName = packageName + "_" + contentPackage.SteamWorkshopId.ToString(); return Path.Combine("Mods", packageName, MetadataFileName); } @@ -1559,8 +1558,7 @@ namespace Barotrauma.Steam attr.Name.ToString() == "characterfile") && attr.Value.CleanUpPath().Contains("/")) { - ContentType type = ContentType.None; - Enum.TryParse(attr.Name.LocalName, true, out type); + Enum.TryParse(attr.Name.LocalName, true, out ContentType type); attr.Value = CorrectContentFilePath(attr.Value, type, package, true); } } @@ -1615,8 +1613,7 @@ namespace Barotrauma.Steam if (checkIfFileExists) { bool exists = File.Exists(contentFilePath); - if (type == ContentType.Executable || - type == ContentType.ServerExecutable) + if (type == ContentType.ServerExecutable) { exists |= File.Exists(Path.GetFileNameWithoutExtension(contentFilePath) + ".dll"); } @@ -1634,7 +1631,7 @@ namespace Barotrauma.Steam { if (checkIfFileExists) { - ContentPackage otherContentPackage = ContentPackage.List.Find(cp => cp.Name.Equals(splitPath[1], StringComparison.OrdinalIgnoreCase)); + ContentPackage otherContentPackage = ContentPackage.AllPackages.FirstOrDefault(cp => cp.Name.Equals(splitPath[1], StringComparison.OrdinalIgnoreCase)); if (otherContentPackage != null) { string otherPackageName = Path.GetDirectoryName(otherContentPackage.Path); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index 6bd5ff3cd..f9724ead2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -167,7 +167,7 @@ namespace Barotrauma.Networking bool prevCaptured = true; int captureTimer; - void UpdateCapture() + private void UpdateCapture() { Array.Copy(uncompressedBuffer, 0, prevUncompressedBuffer, 0, VoipConfig.BUFFER_SIZE); Array.Clear(uncompressedBuffer, 0, VoipConfig.BUFFER_SIZE); @@ -273,6 +273,11 @@ namespace Barotrauma.Networking if (allowEnqueue || captureTimer > 0) { LastEnqueueAudio = DateTime.Now; + if (GameMain.Client?.Character != null) + { + var messageType = !ForceLocal && ChatMessage.CanUseRadio(GameMain.Client.Character, out _) ? ChatMessageType.Radio : ChatMessageType.Default; + GameMain.Client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); + } //encode audio and enqueue it lock (buffers) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index 593b51147..ceec59032 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -91,13 +91,13 @@ namespace Barotrauma if (userData == null) return; foreach (GUIComponent comp in listBox.Content.Children) { - if (comp.UserData != userData) continue; - GUITextBlock voteText = comp.FindChild("votes") as GUITextBlock; - if (voteText == null) + if (comp.UserData != userData) { continue; } + if (!(comp.FindChild("votes") is GUITextBlock voteText)) { voteText = new GUITextBlock(new RectTransform(new Point(30, comp.Rect.Height), comp.RectTransform, Anchor.CenterRight), "", textAlignment: Alignment.CenterRight) { + Padding = Vector4.Zero, UserData = "votes" }; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index c1f64c6cd..8030da641 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -3,6 +3,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Reflection.Metadata; namespace Barotrauma.Particles { @@ -61,6 +62,8 @@ namespace Barotrauma.Particles public bool HighQualityCollisionDetection; + public Vector4 ColorMultiplier; + public bool DrawOnTop { get; private set; } public ParticlePrefab.DrawTargetType DrawTarget @@ -141,7 +144,8 @@ namespace Barotrauma.Particles color = prefab.StartColor; changeColor = prefab.StartColor != prefab.EndColor; - + ColorMultiplier = Vector4.One; + velocityChange = prefab.VelocityChangeDisplay; velocityChangeWater = prefab.VelocityChangeWaterDisplay; @@ -287,22 +291,42 @@ namespace Barotrauma.Particles Vector2 collisionNormal = Vector2.Zero; if (velocity.Y < 0.0f && position.Y - prefab.CollisionRadius * size.Y < hullRect.Y - hullRect.Height) { - if (prefab.DeleteOnCollision) return false; + if (prefab.DeleteOnCollision) { return false; } collisionNormal = new Vector2(0.0f, 1.0f); } else if (velocity.Y > 0.0f && position.Y + prefab.CollisionRadius * size.Y > hullRect.Y) { - if (prefab.DeleteOnCollision) return false; + if (prefab.DeleteOnCollision) { return false; } collisionNormal = new Vector2(0.0f, -1.0f); } - else if (velocity.X < 0.0f && position.X - prefab.CollisionRadius * size.X < hullRect.X) + + if (collisionNormal != Vector2.Zero) { - if (prefab.DeleteOnCollision) return false; + bool gapFound = false; + foreach (Gap gap in hullGaps) + { + if (gap.Open <= 0.9f || gap.IsHorizontal) { continue; } + + if (gap.WorldRect.X > position.X || gap.WorldRect.Right < position.X) { continue; } + float hullCenterY = currentHull.WorldRect.Y - currentHull.WorldRect.Height / 2; + int gapDir = Math.Sign(gap.WorldRect.Y - hullCenterY); + if (Math.Sign(velocity.Y) != gapDir || Math.Sign(position.Y - hullCenterY) != gapDir) { continue; } + + gapFound = true; + break; + } + + handleCollision(gapFound, collisionNormal); + } + + if (velocity.X < 0.0f && position.X - prefab.CollisionRadius * size.X < hullRect.X) + { + if (prefab.DeleteOnCollision) { return false; } collisionNormal = new Vector2(1.0f, 0.0f); } else if (velocity.X > 0.0f && position.X + prefab.CollisionRadius * size.X > hullRect.Right) { - if (prefab.DeleteOnCollision) return false; + if (prefab.DeleteOnCollision) { return false; } collisionNormal = new Vector2(-1.0f, 0.0f); } @@ -311,26 +335,21 @@ namespace Barotrauma.Particles bool gapFound = false; foreach (Gap gap in hullGaps) { - if (gap.Open <= 0.9f || gap.IsHorizontal != (collisionNormal.X != 0.0f)) continue; + if (gap.Open <= 0.9f || !gap.IsHorizontal) { continue; } - if (gap.IsHorizontal) - { - if (gap.WorldRect.Y < position.Y || gap.WorldRect.Y - gap.WorldRect.Height > position.Y) continue; - int gapDir = Math.Sign(gap.WorldRect.Center.X - currentHull.WorldRect.Center.X); - if (Math.Sign(velocity.X) != gapDir || Math.Sign(position.X - currentHull.WorldRect.Center.X) != gapDir) continue; - } - else - { - if (gap.WorldRect.X > position.X || gap.WorldRect.Right < position.X) continue; - float hullCenterY = currentHull.WorldRect.Y - currentHull.WorldRect.Height / 2; - int gapDir = Math.Sign(gap.WorldRect.Y - hullCenterY); - if (Math.Sign(velocity.Y) != gapDir || Math.Sign(position.Y - hullCenterY) != gapDir) continue; - } + if (gap.WorldRect.Y < position.Y || gap.WorldRect.Y - gap.WorldRect.Height > position.Y) { continue; } + int gapDir = Math.Sign(gap.WorldRect.Center.X - currentHull.WorldRect.Center.X); + if (Math.Sign(velocity.X) != gapDir || Math.Sign(position.X - currentHull.WorldRect.Center.X) != gapDir) { continue; } gapFound = true; break; } + handleCollision(gapFound, collisionNormal); + } + + void handleCollision(bool gapFound, Vector2 collisionNormal) + { if (!gapFound) { OnWallCollisionInside(currentHull, collisionNormal); @@ -378,6 +397,8 @@ namespace Barotrauma.Particles private void OnWallCollisionInside(Hull prevHull, Vector2 collisionNormal) { + if (prevHull == null) { return; } + Rectangle prevHullRect = prevHull.WorldRect; Vector2 subVel = prevHull?.Submarine != null ? ConvertUnits.ToDisplayUnits(prevHull.Submarine.Velocity) : Vector2.Zero; @@ -465,12 +486,14 @@ namespace Barotrauma.Particles drawSize *= ((totalLifeTime - lifeTime) / prefab.GrowTime); } + Color currColor = new Color(color.ToVector4() * ColorMultiplier); + if (prefab.Sprites[spriteIndex] is SpriteSheet) { ((SpriteSheet)prefab.Sprites[spriteIndex]).Draw( spriteBatch, animFrame, new Vector2(drawPosition.X, -drawPosition.Y), - color * (color.A / 255.0f), + currColor * (currColor.A / 255.0f), prefab.Sprites[spriteIndex].Origin, drawRotation, drawSize, SpriteEffects.None, prefab.Sprites[spriteIndex].Depth); } @@ -478,7 +501,7 @@ namespace Barotrauma.Particles { prefab.Sprites[spriteIndex].Draw(spriteBatch, new Vector2(drawPosition.X, -drawPosition.Y), - color * (color.A / 255.0f), + currColor * (currColor.A / 255.0f), prefab.Sprites[spriteIndex].Origin, drawRotation, drawSize, SpriteEffects.None, prefab.Sprites[spriteIndex].Depth); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index ca333fd99..fd0a69875 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -23,7 +23,7 @@ namespace Barotrauma.Particles Prefab = prefab; } - public void Emit(float deltaTime, Vector2 position, Hull hullGuess = null, float angle = 0.0f, float particleRotation = 0.0f, float velocityMultiplier = 1.0f, float sizeMultiplier = 1.0f, float amountMultiplier = 1.0f) + public void Emit(float deltaTime, Vector2 position, Hull hullGuess = null, float angle = 0.0f, float particleRotation = 0.0f, float velocityMultiplier = 1.0f, float sizeMultiplier = 1.0f, float amountMultiplier = 1.0f, Color? colorMultiplier = null) { emitTimer += deltaTime * amountMultiplier; burstEmitTimer -= deltaTime; @@ -33,7 +33,7 @@ namespace Barotrauma.Particles float emitInterval = 1.0f / Prefab.ParticlesPerSecond; while (emitTimer > emitInterval) { - Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier); + Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier); emitTimer -= emitInterval; } } @@ -43,11 +43,11 @@ namespace Barotrauma.Particles burstEmitTimer = Prefab.EmitInterval; for (int i = 0; i < Prefab.ParticleAmount * amountMultiplier; i++) { - Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier); + Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier); } } - private void Emit(Vector2 position, Hull hullGuess, float angle, float particleRotation, float velocityMultiplier, float sizeMultiplier) + private void Emit(Vector2 position, Hull hullGuess, float angle, float particleRotation, float velocityMultiplier, float sizeMultiplier, Color? colorMultiplier = null) { angle += Rand.Range(Prefab.AngleMin, Prefab.AngleMax); @@ -61,6 +61,7 @@ namespace Barotrauma.Particles { particle.Size *= Rand.Range(Prefab.ScaleMin, Prefab.ScaleMax) * sizeMultiplier; particle.HighQualityCollisionDetection = Prefab.HighQualityCollisionDetection; + if (colorMultiplier.HasValue) { particle.ColorMultiplier = colorMultiplier.Value.ToVector4(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index 7d8e24236..234b15f6f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -157,9 +157,9 @@ namespace Barotrauma sb.AppendLine("VSync " + (GameMain.Config.VSyncEnabled ? "ON" : "OFF")); sb.AppendLine("Language: " + (GameMain.Config.Language ?? "none")); } - if (GameMain.SelectedPackages != null) + if (GameMain.Config.AllEnabledPackages != null) { - sb.AppendLine("Selected content packages: " + (!GameMain.SelectedPackages.Any() ? "None" : string.Join(", ", GameMain.SelectedPackages.Select(c => c.Name)))); + sb.AppendLine("Selected content packages: " + (!GameMain.Config.AllEnabledPackages.Any() ? "None" : string.Join(", ", GameMain.Config.AllEnabledPackages.Select(c => c.Name)))); } sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed)); sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")")); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs index 176ba9cd0..89fb295b4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs @@ -26,6 +26,9 @@ namespace Barotrauma public Action StartNewGame; public Action LoadGame; + private enum CategoryFilter { All = 0, Vanilla = 1, Custom = 2 }; + private CategoryFilter subFilter = CategoryFilter.All; + public GUIButton StartButton { get; @@ -74,6 +77,12 @@ namespace Barotrauma { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SelectedSub"), font: GUI.SubHeadingFont); + var moddedDropdown = new GUIDropDown(new RectTransform(new Vector2(1f, 0.02f), leftColumn.RectTransform), "", 3); + moddedDropdown.AddItem(TextManager.Get("clientpermission.all"), CategoryFilter.All); + moddedDropdown.AddItem(TextManager.Get("servertag.modded.false"), CategoryFilter.Vanilla); + moddedDropdown.AddItem(TextManager.Get("customrank"), CategoryFilter.Custom); + moddedDropdown.Select(0); + var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform), isHorizontal: true) { Stretch = true @@ -88,6 +97,14 @@ namespace Barotrauma searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; }; + moddedDropdown.OnSelected = (component, data) => + { + searchBox.Text = string.Empty; + subFilter = (CategoryFilter)data; + UpdateSubList(SubmarineInfo.SavedSubmarines); + return true; + }; + subList.OnSelected = OnSubSelected; } else // Spacing to fix the multiplayer campaign setup layout @@ -234,7 +251,7 @@ namespace Barotrauma Stretch = true }; - var subLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), subHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Language == "English" ? "Purchasable submarines" : TextManager.Get("workshoplabelsubmarines"), font: GUI.SubHeadingFont); + var subLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), subHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("purchasablesubmarines", fallBackTag: "workshoplabelsubmarines"), font: GUI.SubHeadingFont); var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subHolder.RectTransform), isHorizontal: true) { @@ -395,7 +412,16 @@ namespace Barotrauma public void UpdateSubList(IEnumerable submarines) { - var subsToShow = submarines.Where(s => s.IsCampaignCompatibleIgnoreClass).ToList(); + List subsToShow; + if (!isMultiplayer && subFilter != CategoryFilter.All) + { + subsToShow = submarines.Where(s => s.IsCampaignCompatibleIgnoreClass && s.IsVanillaSubmarine() == (subFilter == CategoryFilter.Vanilla)).ToList(); + } + else + { + subsToShow = submarines.Where(s => s.IsCampaignCompatibleIgnoreClass).ToList(); + } + subsToShow.Sort((s1, s2) => { int p1 = s1.Price > CampaignMode.MaxInitialSubmarinePrice ? 10 : 0; @@ -428,18 +454,22 @@ namespace Barotrauma ToolTip = textBlock.ToolTip }; #if !DEBUG - if (sub.Price > CampaignMode.MaxInitialSubmarinePrice && !GameMain.DebugDraw) + if (!GameMain.DebugDraw) { - textBlock.CanBeFocused = false; + if (sub.Price > CampaignMode.MaxInitialSubmarinePrice || !sub.IsCampaignCompatible) + { + textBlock.CanBeFocused = false; + textBlock.TextColor *= 0.5f; + } } #endif } if (SubmarineInfo.SavedSubmarines.Any()) { - var nonShuttles = subsToShow.Where(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.Shuttle) && s.Price <= CampaignMode.MaxInitialSubmarinePrice).ToList(); - if (nonShuttles.Count > 0) + var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CampaignMode.MaxInitialSubmarinePrice).ToList(); + if (validSubs.Count > 0) { - subList.Select(nonShuttles[Rand.Int(nonShuttles.Count)]); + subList.Select(validSubs[Rand.Int(validSubs.Count)]); } } } @@ -448,6 +478,7 @@ namespace Barotrauma public void UpdateLoadMenu(IEnumerable saveFiles = null) { prevSaveFiles?.Clear(); + prevSaveFiles = null; loadGameContainer.ClearChildren(); if (saveFiles == null) @@ -504,6 +535,7 @@ namespace Barotrauma }; bool isCompatible = true; + prevSaveFiles ??= new List(); if (!isMultiplayer) { nameText.Text = Path.GetFileNameWithoutExtension(saveFile); @@ -529,10 +561,10 @@ namespace Barotrauma } else { + prevSaveFiles?.Add(saveFile); string[] splitSaveFile = saveFile.Split(';'); saveFrame.UserData = splitSaveFile[0]; fileName = nameText.Text = Path.GetFileNameWithoutExtension(splitSaveFile[0]); - prevSaveFiles?.Add(fileName); if (splitSaveFile.Length > 1) { subName = splitSaveFile[1]; } if (splitSaveFile.Length > 2) { saveTime = splitSaveFile[2]; } if (splitSaveFile.Length > 3) { contentPackageStr = splitSaveFile[3]; } @@ -545,7 +577,7 @@ namespace Barotrauma if (!string.IsNullOrEmpty(contentPackageStr)) { List contentPackagePaths = contentPackageStr.Split('|').ToList(); - if (!GameSession.IsCompatibleWithSelectedContentPackages(contentPackagePaths, out string errorMsg)) + if (!GameSession.IsCompatibleWithEnabledContentPackages(contentPackagePaths, out string errorMsg)) { nameText.TextColor = GUI.Style.Red; saveFrame.ToolTip = string.Join("\n", errorMsg, TextManager.Get("campaignmode.contentpackagemismatchwarning")); @@ -696,9 +728,16 @@ namespace Barotrauma string saveFile = obj as string; if (obj == null) { return false; } - SaveUtil.DeleteSave(saveFile); - prevSaveFiles?.Remove(saveFile); - UpdateLoadMenu(prevSaveFiles); + string header = TextManager.Get("deletedialoglabel"); + string body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveFile)); + + EventEditorScreen.AskForConfirmation(header, body, () => + { + SaveUtil.DeleteSave(saveFile); + prevSaveFiles?.RemoveAll(s => s.StartsWith(saveFile)); + UpdateLoadMenu(prevSaveFiles.ToList()); + return true; + }); return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 306b1f780..43370a74b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -497,7 +497,7 @@ namespace Barotrauma }; if (Level.Loaded != null && - connection.LevelData == Level.Loaded.LevelData && + connection?.LevelData == Level.Loaded.LevelData && currentDisplayLocation == Campaign.Map?.CurrentLocation) { StartButton.Visible = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index d177ab06c..e570c3270 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -1655,9 +1655,9 @@ namespace Barotrauma.CharacterEditor if (contentPackage == null) { #if DEBUG - contentPackage = GameMain.Config.SelectedContentPackages.LastOrDefault(); + contentPackage = GameMain.Config.AllEnabledPackages.LastOrDefault(); #else - contentPackage = GameMain.Config.SelectedContentPackages.LastOrDefault(cp => cp != vanilla); + contentPackage = GameMain.Config.AllEnabledPackages.LastOrDefault(cp => cp != vanilla); #endif } if (contentPackage == null) @@ -1674,9 +1674,9 @@ namespace Barotrauma.CharacterEditor } #endif // Content package - if (!GameMain.Config.SelectedContentPackages.Contains(contentPackage)) + if (!GameMain.Config.AllEnabledPackages.Contains(contentPackage)) { - GameMain.Config.SelectContentPackage(contentPackage); + GameMain.Config.EnableRegularPackage(contentPackage); } GameMain.Config.SaveNewPlayerConfig(); @@ -1757,9 +1757,10 @@ namespace Barotrauma.CharacterEditor #endif // Add to the selected content package contentPackage.AddFile(configFilePath, ContentType.Character); + Barotrauma.IO.Validation.DevException = true; contentPackage.Save(contentPackage.Path); - DebugConsole.NewMessage(GetCharacterEditorTranslation("ContentPackageSaved").Replace("[path]", contentPackage.Path)); - CharacterPrefab.LoadFromFile(configFilePath, contentPackage, forceOverride: true); + Barotrauma.IO.Validation.DevException = false; + DebugConsole.NewMessage(GetCharacterEditorTranslation("ContentPackageSaved").Replace("[path]", contentPackage.Path)); // Ragdoll RagdollParams.ClearCache(); @@ -1783,7 +1784,11 @@ namespace Barotrauma.CharacterEditor element.SetAttributeValue("type", name); string fullPath = AnimationParams.GetDefaultFile(name, animation.AnimationType, contentPackage); element.Name = AnimationParams.GetDefaultFileName(name, animation.AnimationType); +#if DEBUG element.Save(fullPath); +#else + element.SaveSafe(fullPath); +#endif } } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index cdb039970..d60265989 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -305,7 +305,7 @@ namespace Barotrauma.CharacterEditor new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), mainElement.RectTransform, Anchor.CenterLeft), TextManager.Get("ContentPackage")); var rightContainer = new GUIFrame(new RectTransform(new Vector2(0.7f, 1), mainElement.RectTransform, Anchor.CenterRight), style: null); contentPackageDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.5f), rightContainer.RectTransform, Anchor.TopRight)); - foreach (ContentPackage cp in ContentPackage.List) + foreach (ContentPackage cp in ContentPackage.AllPackages) { #if !DEBUG if (cp == GameMain.VanillaContent) { continue; } @@ -334,15 +334,15 @@ namespace Barotrauma.CharacterEditor contentPackageNameElement.Flash(); return false; } - if (ContentPackage.List.Any(cp => cp.Name.ToLower() == contentPackageNameElement.Text.ToLower())) + if (ContentPackage.AllPackages.Any(cp => cp.Name.ToLower() == contentPackageNameElement.Text.ToLower())) { new GUIMessageBox("", TextManager.Get("charactereditor.contentpackagenameinuse", fallBackTag: "leveleditorlevelobjnametaken")); return false; } string modName = ToolBox.RemoveInvalidFileNameChars(contentPackageNameElement.Text); ContentPackage = ContentPackage.CreatePackage(contentPackageNameElement.Text, Path.Combine("Mods", modName, Steam.SteamManager.MetadataFileName), false); - ContentPackage.List.Add(ContentPackage); - GameMain.Config.SelectContentPackage(ContentPackage); + ContentPackage.AddPackage(ContentPackage); + GameMain.Config.EnableRegularPackage(ContentPackage); contentPackageDropDown.AddItem(ContentPackage.Name, ContentPackage, ContentPackage.Path); contentPackageDropDown.SelectItem(ContentPackage); contentPackageNameElement.Text = ""; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index e0d8b17ee..27edd2765 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -32,10 +32,13 @@ namespace Barotrauma private EditorNode? draggedNode; private Vector2 dragOffset; - private Dictionary markedNodes = new Dictionary(); + private readonly Dictionary markedNodes = new Dictionary(); private static string projectName = string.Empty; + private OutpostGenerationParams? lastTestParam; + private LocationType? lastTestType; + private int CreateID() { int maxId = nodeList.Any() ? nodeList.Max(node => node.ID) : 0; @@ -292,6 +295,8 @@ namespace Barotrauma string filePath = System.IO.Path.Combine(directory, $"{projectName}.sevproj"); File.WriteAllText(Path.Combine(directory, $"{projectName}.sevproj"), save.ToString()); GUI.AddMessage($"Project saved to {filePath}", GUI.Style.Green); + + AskForConfirmation(TextManager.Get("EventEditor.TestPromptHeader"), TextManager.Get("EventEditor.TestPromptBody"), CreateTestSetupMenu); return true; }; return true; @@ -520,15 +525,12 @@ namespace Barotrauma public override void Select() { - Cam.Position = Vector2.Zero; - nodeList.Clear(); projectName = TextManager.Get("EventEditor.Unnamed"); base.Select(); } public override void Deselect() { - nodeList.Clear(); base.Deselect(); } @@ -597,9 +599,9 @@ namespace Barotrauma optionElement.Add(new XAttribute("text", text)); if (end) { optionElement.Add(new XAttribute("endconversation", true)); } - if (node != null) + if (node is EventNode eventNode) { - ExportChildNodes((EventNode) node, optionElement); + ExportChildNodes(eventNode, optionElement); } newElement.Add(optionElement); @@ -748,6 +750,57 @@ namespace Barotrauma return true; }; } + + private bool CreateTestSetupMenu() + { + var msgBox = new GUIMessageBox(TextManager.Get("EventEditor.TestPromptHeader"), "", new[] { TextManager.Get("Cancel"), TextManager.Get("OK") }, + relativeSize: new Vector2(0.2f, 0.3f), minSize: new Point(300, 175)); + + var layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), msgBox.Content.RectTransform)); + + new GUITextBlock(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), TextManager.Get("EventEditor.OutpostGenParams"), font: GUI.SubHeadingFont); + GUIDropDown paramInput = new GUIDropDown(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), string.Empty, OutpostGenerationParams.Params.Count); + foreach (OutpostGenerationParams param in OutpostGenerationParams.Params) + { + paramInput.AddItem(param.Identifier, param); + } + paramInput.OnSelected = (_, param) => + { + lastTestParam = param as OutpostGenerationParams; + return true; + }; + paramInput.SelectItem(lastTestParam ?? OutpostGenerationParams.Params.FirstOrDefault()); + + new GUITextBlock(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), TextManager.Get("EventEditor.LocationType"), font: GUI.SubHeadingFont); + GUIDropDown typeInput = new GUIDropDown(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), string.Empty, LocationType.List.Count); + foreach (LocationType type in LocationType.List) + { + typeInput.AddItem(type.Identifier, type); + } + typeInput.OnSelected = (_, type) => + { + lastTestType = type as LocationType; + return true; + }; + typeInput.SelectItem(lastTestType ?? LocationType.List.FirstOrDefault()); + + // Cancel button + msgBox.Buttons[0].OnClicked = (button, o) => + { + msgBox.Close(); + return true; + }; + + // Ok button + msgBox.Buttons[1].OnClicked = (button, o) => + { + TestEvent(lastTestParam, lastTestType); + msgBox.Close(); + return true; + }; + + return true; + } private static void CreateEditMenu(ValueNode? node, NodeConnection? connection = null) { @@ -860,6 +913,40 @@ namespace Barotrauma }; } + private bool TestEvent(OutpostGenerationParams? param, LocationType? type) + { + SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(info => info.HasTag(SubmarineTag.Shuttle)); + + XElement? eventXml = ExportXML(); + EventPrefab? prefab; + if (eventXml != null) + { + prefab = new EventPrefab(eventXml); + } + else + { + GUI.AddMessage("Unable to open test enviroment because the event contains errors.", GUI.Style.Red); + return false; + } + + GameSession gameSession = new GameSession(subInfo, "", GameModePreset.TestMode, null); + TestGameMode gameMode = (TestGameMode) gameSession.GameMode; + + gameMode.SpawnOutpost = true; + gameMode.OutpostParams = param; + gameMode.OutpostType = type; + gameMode.TriggeredEvent = prefab; + gameMode.OnRoundEnd = () => + { + Submarine.Unload(); + GameMain.EventEditorScreen.Select(); + }; + + GameMain.GameScreen.Select(); + gameSession.StartRound(null, false); + return true; + } + public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { DrawnTooltip = string.Empty; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 209191df6..f60568d4c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -18,7 +18,7 @@ namespace Barotrauma private Effect damageEffect; private Texture2D damageStencil; - private Texture2D distortTexture; + private Texture2D distortTexture; private float fadeToBlackState; @@ -171,6 +171,7 @@ namespace Barotrauma //These will be visible through the LOS effect. spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); Submarine.DrawBack(spriteBatch, false, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null)); + Submarine.DrawPaintedColors(spriteBatch, false); spriteBatch.End(); graphics.SetRenderTarget(null); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index c9a688413..b40d4efda 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -274,9 +274,9 @@ namespace Barotrauma } //TODO: hacky workaround to check for wrecks and outposts, refactor SubmarineInfo and ContentType at some point - var nonPlayerFiles = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Wreck).ToList(); - nonPlayerFiles.AddRange(ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Outpost)); - nonPlayerFiles.AddRange(ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.OutpostModule)); + var nonPlayerFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Wreck).ToList(); + nonPlayerFiles.AddRange(ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Outpost)); + nonPlayerFiles.AddRange(ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.OutpostModule)); SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.Equals(GameMain.Config.QuickStartSubmarineName, StringComparison.InvariantCultureIgnoreCase)); subInfo ??= SubmarineInfo.SavedSubmarines.GetRandom(s => s.IsPlayer && !s.HasTag(SubmarineTag.Shuttle) && diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index c0708a3db..4716c4780 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -41,7 +41,6 @@ namespace Barotrauma private Tab selectedTab; private Sprite backgroundSprite; - private Sprite backgroundVignette; private readonly GUIComponent titleText; @@ -65,8 +64,6 @@ namespace Barotrauma CreateCampaignSetupUI(); }; - backgroundVignette = new Sprite("Content/UI/MainMenuVignette.png", Vector2.Zero); - new GUIImage(new RectTransform(new Vector2(0.4f, 0.25f), Frame.RectTransform, Anchor.BottomRight) { RelativeOffset = new Vector2(0.08f, 0.05f), AbsoluteOffset = new Point(-8, -8) }, style: "TitleText") @@ -863,7 +860,7 @@ namespace Barotrauma GameMain.NetLobbyScreen = new NetLobbyScreen(); try { - string exeName = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.ServerExecutable)?.FirstOrDefault()?.Path; + string exeName = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.ServerExecutable)?.FirstOrDefault()?.Path; if (string.IsNullOrEmpty(exeName)) { DebugConsole.ThrowError("No server executable defined in the selected content packages. Attempting to use the default executable..."); @@ -947,14 +944,11 @@ namespace Barotrauma #if USE_STEAM if (GameMain.Config.UseSteamMatchmaking) { - joinServerButton.Enabled = Steam.SteamManager.IsInitialized; - hostServerButton.Enabled = Steam.SteamManager.IsInitialized; + hostServerButton.Enabled = Steam.SteamManager.IsInitialized; } - steamWorkshopButton.Enabled = Steam.SteamManager.IsInitialized; + steamWorkshopButton.Enabled = Steam.SteamManager.IsInitialized; #endif #else - joinServerButton.Enabled = true; - hostServerButton.Enabled = true; #if USE_STEAM steamWorkshopButton.Enabled = true; #endif @@ -976,10 +970,14 @@ namespace Barotrauma aberrationStrength: 0.0f); } - spriteBatch.Begin(blendState: BlendState.NonPremultiplied); - backgroundVignette.Draw(spriteBatch, Vector2.Zero, Color.White, Vector2.Zero, 0.0f, - new Vector2(GameMain.GraphicsWidth / backgroundVignette.size.X, GameMain.GraphicsHeight / backgroundVignette.size.Y)); - spriteBatch.End(); + var vignette = GUI.Style.GetComponentStyle("mainmenuvignette")?.GetDefaultSprite(); + if (vignette != null) + { + spriteBatch.Begin(blendState: BlendState.NonPremultiplied); + vignette.Draw(spriteBatch, Vector2.Zero, Color.White, Vector2.Zero, 0.0f, + new Vector2(GameMain.GraphicsWidth / vignette.size.X, GameMain.GraphicsHeight / vignette.size.Y)); + spriteBatch.End(); + } } readonly string[] legalCrap = new string[] diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 5e6292b9e..02547fe7a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -15,20 +15,20 @@ namespace Barotrauma private readonly List characterSprites = new List(); //private readonly List jobPreferenceSprites = new List(); - private GUIFrame infoFrame, modeFrame; - private GUILayoutGroup infoFrameContent; - private GUIFrame myCharacterFrame; + private readonly GUIFrame infoFrame, modeFrame; + private readonly GUILayoutGroup infoFrameContent; + private readonly GUIFrame myCharacterFrame; - private GUIListBox subList, modeList; + private readonly GUIListBox subList, modeList; - private GUIListBox chatBox, playerList; - private GUIButton serverLogReverseButton; - private GUIListBox serverLogBox, serverLogFilterTicks; + private readonly GUIListBox chatBox, playerList; + private readonly GUIButton serverLogReverseButton; + private readonly GUIListBox serverLogBox, serverLogFilterTicks; private GUIComponent jobVariantTooltip; - private GUITextBox chatInput; - private GUITextBox serverLogFilter; + private readonly GUITextBox chatInput; + private readonly GUITextBox serverLogFilter; public GUITextBox ChatInput { get @@ -82,10 +82,10 @@ namespace Barotrauma private readonly GUITickBox autoRestartBox; private readonly GUITextBlock autoRestartText; - private GUIDropDown shuttleList; - private GUITickBox shuttleTickBox; + private readonly GUIDropDown shuttleList; + private readonly GUITickBox shuttleTickBox; - private GUIComponent settingsBlocker; + private readonly GUIComponent settingsBlocker; private Sprite backgroundSprite; @@ -123,15 +123,6 @@ namespace Barotrauma public GUIProgressBar FileTransferProgressBar { get; private set; } public GUITextBlock FileTransferProgressText { get; private set; } - private bool AllowSubSelection - { - get - { - return GameMain.NetworkMember.ServerSettings.Voting.AllowSubVoting || - (GameMain.Client != null && GameMain.Client.HasPermission(ClientPermissions.SelectSub)); - } - } - public GUITextBox ServerName { get; @@ -150,8 +141,8 @@ namespace Barotrauma private set; } - private GUIButton showChatButton; - private GUIButton showLogButton; + private readonly GUIButton showChatButton; + private readonly GUIButton showLogButton; public GUIListBox SubList { @@ -268,9 +259,7 @@ namespace Barotrauma foreach (MissionType type in Enum.GetValues(typeof(MissionType))) { if (type == MissionType.None || type == MissionType.All) { continue; } - - missionTypeTickBoxes[index].Selected = (((int)type & (int)value) != 0); - + missionTypeTickBoxes[index].Selected = ((int)type & (int)value) != 0; index++; } } @@ -290,8 +279,7 @@ namespace Barotrauma List> jobPreferences = new List>(); foreach (GUIComponent child in JobList.Content.Children) { - var jobPrefab = child.UserData as Pair; - if (jobPrefab == null) { continue; } + if (!(child.UserData is Pair jobPrefab)) { continue; } jobPreferences.Add(jobPrefab); } return jobPreferences; @@ -743,7 +731,7 @@ namespace Barotrauma foreach (GUIComponent child in subList.Content.Children) { if (!(child.UserData is SubmarineInfo sub)) { continue; } - child.Visible = string.IsNullOrEmpty(text) ? true : sub.DisplayName.ToLower().Contains(text.ToLower()); + child.Visible = string.IsNullOrEmpty(text) || sub.DisplayName.ToLower().Contains(text.ToLower()); } return true; }; @@ -887,7 +875,12 @@ namespace Barotrauma ContinueCampaignButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), TextManager.Get("campaigncontinue"), textAlignment: Alignment.Center) { - OnClicked = (_, __) => { GameMain.Client?.RequestStartRound(true); return true; } + OnClicked = (_, __) => + { + CoroutineManager.StartCoroutine(WaitForStartRound(ContinueCampaignButton), "WaitForStartRound"); + GameMain.Client?.RequestStartRound(true); + return true; + } }; QuitCampaignButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), TextManager.Get("pausemenusavequit"), textAlignment: Alignment.Center) @@ -1356,6 +1349,7 @@ namespace Barotrauma if (GameMain.Client == null) { return; } string newName = Client.SanitizeName(tb.Text); newName = newName.Replace(":", "").Replace(";", ""); + if (newName == GameMain.Client.Name) return; if (string.IsNullOrWhiteSpace(newName)) { tb.Text = GameMain.Client.Name; @@ -1365,6 +1359,8 @@ namespace Barotrauma if (isGameRunning) { GameMain.Client.PendingName = tb.Text; + TabMenu.PendingChanges = true; + CreateChangesPendingText(); } else { @@ -1603,13 +1599,13 @@ namespace Barotrauma private void AddSubmarine(GUIComponent subList, SubmarineInfo sub) { - if (subList is GUIListBox) + if (subList is GUIListBox listBox) { - subList = ((GUIListBox)subList).Content; + subList = listBox.Content; } - else if (subList is GUIDropDown) + else if (subList is GUIDropDown dropDown) { - subList = ((GUIDropDown)subList).ListBox.Content; + subList = dropDown.ListBox.Content; } var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), subList.RectTransform) { MinSize = new Point(0, 20) }, @@ -1655,7 +1651,7 @@ namespace Barotrauma if (sub.HasTag(SubmarineTag.Shuttle)) { - var shuttleText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), frame.RectTransform, Anchor.CenterRight), + var shuttleText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), frame.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, TextManager.Get("Shuttle", fallBackTag: "RespawnShuttle"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) { TextColor = subTextBlock.TextColor * 0.8f, @@ -1665,16 +1661,16 @@ namespace Barotrauma //make shuttles more dim in the sub list (selecting a shuttle as the main sub is allowed but not recommended) if (subList == this.subList.Content) { - subTextBlock.TextColor *= 0.5f; + subTextBlock.TextColor *= 0.8f; foreach (GUIComponent child in frame.Children) { - child.Color *= 0.5f; + child.Color *= 0.8f; } } } else { - var classText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), frame.RectTransform, Anchor.CenterRight), + var classText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), frame.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) { UserData = "classtext", @@ -1816,20 +1812,20 @@ namespace Barotrauma public void SetPlayerNameAndJobPreference(Client client) { - var PlayerFrame = (GUITextBlock)PlayerList.Content.FindChild(client); - if (PlayerFrame == null) { return; } - PlayerFrame.Text = client.Name; + var playerFrame = (GUITextBlock)PlayerList.Content.FindChild(client); + if (playerFrame == null) { return; } + playerFrame.Text = client.Name; Color color = Color.White; if (JobPrefab.Prefabs.ContainsKey(client.PreferredJob)) { color = JobPrefab.Prefabs[client.PreferredJob].UIColor; } - PlayerFrame.Color = color * 0.4f; - PlayerFrame.HoverColor = color * 0.6f; - PlayerFrame.SelectedColor = color * 0.8f; - PlayerFrame.OutlineColor = color * 0.5f; - PlayerFrame.TextColor = color; + playerFrame.Color = color * 0.4f; + playerFrame.HoverColor = color * 0.6f; + playerFrame.SelectedColor = color * 0.8f; + playerFrame.OutlineColor = color * 0.5f; + playerFrame.TextColor = color; } public void SetPlayerVoiceIconState(Client client, bool muted, bool mutedLocally) @@ -2671,7 +2667,7 @@ namespace Barotrauma return false; } - private bool SwitchJob(GUIButton button, object obj) + private bool SwitchJob(GUIButton _, object obj) { if (JobList == null) { return false; } @@ -2724,7 +2720,7 @@ namespace Barotrauma return false; } - private bool OpenJobSelection(GUIComponent child, object userData) + private bool OpenJobSelection(GUIComponent _, object __) { if (JobSelectionFrame != null) { @@ -2870,7 +2866,9 @@ namespace Barotrauma { Color = Color.Black, HoverColor = Color.Black, - SelectedColor = Color.Black + PressedColor = Color.Black, + SelectedColor = Color.Black, + CanBeFocused = false }; var textBlock = new GUITextBlock( @@ -2883,6 +2881,7 @@ namespace Barotrauma HoverColor = Color.Transparent, SelectedColor = Color.Transparent, TextColor = jobPrefab.UIColor, + HoverTextColor = Color.Lerp(jobPrefab.UIColor, Color.White, 0.5f), CanBeFocused = false, AutoScaleHorizontal = true }; @@ -2938,7 +2937,7 @@ namespace Barotrauma info.Head = new CharacterInfo.HeadInfo(info.HeadSpriteId, info.Gender, info.Race, info.HairIndex, info.BeardIndex, index, info.FaceAttachmentIndex); break; default: - DebugConsole.ThrowError($"Wearable type not implemented: {type.ToString()}"); + DebugConsole.ThrowError($"Wearable type not implemented: {type}"); return false; } info.ReloadHeadAttachments(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs index 4f37330f2..a75295b1a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs @@ -13,6 +13,8 @@ namespace Barotrauma private RectTransform prevGuiElementParent; + public Exception LoadException; + public static RoundSummaryScreen Select(Sprite backgroundSprite, RoundSummary roundSummary) { var summaryScreen = new RoundSummaryScreen() @@ -51,5 +53,16 @@ namespace Barotrauma spriteBatch.End(); } + + public override void Update(double deltaTime) + { + base.Update(deltaTime); + if (LoadException != null) + { + var temp = LoadException; + LoadException = null; + throw temp; + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index b6c376970..b6eb664b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -1,4 +1,5 @@ using Barotrauma.Extensions; +using Barotrauma.IO; using Barotrauma.Networking; using Barotrauma.Steam; using Microsoft.Xna.Framework; @@ -6,13 +7,10 @@ using Microsoft.Xna.Framework.Graphics; using RestSharp; using System; using System.Collections.Generic; -using Barotrauma.IO; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; -using System.Text; -using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; @@ -21,7 +19,7 @@ namespace Barotrauma class ServerListScreen : Screen { //how often the client is allowed to refresh servers - private TimeSpan AllowedRefreshInterval = new TimeSpan(0, 0, 3); + private readonly TimeSpan AllowedRefreshInterval = new TimeSpan(0, 0, 3); private GUIFrame menu; @@ -31,12 +29,20 @@ namespace Barotrauma private GUIButton joinButton; private ServerInfo selectedServer; + private GUIButton scanServersButton; + //friends list private GUILayoutGroup friendsButtonHolder; private GUIButton friendsDropdownButton; private GUIListBox friendsDropdown; + //Workshop downloads + private GUIFrame workshopDownloadsFrame = null; + private Steamworks.Ugc.Item? currentlyDownloadingWorkshopItem = null; + private Dictionary pendingWorkshopDownloads = null; + private string autoConnectName; private string autoConnectEndpoint; + private class FriendInfo { public UInt64 SteamID; @@ -558,7 +564,7 @@ namespace Barotrauma OnClicked = GameMain.MainMenuScreen.ReturnToMainMenu }; - new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), + scanServersButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), TextManager.Get("ServerListRefresh")) { OnClicked = (btn, userdata) => { RefreshServers(); return true; } @@ -761,7 +767,7 @@ namespace Barotrauma info.GameStarted = Screen.Selected != GameMain.NetLobbyScreen; info.GameVersion = GameMain.Version.ToString(); info.MaxPlayers = serverSettings.MaxPlayers; - info.PlayStyle = PlayStyle.SomethingDifferent; + info.PlayStyle = serverSettings.PlayStyle; info.RespondedToSteamQuery = true; info.UsingWhiteList = serverSettings.Whitelist.Enabled; info.TraitorsEnabled = serverSettings.TraitorsEnabled; @@ -892,11 +898,11 @@ namespace Barotrauma case "ServerListCompatible": bool? s1Compatible = NetworkMember.IsCompatible(GameMain.Version.ToString(), s1.GameVersion); if (!s1.ContentPackageHashes.Any()) { s1Compatible = null; } - if (s1Compatible.HasValue) { s1Compatible = s1Compatible.Value && s1.ContentPackagesMatch(GameMain.SelectedPackages); }; + if (s1Compatible.HasValue) { s1Compatible = s1Compatible.Value && s1.ContentPackagesMatch(); }; bool? s2Compatible = NetworkMember.IsCompatible(GameMain.Version.ToString(), s2.GameVersion); if (!s2.ContentPackageHashes.Any()) { s2Compatible = null; } - if (s2Compatible.HasValue) { s2Compatible = s2Compatible.Value && s2.ContentPackagesMatch(GameMain.SelectedPackages); }; + if (s2Compatible.HasValue) { s2Compatible = s2Compatible.Value && s2.ContentPackagesMatch(); }; //convert to int to make sorting easier //1 Compatible @@ -946,6 +952,9 @@ namespace Barotrauma public override void Deselect() { base.Deselect(); + + pendingWorkshopDownloads?.Clear(); + workshopDownloadsFrame = null; } public override void Update(double deltaTime) @@ -965,6 +974,43 @@ namespace Barotrauma friendsDropdown.Visible = false; } } + + if (currentlyDownloadingWorkshopItem?.IsInstalled ?? true) + { + if (pendingWorkshopDownloads?.Any() ?? false) + { + Steamworks.Ugc.Item? item = pendingWorkshopDownloads.Values.FirstOrDefault(it => it != null); + if (item != null) + { + ulong itemId = item.Value.Id; + currentlyDownloadingWorkshopItem = item; + SteamManager.SubscribeToWorkshopItem(itemId, () => + { + pendingWorkshopDownloads.Remove(itemId); + + if (SteamManager.CheckWorkshopItemInstalled(item)) + { + SteamManager.UninstallWorkshopItem(item, false, out _); + } + + if (SteamManager.InstallWorkshopItem(item, out string errorMsg, enableContentPackage: false, suppressInstallNotif: true)) + { + workshopDownloadsFrame?.FindChild((c) => c.UserData is ulong l && l == itemId, true)?.Flash(GUI.Style.Green); + } + else + { + workshopDownloadsFrame?.FindChild((c) => c.UserData is ulong l && l == itemId, true)?.Flash(GUI.Style.Red); + DebugConsole.ThrowError(errorMsg); + } + }); + } + } + else if (!string.IsNullOrEmpty(autoConnectEndpoint)) + { + JoinServer(autoConnectEndpoint, autoConnectName); + autoConnectEndpoint = null; + } + } } private void FilterServers() @@ -992,7 +1038,7 @@ namespace Barotrauma else { bool incompatible = - (!serverInfo.ContentPackageHashes.Any() && serverInfo.ContentPackagesMatch(GameMain.Config.SelectedContentPackages)) || + (!serverInfo.ContentPackageHashes.Any() && serverInfo.ContentPackagesMatch()) || (remoteVersion != null && !NetworkMember.IsCompatible(GameMain.Version, remoteVersion)); child.Visible = @@ -1018,7 +1064,7 @@ namespace Barotrauma { var playStyle = (PlayStyle)tickBox.UserData; - if (!tickBox.Selected && serverInfo.PlayStyle == playStyle) + if (!tickBox.Selected && (serverInfo.PlayStyle == playStyle || !serverInfo.PlayStyle.HasValue)) { child.Visible = false; break; @@ -1136,7 +1182,7 @@ namespace Barotrauma Port = port.ToString(), QueryPort = NetConfig.DefaultQueryPort.ToString(), GameVersion = GameMain.Version.ToString(), - PlayStyle = PlayStyle.Serious + PlayStyle = null }; var serverFrame = serverList.Content.FindChild(d => (d.UserData is ServerInfo info) && @@ -1546,6 +1592,7 @@ namespace Barotrauma { CanBeFocused = false }; + scanServersButton.Enabled = false; } else { @@ -1555,6 +1602,7 @@ namespace Barotrauma AddToServerList(info); QueueInfoQuery(info); } + scanServersButton.Enabled = true; } } else @@ -1712,7 +1760,7 @@ namespace Barotrauma CanBeFocused = false, Selected = (NetworkMember.IsCompatible(GameMain.Version.ToString(), serverInfo.GameVersion) ?? true) && - serverInfo.ContentPackagesMatch(GameMain.SelectedPackages), + serverInfo.ContentPackagesMatch(), UserData = "compatible" }; @@ -1818,19 +1866,43 @@ namespace Barotrauma for (int i = 0; i < serverInfo.ContentPackageNames.Count; i++) { - if (!GameMain.SelectedPackages.Any(cp => cp.MD5hash.Hash == serverInfo.ContentPackageHashes[i])) + bool listAsIncompatible = false; + if (serverInfo.ContentPackageWorkshopIds[i] == 0) + { + listAsIncompatible = !GameMain.Config.AllEnabledPackages.Any(cp => cp.MD5hash.Hash == serverInfo.ContentPackageHashes[i]); + } + else + { + listAsIncompatible = GameMain.Config.AllEnabledPackages.Any(cp => cp.MD5hash.Hash != serverInfo.ContentPackageHashes[i] && + cp.SteamWorkshopId == serverInfo.ContentPackageWorkshopIds[i]); + } + if (listAsIncompatible) { if (toolTip != "") toolTip += "\n"; toolTip += TextManager.GetWithVariables("ServerListIncompatibleContentPackage", new string[2] { "[contentpackage]", "[hash]" }, new string[2] { serverInfo.ContentPackageNames[i], Md5Hash.GetShortHash(serverInfo.ContentPackageHashes[i]) }); } } - + serverContent.Children.ForEach(c => c.ToolTip = toolTip); serverName.TextColor *= 0.5f; serverPlayers.TextColor *= 0.5f; } + else + { + string toolTip = ""; + for (int i = 0; i < serverInfo.ContentPackageNames.Count; i++) + { + if (!GameMain.Config.AllEnabledPackages.Any(cp => cp.MD5hash.Hash == serverInfo.ContentPackageHashes[i])) + { + if (toolTip != "") toolTip += "\n"; + toolTip += TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", serverInfo.ContentPackageNames[i]); + break; + } + } + serverContent.Children.ForEach(c => c.ToolTip = toolTip); + } serverContent.Recalculate(); @@ -1921,17 +1993,17 @@ namespace Barotrauma serverList.ClearChildren(); new GUIMessageBox(TextManager.Get("MasterServerErrorLabel"), TextManager.GetWithVariable("MasterServerErrorException", "[error]", masterServerResponse.ErrorException.ToString())); } - else if (masterServerResponse.StatusCode != System.Net.HttpStatusCode.OK) + else if (masterServerResponse.StatusCode != HttpStatusCode.OK) { serverList.ClearChildren(); switch (masterServerResponse.StatusCode) { - case System.Net.HttpStatusCode.NotFound: + case HttpStatusCode.NotFound: new GUIMessageBox(TextManager.Get("MasterServerErrorLabel"), TextManager.GetWithVariable("MasterServerError404", "[masterserverurl]", NetConfig.MasterServerUrl)); break; - case System.Net.HttpStatusCode.ServiceUnavailable: + case HttpStatusCode.ServiceUnavailable: new GUIMessageBox(TextManager.Get("MasterServerErrorLabel"), TextManager.Get("MasterServerErrorUnavailable")); break; @@ -1958,6 +2030,79 @@ namespace Barotrauma masterServerResponded = true; } + public void DownloadWorkshopItems(IEnumerable ids, string serverName, string endPointString) + { + if (workshopDownloadsFrame != null) { return; } + int rowCount = ids.Count() + 2; + + autoConnectName = serverName; autoConnectEndpoint = endPointString; + + workshopDownloadsFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), null, Color.Black * 0.5f); + pendingWorkshopDownloads = new Dictionary(); + + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.1f + 0.03f * rowCount), workshopDownloadsFrame.RectTransform, Anchor.Center, Pivot.Center)); + var innerLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, (float)rowCount / (float)(rowCount + 3)), innerFrame.RectTransform, Anchor.Center, Pivot.Center)); + + foreach (ulong id in ids) + { + pendingWorkshopDownloads.Add(id, null); + + var itemLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f / rowCount), innerLayout.RectTransform), true, Anchor.CenterLeft) + { + UserData = id + }; + TaskPool.Add("RetrieveWorkshopItemData", Steamworks.SteamUGC.QueryFileAsync(id), (t) => + { + if (t.IsFaulted) + { + TaskPool.PrintTaskExceptions(t, $"Failed to retrieve Workshop item info (ID {id})"); + return; + } + Steamworks.Ugc.Item? item = ((Task)t).Result; + + if (!item.HasValue) + { + DebugConsole.ThrowError($"Failed to find a Steam Workshop item with the ID {id}."); + return; + } + + if (pendingWorkshopDownloads.ContainsKey(id)) + { + pendingWorkshopDownloads[id] = item; + + new GUITextBlock(new RectTransform(new Vector2(0.4f, 0.67f), itemLayout.RectTransform, Anchor.CenterLeft, Pivot.CenterLeft), item.Value.Title); + + new GUIProgressBar(new RectTransform(new Vector2(0.6f, 0.67f), itemLayout.RectTransform, Anchor.CenterLeft, Pivot.CenterLeft), 0f, Color.Lime) + { + ProgressGetter = () => + { + if (item.Value.IsInstalled) { return 1.0f; } + else if (!item.Value.IsDownloading) { return 0.0f; } + return item.Value.DownloadAmount; + } + }; + } + }); + } + + var buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 2.0f / rowCount), innerLayout.RectTransform), true, Anchor.CenterLeft) + { + UserData = "buttons" + }; + + new GUIButton(new RectTransform(new Vector2(0.3f, 0.67f), buttonLayout.RectTransform, Anchor.CenterLeft, Pivot.CenterLeft), TextManager.Get("Cancel")) + { + OnClicked = (btn, obj) => + { + autoConnectEndpoint = null; + autoConnectName = null; + pendingWorkshopDownloads.Clear(); + workshopDownloadsFrame = null; + return true; + } + }; + } + private bool JoinServer(string endpoint, string serverName) { if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) @@ -2113,6 +2258,8 @@ namespace Barotrauma friendPopup?.AddToGUIUpdateList(); friendsDropdown?.AddToGUIUpdateList(); + + workshopDownloadsFrame?.AddToGUIUpdateList(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index 255b7fa58..8e4a46c49 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -325,7 +325,7 @@ namespace Barotrauma { loadedSprites.ForEach(s => s.Remove()); loadedSprites.Clear(); - var contentPackages = GameMain.Config.SelectedContentPackages.ToList(); + var contentPackages = GameMain.Config.AllEnabledPackages.ToList(); #if !DEBUG var vanilla = GameMain.VanillaContent; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs index 00ca09c65..5c9ad31fc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs @@ -1,12 +1,11 @@ -using Barotrauma.Steam; +using Barotrauma.IO; +using Barotrauma.Steam; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using RestSharp; using System; using System.Collections.Generic; -using Barotrauma.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; namespace Barotrauma @@ -44,7 +43,7 @@ namespace Barotrauma public int PendingLoads = 1; } private readonly Dictionary pendingPreviewImageDownloads = new Dictionary(); - private Dictionary itemPreviewSprites = new Dictionary(); + private readonly Dictionary itemPreviewSprites = new Dictionary(); private enum Tab { @@ -237,7 +236,7 @@ namespace Barotrauma SelectTab(Tab.Mods); - subscribedCoroutine = CoroutineManager.StartCoroutine(PollSubscribedItems()); + CoroutineManager.StartCoroutine(PollSubscribedItems()); } private GUITextBox CreateFilterBox(GUIComponent parent, GUIListBox listbox) @@ -283,7 +282,7 @@ namespace Barotrauma RefreshSubscribedItems(); } - CoroutineHandle subscribedCoroutine; + float subscribePollAdditionalWait = 0.0f; private IEnumerable PollSubscribedItems() { @@ -293,6 +292,13 @@ namespace Barotrauma while (true) { while (CoroutineManager.IsCoroutineRunning("Load")) { yield return new WaitForSeconds(1.0f); } + while (subscribePollAdditionalWait > 0.01f) + { + subscribePollAdditionalWait = Math.Min(subscribePollAdditionalWait, 3.0f); + float wait = subscribePollAdditionalWait; + yield return new WaitForSeconds(wait); + subscribePollAdditionalWait -= wait; + } uint newNumSubscribed = Steamworks.SteamUGC.NumSubscribedItems; if (newNumSubscribed != numSubscribed) { @@ -338,15 +344,6 @@ namespace Barotrauma } } - public void SubscribeToPackages(List packageUrls) - { - foreach (string url in packageUrls) - { - SteamManager.SubscribeToWorkshopItem(url); - } - GameMain.SteamWorkshopScreen.Select(); - } - public IEnumerable RefreshDownloadState() { bool isDownloading = true; @@ -420,14 +417,14 @@ namespace Barotrauma continue; } //ignore subs that are part of a workshop content package - if (ContentPackage.List.Any(cp => !string.IsNullOrEmpty(cp.SteamWorkshopUrl) && + if (ContentPackage.AllPackages.Any(cp => cp.SteamWorkshopId != 0 && cp.Files.Any(f => f.Type == ContentType.Submarine && Path.GetFullPath(f.Path) == subPath))) { continue; } //ignore subs that are defined in a content package with more files than just the sub //(these will be listed in the "content packages" section) - if (ContentPackage.List.Any(cp => cp.Files.Count > 1 && + if (ContentPackage.AllPackages.Any(cp => cp.Files.Count > 1 && cp.Files.Any(f => f.Type == ContentType.Submarine && Path.GetFullPath(f.Path) == subPath))) { continue; @@ -441,9 +438,9 @@ namespace Barotrauma { CanBeFocused = false }; - foreach (ContentPackage contentPackage in ContentPackage.List) + foreach (ContentPackage contentPackage in ContentPackage.AllPackages) { - if (!string.IsNullOrEmpty(contentPackage.SteamWorkshopUrl) || contentPackage.HideInWorkshopMenu) { continue; } + if (contentPackage.SteamWorkshopId != 0 || contentPackage.HideInWorkshopMenu) { continue; } if (contentPackage == GameMain.VanillaContent) { continue; } //don't list content packages that only define one sub (they're visible in the "Submarines" section) if (contentPackage.Files.Count == 1 && contentPackage.Files[0].Type == ContentType.Submarine) { continue; } @@ -488,7 +485,7 @@ namespace Barotrauma text = topItemFilter.Text; } - bool visible = string.IsNullOrEmpty(text) ? true : (item?.Title?.ToLower().Contains(text.ToLower()) ?? false); + bool visible = string.IsNullOrEmpty(text) || (item?.Title?.ToLower().Contains(text.ToLower()) ?? false); int prevIndex = -1; var existingFrame = listBox.Content.FindChild((component) => { return (component.UserData is Steamworks.Ugc.Item?) && (component.UserData as Steamworks.Ugc.Item?)?.Id == item?.Id; }); @@ -611,7 +608,7 @@ namespace Barotrauma if ((item?.IsSubscribed ?? false) && (item?.IsInstalled ?? false) && Directory.Exists(item?.Directory)) { - bool installed = SteamManager.CheckWorkshopItemEnabled(item); + bool installed = SteamManager.CheckWorkshopItemInstalled(item); if (!installed) { @@ -626,7 +623,7 @@ namespace Barotrauma } else { - installed = SteamManager.EnableWorkShopItem(item, out string errorMsg, Selected == this); + installed = SteamManager.InstallWorkshopItem(item, out string errorMsg, Selected == this); if (!installed) { DebugConsole.NewMessage(errorMsg, Color.Red); @@ -660,7 +657,7 @@ namespace Barotrauma { new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), rightColumn.RectTransform), TextManager.Get("WorkshopItemDownloadPending")); } - else if (!(item?.IsSubscribed ?? false)) + else if (!(item?.IsSubscribed ?? false) && (listBox != subscribedItemList)) { var downloadBtn = new GUIButton(new RectTransform(new Point((int)(32 * GUI.Scale)), rightColumn.RectTransform), "", style: "GUIPlusButton") { @@ -684,9 +681,9 @@ namespace Barotrauma var elem = subscribedItemList.Content.GetChildByUserData(item); try { - bool reselect = GameMain.Config.SelectedContentPackages.Any(cp => !string.IsNullOrWhiteSpace(cp.SteamWorkshopUrl) && cp.SteamWorkshopUrl == item?.Url); - if (!SteamManager.DisableWorkShopItem(item, false, out string errorMsg) || - !SteamManager.EnableWorkShopItem(item, out errorMsg, reselect, true)) + bool reselect = GameMain.Config.AllEnabledPackages.Any(cp => cp.SteamWorkshopId != 0 && cp.SteamWorkshopId == item?.Id); + if (!SteamManager.UninstallWorkshopItem(item, false, out string errorMsg) || + !SteamManager.InstallWorkshopItem(item, out errorMsg, reselect, true)) { DebugConsole.ThrowError($"Failed to reinstall \"{item?.Title}\": {errorMsg}", null, true); elem.Flash(GUI.Style.Red); @@ -707,8 +704,9 @@ namespace Barotrauma }; unsubBtn.OnClicked = (btn, userdata) => { - SteamManager.DisableWorkShopItem(item, true, out _); + subscribePollAdditionalWait += 1.0f; item?.Unsubscribe(); + SteamManager.UninstallWorkshopItem(item, true, out _); subscribedItemList.RemoveChild(subscribedItemList.Content.GetChildByUserData(item)); return true; }; @@ -819,31 +817,34 @@ namespace Barotrauma new Tuple(item, listBox), (task, tuple) => { - (var it, var lb) = tuple; - var previewImage = lb.Content.FindChild(item)?.GetChildByUserData("previewimage") as GUIImage; - if (previewImage != null) + //must be done in the main thread because creating/removing GUI elements is not thread-safe + CrossThread.RequestExecutionOnMainThread(() => { - previewImage.Sprite = ((Task)task).Result; - } - else - { - CreateWorkshopItemFrame(it, lb); - } + (var it, var lb) = tuple; + if (lb.Content.FindChild(item)?.GetChildByUserData("previewimage") is GUIImage previewImage) + { + previewImage.Sprite = ((Task)task).Result; + } + else + { + CreateWorkshopItemFrame(it, lb); + } - if (modsPreviewFrame.FindChild(it) != null) - { - ShowItemPreview(it, modsPreviewFrame); - } - if (browsePreviewFrame.FindChild(item) != null) - { - ShowItemPreview(it, browsePreviewFrame); - } + if (modsPreviewFrame.FindChild(it) != null) + { + ShowItemPreview(it, modsPreviewFrame); + } + if (browsePreviewFrame.FindChild(item) != null) + { + ShowItemPreview(it, browsePreviewFrame); + } - lock (pendingPreviewImageDownloads) - { - pendingPreviewImageDownloads[it.Value.Id].PendingLoads--; - if (pendingPreviewImageDownloads[it.Value.Id].PendingLoads <= 0) { pendingPreviewImageDownloads.Remove(it.Value.Id); } - } + lock (pendingPreviewImageDownloads) + { + pendingPreviewImageDownloads[it.Value.Id].PendingLoads--; + if (pendingPreviewImageDownloads[it.Value.Id].PendingLoads <= 0) { pendingPreviewImageDownloads.Remove(it.Value.Id); } + } + }); }); } @@ -872,15 +873,13 @@ namespace Barotrauma { if (item == null) { return false; } - if (!(item?.IsSubscribed ?? false)) { item?.Subscribe(); } - var parentElement = downloadButton.Parent; parentElement.RemoveChild(downloadButton); var textBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), parentElement.RectTransform), TextManager.Get("WorkshopItemDownloading")); - item?.Download(onInstalled: () => + SteamManager.SubscribeToWorkshopItem(item.Value.Id, () => { - if (SteamManager.EnableWorkShopItem(item, out _)) + if (SteamManager.InstallWorkshopItem(item, out _)) { textBlock.Text = TextManager.Get("workshopiteminstalled"); frame.Flash(GUI.Style.Green); @@ -1022,8 +1021,9 @@ namespace Barotrauma UserData = item, OnClicked = (btn, userdata) => { - SteamManager.DisableWorkShopItem(item, true, out _); + subscribePollAdditionalWait += 1.0f; item?.Unsubscribe(); + SteamManager.UninstallWorkshopItem(item, true, out _); subscribedItemList.RemoveChild(subscribedItemList.Content.GetChildByUserData(item)); itemPreviewFrame.ClearChildren(); return true; @@ -1294,7 +1294,7 @@ namespace Barotrauma new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), topLeftColumn.RectTransform), TextManager.Get("WorkshopItemCorePackage")) { ToolTip = TextManager.Get("WorkshopItemCorePackageTooltip"), - Selected = itemContentPackage.CorePackage, + Selected = itemContentPackage.IsCorePackage, OnSelected = (tickbox) => { if (tickbox.Selected) @@ -1309,12 +1309,12 @@ namespace Barotrauma } else { - itemContentPackage.CorePackage = tickbox.Selected; + itemContentPackage.IsCorePackage = tickbox.Selected; } } else { - itemContentPackage.CorePackage = false; + itemContentPackage.IsCorePackage = false; } return true; } @@ -1478,7 +1478,7 @@ namespace Barotrauma SelectTab(Tab.Browse); deleteVerification.Close(); createItemFrame.ClearChildren(); - itemContentPackage.SteamWorkshopUrl = ""; + itemContentPackage.SteamWorkshopId = 0; itemContentPackage.Save(itemContentPackage.Path); return true; }; @@ -1535,9 +1535,17 @@ namespace Barotrauma return; } - if (filePath != previewImagePath) + if (Path.GetFullPath(filePath) != previewImagePath) { - File.Copy(filePath, previewImagePath, overwrite: true); + try + { + File.Copy(filePath, previewImagePath, overwrite: true); + } + catch (System.IO.IOException e) + { + DebugConsole.ThrowError("Failed to copy the preview image \"{previewImagePath}\" to the mod folder.", e); + return; + } } if (itemPreviewSprites.ContainsKey(previewImagePath)) @@ -1560,13 +1568,12 @@ namespace Barotrauma string modFolder = Path.GetDirectoryName(itemContentPackage.Path); string filePathRelativeToModFolder = UpdaterUtil.GetRelativePath(file, Path.Combine(Environment.CurrentDirectory, modFolder)); - string destinationPath; //file is not inside the mod folder, we need to move it if (filePathRelativeToModFolder.StartsWith("..") || Path.GetPathRoot(Environment.CurrentDirectory) != Path.GetPathRoot(file)) { - destinationPath = Path.Combine(modFolder, Path.GetFileName(file)); + string destinationPath = Path.Combine(modFolder, Path.GetFileName(file)); //add a number to the filename if a file with the same name already exists i = 2; while (File.Exists(destinationPath)) @@ -1582,11 +1589,7 @@ namespace Barotrauma { DebugConsole.ThrowError("Copying the file \"" + file + "\" to the mod folder failed.", e); return; - } - } - else - { - destinationPath = Path.Combine(modFolder, filePathRelativeToModFolder); + } } } RefreshCreateItemFileList(); @@ -1605,7 +1608,7 @@ namespace Barotrauma if (itemContentPackage == null) return; var contentTypes = Enum.GetValues(typeof(ContentType)); - List files = itemContentPackage.Files.ToList(); + List files = itemContentPackage.FilesUnsaved.ToList(); for (int i = files.Count - 1; i >= 0; i--) { @@ -1613,15 +1616,14 @@ namespace Barotrauma bool fileExists = File.Exists(contentFile.Path); - if (contentFile.Type == ContentType.Executable || - contentFile.Type == ContentType.ServerExecutable) + if (contentFile.Type == ContentType.ServerExecutable) { fileExists |= File.Exists(Path.GetFileNameWithoutExtension(contentFile.Path) + ".dll"); } if (!fileExists) { - itemContentPackage.Files.Remove(contentFile); + itemContentPackage.RemoveFile(contentFile); files.RemoveAt(i); } } @@ -1653,8 +1655,7 @@ namespace Barotrauma bool illegalPath = !ContentPackage.IsModFilePathAllowed(contentFile); bool fileExists = File.Exists(contentFile.Path); - if (contentFile.Type == ContentType.Executable || - contentFile.Type == ContentType.ServerExecutable) + if (contentFile.Type == ContentType.ServerExecutable) { fileExists |= File.Exists(Path.GetFileNameWithoutExtension(contentFile.Path) + ".dll"); } @@ -1674,7 +1675,7 @@ namespace Barotrauma var tickBox = new GUITickBox(new RectTransform(Vector2.One, content.RectTransform, scaleBasis: ScaleBasis.BothHeight), "") { - Selected = itemContentPackage.Files.Contains(contentFile), + Selected = itemContentPackage.FilesUnsaved.Contains(contentFile), UserData = contentFile }; @@ -1683,11 +1684,11 @@ namespace Barotrauma ContentFile f = tb.UserData as ContentFile; if (tb.Selected) { - if (!itemContentPackage.Files.Contains(f)) { itemContentPackage.Files.Add(f); } + if (!itemContentPackage.FilesUnsaved.Contains(f)) { itemContentPackage.AddFile(f); } } else { - if (itemContentPackage.Files.Contains(f)) { itemContentPackage.Files.Remove(f); } + if (itemContentPackage.FilesUnsaved.Contains(f)) { itemContentPackage.RemoveFile(f); } } return true; @@ -1702,7 +1703,7 @@ namespace Barotrauma nameText.TextColor = GUI.Style.Red; tickBox.ToolTip = TextManager.Get("WorkshopItemFileNotFound"); } - else if (illegalPath && !ContentPackage.List.Any(cp => cp.Files.Any(f => Path.GetFullPath(f.Path) == Path.GetFullPath(contentFile.Path)))) + else if (illegalPath && !ContentPackage.AllPackages.Any(cp => cp.FilesUnsaved.Any(f => Path.GetFullPath(f.Path) == Path.GetFullPath(contentFile.Path)))) { nameText.TextColor = GUI.Style.Red; tickBox.ToolTip = TextManager.Get("WorkshopItemIllegalPath"); @@ -1839,7 +1840,10 @@ namespace Barotrauma { new GUIMessageBox( TextManager.Get("Error"), - TextManager.GetWithVariable("WorkshopItemPublishFailed", "[itemname]", item?.Title) + " Task ended with status "+workshopPublishStatus?.TaskStatus?.ToString()); + TextManager.GetWithVariable("WorkshopItemPublishFailed", "[itemname]", item?.Title) + + (workshopPublishStatus?.TaskStatus != null ? + " Task ended with status " +workshopPublishStatus?.TaskStatus?.ToString() : + " Publish failed with result "+ workshopPublishStatus.Result?.Result.ToString())); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index e0789d9cc..4b6c4a82e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -87,6 +87,9 @@ namespace Barotrauma private GUIDropDown linkedSubBox; + private static GUIComponent autoSaveLabel; + private static int maxAutoSaves = GameSettings.MaximumAutoSaves; + //a Character used for picking up and manipulating items private Character dummyCharacter; @@ -113,7 +116,8 @@ namespace Barotrauma private const string containerDeleteTag = "containerdelete"; private GUIImage previewImage; - + private GUILayoutGroup previewImageButtonHolder; + private GUIListBox contextMenu; private const int submarineNameLimit = 30; @@ -133,6 +137,10 @@ namespace Barotrauma public override Camera Cam => cam; + public static XDocument AutoSaveInfo; + private static readonly string autoSavePath = Path.Combine(SubmarineInfo.SavePath, ".AutoSaves"); + private static readonly string autoSaveInfoPath = Path.Combine(autoSavePath, "autosaves.xml"); + private static string GetSubDescription() { string localizedDescription = TextManager.Get("submarine.description." + (Submarine.MainSub?.Info.Name ?? ""), true); @@ -429,6 +437,8 @@ namespace Barotrauma lightComponent.Light.Color = item.Container != null || (item.body != null && !item.body.Enabled) ? Color.Transparent : lightComponent.LightColor; + lightComponent.Light.Rotation = (-lightComponent.Rotation - MathHelper.ToRadians(lightComponent.Item.Rotation)); + lightComponent.Light.LightSpriteEffect = lightComponent.Item.SpriteEffects; } } } @@ -875,6 +885,33 @@ namespace Barotrauma { base.Select(); + if (!Directory.Exists(autoSavePath)) + { + System.IO.DirectoryInfo e = Directory.CreateDirectory(autoSavePath); + e.Attributes = System.IO.FileAttributes.Directory | System.IO.FileAttributes.Hidden; + if (!e.Exists) + { + DebugConsole.ThrowError("Failed to create auto save directory!"); + } + } + + if (!File.Exists(autoSaveInfoPath)) + { + try + { + AutoSaveInfo = new XDocument(new XElement("AutoSaves")); + IO.SafeXML.SaveSafe(AutoSaveInfo, autoSaveInfoPath); + } + catch (Exception e) + { + DebugConsole.ThrowError("Saving auto save info to \"" + autoSaveInfoPath + "\" failed!", e); + } + } + else + { + AutoSaveInfo = XMLExtensions.TryLoadXml(autoSaveInfoPath); + } + GameMain.LightManager.AmbientLight = Level.Loaded?.GenerationParams?.AmbientLightColor ?? LevelGenerationParams.LevelParams?.FirstOrDefault()?.AmbientLightColor ?? @@ -989,6 +1026,9 @@ namespace Barotrauma { base.Deselect(); + autoSaveLabel?.Parent?.RemoveChild(autoSaveLabel); + autoSaveLabel = null; + TimeSpan timeInEditor = DateTime.Now - editorSelectedTime; #if USE_STEAM Steam.SteamManager.IncrementStat("hoursineditor", (float)timeInEditor.TotalHours); @@ -1191,13 +1231,7 @@ namespace Barotrauma if (Submarine.MainSub != null) { isAutoSaving = true; - string filePath = Path.Combine(SubmarineInfo.SavePath, ".AutoSaves"); - if (!Directory.Exists(filePath)) - { - var e = Directory.CreateDirectory(filePath); - e.Attributes = System.IO.FileAttributes.Directory | System.IO.FileAttributes.Hidden; - if (!e.Exists) { return; } - } + if (!Directory.Exists(autoSavePath)) { return; } XDocument doc = new XDocument(new XElement("Submarine")); Submarine.MainSub.SaveToXElement(doc.Root); @@ -1205,12 +1239,48 @@ namespace Barotrauma { try { - SaveUtil.CompressStringToFile(Path.Combine(filePath, "AutoSave.sub"), doc.ToString()); - CrossThread.RequestExecutionOnMainThread(() => GUI.AddMessage(TextManager.Get("AutoSaved"), GUI.Style.Green, playSound: false)); + Barotrauma.IO.Validation.DevException = true; + TimeSpan time = DateTime.UtcNow - DateTime.MinValue; + string filePath = Path.Combine(autoSavePath, $"AutoSave_{(ulong)time.TotalMilliseconds}.sub"); + SaveUtil.CompressStringToFile(filePath, doc.ToString()); + + CrossThread.RequestExecutionOnMainThread(() => + { + if (AutoSaveInfo?.Root == null) { return; } + + int saveCount = AutoSaveInfo.Root.Elements().Count(); + while (AutoSaveInfo.Root.Elements().Count() > maxAutoSaves) + { + XElement min = AutoSaveInfo.Root.Elements().OrderBy(element => element.GetAttributeUInt64("time", 0)).FirstOrDefault(); + string path = min.GetAttributeString("file", ""); + if (string.IsNullOrWhiteSpace(path)) { continue; } + + if (IO.File.Exists(path)) { IO.File.Delete(path); } + min?.Remove(); + } + + XElement newElement = new XElement("AutoSave", + new XAttribute("file", filePath), + new XAttribute("name", Submarine.MainSub.Info.Name), + new XAttribute("time", (ulong)time.TotalSeconds)); + AutoSaveInfo.Root.Add(newElement); + + try + { + IO.SafeXML.SaveSafe(AutoSaveInfo, autoSaveInfoPath); + } + catch (Exception e) + { + DebugConsole.ThrowError("Saving auto save info to \"" + autoSaveInfoPath + "\" failed!", e); + } + }); + + Barotrauma.IO.Validation.DevException = false; + CrossThread.RequestExecutionOnMainThread(DisplayAutoSavePrompt); } catch (Exception e) { - CrossThread.RequestExecutionOnMainThread(() => DebugConsole.ThrowError("Saving submarine \"" + filePath + "\" failed!", e)); + CrossThread.RequestExecutionOnMainThread(() => DebugConsole.ThrowError("Auto saving submarine failed!", e)); } isAutoSaving = false; }) { Name = "Auto Save Thread" }; @@ -1219,6 +1289,33 @@ namespace Barotrauma } } + private static void DisplayAutoSavePrompt() + { + if (Selected != GameMain.SubEditorScreen) { return; } + autoSaveLabel?.Parent?.RemoveChild(autoSaveLabel); + + string label = TextManager.Get("AutoSaved"); + autoSaveLabel = new GUILayoutGroup(new RectTransform(new Point(GUI.IntScale(150), GUI.IntScale(32)), GameMain.SubEditorScreen.EntityMenu.RectTransform, Anchor.TopRight) + { + ScreenSpaceOffset = new Point(-GUI.IntScale(16), -GUI.IntScale(48)) + }, isHorizontal: true) + { + CanBeFocused = false + }; + + GUIImage checkmark = new GUIImage(new RectTransform(new Vector2(0.25f, 1f), autoSaveLabel.RectTransform), style: "MissionCompletedIcon", scaleToFit: true); + GUITextBlock labelComponent = new GUITextBlock(new RectTransform(new Vector2(0.75f, 1f), autoSaveLabel.RectTransform), label, font: GUI.SubHeadingFont, color: GUI.Style.Green) + { + Padding = Vector4.Zero, + AutoScaleHorizontal = true, + AutoScaleVertical = true + }; + + labelComponent.FadeOut(0.5f, true, 1f); + checkmark.FadeOut(0.5f, true, 1f); + autoSaveLabel?.FadeOut(0.5f, true, 1f); + } + private bool SaveSub(GUIButton button, object obj) { if (string.IsNullOrWhiteSpace(nameBox.Text)) @@ -1238,6 +1335,7 @@ namespace Barotrauma if (Submarine.MainSub.Info?.OutpostModuleInfo != null) { contentType = ContentType.OutpostModule; + Submarine.MainSub.Info.PreviewImage = null; } break; case SubmarineType.Outpost: @@ -1252,7 +1350,7 @@ namespace Barotrauma #if DEBUG var existingFiles = ContentPackage.GetFilesOfType(GameMain.VanillaContent.ToEnumerable(), contentType); #else - var existingFiles = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages.Where(c => c != GameMain.VanillaContent), contentType); + var existingFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages.Where(c => c != GameMain.VanillaContent), contentType); #endif specialSavePath = existingFiles.FirstOrDefault(f => Path.GetFullPath(f.Path) != Path.GetFullPath(SubmarineInfo.SavePath) && ContentPackage.IsModFilePathAllowed(f.Path))?.Path; @@ -1332,7 +1430,7 @@ namespace Barotrauma { directoryName = specialSavePath; savePath = Path.Combine(directoryName, savePath); - ContentPackage contentPackage = GameMain.Config.SelectedContentPackages.Find(cp => cp.Files.Any(f => Path.GetDirectoryName(f.Path) == directoryName)); + ContentPackage contentPackage = GameMain.Config.AllEnabledPackages.FirstOrDefault(cp => cp.Files.Any(f => Path.GetDirectoryName(f.Path) == directoryName)); bool allowSavingToVanilla = false; #if DEBUG @@ -1345,7 +1443,7 @@ namespace Barotrauma msgBox.Buttons[0].OnClicked = (bt, userdata) => { contentPackage.AddFile(savePath, ContentType.OutpostModule); - contentPackage.Save(contentPackage.Path); + contentPackage.Save(contentPackage.Path, reload: false); msgBox.Close(); return true; }; @@ -1367,10 +1465,10 @@ namespace Barotrauma if (forceToSubFolder && subDirs.Length > 1 && subDirs[0].Equals("Mods", StringComparison.InvariantCultureIgnoreCase)) { string modName = subDirs[1]; - ContentPackage contentPackage = ContentPackage.List.Find(p => p.Name.Equals(modName, StringComparison.InvariantCultureIgnoreCase)); + ContentPackage contentPackage = ContentPackage.AllPackages.FirstOrDefault(p => p.Name.Equals(modName, StringComparison.InvariantCultureIgnoreCase)); if (contentPackage != null) { - Steamworks.Data.PublishedFileId packageId = Steam.SteamManager.GetWorkshopItemIDFromUrl(contentPackage.SteamWorkshopUrl); + Steamworks.Data.PublishedFileId packageId = contentPackage.SteamWorkshopId; Task itemInfoTask = Steamworks.Ugc.Item.GetAsync(packageId); Task itemUpdateTask = Task.Run(async () => @@ -1389,11 +1487,11 @@ namespace Barotrauma forceToSubFolder = false; string targetPath = Path.Combine(prevDir, savePath).CleanUpPath(); if (!contentPackage.Files.Any(f => f.Type == ContentType.Submarine && - f.Path.CleanUpPath().Equals(targetPath, StringComparison.InvariantCultureIgnoreCase))) + f.Path.CleanUpPath().Equals(targetPath, StringComparison.InvariantCultureIgnoreCase))) { - contentPackage.Files.Add(new ContentFile(targetPath, ContentType.Submarine)); + contentPackage.AddFile(new ContentFile(targetPath, ContentType.Submarine)); } - contentPackage.Save(contentPackage.Path); + contentPackage.Save(contentPackage.Path, reload: false); } } } @@ -1420,7 +1518,8 @@ namespace Barotrauma if (Submarine.MainSub != null) { - if (previewImage?.Sprite?.Texture != null) + Barotrauma.IO.Validation.DevException = true; + if (previewImage?.Sprite?.Texture != null && Submarine.MainSub.Info.Type != SubmarineType.OutpostModule) { bool savePreviewImage = true; using System.IO.MemoryStream imgStream = new System.IO.MemoryStream(); @@ -1439,7 +1538,8 @@ namespace Barotrauma { Submarine.MainSub.SaveAs(savePath); } - + Barotrauma.IO.Validation.DevException = false; + Submarine.MainSub.CheckForErrors(); GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUI.Style.Green); @@ -1918,6 +2018,7 @@ namespace Barotrauma { Submarine.MainSub.Info.OutpostModuleInfo ??= new OutpostModuleInfo(Submarine.MainSub.Info); } + previewImageButtonHolder.Children.ForEach(c => c.Enabled = type != SubmarineType.OutpostModule); outpostSettingsContainer.Visible = type == SubmarineType.OutpostModule; outpostSettingsContainer.IgnoreLayoutGroups = !outpostSettingsContainer.Visible; @@ -1925,7 +2026,6 @@ namespace Barotrauma subSettingsContainer.IgnoreLayoutGroups = !subSettingsContainer.Visible; return true; }; - subTypeDropdown.SelectItem(Submarine.MainSub.Info.Type); subSettingsContainer.RectTransform.MinSize = new Point(0, subSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); // right column --------------------------------------------------- @@ -1935,7 +2035,7 @@ namespace Barotrauma var previewImageHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), rightColumn.RectTransform), style: null) { Color = Color.Black, CanBeFocused = false }; previewImage = new GUIImage(new RectTransform(Vector2.One, previewImageHolder.RectTransform), Submarine.MainSub?.Info.PreviewImage, scaleToFit: true); - var previewImageButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; + previewImageButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), previewImageButtonHolder.RectTransform), TextManager.Get("SubPreviewImageCreate"), style: "GUIButtonSmall") { @@ -2026,7 +2126,7 @@ namespace Barotrauma if (Submarine.MainSub != null) { List contentPacks = Submarine.MainSub.Info.RequiredContentPackages.ToList(); - foreach (ContentPackage contentPack in ContentPackage.List) + foreach (ContentPackage contentPack in ContentPackage.AllPackages) { //don't show content packages that only define submarine files //(it doesn't make sense to require another sub to be installed to install this one) @@ -2085,6 +2185,8 @@ namespace Barotrauma descriptionBox.Text = Submarine.MainSub == null ? "" : Submarine.MainSub.Info.Description; submarineDescriptionCharacterCount.Text = descriptionBox.Text.Length + " / " + submarineDescriptionLimit; + subTypeDropdown.SelectItem(Submarine.MainSub.Info.Type); + if (quickSave) { SaveSub(saveButton, saveButton.UserData); } } @@ -2252,7 +2354,7 @@ namespace Barotrauma new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, loadFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); - var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.5f), loadFrame.RectTransform, Anchor.Center) { MinSize = new Point(350, 500) }); + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.25f, 0.5f), loadFrame.RectTransform, Anchor.Center) { MinSize = new Point(350, 500) }); var paddedLoadFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f }; @@ -2352,17 +2454,63 @@ namespace Barotrauma return true; }; - var loadAutoSave = new GUIButton(new RectTransform(Vector2.One, deleteButtonHolder.RectTransform, Anchor.BottomCenter), TextManager.Get("LoadAutoSave")) + + if (AutoSaveInfo?.Root != null) { - Enabled = File.Exists(Path.Combine(SubmarineInfo.SavePath, ".AutoSaves", "AutoSave.sub")), - ToolTip = TextManager.Get("LoadAutoSaveTooltip"), - UserData = "loadautosave", - OnClicked = (button, o) => + int min = Math.Min(6, AutoSaveInfo.Root.Elements().Count()); + var loadAutoSave = new GUIDropDown(new RectTransform(Vector2.One, deleteButtonHolder.RectTransform, Anchor.BottomCenter), TextManager.Get("LoadAutoSave"), elementCount: min) { - LoadAutoSave(); - return true; + Enabled = File.Exists(Path.Combine(SubmarineInfo.SavePath, ".AutoSaves", "AutoSave.sub")), + ToolTip = TextManager.Get("LoadAutoSaveTooltip"), + UserData = "loadautosave", + OnSelected = (button, o) => + { + LoadAutoSave(o); + return true; + } + }; + foreach (XElement saveElement in AutoSaveInfo.Root.Elements().Reverse()) + { + DateTime time = DateTime.MinValue.AddSeconds(saveElement.GetAttributeUInt64("time", 0)); + TimeSpan difference = DateTime.UtcNow - time; + + string tooltip = TextManager.GetWithVariables("subeditor.autosaveage", + new[] + { + "[hours]", + "[minutes]", + "[seconds]" + }, + new[] + { + ((int)Math.Floor(difference.TotalHours)).ToString(), + difference.Minutes.ToString(), + difference.Seconds.ToString() + }); + + string submarineName = saveElement.GetAttributeString("name", TextManager.Get("UnspecifiedSubFileName")); + string timeFormat; + + double totalMinutes = difference.TotalMinutes; + + if (totalMinutes < 1) + { + timeFormat = TextManager.Get("subeditor.savedjustnow"); + } + else if (totalMinutes > 60) + { + timeFormat = TextManager.Get("subeditor.savedmorethanhour"); + } + else + { + timeFormat = TextManager.GetWithVariable("subeditor.saveageminutes", "[minutes]", difference.Minutes.ToString()); + } + + string entryName = TextManager.GetWithVariables("subeditor.autosaveentry", new []{ "[submarine]", "[saveage]" }, new []{ submarineName, timeFormat }); + + loadAutoSave.AddItem(entryName, saveElement, tooltip); } - }; + } var controlBtnHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), paddedLoadFrame.RectTransform), isHorizontal: true) { RelativeSpacing = 0.2f, Stretch = true }; @@ -2396,12 +2544,15 @@ namespace Barotrauma /// Recovers the auto saved submarine /// /// - private void LoadAutoSave() + private void LoadAutoSave(object UserData) { - string filePath = Path.Combine(SubmarineInfo.SavePath, ".AutoSaves", "AutoSave.sub"); + if (!(UserData is XElement element)) { return; } + + string filePath = element.GetAttributeString("file", ""); + if (string.IsNullOrWhiteSpace(filePath)) { return; } var loadedSub = Submarine.Load(new SubmarineInfo(filePath), true); - + // set the submarine file path to the "default" value loadedSub.Info.FilePath = Path.Combine(SubmarineInfo.SavePath, $"{TextManager.Get("UnspecifiedSubFileName")}.sub"); loadedSub.Info.Name = TextManager.Get("UnspecifiedSubFileName"); @@ -2418,13 +2569,13 @@ namespace Barotrauma Submarine.MainSub.UpdateTransform(); Submarine.MainSub.Info.Name = loadedSub.Info.Name; subNameLabel.Text = ToolBox.LimitString(loadedSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); - + CreateDummyCharacter(); - + cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; loadFrame = null; - + //turn off lights that are inside an inventory (cabinet for example) foreach (Item item in Item.ItemList) { @@ -2507,9 +2658,9 @@ namespace Barotrauma //if the sub is included in a content package that only defines that one sub, //delete the content package as well ContentPackage subPackage = null; - foreach (ContentPackage cp in ContentPackage.List) + foreach (ContentPackage cp in ContentPackage.RegularPackages) { - if (!cp.CorePackage && cp.Files.Count == 1 && Path.GetFullPath(cp.Files[0].Path) == Path.GetFullPath(sub.FilePath)) + if (cp.Files.Count == 1 && Path.GetFullPath(cp.Files[0].Path) == Path.GetFullPath(sub.FilePath)) { subPackage = cp; break; @@ -2602,7 +2753,7 @@ namespace Barotrauma { var textBlock = child.GetChild(); child.Visible = - (!selectedCategory.HasValue || selectedCategory == ((MapEntityPrefab) child.UserData).Category) && + (!selectedCategory.HasValue || ((MapEntityPrefab) child.UserData).Category.HasFlag(selectedCategory)) && ((MapEntityPrefab) child.UserData).Name.ToLower().Contains(filter); if (child.Visible && dummyCharacter?.SelectedConstruction?.OwnInventory != null) @@ -2677,7 +2828,7 @@ namespace Barotrauma }; Item target = null; - + var single = targets.Count == 1 ? targets.Single() : null; if (single is Item item && item.Components.Any(ic => !(ic is ConnectionPanel) && !(ic is Repairable) && ic.GuiFrame != null)) { @@ -2690,10 +2841,17 @@ namespace Barotrauma if (PlayerInput.IsShiftDown()) { new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("CharacterEditor.EditBackgroundColor"), font: GUI.SmallFont) + TextManager.Get("CharacterEditor.EditBackgroundColor"), font: GUI.SmallFont) { UserData = "bgcolor" }; + + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("editor.selectsame"), font: GUI.SmallFont) + { + UserData = "selectsame", + Enabled = targets.Count > 0 + }; } else { @@ -2763,6 +2921,10 @@ namespace Barotrauma case "bgcolor": CreateBackgroundColorPicker(); break; + case "selectsame": + IEnumerable matching = MapEntity.mapEntityList.Where(e => targets.Any(t => t.prefab.Identifier == e.prefab.Identifier) && !MapEntity.SelectedList.Contains(e)); + MapEntity.SelectedList.AddRange(matching); + break; case "copy": MapEntity.Copy(targets); break; @@ -2912,7 +3074,7 @@ namespace Barotrauma { if (dummyCharacter == null || itemContainer == null) { return; } - if ((itemContainer.GetComponent() != null || itemContainer.GetComponent() != null) && itemContainer.GetComponent() != null) + if (((itemContainer.GetComponent() is { } holdable && !holdable.Attached) || itemContainer.GetComponent() != null) && itemContainer.GetComponent() != null) { // We teleport our dummy character to the item so it appears as the entity stays still when in reality the dummy is holding it oldItemPosition = itemContainer.SimPosition; @@ -3443,6 +3605,8 @@ namespace Barotrauma public override void AddToGUIUpdateList() { + if (GUI.DisableHUD) { return; } + MapEntity.FilteredSelectedList.FirstOrDefault()?.AddToGUIUpdateList(); EntityMenu.AddToGUIUpdateList(); showEntitiesPanel.AddToGUIUpdateList(); @@ -4079,6 +4243,7 @@ namespace Barotrauma e is Structure s && (ShowThalamus || !s.prefab.Category.HasFlag(MapEntityCategory.Thalamus)) && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null)); + Submarine.DrawPaintedColors(spriteBatch, true); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); @@ -4121,7 +4286,7 @@ namespace Barotrauma } //-------------------- HUD ----------------------------- - + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); if (Submarine.MainSub != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs index 72f509c2e..fb85c55a1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs @@ -114,12 +114,17 @@ namespace Barotrauma.Sounds public virtual SoundChannel Play(float gain, float range, Vector2 position, bool muffle = false) { - return new SoundChannel(this, gain, new Vector3(position.X, position.Y, 0.0f), range * 0.4f, range, "default", muffle); + return new SoundChannel(this, gain, new Vector3(position.X, position.Y, 0.0f), 1.0f, range * 0.4f, range, "default", muffle); } - public virtual SoundChannel Play(Vector3? position, float gain, bool muffle = false) + public virtual SoundChannel Play(float gain, float range, float freqMult, Vector2 position, bool muffle = false) { - return new SoundChannel(this, gain, position, BaseNear, BaseFar, "default", muffle); + return new SoundChannel(this, gain, new Vector3(position.X, position.Y, 0.0f), freqMult, range * 0.4f, range, "default", muffle); + } + + public virtual SoundChannel Play(Vector3? position, float gain, float freqMult = 1.0f, bool muffle = false) + { + return new SoundChannel(this, gain, position, freqMult, BaseNear, BaseFar, "default", muffle); } public virtual SoundChannel Play(float gain) @@ -135,7 +140,7 @@ namespace Barotrauma.Sounds public virtual SoundChannel Play(float? gain, string category) { if (Owner.CountPlayingInstances(this) >= MaxSimultaneousInstances) { return null; } - return new SoundChannel(this, gain ?? BaseGain, null, BaseNear, BaseFar, category); + return new SoundChannel(this, gain ?? BaseGain, null, 1.0f, BaseNear, BaseFar, category); } static protected void CastBuffer(float[] inBuffer, short[] outBuffer, int length) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index 568ab0619..b47541a36 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -3,6 +3,7 @@ using OpenAL; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Threading; +using System.Diagnostics; namespace Barotrauma.Sounds { @@ -100,28 +101,34 @@ namespace Barotrauma.Sounds { if (float.IsNaN(position.Value.X)) { - throw new Exception("Failed to set source's position: " + debugName + ", position.X is NaN"); + DebugConsole.ThrowError("Failed to set source's position: " + debugName + ", position.X is NaN", appendStackTrace: true); + return; } if (float.IsNaN(position.Value.Y)) { - throw new Exception("Failed to set source's position: " + debugName + ", position.Y is NaN"); + DebugConsole.ThrowError("Failed to set source's position: " + debugName + ", position.Y is NaN", appendStackTrace: true); + return; } if (float.IsNaN(position.Value.Z)) { - throw new Exception("Failed to set source's position: " + debugName + ", position.Z is NaN"); + DebugConsole.ThrowError("Failed to set source's position: " + debugName + ", position.Z is NaN", appendStackTrace: true); + return; } if (float.IsInfinity(position.Value.X)) { - throw new Exception("Failed to set source's position: " + debugName + ", position.X is Infinity"); + DebugConsole.ThrowError("Failed to set source's position: " + debugName + ", position.X is Infinity", appendStackTrace: true); + return; } if (float.IsInfinity(position.Value.Y)) { - throw new Exception("Failed to set source's position: " + debugName + ", position.Y is Infinity"); + DebugConsole.ThrowError("Failed to set source's position: " + debugName + ", position.Y is Infinity", appendStackTrace: true); + return; } if (float.IsInfinity(position.Value.Z)) { - throw new Exception("Failed to set source's position: " + debugName + ", position.Z is Infinity"); + DebugConsole.ThrowError("Failed to set source's position: " + debugName + ", position.Z is Infinity", appendStackTrace: true); + return; } uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); @@ -129,14 +136,16 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to enable source's relative flag: " + debugName + ", " + Al.GetErrorString(alError)); + DebugConsole.ThrowError("Failed to enable source's relative flag: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; } Al.Source3f(alSource, Al.Position, position.Value.X, position.Value.Y, position.Value.Z); alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to set source's position: " + debugName + ", " + Al.GetErrorString(alError)); + DebugConsole.ThrowError("Failed to set source's position: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; } } else @@ -146,14 +155,16 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to disable source's relative flag: " + debugName + ", " + Al.GetErrorString(alError)); + DebugConsole.ThrowError("Failed to disable source's relative flag: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; } Al.Source3f(alSource, Al.Position, 0.0f, 0.0f, 0.0f); alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to reset source's position: " + debugName + ", " + Al.GetErrorString(alError)); + DebugConsole.ThrowError("Failed to reset source's position: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; } } } @@ -175,7 +186,8 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to set source's reference distance: " + debugName + ", " + Al.GetErrorString(alError)); + DebugConsole.ThrowError("Failed to set source's reference distance: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; } } } @@ -195,7 +207,8 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to set source's max distance: " + debugName + ", " + Al.GetErrorString(alError)); + DebugConsole.ThrowError("Failed to set source's max distance: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; } } } @@ -206,6 +219,8 @@ namespace Barotrauma.Sounds get { return gain; } set { + if (!MathUtils.IsValid(value)) { return; } + gain = Math.Clamp(value, 0.0f, 1.0f); if (ALSourceIndex < 0) { return; } @@ -219,7 +234,8 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to set source's gain: " + debugName + ", " + Al.GetErrorString(alError)); + DebugConsole.ThrowError("Failed to set source's gain: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; } } } @@ -241,12 +257,41 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to set source's looping state: " + debugName + ", " + Al.GetErrorString(alError)); + DebugConsole.ThrowError("Failed to set source's looping state: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; } } } } + public float frequencyMultiplier; + public float FrequencyMultiplier + { + get + { + return frequencyMultiplier; + } + set + { + if (value < 0.25f || value > 4.0f) + { + DebugConsole.ThrowError($"Frequency multiplier out of range: {value}" + Environment.StackTrace); + } + frequencyMultiplier = Math.Clamp(value, 0.25f, 4.0f); + + if (ALSourceIndex < 0) { return; } + + uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); + + Al.Sourcef(alSource, Al.Pitch, frequencyMultiplier); + int alError = Al.GetError(); + if (alError != Al.NoError) + { + throw new Exception("Failed to set source's frequency multiplier: " + debugName + ", " + Al.GetErrorString(alError)); + } + } + } + public bool FilledByNetwork { get; @@ -276,7 +321,8 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to get source's playback position: " + debugName + ", " + Al.GetErrorString(alError)); + DebugConsole.ThrowError("Failed to get source's playback position: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; } Al.SourceStop(alSource); @@ -284,7 +330,8 @@ namespace Barotrauma.Sounds alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to stop source: " + debugName + ", " + Al.GetErrorString(alError)); + DebugConsole.ThrowError("Failed to stop source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; } Al.Sourcei(alSource, Al.Buffer, muffled ? (int)Sound.ALMuffledBuffer : (int)Sound.ALBuffer); @@ -292,21 +339,24 @@ namespace Barotrauma.Sounds alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to bind buffer to source: " + debugName + ", " + Al.GetErrorString(alError)); + DebugConsole.ThrowError("Failed to bind buffer to source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; } Al.SourcePlay(alSource); alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to replay source: " + debugName + ", " + Al.GetErrorString(alError)); + DebugConsole.ThrowError("Failed to replay source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; } Al.Sourcei(alSource, Al.SampleOffset, playbackPos); alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to reset playback position: " + debugName + ", " + Al.GetErrorString(alError)); + DebugConsole.ThrowError("Failed to reset playback position: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return; } } } @@ -329,7 +379,8 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to get source's playback position: " + debugName + ", " + Al.GetErrorString(alError)); + DebugConsole.ThrowError("Failed to get source's playback position: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return 0.0f; } return Sound.GetAmplitudeAtPlaybackPos(playbackPos); } @@ -408,14 +459,15 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to determine playing state from source: " + debugName + ", " + Al.GetErrorString(alError)); + DebugConsole.ThrowError("Failed to determine playing state from source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + return false; } bool playing = state == Al.Playing; return playing; } } - public SoundChannel(Sound sound, float gain, Vector3? position, float near, float far, string category, bool muffle = false) + public SoundChannel(Sound sound, float gain, Vector3? position, float freqMult, float near, float far, string category, bool muffle = false) { Sound = sound; @@ -517,6 +569,7 @@ namespace Barotrauma.Sounds this.Position = position; this.Gain = gain; + this.FrequencyMultiplier = freqMult; this.Looping = false; this.Near = near; this.Far = far; @@ -632,10 +685,10 @@ namespace Barotrauma.Sounds public void UpdateStream() { - if (!IsStream) { throw new Exception("Called UpdateStream on a non-streamed sound channel!"); } - try { + if (!IsStream) { throw new Exception("Called UpdateStream on a non-streamed sound channel!"); } + Monitor.Enter(mutex); if (!reachedEndSample) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 3f6993393..8709bd52d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -630,10 +630,10 @@ namespace Barotrauma { var sound = GetSound(soundTag); if (sound == null) { return null; } - return PlaySound(sound, position, volume ?? sound.BaseGain, range ?? sound.BaseFar, hullGuess); + return PlaySound(sound, position, volume ?? sound.BaseGain, range ?? sound.BaseFar, 1.0f, hullGuess); } - public static SoundChannel PlaySound(Sound sound, Vector2 position, float? volume = null, float? range = null, Hull hullGuess = null) + public static SoundChannel PlaySound(Sound sound, Vector2 position, float? volume = null, float? range = null, float? freqMult = null, Hull hullGuess = null) { if (sound == null) { @@ -649,7 +649,7 @@ namespace Barotrauma return null; } bool muffle = !sound.IgnoreMuffling && ShouldMuffleSound(Character.Controlled, position, far, hullGuess); - return sound.Play(volume ?? sound.BaseGain, far, position, muffle: muffle); + return sound.Play(volume ?? sound.BaseGain, far, freqMult ?? 1.0f, position, muffle: muffle); } private static void UpdateMusic(float deltaTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs index c2dd244fd..11bf78a85 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs @@ -60,7 +60,7 @@ namespace Barotrauma.Sounds throw new InvalidOperationException(); } - public override SoundChannel Play(Vector3? position, float gain, bool muffle = false) + public override SoundChannel Play(Vector3? position, float gain, float freqMult = 1.0f, bool muffle = false) { throw new InvalidOperationException(); } @@ -76,7 +76,7 @@ namespace Barotrauma.Sounds soundChannel = null; } } - chn = new SoundChannel(this, gain, null, 1.0f, 3.0f, "video", false); + chn = new SoundChannel(this, gain, null, 1.0f, 1.0f, 3.0f, "video", false); lock (mutex) { soundChannel = chn; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs index 734925a07..072a38cfe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs @@ -75,7 +75,7 @@ namespace Barotrauma.Sounds soundChannel = null; - SoundChannel chn = new SoundChannel(this, 1.0f, null, 0.4f, 1.0f, "voip", false); + SoundChannel chn = new SoundChannel(this, 1.0f, null, 1.0f, 0.4f, 1.0f, "voip", false); soundChannel = chn; Gain = 1.0f; } @@ -130,7 +130,7 @@ namespace Barotrauma.Sounds throw new InvalidOperationException(); } - public override SoundChannel Play(Vector3? position, float gain, bool muffle = false) + public override SoundChannel Play(Vector3? position, float gain, float freqMult = 1.0f, bool muffle = false) { throw new InvalidOperationException(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs index 64fc96a80..c82285907 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs @@ -36,6 +36,8 @@ namespace Barotrauma public float OffsetAnimSpeed { get; private set; } private float rotationSpeedRadians; + private float absRotationSpeedRadians; + [Serialize(0.0f, true), Editable] public float RotationSpeed { @@ -46,6 +48,7 @@ namespace Barotrauma private set { rotationSpeedRadians = MathHelper.ToRadians(value); + absRotationSpeedRadians = Math.Abs(rotationSpeedRadians); } } @@ -121,43 +124,46 @@ namespace Barotrauma } } - public Vector2 GetOffset(ref float offsetState) + public Vector2 GetOffset(ref float offsetState, float rotation = 0.0f) { - if (OffsetAnimSpeed <= 0.0f) + Vector2 offset = Offset; + if (OffsetAnimSpeed > 0.0f) { - return Offset; - } - switch (OffsetAnim) - { - case AnimationType.Sine: - offsetState %= (MathHelper.TwoPi / OffsetAnimSpeed); - return Offset * (float)Math.Sin(offsetState * OffsetAnimSpeed); - case AnimationType.Noise: - offsetState %= (1.0f / (OffsetAnimSpeed * 0.1f)); + switch (OffsetAnim) + { + case AnimationType.Sine: + offsetState %= (MathHelper.TwoPi / OffsetAnimSpeed); + offset *= (float)Math.Sin(offsetState * OffsetAnimSpeed); + break; + case AnimationType.Noise: + offsetState %= 1.0f / (OffsetAnimSpeed * 0.1f); - float t = offsetState * 0.1f * OffsetAnimSpeed; - return new Vector2( - Offset.X * (PerlinNoise.GetPerlin(t, t) - 0.5f), - Offset.Y * (PerlinNoise.GetPerlin(t + 0.5f, t + 0.5f) - 0.5f)); - default: - return Offset; + float t = offsetState * 0.1f * OffsetAnimSpeed; + offset = new Vector2( + offset.X * (PerlinNoise.GetPerlin(t, t) - 0.5f), + offset.Y * (PerlinNoise.GetPerlin(t + 0.5f, t + 0.5f) - 0.5f)); + break; + } } + if (Math.Abs(rotation) > 0.01f) + { + Matrix transform = Matrix.CreateRotationZ(rotation); + offset = Vector2.Transform(offset, transform); + } + return offset; } public float GetRotation(ref float rotationState) { - if (rotationSpeedRadians <= 0.0f) - { - return rotationRadians; - } + RotationSpeed = -Math.Abs(RotationSpeed); switch (RotationAnim) { case AnimationType.Sine: - rotationState = rotationState % (MathHelper.TwoPi / rotationSpeedRadians); + rotationState %= MathHelper.TwoPi / absRotationSpeedRadians; return rotationRadians * (float)Math.Sin(rotationState * rotationSpeedRadians); case AnimationType.Noise: - rotationState = rotationState % (1.0f / rotationSpeedRadians); - return rotationRadians * (PerlinNoise.GetPerlin(rotationState * rotationSpeedRadians, rotationState * rotationSpeedRadians) - 0.5f); + rotationState %= 1.0f / absRotationSpeedRadians; + return rotationRadians * (PerlinNoise.GetPerlin(rotationState * absRotationSpeedRadians, rotationState * absRotationSpeedRadians) - 0.5f); default: return rotationState * rotationSpeedRadians; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index eefd8a241..01524a0ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -4,6 +4,7 @@ using System; using Barotrauma.IO; using System.Linq; using System.Collections.Generic; +using System.Threading.Tasks; namespace Barotrauma { @@ -11,6 +12,8 @@ namespace Barotrauma { private bool cannotBeLoaded; + protected volatile bool loadingAsync = false; + protected Texture2D texture; public Texture2D Texture { @@ -64,9 +67,17 @@ namespace Barotrauma if (sourceVector.W == 0.0f) sourceVector.W = texture.Height; } - public void EnsureLazyLoaded() + public async Task LazyLoadAsync() { - if (!LazyLoad || texture != null || cannotBeLoaded) { return; } + await Task.Yield(); + if (!LazyLoad || texture != null || cannotBeLoaded || loadingAsync) { return; } + EnsureLazyLoaded(isAsync: true); + } + + public void EnsureLazyLoaded(bool isAsync=false) + { + if (!LazyLoad || texture != null || cannotBeLoaded || loadingAsync) { return; } + loadingAsync = isAsync; Vector4 sourceVector = Vector4.Zero; bool temp2 = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index 0e3b95f8d..5acb51127 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -6,6 +6,7 @@ using Barotrauma.Sounds; using Microsoft.Xna.Framework; using System.Xml.Linq; using Barotrauma.Items.Components; +using System.Linq; namespace Barotrauma { @@ -52,11 +53,11 @@ namespace Barotrauma } } - partial void ApplyProjSpecific(float deltaTime, Entity entity, IEnumerable targets, Hull hull, Vector2 worldPosition) + partial void ApplyProjSpecific(float deltaTime, Entity entity, IEnumerable targets, Hull hull, Vector2 worldPosition, bool playSound) { if (entity == null) { return; } - if (sounds.Count > 0) + if (sounds.Count > 0 && playSound) { if (soundChannel == null || !soundChannel.IsPlaying) { @@ -70,7 +71,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull1" + Environment.StackTrace, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; } - soundChannel = SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, hull); + soundChannel = SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, hullGuess: hull); if (soundChannel != null) { soundChannel.Looping = loopSound; } } } @@ -96,7 +97,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull2" + Environment.StackTrace, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; } - soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hull); + soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: hull); if (soundChannel != null) { soundChannel.Looping = loopSound; } } } @@ -115,12 +116,27 @@ namespace Barotrauma float particleRotation = 0.0f; if (emitter.Prefab.CopyEntityAngle) { + Limb targetLimb = null; if (entity is Item item && item.body != null) { angle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); particleRotation = -item.body.Rotation; if (item.body.Dir < 0.0f) { particleRotation += MathHelper.Pi; } } + else if (entity is Character c && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) + { + targetLimb = c.AnimController.GetLimb(l); + } + else + { + targetLimb = targets.FirstOrDefault(t => t is Limb) as Limb; + } + if (targetLimb != null && !targetLimb.Removed) + { + angle = targetLimb.body.Rotation + ((targetLimb.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); + particleRotation = -targetLimb.body.Rotation; + if (targetLimb.body.Dir < 0.0f) { particleRotation += MathHelper.Pi; } + } } emitter.Emit(deltaTime, worldPosition, hull, angle: angle, particleRotation: particleRotation); @@ -137,7 +153,7 @@ namespace Barotrauma //stop looping sounds if the statuseffect hasn't been applied in 0.1 //= keeping the sound looping requires continuously applying the statuseffect - if (Timing.TotalTime > statusEffect.loopStartTime + 0.1) + if (Timing.TotalTime > statusEffect.loopStartTime + 0.1 && !DurationList.Any(e => e.Parent == statusEffect)) { statusEffect.soundChannel.FadeOutAndDispose(); statusEffect.soundChannel = null; diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index fed3f8f6c..28e99939f 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.10.4.0 + 0.10.5.1 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 0e9f2494e..a0cd81d75 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.10.4.0 + 0.10.5.1 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 7d5e7d711..6a8473788 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.10.4.0 + 0.10.5.1 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/soft_oal_x64.dll b/Barotrauma/BarotraumaClient/soft_oal_x64.dll index 3963d4d89..7ca46d8c3 100644 Binary files a/Barotrauma/BarotraumaClient/soft_oal_x64.dll and b/Barotrauma/BarotraumaClient/soft_oal_x64.dll differ diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index d63550a66..61f3d3ba2 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.10.4.0 + 0.10.5.1 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 71a7d3304..bdbea03ac 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.10.4.0 + 0.10.5.1 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index fed973a06..acf0f00de 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -30,6 +30,14 @@ namespace Barotrauma healthUpdateTimer = 0.0f; + if (CauseOfDeath.Killer != null && CauseOfDeath.Killer.IsTraitor && CauseOfDeath.Killer != this) + { + var owner = GameMain.Server.ConnectedClients.Find(c => c.Character == this); + if (owner != null) + { + GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("KilledByTraitorNotification"), owner, ChatMessageType.ServerMessageBoxInGame); + } + } foreach (Client client in GameMain.Server.ConnectedClients) { if (client.InGame) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index fdf9fe437..a0595994a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -17,6 +17,8 @@ namespace Barotrauma private double LastInputTime; + public bool HealthUpdatePending; + public float GetPositionUpdateInterval(Client recipient) { if (!Enabled) { return 1000.0f; } @@ -418,6 +420,7 @@ namespace Barotrauma if (writeStatus) { WriteStatus(tempBuffer); + HealthUpdatePending = false; } tempBuffer.WritePadBits(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index e9eef7e53..839cab056 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1443,7 +1443,7 @@ namespace Barotrauma if (GameMain.Server == null) return null; return new string[][] { - GameMain.Server.ServerSettings.BanList.BannedIPs.Where(ip => !string.IsNullOrEmpty(ip)).ToArray() + GameMain.Server.ServerSettings.BanList.BannedEndPoints.ToArray() }; })); @@ -1563,15 +1563,45 @@ namespace Barotrauma } ); + AssignOnClientRequestExecute("togglecampaignteleport", + (Client client, Vector2 cursorWorldPos, string[] args) => + { + if (!(GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)) + { + GameMain.Server.SendConsoleMessage("No campaign active.", client); + return; + } + mpCampaign.LastUpdateID++; + GameMain.GameSession.Map.AllowDebugTeleport = !GameMain.GameSession.Map.AllowDebugTeleport; + NewMessage(client.Name + (GameMain.GameSession.Map.AllowDebugTeleport ? " enabled" : " disabled") + " teleportation on the campaign map.", Color.White); + GameMain.Server.SendConsoleMessage((GameMain.GameSession.Map.AllowDebugTeleport ? "Enabled" : "Disabled") + " teleportation on the campaign map.", client); + } + ); + AssignOnClientRequestExecute( "godmode", (Client client, Vector2 cursorWorldPos, string[] args) => + { + Character targetCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args, false); + + if (targetCharacter == null) { return; } + + targetCharacter.GodMode = !targetCharacter.GodMode; + + NewMessage(targetCharacter.Name + (targetCharacter.GodMode ? "'s godmode turned on by \"" : "'s godmode turned off by \"") + client.Name + "\"", Color.White); + GameMain.Server.SendConsoleMessage(targetCharacter.Name + (targetCharacter.GodMode ? "'s godmode on" : "'s godmode off"), client); + } + ); + + AssignOnClientRequestExecute( + "godmode_mainsub", + (Client client, Vector2 cursorWorldPos, string[] args) => { if (Submarine.MainSub == null) return; Submarine.MainSub.GodMode = !Submarine.MainSub.GodMode; - NewMessage((Submarine.MainSub.GodMode ? "Godmode turned on by \"" : "Godmode off by \"") + client.Name + "\"", Color.White); - GameMain.Server.SendConsoleMessage(Submarine.MainSub.GodMode ? "Godmode on" : "Godmode off", client); + NewMessage((Submarine.MainSub.GodMode ? "Mainsub godmode turned on by \"" : "Mainsub godmode turned off by \"") + client.Name + "\"", Color.White); + GameMain.Server.SendConsoleMessage(Submarine.MainSub.GodMode ? "Mainsub godmode on" : "Mainsub godmode off", client); } ); @@ -1581,7 +1611,9 @@ namespace Barotrauma { if (args.Length < 2) return; - AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => + a.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || + a.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab == null) { GameMain.Server.SendConsoleMessage("Affliction \"" + args[0] + "\" not found.", client); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index bc8fe1b30..bb888137a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -43,16 +43,13 @@ namespace Barotrauma //but they're checked for all over the place //TODO: maybe clean up instead of having these constants public static readonly Screen SubEditorScreen = UnimplementedScreen.Instance; - + + public static DecalManager DecalManager; + public static bool ShouldRun = true; private static Stopwatch stopwatch; - public static IEnumerable SelectedPackages - { - get { return Config?.SelectedContentPackages; } - } - private static ContentPackage vanillaContent; public static ContentPackage VanillaContent { @@ -61,7 +58,7 @@ namespace Barotrauma if (vanillaContent == null) { // TODO: Dynamic method for defining and finding the vanilla content package. - vanillaContent = ContentPackage.List.SingleOrDefault(cp => Path.GetFileName(cp.Path).Equals("vanilla 0.9.xml", StringComparison.OrdinalIgnoreCase)); + vanillaContent = ContentPackage.CorePackages.SingleOrDefault(cp => Path.GetFileName(cp.Path).Equals("vanilla 0.9.xml", StringComparison.OrdinalIgnoreCase)); } return vanillaContent; } @@ -110,10 +107,10 @@ namespace Barotrauma EventSet.LoadPrefabs(); Order.Init(); EventManagerSettings.Init(); + ItemPrefab.LoadAll(GetFilesOfType(ContentType.Item)); AfflictionPrefab.LoadAll(GetFilesOfType(ContentType.Afflictions)); SkillSettings.Load(GetFilesOfType(ContentType.SkillSettings)); StructurePrefab.LoadAll(GetFilesOfType(ContentType.Structure)); - ItemPrefab.LoadAll(GetFilesOfType(ContentType.Item)); UpgradePrefab.LoadAll(GetFilesOfType(ContentType.UpgradeModules)); JobPrefab.LoadAll(GetFilesOfType(ContentType.Jobs)); CorpsePrefab.LoadAll(GetFilesOfType(ContentType.Corpses)); @@ -122,6 +119,7 @@ namespace Barotrauma LevelObjectPrefab.LoadAll(); GameModePreset.Init(); + DecalManager = new DecalManager(); LocationType.Init(); SubmarineInfo.RefreshSavedSubs(); @@ -136,8 +134,8 @@ namespace Barotrauma private void CheckContentPackage() { - - foreach (ContentPackage contentPackage in Config.SelectedContentPackages) + //TODO: reimplement using only core package? + /*foreach (ContentPackage contentPackage in Config.AllEnabledPackages) { var exePaths = contentPackage.GetFilesOfType(ContentType.ServerExecutable); if (exePaths.Count() > 0 && AppDomain.CurrentDomain.FriendlyName != exePaths.First()) @@ -155,7 +153,7 @@ namespace Barotrauma }); break; } - } + }*/ } /// @@ -167,11 +165,11 @@ namespace Barotrauma { if (searchAllContentPackages) { - return ContentPackage.GetFilesOfType(ContentPackage.List, type); + return ContentPackage.GetFilesOfType(ContentPackage.AllPackages, type); } else { - return ContentPackage.GetFilesOfType(SelectedPackages, type); + return ContentPackage.GetFilesOfType(Config.AllEnabledPackages, type); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs index 7b4ba88ac..dc610006b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs @@ -9,7 +9,7 @@ namespace Barotrauma foreach (PurchasedItem item in itemsToSell) { var itemValue = GetBuyValueAtCurrentLocation(item); - campaign.Map.CurrentLocation.StoreCurrentBalance -= itemValue; + Location.StoreCurrentBalance -= itemValue; campaign.Money += itemValue; PurchasedItems.Remove(item); } @@ -20,8 +20,8 @@ namespace Barotrauma foreach (SoldItem item in itemsToBuy) { var itemValue = GetSellValueAtCurrentLocation(item.ItemPrefab); - if (location.StoreCurrentBalance < itemValue || item.Removed) { continue; } - location.StoreCurrentBalance += itemValue; + if (Location.StoreCurrentBalance < itemValue || item.Removed) { continue; } + Location.StoreCurrentBalance += itemValue; campaign.Money -= itemValue; SoldItems.Remove(item); } @@ -35,7 +35,7 @@ namespace Barotrauma var itemValue = GetSellValueAtCurrentLocation(item.ItemPrefab); // check if the store can afford the item and if the item hasn't been removed already - if (location.StoreCurrentBalance < itemValue || item.Removed) { continue; } + if (Location.StoreCurrentBalance < itemValue || item.Removed) { continue; } if (!item.Removed && canAddToRemoveQueue && Entity.FindEntityByID(item.ID) is Item entity) { @@ -43,7 +43,7 @@ namespace Barotrauma Entity.Spawner.AddToRemoveQueue(entity); } SoldItems.Add(item); - location.StoreCurrentBalance -= itemValue; + Location.StoreCurrentBalance -= itemValue; campaign.Money += itemValue; } OnSoldItemsChanged?.Invoke(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs index ba96d64ba..4e8fee3f3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs @@ -32,7 +32,10 @@ namespace Barotrauma XElement saveElement = new XElement("bots", new XAttribute("hasbots", HasBots)); foreach (CharacterInfo info in characterInfos) { - if (info?.Character == null || info.Character.IsDead) { continue; } + if (Level.Loaded != null) + { + if (!info.IsNewHire && (info.Character == null || info.Character.IsDead)) { continue; } + } XElement characterElement = info.Save(saveElement); if (info.InventoryData != null) { characterElement.Add(info.InventoryData); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 6c08a327c..f619c4b03 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -161,7 +161,6 @@ namespace Barotrauma break; case TransitionType.ProgressToNextLocation: Map.MoveToNextLocation(); - Map.ProgressWorld(); break; case TransitionType.End: EndCampaign(); @@ -169,6 +168,8 @@ namespace Barotrauma break; } + Map.ProgressWorld(transitionType, (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime)); + bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); GameMain.GameSession.EndRound("", traitorResults, transitionType); @@ -196,7 +197,7 @@ namespace Barotrauma { if (data.HasSpawned && !characterData.Any(cd => cd.IsDuplicate(data))) { - var character = Character.CharacterList.Find(c => c.Info == data.CharacterInfo); + var character = Character.CharacterList.Find(c => c.Info == data.CharacterInfo && !c.IsHusk); if (character != null && (!character.IsDead || character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected)) { data.Refresh(character); @@ -243,10 +244,28 @@ namespace Barotrauma } NextLevel = newLevel; GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + + if (PendingSubmarineSwitch != null) + { + SubmarineInfo previousSub = GameMain.GameSession.SubmarineInfo; + GameMain.GameSession.SubmarineInfo = PendingSubmarineSwitch; + PendingSubmarineSwitch = null; + + for (int i = 0; i < GameMain.GameSession.OwnedSubmarines.Count; i++) + { + if (GameMain.GameSession.OwnedSubmarines[i].Name == previousSub.Name) + { + GameMain.GameSession.OwnedSubmarines[i] = previousSub; + break; + } + } + } + SaveUtil.SaveGame(GameMain.GameSession.SavePath); } else { + PendingSubmarineSwitch = null; GameMain.Server.EndGame(TransitionType.None); LoadCampaign(GameMain.GameSession.SavePath); LastSaveID++; @@ -262,23 +281,6 @@ namespace Barotrauma NextLevel = newLevel; MirrorLevel = mirror; - if (PendingSubmarineSwitch != null) - { - SubmarineInfo previousSub = GameMain.GameSession.SubmarineInfo; - GameMain.GameSession.SubmarineInfo = PendingSubmarineSwitch; - PendingSubmarineSwitch = null; - - for (int i = 0; i < GameMain.GameSession.OwnedSubmarines.Count; i++) - { - if (GameMain.GameSession.OwnedSubmarines[i].Name == previousSub.Name) - { - GameMain.GameSession.OwnedSubmarines[i] = previousSub; - } - } - - SaveUtil.SaveGame(GameMain.GameSession.SavePath); - LastSaveID++; - } //give clients time to play the end cinematic before starting the next round if (transitionType == TransitionType.End) @@ -305,6 +307,7 @@ namespace Barotrauma UpgradeManager.OnUpgradesChanged += () => { LastUpdateID++; }; Map.OnLocationSelected += (loc, connection) => { LastUpdateID++; }; Map.OnMissionSelected += (loc, mission) => { LastUpdateID++; }; + Reputation.OnAnyReputationValueChanged += () => { LastUpdateID++; }; } //increment save ID so clients know they're lacking the most up-to-date save file LastSaveID++; @@ -363,13 +366,16 @@ namespace Barotrauma { LoadNewLevel(); } - else if (transitionType == TransitionType.ProgressToNextLocation && Level.Loaded.EndOutpost != null && Level.Loaded.EndOutpost.DockedTo.Contains(leavingSub)) + else if (GameMain.Server.ConnectedClients.Count == 0 || GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead)) { - LoadNewLevel(); - } - else if (transitionType == TransitionType.ReturnToPreviousLocation && Level.Loaded.StartOutpost != null && Level.Loaded.StartOutpost.DockedTo.Contains(leavingSub)) - { - LoadNewLevel(); + if (transitionType == TransitionType.ProgressToNextLocation && Level.Loaded.EndOutpost != null && Level.Loaded.EndOutpost.DockedTo.Contains(leavingSub)) + { + LoadNewLevel(); + } + else if (transitionType == TransitionType.ReturnToPreviousLocation && Level.Loaded.StartOutpost != null && Level.Loaded.StartOutpost.DockedTo.Contains(leavingSub)) + { + LoadNewLevel(); + } } } else if (Level.Loaded.Type == LevelData.LevelType.Outpost) @@ -399,6 +405,7 @@ namespace Barotrauma msg.Write(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex); msg.Write(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex); msg.Write(map.SelectedMissionIndex == -1 ? byte.MaxValue : (byte)map.SelectedMissionIndex); + msg.Write(map.AllowDebugTeleport); msg.Write(reputation != null); if (reputation != null) { msg.Write(reputation.Value); } @@ -592,12 +599,10 @@ namespace Barotrauma } } -#if DEBUG - if (currentLocIndex < Map.Locations.Count) + if (currentLocIndex < Map.Locations.Count && Map.AllowDebugTeleport) { Map.SetLocation(currentLocIndex); } -#endif Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex); if (Map.SelectedLocation == null) { Map.SelectRandomLocation(preferUndiscovered: true); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs new file mode 100644 index 000000000..72d130ff1 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs @@ -0,0 +1,22 @@ +using Barotrauma.Networking; +using System; + +namespace Barotrauma.Items.Components +{ + partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable + { + + private UInt16 originalDockingTargetID; + + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + msg.Write(docked); + + if (docked) + { + msg.Write(originalDockingTargetID); + msg.Write(hulls != null && hulls[0] != null && hulls[1] != null && gap != null); + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs index 7072e5b2b..0b988a0dc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs @@ -30,6 +30,7 @@ namespace Barotrauma.Items.Components msg.Write(isOpen); msg.Write(isBroken); msg.Write(extraData.Length == 3 ? (bool)extraData[2] : false); //forced open + msg.Write(isStuck); msg.WriteRangedSingle(stuck, 0.0f, 100.0f, 8); msg.Write(lastUser == null ? (UInt16)0 : lastUser.ID); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs new file mode 100644 index 000000000..16fe7f125 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs @@ -0,0 +1,52 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.Networking; + +namespace Barotrauma.Items.Components +{ + internal partial class Growable + { + partial void LoadVines(XElement element) + { + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "flowersprite": + flowerVariants++; + break; + case "leafsprite": + leafVariants++; + break; + } + } + } + + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + msg.WriteRangedSingle(Health, 0f, (float) MaxHealth, 8); + if (extraData != null && extraData.Length >= 3 && extraData[2] is int offset) + { + int amountToSend = Math.Min(Vines.Count - offset, VineChunkSize); + msg.WriteRangedInteger(offset, -1, MaximumVines); + msg.WriteRangedInteger(amountToSend, 0, VineChunkSize); + for (int i = offset; i < offset + amountToSend; i++) + { + VineTile vine = Vines[i]; + var (x, y) = vine.Position; + msg.WriteRangedInteger((byte) vine.Type, 0b0000, 0b1111); + msg.WriteRangedInteger(vine.FlowerConfig.Serialize(), 0, 0xFFF); + msg.WriteRangedInteger(vine.LeafConfig.Serialize(), 0, 0xFFF); + msg.Write((byte) (x / VineTile.Size)); + msg.Write((byte) (y / VineTile.Size)); + } + } + else + { + msg.WriteRangedInteger(-1, -1, MaximumVines); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs index 90d4be060..b0a85019a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs @@ -31,6 +31,8 @@ namespace Barotrauma.Items.Components AttachToWall(); item.CreateServerEvent(this); + c.Character.Inventory?.CreateNetworkEvent(); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " attached " + item.Name + " to a wall", ServerLog.MessageType.ItemInteraction); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs index 6ce09e03e..7235d11c7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs @@ -17,6 +17,8 @@ namespace Barotrauma.Items.Components } return true; //element processed } + + public virtual void ServerAppendExtraData(ref object[] extraData) { } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs index 732d58cba..c92401bec 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs @@ -1,10 +1,16 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System.Collections.Generic; using System.Xml.Linq; namespace Barotrauma.Items.Components { partial class ItemLabel : ItemComponent, IDrawableComponent { + private CoroutineHandle sendStateCoroutine; + private string lastSentText; + private float sendStateTimer; + [Serialize("", true, description: "The text to display on the label.", alwaysUseInstanceValues: true), Editable(100)] public string Text { @@ -35,5 +41,38 @@ namespace Barotrauma.Items.Components : base(item, element) { } + + partial void OnStateChanged() + { + sendStateTimer = 0.1f; + if (sendStateCoroutine == null) + { + sendStateCoroutine = CoroutineManager.StartCoroutine(SendStateAfterDelay()); + } + } + + private IEnumerable SendStateAfterDelay() + { + while (sendStateTimer > 0.0f) + { + sendStateTimer -= CoroutineManager.DeltaTime; + yield return CoroutineStatus.Running; + } + + if (item.Removed || GameMain.NetworkMember == null) + { + yield return CoroutineStatus.Success; + } + + sendStateCoroutine = null; + if (lastSentText != Text) { item.CreateServerEvent(this); } + yield return CoroutineStatus.Success; + } + + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + msg.Write(Text); + lastSentText = Text; + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs index d39ad733f..3be3b0dc5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs @@ -9,6 +9,7 @@ namespace Barotrauma.Items.Components { //force can only be adjusted at 10% intervals -> no need for more accuracy than this msg.WriteRangedInteger((int)(targetForce / 10.0f), -10, 10); + msg.Write(User == null ? Entity.NullEntityID : User.ID); } public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) @@ -23,6 +24,7 @@ namespace Barotrauma.Items.Components } targetForce = newTargetForce; + User = c.Character; } //notify all clients of the changed state diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs index 5c915dec8..335137727 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs @@ -31,9 +31,22 @@ namespace Barotrauma.Items.Components } } + private ulong serverEventId = 0; + public override void ServerAppendExtraData(ref object[] extraData) + { + //ensuring the uniqueness of this event is + //required for the fabricator to sync correctly; + //otherwise, the event manager would incorrectly + //assume that the client actually has the latest state + Array.Resize(ref extraData, 4); + extraData[2] = serverEventId; + extraData[3] = State; + } + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { - msg.Write((byte)State); + FabricatorState stateAtEvent = (FabricatorState)extraData[3]; + msg.Write((byte)stateAtEvent); msg.Write(timeUntilReady); int itemIndex = fabricatedItem == null ? -1 : fabricationRecipes.IndexOf(fabricatedItem); msg.WriteRangedInteger(itemIndex, -1, fabricationRecipes.Count - 1); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs index 903b05330..1f3c4530d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs @@ -23,6 +23,10 @@ namespace Barotrauma.Items.Components { GameServer.Log(GameServer.CharacterLogName(c.Character) + (newIsActive ? " turned on " : " turned off ") + item.Name, ServerLog.MessageType.ItemInteraction); } + if (pumpSpeedLockTimer <= 0.0f) + { + targetLevel = null; + } FlowPercentage = newFlowPercentage; IsActive = newIsActive; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs index 7ff0d9ea4..c57ba42cf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs @@ -22,13 +22,12 @@ namespace Barotrauma.Items.Components bool autoPilot = msg.ReadBoolean(); bool dockingButtonClicked = msg.ReadBoolean(); Vector2 newSteeringInput = targetVelocity; - bool maintainPos = false; Vector2? newPosToMaintain = null; bool headingToStart = false; if (autoPilot) { - maintainPos = msg.ReadBoolean(); + bool maintainPos = msg.ReadBoolean(); if (maintainPos) { newPosToMaintain = new Vector2( diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/MemoryComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/MemoryComponent.cs new file mode 100644 index 000000000..39297e684 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/MemoryComponent.cs @@ -0,0 +1,45 @@ +using Barotrauma.Networking; +using System.Collections.Generic; + +namespace Barotrauma.Items.Components +{ + partial class MemoryComponent : ItemComponent + { + private CoroutineHandle sendStateCoroutine; + private string lastSentValue; + private float sendStateTimer; + + partial void OnStateChanged() + { + sendStateTimer = 0.5f; + if (sendStateCoroutine == null) + { + sendStateCoroutine = CoroutineManager.StartCoroutine(SendStateAfterDelay()); + } + } + + private IEnumerable SendStateAfterDelay() + { + while (sendStateTimer > 0.0f) + { + sendStateTimer -= CoroutineManager.DeltaTime; + yield return CoroutineStatus.Running; + } + + if (item.Removed || GameMain.NetworkMember == null) + { + yield return CoroutineStatus.Success; + } + + sendStateCoroutine = null; + if (lastSentValue != Value) { item.CreateServerEvent(this); } + yield return CoroutineStatus.Success; + } + + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + msg.Write(Value); + lastSentValue = Value; + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 254810775..6fce00726 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -139,7 +139,6 @@ namespace Barotrauma } break; case NetEntityEvent.Type.Upgrade: - { if (extraData.Length > 0 && extraData[1] is Upgrade upgrade) { var upgradeTargets = upgrade.TargetComponents; @@ -163,7 +162,6 @@ namespace Barotrauma : $"Failed to write a network event for the item \"{Name}\". No upgrade specified."; } break; - } default: errorMsg = "Failed to write a network event for the item \"" + Name + "\" - \"" + eventType + "\" is not a valid entity event type for items."; break; @@ -382,7 +380,10 @@ namespace Barotrauma int index = components.IndexOf(ic); if (index == -1) return; - GameMain.Server.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.ComponentState, index }); + object[] extraData = new object[] { NetEntityEvent.Type.ComponentState, index }; + ic.ServerAppendExtraData(ref extraData); + + GameMain.Server.CreateEntityEvent(this, extraData); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index 6300da769..d8fe84369 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Items.Components; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -11,6 +12,8 @@ namespace Barotrauma private float lastSentVolume, lastSentOxygen, lastSentFireCount; private float sendUpdateTimer; + private bool decalsChanged; + public override bool IsMouseOn(Vector2 position) { return false; @@ -29,18 +32,40 @@ namespace Barotrauma return; } + if (decalsChanged) + { + GameMain.NetworkMember.CreateEntityEvent(this, new object[] { false }); + lastSentVolume = waterVolume; + lastSentOxygen = OxygenPercentage; + lastSentFireCount = FireSources.Count; + sendUpdateTimer = NetConfig.HullUpdateInterval; + decalsChanged = false; + return; + } + sendUpdateTimer -= deltaTime; //update client hulls if the amount of water has changed by >10% //or if oxygen percentage has changed by 5% - if (Math.Abs(lastSentVolume - waterVolume) > Volume * 0.1f || - Math.Abs(lastSentOxygen - OxygenPercentage) > 5f || - lastSentFireCount != FireSources.Count || - FireSources.Count > 0 || + if (Math.Abs(lastSentVolume - waterVolume) > Volume * 0.1f || Math.Abs(lastSentOxygen - OxygenPercentage) > 5f || + lastSentFireCount != FireSources.Count || FireSources.Count > 0 || + pendingSectionUpdates.Count > 0 || sendUpdateTimer < -NetConfig.SparseHullUpdateInterval) { if (sendUpdateTimer < 0.0f) { - GameMain.NetworkMember.CreateEntityEvent(this); + if (pendingSectionUpdates.Count > 0) + { + foreach (int pendingSectionUpdate in pendingSectionUpdates) + { + GameMain.NetworkMember.CreateEntityEvent(this, new object[] { true, pendingSectionUpdate } ); + } + pendingSectionUpdates.Clear(); + } + else + { + GameMain.NetworkMember.CreateEntityEvent(this); + } + lastSentVolume = waterVolume; lastSentOxygen = OxygenPercentage; lastSentFireCount = FireSources.Count; @@ -70,65 +95,122 @@ namespace Barotrauma message.WriteRangedSingle(MathHelper.Clamp(fireSource.Size.X / rect.Width, 0.0f, 1.0f), 0, 1.0f, 8); } } + + message.Write(extraData != null); + if (extraData != null) + { + message.Write((bool)extraData[0]); + + // Section update + if ((bool)extraData[0]) + { + int sectorToUpdate = (int)extraData[1]; + int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent; + int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1); + message.WriteRangedInteger(sectorToUpdate, 0, BackgroundSections.Count - 1); + for (int i = start; i < end; i++) + { + message.WriteRangedSingle(BackgroundSections[i].ColorStrength, 0.0f, 1.0f, 8); + message.Write(BackgroundSections[i].Color.PackedValue); + } + } + else // Decal update + { + message.WriteRangedInteger(decals.Count, 0, MaxDecalsPerHull); + foreach (Decal decal in decals) + { + message.Write(decal.Prefab.UIntIdentifier); + float normalizedXPos = MathHelper.Clamp(MathUtils.InverseLerp(rect.X, rect.Right, decal.Position.X), 0.0f, 1.0f); + float normalizedYPos = MathHelper.Clamp(MathUtils.InverseLerp(rect.Y - rect.Height, rect.Y, decal.Position.Y), 0.0f, 1.0f); + message.WriteRangedSingle(normalizedXPos, 0.0f, 1.0f, 8); + message.WriteRangedSingle(normalizedYPos, 0.0f, 1.0f, 8); + message.WriteRangedSingle(decal.Scale, 0f, 2f, 12); + } + } + } } - //used when clients use the water/fire console commands + //used when clients use the water/fire console commands or section / decal updates are received public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { - float newWaterVolume = msg.ReadRangedSingle(0.0f, 1.5f, 8) * Volume; - - bool hasFireSources = msg.ReadBoolean(); - int fireSourceCount = 0; - List newFireSources = new List(); - if (hasFireSources) + bool hasExtraData = msg.ReadBoolean(); + if (hasExtraData) { - fireSourceCount = msg.ReadRangedInteger(0, 16); + int sectorToUpdate = msg.ReadRangedInteger(0, BackgroundSections.Count - 1); + int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent; + int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1); + for (int i = start; i < end; i++) + { + float colorStrength = msg.ReadRangedSingle(0.0f, 1.0f, 8); + Color color = new Color(msg.ReadUInt32()); + + //TODO: verify the client is close enough to this hull to paint it, that the sprayer is functional and that the color matches + if (c.Character != null && c.Character.AllowInput && c.Character.SelectedItems.Any(it => it?.GetComponent() != null)) + { + BackgroundSections[i].SetColorStrength(colorStrength); + BackgroundSections[i].SetColor(color); + } + } + //add to pending updates to notify other clients as well + pendingSectionUpdates.Add(sectorToUpdate); + } + else + { + float newWaterVolume = msg.ReadRangedSingle(0.0f, 1.5f, 8) * Volume; + + bool hasFireSources = msg.ReadBoolean(); + int fireSourceCount = 0; + List newFireSources = new List(); + if (hasFireSources) + { + fireSourceCount = msg.ReadRangedInteger(0, 16); + for (int i = 0; i < fireSourceCount; i++) + { + newFireSources.Add(new Vector3( + MathHelper.Clamp(msg.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f), + MathHelper.Clamp(msg.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f), + msg.ReadRangedSingle(0.0f, 1.0f, 8))); + } + } + + if (!c.HasPermission(ClientPermissions.ConsoleCommands) || + !c.PermittedConsoleCommands.Any(command => command.names.Contains("fire") || command.names.Contains("editfire"))) + { + return; + } + + WaterVolume = newWaterVolume; + for (int i = 0; i < fireSourceCount; i++) { - newFireSources.Add(new Vector3( - MathHelper.Clamp(msg.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f), - MathHelper.Clamp(msg.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f), - msg.ReadRangedSingle(0.0f, 1.0f, 8))); + Vector2 pos = new Vector2( + rect.X + rect.Width * newFireSources[i].X, + rect.Y - rect.Height + (rect.Height * newFireSources[i].Y)); + float size = newFireSources[i].Z * rect.Width; + + var newFire = i < FireSources.Count ? + FireSources[i] : + new FireSource(Submarine == null ? pos : pos + Submarine.Position, null, true); + newFire.Position = pos; + newFire.Size = new Vector2(size, newFire.Size.Y); + + //ignore if the fire wasn't added to this room (invalid position)? + if (!FireSources.Contains(newFire)) + { + newFire.Remove(); + continue; + } } - } - if (!c.HasPermission(ClientPermissions.ConsoleCommands) || - !c.PermittedConsoleCommands.Any(command => command.names.Contains("fire") || command.names.Contains("editfire"))) - { - return; - } - - WaterVolume = newWaterVolume; - - for (int i = 0; i < fireSourceCount; i++) - { - Vector2 pos = new Vector2( - rect.X + rect.Width * newFireSources[i].X, - rect.Y - rect.Height + (rect.Height * newFireSources[i].Y)); - float size = newFireSources[i].Z * rect.Width; - - var newFire = i < FireSources.Count ? - FireSources[i] : - new FireSource(Submarine == null ? pos : pos + Submarine.Position, null, true); - newFire.Position = pos; - newFire.Size = new Vector2(size, newFire.Size.Y); - - //ignore if the fire wasn't added to this room (invalid position)? - if (!FireSources.Contains(newFire)) + for (int i = FireSources.Count - 1; i >= fireSourceCount; i--) { - newFire.Remove(); - continue; + FireSources[i].Remove(); + if (i < FireSources.Count) + { + FireSources.RemoveAt(i); + } } - } - - for (int i = FireSources.Count - 1; i >= fireSourceCount; i--) - { - FireSources[i].Remove(); - if (i < FireSources.Count) - { - FireSources.RemoveAt(i); - } - } + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index e02c6ba3f..008944852 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Barotrauma.IO; using System.Linq; using System.Net; +using Barotrauma.Steam; namespace Barotrauma.Networking { @@ -10,15 +11,16 @@ namespace Barotrauma.Networking { private static UInt16 LastIdentifier = 0; - public BannedPlayer(string name, string ip, string reason, DateTime? expirationTime) + public BannedPlayer(string name, string endPoint, string reason, DateTime? expirationTime) { this.Name = name; - this.IP = ip; + this.EndPoint = endPoint; + ParseEndPointAsSteamId(); this.Reason = reason; this.ExpirationTime = expirationTime; this.UniqueIdentifier = LastIdentifier; LastIdentifier++; - this.IsRangeBan = IP.IndexOf(".x") > -1; + this.IsRangeBan = EndPoint.IndexOf(".x") > -1; } public BannedPlayer(string name, ulong steamID, string reason, DateTime? expirationTime) @@ -31,27 +33,27 @@ namespace Barotrauma.Networking this.IsRangeBan = false; - this.IP = ""; + this.EndPoint = ""; } - public bool CompareTo(string ipCompare) + public bool CompareTo(string endpointCompare) { - if (string.IsNullOrEmpty(IP) || string.IsNullOrEmpty(IP)) { return false; } + if (string.IsNullOrEmpty(EndPoint) || string.IsNullOrEmpty(EndPoint)) { return false; } if (!IsRangeBan) { - return ipCompare == IP; + return endpointCompare == EndPoint; } else { - int rangeBanIndex = IP.IndexOf(".x"); - if (ipCompare.Length < rangeBanIndex) return false; - return ipCompare.Substring(0, rangeBanIndex) == IP.Substring(0, rangeBanIndex); + int rangeBanIndex = EndPoint.IndexOf(".x"); + if (endpointCompare.Length < rangeBanIndex) return false; + return endpointCompare.Substring(0, rangeBanIndex) == EndPoint.Substring(0, rangeBanIndex); } } public bool CompareTo(IPAddress ipCompare) { - if (string.IsNullOrEmpty(IP) || ipCompare == null) { return false; } + if (string.IsNullOrEmpty(EndPoint) || ipCompare == null) { return false; } if (ipCompare.IsIPv4MappedToIPv6 && CompareTo(ipCompare.MapToIPv4NoThrow().ToString())) { return true; @@ -123,7 +125,10 @@ namespace Barotrauma.Networking { reason = string.Empty; if (IPAddress.IsLoopback(IP)) { return false; } - var bannedPlayer = bannedPlayers.Find(bp => bp.CompareTo(IP) || (steamID > 0 && bp.SteamID == steamID)); + var bannedPlayer = bannedPlayers.Find(bp => + bp.CompareTo(IP) || + (steamID > 0 && bp.SteamID == steamID) || + (SteamManager.SteamIDStringToUInt64(bp.EndPoint) == steamID)); reason = bannedPlayer?.Reason; return bannedPlayer != null; } @@ -142,7 +147,9 @@ namespace Barotrauma.Networking { reason = string.Empty; bannedPlayers.RemoveAll(bp => bp.ExpirationTime.HasValue && DateTime.Now > bp.ExpirationTime.Value); - var bannedPlayer = bannedPlayers.Find(bp => steamID > 0 && bp.SteamID == steamID); + var bannedPlayer = bannedPlayers.Find(bp => + steamID > 0 && + (bp.SteamID == steamID || SteamManager.SteamIDStringToUInt64(bp.EndPoint) == steamID)); reason = bannedPlayer?.Reason; return bannedPlayer != null; } @@ -153,9 +160,9 @@ namespace Barotrauma.Networking BanPlayer(name, ipStr, 0, reason, duration); } - public void BanPlayer(string name, string ip, string reason, TimeSpan? duration) + public void BanPlayer(string name, string endPoint, string reason, TimeSpan? duration) { - BanPlayer(name, ip, 0, reason, duration); + BanPlayer(name, endPoint, 0, reason, duration); } public void BanPlayer(string name, ulong steamID, string reason, TimeSpan? duration) @@ -163,9 +170,9 @@ namespace Barotrauma.Networking BanPlayer(name, "", steamID, reason, duration); } - private void BanPlayer(string name, string ip, ulong steamID, string reason, TimeSpan? duration) + private void BanPlayer(string name, string endPoint, ulong steamID, string reason, TimeSpan? duration) { - var existingBan = bannedPlayers.Find(bp => bp.IP == ip && bp.SteamID == steamID); + var existingBan = bannedPlayers.Find(bp => bp.EndPoint == endPoint && bp.SteamID == steamID); if (existingBan != null) { if (!duration.HasValue) return; @@ -190,9 +197,9 @@ namespace Barotrauma.Networking expirationTime = DateTime.Now + duration.Value; } - if (!string.IsNullOrEmpty(ip)) + if (!string.IsNullOrEmpty(endPoint)) { - bannedPlayers.Add(new BannedPlayer(name, ip, reason, expirationTime)); + bannedPlayers.Add(new BannedPlayer(name, endPoint, reason, expirationTime)); } else if (steamID > 0) { @@ -221,12 +228,15 @@ namespace Barotrauma.Networking } } - public void UnbanIP(string ip) + public void UnbanEndPoint(string endPoint) { - var player = bannedPlayers.Find(bp => bp.IP == ip); + ulong steamId = SteamManager.SteamIDStringToUInt64(endPoint); + var player = bannedPlayers.Find(bp => + bp.EndPoint == endPoint || + (steamId != 0 && steamId == SteamManager.SteamIDStringToUInt64(bp.EndPoint))); if (player == null) { - DebugConsole.Log("Could not unban IP \"" + ip + "\". Matching player not found."); + DebugConsole.Log("Could not unban endpoint \"" + endPoint + "\". Matching player not found."); } else { @@ -246,10 +256,10 @@ namespace Barotrauma.Networking private void RangeBan(BannedPlayer banned) { - banned.IP = ToRange(banned.IP); + banned.EndPoint = ToRange(banned.EndPoint); BannedPlayer bp; - while ((bp = bannedPlayers.Find(x => banned.CompareTo(x.IP))) != null) + while ((bp = bannedPlayers.Find(x => banned.CompareTo(x.EndPoint))) != null) { //remove all specific bans that are now covered by the rangeban bannedPlayers.Remove(bp); @@ -270,7 +280,7 @@ namespace Barotrauma.Networking foreach (BannedPlayer banned in bannedPlayers) { string line = banned.Name; - line += "," + ((banned.SteamID > 0) ? banned.SteamID.ToString() : banned.IP); + line += "," + ((banned.SteamID > 0) ? SteamManager.SteamIDUInt64ToString(banned.SteamID) : banned.EndPoint); line += "," + (banned.ExpirationTime.HasValue ? banned.ExpirationTime.Value.ToString() : ""); if (!string.IsNullOrWhiteSpace(banned.Reason)) line += "," + banned.Reason; @@ -314,7 +324,7 @@ namespace Barotrauma.Networking outMsg.Write(bannedPlayer.IsRangeBan); outMsg.WritePadBits(); if (c.Connection == GameMain.Server.OwnerConnection) { - outMsg.Write(bannedPlayer.IP); + outMsg.Write(bannedPlayer.EndPoint); outMsg.Write(bannedPlayer.SteamID); } } @@ -347,7 +357,7 @@ namespace Barotrauma.Networking BannedPlayer bannedPlayer = bannedPlayers.Find(p => p.UniqueIdentifier == id); if (bannedPlayer != null) { - GameServer.Log(GameServer.ClientLogName(c) + " unbanned " + bannedPlayer.Name + " (" + bannedPlayer.IP + ")", ServerLog.MessageType.ConsoleUsage); + GameServer.Log(GameServer.ClientLogName(c) + " unbanned " + bannedPlayer.Name + " (" + bannedPlayer.EndPoint + ")", ServerLog.MessageType.ConsoleUsage); RemoveBan(bannedPlayer); } } @@ -358,7 +368,7 @@ namespace Barotrauma.Networking BannedPlayer bannedPlayer = bannedPlayers.Find(p => p.UniqueIdentifier == id); if (bannedPlayer != null) { - GameServer.Log(GameServer.ClientLogName(c) + " rangebanned " + bannedPlayer.Name + " (" + bannedPlayer.IP + ")", ServerLog.MessageType.ConsoleUsage); + GameServer.Log(GameServer.ClientLogName(c) + " rangebanned " + bannedPlayer.Name + " (" + bannedPlayer.EndPoint + ")", ServerLog.MessageType.ConsoleUsage); RangeBan(bannedPlayer); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index 7d30c1db1..9beb811a0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -48,8 +48,8 @@ namespace Barotrauma.Networking public readonly Dictionary EntityEventLastSent = new Dictionary(); //when was a position update for a given entity last sent to the client - // key = entity id, value = NetTime.Now when sending - public readonly Dictionary PositionUpdateLastSent = new Dictionary(); + // key = entity, value = NetTime.Now when sending + public readonly Dictionary PositionUpdateLastSent = new Dictionary(); public readonly Queue PendingPositionUpdates = new Queue(); public bool ReadyToStart; @@ -145,19 +145,9 @@ namespace Barotrauma.Networking return true; } - public bool EndpointMatches(string endpoint) + public bool EndpointMatches(string endPoint) { - if (Connection is LidgrenConnection lidgrenConn) - { - if (lidgrenConn.IPEndPoint?.Address == null) { return false; } - if ((lidgrenConn.IPEndPoint?.Address.IsIPv4MappedToIPv6 ?? false) && - lidgrenConn.IPEndPoint?.Address.MapToIPv4NoThrow().ToString() == endpoint) - { - return true; - } - } - - return Connection.EndPointString == endpoint; + return Connection.EndpointMatches(endPoint); } public void SetPermissions(ClientPermissions permissions, List permittedConsoleCommands) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs index c7608ba48..264253c0a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs @@ -135,7 +135,7 @@ namespace Barotrauma.Networking if (!File.Exists(filePath)) { - DebugConsole.ThrowError("Failed to initiate file transfer (file \"" + filePath + "\" not found."); + DebugConsole.ThrowError("Failed to initiate file transfer (file \"" + filePath + "\" not found).\n" + Environment.StackTrace); return null; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 6129761bb..71e3035e5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -282,7 +282,9 @@ namespace Barotrauma.Networking Client newClient = new Client(clName, GetNewClientID()); newClient.InitClientSync(); newClient.Connection = connection; + newClient.Connection.Status = NetworkConnectionStatus.Connected; newClient.SteamID = connection.SteamID; + newClient.Language = connection.Language; ConnectedClients.Add(newClient); var previousPlayer = previousPlayers.Find(p => p.MatchesClient(newClient)); @@ -382,14 +384,12 @@ namespace Barotrauma.Networking for (int i = Character.CharacterList.Count - 1; i >= 0; i--) { Character character = Character.CharacterList[i]; - if (character.IsDead || !character.ClientDisconnected) continue; + if (character.IsDead || !character.ClientDisconnected) { continue; } character.KillDisconnectedTimer += deltaTime; character.SetStun(1.0f); - Client owner = connectedClients.Find(c => - c.Name == character.OwnerClientName && - c.EndpointMatches(character.OwnerClientEndPoint)); + Client owner = connectedClients.Find(c => c.EndpointMatches(character.OwnerClientEndPoint)); if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > serverSettings.KillDisconnectedTime) { @@ -397,8 +397,7 @@ namespace Barotrauma.Networking continue; } - if (owner != null && - owner.InGame && !owner.NeedsMidRoundSync && + if (owner != null && owner.InGame && !owner.NeedsMidRoundSync && (!serverSettings.AllowSpectating || !owner.SpectateOnly)) { SetClientCharacter(owner, character); @@ -436,16 +435,21 @@ namespace Barotrauma.Networking { if (Level.Loaded?.EndOutpost != null) { - bool charactersInsideOutpost = connectedClients.Any(c => + int charactersInsideOutpost = connectedClients.Count(c => c.Character != null && - !c.Character.IsDead && + !c.Character.IsDead && !c.Character.IsUnconscious && c.Character.Submarine == Level.Loaded.EndOutpost); + int charactersOutsideOutpost = connectedClients.Count(c => + c.Character != null && + !c.Character.IsDead && !c.Character.IsUnconscious && + c.Character.Submarine != Level.Loaded.EndOutpost); //level finished if the sub is docked to the outpost //or very close and someone from the crew made it inside the outpost subAtLevelEnd = Submarine.MainSub.DockedTo.Contains(Level.Loaded.EndOutpost) || - (Submarine.MainSub.AtEndPosition && charactersInsideOutpost); + (Submarine.MainSub.AtEndPosition && charactersInsideOutpost > 0) || + (charactersInsideOutpost > charactersOutsideOutpost); } else { @@ -464,7 +468,7 @@ namespace Barotrauma.Networking endRoundDelay = 5.0f; endRoundTimer += deltaTime; } - else if (serverSettings.EndRoundAtLevelEnd && subAtLevelEnd && !(GameMain.GameSession?.GameMode is CampaignMode)) + else if (subAtLevelEnd && !(GameMain.GameSession?.GameMode is CampaignMode)) { endRoundDelay = 5.0f; endRoundTimer += deltaTime; @@ -493,7 +497,7 @@ namespace Barotrauma.Networking { Log("Ending round (entire crew dead)", ServerLog.MessageType.ServerMessage); } - else if (serverSettings.EndRoundAtLevelEnd && subAtLevelEnd) + else if (subAtLevelEnd) { Log("Ending round (submarine reached the end of the level)", ServerLog.MessageType.ServerMessage); } @@ -628,7 +632,11 @@ namespace Barotrauma.Networking { if (character.healthUpdateTimer <= 0.0f) { - character.healthUpdateTimer = character.HealthUpdateInterval; + if (!character.HealthUpdatePending) + { + character.healthUpdateTimer = character.HealthUpdateInterval; + } + character.HealthUpdatePending = true; } else { @@ -1532,8 +1540,7 @@ namespace Barotrauma.Networking { foreach (Character character in Character.CharacterList) { - if (!character.Enabled) continue; - + if (!character.Enabled) { continue; } if (c.SpectatePos == null) { if (c.Character != null && Vector2.DistanceSquared(character.WorldPosition, c.Character.WorldPosition) >= NetConfig.DisableCharacterDistSqr) @@ -1543,17 +1550,24 @@ namespace Barotrauma.Networking } else { - if (Vector2.DistanceSquared(character.WorldPosition, c.SpectatePos.Value) >= NetConfig.DisableCharacterDistSqr) + if (character != c.Character && Vector2.DistanceSquared(character.WorldPosition, c.SpectatePos.Value) >= NetConfig.DisableCharacterDistSqr) { continue; } } float updateInterval = character.GetPositionUpdateInterval(c); - c.PositionUpdateLastSent.TryGetValue(character.ID, out float lastSent); - if (lastSent > Lidgren.Network.NetTime.Now - updateInterval) { continue; } - - if (!c.PendingPositionUpdates.Contains(character)) c.PendingPositionUpdates.Enqueue(character); + c.PositionUpdateLastSent.TryGetValue(character, out float lastSent); + if (lastSent > NetTime.Now) + { + //sent in the future -> can't be right, remove + c.PositionUpdateLastSent.Remove(character); + } + else + { + if (lastSent > NetTime.Now - updateInterval) { continue; } + } + if (!c.PendingPositionUpdates.Contains(character)) { c.PendingPositionUpdates.Enqueue(character); } } foreach (Submarine sub in Submarine.Loaded) @@ -1568,16 +1582,24 @@ namespace Barotrauma.Networking { if (item.PositionUpdateInterval == float.PositiveInfinity) { continue; } float updateInterval = item.GetPositionUpdateInterval(c); - c.PositionUpdateLastSent.TryGetValue(item.ID, out float lastSent); - if (lastSent > Lidgren.Network.NetTime.Now - updateInterval) { continue; } - if (!c.PendingPositionUpdates.Contains(item)) c.PendingPositionUpdates.Enqueue(item); + c.PositionUpdateLastSent.TryGetValue(item, out float lastSent); + if (lastSent > NetTime.Now) + { + //sent in the future -> can't be right, remove + c.PositionUpdateLastSent.Remove(item); + } + else + { + if (lastSent > NetTime.Now - updateInterval) { continue; } + } + if (!c.PendingPositionUpdates.Contains(item)) { c.PendingPositionUpdates.Enqueue(item); } } } IWriteMessage outmsg = new WriteOnlyMessage(); outmsg.Write((byte)ServerPacketHeader.UPDATE_INGAME); - outmsg.Write((float)Lidgren.Network.NetTime.Now); + outmsg.Write((float)NetTime.Now); outmsg.Write((byte)ServerNetObject.SYNC_IDS); outmsg.Write(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server @@ -1636,7 +1658,7 @@ namespace Barotrauma.Networking outmsg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); outmsg.WritePadBits(); - c.PositionUpdateLastSent[entity.ID] = (float)Lidgren.Network.NetTime.Now; + c.PositionUpdateLastSent[entity] = (float)NetTime.Now; c.PendingPositionUpdates.Dequeue(); } positionUpdateBytes = outmsg.LengthBytes - positionUpdateBytes; @@ -1713,7 +1735,7 @@ namespace Barotrauma.Networking outmsg.Write(client.SteamID); outmsg.Write(client.NameID); outmsg.Write(client.Name); - outmsg.Write(client.Character == null || !gameStarted ? (client.PreferredJob ?? "") : ""); + outmsg.Write(client.Character?.Info?.Job != null && gameStarted ? client.Character.Info.Job.Prefab.Identifier : (client.PreferredJob ?? "")); outmsg.Write(client.Character == null || !gameStarted ? (ushort)0 : client.Character.ID); if (c.HasPermission(ClientPermissions.ServerLog)) { @@ -2099,11 +2121,12 @@ namespace Barotrauma.Networking } MissionMode missionMode = GameMain.GameSession.GameMode as MissionMode; - bool missionAllowRespawn = campaign == null && (missionMode?.Mission == null || missionMode.Mission.AllowRespawn); + bool missionAllowRespawn = GameMain.GameSession.Campaign == null && (missionMode?.Mission == null || missionMode.Mission.AllowRespawn); + bool outpostAllowRespawn = GameMain.GameSession.Campaign != null && Level.Loaded?.Type == LevelData.LevelType.Outpost; - if (serverSettings.AllowRespawn && missionAllowRespawn) + if (serverSettings.AllowRespawn && (missionAllowRespawn || outpostAllowRespawn)) { - respawnManager = new RespawnManager(this, serverSettings.UseRespawnShuttle ? selectedShuttle : null); + respawnManager = new RespawnManager(this, serverSettings.UseRespawnShuttle && !outpostAllowRespawn ? selectedShuttle : null); } Level.Loaded?.SpawnNPCs(); @@ -2298,6 +2321,11 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Running; + if (GameMain.Server?.ServerSettings?.Voting != null) + { + GameMain.Server.ServerSettings.Voting.ResetVotes(GameMain.Server.ConnectedClients); + } + GameMain.GameScreen.Select(); Log("Round started.", ServerLog.MessageType.ServerMessage); @@ -2332,7 +2360,8 @@ namespace Barotrauma.Networking msg.Write(gameSession.GameMode.Preset.Identifier); bool missionAllowRespawn = campaign == null && (missionMode?.Mission == null || missionMode.Mission.AllowRespawn); - msg.Write(serverSettings.AllowRespawn && missionAllowRespawn); + bool outpostAllowRespawn = campaign != null && campaign.NextLevel?.Type == LevelData.LevelType.Outpost; + msg.Write(missionAllowRespawn || outpostAllowRespawn); msg.Write(serverSettings.AllowDisguises); msg.Write(serverSettings.AllowRewiring); msg.Write(serverSettings.AllowRagdollButton); @@ -2470,23 +2499,6 @@ namespace Barotrauma.Networking traitorResult.ServerWrite(msg); } - // used to check if client and server mismatch upgrades - if (GameMain.GameSession?.GameMode is CampaignMode campaign) - { - msg.Write(true); - Dictionary dict = UpgradeManager.GetMetadataLevels(campaign?.CampaignMetadata); - msg.Write((ushort)dict.Count); - foreach (var (key, value) in dict) - { - msg.Write(key); - msg.Write((byte)value); - } - } - else - { - msg.Write(false); - } - foreach (Client client in connectedClients) { serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); @@ -2544,12 +2556,18 @@ namespace Barotrauma.Networking { if (!Client.IsValidName(newName, serverSettings)) { - SendDirectChatMessage("Could not change your name to \"" + newName + "\" (the name contains disallowed symbols).", c, ChatMessageType.MessageBox); + SendDirectChatMessage($"ServerMessage.NameChangeFailedSymbols~[newname]={newName}", c, ChatMessageType.ServerMessageBox); return false; } if (Homoglyphs.Compare(newName.ToLower(), ServerName.ToLower())) { - SendDirectChatMessage("Could not change your name to \"" + newName + "\" (too similar to the server's name).", c, ChatMessageType.MessageBox); + SendDirectChatMessage($"ServerMessage.NameChangeFailedServerTooSimilar~[newname]={newName}", c, ChatMessageType.ServerMessageBox); + return false; + } + + if (c.KickVoteCount > 0) + { + SendDirectChatMessage($"ServerMessage.NameChangeFailedVoteKick~[newname]={newName}", c, ChatMessageType.ServerMessageBox); return false; } } @@ -2557,11 +2575,11 @@ namespace Barotrauma.Networking Client nameTaken = ConnectedClients.Find(c2 => c != c2 && Homoglyphs.Compare(c2.Name.ToLower(), newName.ToLower())); if (nameTaken != null) { - SendDirectChatMessage("Could not change your name to \"" + newName + "\" (too similar to the name of the client \"" + nameTaken.Name + "\").", c, ChatMessageType.MessageBox); + SendDirectChatMessage($"ServerMessage.NameChangeFailedClientTooSimilar~[newname]={newName}~[takenname]={nameTaken.Name}", c, ChatMessageType.ServerMessageBox); return false; } - SendChatMessage("Player \"" + c.Name + "\" has changed their name to \"" + newName + "\".", ChatMessageType.Server); + SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={c.Name}~[newname]={newName}", ChatMessageType.Server); c.Name = newName; c.Connection.Name = newName; return true; @@ -2633,16 +2651,13 @@ namespace Barotrauma.Networking string targetMsg = DisconnectReason.Banned.ToString(); DisconnectClient(client, $"ServerMessage.BannedFromServer~[client]={client.Name}", targetMsg, reason, PlayerConnectionChangeType.Banned); - if (client.SteamID == 0 || range) + if (client.Connection is LidgrenConnection lidgrenConn && (client.SteamID == 0 || range)) { string ip = ""; - if (client.Connection is LidgrenConnection lidgrenConn) - { - ip = lidgrenConn.IPEndPoint.Address.IsIPv4MappedToIPv6 ? - lidgrenConn.IPEndPoint.Address.MapToIPv4NoThrow().ToString() : - lidgrenConn.IPEndPoint.Address.ToString(); - if (range) { ip = BanList.ToRange(ip); } - } + ip = lidgrenConn.IPEndPoint.Address.IsIPv4MappedToIPv6 ? + lidgrenConn.IPEndPoint.Address.MapToIPv4NoThrow().ToString() : + lidgrenConn.IPEndPoint.Address.ToString(); + if (range) { ip = BanList.ToRange(ip); } serverSettings.BanList.BanPlayer(client.Name, ip, reason, duration); } @@ -2652,11 +2667,11 @@ namespace Barotrauma.Networking } } - public override void UnbanPlayer(string playerName, string playerIP) + public override void UnbanPlayer(string playerName, string playerEndPoint) { - if (!string.IsNullOrEmpty(playerIP)) + if (!string.IsNullOrEmpty(playerEndPoint)) { - serverSettings.BanList.UnbanIP(playerIP); + serverSettings.BanList.UnbanEndPoint(playerEndPoint); } else if (!string.IsNullOrEmpty(playerName)) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index 1f74943b3..6704bac5f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -8,41 +8,9 @@ namespace Barotrauma.Networking { class LidgrenServerPeer : ServerPeer { - private readonly ServerSettings serverSettings; - private NetPeerConfiguration netPeerConfiguration; private NetServer netServer; - private class PendingClient - { - public string Name; - public int OwnerKey; - public NetConnection Connection; - public ConnectionInitialization InitializationStep; - public double UpdateTime; - public double TimeOut; - public int Retries; - public UInt64? SteamID; - public Int32? PasswordSalt; - public bool AuthSessionStarted; - - public PendingClient(NetConnection conn) - { - OwnerKey = 0; - Connection = conn; - InitializationStep = ConnectionInitialization.SteamTicketAndVersion; - Retries = 0; - SteamID = null; - PasswordSalt = null; - UpdateTime = Timing.TotalTime + Timing.Step * 3.0; - TimeOut = NetworkConnection.TimeoutThreshold; - AuthSessionStarted = false; - } - } - - private readonly List connectedClients; - private readonly List pendingClients; - private readonly List incomingLidgrenMessages; public LidgrenServerPeer(int? ownKey, ServerSettings settings) @@ -51,7 +19,7 @@ namespace Barotrauma.Networking netServer = null; - connectedClients = new List(); + connectedClients = new List(); pendingClients = new List(); incomingLidgrenMessages = new List(); @@ -168,7 +136,16 @@ namespace Barotrauma.Networking for (int i = 0; i < pendingClients.Count; i++) { PendingClient pendingClient = pendingClients[i]; - UpdatePendingClient(pendingClient, deltaTime); + + var connection = pendingClient.Connection as LidgrenConnection; + if (connection.NetConnection.Status == NetConnectionStatus.InitiatedConnect || + connection.NetConnection.Status == NetConnectionStatus.ReceivedInitiation || + connection.NetConnection.Status == NetConnectionStatus.RespondedAwaitingApproval || + connection.NetConnection.Status == NetConnectionStatus.RespondedConnect) + { + continue; + } + UpdatePendingClient(pendingClient); if (i >= pendingClients.Count || pendingClients[i] != pendingClient) { i--; } } @@ -214,11 +191,11 @@ namespace Barotrauma.Networking return; } - PendingClient pendingClient = pendingClients.Find(c => c.Connection == inc.SenderConnection); + PendingClient pendingClient = pendingClients.Find(c => c.Connection is LidgrenConnection l && l.NetConnection == inc.SenderConnection); if (pendingClient == null) { - pendingClient = new PendingClient(inc.SenderConnection); + pendingClient = new PendingClient(new LidgrenConnection("PENDING", inc.SenderConnection, 0)); pendingClients.Add(pendingClient); } @@ -229,7 +206,7 @@ namespace Barotrauma.Networking { if (netServer == null) { return; } - PendingClient pendingClient = pendingClients.Find(c => c.Connection == inc.SenderConnection); + PendingClient pendingClient = pendingClients.Find(c => (c.Connection is LidgrenConnection l) && l.NetConnection == inc.SenderConnection); byte incByte = inc.ReadByte(); bool isCompressed = (incByte & (byte)PacketHeader.IsCompressed) != 0; @@ -237,11 +214,11 @@ namespace Barotrauma.Networking if (isConnectionInitializationStep && pendingClient != null) { - ReadConnectionInitializationStep(pendingClient, inc); + ReadConnectionInitializationStep(pendingClient, new ReadWriteMessage(inc.Data, (int)inc.Position, inc.LengthBits, false)); } else if (!isConnectionInitializationStep) { - LidgrenConnection conn = connectedClients.Find(c => c.NetConnection == inc.SenderConnection); + LidgrenConnection conn = connectedClients.Find(c => (c is LidgrenConnection l) && l.NetConnection == inc.SenderConnection) as LidgrenConnection; if (conn == null) { if (pendingClient != null) @@ -278,7 +255,7 @@ namespace Barotrauma.Networking { case NetConnectionStatus.Disconnected: string disconnectMsg; - LidgrenConnection conn = connectedClients.Find(c => c.NetConnection == inc.SenderConnection); + LidgrenConnection conn = connectedClients.Select(c => c as LidgrenConnection).FirstOrDefault(c => c.NetConnection == inc.SenderConnection); if (conn != null) { if (conn == OwnerConnection) @@ -295,7 +272,7 @@ namespace Barotrauma.Networking } else { - PendingClient pendingClient = pendingClients.Find(c => c.Connection == inc.SenderConnection); + PendingClient pendingClient = pendingClients.Find(c => (c.Connection is LidgrenConnection l) && l.NetConnection == inc.SenderConnection); if (pendingClient != null) { RemovePendingClient(pendingClient, DisconnectReason.Unknown, $"ServerMessage.HasDisconnected~[client]={pendingClient.Name}"); @@ -305,303 +282,6 @@ namespace Barotrauma.Networking } } - private void ReadConnectionInitializationStep(PendingClient pendingClient, NetIncomingMessage inc) - { - if (netServer == null) { return; } - - pendingClient.TimeOut = NetworkConnection.TimeoutThreshold; - - ConnectionInitialization initializationStep = (ConnectionInitialization)inc.ReadByte(); - - //DebugConsole.NewMessage(initializationStep+" "+pendingClient.InitializationStep); - - if (pendingClient.InitializationStep != initializationStep) return; - - pendingClient.UpdateTime = Timing.TotalTime + Timing.Step; - - switch (initializationStep) - { - case ConnectionInitialization.SteamTicketAndVersion: - string name = Client.SanitizeName(inc.ReadString()); - int ownKey = inc.ReadInt32(); - UInt64 steamId = inc.ReadUInt64(); - UInt16 ticketLength = inc.ReadUInt16(); - byte[] ticket = inc.ReadBytes(ticketLength); - - if (!Client.IsValidName(name, serverSettings)) - { - if (OwnerConnection != null || - !IPAddress.IsLoopback(pendingClient.Connection.RemoteEndPoint.Address.MapToIPv4NoThrow()) && - ownerKey == null || ownKey == 0 && ownKey != ownerKey) - { - RemovePendingClient(pendingClient, DisconnectReason.InvalidName, "The name \"" + name + "\" is invalid"); - return; - } - } - - string version = inc.ReadString(); - bool isCompatibleVersion = NetworkMember.IsCompatible(version, GameMain.Version.ToString()) ?? false; - if (!isCompatibleVersion) - { - RemovePendingClient(pendingClient, DisconnectReason.InvalidVersion, - $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version}~[clientversion]={version}"); - - GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); - DebugConsole.NewMessage(name + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red); - return; - } - - Client nameTaken = GameMain.Server.ConnectedClients.Find(c => Homoglyphs.Compare(c.Name.ToLower(), name.ToLower())); - if (nameTaken != null) - { - RemovePendingClient(pendingClient, DisconnectReason.NameTaken, ""); - GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address + ") couldn't join the server (name too similar to the name of the client \"" + nameTaken.Name + "\").", ServerLog.MessageType.Error); - return; - } - - int contentPackageCount = inc.ReadVariableInt32(); - List clientContentPackages = new List(); - for (int i = 0; i < contentPackageCount; i++) - { - string packageName = inc.ReadString(); - string packageHash = inc.ReadString(); - clientContentPackages.Add(new ClientContentPackage(packageName, packageHash)); - } - - //check if the client is missing any of our packages - List missingPackages = new List(); - foreach (ContentPackage serverContentPackage in GameMain.SelectedPackages) - { - if (!serverContentPackage.HasMultiplayerIncompatibleContent) continue; - bool packageFound = clientContentPackages.Any(cp => cp.Name == serverContentPackage.Name && cp.Hash == serverContentPackage.MD5hash.Hash); - if (!packageFound) { missingPackages.Add(serverContentPackage); } - } - - //check if the client is using packages we don't have - List redundantPackages = new List(); - foreach (ClientContentPackage clientContentPackage in clientContentPackages) - { - bool packageFound = GameMain.SelectedPackages.Any(cp => cp.Name == clientContentPackage.Name && cp.MD5hash.Hash == clientContentPackage.Hash); - if (!packageFound) { redundantPackages.Add(clientContentPackage); } - } - - if (missingPackages.Count == 1) - { - RemovePendingClient(pendingClient, DisconnectReason.MissingContentPackage, - $"DisconnectMessage.MissingContentPackage~[missingcontentpackage]={GetPackageStr(missingPackages[0])}"); - GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address + ") couldn't join the server (missing content package " + GetPackageStr(missingPackages[0]) + ")", ServerLog.MessageType.Error); - return; - } - else if (missingPackages.Count > 1) - { - List packageStrs = new List(); - missingPackages.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); - RemovePendingClient(pendingClient, DisconnectReason.MissingContentPackage, - $"DisconnectMessage.MissingContentPackages~[missingcontentpackages]={string.Join(", ", packageStrs)}"); - GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address + ") couldn't join the server (missing content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error); - return; - } - if (redundantPackages.Count == 1) - { - RemovePendingClient(pendingClient, DisconnectReason.IncompatibleContentPackage, - $"DisconnectMessage.IncompatibleContentPackage~[incompatiblecontentpackage]={GetPackageStr(redundantPackages[0])}"); - GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address + ") couldn't join the server (using an incompatible content package " + GetPackageStr(redundantPackages[0]) + ")", ServerLog.MessageType.Error); - return; - } - if (redundantPackages.Count > 1) - { - List packageStrs = new List(); - redundantPackages.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); - RemovePendingClient(pendingClient, DisconnectReason.IncompatibleContentPackage, - $"DisconnectMessage.IncompatibleContentPackages~[incompatiblecontentpackages]={string.Join(", ", packageStrs)}"); - GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address + ") couldn't join the server (using incompatible content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error); - return; - } - - if (pendingClient.SteamID == null) - { - bool requireSteamAuth = GameMain.Config.RequireSteamAuthentication; -#if DEBUG - requireSteamAuth = false; -#endif - - //steam auth cannot be done (SteamManager not initialized or no ticket given), - //but it's not required either -> let the client join without auth - if ((!Steam.SteamManager.IsInitialized || (ticket?.Length ?? 0) == 0) && - !requireSteamAuth) - { - pendingClient.Name = name; - pendingClient.OwnerKey = ownKey; - pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; - } - else - { - Steamworks.BeginAuthResult authSessionStartState = Steam.SteamManager.StartAuthSession(ticket, steamId); - if (authSessionStartState != Steamworks.BeginAuthResult.OK) - { - RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "Steam auth session failed to start: " + authSessionStartState.ToString()); - return; - } - pendingClient.SteamID = steamId; - pendingClient.Name = name; - pendingClient.OwnerKey = ownKey; - pendingClient.AuthSessionStarted = true; - } - } - else //TODO: could remove since this seems impossible - { - if (pendingClient.SteamID != steamId) - { - RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "SteamID mismatch"); - return; - } - } - break; - case ConnectionInitialization.Password: - int pwLength = inc.ReadByte(); - byte[] incPassword = new byte[pwLength]; - inc.ReadBytes(incPassword, 0, pwLength); - if (pendingClient.PasswordSalt == null) - { - DebugConsole.ThrowError("Received password message from client without salt"); - return; - } - if (serverSettings.IsPasswordCorrect(incPassword, pendingClient.PasswordSalt.Value)) - { - pendingClient.InitializationStep = ConnectionInitialization.ContentPackageOrder; - } - else - { - pendingClient.Retries++; - if (serverSettings.BanAfterWrongPassword && pendingClient.Retries > serverSettings.MaxPasswordRetriesBeforeBan) - { - string banMsg = "Failed to enter correct password too many times"; - if (pendingClient.SteamID != null) - { - serverSettings.BanList.BanPlayer(pendingClient.Name, pendingClient.SteamID.Value, banMsg, null); - } - serverSettings.BanList.BanPlayer(pendingClient.Name, pendingClient.Connection.RemoteEndPoint.Address, banMsg, null); - RemovePendingClient(pendingClient, DisconnectReason.Banned, banMsg); - return; - } - } - pendingClient.UpdateTime = Timing.TotalTime; - break; - case ConnectionInitialization.ContentPackageOrder: - pendingClient.InitializationStep = ConnectionInitialization.Success; - pendingClient.UpdateTime = Timing.TotalTime; - break; - } - } - - - private void UpdatePendingClient(PendingClient pendingClient, float deltaTime) - { - if (netServer == null) { return; } - - if (serverSettings.BanList.IsBanned(pendingClient.Connection.RemoteEndPoint.Address, pendingClient.SteamID ?? 0, out string banReason)) - { - RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason); - return; - } - - //DebugConsole.NewMessage("pending client status: " + pendingClient.InitializationStep); - - if (connectedClients.Count >= serverSettings.MaxPlayers) - { - RemovePendingClient(pendingClient, DisconnectReason.ServerFull, ""); - } - - if (pendingClient.InitializationStep == ConnectionInitialization.Success) - { - LidgrenConnection newConnection = new LidgrenConnection(pendingClient.Name, pendingClient.Connection, pendingClient.SteamID ?? 0) - { - Status = NetworkConnectionStatus.Connected - }; - connectedClients.Add(newConnection); - pendingClients.Remove(pendingClient); - - if (OwnerConnection == null && - IPAddress.IsLoopback(pendingClient.Connection.RemoteEndPoint.Address.MapToIPv4NoThrow()) && - ownerKey != null && pendingClient.OwnerKey != 0 && pendingClient.OwnerKey == ownerKey) - { - ownerKey = null; - OwnerConnection = newConnection; - } - - OnInitializationComplete?.Invoke(newConnection); - return; - } - - - pendingClient.TimeOut -= deltaTime; - if (pendingClient.TimeOut < 0.0) - { - RemovePendingClient(pendingClient, DisconnectReason.Unknown, Lidgren.Network.NetConnection.NoResponseMessage); - } - - if (Timing.TotalTime < pendingClient.UpdateTime) { return; } - pendingClient.UpdateTime = Timing.TotalTime + 1.0; - - NetOutgoingMessage outMsg = netServer.CreateMessage(); - outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); - outMsg.Write((byte)pendingClient.InitializationStep); - switch (pendingClient.InitializationStep) - { - case ConnectionInitialization.ContentPackageOrder: - var mpContentPackages = GameMain.SelectedPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList(); - outMsg.WriteVariableInt32(mpContentPackages.Count); - for (int i = 0; i < mpContentPackages.Count; i++) - { - outMsg.Write(mpContentPackages[i].MD5hash.Hash); - } - break; - case ConnectionInitialization.Password: - outMsg.Write(pendingClient.PasswordSalt == null); outMsg.WritePadBits(); - if (pendingClient.PasswordSalt == null) - { - pendingClient.PasswordSalt = CryptoRandom.Instance.Next(); - outMsg.Write(pendingClient.PasswordSalt.Value); - } - else - { - outMsg.Write(pendingClient.Retries); - } - break; - } -#if DEBUG - netPeerConfiguration.SimulatedDuplicatesChance = GameMain.Server.SimulatedDuplicatesChance; - netPeerConfiguration.SimulatedMinimumLatency = GameMain.Server.SimulatedMinimumLatency; - netPeerConfiguration.SimulatedRandomLatency = GameMain.Server.SimulatedRandomLatency; - netPeerConfiguration.SimulatedLoss = GameMain.Server.SimulatedLoss; -#endif - NetSendResult result = netServer.SendMessage(outMsg, pendingClient.Connection, NetDeliveryMethod.ReliableUnordered); - if (result != NetSendResult.Sent && result != NetSendResult.Queued) - { - DebugConsole.NewMessage("Failed to send initialization step " + pendingClient.InitializationStep.ToString() + " to pending client: " + result.ToString(), Microsoft.Xna.Framework.Color.Yellow); - } - //DebugConsole.NewMessage("sent update to pending client: " + pendingClient.InitializationStep); - } - - private void RemovePendingClient(PendingClient pendingClient, DisconnectReason reason, string msg) - { - if (netServer == null) { return; } - - if (pendingClients.Contains(pendingClient)) - { - pendingClients.Remove(pendingClient); - - if (pendingClient.AuthSessionStarted) - { - Steam.SteamManager.StopAuthSession(pendingClient.SteamID.Value); - pendingClient.SteamID = null; - pendingClient.AuthSessionStarted = false; - } - - pendingClient.Connection.Disconnect(reason + "/" + msg); - } - } - public override void InitializeSteamServerCallbacks() { Steamworks.SteamServer.OnValidateAuthTicketResponse += OnAuthChange; @@ -618,7 +298,7 @@ namespace Barotrauma.Networking { if (status != Steamworks.AuthResponse.OK) { - LidgrenConnection connection = connectedClients.Find(c => c.SteamID == steamID); + LidgrenConnection connection = connectedClients.Find(c => c.SteamID == steamID) as LidgrenConnection; if (connection != null) { Disconnect(connection, DisconnectReason.SteamAuthenticationFailed.ToString() + "/ Steam authentication status changed: " + status.ToString()); @@ -627,7 +307,8 @@ namespace Barotrauma.Networking return; } - if (serverSettings.BanList.IsBanned(pendingClient.Connection.RemoteEndPoint.Address, steamID, out string banReason)) + LidgrenConnection pendingConnection = pendingClient.Connection as LidgrenConnection; + if (serverSettings.BanList.IsBanned(pendingConnection.NetConnection.RemoteEndPoint.Address, steamID, out string banReason)) { RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason); return; @@ -698,5 +379,87 @@ namespace Barotrauma.Networking } lidgrenConn.NetConnection.Disconnect(msg ?? "Disconnected"); } + + protected override void SendMsgInternal(NetworkConnection conn, DeliveryMethod deliveryMethod, IWriteMessage msg) + { + LidgrenConnection lidgrenConn = conn as LidgrenConnection; + NetDeliveryMethod lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable; + switch (deliveryMethod) + { + case DeliveryMethod.Unreliable: + lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable; + break; + case DeliveryMethod.Reliable: + lidgrenDeliveryMethod = NetDeliveryMethod.ReliableUnordered; + break; + case DeliveryMethod.ReliableOrdered: + lidgrenDeliveryMethod = NetDeliveryMethod.ReliableOrdered; + break; + } + + NetOutgoingMessage lidgrenMsg = netServer.CreateMessage(); + lidgrenMsg.Write(msg.Buffer, 0, msg.LengthBytes); + NetSendResult result = netServer.SendMessage(lidgrenMsg, lidgrenConn.NetConnection, lidgrenDeliveryMethod); + if (result != NetSendResult.Sent && result != NetSendResult.Queued) + { + DebugConsole.NewMessage("Failed to send message to " + conn.Name + ": " + result.ToString(), Microsoft.Xna.Framework.Color.Yellow); + } + } + + protected override void CheckOwnership(PendingClient pendingClient) + { + LidgrenConnection l = pendingClient.Connection as LidgrenConnection; + if (OwnerConnection == null && + IPAddress.IsLoopback(l.NetConnection.RemoteEndPoint.Address.MapToIPv4NoThrow()) && + ownerKey != null && pendingClient.OwnerKey != 0 && pendingClient.OwnerKey == ownerKey) + { + ownerKey = null; + OwnerConnection = pendingClient.Connection; + } + } + + protected override void ProcessAuthTicket(string name, int ownKey, ulong steamId, PendingClient pendingClient, byte[] ticket) + { + if (pendingClient.SteamID == null) + { + bool requireSteamAuth = GameMain.Config.RequireSteamAuthentication; +#if DEBUG + requireSteamAuth = false; +#endif + + //steam auth cannot be done (SteamManager not initialized or no ticket given), + //but it's not required either -> let the client join without auth + if ((!Steam.SteamManager.IsInitialized || (ticket?.Length ?? 0) == 0) && + !requireSteamAuth) + { + pendingClient.Connection.Name = name; + pendingClient.Name = name; + pendingClient.OwnerKey = ownKey; + pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; + } + else + { + Steamworks.BeginAuthResult authSessionStartState = Steam.SteamManager.StartAuthSession(ticket, steamId); + if (authSessionStartState != Steamworks.BeginAuthResult.OK) + { + RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "Steam auth session failed to start: " + authSessionStartState.ToString()); + return; + } + pendingClient.SteamID = steamId; + pendingClient.Connection.Name = name; + pendingClient.Name = name; + pendingClient.OwnerKey = ownKey; + pendingClient.AuthSessionStarted = true; + } + } + else //TODO: could remove since this seems impossible + { + if (pendingClient.SteamID != steamId) + { + RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "SteamID mismatch"); + return; + } + } + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 825d59b8b..a92d04706 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; @@ -7,26 +8,6 @@ namespace Barotrauma.Networking { abstract class ServerPeer { - protected struct ClientContentPackage - { - public string Name; - public string Hash; - - public ClientContentPackage(string name, string hash) - { - Name = name; Hash = hash; - } - } - - protected string GetPackageStr(ContentPackage contentPackage) - { - return "\"" + contentPackage.Name + "\" (hash " + contentPackage.MD5hash.ShortHash + ")"; - } - protected string GetPackageStr(ClientContentPackage contentPackage) - { - return "\"" + contentPackage.Name + "\" (hash " + Md5Hash.GetShortHash(contentPackage.Hash) + ")"; - } - public delegate void MessageCallback(NetworkConnection connection, IReadMessage message); public delegate void DisconnectCallback(NetworkConnection connection, string reason); public delegate void InitializationCompleteCallback(NetworkConnection connection); @@ -48,7 +29,245 @@ namespace Barotrauma.Networking public abstract void Start(); public abstract void Close(string msg = null); public abstract void Update(float deltaTime); - + + protected class PendingClient + { + public string Name; + public int OwnerKey; + public NetworkConnection Connection; + public ConnectionInitialization InitializationStep; + public double UpdateTime; + public double TimeOut; + public int Retries; + public UInt64? SteamID; + public Int32? PasswordSalt; + public bool AuthSessionStarted; + + public PendingClient(NetworkConnection conn) + { + OwnerKey = 0; + Connection = conn; + InitializationStep = ConnectionInitialization.SteamTicketAndVersion; + Retries = 0; + SteamID = null; + PasswordSalt = null; + UpdateTime = Timing.TotalTime + Timing.Step * 3.0; + TimeOut = NetworkConnection.TimeoutThreshold; + AuthSessionStarted = false; + } + + public void Heartbeat() + { + TimeOut = NetworkConnection.TimeoutThreshold; + } + } + protected List connectedClients; + protected List pendingClients; + + protected ServerSettings serverSettings; + + protected void ReadConnectionInitializationStep(PendingClient pendingClient, IReadMessage inc) + { + pendingClient.TimeOut = NetworkConnection.TimeoutThreshold; + + ConnectionInitialization initializationStep = (ConnectionInitialization)inc.ReadByte(); + + if (pendingClient.InitializationStep != initializationStep) return; + + pendingClient.UpdateTime = Timing.TotalTime + Timing.Step; + + switch (initializationStep) + { + case ConnectionInitialization.SteamTicketAndVersion: + string name = Client.SanitizeName(inc.ReadString()); + int ownerKey = inc.ReadInt32(); + UInt64 steamId = inc.ReadUInt64(); + UInt16 ticketLength = inc.ReadUInt16(); + byte[] ticketBytes = inc.ReadBytes(ticketLength); + + if (!Client.IsValidName(name, serverSettings)) + { + RemovePendingClient(pendingClient, DisconnectReason.InvalidName, "The name \"" + name + "\" is invalid"); + return; + } + + string version = inc.ReadString(); + bool isCompatibleVersion = NetworkMember.IsCompatible(version, GameMain.Version.ToString()) ?? false; + if (!isCompatibleVersion) + { + RemovePendingClient(pendingClient, DisconnectReason.InvalidVersion, + $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version}~[clientversion]={version}"); + + GameServer.Log(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); + DebugConsole.NewMessage(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red); + return; + } + + string language = inc.ReadString(); + pendingClient.Connection.Language = language; + + Client nameTaken = GameMain.Server.ConnectedClients.Find(c => Homoglyphs.Compare(c.Name.ToLower(), name.ToLower())); + if (nameTaken != null) + { + RemovePendingClient(pendingClient, DisconnectReason.NameTaken, ""); + GameServer.Log(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (name too similar to the name of the client \"" + nameTaken.Name + "\").", ServerLog.MessageType.Error); + return; + } + + if (!pendingClient.AuthSessionStarted) + { + ProcessAuthTicket(name, ownerKey, steamId, pendingClient, ticketBytes); + } + break; + case ConnectionInitialization.Password: + int pwLength = inc.ReadByte(); + byte[] incPassword = inc.ReadBytes(pwLength); + if (pendingClient.PasswordSalt == null) + { + DebugConsole.ThrowError("Received password message from client without salt"); + return; + } + if (serverSettings.IsPasswordCorrect(incPassword, pendingClient.PasswordSalt.Value)) + { + pendingClient.InitializationStep = ConnectionInitialization.ContentPackageOrder; + } + else + { + pendingClient.Retries++; + if (serverSettings.BanAfterWrongPassword && pendingClient.Retries > serverSettings.MaxPasswordRetriesBeforeBan) + { + string banMsg = "Failed to enter correct password too many times"; + BanPendingClient(pendingClient, banMsg, null); + + RemovePendingClient(pendingClient, DisconnectReason.Banned, banMsg); + return; + } + } + pendingClient.UpdateTime = Timing.TotalTime; + break; + case ConnectionInitialization.ContentPackageOrder: + pendingClient.InitializationStep = ConnectionInitialization.Success; + pendingClient.UpdateTime = Timing.TotalTime; + break; + } + } + + protected abstract void ProcessAuthTicket(string name, int ownKey, ulong steamId, PendingClient pendingClient, byte[] ticket); + + protected void BanPendingClient(PendingClient pendingClient, string banReason, TimeSpan? duration) + { + if (pendingClient.Connection is LidgrenConnection l) + { + serverSettings.BanList.BanPlayer(pendingClient.Name, l.NetConnection.RemoteEndPoint.Address, banReason, duration); + } + else if (pendingClient.Connection is SteamP2PConnection s) + { + serverSettings.BanList.BanPlayer(pendingClient.Name, s.SteamID, banReason, duration); + } + } + + protected bool IsPendingClientBanned(PendingClient pendingClient, out string banReason) + { + if (pendingClient.Connection is LidgrenConnection l) + { + return serverSettings.BanList.IsBanned(l.NetConnection.RemoteEndPoint.Address, out banReason); + } + else if (pendingClient.Connection is SteamP2PConnection s) + { + return serverSettings.BanList.IsBanned(s.SteamID, out banReason); + } + banReason = null; + return false; + } + + protected abstract void SendMsgInternal(NetworkConnection conn, DeliveryMethod deliveryMethod, IWriteMessage msg); + + protected void UpdatePendingClient(PendingClient pendingClient) + { + if (IsPendingClientBanned(pendingClient, out string banReason)) + { + RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason); + return; + } + + if (connectedClients.Count >= serverSettings.MaxPlayers - 1) + { + RemovePendingClient(pendingClient, DisconnectReason.ServerFull, ""); + } + + if (pendingClient.InitializationStep == ConnectionInitialization.Success) + { + NetworkConnection newConnection = pendingClient.Connection; + connectedClients.Add(newConnection); + pendingClients.Remove(pendingClient); + + CheckOwnership(pendingClient); + + OnInitializationComplete?.Invoke(newConnection); + } + + pendingClient.TimeOut -= Timing.Step; + if (pendingClient.TimeOut < 0.0) + { + RemovePendingClient(pendingClient, DisconnectReason.Unknown, Lidgren.Network.NetConnection.NoResponseMessage); + } + + if (Timing.TotalTime < pendingClient.UpdateTime) { return; } + pendingClient.UpdateTime = Timing.TotalTime + 1.0; + + IWriteMessage outMsg = new WriteOnlyMessage(); + outMsg.Write((byte)(PacketHeader.IsConnectionInitializationStep | + PacketHeader.IsServerMessage)); + outMsg.Write((byte)pendingClient.InitializationStep); + switch (pendingClient.InitializationStep) + { + case ConnectionInitialization.ContentPackageOrder: + outMsg.Write(GameMain.Server.ServerName); + + var mpContentPackages = GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList(); + outMsg.WriteVariableUInt32((UInt32)mpContentPackages.Count); + for (int i = 0; i < mpContentPackages.Count; i++) + { + outMsg.Write(mpContentPackages[i].Name); + outMsg.Write(mpContentPackages[i].MD5hash.Hash); + outMsg.Write(mpContentPackages[i].SteamWorkshopId); + } + break; + case ConnectionInitialization.Password: + outMsg.Write(pendingClient.PasswordSalt == null); outMsg.WritePadBits(); + if (pendingClient.PasswordSalt == null) + { + pendingClient.PasswordSalt = Lidgren.Network.CryptoRandom.Instance.Next(); + outMsg.Write(pendingClient.PasswordSalt.Value); + } + else + { + outMsg.Write(pendingClient.Retries); + } + break; + } + + SendMsgInternal(pendingClient.Connection, DeliveryMethod.Reliable, outMsg); + } + + protected virtual void CheckOwnership(PendingClient pendingClient) { } + + protected void RemovePendingClient(PendingClient pendingClient, DisconnectReason reason, string msg) + { + if (pendingClients.Contains(pendingClient)) + { + Disconnect(pendingClient.Connection, reason + "/" + msg); + + pendingClients.Remove(pendingClient); + + if (pendingClient.AuthSessionStarted) + { + Steam.SteamManager.StopAuthSession(pendingClient.SteamID.Value); + pendingClient.SteamID = null; + pendingClient.AuthSessionStarted = false; + } + } + } public abstract void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod); public abstract void Disconnect(NetworkConnection conn, string msg = null); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs index 62898a9c2..56dda28c4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs @@ -10,50 +10,17 @@ namespace Barotrauma.Networking { private bool started; - private ServerSettings serverSettings; - public UInt64 OwnerSteamID { get; private set; } - - private class PendingClient - { - public string Name; - public ConnectionInitialization InitializationStep; - public double UpdateTime; - public double TimeOut; - public int Retries; - public UInt64 SteamID; - public Int32? PasswordSalt; - public bool AuthSessionStarted; - - public PendingClient(UInt64 steamId) - { - InitializationStep = ConnectionInitialization.SteamTicketAndVersion; - Retries = 0; - SteamID = steamId; - PasswordSalt = null; - UpdateTime = Timing.TotalTime+Timing.Step*3.0; - TimeOut = NetworkConnection.TimeoutThreshold; - AuthSessionStarted = false; - } - - public void Heartbeat() - { - TimeOut = NetworkConnection.TimeoutThreshold; - } - } - - private List connectedClients; - private List pendingClients; public SteamP2PServerPeer(UInt64 steamId, ServerSettings settings) { serverSettings = settings; - connectedClients = new List(); + connectedClients = new List(); pendingClients = new List(); ownerKey = null; @@ -114,10 +81,11 @@ namespace Barotrauma.Networking //backwards for loop so we can remove elements while iterating for (int i = connectedClients.Count - 1; i >= 0; i--) { - connectedClients[i].Decay(deltaTime); - if (connectedClients[i].Timeout < 0.0) + SteamP2PConnection conn = connectedClients[i] as SteamP2PConnection; + conn.Decay(deltaTime); + if (conn.Timeout < 0.0) { - Disconnect(connectedClients[i], "Timed out"); + Disconnect(conn, "Timed out"); } } @@ -172,7 +140,7 @@ namespace Barotrauma.Networking if (senderSteamId != OwnerSteamID) //sender is remote, handle disconnects and heartbeats { PendingClient pendingClient = pendingClients.Find(c => c.SteamID == senderSteamId); - SteamP2PConnection connectedClient = connectedClients.Find(c => c.SteamID == senderSteamId); + SteamP2PConnection connectedClient = connectedClients.Find(c => c.SteamID == senderSteamId) as SteamP2PConnection; pendingClient?.Heartbeat(); connectedClient?.Heartbeat(); @@ -210,6 +178,7 @@ namespace Barotrauma.Networking } else if (isConnectionInitializationStep) { + if (pendingClient != null) { ReadConnectionInitializationStep(pendingClient, new ReadOnlyMessage(inc.Buffer, false, inc.BytePosition, inc.LengthBytes - inc.BytePosition, null)); @@ -219,7 +188,7 @@ namespace Barotrauma.Networking ConnectionInitialization initializationStep = (ConnectionInitialization)inc.ReadByte(); if (initializationStep == ConnectionInitialization.ConnectionStarted) { - pendingClients.Add(new PendingClient(senderSteamId)); + pendingClients.Add(new PendingClient(new SteamP2PConnection("PENDING", senderSteamId)) { SteamID = senderSteamId }); } } } @@ -252,7 +221,7 @@ namespace Barotrauma.Networking string ownerName = inc.ReadString(); OwnerConnection = new SteamP2PConnection(ownerName, OwnerSteamID) { - Status = NetworkConnectionStatus.Connected + Language = GameMain.Config.Language }; OnInitializationComplete?.Invoke(OwnerConnection); @@ -273,246 +242,6 @@ namespace Barotrauma.Networking } } - private void ReadConnectionInitializationStep(PendingClient pendingClient, IReadMessage inc) - { - if (!started) { return; } - - pendingClient.TimeOut = NetworkConnection.TimeoutThreshold; - - ConnectionInitialization initializationStep = (ConnectionInitialization)inc.ReadByte(); - - //DebugConsole.NewMessage(initializationStep+" "+pendingClient.InitializationStep); - - if (pendingClient.InitializationStep != initializationStep) return; - - pendingClient.UpdateTime = Timing.TotalTime+Timing.Step; - - switch (initializationStep) - { - case ConnectionInitialization.SteamTicketAndVersion: - string name = Client.SanitizeName(inc.ReadString()); - UInt64 steamId = inc.ReadUInt64(); - UInt16 ticketLength = inc.ReadUInt16(); - inc.BitPosition += ticketLength * 8; //skip ticket, owner handles steam authentication - - if (!Client.IsValidName(name, serverSettings)) - { - RemovePendingClient(pendingClient, DisconnectReason.InvalidName, "The name \"" + name + "\" is invalid"); - return; - } - - string version = inc.ReadString(); - bool isCompatibleVersion = NetworkMember.IsCompatible(version, GameMain.Version.ToString()) ?? false; - if (!isCompatibleVersion) - { - RemovePendingClient(pendingClient, DisconnectReason.InvalidVersion, - $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version}~[clientversion]={version}"); - - GameServer.Log(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); - DebugConsole.NewMessage(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red); - return; - } - - Client nameTaken = GameMain.Server.ConnectedClients.Find(c => Homoglyphs.Compare(c.Name.ToLower(), name.ToLower())); - if (nameTaken != null) - { - RemovePendingClient(pendingClient, DisconnectReason.NameTaken, ""); - GameServer.Log(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (name too similar to the name of the client \"" + nameTaken.Name + "\").", ServerLog.MessageType.Error); - return; - } - - int contentPackageCount = (int)inc.ReadVariableUInt32(); - List clientContentPackages = new List(); - for (int i = 0; i < contentPackageCount; i++) - { - string packageName = inc.ReadString(); - string packageHash = inc.ReadString(); - clientContentPackages.Add(new ClientContentPackage(packageName, packageHash)); - } - - //check if the client is missing any of our packages - List missingPackages = new List(); - foreach (ContentPackage serverContentPackage in GameMain.SelectedPackages) - { - if (!serverContentPackage.HasMultiplayerIncompatibleContent) continue; - bool packageFound = clientContentPackages.Any(cp => cp.Name == serverContentPackage.Name && cp.Hash == serverContentPackage.MD5hash.Hash); - if (!packageFound) { missingPackages.Add(serverContentPackage); } - } - - //check if the client is using packages we don't have - List redundantPackages = new List(); - foreach (ClientContentPackage clientContentPackage in clientContentPackages) - { - bool packageFound = GameMain.SelectedPackages.Any(cp => cp.Name == clientContentPackage.Name && cp.MD5hash.Hash == clientContentPackage.Hash); - if (!packageFound) { redundantPackages.Add(clientContentPackage); } - } - - if (missingPackages.Count == 1) - { - RemovePendingClient(pendingClient, DisconnectReason.MissingContentPackage, - $"DisconnectMessage.MissingContentPackage~[missingcontentpackage]={GetPackageStr(missingPackages[0])}"); - GameServer.Log(name + " (" + pendingClient.SteamID + ") couldn't join the server (missing content package " + GetPackageStr(missingPackages[0]) + ")", ServerLog.MessageType.Error); - return; - } - else if (missingPackages.Count > 1) - { - List packageStrs = new List(); - missingPackages.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); - RemovePendingClient(pendingClient, DisconnectReason.MissingContentPackage, - $"DisconnectMessage.MissingContentPackages~[missingcontentpackages]={string.Join(", ", packageStrs)}"); - GameServer.Log(name + " (" + pendingClient.SteamID + ") couldn't join the server (missing content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error); - return; - } - if (redundantPackages.Count == 1) - { - RemovePendingClient(pendingClient, DisconnectReason.IncompatibleContentPackage, - $"DisconnectMessage.IncompatibleContentPackage~[incompatiblecontentpackage]={GetPackageStr(redundantPackages[0])}"); - GameServer.Log(name + " (" + pendingClient.SteamID + ") couldn't join the server (using an incompatible content package " + GetPackageStr(redundantPackages[0]) + ")", ServerLog.MessageType.Error); - return; - } - if (redundantPackages.Count > 1) - { - List packageStrs = new List(); - redundantPackages.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); - RemovePendingClient(pendingClient, DisconnectReason.IncompatibleContentPackage, - $"DisconnectMessage.IncompatibleContentPackages~[incompatiblecontentpackages]={string.Join(", ", packageStrs)}"); - GameServer.Log(name + " (" + pendingClient.SteamID + ") couldn't join the server (using incompatible content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error); - return; - } - - if (!pendingClient.AuthSessionStarted) - { - pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; - - pendingClient.Name = name; - pendingClient.AuthSessionStarted = true; - } - break; - case ConnectionInitialization.Password: - int pwLength = inc.ReadByte(); - byte[] incPassword = inc.ReadBytes(pwLength); - if (pendingClient.PasswordSalt == null) - { - DebugConsole.ThrowError("Received password message from client without salt"); - return; - } - if (serverSettings.IsPasswordCorrect(incPassword, pendingClient.PasswordSalt.Value)) - { - pendingClient.InitializationStep = ConnectionInitialization.ContentPackageOrder; - } - else - { - pendingClient.Retries++; - if (serverSettings.BanAfterWrongPassword && pendingClient.Retries > serverSettings.MaxPasswordRetriesBeforeBan) - { - string banMsg = "Failed to enter correct password too many times"; - serverSettings.BanList.BanPlayer(pendingClient.Name, pendingClient.SteamID, banMsg, null); - - RemovePendingClient(pendingClient, DisconnectReason.Banned, banMsg); - return; - } - } - pendingClient.UpdateTime = Timing.TotalTime; - break; - case ConnectionInitialization.ContentPackageOrder: - pendingClient.InitializationStep = ConnectionInitialization.Success; - pendingClient.UpdateTime = Timing.TotalTime; - break; - } - } - - - private void UpdatePendingClient(PendingClient pendingClient) - { - if (!started) { return; } - - if (serverSettings.BanList.IsBanned(pendingClient.SteamID, out string banReason)) - { - RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason); - return; - } - - //DebugConsole.NewMessage("pending client status: " + pendingClient.InitializationStep); - - if (connectedClients.Count >= serverSettings.MaxPlayers - 1) - { - RemovePendingClient(pendingClient, DisconnectReason.ServerFull, ""); - } - - if (pendingClient.InitializationStep == ConnectionInitialization.Success) - { - SteamP2PConnection newConnection = new SteamP2PConnection(pendingClient.Name, pendingClient.SteamID) - { - Status = NetworkConnectionStatus.Connected - }; - connectedClients.Add(newConnection); - pendingClients.Remove(pendingClient); - OnInitializationComplete?.Invoke(newConnection); - } - - pendingClient.TimeOut -= Timing.Step; - if (pendingClient.TimeOut < 0.0) - { - RemovePendingClient(pendingClient, DisconnectReason.Unknown, Lidgren.Network.NetConnection.NoResponseMessage); - } - - if (Timing.TotalTime < pendingClient.UpdateTime) { return; } - pendingClient.UpdateTime = Timing.TotalTime + 1.0; - - IWriteMessage outMsg = new WriteOnlyMessage(); - outMsg.Write(pendingClient.SteamID); - outMsg.Write((byte)DeliveryMethod.Reliable); - outMsg.Write((byte)(PacketHeader.IsConnectionInitializationStep | - PacketHeader.IsServerMessage)); - outMsg.Write((byte)pendingClient.InitializationStep); - switch (pendingClient.InitializationStep) - { - case ConnectionInitialization.ContentPackageOrder: - var mpContentPackages = GameMain.SelectedPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList(); - outMsg.WriteVariableUInt32((UInt32)mpContentPackages.Count); - for (int i = 0; i < mpContentPackages.Count; i++) - { - outMsg.Write(mpContentPackages[i].MD5hash.Hash); - } - break; - case ConnectionInitialization.Password: - outMsg.Write(pendingClient.PasswordSalt == null); outMsg.WritePadBits(); - if (pendingClient.PasswordSalt == null) - { - pendingClient.PasswordSalt = Lidgren.Network.CryptoRandom.Instance.Next(); - outMsg.Write(pendingClient.PasswordSalt.Value); - } - else - { - outMsg.Write(pendingClient.Retries); - } - break; - } - - byte[] msgToSend = (byte[])outMsg.Buffer.Clone(); - Array.Resize(ref msgToSend, outMsg.LengthBytes); - ChildServerRelay.Write(msgToSend); - } - - private void RemovePendingClient(PendingClient pendingClient, DisconnectReason reason, string msg) - { - if (!started) { return; } - - if (pendingClients.Contains(pendingClient)) - { - SendDisconnectMessage(pendingClient.SteamID, reason + "/" + msg); - - pendingClients.Remove(pendingClient); - - if (pendingClient.AuthSessionStarted) - { - Steam.SteamManager.StopAuthSession(pendingClient.SteamID); - pendingClient.SteamID = 0; - pendingClient.AuthSessionStarted = false; - } - } - } - public override void InitializeSteamServerCallbacks() { throw new InvalidOperationException("Called InitializeSteamServerCallbacks on SteamP2PServerPeer!"); @@ -582,5 +311,25 @@ namespace Barotrauma.Networking { Disconnect(conn, msg, true); } + + protected override void SendMsgInternal(NetworkConnection conn, DeliveryMethod deliveryMethod, IWriteMessage msg) + { + IWriteMessage msgToSend = new WriteOnlyMessage(); + msgToSend.Write(conn.SteamID); + msgToSend.Write((byte)deliveryMethod); + msgToSend.Write(msg.Buffer, 0, msg.LengthBytes); + byte[] bufToSend = (byte[])msgToSend.Buffer.Clone(); + Array.Resize(ref bufToSend, msgToSend.LengthBytes); + ChildServerRelay.Write(bufToSend); + } + + protected override void ProcessAuthTicket(string name, int ownKey, ulong steamId, PendingClient pendingClient, byte[] ticket) + { + pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; + + pendingClient.Connection.Name = name; + pendingClient.Name = name; + pendingClient.AuthSessionStarted = true; + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index f86579619..dcbeafe5b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -1,5 +1,4 @@ using Barotrauma.Items.Components; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -9,12 +8,27 @@ namespace Barotrauma.Networking { partial class RespawnManager : Entity, IServerSerializable { - private List GetClientsToRespawn() + private DateTime despawnTime; + + private IEnumerable GetClientsToRespawn() { - return networkMember.ConnectedClients.FindAll(c => - c.InGame && - (!c.SpectateOnly || (!GameMain.Server.ServerSettings.AllowSpectating && GameMain.Server.OwnerConnection != c.Connection)) && - (c.Character == null || c.Character.IsDead)); + MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign; + foreach (Client c in networkMember.ConnectedClients) + { + if (!c.InGame) { continue; } + if (c.SpectateOnly && (GameMain.Server.ServerSettings.AllowSpectating || GameMain.Server.OwnerConnection == c.Connection)) { continue; } + if (c.Character != null && !c.Character.IsDead) { continue; } + + //don't allow respawning if the client has previously disconnected and their corpse is still present on the server + var matchingData = campaign?.GetClientCharacterData(c); + if (matchingData != null && matchingData.HasSpawned && + Character.CharacterList.Any(c => c.Info == matchingData.CharacterInfo && c.CauseOfDeath?.Type == CauseOfDeathType.Disconnected)) + { + continue; + } + + yield return c; + } } private List GetBotsToRespawn() @@ -56,7 +70,7 @@ namespace Barotrauma.Networking private bool RespawnPending() { - int characterToRespawnCount = GetClientsToRespawn().Count; + int characterToRespawnCount = GetClientsToRespawn().Count(); int totalCharacterCount = GameMain.Server.ConnectedClients.Count; return (float)characterToRespawnCount >= Math.Max((float)totalCharacterCount * GameMain.Server.ServerSettings.MinRespawnRatio, 1.0f); } @@ -82,15 +96,9 @@ namespace Barotrauma.Networking if (RespawnShuttle == null) { return; } RespawnShuttle.Velocity = Vector2.Zero; - - if (shuttleSteering != null) - { - shuttleSteering.AutoPilot = false; - shuttleSteering.MaintainPos = false; - } } - partial void DispatchShuttle() + private void DispatchShuttle() { if (RespawnShuttle != null) { @@ -118,13 +126,8 @@ namespace Barotrauma.Networking { RespawnShuttle.SetPosition(spawnPos); RespawnShuttle.Velocity = Vector2.Zero; - if (shuttleSteering != null) - { - shuttleSteering.AutoPilot = true; - shuttleSteering.MaintainPos = true; - shuttleSteering.PosToMaintain = RespawnShuttle.WorldPosition; - shuttleSteering.UnsentChanges = true; - } + RespawnShuttle.NeutralizeBallast(); + RespawnShuttle.EnableMaintainPosition(); } } else @@ -219,12 +222,20 @@ namespace Barotrauma.Networking { var respawnSub = RespawnShuttle ?? Submarine.MainSub; - var clients = GetClientsToRespawn(); + MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign; + + var clients = GetClientsToRespawn().ToList(); foreach (Client c in clients) { //get rid of the existing character c.Character?.DespawnNow(); + var matchingData = campaign?.GetClientCharacterData(c); + if (matchingData != null && !matchingData.HasSpawned) + { + c.CharacterInfo = matchingData.CharacterInfo; + } + //all characters are in Team 1 in game modes/missions with only one team. //if at some point we add a game mode with multiple teams where respawning is possible, this needs to be reworked c.TeamID = Character.TeamType.Team1; @@ -238,7 +249,10 @@ namespace Barotrauma.Networking GameMain.Server.AssignJobs(clients); foreach (Client c in clients) { - c.CharacterInfo.Job = new Job(c.AssignedJob.First, c.AssignedJob.Second); + if (campaign?.GetClientCharacterData(c) == null || c.CharacterInfo.Job == null) + { + c.CharacterInfo.Job = new Job(c.AssignedJob.First, c.AssignedJob.Second); + } } //the spawnpoints where the characters will spawn @@ -314,19 +328,37 @@ namespace Barotrauma.Networking } } - //give the character the items they would've gotten if they had spawned in the main sub - character.GiveJobItems(mainSubSpawnPoints[i]); + var characterData = campaign?.GetClientCharacterData(clients[i]); + if (characterData == null || characterData.HasSpawned) + { + //give the character the items they would've gotten if they had spawned in the main sub + character.GiveJobItems(mainSubSpawnPoints[i]); + if (campaign != null) + { + characterData = campaign.SetClientCharacterData(clients[i]); + characterData.HasSpawned = true; + } + } + else + { + characterData.SpawnInventoryItems(character.Info, character.Inventory); + characterData.ApplyHealthData(character.Info, character); + character.GiveIdCardTags(mainSubSpawnPoints[i]); + characterData.HasSpawned = true; + } //add the ID card tags they should've gotten when spawning in the shuttle foreach (Item item in character.Inventory.Items) { - if (item == null || item.Prefab.Identifier != "idcard") continue; + if (item == null || item.Prefab.Identifier != "idcard") { continue; } foreach (string s in shuttleSpawnPoints[i].IdCardTags) { item.AddTag(s); } if (!string.IsNullOrWhiteSpace(shuttleSpawnPoints[i].IdCardDesc)) + { item.Description = shuttleSpawnPoints[i].IdCardDesc; + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/SteamManager.cs index ffba7cc24..beb842ec4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/SteamManager.cs @@ -8,8 +8,6 @@ namespace Barotrauma.Steam private static void InitializeProjectSpecific() { isInitialized = true; } - private static void UpdateProjectSpecific(float deltaTime) { } - public static bool CreateServer(Networking.GameServer server, bool isPublic) { isInitialized = true; @@ -17,8 +15,8 @@ namespace Barotrauma.Steam Steamworks.SteamServerInit options = new Steamworks.SteamServerInit("Barotrauma", "Barotrauma") { GamePort = (ushort)server.Port, - QueryPort = (ushort)server.QueryPort, - Secure = false + QueryPort = isPublic ? (ushort)server.QueryPort : (ushort)0, + Mode = isPublic ? Steamworks.InitServerMode.Authentication : Steamworks.InitServerMode.NoAuthentication }; //options.QueryShareGamePort(); @@ -46,7 +44,7 @@ namespace Barotrauma.Steam return false; } - var contentPackages = GameMain.Config.SelectedContentPackages.Where(cp => cp.HasMultiplayerIncompatibleContent); + var contentPackages = GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent); // These server state variables may be changed at any time. Note that there is no longer a mechanism // to send the player count. The player count is maintained by steam and you should use the player @@ -55,12 +53,13 @@ namespace Barotrauma.Steam Steamworks.SteamServer.MaxPlayers = server.ServerSettings.MaxPlayers; Steamworks.SteamServer.Passworded = server.ServerSettings.HasPassword; Steamworks.SteamServer.MapName = GameMain.NetLobbyScreen?.SelectedSub?.DisplayName ?? ""; + Steamworks.SteamServer.SetKey("haspassword", server.ServerSettings.HasPassword.ToString()); Steamworks.SteamServer.SetKey("message", GameMain.Server.ServerSettings.ServerMessageText); Steamworks.SteamServer.SetKey("version", GameMain.Version.ToString()); Steamworks.SteamServer.SetKey("playercount", GameMain.Server.ConnectedClients.Count.ToString()); Steamworks.SteamServer.SetKey("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); Steamworks.SteamServer.SetKey("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.MD5hash.Hash))); - Steamworks.SteamServer.SetKey("contentpackageurl", string.Join(",", contentPackages.Select(cp => cp.SteamWorkshopUrl ?? ""))); + Steamworks.SteamServer.SetKey("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.SteamWorkshopId))); Steamworks.SteamServer.SetKey("usingwhitelist", (server.ServerSettings.Whitelist != null && server.ServerSettings.Whitelist.Enabled).ToString()); Steamworks.SteamServer.SetKey("modeselectionmode", server.ServerSettings.ModeSelectionMode.ToString()); Steamworks.SteamServer.SetKey("subselectionmode", server.ServerSettings.SubSelectionMode.ToString()); @@ -70,6 +69,7 @@ namespace Barotrauma.Steam Steamworks.SteamServer.SetKey("traitors", server.ServerSettings.TraitorsEnabled.ToString()); Steamworks.SteamServer.SetKey("gamestarted", server.GameStarted.ToString()); Steamworks.SteamServer.SetKey("gamemode", server.ServerSettings.GameModeIdentifier); + Steamworks.SteamServer.SetKey("playstyle", server.ServerSettings.PlayStyle.ToString()); Steamworks.SteamServer.DedicatedServer = true; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index c44c8315d..f00dec7dd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -82,20 +82,18 @@ namespace Barotrauma switch (voteType) { case VoteType.Sub: - string subName = inc.ReadString(); - SubmarineInfo sub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName); + int equalityCheckVal = inc.ReadInt32(); + SubmarineInfo sub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.EqualityCheckVal == equalityCheckVal); sender.SetVote(voteType, sub); break; - case VoteType.Mode: string modeIdentifier = inc.ReadString(); GameModePreset mode = GameModePreset.List.Find(gm => gm.Identifier == modeIdentifier); - if (!mode.Votable) break; - + if (!mode.Votable) { break; } sender.SetVote(voteType, mode); break; case VoteType.EndRound: - if (!sender.HasSpawned) return; + if (!sender.HasSpawned) { return; } sender.SetVote(voteType, inc.ReadBoolean()); GameMain.NetworkMember.EndVoteCount = GameMain.Server.ConnectedClients.Count(c => c.HasSpawned && c.GetVote(VoteType.EndRound)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index a493779da..2226c770b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -43,6 +43,8 @@ namespace Barotrauma Console.WriteLine("Barotrauma Dedicated Server " + GameMain.Version + " (" + AssemblyInfo.GetBuildString() + ", branch " + AssemblyInfo.GetGitBranch() + ", revision " + AssemblyInfo.GetGitRevision() + ")"); + string executableDir = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location); + Directory.SetCurrentDirectory(executableDir); Game = new GameMain(args); Game.Run(); @@ -102,9 +104,9 @@ namespace Barotrauma { sb.AppendLine("Language: " + (GameMain.Config.Language ?? "none")); } - if (GameMain.SelectedPackages != null) + if (GameMain.Config.AllEnabledPackages != null) { - sb.AppendLine("Selected content packages: " + (!GameMain.SelectedPackages.Any() ? "None" : string.Join(", ", GameMain.SelectedPackages.Select(c => c.Name)))); + sb.AppendLine("Selected content packages: " + (!GameMain.Config.AllEnabledPackages.Any() ? "None" : string.Join(", ", GameMain.Config.AllEnabledPackages.Select(c => c.Name)))); } sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed)); sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")")); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalInjectTarget.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalInjectTarget.cs index b33b65336..10cc8f56a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalInjectTarget.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalInjectTarget.cs @@ -34,6 +34,8 @@ namespace Barotrauma private bool WereAllTargetsInfected() { + if (targetWasInfected == null) { return false; } + for (int i = 0; i < targetWasInfected.Length; i++) { if (targetWasInfected[i]) continue; @@ -52,8 +54,12 @@ namespace Barotrauma poisonName = TextManager.FormatServerMessage(poisonId) ?? poisonId; Targets = traitor.Mission.FindKillTarget(traitor.Character, Filter, targetCount, targetPercentage); + if (Targets == null) + { + return false; + } targetWasInfected = new bool[Targets.Count]; - return Targets != null && !Targets.All(t => t.IsDead); + return !Targets.All(t => t.IsDead); } public GoalInjectTarget(TraitorMission.CharacterFilter filter, string poisonId, string afflictionId, int targetCount, float targetPercentage) : base() diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs index 22bf9dc39..a9eb9cb52 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs @@ -137,6 +137,10 @@ namespace Barotrauma startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)RandomDouble()); return; } + if (Character.CharacterList.Count(c => !c.IsDead && c.TeamID == Character.TeamType.Team1 || c.TeamID == Character.TeamType.Team2) <= 1) + { + return; + } if (GameMain.GameSession.Mission is CombatMission) { var teamIds = new[] { Character.TeamType.Team1, Character.TeamType.Team2 }; diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index bedd8bbe0..079efed39 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.10.4.0 + 0.10.5.1 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer @@ -60,6 +60,7 @@ + diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index 48f2064f0..683e82d3c 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -54,6 +54,9 @@ + + + @@ -107,6 +110,7 @@ + @@ -163,7 +167,6 @@ - diff --git a/Barotrauma/BarotraumaShared/Data/permissionpresets.xml b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml index d06d1d321..8b60010c0 100644 --- a/Barotrauma/BarotraumaShared/Data/permissionpresets.xml +++ b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml @@ -67,6 +67,7 @@ + @@ -88,6 +89,7 @@ + diff --git a/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs b/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs index 051acd988..1a578ebf4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs @@ -90,6 +90,19 @@ namespace Barotrauma yield return CoroutineStatus.Success; } + //switched control to some other character during the transition -> remove control again + if (Character.Controlled != null) + { + prevControlled = Character.Controlled; + if (RemoveControlFromCharacter) + { +#if CLIENT + GameMain.LightManager.LosEnabled = false; +#endif + Character.Controlled = null; + } + } + if (prevControlled != null && prevControlled.Removed) { prevControlled = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index b0bdc41e9..ac64ac356 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -13,12 +13,6 @@ namespace Barotrauma private AIState state; - protected void ResetAITarget() - { - _lastAiTarget = null; - _selectedAiTarget = null; - } - // Update only when the value changes, not when it keeps the same. protected AITarget _lastAiTarget; // Updated each time the value is updated (also when the value is the same). @@ -142,6 +136,17 @@ namespace Barotrauma } } + public virtual void Reset() + { + ResetAITarget(); + } + + protected void ResetAITarget() + { + _lastAiTarget = null; + _selectedAiTarget = null; + } + protected virtual void OnStateChanged(AIState from, AIState to) { } protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 8fc931e48..c6efd4fcb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -153,6 +153,11 @@ namespace Barotrauma { throw new Exception($"Tried to create an enemy ai controller for human!"); } + if (Character.Params.Group.Equals("human", StringComparison.OrdinalIgnoreCase)) + { + // Pet + Character.TeamID = Character.TeamType.FriendlyNPC; + } CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(c.SpeciesName); var mainElement = prefab.XDocument.Root.IsOverride() ? prefab.XDocument.Root.FirstElement() : prefab.XDocument.Root; targetMemories = new Dictionary(); @@ -733,7 +738,7 @@ namespace Barotrauma { if (door.LinkedGap.Size > ConvertUnits.ToDisplayUnits(colliderWidth)) { - LatchOntoAI?.DeattachFromBody(); + LatchOntoAI?.DeattachFromBody(cooldown: 2); Character.AnimController.ReleaseStuckLimbs(); var velocity = Vector2.Normalize(door.LinkedGap.FlowTargetHull.WorldPosition - Character.WorldPosition); steeringManager.SteeringManual(deltaTime, velocity); @@ -1121,7 +1126,7 @@ namespace Barotrauma { IsSteeringThroughGap = true; wallTarget = null; - LatchOntoAI?.DeattachFromBody(); + LatchOntoAI?.DeattachFromBody(cooldown: 2); Character.AnimController.ReleaseStuckLimbs(); Hull targetHull = section.gap?.FlowTargetHull; float maxDistance = Math.Min(wall.Rect.Width, wall.Rect.Height); @@ -1303,7 +1308,7 @@ namespace Barotrauma bool wasLatched = IsLatchedOnSub; Character.AnimController.ReleaseStuckLimbs(); - LatchOntoAI?.DeattachFromBody(); + LatchOntoAI?.DeattachFromBody(cooldown: 1); if (attacker == null || attacker.AiTarget == null) { return; } bool isFriendly = IsFriendly(Character, attacker); if (wasLatched) @@ -1544,39 +1549,6 @@ namespace Barotrauma //sight/hearing range public AITarget UpdateTargets(Character character, out CharacterParams.TargetParams targetingParams) { - if ((SelectedAiTarget != null || wallTarget != null) && IsLatchedOnSub) - { - var wall = SelectedAiTarget.Entity as Structure; - if (wall == null) - { - wall = wallTarget?.Structure; - } - // The target is not a wall or it's not the same as we are attached to -> release - bool releaseTarget = wall == null || !wall.Bodies.Contains(LatchOntoAI.AttachJoints[0].BodyB); - if (!releaseTarget) - { - for (int i = 0; i < wall.Sections.Length; i++) - { - if (CanPassThroughHole(wall, i)) - { - releaseTarget = true; - } - } - } - if (releaseTarget) - { - SelectedAiTarget = null; - wallTarget = null; - LatchOntoAI.DeattachFromBody(cooldown: 1); - } - else if (SelectedAiTarget?.Entity == wallTarget?.Structure) - { - // If attached to a valid target, just keep the target. - // Priority not used in this case. - targetingParams = null; - return SelectedAiTarget; - } - } AITarget newTarget = null; targetValue = 0; selectedTargetMemory = null; @@ -1611,42 +1583,44 @@ namespace Barotrauma { targetingTag = tP.Tag; } - else if (targetCharacter.AIController is EnemyAIController enemy) + else { - if (targetCharacter.Params.CompareGroup(Character.Params.Group)) + if (IsFriendly(Character, targetCharacter)) { - // Ignore targets that are in the same group (treat them like they were of the same species) continue; } - if (targetCharacter.IsHusk && AIParams.HasTag("husk")) + if (targetCharacter.AIController is EnemyAIController enemy) { - targetingTag = "husk"; - } - else - { - if (enemy.CombatStrength > CombatStrength) + if (targetCharacter.IsHusk && AIParams.HasTag("husk")) { - targetingTag = "stronger"; + targetingTag = "husk"; } - else if (enemy.CombatStrength < CombatStrength) + else { - targetingTag = "weaker"; - } - if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee)) - { - if (SelectedAiTarget == aiTarget) + if (enemy.CombatStrength > CombatStrength) { - // Freightened -> hold on to the target - valueModifier *= 2; + targetingTag = "stronger"; } - if (IsBeingChasedBy(targetCharacter)) + else if (enemy.CombatStrength < CombatStrength) { - valueModifier *= 2; + targetingTag = "weaker"; } - if (Character.CurrentHull != null && !VisibleHulls.Contains(targetCharacter.CurrentHull)) + if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee)) { - // Inside but in a different room - valueModifier /= 2; + if (SelectedAiTarget == aiTarget) + { + // Freightened -> hold on to the target + valueModifier *= 2; + } + if (IsBeingChasedBy(targetCharacter)) + { + valueModifier *= 2; + } + if (Character.CurrentHull != null && !VisibleHulls.Contains(targetCharacter.CurrentHull)) + { + // Inside but in a different room + valueModifier /= 2; + } } } } @@ -1855,25 +1829,28 @@ namespace Barotrauma if (valueModifier == 0.0f) { continue; } - if (SwarmBehavior != null && SwarmBehavior.Members.Any()) + if (targetingTag != "decoy") { - // Halve the priority for each swarm mate targeting the same target -> reduces stacking - foreach (Character otherCharacter in SwarmBehavior.Members) + if (SwarmBehavior != null && SwarmBehavior.Members.Any()) { - if (otherCharacter == character) { continue; } - if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; } - valueModifier /= 2; + // Halve the priority for each swarm mate targeting the same target -> reduces stacking + foreach (Character otherCharacter in SwarmBehavior.Members) + { + if (otherCharacter == character) { continue; } + if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; } + valueModifier /= 2; + } } - } - else - { - // The same as above, but using all the friendly characters in the level. - foreach (Character otherCharacter in Character.CharacterList) + else { - if (otherCharacter == character) { continue; } - if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; } - if (!IsFriendly(character, otherCharacter)) { continue; } - valueModifier /= 2; + // The same as above, but using all the friendly characters in the level. + foreach (Character otherCharacter in Character.CharacterList) + { + if (otherCharacter == character) { continue; } + if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; } + if (!IsFriendly(character, otherCharacter)) { continue; } + valueModifier /= 2; + } } } @@ -1969,7 +1946,34 @@ namespace Barotrauma SelectedAiTarget = newTarget; if (SelectedAiTarget != _previousAiTarget) { - wallTarget = null; + if ((SelectedAiTarget != null || wallTarget != null) && IsLatchedOnSub) + { + if (!(SelectedAiTarget.Entity is Structure wall)) + { + wall = wallTarget?.Structure; + } + // The target is not a wall or it's not the same as we are attached to -> release + bool releaseTarget = wall == null || !wall.Bodies.Contains(LatchOntoAI.AttachJoints[0].BodyB); + if (!releaseTarget) + { + for (int i = 0; i < wall.Sections.Length; i++) + { + if (CanPassThroughHole(wall, i)) + { + releaseTarget = true; + } + } + } + if (releaseTarget) + { + wallTarget = null; + LatchOntoAI.DeattachFromBody(cooldown: 1); + } + } + else + { + wallTarget = null; + } } return SelectedAiTarget; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 9f8b5ebc0..9d2715b01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -12,7 +12,7 @@ namespace Barotrauma { public static bool DisableCrewAI; - private AIObjectiveManager objectiveManager; + private readonly AIObjectiveManager objectiveManager; private float sortTimer; private float crouchRaycastTimer; @@ -29,6 +29,7 @@ namespace Barotrauma private const float FlipInterval = 0.5f; public static float HULL_SAFETY_THRESHOLD = 50; + private static readonly float characterWaitOnSwitch = 5; public readonly HashSet UnreachableHulls = new HashSet(); public readonly HashSet UnsafeHulls = new HashSet(); @@ -90,8 +91,7 @@ namespace Barotrauma public float CurrentHullSafety { get; private set; } = 100; private readonly Dictionary damageDoneByAttacker = new Dictionary(); - private readonly List attackers = new List(); - + private readonly HashSet attackers = new HashSet(); public HumanAIController(Character c) : base(c) { @@ -106,11 +106,38 @@ namespace Barotrauma sortTimer = Rand.Range(0f, sortObjectiveInterval); InitProjSpecific(); } + partial void InitProjSpecific(); + private bool freezeAI; + public override void Update(float deltaTime) { - if (DisableCrewAI || Character.IsIncapacitated || Character.Removed) { return; } + if (DisableCrewAI || Character.Removed) { return; } + + //slowly forget about damage done by attackers + foreach (Character enemy in attackers) + { + float cumulativeDamage = damageDoneByAttacker[enemy]; + if (cumulativeDamage > 0) + { + float reduction = deltaTime; + if (cumulativeDamage < 2) + { + // If the damage is very low, let's not forget so quickly, or we can't cumulate the damage from repair tools (high frequency, low damage) + reduction *= 0.5f; + } + damageDoneByAttacker[enemy] -= reduction; + } + } + + bool isIncapacitated = Character.IsIncapacitated; + if (freezeAI && !isIncapacitated) + { + freezeAI = false; + } + if (isIncapacitated) { return; } + base.Update(deltaTime); foreach (var values in knownHulls) @@ -164,15 +191,6 @@ namespace Barotrauma } objectiveManager.UpdateObjectives(deltaTime); - //slowly forget about damage done by attackers - foreach (Character enemy in attackers) - { - if (damageDoneByAttacker[enemy] > 0) - { - damageDoneByAttacker[enemy] -= deltaTime * 0.01f; - } - } - if (reactTimer > 0.0f) { reactTimer -= deltaTime; @@ -277,7 +295,10 @@ namespace Barotrauma { newDir = Direction.Left; } - if (Character.SelectedConstruction != null) Character.SelectedConstruction.SecondaryUse(deltaTime, Character); + if (Character.SelectedConstruction != null) + { + Character.SelectedConstruction.SecondaryUse(deltaTime, Character); + } } else if (Math.Abs(Character.AnimController.TargetMovement.X) > 0.1f && !Character.AnimController.InWater) { @@ -295,53 +316,49 @@ namespace Barotrauma { if (Character.LockHands) { return; } if (ObjectiveManager.CurrentObjective == null) { return; } - if (ObjectiveManager.HasActiveObjective()) { return; } - if (findItemState == FindItemState.None || findItemState == FindItemState.Extinguisher) + bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold; + bool isCarrying = ObjectiveManager.HasActiveObjective() || ObjectiveManager.HasActiveObjective(); + + bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective) { - if (!ObjectiveManager.IsCurrentObjective() && !objectiveManager.HasActiveObjective()) + bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty; + Hull targetHull = gotoObjective.GetTargetHull(); + return gotoObjective.Target != null && targetHull == null || + NeedsDivingGear(targetHull, out _) || + insideSteering && (PathSteering.CurrentPath.HasOutdoorsNodes || PathSteering.CurrentPath.Nodes.Any(n => NeedsDivingGear(n.CurrentHull, out _))); + } + + if (isCarrying) + { + if (findItemState != FindItemState.OtherItem) { - var extinguisher = Character.Inventory.FindItemByTag("extinguisher"); - if (extinguisher != null && Character.HasEquippedItem(extinguisher)) + if (ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && NeedsDivingGearOnPath(gotoObjective)) { - if (ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) - { - extinguisher.Drop(Character); - } - else - { - findItemState = FindItemState.Extinguisher; - if (FindSuitableContainer(extinguisher, out Item targetContainer)) - { - findItemState = FindItemState.None; - itemIndex = 0; - if (targetContainer != null) - { - var decontainObjective = new AIObjectiveDecontainItem(Character, extinguisher, ObjectiveManager, targetContainer: targetContainer.GetComponent()); - decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer); - ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); - return; - } - else - { - extinguisher.Drop(Character); - } - } - } + gotoObjective.Abandon = true; } } + if (!oxygenLow) + { + return; + } } - if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit || findItemState == FindItemState.DivingMask) + + // Diving gear + if (oxygenLow || findItemState != FindItemState.OtherItem) { - if (!NeedsDivingGear(Character, Character.CurrentHull, out _)) + if (!NeedsDivingGear(Character.CurrentHull, out bool needsSuit) || !needsSuit || oxygenLow) { bool shouldKeepTheGearOn = Character.AnimController.HeadInWater || ObjectiveManager.IsCurrentObjective() || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn); - bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold; - if (oxygenLow) + if (oxygenLow && Character.CurrentHull.Oxygen > 0) { shouldKeepTheGearOn = false; } + else if (Character.CurrentHull.Oxygen < CharacterHealth.LowOxygenThreshold) + { + shouldKeepTheGearOn = true; + } bool removeDivingSuit = !shouldKeepTheGearOn; bool takeMaskOff = !shouldKeepTheGearOn; if (!shouldKeepTheGearOn && !oxygenLow) @@ -359,10 +376,7 @@ namespace Barotrauma { if (objective is AIObjectiveGoTo gotoObjective) { - bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty; - Hull targetHull = gotoObjective.GetTargetHull(); - bool targetIsOutside = (gotoObjective.Target != null && targetHull == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes); - if (targetIsOutside || NeedsDivingGear(Character, targetHull, out _)) + if (NeedsDivingGearOnPath(gotoObjective)) { removeDivingSuit = false; takeMaskOff = false; @@ -390,81 +404,75 @@ namespace Barotrauma } } } - if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit) + } + if (removeDivingSuit) + { + var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR); + if (divingSuit != null) { - if (removeDivingSuit) + if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { - var divingSuit = Character.Inventory.FindItemByTag("divingsuit"); - if (divingSuit != null) + divingSuit.Drop(Character); + } + else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit) + { + findItemState = FindItemState.DivingSuit; + if (FindSuitableContainer(divingSuit, out Item targetContainer)) { - if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) + findItemState = FindItemState.None; + itemIndex = 0; + if (targetContainer != null) { - divingSuit.Drop(Character); + var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent()) + { + DropIfFailsToContain = false + }; + decontainObjective.Abandoned += () => + { + IgnoredItems.Add(targetContainer); + }; + ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); + return; } else { - findItemState = FindItemState.DivingSuit; - if (FindSuitableContainer(divingSuit, out Item targetContainer)) + divingSuit.Drop(Character); + } + } + } + } + } + if (takeMaskOff) + { + if (Character.HasEquippedItem(AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR)) + { + var mask = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR); + if (mask != null) + { + if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List() { InvSlotType.Any })) + { + if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) + { + mask.Drop(Character); + } + else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask) + { + findItemState = FindItemState.DivingMask; + if (FindSuitableContainer(mask, out Item targetContainer)) { findItemState = FindItemState.None; itemIndex = 0; if (targetContainer != null) { - var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent()) - { - DropIfFailsToContain = false - }; - decontainObjective.Abandoned += () => - { - IgnoredItems.Add(targetContainer); - }; + var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent()); + decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer); ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); return; } else - { - divingSuit.Drop(Character); - } - } - } - } - } - } - if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask) - { - if (takeMaskOff) - { - if (Character.HasEquippedItem("divingmask")) - { - var mask = Character.Inventory.FindItemByTag("divingmask"); - if (mask != null) - { - if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List() { InvSlotType.Any })) - { - if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { mask.Drop(Character); } - else - { - findItemState = FindItemState.DivingMask; - if (FindSuitableContainer(mask, out Item targetContainer)) - { - findItemState = FindItemState.None; - itemIndex = 0; - if (targetContainer != null) - { - var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent()); - decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer); - ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); - return; - } - else - { - mask.Drop(Character); - } - } - } } } } @@ -472,39 +480,37 @@ namespace Barotrauma } } } - if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem) + } + // Other items + if (isCarrying) { return; } + if (!ObjectiveManager.CurrentObjective.AllowAutomaticItemUnequipping || !ObjectiveManager.GetActiveObjective().AllowAutomaticItemUnequipping) { return; } + foreach (var item in Character.Inventory.Items) + { + if (item == null) { continue; } + if (Character.HasEquippedItem(item) && + (Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand) || + Character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand) || + Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand | InvSlotType.LeftHand))) { - if (!ObjectiveManager.CurrentObjective.UnequipItems || !ObjectiveManager.GetActiveObjective().UnequipItems) { return; } - if (ObjectiveManager.HasActiveObjective() || ObjectiveManager.HasActiveObjective()) { return; } - foreach (var item in Character.Inventory.Items) + if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Any })) { - if (item == null) { continue; } - if (Character.HasEquippedItem(item) && - (Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand) || - Character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand) || - Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand | InvSlotType.LeftHand))) + if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem) { - if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Any })) + findItemState = FindItemState.OtherItem; + if (FindSuitableContainer(item, out Item targetContainer)) { - if (FindSuitableContainer(item, out Item targetContainer)) + findItemState = FindItemState.None; + itemIndex = 0; + if (targetContainer != null) { - findItemState = FindItemState.None; - itemIndex = 0; - if (targetContainer != null) - { - var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent()); - decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer); - ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); - return; - } - else - { - item.Drop(Character); - } + var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent()); + decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer); + ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); + return; } else { - findItemState = FindItemState.OtherItem; + item.Drop(Character); } } } @@ -518,7 +524,6 @@ namespace Barotrauma None, DivingSuit, DivingMask, - Extinguisher, OtherItem } private FindItemState findItemState; @@ -687,16 +692,25 @@ namespace Barotrauma totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength; } if (totalDamage <= 0) { return; } - if (attacker != null) + if (Character.IsBot) { - if (!damageDoneByAttacker.ContainsKey(attacker)) + if (attacker != null) { - damageDoneByAttacker[attacker] = 0.0f; + if (!damageDoneByAttacker.ContainsKey(attacker)) + { + damageDoneByAttacker[attacker] = 0.0f; + } + damageDoneByAttacker[attacker] += totalDamage; + attackers.Add(attacker); + } + if (!freezeAI && !Character.IsDead && Character.IsIncapacitated) + { + // Removes the combat objective and resets all objectives. + objectiveManager.CreateAutonomousObjectives(); + objectiveManager.SortObjectives(); + freezeAI = true; } - damageDoneByAttacker[attacker] += totalDamage; - attackers.Add(attacker); } - if (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; } if (attacker == null || attacker.IsDead || attacker.Removed) { // Don't react on the damage if there's no attacker. @@ -720,76 +734,99 @@ namespace Barotrauma // Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate). return; } - if (attacker.IsBot) + float cumulativeDamage = GetDamageDoneByAttacker(attacker); + if (!Character.IsSecurity && attacker.IsBot && !attacker.IsInstigator) { - // Don't retaliate on damage done by human ai, because we know it's accidental - AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker, GetReactionTime() * 2); + if (cumulativeDamage > 1) + { + // Don't retaliate on damage done by human ai, because we know it's accidental + AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker); + } } else { - if (Character.IsSecurity) + (GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult); + // Inform other NPCs + if (cumulativeDamage > 1) { - // TODO - } - else - { - Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.50f, "attackedbyfriendly", minDurationBetweenSimilar: 30.0f); - } - if (Character.TeamID == Character.TeamType.FriendlyNPC && !Character.TurnedHostileByEvent) - { - // Inform other characters in the same team foreach (Character otherCharacter in Character.CharacterList) { - if (otherCharacter == Character || otherCharacter.TeamID != Character.TeamID || otherCharacter.IsDead || - otherCharacter.Info?.Job == null || + if (otherCharacter == Character || otherCharacter.IsDead || otherCharacter.IsUnconscious || otherCharacter.Removed || + otherCharacter.Info?.Job == null || otherCharacter.TeamID != Character.TeamType.FriendlyNPC || !(otherCharacter.AIController is HumanAIController otherHumanAI) || - otherCharacter.TurnedHostileByEvent) + otherCharacter.IsInstigator) { - continue; + continue; } + if (!otherHumanAI.IsFriendly(Character)) { continue; } bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull); if (otherCharacter.IsSecurity) { // Alert all the security officers magically float delay = isWitnessing ? GetReactionTime() * 2 : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced); - otherHumanAI.AddCombatObjective(DetermineCombatMode(otherCharacter), attacker, delay); + otherHumanAI.AddCombatObjective(DetermineCombatMode(otherCharacter, cumulativeDamage), attacker, delay); } else if (isWitnessing) { + var mode = Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat; // Other witnesses retreat to safety - otherHumanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker, GetReactionTime()); + otherHumanAI.AddCombatObjective(mode, attacker, GetReactionTime()); } } - (GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult); } - - if (attacker.TeamID != Character.TeamID) + if (Character.IsBot) { - AddCombatObjective(DetermineCombatMode(Character), attacker, GetReactionTime()); - } - else - { - // Don't react on minor (accidental) dmg done by characters that are in the same team - if (GetDamageDoneByAttacker(attacker) < 10) + if (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; } + if (Character.IsSecurity) { - if (!Character.IsSecurity) + if (attacker.TeamID != Character.TeamID && cumulativeDamage > 1 || cumulativeDamage > 10) { - AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker, GetReactionTime() * 2); + Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.50f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 30.0f); } + else + { + Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.50f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 30.0f); + } + } + else if (!Character.IsInstigator && cumulativeDamage > 1) + { + Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.50f, "attackedbyfriendly", minDurationBetweenSimilar: 30.0f); + } + if (cumulativeDamage > 1 && attacker.TeamID != Character.TeamID) + { + // If the attacker is using a low damage and high frequency weapon like a repair tool, we shouldn't use any delay. + AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage), attacker, delay: realDamage > 1 ? GetReactionTime() : 0); } else { - AddCombatObjective(DetermineCombatMode(Character, dmgThreshold: 20, allowOffensive: false), attacker, GetReactionTime() * 2); + bool allowOffensive = HasItem(attacker, "handlocker", out _, requireEquipped: true); + if (attackResult.Afflictions.Any(a => a is AfflictionHusk)) + { + cumulativeDamage = 100; + } + // Don't react on minor (accidental) dmg done by characters that are in the same team + if (cumulativeDamage < 10) + { + if (!Character.IsSecurity && cumulativeDamage > 1) + { + AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker); + } + } + else + { + AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage, dmgThreshold: 20, allowOffensive: allowOffensive), attacker, GetReactionTime() * 2); + } } } } } - else + else if (Character.IsBot) { - AddCombatObjective(DetermineCombatMode(Character), attacker); + // Non-friendly + AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage: realDamage), attacker); } - AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float dmgThreshold = 10, bool allowOffensive = true) + AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage, float dmgThreshold = 10, bool allowOffensive = true) { if (!IsFriendly(attacker)) { @@ -797,13 +834,38 @@ namespace Barotrauma } else { - if (GetDamageDoneByAttacker(attacker) > dmgThreshold) + if (attacker.TeamID == Character.TeamType.FriendlyNPC) { - return c.IsSecurity && allowOffensive ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive; + if (c.IsSecurity) + { + return Character.CombatAction != null ? Character.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.None; + } + else + { + return Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.None; + } } else { - return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat; + if (Character.IsInstigator) + { + return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat; + } + else if (cumulativeDamage > dmgThreshold) + { + if (c.IsSecurity) + { + return c.IsSecurity && allowOffensive ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest; + } + else + { + return c == Character ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat; + } + } + else + { + return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat; + } } } } @@ -811,6 +873,8 @@ namespace Barotrauma private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character attacker, float delay = 0, Func abortCondition = null, Action onAbort = null, bool allowHoldFire = false) { + if (mode == AIObjectiveCombat.CombatMode.None) { return; } + if (Character.IsDead || Character.IsIncapacitated) { return; } if (ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective) { // Don't replace offensive mode with something else @@ -896,6 +960,19 @@ namespace Barotrauma SelectedAiTarget = target; } + public override void Reset() + { + base.Reset(); + objectiveManager.SortObjectives(); + sortTimer = sortObjectiveInterval; + float waitDuration = characterWaitOnSwitch; + if (ObjectiveManager.IsCurrentObjective()) + { + waitDuration *= 2; + } + ObjectiveManager.WaitTimer = waitDuration; + } + private void CheckCrouching(float deltaTime) { crouchRaycastTimer -= deltaTime; @@ -964,13 +1041,13 @@ namespace Barotrauma return targetInventory.TryPutItem(item, targetSlot, false, false, Character); } - public static bool NeedsDivingGear(Character character, Hull hull, out bool needsSuit) + public static bool NeedsDivingGear(Hull hull, out bool needsSuit) { needsSuit = false; if (hull == null || - hull.WaterPercentage > 80 || - (hull.LethalPressure > 0 && character.PressureProtection <= 0) || - (hull.ConnectedGaps.Any() && hull.ConnectedGaps.Max(g => AIObjectiveFixLeaks.GetLeakSeverity(g)) > 60)) + hull.WaterPercentage > 90 || + hull.LethalPressure > 0 || + hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.5f)) { needsSuit = true; return true; @@ -987,25 +1064,28 @@ namespace Barotrauma /// /// Check whether the character has a diving suit in usable condition plus some oxygen. /// - public static bool HasDivingSuit(Character character, float conditionPercentage = 0) => HasItem(character, "divingsuit", out _, "oxygensource", conditionPercentage, requireEquipped: true); + public static bool HasDivingSuit(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true); /// /// Check whether the character has a diving mask in usable condition plus some oxygen. /// - public static bool HasDivingMask(Character character, float conditionPercentage = 0) => HasItem(character, "divingmask", out _, "oxygensource", conditionPercentage, requireEquipped: true); + public static bool HasDivingMask(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true); - public static bool HasItem(Character character, string tagOrIdentifier, out Item item, string containedTag = null, float conditionPercentage = 0, bool requireEquipped = false) + private static List matchingItems = new List(); + public static bool HasItem(Character character, string tagOrIdentifier, out IEnumerable items, string containedTag = null, float conditionPercentage = 0, bool requireEquipped = false) { - item = null; + matchingItems.Clear(); + items = matchingItems; if (character == null) { return false; } if (character.Inventory == null) { return false; } - item = character.Inventory.FindItemByIdentifier(tagOrIdentifier) ?? character.Inventory.FindItemByTag(tagOrIdentifier); - return item != null && - item.ConditionPercentage >= conditionPercentage && - (!requireEquipped || character.HasEquippedItem(item)) && + matchingItems = character.Inventory.FindAllItems(i => i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier), recursive: true, matchingItems); + items = matchingItems; + return matchingItems.Any(i => i != null && + i.ConditionPercentage >= conditionPercentage && + (!requireEquipped || character.HasEquippedItem(i)) && (containedTag == null || - (item.ContainedItems != null && - item.ContainedItems.Any(i => i.HasTag(containedTag) && i.ConditionPercentage > conditionPercentage))); + (i.OwnInventory?.Items != null && + i.OwnInventory.Items.Any(it => it != null && it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage)))); } public static void ItemTaken(Item item, Character character) @@ -1040,6 +1120,8 @@ namespace Barotrauma otherCharacter.Speak(TextManager.Get("dialogstealwarning"), null, Rand.Range(0.5f, 1.0f), "thief", 10.0f); someoneSpoke = true; } + // Don't react if the player is taking an extinguisher and there's any fires on the sub -> allow them to use the emergency items + if (item.HasTag("fireextinguisher") && character.Submarine.GetHulls(alsoFromConnectedSubs: true).Any(h => h.FireSources.Any())) { continue; } // React if we are security if (!TriggerSecurity(otherHumanAI)) { @@ -1240,6 +1322,7 @@ namespace Barotrauma { if (hull == null) { return 0; } if (hull.LethalPressure > 0 && character.PressureProtection <= 0) { return 0; } + // TODO: take the visiblehulls into account? float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp(0.25f, 1, hull.OxygenPercentage / 100); float waterFactor = ignoreWater ? 1 : MathHelper.Lerp(1, 0.25f, hull.WaterPercentage / 100); if (!character.NeedsAir) @@ -1409,14 +1492,18 @@ namespace Barotrauma if (target?.Item == null) { return false; } foreach (var c in Character.CharacterList) { - if (character != null && c == character) { continue; } - if (character?.AIController is HumanAIController humanAi && !humanAi.IsFriendly(c)) { continue; } + if (character == null) { continue; } + if (c == character) { continue; } + if (c.IsDead || c.IsIncapacitated) { continue; } if (c.SelectedConstruction != target.Item) { continue; } + if (!IsFriendly(character, c, onlySameTeam: true)) { continue; } operatingCharacter = c; // If the other character is player, don't try to operate - if (c.IsRemotePlayer || Character.Controlled == c) { return true; } + if (c.IsPlayer) { return true; } if (c.AIController is HumanAIController controllingHumanAi) { + Item otherTarget = controllingHumanAi.objectiveManager.GetActiveObjective()?.Component.Item ?? c.SelectedConstruction; + if (otherTarget != target.Item) { continue; } // If the other character is ordered to operate the item, let him do it if (controllingHumanAi.ObjectiveManager.IsCurrentOrder()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 113811368..bca8de434 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -111,9 +111,9 @@ namespace Barotrauma IsPathDirty = true; } - public void SteeringSeek(Vector2 target, float weight, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null) + public void SteeringSeek(Vector2 target, float weight, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisiblity = true) { - steering += CalculateSteeringSeek(target, weight, startNodeFilter, endNodeFilter, nodeFilter); + steering += CalculateSteeringSeek(target, weight, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity); } /// @@ -158,7 +158,7 @@ namespace Barotrauma } } - private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null) + private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisibility = true) { Vector2 targetDiff = target - currentTarget; if (currentPath != null && currentPath.Nodes.Any()) @@ -172,40 +172,42 @@ namespace Barotrauma targetDiff += subDiff; } } - bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || currentPath.Finished || targetDiff.LengthSquared() > 1); + bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || targetDiff.LengthSquared() > 1); //find a new path if one hasn't been found yet or the target is different from the current target if (needsNewPath || findPathTimer < -1.0f) { IsPathDirty = true; - if (findPathTimer > 0.0f) { return Vector2.Zero; } - currentTarget = target; - Vector2 currentPos = host.SimPosition; - if (character != null && character.Submarine == null) + if (findPathTimer < 0) { - var targetHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(target), null, false); - if (targetHull != null && targetHull.Submarine != null) + currentTarget = target; + Vector2 currentPos = host.SimPosition; + if (character != null && character.Submarine == null) { - currentPos -= targetHull.Submarine.SimPosition; + var targetHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(target), null, false); + if (targetHull != null && targetHull.Submarine != null) + { + currentPos -= targetHull.Submarine.SimPosition; + } } + pathFinder.InsideSubmarine = character.Submarine != null; + var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility); + bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null; + if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable) + { + // It's possible that the current path was calculated from a start point that is no longer valid. + // Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node. + useNewPath = newPath.Cost < currentPath.Cost || + Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2); + } + if (useNewPath) + { + currentPath = newPath; + } + float priority = MathHelper.Lerp(3, 1, character.Params.PathFinderPriority); + findPathTimer = priority * Rand.Range(1.0f, 1.2f); + IsPathDirty = false; + return DiffToCurrentNode(); } - pathFinder.InsideSubmarine = character.Submarine != null; - var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter); - bool useNewPath = currentPath == null || needsNewPath || currentPath.Finished; - if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable) - { - // It's possible that the current path was calculated from a start point that is no longer valid. - // Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node. - useNewPath = newPath.Cost < currentPath.Cost || - Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2); - } - if (useNewPath) - { - currentPath = newPath; - } - float priority = MathHelper.Lerp(3, 1, character.Params.PathFinderPriority); - findPathTimer = priority * Rand.Range(1.0f, 1.2f); - IsPathDirty = false; - return DiffToCurrentNode(); } Vector2 diff = DiffToCurrentNode(); @@ -221,7 +223,7 @@ namespace Barotrauma return Vector2.Normalize(diff) * weight; } - protected override Vector2 DoSteeringSeek(Vector2 target, float weight) => CalculateSteeringSeek(target, weight, null, null, null); + protected override Vector2 DoSteeringSeek(Vector2 target, float weight) => CalculateSteeringSeek(target, weight); private Vector2 DiffToCurrentNode() { @@ -260,7 +262,7 @@ namespace Barotrauma } bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater; // Only humanoids can climb ladders - bool canClimb = character.AnimController is HumanoidAnimController; + bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands; var ladders = GetNextLadder(); if (canClimb && !isDiving && ladders != null && character.SelectedConstruction != ladders.Item) { @@ -588,7 +590,7 @@ namespace Barotrauma //non-humanoids can't climb up ladders if (!(character.AnimController is HumanoidAnimController)) { - if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && nextNode.Waypoint.Ladders.Item.NonInteractable || + if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (nextNode.Waypoint.Ladders.Item.NonInteractable || character.LockHands)|| (nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y)) //upper node not underwater { @@ -628,6 +630,7 @@ namespace Barotrauma return penalty; } + public static float smallRoomSize = 500; public void Wander(float deltaTime, float wallAvoidDistance = 150, bool stayStillInTightSpace = true) { //steer away from edges of the hull @@ -637,7 +640,7 @@ namespace Barotrauma if (currentHull != null && !inWater) { float roomWidth = currentHull.Rect.Width; - if (stayStillInTightSpace && roomWidth < wallAvoidDistance * 4) + if (stayStillInTightSpace && roomWidth < Math.Max(wallAvoidDistance * 3, smallRoomSize)) { Reset(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index e1d530ff6..27f2a51a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -125,8 +125,14 @@ namespace Barotrauma } } - attachCooldown -= deltaTime; - deattachTimer -= deltaTime; + if (attachCooldown > 0) + { + attachCooldown -= deltaTime; + } + if (deattachTimer > 0) + { + deattachTimer -= deltaTime; + } Vector2 transformedAttachPos = wallAttachPos; if (character.Submarine == null && attachTargetSubmarine != null) @@ -255,6 +261,7 @@ namespace Barotrauma private void AttachToBody(PhysicsBody collider, Limb attachLimb, Body targetBody, Vector2 attachPos) { + if (attachCooldown > 0) { return; } //already attached to something if (attachJoints.Count > 0) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 80bc37a49..13ab648d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -17,8 +17,7 @@ namespace Barotrauma public virtual bool AllowSubObjectiveSorting => false; /// - /// Can there be multiple objective instaces of the same type? Currently multiple instances allowed only for main objectives and the subobjectives of objetive loops. - /// In theory, there could be multiple subobjectives of same type for concurrent objectives, but that would make things more complex -> potential issues + /// Can there be multiple objective instaces of the same type? /// public virtual bool AllowMultipleInstances => false; @@ -29,7 +28,10 @@ namespace Barotrauma public virtual bool ConcurrentObjectives => false; public virtual bool KeepDivingGearOn => false; - public virtual bool UnequipItems => false; + /// + /// There's a separate property for diving suit and mask: KeepDivingGearOn. + /// + public virtual bool AllowAutomaticItemUnequipping => false; public virtual bool AllowOutsideSubmarine => false; public virtual bool AllowInFriendlySubs => false; @@ -173,6 +175,7 @@ namespace Barotrauma { if (!AllowSubObjectiveSorting) { return; } if (subObjectives.None()) { return; } + var previousSubObjective = subObjectives.First(); subObjectives.ForEach(so => so.GetPriority()); subObjectives.Sort((x, y) => y.Priority.CompareTo(x.Priority)); if (ConcurrentObjectives) @@ -181,7 +184,13 @@ namespace Barotrauma } else { - subObjectives.First().SortSubObjectives(); + var currentSubObjective = subObjectives.First(); + if (previousSubObjective != currentSubObjective) + { + previousSubObjective.OnDeselected(); + currentSubObjective.OnSelected(); + } + currentSubObjective.SortSubObjectives(); } } @@ -222,7 +231,7 @@ namespace Barotrauma private void UpdateDevotion(float deltaTime) { var currentObjective = objectiveManager.CurrentObjective; - if (currentObjective != null && (currentObjective == this || currentObjective.subObjectives.Any(so => so == this))) + if (currentObjective != null && (currentObjective == this || currentObjective.subObjectives.FirstOrDefault() == this)) { CumulatedDevotion += Devotion * deltaTime; } @@ -327,6 +336,7 @@ namespace Barotrauma public virtual void Reset() { + subObjectives.Clear(); isCompleted = false; hasBeenChecked = false; _abandon = false; @@ -369,8 +379,7 @@ namespace Barotrauma { if (Check()) { - isCompleted = true; - OnCompleted(); + IsCompleted = true; } } return isCompleted; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs index 74e1ffd5e..1f286492f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs @@ -1,16 +1,16 @@ using Barotrauma.Items.Components; -using Barotrauma.Extensions; using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; +using Barotrauma.Extensions; namespace Barotrauma { class AIObjectiveChargeBatteries : AIObjectiveLoop { public override string DebugTag => "charge batteries"; - public override bool UnequipItems => true; + public override bool AllowAutomaticItemUnequipping => true; private IEnumerable batteryList; public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier) @@ -26,8 +26,7 @@ namespace Barotrauma if (item.Submarine.TeamID != character.TeamID) { return false; } if (character.Submarine != null) { - if (item.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } - if (!character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; } + if (!character.Submarine.IsConnectedTo(item.Submarine)) { return false; } } if (item.ConditionPercentage <= 0) { return false; } if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } @@ -37,6 +36,7 @@ namespace Barotrauma protected override float TargetEvaluation() { + if (Targets.None()) { return 0; } if (Option == "charge") { return Targets.Max(t => MathHelper.Lerp(100, 0, Math.Abs(PowerContainer.aiRechargeTargetRatio - t.RechargeRatio))); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index f5d8ab099..9a894bba1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using FarseerPhysics.Dynamics; namespace Barotrauma { @@ -17,12 +18,11 @@ namespace Barotrauma private readonly CombatMode initialMode; - private float seekWeaponsTimer; - private readonly float seekWeaponsInterval = 1; + private float checkWeaponsTimer; + private readonly float checkWeaponsInterval = 1; private float ignoreWeaponTimer; private readonly float ignoredWeaponsClearTime = 10; - // Won't (by default) start the offensive with weapons that have lower priority than this private readonly float goodWeaponPriority = 30; private readonly float arrestHoldFireTime = 8; @@ -42,7 +42,7 @@ namespace Barotrauma _weapon = value; _weaponComponent = null; hasAimed = false; - RemoveSubObjective(ref seekAmmunition); + RemoveSubObjective(ref seekAmmunitionObjective); } } private ItemComponent _weaponComponent; @@ -69,13 +69,14 @@ namespace Barotrauma private readonly HashSet weapons = new HashSet(); private readonly HashSet ignoredWeapons = new HashSet(); - private AIObjectiveContainItem seekAmmunition; + private AIObjectiveContainItem seekAmmunitionObjective; private AIObjectiveGoTo retreatObjective; private AIObjectiveGoTo followTargetObjective; + private AIObjectiveGetItem seekWeaponObjective; private Hull retreatTarget; private float coolDownTimer; - private IEnumerable myBodies; + private IEnumerable myBodies; private float aimTimer; private bool canSeeTarget; @@ -99,17 +100,27 @@ namespace Barotrauma Defensive, Offensive, Arrest, - Retreat + Retreat, + None } public CombatMode Mode { get; private set; } private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest; private bool TargetEliminated => Enemy == null || Enemy.Removed || Enemy.IsUnconscious; + private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead; + private bool EnemyIsClose() => Enemy != null && character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500; public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f) : base(character, objectiveManager, priorityModifier) { + if (mode == CombatMode.None) + { +#if DEBUG + DebugConsole.ThrowError("Combat mode == None"); +#endif + return; + } Enemy = enemy; coolDownTimer = coolDown; findSafety = objectiveManager.GetObjective(); @@ -145,12 +156,16 @@ namespace Barotrauma { base.Update(deltaTime); ignoreWeaponTimer -= deltaTime; - seekWeaponsTimer -= deltaTime; + checkWeaponsTimer -= deltaTime; if (ignoreWeaponTimer < 0) { ignoredWeapons.Clear(); ignoreWeaponTimer = ignoredWeaponsClearTime; } + if (findSafety != null) + { + findSafety.Priority = 0; + } } protected override bool Check() @@ -164,8 +179,6 @@ namespace Barotrauma return IsEnemyDisabled || (!IsOffensiveOrArrest && coolDownTimer <= 0); } - private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead; - protected override void Act(float deltaTime) { if (abortCondition != null && abortCondition()) @@ -178,13 +191,13 @@ namespace Barotrauma { coolDownTimer -= deltaTime; } - if (seekAmmunition == null) + if (seekAmmunitionObjective == null && seekWeaponObjective == null) { if (Mode != CombatMode.Retreat && TryArm() && !IsEnemyDisabled) { OperateWeapon(deltaTime); } - if (!HoldPosition) + if (!HoldPosition && seekAmmunitionObjective == null && seekWeaponObjective == null) { Move(deltaTime); } @@ -193,8 +206,7 @@ namespace Barotrauma case CombatMode.Offensive: if (TargetEliminated && objectiveManager.IsCurrentOrder()) { - // TODO: enable - //character.Speak(TextManager.Get("DialogTargetDown"), null, 3.0f, "targetdown", 30.0f); + character.Speak(TextManager.Get("DialogTargetDown"), null, 3.0f, "targetdown", 30.0f); } break; case CombatMode.Arrest: @@ -233,11 +245,11 @@ namespace Barotrauma Weapon = null; return false; } - if (seekWeaponsTimer < 0) + if (checkWeaponsTimer < 0) { - seekWeaponsTimer = seekWeaponsInterval; + checkWeaponsTimer = checkWeaponsInterval; // First go through all weapons and try to reload without seeking ammunition - var allWeapons = GetAllWeapons(); + var allWeapons = FindWeaponsFromInventory(); while (allWeapons.Any()) { Weapon = GetWeapon(allWeapons, out _weaponComponent); @@ -273,12 +285,12 @@ namespace Barotrauma if (Weapon == null) { // No weapon found with the conditions above. Try again, now let's try to seek ammunition too - Weapon = GetWeapon(out _weaponComponent); + Weapon = FindWeapon(out _weaponComponent); if (Weapon != null) { if (!CheckWeapon(seekAmmo: true)) { - if (seekAmmunition != null) + if (seekAmmunitionObjective != null) { // No loaded weapon, but we are trying to seek ammunition. return false; @@ -290,9 +302,58 @@ namespace Barotrauma } } } - if (Weapon == null) + bool isAllowedToSeekWeapons = !EnemyIsClose() && character.TeamID != Character.TeamType.FriendlyNPC && IsOffensiveOrArrest; + if (!isAllowedToSeekWeapons) { - Mode = CombatMode.Retreat; + if (WeaponComponent == null) + { + Mode = CombatMode.Retreat; + } + } + else if (seekAmmunitionObjective == null && (WeaponComponent == null || WeaponComponent.CombatPriority < goodWeaponPriority)) + { + // Poor weapon equipped -> try to find better. + RemoveSubObjective(ref seekAmmunitionObjective); + RemoveSubObjective(ref retreatObjective); + RemoveSubObjective(ref followTargetObjective); + TryAddSubObjective(ref seekWeaponObjective, + constructor: () => new AIObjectiveGetItem(character, "weapon", objectiveManager, equip: true, checkInventory: false) + { + GetItemPriority = i => + { + if (Weapon != null && (i == Weapon || i.Prefab.Identifier == Weapon.Prefab.Identifier)) { return 0; } + if (i.IsOwnedBy(character)) { return 0; } + var mw = i.GetComponent(); + var rw = i.GetComponent(); + float priority = 0; + if (mw != null) + { + priority = mw.CombatPriority / 100; + } + else if (rw != null) + { + priority = rw.CombatPriority / 100; + } + if (i.HasTag("stunner")) + { + if (Mode == CombatMode.Arrest) + { + priority *= 2; + } + else + { + priority /= 2; + } + } + return priority; + } + }, + onCompleted: () => RemoveSubObjective(ref seekWeaponObjective), + onAbandon: () => + { + RemoveSubObjective(ref seekWeaponObjective); + Mode = CombatMode.Retreat; + }); } } else @@ -342,32 +403,20 @@ namespace Barotrauma } } - private Item GetWeapon(out ItemComponent weaponComponent) => GetWeapon(GetAllWeapons(), out weaponComponent); + private Item FindWeapon(out ItemComponent weaponComponent) => GetWeapon(FindWeaponsFromInventory(), out weaponComponent); private Item GetWeapon(IEnumerable weaponList, out ItemComponent weaponComponent) { weaponComponent = null; float bestPriority = 0; float lethalDmg = -1; + bool enemyIsClose = EnemyIsClose(); foreach (var weapon in weaponList) { - // By default, the bots won't go offensive with bad weapons, unless they are close to the enemy or ordered to fight enemies. - // NPC characters ignore this check. - if ((initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest) && character.TeamID != Character.TeamType.FriendlyNPC) - { - if (!objectiveManager.IsCurrentOrder() && !EnemyIsClose()) - { - if (weapon.CombatPriority < goodWeaponPriority) - { - continue; - } - } - } - float priority = weapon.CombatPriority; if (!IsLoaded(weapon)) { - if (weapon is RangedWeapon && EnemyIsClose()) + if (weapon is RangedWeapon && enemyIsClose) { // Close to the enemy. Ignore weapons that don't have any ammunition (-> Don't seek ammo). continue; @@ -420,6 +469,11 @@ namespace Barotrauma } } } + else if (weapon is MeleeWeapon && weapon.Item.HasTag("stunner") && !CanMeleeStunnerStun(weapon)) + { + Attack attack = GetAttackDefinition(weapon); + priority = attack?.GetTotalDamage() ?? priority / 2; + } if (priority > bestPriority) { weaponComponent = weapon; @@ -449,9 +503,7 @@ namespace Barotrauma } return weaponComponent.Item; - bool EnemyIsClose() => character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500; - - Attack GetAttackDefinition(ItemComponent weapon) + static Attack GetAttackDefinition(ItemComponent weapon) { Attack attack = null; if (weapon is MeleeWeapon meleeWeapon) @@ -465,7 +517,7 @@ namespace Barotrauma return attack; } - float GetLethalDamage(ItemComponent weapon) + static float GetLethalDamage(ItemComponent weapon) { float lethalDmg = 0; Attack attack = GetAttackDefinition(weapon); @@ -499,25 +551,38 @@ namespace Barotrauma }); return attack.Stun + afflictionsStun + effectsStun; } + + bool CanMeleeStunnerStun(ItemComponent weapon) + { + // If there's an item container that takes a battery, + // assume that it's required for the stun effect + // as we can't check the status effect conditions here. + var mobileBatteryTag = "mobilebattery"; + var containers = weapon.Item.Components.Where(ic => ic is ItemContainer container && + container.ContainableItems.Any(containable => containable.Identifiers.Any(id => id.Equals(mobileBatteryTag)))); + // If there's no such container, assume that the melee weapon can stun without a battery. + return containers.None() || containers.Any(container => + (container as ItemContainer)?.Inventory.Items.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false); + } } - private HashSet GetAllWeapons() + private HashSet FindWeaponsFromInventory() { weapons.Clear(); foreach (var item in character.Inventory.Items) { if (item == null) { continue; } if (ignoredWeapons.Contains(item)) { continue; } - SeekWeapons(item, weapons); + GetWeapons(item, weapons); if (item.OwnInventory != null) { - item.OwnInventory.Items.ForEach(i => SeekWeapons(i, weapons)); + item.OwnInventory.Items.ForEach(i => GetWeapons(i, weapons)); } } return weapons; } - private void SeekWeapons(Item item, ICollection weaponList) + private void GetWeapons(Item item, ICollection weaponList) { if (item == null) { return; } foreach (var component in item.Components) @@ -571,7 +636,7 @@ namespace Barotrauma private void Retreat(float deltaTime) { RemoveFollowTarget(); - RemoveSubObjective(ref seekAmmunition); + RemoveSubObjective(ref seekAmmunitionObjective); if (retreatObjective != null && retreatObjective.Target != retreatTarget) { RemoveSubObjective(ref retreatObjective); @@ -611,6 +676,12 @@ namespace Barotrauma private void Engage() { + if (WeaponComponent == null) + { + RemoveFollowTarget(); + SteeringManager.Reset(); + return; + } if (character.LockHands || Enemy == null) { Mode = CombatMode.Retreat; @@ -619,7 +690,8 @@ namespace Barotrauma } retreatTarget = null; RemoveSubObjective(ref retreatObjective); - RemoveSubObjective(ref seekAmmunition); + RemoveSubObjective(ref seekAmmunitionObjective); + RemoveSubObjective(ref seekWeaponObjective); if (followTargetObjective != null && followTargetObjective.Target != Enemy) { RemoveFollowTarget(); @@ -639,7 +711,7 @@ namespace Barotrauma if (followTargetObjective == null) { return; } if (Mode == CombatMode.Arrest && Enemy.Stun > 2) { - if (HumanAIController.HasItem(character, "handlocker", out Item handCuffs)) + if (HumanAIController.HasItem(character, "handlocker", out _)) { if (!arrestingRegistered) { @@ -650,16 +722,19 @@ namespace Barotrauma } else { + if (character.TeamID == Character.TeamType.FriendlyNPC) + { + ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs"); + if (prefab != null) + { + Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInOutpost = true); + } + } RemoveFollowTarget(); SteeringManager.Reset(); } } - else if (WeaponComponent == null) - { - RemoveFollowTarget(); - SteeringManager.Reset(); - } - else + if (followTargetObjective != null) { followTargetObjective.CloseEnough = WeaponComponent is RangedWeapon ? 1000 : @@ -682,8 +757,9 @@ namespace Barotrauma private void OnArrestTargetReached() { - if (HumanAIController.HasItem(character, "handlocker", out Item handCuffs) && Enemy.Stun > 0 && character.CanInteractWith(Enemy)) + if (HumanAIController.HasItem(character, "handlocker", out IEnumerable matchingItems) && Enemy.Stun > 0 && character.CanInteractWith(Enemy)) { + var handCuffs = matchingItems.First(); if (HumanAIController.TryToMoveItem(handCuffs, Enemy.Inventory)) { handCuffs.Equip(Enemy); @@ -704,8 +780,7 @@ namespace Barotrauma character.Inventory.TryPutItem(item, character, new List() { InvSlotType.Any }); } } - // TODO: enable - //character.Speak(TextManager.Get("DialogTargetArrested"), null, 3.0f, "targetarrested", 30.0f); + character.Speak(TextManager.Get("DialogTargetArrested"), null, 3.0f, "targetarrested", 30.0f); IsCompleted = true; } } @@ -717,18 +792,19 @@ namespace Barotrauma { retreatTarget = null; RemoveSubObjective(ref retreatObjective); + RemoveSubObjective(ref seekWeaponObjective); RemoveFollowTarget(); - TryAddSubObjective(ref seekAmmunition, + TryAddSubObjective(ref seekAmmunitionObjective, constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, Weapon.GetComponent(), objectiveManager) { targetItemCount = Weapon.GetComponent().Capacity, checkInventory = false }, - onCompleted: () => RemoveSubObjective(ref seekAmmunition), + onCompleted: () => RemoveSubObjective(ref seekAmmunitionObjective), onAbandon: () => { SteeringManager.Reset(); - RemoveSubObjective(ref seekAmmunition); + RemoveSubObjective(ref seekAmmunitionObjective); ignoredWeapons.Add(Weapon); Weapon = null; }); @@ -742,7 +818,8 @@ namespace Barotrauma { if (WeaponComponent == null) { return false; } if (!WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return false; } - var containedItems = Weapon.ContainedItems; + var containedItems = Weapon.OwnInventory?.Items; + if (containedItems == null) { return true; } // Drop empty ammo foreach (Item containedItem in containedItems) { @@ -757,7 +834,7 @@ namespace Barotrauma string[] ammunitionIdentifiers = null; foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained]) { - ammunition = containedItems.FirstOrDefault(it => it.Condition > 0 && requiredItem.MatchesItem(it)); + ammunition = containedItems.FirstOrDefault(it => it != null && it.Condition > 0 && requiredItem.MatchesItem(it)); if (ammunition != null) { // Ammunition still remaining @@ -831,36 +908,50 @@ namespace Barotrauma aimTimer -= deltaTime; return; } - if (Mode == CombatMode.Arrest && isLethalWeapon && Enemy.Stun > 0) { return; } + if (Mode == CombatMode.Arrest && isLethalWeapon && Enemy.Stun > 1) { return; } if (holdFireCondition != null && holdFireCondition()) { return; } float sqrDist = Vector2.DistanceSquared(character.Position, Enemy.Position); - if (!character.IsFacing(Enemy.WorldPosition)) - { - aimTimer = Rand.Range(1f, 1.5f); - return; - } if (WeaponComponent is MeleeWeapon meleeWeapon) { + bool closeEnough = true; float sqrRange = meleeWeapon.Range * meleeWeapon.Range; if (character.AnimController.InWater) { - if (sqrDist > sqrRange) { return; } + if (sqrDist > sqrRange) + { + closeEnough = false; + } } else { // It's possible that the center point of the creature is out of reach, but we could still hit the character. float xDiff = Math.Abs(Enemy.WorldPosition.X - character.WorldPosition.X); - if (xDiff > meleeWeapon.Range) { return; } + if (xDiff > meleeWeapon.Range) + { + closeEnough = false; + } float yDiff = Math.Abs(Enemy.WorldPosition.Y - character.WorldPosition.Y); - if (yDiff > Math.Max(meleeWeapon.Range, 100)) { return; } - if (Enemy.WorldPosition.Y < character.WorldPosition.Y && yDiff > 25) + if (yDiff > Math.Max(meleeWeapon.Range, 100)) + { + closeEnough = false; + } + if (closeEnough && Enemy.WorldPosition.Y < character.WorldPosition.Y && yDiff > 25) { // The target is probably knocked down? -> try to reach it by crouching. HumanAIController.AnimController.Crouching = true; } } - character.SetInput(InputType.Shoot, false, true); - Weapon.Use(deltaTime, character); + if (closeEnough) + { + SteeringManager.Reset(); + character.SetInput(InputType.Shoot, false, true); + Weapon.Use(deltaTime, character); + } + else if (!character.IsFacing(Enemy.WorldPosition)) + { + // Don't do the facing check if we are close to the target, because it easily causes the character to get stuck here when it flips around. + aimTimer = Rand.Range(1f, 1.5f); + } } else { @@ -916,6 +1007,19 @@ namespace Barotrauma } } + public override void Reset() + { + base.Reset(); + hasAimed = false; + isLethalWeapon = false; + canSeeTarget = false; + seekWeaponObjective = null; + seekAmmunitionObjective = null; + retreatObjective = null; + followTargetObjective = null; + retreatTarget = null; + } + //private float CalculateEnemyStrength() //{ // float enemyStrength = 0; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 8fd190aec..9e282bb99 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -1,5 +1,4 @@ using Barotrauma.Items.Components; -using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -30,6 +29,7 @@ namespace Barotrauma private readonly HashSet containedItems = new HashSet(); public bool AllowToFindDivingGear { get; set; } = true; + public bool AllowDangerousPressure { get; set; } public float ConditionLevel { get; set; } public bool Equip { get; set; } public bool RemoveEmpty { get; set; } = true; @@ -53,13 +53,17 @@ namespace Barotrauma { itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant(); } - this.container = container; } protected override bool Check() { if (IsCompleted) { return true; } + if (container == null) + { + Abandon = true; + return false; + } if (item != null) { return container.Inventory.Items.Contains(item); @@ -143,8 +147,8 @@ namespace Barotrauma DialogueIdentifier = "dialogcannotreachtarget", TargetName = container.Item.Name }, - onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref goToObjective)); + onAbandon: () => Abandon = true, + onCompleted: () => RemoveSubObjective(ref goToObjective)); } } else @@ -156,7 +160,9 @@ namespace Barotrauma GetItemPriority = GetItemPriority, ignoredContainerIdentifiers = ignoredContainerIdentifiers, ignoredItems = containedItems, - AllowToFindDivingGear = this.AllowToFindDivingGear + AllowToFindDivingGear = AllowToFindDivingGear, + AllowDangerousPressure = AllowDangerousPressure, + TargetCondition = ConditionLevel }, onAbandon: () => { Abandon = true; @@ -166,20 +172,17 @@ namespace Barotrauma { containedItems.Add(getItemObjective.TargetItem); } - else - { - if (container.Inventory.FindItem(i => CheckItem(i), recursive: false) != null) - { - IsCompleted = true; - } - else - { - Abandon = true; - } - } RemoveSubObjective(ref getItemObjective); }); } - } + } + + public override void Reset() + { + base.Reset(); + getItemObjective = null; + goToObjective = null; + containedItems.Clear(); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index f13e6e548..c155cc3bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -1,5 +1,4 @@ using Barotrauma.Items.Components; -using Microsoft.Xna.Framework; using System; using System.Linq; @@ -130,5 +129,12 @@ namespace Barotrauma IsCompleted = true; } } + + public override void Reset() + { + base.Reset(); + goToObjective = null; + containObjective = null; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index b6fb5ebce..82038d23b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -60,7 +60,7 @@ namespace Barotrauma private float sinTime; protected override void Act(float deltaTime) { - var extinguisherItem = character.Inventory.FindItemByIdentifier("fireextinguisher") ?? character.Inventory.FindItemByTag("fireextinguisher"); + var extinguisherItem = character.Inventory.FindItemByTag("fireextinguisher"); if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem)) { TryAddSubObjective(ref getExtinguisherObjective, () => @@ -147,5 +147,14 @@ namespace Barotrauma } } } + + public override void Reset() + { + base.Reset(); + getExtinguisherObjective = null; + gotoObjective = null; + useExtinquisherTimer = 0; + sinTime = 0; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs index e41f6675f..c34f28aa2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs @@ -1,7 +1,6 @@ using System.Linq; using System.Collections.Generic; using Barotrauma.Extensions; -using System; namespace Barotrauma { @@ -14,7 +13,7 @@ namespace Barotrauma protected override bool Filter(Hull hull) => IsValidTarget(hull, character); - protected override float TargetEvaluation() => objectiveManager.CurrentObjective == this ? 100 : Targets.Sum(t => GetFireSeverity(t)); + protected override float TargetEvaluation() => Targets.Sum(t => GetFireSeverity(t)); public static float GetFireSeverity(Hull hull) => hull.FireSources.Sum(fs => fs.Size.X); @@ -31,11 +30,22 @@ namespace Barotrauma if (hull == null) { return false; } if (hull.FireSources.None()) { return false; } if (hull.Submarine == null) { return false; } - if (hull.Submarine.TeamID != character.TeamID) { return false; } - if (character.Submarine != null) + if (character.Submarine == null) { return false; } + if (!character.Submarine.IsConnectedTo(hull.Submarine)) { return false; } + if (character.AIController is HumanAIController humanAI) { - if (hull.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } - if (!character.Submarine.IsEntityFoundOnThisSub(hull, true)) { return false; } + if (hull.Submarine.TeamID != character.TeamID) + { + if (humanAI.ObjectiveManager.IsCurrentOrder()) + { + // For orders, allow targets in the current sub (for example if the bot is inside an outpost or a wreck) + if (hull.Submarine != character.Submarine) { return false; } + } + else + { + return false; + } + } } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index 2bb2dddb3..11cd94e7f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -9,7 +10,6 @@ namespace Barotrauma protected override float IgnoreListClearInterval => 30; public override bool IgnoreUnsafeHulls => true; - public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } @@ -20,7 +20,7 @@ namespace Barotrauma protected override float TargetEvaluation() { // TODO: sorting criteria - return 100; + return Targets.None() ? 0 : 100; } protected override AIObjective ObjectiveConstructor(Character target) @@ -56,8 +56,7 @@ namespace Barotrauma if (target.CurrentHull == null) { return false; } if (character.Submarine != null) { - if (target.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } - if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; } + if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; } } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index 0ba2e4398..b3cd2cb0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -1,5 +1,4 @@ using Barotrauma.Items.Components; -using Microsoft.Xna.Framework; using Barotrauma.Extensions; namespace Barotrauma @@ -9,21 +8,24 @@ namespace Barotrauma public override string DebugTag => $"find diving gear ({gearTag})"; public override bool ForceRun => true; public override bool KeepDivingGearOn => true; + public override bool AbandonWhenCannotCompleteSubjectives => false; private readonly string gearTag; - private readonly string fallbackTag; private AIObjectiveGetItem getDivingGear; private AIObjectiveContainItem getOxygen; + private Item targetItem; - public static float lowOxygenThreshold = 10; + public static float MIN_OXYGEN = 10; + public static string HEAVY_DIVING_GEAR = "heavydiving"; + public static string LIGHT_DIVING_GEAR = "lightdiving"; + public static string OXYGEN_SOURCE = "oxygensource"; - protected override bool Check() => HumanAIController.HasItem(character, gearTag, out _, "oxygensource", requireEquipped: true) || HumanAIController.HasItem(character, fallbackTag, out _, "oxygensource", requireEquipped: true); + protected override bool Check() => targetItem != null && character.HasEquippedItem(targetItem); - public AIObjectiveFindDivingGear(Character character, bool needDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) + public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { - gearTag = needDivingSuit ? "divingsuit" : "divingmask"; - fallbackTag = needDivingSuit ? "divingsuit" : "diving"; + gearTag = needsDivingSuit ? HEAVY_DIVING_GEAR : LIGHT_DIVING_GEAR; } protected override void Act(float deltaTime) @@ -33,98 +35,95 @@ namespace Barotrauma Abandon = true; return; } - var item = character.Inventory.FindItemByIdentifier(gearTag, true) ?? character.Inventory.FindItemByTag(gearTag, true); - if (item == null && fallbackTag != gearTag) - { - item = character.Inventory.FindItemByTag(fallbackTag, true); - } - if (item == null || !character.HasEquippedItem(item)) + targetItem = character.Inventory.FindItemByTag(gearTag, true); + if (targetItem == null || !character.HasEquippedItem(targetItem)) { TryAddSubObjective(ref getDivingGear, () => { - if (item == null) + if (targetItem == null) { character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f); } - return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) { AllowToFindDivingGear = false }; + return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) + { + AllowToFindDivingGear = false, + AllowDangerousPressure = true + }; }, onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref getDivingGear)); } else { - var containedItems = item.ContainedItems; - if (containedItems == null) + if (!DropEmptyTanks(character, targetItem, out Item[] containedItems)) { #if DEBUG - DebugConsole.ThrowError($"{character.Name}: AIObjectiveFindDivingGear failed - the item \"" + item + "\" has no proper inventory"); + DebugConsole.ThrowError($"{character.Name}: AIObjectiveFindDivingGear failed - the item \"" + targetItem + "\" has no proper inventory"); #endif Abandon = true; return; } - // Drop empty tanks - foreach (Item containedItem in containedItems) + if (containedItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > MIN_OXYGEN)) { - if (containedItem == null) { continue; } - if (containedItem.Condition <= 0.0f) + // No valid oxygen source loaded. + // Seek oxygen that has min 10% condition left. + TryAddSubObjective(ref getOxygen, () => { - containedItem.Drop(character); - } - } - if (containedItems.None(it => it.HasTag("oxygensource") && it.Condition > lowOxygenThreshold)) - { - var oxygenTank = character.Inventory.FindItemByTag("oxygensource", true); - if (oxygenTank != null) - { - var container = item.GetComponent(); - if (container.Item.ParentInventory == character.Inventory) + character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f); + return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC) { - if (!container.Inventory.CanBePut(oxygenTank)) - { - Abandon = true; - } - character.Inventory.RemoveItem(oxygenTank); - if (!container.Inventory.TryPutItem(oxygenTank, null)) - { - oxygenTank.Drop(character); - Abandon = true; - } - } - else - { - container.Combine(oxygenTank, character); - } - } - else + AllowToFindDivingGear = false, + AllowDangerousPressure = true, + ConditionLevel = MIN_OXYGEN + }; + }, + onAbandon: () => { - // Seek oxygen that has min 10% condition left + // Try to seek any oxygen sources. TryAddSubObjective(ref getOxygen, () => { - character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f); - return new AIObjectiveContainItem(character, new string[] { "oxygensource" }, item.GetComponent(), objectiveManager) + return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC) { AllowToFindDivingGear = false, - ConditionLevel = lowOxygenThreshold + AllowDangerousPressure = true, + ConditionLevel = 0 }; - }, - onAbandon: () => - { - // Try to seek any oxygen sources - TryAddSubObjective(ref getOxygen, () => - { - return new AIObjectiveContainItem(character, new string[] { "oxygensource" }, item.GetComponent(), objectiveManager) - { - AllowToFindDivingGear = false, - ConditionLevel = 0 - }; - }, - onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref getOxygen)); }, + onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref getOxygen)); - } + }, + onCompleted: () => RemoveSubObjective(ref getOxygen)); } } } + + /// + /// Returns false only when no inventory can be found from the item. + /// + public static bool DropEmptyTanks(Character actor, Item target, out Item[] containedItems) + { + containedItems = target.OwnInventory?.Items; + if (containedItems == null) + { + return false; + } + foreach (Item containedItem in containedItems) + { + if (containedItem == null) { continue; } + if (containedItem.Condition <= 0.0f) + { + containedItem.Drop(actor); + } + } + return true; + } + + public override void Reset() + { + base.Reset(); + getDivingGear = null; + getOxygen = null; + targetItem = null; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index cae1b0a73..13d99b29f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -14,7 +14,8 @@ namespace Barotrauma public override bool IgnoreUnsafeHulls => true; public override bool ConcurrentObjectives => true; public override bool AllowOutsideSubmarine => true; - public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); } + public override bool AbandonWhenCannotCompleteSubjectives => false; + public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace); } // TODO: expose? const float priorityIncrease = 100; @@ -48,7 +49,7 @@ namespace Barotrauma } else { - if (HumanAIController.NeedsDivingGear(character, character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character)) + if (HumanAIController.NeedsDivingGear(character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character)) { Priority = 100; } @@ -64,6 +65,14 @@ namespace Barotrauma public override void Update(float deltaTime) { + if (retryTimer > 0) + { + retryTimer -= deltaTime; + if (retryTimer <= 0) + { + retryCounter = 0; + } + } if (resetPriority) { Priority = 0; @@ -92,39 +101,56 @@ namespace Barotrauma private Hull currentSafeHull; private Hull previousSafeHull; + private bool cannotFindSafeHull; + private bool cannotFindDivingGear; + private readonly int findDivingGearAttempts = 2; + private int retryCounter; + private readonly float retryResetTime = 5; + private float retryTimer; protected override void Act(float deltaTime) { var currentHull = character.CurrentHull; - bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0; - if (!dangerousPressure) + bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0 && character.PressureProtection <= 0; + if (!character.LockHands && (!dangerousPressure || cannotFindSafeHull)) { - // Don't try to seek diving gear if the pressure is dangerous. Just get out. - bool needsDivingGear = HumanAIController.NeedsDivingGear(character, currentHull, out bool needsDivingSuit); + bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit); bool needsEquipment = false; if (needsDivingSuit) { - needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold); + needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.MIN_OXYGEN); } else if (needsDivingGear) { - needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold); + needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.MIN_OXYGEN); } - if (needsEquipment && divingGearObjective == null && !character.LockHands) + if (needsEquipment) { - RemoveSubObjective(ref goToObjective); - TryAddSubObjective(ref divingGearObjective, + if (cannotFindDivingGear && retryCounter < findDivingGearAttempts) + { + retryTimer = retryResetTime; + retryCounter++; + needsDivingSuit = !needsDivingSuit; + RemoveSubObjective(ref divingGearObjective); + } + if (divingGearObjective == null) + { + cannotFindDivingGear = false; + RemoveSubObjective(ref goToObjective); + TryAddSubObjective(ref divingGearObjective, constructor: () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), onAbandon: () => { searchHullTimer = Math.Min(1, searchHullTimer); - // Don't reset the diving gear objective, because it's possible that there is no diving gear -> seek a safe hull and then reset so that we can check again. - }, + cannotFindDivingGear = true; + // Don't reset the diving gear objective, because it's possible that there is no diving gear -> seek a safe hull and then reset so that we can check again. + }, onCompleted: () => { resetPriority = true; searchHullTimer = Math.Min(1, searchHullTimer); RemoveSubObjective(ref divingGearObjective); }); + } } } if (divingGearObjective == null || !divingGearObjective.CanBeCompleted) @@ -142,6 +168,7 @@ namespace Barotrauma searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f); previousSafeHull = currentSafeHull; currentSafeHull = FindBestHull(allowChangingTheSubmarine: character.TeamID != Character.TeamType.FriendlyNPC); + cannotFindSafeHull = currentSafeHull == null || HumanAIController.NeedsDivingGear(currentSafeHull, out _); if (currentSafeHull == null) { currentSafeHull = previousSafeHull; @@ -153,35 +180,38 @@ namespace Barotrauma RemoveSubObjective(ref goToObjective); } TryAddSubObjective(ref goToObjective, - constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true) + constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true) + { + AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50) + }, + onCompleted: () => + { + if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD || + HumanAIController.NeedsDivingGear(currentHull, out bool needsSuit) && (needsSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingMask(character))) { - AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50) - }, - onCompleted: () => + resetPriority = true; + searchHullTimer = Math.Min(1, searchHullTimer); + } + RemoveSubObjective(ref goToObjective); + if (cannotFindDivingGear) { - if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD || - HumanAIController.NeedsDivingGear(character, currentHull, out bool needsSuit) && (needsSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingMask(character))) - { - resetPriority = true; - searchHullTimer = Math.Min(1, searchHullTimer); - } - RemoveSubObjective(ref goToObjective); // If diving gear objective failed, let's reset it here. RemoveSubObjective(ref divingGearObjective); - }, - onAbandon: () => + } + }, + onAbandon: () => + { + // Don't ignore any hulls if outside, because apparently it happens that we can't find a path, in which case we just want to try again. + // If we ignore the hull, it might be the only airlock in the target sub, which ignores the whole sub. + if (currentHull != null && goToObjective != null) { - // Don't ignore any hulls if outside, because apparently it happens that we can't find a path, in which case we just want to try again. - // If we ignore the hull, it might be the only airlock in the target sub, which ignores the whole sub. - if (currentHull != null && goToObjective != null) + if (goToObjective.Target is Hull hull) { - if (goToObjective.Target is Hull hull) - { - HumanAIController.UnreachableHulls.Add(hull); - } + HumanAIController.UnreachableHulls.Add(hull); } - RemoveSubObjective(ref goToObjective); - }); + } + RemoveSubObjective(ref goToObjective); + }); } else { @@ -194,12 +224,14 @@ namespace Barotrauma //goto objective doesn't exist (a safe hull not found, or a path to a safe hull not found) // -> attempt to manually steer away from hazards Vector2 escapeVel = Vector2.Zero; - // TODO: optimize - foreach (FireSource fireSource in HumanAIController.VisibleHulls.SelectMany(h => h.FireSources)) + foreach (Hull hull in HumanAIController.VisibleHulls) { - Vector2 dir = character.Position - fireSource.Position; - float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f); - escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier); + foreach (FireSource fireSource in hull.FireSources) + { + Vector2 dir = character.Position - fireSource.Position; + float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f); + escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier); + } } foreach (Character enemy in Character.CharacterList) { @@ -335,5 +367,17 @@ namespace Barotrauma } return bestHull; } + + public override void Reset() + { + base.Reset(); + goToObjective = null; + divingGearObjective = null; + currentSafeHull = null; + previousSafeHull = null; + retryCounter = 0; + cannotFindDivingGear = false; + cannotFindSafeHull = false; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 140eae463..8723d0f1a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -20,12 +20,12 @@ namespace Barotrauma private AIObjectiveGoTo gotoObjective; private AIObjectiveOperateItem operateObjective; - public bool IgnoreSeverityAndDistance { get; private set; } + public readonly bool isPriority; - public AIObjectiveFixLeak(Gap leak, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool ignoreSeverityAndDistance = false) : base (character, objectiveManager, priorityModifier) + public AIObjectiveFixLeak(Gap leak, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool isPriority = false) : base (character, objectiveManager, priorityModifier) { Leak = leak; - IgnoreSeverityAndDistance = ignoreSeverityAndDistance; + this.isPriority = isPriority; } protected override bool Check() => Leak.Open <= 0 || Leak.Removed; @@ -41,15 +41,21 @@ namespace Barotrauma { Priority = 0; } + else if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective()?.Leak == Leak)) + { + Priority = 0; + Abandon = true; + } else { float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X); float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y); // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally). // If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby. - float distanceFactor = IgnoreSeverityAndDistance || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, xDist + yDist * 3.0f)); - float severity = IgnoreSeverityAndDistance ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; - float max = Math.Min((AIObjectiveManager.OrderPriority - 1), 90); + float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f)); + float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; + float reduction = isPriority ? 1 : 2; + float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90); float devotion = CumulatedDevotion / 100; Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); } @@ -68,7 +74,7 @@ namespace Barotrauma } else { - var containedItems = weldingTool.ContainedItems; + var containedItems = weldingTool.OwnInventory?.Items; if (containedItems == null) { #if DEBUG @@ -86,7 +92,7 @@ namespace Barotrauma containedItem.Drop(character); } } - if (containedItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f)) + if (containedItems.None(i => i != null && i.HasTag("weldingfuel") && i.Condition > 0.0f)) { TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC), onAbandon: () => Abandon = true, @@ -130,11 +136,10 @@ namespace Barotrauma { TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(Leak, character, objectiveManager) { - // Disabled for now - //AllowGoingOutside = !Leak.IsRoomToRoom && objectiveManager.IsCurrentOrder() && HumanAIController.HasDivingSuit(character, conditionPercentage: 50), CloseEnough = reach, DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak" : null, - TargetName = Leak.FlowTargetHull?.DisplayName + TargetName = Leak.FlowTargetHull?.DisplayName, + CheckVisibility = false }, onAbandon: () => { @@ -153,5 +158,14 @@ namespace Barotrauma onCompleted: () => RemoveSubObjective(ref gotoObjective)); } } + + public override void Reset() + { + base.Reset(); + getWeldingTool = null; + refuelObjective = null; + gotoObjective = null; + operateObjective = null; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index 995d71611..f77e9fb0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -37,11 +37,9 @@ namespace Barotrauma protected override float TargetEvaluation() { - int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective() && !c.Character.IsIncapacitated, onlyBots: true); int totalLeaks = Targets.Count(); if (totalLeaks == 0) { return 0; } - int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom); - int leaks = totalLeaks - secondaryLeaks; + int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective() && !c.Character.IsIncapacitated, onlyBots: true); bool anyFixers = otherFixers > 0; if (objectiveManager.CurrentOrder == this) { @@ -50,6 +48,8 @@ namespace Barotrauma } else { + int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom); + int leaks = totalLeaks - secondaryLeaks; float ratio = leaks == 0 ? 1 : anyFixers ? leaks / otherFixers : 1; if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f)) { @@ -62,7 +62,7 @@ namespace Barotrauma protected override IEnumerable GetList() => Gap.GapList; protected override AIObjective ObjectiveConstructor(Gap gap) - => new AIObjectiveFixLeak(gap, character, objectiveManager, priorityModifier: PriorityModifier, ignoreSeverityAndDistance: gap.FlowTargetHull == PrioritizedHull); + => new AIObjectiveFixLeak(gap, character, objectiveManager, priorityModifier: PriorityModifier, isPriority: gap.FlowTargetHull == PrioritizedHull); protected override void OnObjectiveCompleted(AIObjective objective, Gap target) => HumanAIController.RemoveTargets(character, target); @@ -75,8 +75,7 @@ namespace Barotrauma if (gap.Submarine.TeamID != character.TeamID) { return false; } if (character.Submarine != null) { - if (gap.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } - if (!character.Submarine.IsEntityFoundOnThisSub(gap, true)) { return false; } + if (!character.Submarine.IsConnectedTo(gap.Submarine)) { return false; } } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 0ce4b3aa2..4defd8d0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -3,7 +3,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -17,10 +16,9 @@ namespace Barotrauma public Func GetItemPriority; public Func ItemFilter; public float TargetCondition { get; set; } = 1; + public bool AllowDangerousPressure { get; set; } - //can be either tags or identifiers - private string[] itemIdentifiers; - public IEnumerable Identifiers => itemIdentifiers; + private string[] identifiersOrTags; //if the item can't be found, spawn it in the character's inventory (used by outpost NPCs) private bool spawnItemIfNotFound = false; @@ -50,26 +48,26 @@ namespace Barotrauma moveToTarget = targetItem?.GetRootInventoryOwner(); } - public AIObjectiveGetItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false) - : this(character, new string[] { itemIdentifier }, objectiveManager, equip, checkInventory, priorityModifier, spawnItemIfNotFound) { } + public AIObjectiveGetItem(Character character, string identifierOrTag, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false) + : this(character, new string[] { identifierOrTag }, objectiveManager, equip, checkInventory, priorityModifier, spawnItemIfNotFound) { } - public AIObjectiveGetItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false) + public AIObjectiveGetItem(Character character, string[] identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false) : base(character, objectiveManager, priorityModifier) { currSearchIndex = -1; this.equip = equip; - this.itemIdentifiers = itemIdentifiers; + this.identifiersOrTags = identifiersOrTags; this.spawnItemIfNotFound = spawnItemIfNotFound; - for (int i = 0; i < itemIdentifiers.Length; i++) + for (int i = 0; i < identifiersOrTags.Length; i++) { - itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant(); + identifiersOrTags[i] = identifiersOrTags[i].ToLowerInvariant(); } this.checkInventory = checkInventory; } private bool CheckInventory() { - if (itemIdentifiers == null) { return false; } + if (identifiersOrTags == null) { return false; } var item = character.Inventory.FindItem(i => CheckItem(i), recursive: true); if (item != null) { @@ -86,7 +84,7 @@ namespace Barotrauma Abandon = true; return; } - if (itemIdentifiers != null && !isDoneSeeking) + if (identifiersOrTags != null && !isDoneSeeking) { if (checkInventory) { @@ -97,14 +95,18 @@ namespace Barotrauma } if (!isDoneSeeking) { - bool dangerousPressure = character.CurrentHull == null || character.CurrentHull.LethalPressure > 0; - if (dangerousPressure) + if (!AllowDangerousPressure) { + bool dangerousPressure = character.CurrentHull == null || character.CurrentHull.LethalPressure > 0 && character.PressureProtection <= 0; + if (dangerousPressure) + { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Seeking item aborted, because the pressure is dangerous.", Color.Yellow); + string itemName = targetItem != null ? targetItem.Name : identifiersOrTags.FirstOrDefault(); + DebugConsole.NewMessage($"{character.Name}: Seeking item ({itemName}) aborted, because the pressure is dangerous.", Color.Yellow); #endif - Abandon = true; - return; + Abandon = true; + return; + } } FindTargetItem(); objectiveManager.GetObjective().Wander(deltaTime); @@ -174,34 +176,20 @@ namespace Barotrauma return; } - if (equip) + if (HumanAIController.TryToMoveItem(targetItem, character.Inventory)) { - if (HumanAIController.TryToMoveItem(targetItem, character.Inventory)) + if (equip) { targetItem.Equip(character); - IsCompleted = true; - } - else - { -#if DEBUG - DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red); -#endif - Abandon = true; } + IsCompleted = true; } else { - if (character.Inventory.TryPutItem(targetItem, character, new List() { InvSlotType.Any })) - { - IsCompleted = true; - } - else - { - Abandon = true; #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red); + DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red); #endif - } + Abandon = true; } } else if (moveToTarget != null) @@ -228,7 +216,7 @@ namespace Barotrauma private void FindTargetItem() { - if (itemIdentifiers == null) + if (identifiersOrTags == null) { if (targetItem == null) { @@ -244,18 +232,16 @@ namespace Barotrauma currSearchIndex++; var item = Item.ItemList[currSearchIndex]; Submarine itemSub = item.Submarine ?? item.ParentInventory?.Owner?.Submarine; + Submarine mySub = character.Submarine; if (itemSub == null) { continue; } - if (itemSub.TeamID != character.TeamID) { continue; } + if (mySub == null) { continue; } + if (itemSub.TeamID != mySub.TeamID && itemSub.TeamID != character.TeamID) { continue; } if (!CheckItem(item)) { continue; } if (ignoredContainerIdentifiers != null && item.Container != null) { if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; } } - if (character.Submarine != null) - { - if (itemSub.Info.Type != character.Submarine.Info.Type) { continue; } - if (character.Submarine.GetConnectedSubs().None(s => s == itemSub && itemSub.TeamID == character.TeamID && itemSub.Info.Type == character.Submarine.Info.Type)) { continue; } - } + if (!mySub.IsConnectedTo(itemSub)) { continue; } if (character.IsItemTakenBySomeoneElse(item)) { continue; } float itemPriority = 1; if (GetItemPriority != null) @@ -283,10 +269,10 @@ namespace Barotrauma { if (spawnItemIfNotFound) { - if (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && itemIdentifiers.Any(id => id == ip.Identifier || ip.Tags.Contains(id))) is ItemPrefab prefab)) + if (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && identifiersOrTags.Any(id => id == ip.Identifier || ip.Tags.Contains(id))) is ItemPrefab prefab)) { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow); + DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", identifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow); #endif Abandon = true; } @@ -305,7 +291,7 @@ namespace Barotrauma else { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}", Color.Yellow); + DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow); #endif Abandon = true; } @@ -320,7 +306,7 @@ namespace Barotrauma { return character.HasItem(targetItem, equip); } - else if (itemIdentifiers != null) + else if (identifiersOrTags != null) { var matchingItem = character.Inventory.FindItem(i => CheckItem(i), recursive: true); if (matchingItem != null) @@ -338,13 +324,13 @@ namespace Barotrauma if (ignoredItems.Contains(item)) { return false; }; if (item.Condition < TargetCondition) { return false; } if (ItemFilter != null && !ItemFilter(item)) { return false; } - return itemIdentifiers.Any(id => id == item.Prefab.Identifier || item.HasTag(id)); + return identifiersOrTags.Any(id => id == item.Prefab.Identifier || item.HasTag(id)); } public override void Reset() { base.Reset(); - RemoveSubObjective(ref goToObjective); + goToObjective = null; targetItem = originalTarget; moveToTarget = targetItem?.GetRootInventoryOwner(); isDoneSeeking = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 0530ef39e..1430b5155 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -1,7 +1,6 @@ using Microsoft.Xna.Framework; using System; using System.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -31,7 +30,7 @@ namespace Barotrauma public bool mimic; private float _closeEnough = 50; - private readonly float minDistance = 25; + private readonly float minDistance = 50; /// /// Display units /// @@ -43,6 +42,8 @@ namespace Barotrauma _closeEnough = Math.Max(minDistance, value); } } + + public bool CheckVisibility { get; set; } public bool IgnoreIfTargetDead { get; set; } public bool AllowGoingOutside { get; set; } @@ -103,7 +104,7 @@ namespace Barotrauma else if (Target is Character) { //if closeEnough value is given, allow setting CloseEnough as low as 50, otherwise above AIObjectiveGetItem.DefaultReach - CloseEnough = Math.Max(closeEnough, MathUtils.NearlyEqual(closeEnough, 0.0f) ? AIObjectiveGetItem.DefaultReach : 50); + CloseEnough = Math.Max(closeEnough, MathUtils.NearlyEqual(closeEnough, 0.0f) ? AIObjectiveGetItem.DefaultReach : minDistance); } else { @@ -138,7 +139,7 @@ namespace Barotrauma } Target = Character.Controlled; } - if (Target == character) + if (Target == character || character.SelectedBy != null && HumanAIController.IsFriendly(character.SelectedBy)) { // Wait character.AIController.SteeringManager.Reset(); @@ -207,7 +208,7 @@ namespace Barotrauma { Character followTarget = Target as Character; bool needsDivingSuit = targetIsOutside; - bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(character, targetHull, out needsDivingSuit); + bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit); if (!needsDivingGear && mimic) { if (HumanAIController.HasDivingSuit(followTarget)) @@ -223,17 +224,25 @@ namespace Barotrauma bool needsEquipment = false; if (needsDivingSuit) { - needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold); + needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.MIN_OXYGEN); } else if (needsDivingGear) { - needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold); + needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.MIN_OXYGEN); } if (needsEquipment) { - TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), - onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref findDivingGear)); + if (findDivingGear != null && !findDivingGear.CanBeCompleted) + { + TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), + onAbandon: () => Abandon = true, + onCompleted: () => RemoveSubObjective(ref findDivingGear)); + } + else + { + TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), + onCompleted: () => RemoveSubObjective(ref findDivingGear)); + } return; } } @@ -262,11 +271,14 @@ namespace Barotrauma { if (n.Waypoint.isObstructed) { return false; } return (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null); - }, endNodeFilter, nodeFilter); + }, endNodeFilter, nodeFilter, CheckVisibility); if (!isInside && PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable) { SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition)); - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15); + if (character.AnimController.InWater) + { + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15); + } } } else @@ -392,5 +404,11 @@ namespace Barotrauma HumanAIController.FaceTarget(Target); base.OnCompleted(); } + + public override void Reset() + { + base.Reset(); + findDivingGear = null; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 87c4aeaa2..28ad9c764 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -10,7 +10,7 @@ namespace Barotrauma class AIObjectiveIdle : AIObjective { public override string DebugTag => "idle"; - public override bool UnequipItems => true; + public override bool AllowAutomaticItemUnequipping => true; public override bool AllowOutsideSubmarine => true; private BehaviorType behavior; @@ -69,6 +69,8 @@ namespace Barotrauma const float chairCheckInterval = 5.0f; private float chairCheckTimer; + private float autonomousObjectiveRetryTimer = 10; + private readonly List targetHulls = new List(20); private readonly List hullWeights = new List(20); @@ -88,11 +90,6 @@ namespace Barotrauma public readonly HashSet PreferredOutpostModuleTypes = new HashSet(); - private bool IsInWrongSub() => - character.Submarine == null || - currentTarget != null && currentTarget.Submarine != character.Submarine || - character.TeamID == Character.TeamType.FriendlyNPC && character.Submarine.TeamID != character.TeamID; - public void CalculatePriority(float max = 0) { //Random = Rand.Range(0.5f, 1.5f); @@ -149,6 +146,18 @@ namespace Barotrauma { if (PathSteering == null) { return; } + if (objectiveManager.FailedAutonomousObjectives) + { + if (autonomousObjectiveRetryTimer > 0) + { + autonomousObjectiveRetryTimer -= deltaTime; + } + else + { + objectiveManager.CreateAutonomousObjectives(); + } + } + //don't keep dragging others when idling if (character.SelectedCharacter != null) { @@ -160,13 +169,34 @@ namespace Barotrauma bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); - bool IsSteeringFinished() => PathSteering.CurrentPath != null && PathSteering.CurrentPath.Finished; + bool IsSteeringFinished() => PathSteering.CurrentPath != null && (PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable); - if (currentTargetIsInvalid || currentTarget == null || IsSteeringFinished() && (IsForbidden(character.CurrentHull) || IsInWrongSub())) + if (currentTarget != null && !currentTargetIsInvalid) { - //don't reset to zero, otherwise the character will keep calling FindTargetHulls - //almost constantly when there's a small number of potential hulls to move to - SetTargetTimerLow(); + if (character.TeamID == Character.TeamType.FriendlyNPC) + { + if (currentTarget.Submarine.TeamID != character.TeamID) + { + currentTargetIsInvalid = true; + } + } + else + { + if (currentTarget.Submarine != character.Submarine) + { + currentTargetIsInvalid = true; + } + } + } + + if (currentTargetIsInvalid || currentTarget == null || IsForbidden(character.CurrentHull) && IsSteeringFinished()) + { + if (newTargetTimer > timerMargin) + { + //don't reset to zero, otherwise the character will keep calling FindTargetHulls + //almost constantly when there's a small number of potential hulls to move to + SetTargetTimerLow(); + } } else if (character.IsClimbing) { @@ -200,7 +230,8 @@ namespace Barotrauma { //choose a random available hull currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced); - bool isCurrentHullAllowed = !IsInWrongSub() && !IsForbidden(character.CurrentHull); + bool isInWrongSub = character.TeamID == Character.TeamType.FriendlyNPC && character.Submarine.TeamID != character.TeamID; + bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull); var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: $"AIObjectiveIdle {character.DisplayName}", nodeFilter: node => { if (node.Waypoint.CurrentHull == null) { return false; } @@ -285,12 +316,12 @@ namespace Barotrauma if (standStillTimer > 0.0f) { walkDuration = Rand.Range(walkDurationMin, walkDurationMax); - - if (character.CurrentHull != null && character.CurrentHull.Rect.Width > 150 && tooCloseCharacter == null) + var currentHull = character.CurrentHull; + if (currentHull != null && currentHull.Rect.Width > IndoorsSteeringManager.smallRoomSize / 2 && tooCloseCharacter == null) { foreach (Character c in Character.CharacterList) { - if (c == character || !c.IsBot || c.CurrentHull != character.CurrentHull || !(c.AIController is HumanAIController humanAI)) { continue; } + if (c == character || !c.IsBot || c.CurrentHull != currentHull || !(c.AIController is HumanAIController humanAI)) { continue; } if (Vector2.DistanceSquared(c.WorldPosition, character.WorldPosition) > 60.0f * 60.0f) { continue; } if ((humanAI.ObjectiveManager.CurrentObjective is AIObjectiveIdle idleObjective && idleObjective.standStillTimer > 0.0f) || (humanAI.ObjectiveManager.CurrentObjective is AIObjectiveGoTo gotoObjective && gotoObjective.IsCloseEnough)) @@ -303,7 +334,7 @@ namespace Barotrauma tooCloseCharacter = null; break; } - tooCloseCharacter = c; + tooCloseCharacter = c; } HumanAIController.FaceTarget(c); } @@ -313,9 +344,24 @@ namespace Barotrauma { Vector2 diff = character.WorldPosition - tooCloseCharacter.WorldPosition; if (diff.LengthSquared() < 0.0001f) { diff = Rand.Vector(1.0f); } - if (diff.X > 0 && character.WorldPosition.X > character.CurrentHull.WorldRect.Right - 50) { diff.X = -diff.X; } - if (diff.X < 0 && character.WorldPosition.X < character.CurrentHull.WorldRect.X + 50) { diff.X = -diff.X; } - PathSteering.SteeringManual(deltaTime, Vector2.Normalize(diff)); + if (Math.Abs(diff.X) > 0 && + (character.WorldPosition.X > currentHull.WorldRect.Right - 50 || character.WorldPosition.X < currentHull.WorldRect.Left + 50)) + { + // Between a wall and a character -> move away + tooCloseCharacter = null; + PathSteering.Reset(); + standStillTimer = 0; + walkDuration = Math.Min(walkDuration, walkDurationMin); + if (Behavior != BehaviorType.StayInHull && (currentHull.Size.X < IndoorsSteeringManager.smallRoomSize || currentHull.Size.X < (IndoorsSteeringManager.smallRoomSize / 2 * Character.CharacterList.Count(c => c.CurrentHull == currentHull)))) + { + // Small room -> find another + newTargetTimer = Math.Min(newTargetTimer, 1); + } + } + else + { + PathSteering.SteeringManual(deltaTime, Vector2.Normalize(diff)); + } return; } else @@ -329,14 +375,13 @@ namespace Barotrauma { foreach (Item item in Item.ItemList) { - if (item.CurrentHull != character.CurrentHull || !item.HasTag("chair")) { continue; } + if (item.CurrentHull != currentHull || !item.HasTag("chair")) { continue; } var controller = item.GetComponent(); if (controller == null || controller.User != null) { continue; } item.TryInteract(character, forceSelectKey: true); } chairCheckTimer = chairCheckInterval; } - return; } if (standStillTimer < -walkDuration) @@ -376,7 +421,9 @@ namespace Barotrauma } if (IsForbidden(hull)) { continue; } // Check that the hull is linked - if (!character.Submarine.GetConnectedSubs().Contains(hull.Submarine)) { continue; } + if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; } + // Ignore very narrow hulls. + if (hull.RectWidth < 200) { continue; } // Ignore hulls that are too low to stand inside. if (character.AnimController is HumanoidAnimController animController) { @@ -388,13 +435,14 @@ namespace Barotrauma if (!targetHulls.Contains(hull)) { targetHulls.Add(hull); - float weight = hull.Volume; + float weight = hull.RectWidth; // Prefer rooms that are closer. Avoid rooms that are not in the same level. float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y); yDist = yDist > 100 ? yDist * 5 : 0; float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist; float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 2500, dist)); - weight *= distanceFactor; + float waterFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 100, hull.WaterPercentage * 2)); + weight *= distanceFactor * waterFactor; hullWeights.Add(weight); } } @@ -418,5 +466,16 @@ namespace Barotrauma if (hullName == null) { return false; } return hullName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || hullName.Contains("airlock", StringComparison.OrdinalIgnoreCase); } + + public override void Reset() + { + base.Reset(); + currentTarget = null; + searchingNewHull = false; + tooCloseCharacter = null; + targetHulls.Clear(); + hullWeights.Clear(); + autonomousObjectiveRetryTimer = 10; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index 7d51adb9f..bde055907 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Barotrauma.Extensions; using Microsoft.Xna.Framework; @@ -89,10 +88,6 @@ namespace Barotrauma { syncTimer -= deltaTime; } - if (Objectives.None() && Targets.Any(t => !ignoreList.Contains(t))) - { - CreateObjectives(); - } } // the timer is set between 1 and 10 seconds, depending on the priority modifier and a random +-25% @@ -113,7 +108,7 @@ namespace Barotrauma Priority = 0; return Priority; } - if (character.LockHands || character.Submarine == null || Targets.None()) + if (character.LockHands || character.Submarine == null) { Priority = 0; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 6e9577fbb..4682b8b37 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -92,6 +92,7 @@ namespace Barotrauma } public Dictionary DelayedObjectives { get; private set; } = new Dictionary(); + public bool FailedAutonomousObjectives { get; private set; } private void ClearIgnored() { @@ -118,10 +119,11 @@ namespace Barotrauma } DelayedObjectives.Clear(); Objectives.Clear(); + FailedAutonomousObjectives = false; AddObjective(new AIObjectiveFindSafety(character, this)); AddObjective(new AIObjectiveIdle(character, this)); int objectiveCount = Objectives.Count; - foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjective) + foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives) { var orderPrefab = Order.GetPrefab(autonomousObjective.identifier); if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.identifier}'"); } @@ -129,6 +131,7 @@ namespace Barotrauma var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType), orderGiver: character); if (order == null) { continue; } + if (autonomousObjective.ignoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != Character.TeamType.FriendlyNPC) { continue; } var objective = CreateObjective(order, autonomousObjective.option, character, isAutonomous: true, autonomousObjective.priorityModifier); if (objective != null && objective.CanBeCompleted) { @@ -220,7 +223,7 @@ namespace Barotrauma if (objective.IsCompleted) { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it is completed.", Color.LightGreen); + DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it is completed.", Color.LightBlue); #endif Objectives.Remove(objective); } @@ -230,6 +233,7 @@ namespace Barotrauma DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it cannot be completed.", Color.Red); #endif Objectives.Remove(objective); + FailedAutonomousObjectives = true; } else { @@ -286,6 +290,7 @@ namespace Barotrauma } else { + // This should be redundant, because all the objectives are reset when they are selected as active. CurrentOrder.Reset(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 35cff58da..0e0cfbbca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -8,8 +8,9 @@ namespace Barotrauma { class AIObjectiveOperateItem : AIObjective { - public override string DebugTag => "operate item"; - public override bool UnequipItems => true; + public override string DebugTag => $"operate item {component.Name}"; + public override bool AllowAutomaticItemUnequipping => true; + public override bool AllowMultipleInstances => true; private ItemComponent component, controller; private Entity operateTarget; @@ -22,6 +23,8 @@ namespace Barotrauma public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null); + public override bool IsDuplicate(T otherObjective) => base.IsDuplicate(otherObjective) && otherObjective is AIObjectiveOperateItem operateObjective && operateObjective.component == component; + public Entity OperateTarget => operateTarget; public ItemComponent Component => component; @@ -32,7 +35,7 @@ namespace Barotrauma public override float GetPriority() { - if (!IsAllowed) + if (!IsAllowed || character.LockHands) { Priority = 0; return Priority; @@ -43,7 +46,8 @@ namespace Barotrauma } else { - if (objectiveManager.CurrentOrder == this) + bool isOrder = objectiveManager.CurrentOrder == this; + if (isOrder) { Priority = AIObjectiveManager.OrderPriority; } @@ -61,6 +65,16 @@ namespace Barotrauma var reactor = component?.Item.GetComponent(); if (reactor != null) { + if (!isOrder) + { + if (reactor.LastUserWasPlayer && character.TeamID != Character.TeamType.FriendlyNPC || + HumanAIController.IsTrueForAnyCrewMember(c => + c.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.GetTarget() == target)) + { + Priority = 0; + return Priority; + } + } switch (Option) { case "shutdown": @@ -73,7 +87,7 @@ namespace Barotrauma case "powerup": // Check that we don't already have another order that is targeting the same item. // Without this the autonomous objective will tell the bot to turn the reactor on again. - if (objectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder != this && operateOrder.GetTarget() == target) + if (objectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder != this && operateOrder.GetTarget() == target && operateOrder.Option != Option) { Priority = 0; return Priority; @@ -82,7 +96,7 @@ namespace Barotrauma } } if (targetItem.CurrentHull == null || - targetItem.Submarine != character.Submarine && objectiveManager.CurrentOrder != this || + targetItem.Submarine != character.Submarine && !isOrder || targetItem.CurrentHull.FireSources.Any() || HumanAIController.IsItemOperatedByAnother(target, out _) || Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) @@ -92,7 +106,12 @@ namespace Barotrauma else { float value = CumulatedDevotion + (AIObjectiveManager.OrderPriority * PriorityModifier); - float max = objectiveManager.CurrentOrder == this ? MathHelper.Min(AIObjectiveManager.OrderPriority, 90) : AIObjectiveManager.RunPriority - 1; + float max = isOrder ? MathHelper.Min(AIObjectiveManager.OrderPriority, 90) : AIObjectiveManager.RunPriority - 1; + if (!isOrder && reactor != null && reactor.PowerOn && Option == "powerup") + { + // Decrease the priority when targeting a reactor that is already on. + value /= 2; + } Priority = MathHelper.Clamp(value, 0, max); } } @@ -142,6 +161,15 @@ namespace Barotrauma // Don't abandon return; } + if (operateTarget != null) + { + if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget)) + { + // Another crew member is already targeting this entity. + Abandon = true; + return; + } + } if (target.CanBeSelected) { if (character.CanInteractWith(target.Item, out _, checkLinked: false)) @@ -227,5 +255,12 @@ namespace Barotrauma } protected override bool Check() => isDoneOperating && !IsLoop; + + public override void Reset() + { + base.Reset(); + goToObjective = null; + getItemObjective = null; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index a7fe83d4b..332d34fda 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; +using Barotrauma.Extensions; namespace Barotrauma { @@ -10,7 +11,7 @@ namespace Barotrauma { public override string DebugTag => "pump water"; public override bool KeepDivingGearOn => true; - public override bool UnequipItems => true; + public override bool AllowAutomaticItemUnequipping => true; private IEnumerable pumpList; @@ -35,8 +36,7 @@ namespace Barotrauma if (pump.Item.CurrentHull.FireSources.Count > 0) { return false; } if (character.Submarine != null) { - if (pump.Item.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } - if (!character.Submarine.IsEntityFoundOnThisSub(pump.Item, true)) { return false; } + if (!character.Submarine.IsConnectedTo(pump.Item.Submarine)) { return false; } } if (Character.CharacterList.Any(c => c.CurrentHull == pump.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } if (IsReady(pump)) { return false; } @@ -54,6 +54,7 @@ namespace Barotrauma protected override float TargetEvaluation() { + if (Targets.None()) { return 0; } if (Option == "stoppumping") { return Targets.Max(t => MathHelper.Lerp(0, 100, Math.Abs(t.FlowPercentage / 100))); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 3aba66f2d..b363f8999 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -9,7 +9,6 @@ namespace Barotrauma class AIObjectiveRepairItem : AIObjective { public override string DebugTag => "repair item"; - public override bool KeepDivingGearOn => true; public Item Item { get; private set; } @@ -18,9 +17,11 @@ namespace Barotrauma private float previousCondition = -1; private RepairTool repairTool; - private bool IsRepairing => character.SelectedConstruction == Item && Item.GetComponent()?.CurrentFixer == character; + private bool IsRepairing() => IsRepairing(character, Item); private readonly bool isPriority; + public static bool IsRepairing(Character character, Item item) => character.SelectedConstruction == item && item.Repairables.Any(r => r.CurrentFixer == character); + public AIObjectiveRepairItem(Character character, Item item, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool isPriority = false) : base(character, objectiveManager, priorityModifier) { @@ -49,12 +50,15 @@ namespace Barotrauma float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y); yDist = yDist > 100 ? yDist * 5 : 0; float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist; - distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist)); + distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 4000, dist)); } - float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character, requiredSuccessFactor: objectiveManager.CurrentOrder != this ? AIObjectiveRepairItems.RequiredSuccessFactor : 0); - float isSelected = IsRepairing ? 50 : 0; - float devotion = (CumulatedDevotion + isSelected) / 100; - float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90); + float requiredSuccessFactor = objectiveManager.IsCurrentOrder() ? 0 : AIObjectiveRepairItems.RequiredSuccessFactor; + float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character, requiredSuccessFactor) / 100; + bool isSelected = IsRepairing(); + float selectedBonus = isSelected ? 100 - MaxDevotion : 0; + float devotion = (CumulatedDevotion + selectedBonus) / 100; + float reduction = isPriority ? 1 : isSelected ? 2 : 3; + float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90); Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); } return Priority; @@ -63,7 +67,7 @@ namespace Barotrauma protected override bool Check() { IsCompleted = Item.IsFullCondition; - if (IsCompleted && IsRepairing) + if (IsCompleted && IsRepairing()) { character?.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f); } @@ -95,7 +99,7 @@ namespace Barotrauma } if (repairTool != null) { - var containedItems = repairTool.Item.ContainedItems; + var containedItems = repairTool.Item.OwnInventory?.Items; if (containedItems == null) { #if DEBUG @@ -118,13 +122,13 @@ namespace Barotrauma foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained]) { item = requiredItem; - fuel = containedItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it)); + fuel = containedItems.FirstOrDefault(it => it != null && it.Condition > 0.0f && requiredItem.MatchesItem(it)); if (fuel != null) { break; } } if (fuel == null) { RemoveSubObjective(ref goToObjective); - TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent(), objectiveManager), + TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC), onCompleted: () => RemoveSubObjective(ref refuelObjective), onAbandon: () => Abandon = true); return; @@ -142,7 +146,7 @@ namespace Barotrauma if (repairable.CurrentFixer != null && repairable.CurrentFixer != character) { // Someone else is repairing the target. Abandon the objective if the other is better at this than us. - Abandon = repairable.DegreeOfSuccess(character) < repairable.DegreeOfSuccess(repairable.CurrentFixer); + Abandon = repairable.CurrentFixer.IsPlayer || repairable.DegreeOfSuccess(character) < repairable.DegreeOfSuccess(repairable.CurrentFixer); } if (!Abandon) { @@ -166,7 +170,7 @@ namespace Barotrauma } if (Abandon) { - if (IsRepairing) + if (IsRepairing()) { character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f); } @@ -201,7 +205,7 @@ namespace Barotrauma onAbandon: () => { Abandon = true; - if (IsRepairing) + if (IsRepairing()) { character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f); } @@ -251,5 +255,14 @@ namespace Barotrauma repairTool.Use(deltaTime, character); } } + + public override void Reset() + { + base.Reset(); + goToObjective = null; + refuelObjective = null; + previousCondition = -1; + repairTool = null; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index 2d71c9334..101f66cf9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -90,18 +90,23 @@ namespace Barotrauma protected override float TargetEvaluation() { - if (character.SelectedConstruction != null && Targets.Any(t => character.SelectedConstruction == t && t.ConditionPercentage < 100)) + var selectedItem = character.SelectedConstruction; + if (selectedItem != null && AIObjectiveRepairItem.IsRepairing(character, selectedItem) && selectedItem.ConditionPercentage < 100) { - // Don't stop fixing until done + // Don't stop fixing until completely done return 100; } int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective() && !c.Character.IsIncapacitated, onlyBots: true); int items = Targets.Count; + if (items == 0) + { + return 0; + } bool anyFixers = otherFixers > 0; float ratio = anyFixers ? items / (float)otherFixers : 1; if (objectiveManager.CurrentOrder == this) { - return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio; + return Targets.Sum(t => 100 - t.ConditionPercentage); } else { @@ -151,8 +156,7 @@ namespace Barotrauma if (item.Repairables.None()) { return false; } if (character.Submarine != null) { - if (item.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } - if (!character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; } + if (!character.Submarine.IsConnectedTo(item.Submarine)) { return false; } } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 9257848c9..25512b72a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -21,10 +22,12 @@ namespace Barotrauma private readonly Character targetCharacter; private AIObjectiveGoTo goToObjective; + private AIObjectiveContainItem replaceOxygenObjective; private AIObjectiveGetItem getItemObjective; private float treatmentTimer; private Hull safeHull; private float findHullTimer; + private bool ignoreOxygen; private readonly float findHullInterval = 1.0f; public AIObjectiveRescue(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1) @@ -69,65 +72,130 @@ namespace Barotrauma } if (targetCharacter != character) { - // Incapacitated target is not in a safe place -> Move to a safe place first - if (targetCharacter.IsIncapacitated && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD) + if (targetCharacter.IsIncapacitated) { - if (character.SelectedCharacter != targetCharacter) + // Check if the character needs more oxygen + if (!ignoreOxygen && character.SelectedCharacter == targetCharacter || character.CanInteractWith(targetCharacter)) { - if (targetCharacter.CurrentHull.DisplayName != null) + // Replace empty oxygen tank + // First remove empty tanks + if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out IEnumerable suits, requireEquipped: true)) { - character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" }, - new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }), - null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f); - } - - // Go to the target and select it - if (!character.CanInteractWith(targetCharacter)) - { - RemoveSubObjective(ref goToObjective); - TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) + Item suit = suits.FirstOrDefault(); + if (suit != null) { - CloseEnough = CloseEnoughToTreat, - DialogueIdentifier = "dialogcannotreachpatient", - TargetName = targetCharacter.DisplayName - }, + AIObjectiveFindDivingGear.DropEmptyTanks(character, suit, out _); + } + } + else if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out IEnumerable masks, requireEquipped: true)) + { + Item mask = masks.FirstOrDefault(); + if (mask != null) + { + AIObjectiveFindDivingGear.DropEmptyTanks(character, mask, out _); + } + } + bool ShouldRemoveDivingSuit() => targetCharacter.OxygenAvailable < CharacterHealth.InsufficientOxygenThreshold && targetCharacter.CurrentHull?.LethalPressure <= 0; + if (ShouldRemoveDivingSuit()) + { + suits.ForEach(suit => suit.Drop(character)); + } + else if (suits.Any() && suits.None(s => s.OwnInventory?.Items != null && s.OwnInventory.Items.Any(it => it != null && it.HasTag(AIObjectiveFindDivingGear.OXYGEN_SOURCE) && it.ConditionPercentage > 0))) + { + // The target has a suit equipped with an empty oxygen tank. + // Can't remove the suit, because the target needs it. + // If we happen to have an extra oxygen tank in the inventory, let's swap it. + Item spareOxygenTank = FindOxygenTank(targetCharacter) ?? FindOxygenTank(character); + if (spareOxygenTank != null) + { + Item suit = suits.FirstOrDefault(); + if (suit != null) + { + // Insert the new oxygen tank + TryAddSubObjective(ref replaceOxygenObjective, () => new AIObjectiveContainItem(character, spareOxygenTank, suit.GetComponent(), objectiveManager), + onCompleted: () => RemoveSubObjective(ref replaceOxygenObjective), + onAbandon: () => + { + RemoveSubObjective(ref replaceOxygenObjective); + ignoreOxygen = true; + if (ShouldRemoveDivingSuit()) + { + suits.ForEach(suit => suit.Drop(character)); + } + }); + return; + } + } + + Item FindOxygenTank(Character c) => + c.Inventory.FindItem(i => + i.HasTag(AIObjectiveFindDivingGear.OXYGEN_SOURCE) && + i.ConditionPercentage > 1 && + i.FindParentInventory(inv => inv.Owner is Item otherItem && otherItem.HasTag("diving")) == null, + recursive: true); + } + } + if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD) + { + // Incapacitated target is not in a safe place -> Move to a safe place first + if (character.SelectedCharacter != targetCharacter) + { + if (targetCharacter.CurrentHull != null && HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null) + { + character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" }, + new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }), + null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f); + } + // Go to the target and select it + if (!character.CanInteractWith(targetCharacter)) + { + RemoveSubObjective(ref replaceOxygenObjective); + RemoveSubObjective(ref goToObjective); + TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) + { + CloseEnough = CloseEnoughToTreat, + DialogueIdentifier = "dialogcannotreachpatient", + TargetName = targetCharacter.DisplayName + }, onCompleted: () => RemoveSubObjective(ref goToObjective), onAbandon: () => { RemoveSubObjective(ref goToObjective); Abandon = true; }); - } - else - { - character.SelectCharacter(targetCharacter); - } - } - else - { - // Drag the character into safety - if (safeHull == null) - { - if (findHullTimer > 0) - { - findHullTimer -= deltaTime; } else { - safeHull = objectiveManager.GetObjective().FindBestHull(HumanAIController.VisibleHulls); - findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f); + character.SelectCharacter(targetCharacter); } } - if (safeHull != null && character.CurrentHull != safeHull) + else { - RemoveSubObjective(ref goToObjective); - TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager), - onCompleted: () => RemoveSubObjective(ref goToObjective), - onAbandon: () => + // Drag the character into safety + if (safeHull == null) + { + if (findHullTimer > 0) { - RemoveSubObjective(ref goToObjective); - safeHull = character.CurrentHull; - }); + findHullTimer -= deltaTime; + } + else + { + safeHull = objectiveManager.GetObjective().FindBestHull(HumanAIController.VisibleHulls); + findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f); + } + } + if (safeHull != null && character.CurrentHull != safeHull) + { + RemoveSubObjective(ref replaceOxygenObjective); + RemoveSubObjective(ref goToObjective); + TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager), + onCompleted: () => RemoveSubObjective(ref goToObjective), + onAbandon: () => + { + RemoveSubObjective(ref goToObjective); + safeHull = character.CurrentHull; + }); + } } } } @@ -137,6 +205,7 @@ namespace Barotrauma if (targetCharacter != character && !character.CanInteractWith(targetCharacter)) { + RemoveSubObjective(ref replaceOxygenObjective); RemoveSubObjective(ref goToObjective); // Go to the target and select it TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) @@ -325,7 +394,7 @@ namespace Barotrauma Priority = 0; return Priority; } - if (targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead) + if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead) { Priority = 0; } @@ -346,5 +415,15 @@ namespace Barotrauma } public static IEnumerable GetSortedAfflictions(Character character) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions()); + + public override void Reset() + { + base.Reset(); + goToObjective = null; + getItemObjective = null; + replaceOxygenObjective = null; + safeHull = null; + ignoreOxygen = false; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 5f690bebc..13e1b1c0f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -1,4 +1,5 @@ -using System; +using Barotrauma.Extensions; +using System; using System.Collections.Generic; using System.Linq; @@ -34,6 +35,7 @@ namespace Barotrauma protected override float TargetEvaluation() { + if (Targets.None()) { return 100; } if (objectiveManager.CurrentOrder != this) { if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsMedic && !c.Character.IsUnconscious)) @@ -72,7 +74,7 @@ namespace Barotrauma public static bool IsValidTarget(Character target, Character character) { if (target == null || target.IsDead || target.Removed) { return false; } - if (target.TurnedHostileByEvent) { return false; } + if (target.IsInstigator) { return false; } if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; } if (character.AIController is HumanAIController humanAI) { @@ -83,7 +85,7 @@ namespace Barotrauma { // Don't allow to treat others autonomously return false; - } + } // Ignore unsafe hulls, unless ordered if (humanAI.UnsafeHulls.Contains(target.CurrentHull)) { @@ -100,10 +102,11 @@ namespace Barotrauma if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, includingConnectedSubs: true)) { return false; } if (target != character &&!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI) { - // Ignore all concious targets that are currently fighting, fleeing or treating characters + // Ignore all concious targets that are currently fighting, fleeing, fixing, or treating characters if (targetAI.ObjectiveManager.HasActiveObjective() || targetAI.ObjectiveManager.HasActiveObjective() || - targetAI.ObjectiveManager.HasActiveObjective()) + targetAI.ObjectiveManager.HasActiveObjective() || + targetAI.ObjectiveManager.HasActiveObjective()) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index a0afd8751..eaf61af4c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -36,6 +36,9 @@ namespace Barotrauma Order = orderInfo.Order; OrderOption = orderInfo.OrderOption; } + + public bool MatchesOrder(Order order, string option) => + order.Identifier == Order.Identifier && option == OrderOption && order.TargetEntity == Order.TargetEntity; } class Order @@ -100,8 +103,7 @@ namespace Barotrauma public Character OrderGiver; - private readonly OrderCategory? category; - public OrderCategory? Category => category; + public OrderCategory? Category { get; private set; } //legacy support public readonly string[] AppropriateJobs; @@ -225,7 +227,7 @@ namespace Barotrauma AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]); Options = orderElement.GetAttributeStringArray("options", new string[0]); var category = orderElement.GetAttributeString("category", null); - if (!string.IsNullOrWhiteSpace(category)) { this.category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); } + if (!string.IsNullOrWhiteSpace(category)) { this.Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); } Weight = orderElement.GetAttributeFloat(0.0f, "weight"); MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false); AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null); @@ -299,7 +301,7 @@ namespace Barotrauma Weight = prefab.Weight; MustSetTarget = prefab.MustSetTarget; AppropriateSkill = prefab.AppropriateSkill; - category = prefab.Category; + Category = prefab.Category; MustManuallyAssign = prefab.MustManuallyAssign; OrderGiver = orderGiver; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index bab7ba613..920717e41 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs @@ -160,7 +160,7 @@ namespace Barotrauma private static readonly List sortedNodes = new List(); - public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null) + public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisibility = true) { //sort nodes roughly according to distance sortedNodes.Clear(); @@ -202,12 +202,12 @@ namespace Barotrauma //if searching for a path inside the sub, make sure the waypoint is visible if (InsideSubmarine) { + // Always check the visibility for the start node var body = Submarine.PickBody( start, node.TempPosition, null, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs); if (body != null) { - //if (body.UserData is Submarine) continue; if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; } if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; } } @@ -257,14 +257,13 @@ namespace Barotrauma if (endNodeFilter != null && !endNodeFilter(node)) { continue; } //if searching for a path inside the sub, make sure the waypoint is visible - if (InsideSubmarine) + if (InsideSubmarine && checkVisibility) { + // Only check the visibility for the end node when allowed (fix leaks) var body = Submarine.PickBody(end, node.TempPosition, null, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs ); - if (body != null) { - //if (body.UserData is Submarine) continue; if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; } if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs index 145c9096f..e603b5b7c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs @@ -78,11 +78,10 @@ namespace Barotrauma } } - if (!aiController.Enabled) { return; } if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; } if (Controlled == this) { return; } - - if (!IsRemotelyControlled) + + if (!IsRemotelyControlled && aiController != null && aiController.Enabled) { aiController.Update(deltaTime); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 7c61d6278..980aa32e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -613,8 +613,13 @@ namespace Barotrauma movementAngle -= MathHelper.TwoPi; } + float offset = MathHelper.Pi * CurrentGroundedParams.StepLiftOffset; + if (CurrentGroundedParams.MultiplyByDir) + { + offset *= Dir; + } float stepLift = TargetMovement.X == 0.0f ? 0 : - (float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100); + (float)Math.Sin(WalkPos * Dir * CurrentGroundedParams.StepLiftFrequency + offset) * (CurrentGroundedParams.StepLiftAmount / 100); float limpAmount = character.GetLegPenalty(); if (limpAmount > 0) @@ -631,7 +636,7 @@ namespace Barotrauma { SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, mainLimb, TorsoTorque); } - if (TorsoPosition.HasValue) + if (TorsoPosition.HasValue && TorsoMoveForce > 0.0f) { Vector2 pos = colliderBottom + new Vector2(limpAmount, TorsoPosition.Value + stepLift); @@ -649,11 +654,16 @@ namespace Barotrauma Limb head = GetLimb(LimbType.Head); if (head != null) { + bool headFacingBackwards = false; if (HeadAngle.HasValue) { SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque); + if (Math.Sign(head.SimPosition.X - mainLimb.SimPosition.X) != Math.Sign(Dir)) + { + headFacingBackwards = true; + } } - if (HeadPosition.HasValue) + if (HeadPosition.HasValue && HeadMoveForce > 0.0f && !headFacingBackwards) { Vector2 pos = colliderBottom + new Vector2(limpAmount, HeadPosition.Value + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 5078bdd2a..d58854b36 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -1873,6 +1873,7 @@ namespace Barotrauma forearm = GetLimb(LimbType.RightForearm); RightHandIKPos = pos; } + if (arm == null) { return; } //distance from shoulder to holdpos float c = Vector2.Distance(pos, shoulderPos); @@ -2018,12 +2019,9 @@ namespace Barotrauma { break; } - if (character.SelectedItems[i]?.body != null && !character.SelectedItems[i].Removed) + if (character.SelectedItems[i]?.body != null && !character.SelectedItems[i].Removed && character.SelectedItems[i].GetComponent() != null) { - /*character.SelectedItems[i].body.SetTransform( - character.SelectedItems[i].body.SimPosition, - MathUtils.WrapAngleTwoPi(character.SelectedItems[i].body.Rotation + MathHelper.Pi));*/ - character.SelectedItems[i].GetComponent()?.Flip(); + character.SelectedItems[i].FlipX(relativeToSub: false); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index e81284fad..a4c12d631 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -764,6 +764,13 @@ namespace Barotrauma } } + + if (!string.IsNullOrEmpty(character.BloodDecalName)) + { + character.CurrentHull?.AddDecal(character.BloodDecalName, + (limbJoint.LimbA.WorldPosition + limbJoint.LimbB.WorldPosition) / 2, MathHelper.Clamp(Math.Min(limbJoint.LimbA.Mass, limbJoint.LimbB.Mass), 0.5f, 2.0f), true); + } + SeverLimbJointProjSpecific(limbJoint, playSound: true); if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { @@ -1430,6 +1437,11 @@ namespace Barotrauma } } + public void ForceRefreshFloorY() + { + lastFloorCheckPos = Vector2.Zero; + } + private void RefreshFloorY(Limb refLimb = null, bool ignoreStairs = false) { PhysicsBody refBody = refLimb == null ? Collider : refLimb.body; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 4729ea027..986992272 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -184,7 +184,7 @@ namespace Barotrauma [Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable] public Vector2 TargetForceWorld { get; private set; } - [Serialize(0.0f, true, description: "How likely the attack causes target limbs to be severed when the target is dead."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.0f, true, description: "How likely the attack causes target limbs to be severed."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float SeverLimbsProbability { get; set; } // TODO: disabled because not synced @@ -235,7 +235,7 @@ namespace Barotrauma List multipliedAfflictions = new List(); foreach (Affliction affliction in Afflictions.Keys) { - multipliedAfflictions.Add(affliction.Prefab.Instantiate(affliction.Strength * multiplier, affliction.Source)); + multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier)); } return multipliedAfflictions; } @@ -495,14 +495,14 @@ namespace Barotrauma if (SecondaryCoolDownTimer < 0) { SecondaryCoolDownTimer = 0; } } - public void UpdateAttackTimer(float deltaTime) + public void UpdateAttackTimer(float deltaTime, Character character) { IsRunning = true; AttackTimer += deltaTime; if (AttackTimer >= Duration) { ResetAttackTimer(); - SetCoolDown(); + SetCoolDown(applyRandom: !character.IsPlayer); } } @@ -512,13 +512,22 @@ namespace Barotrauma IsRunning = false; } - public void SetCoolDown() + public void SetCoolDown(bool applyRandom) { - float randomFraction = CoolDown * CoolDownRandomFactor; - CurrentRandomCoolDown = MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value()); - CoolDownTimer = CoolDown + CurrentRandomCoolDown; - randomFraction = SecondaryCoolDown * CoolDownRandomFactor; - SecondaryCoolDownTimer = SecondaryCoolDown + MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value()); + if (applyRandom) + { + float randomFraction = CoolDown * CoolDownRandomFactor; + CurrentRandomCoolDown = MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value()); + CoolDownTimer = CoolDown + CurrentRandomCoolDown; + randomFraction = SecondaryCoolDown * CoolDownRandomFactor; + SecondaryCoolDownTimer = SecondaryCoolDown + MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value()); + } + else + { + CoolDownTimer = CoolDown; + SecondaryCoolDownTimer = SecondaryCoolDown; + CurrentRandomCoolDown = 0; + } } public void ResetCoolDown() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 5463d7543..f03c6ab7a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -121,7 +121,8 @@ namespace Barotrauma } } - public bool TurnedHostileByEvent; + public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator; + public CombatAction CombatAction; public AnimController AnimController; @@ -143,6 +144,8 @@ namespace Barotrauma public bool IsHumanoid => Params.Humanoid; public bool IsHusk => Params.Husk; + public string BloodDecalName => Params.BloodDecal; + public bool CanSpeak { get => Params.CanSpeak; @@ -637,6 +640,8 @@ namespace Barotrauma } } + public bool GodMode = false; + public CampaignMode.InteractionType CampaignInteractionType; private bool accessRemovedCharacterErrorShown; @@ -1668,14 +1673,14 @@ namespace Barotrauma return false; } - public bool HasEquippedItem(string itemIdentifier, bool allowBroken = true) + public bool HasEquippedItem(string tagOrIdentifier, bool allowBroken = true) { if (Inventory == null) { return false; } for (int i = 0; i < Inventory.Capacity; i++) { if (Inventory.SlotTypes[i] == InvSlotType.Any || Inventory.Items[i] == null) { continue; } if (!allowBroken && Inventory.Items[i].Condition <= 0.0f) { continue; } - if (Inventory.Items[i].Prefab.Identifier == itemIdentifier || Inventory.Items[i].HasTag(itemIdentifier)) { return true; } + if (Inventory.Items[i].Prefab.Identifier == tagOrIdentifier || Inventory.Items[i].HasTag(tagOrIdentifier)) { return true; } } return false; @@ -1738,7 +1743,7 @@ namespace Barotrauma if (inventory.Owner is Item) { var owner = (Item)inventory.Owner; - if (!CanInteractWith(owner)) { return false; } + if (!CanInteractWith(owner) && !owner.linkedTo.Any(lt => lt is Item item && item.DisplaySideBySideWhenLinked && CanInteractWith(item))) { return false; } ItemContainer container = owner.GetComponents().FirstOrDefault(ic => ic.Inventory == inventory); if (container != null && !container.HasRequiredItems(this, addMessage: false)) { return false; } } @@ -1833,7 +1838,7 @@ namespace Barotrauma #if CLIENT if (Screen.Selected == GameMain.SubEditorScreen) { hidden = false; } #endif - if (!CanInteract || hidden || item.NonInteractable) return false; + if (!CanInteract || hidden || item.NonInteractable) { return false; } if (item.ParentInventory != null) { @@ -1845,6 +1850,7 @@ namespace Barotrauma { //locked wires are never interactable if (wire.Locked) { return false; } + if (wire.HiddenInGame && Screen.Selected == GameMain.GameScreen) { return false; } //wires are interactable if the character has selected an item the wire is connected to, //and it's disconnected from the other end @@ -2314,7 +2320,7 @@ namespace Barotrauma if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { Implode(); - return; + if (IsDead) { return; } } } } @@ -2329,7 +2335,7 @@ namespace Barotrauma if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f) { Implode(); - return; + if (IsDead) { return; } } } @@ -2509,6 +2515,12 @@ namespace Barotrauma if (subCorpseCount < GameMain.Config.CorpsesPerSubDespawnThreshold) { return; } } + if (SelectedBy != null) + { + despawnTimer = 0.0f; + return; + } + float distToClosestPlayer = GetDistanceToClosestPlayer(); if (distToClosestPlayer > NetConfig.DisableCharacterDist) { @@ -2866,20 +2878,18 @@ namespace Barotrauma wasSevered = severed; } if (severed) - { + { Limb otherLimb = joint.LimbA == targetLimb ? joint.LimbB : joint.LimbA; - otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass); + otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass); + ApplyStatusEffects(ActionType.OnSevered, 1.0f); + targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f); + otherLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f); } } - if (wasSevered) + if (wasSevered && targetLimb.character.AIController is EnemyAIController enemyAI) { - if (targetLimb.character.AIController is EnemyAIController enemyAI) - { - enemyAI.ReevaluateAttacks(); - } - ApplyStatusEffects(ActionType.OnSevered, 1.0f); - targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f); - } + enemyAI.ReevaluateAttacks(); + } } public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, float attackImpulse = 0.0f, Character attacker = null) @@ -3075,7 +3085,7 @@ namespace Barotrauma private void Implode(bool isNetworkMessage = false) { - if (CharacterHealth.Unkillable || IsDead) { return; } + if (CharacterHealth.Unkillable || GodMode || IsDead) { return; } if (!isNetworkMessage) { @@ -3127,7 +3137,7 @@ namespace Barotrauma public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false, bool log = true) { - if (IsDead || CharacterHealth.Unkillable) { return; } + if (IsDead || CharacterHealth.Unkillable || GodMode) { return; } HealthUpdateInterval = 0.0f; @@ -3447,7 +3457,7 @@ namespace Barotrauma public bool IsMechanic => HasJob("mechanic"); public bool IsMedic => HasJob("medicaldoctor"); public bool IsSecurity => HasJob("securityofficer"); - public bool IsAsssitant => HasJob("assistant"); + public bool IsAssistant => HasJob("assistant"); public bool IsWatchman => HasJob("watchman"); public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 082ef4533..e4a39925f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -100,7 +100,7 @@ namespace Barotrauma head = value; if (head.race == Race.None) { - head.race = GetRandomRace(); + head.race = GetRandomRace(Rand.RandSync.Unsynced); } CalculateHeadSpriteRange(); Head.HeadSpriteId = value.HeadSpriteId; @@ -296,7 +296,7 @@ namespace Barotrauma public Character.TeamType TeamID; - private NPCPersonalityTrait personalityTrait; + private readonly NPCPersonalityTrait personalityTrait; public Order CurrentOrder { get; set; } public string CurrentOrderOption { get; set; } @@ -400,7 +400,7 @@ namespace Barotrauma public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1; // Used for creating the data - public CharacterInfo(string speciesName, string name = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0) + public CharacterInfo(string speciesName, string name = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced) { if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) { @@ -417,12 +417,12 @@ namespace Barotrauma HasGenders = CharacterConfigElement.GetAttributeBool("genders", false); if (HasGenders) { - Head.gender = GetRandomGender(); + Head.gender = GetRandomGender(randSync); } - Head.race = GetRandomRace(); + Head.race = GetRandomRace(randSync); CalculateHeadSpriteRange(); - Head.HeadSpriteId = GetRandomHeadID(); - Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Server) : new Job(jobPrefab, variant); + Head.HeadSpriteId = GetRandomHeadID(randSync); + Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, variant); if (!string.IsNullOrEmpty(name)) { @@ -485,7 +485,7 @@ namespace Barotrauma HasGenders = CharacterConfigElement.GetAttributeBool("genders", false); if (HasGenders && gender == Gender.None) { - gender = GetRandomGender(); + gender = GetRandomGender(Rand.RandSync.Unsynced); } else if (!HasGenders) { @@ -539,11 +539,9 @@ namespace Barotrauma LoadHeadAttachments(); } - public int SetRandomHead() => HeadSpriteId = GetRandomHeadID(); - - public Gender GetRandomGender() => (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < CharacterConfigElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male; - public Race GetRandomRace() => new Race[] { Race.White, Race.Black, Race.Asian }.GetRandom(Rand.RandSync.Server); - public int GetRandomHeadID() => Head.headSpriteRange != Vector2.Zero ? Rand.Range((int)Head.headSpriteRange.X, (int)Head.headSpriteRange.Y + 1, Rand.RandSync.Server) : 0; + public Gender GetRandomGender(Rand.RandSync randSync) => (Rand.Range(0.0f, 1.0f, randSync) < CharacterConfigElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male; + public Race GetRandomRace(Rand.RandSync randSync) => new Race[] { Race.White, Race.Black, Race.Asian }.GetRandom(randSync); + public int GetRandomHeadID(Rand.RandSync randSync) => Head.headSpriteRange != Vector2.Zero ? Rand.Range((int)Head.headSpriteRange.X, (int)Head.headSpriteRange.Y + 1, randSync) : 0; private List hairs; private List beards; @@ -670,12 +668,16 @@ namespace Barotrauma { if (HasGenders && gender == Gender.None) { - gender = GetRandomGender(); + gender = GetRandomGender(Rand.RandSync.Unsynced); } else if (!HasGenders) { gender = Gender.None; } + if (heads == null) + { + LoadHeadPresets(); + } head = new HeadInfo(headID, gender, race, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); CalculateHeadSpriteRange(); ReloadHeadAttachments(); @@ -788,7 +790,7 @@ namespace Barotrauma Head.FaceAttachmentIndex = faceAttachments.IndexOf(Head.FaceAttachment); } - List AddEmpty(IEnumerable elements, WearableType type, float commonness = 1) + static List AddEmpty(IEnumerable elements, WearableType type, float commonness = 1) { // Let's add an empty element so that there's a chance that we don't get any actual element -> allows bald and beardless guys, for example. var emptyElement = new XElement("EmptyWearable", type.ToString(), new XAttribute("commonness", commonness)); @@ -799,10 +801,9 @@ namespace Barotrauma XElement GetRandomElement(IEnumerable elements) { - var filtered = elements.Where(e => IsWearableAllowed(e)).ToList(); - if (filtered.Count == 0) { return null; } - var weights = GetWeights(filtered).ToList(); - var element = ToolBox.SelectWeightedRandom(filtered, weights, Rand.RandSync.Server); + var filtered = elements.Where(e => IsWearableAllowed(e)); + if (filtered.Count() == 0) { return null; } + var element = ToolBox.SelectWeightedRandom(filtered.ToList(), GetWeights(filtered).ToList(), Rand.RandSync.Unsynced); return element == null || element.Name == "Empty" ? null : element; } @@ -825,8 +826,9 @@ namespace Barotrauma return true; } - bool IsValidIndex(int index, List list) => index >= 0 && index < list.Count; - IEnumerable GetWeights(IEnumerable elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f)); + static bool IsValidIndex(int index, List list) => index >= 0 && index < list.Count; + + static IEnumerable GetWeights(IEnumerable elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f)); } } @@ -861,7 +863,7 @@ namespace Barotrauma OnSkillChanged(skillIdentifier, prevLevel, newLevel, worldPos); - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && (int)newLevel != (int)prevLevel) + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && !MathUtils.NearlyEqual(newLevel, prevLevel)) { GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdateSkills }); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index afbee5aaa..36c9befdf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -111,7 +111,7 @@ namespace Barotrauma public static void LoadAll() { - foreach (ContentFile file in ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Character)) + foreach (ContentFile file in ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Character)) { LoadFromFile(file); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 9652a54ad..b1d2ddc12 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -15,13 +15,24 @@ namespace Barotrauma public Dictionary SerializableProperties { get; set; } protected float _strength; + [Serialize(0f, true), Editable] public virtual float Strength { get { return _strength; } - set { _strength = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength); } + set + { + if (_nonClampedStrength < 0 && value > 0) + { + _nonClampedStrength = value; + } + _strength = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength); + } } + private float _nonClampedStrength = -1; + public float NonClampedStrength => _nonClampedStrength > 0 ? _nonClampedStrength : _strength; + [Serialize("", true), Editable] public string Identifier { get; private set; } @@ -35,6 +46,8 @@ namespace Barotrauma public float StrengthDiminishMultiplier = 1.0f; public Affliction MultiplierSource; + public readonly Dictionary PeriodicEffectTimers = new Dictionary(); + /// /// Which character gave this affliction /// @@ -45,6 +58,11 @@ namespace Barotrauma Prefab = prefab; _strength = strength; Identifier = prefab?.Identifier; + + foreach (var periodicEffect in prefab.PeriodicEffects) + { + PeriodicEffectTimers[periodicEffect] = Rand.Range(periodicEffect.MinInterval, periodicEffect.MaxInterval); + } } public void Serialize(XElement element) @@ -59,24 +77,27 @@ namespace Barotrauma public Affliction CreateMultiplied(float multiplier) { - return Prefab.Instantiate(Strength * multiplier, Source); + return Prefab.Instantiate(NonClampedStrength * multiplier, Source); } public override string ToString() => Prefab == null ? "Affliction (Invalid)" : $"Affliction ({Prefab.Name})"; public float GetVitalityDecrease(CharacterHealth characterHealth) { - if (Strength < Prefab.ActivationThreshold) return 0.0f; + if (Strength < Prefab.ActivationThreshold) { return 0.0f; } AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); - if (currentEffect == null) return 0.0f; - if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) return 0.0f; + if (currentEffect == null) { return 0.0f; } + if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) { return 0.0f; } float currVitalityDecrease = MathHelper.Lerp( currentEffect.MinVitalityDecrease, currentEffect.MaxVitalityDecrease, (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); - if (currentEffect.MultiplyByMaxVitality) currVitalityDecrease *= characterHealth == null ? 100.0f : characterHealth.MaxVitality; + if (currentEffect.MultiplyByMaxVitality) + { + currVitalityDecrease *= characterHealth == null ? 100.0f : characterHealth.MaxVitality; + } return currVitalityDecrease; } @@ -173,8 +194,28 @@ namespace Barotrauma public virtual void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime) { + foreach (AfflictionPrefab.PeriodicEffect periodicEffect in Prefab.PeriodicEffects) + { + PeriodicEffectTimers[periodicEffect] -= deltaTime; + if (PeriodicEffectTimers[periodicEffect] <= 0.0f) + { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) + { + PeriodicEffectTimers[periodicEffect] = 0.0f; + } + else + { + foreach (StatusEffect statusEffect in periodicEffect.StatusEffects) + { + ApplyStatusEffect(statusEffect, 1.0f, characterHealth, targetLimb); + PeriodicEffectTimers[periodicEffect] = Rand.Range(periodicEffect.MinInterval, periodicEffect.MaxInterval); + } + } + } + } + AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); - if (currentEffect == null) return; + if (currentEffect == null) { return; } if (currentEffect.StrengthChange < 0) // Reduce diminishing of buffs if boosted { @@ -184,32 +225,44 @@ namespace Barotrauma { _strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab.Identifier)); } - // Don't use the property, because its virtual and some afflictions like husk overload it for external use. + // Don't use the property, because it's virtual and some afflictions like husk overload it for external use. _strength = MathHelper.Clamp(_strength, 0.0f, Prefab.MaxStrength); foreach (StatusEffect statusEffect in currentEffect.StatusEffects) { - statusEffect.SetUser(Source); - if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) - { - statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, characterHealth.Character); - } - if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) - { - statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, targetLimb); - } - if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) - { - statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast().ToList()); - } - if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) || - statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) - { - var targets = new List(); - statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets); - statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targets); - } + ApplyStatusEffect(statusEffect, deltaTime, characterHealth, targetLimb); } } + + public void ApplyStatusEffect(StatusEffect statusEffect, float deltaTime, CharacterHealth characterHealth, Limb targetLimb) + { + statusEffect.SetUser(Source); + if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) + { + statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, characterHealth.Character); + } + if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) + { + statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, targetLimb); + } + if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) + { + statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast().ToList()); + } + if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) || + statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) + { + var targets = new List(); + statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets); + statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targets); + } + } + + /// + /// Use this method to skip clamping and additional logic of the setters. + /// Intended only to be used when the value is already clamped! (networking code) + /// Ideally we would keep this private, but doing so would require too much refactoring. + /// + public void SetStrength(float strength) => _strength = strength; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 80cdaa08d..90bdbf3ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -29,8 +29,9 @@ namespace Barotrauma set { // Don't allow to set the strength too high (from outside) to avoid rapid transformation into husk when taking lots of damage from husks. - // If the strength is more than the value, this will effectively reset the current strength to the max. That's why we use two steps. - float max = _strength > ActiveThreshold ? ActiveThreshold + 1 : DormantThreshold - 1; + float previousValue = _strength; + float threshold = _strength > ActiveThreshold ? ActiveThreshold + 1 : DormantThreshold - 1; + float max = Math.Max(threshold, previousValue); _strength = Math.Clamp(value, 0, max); } } @@ -79,7 +80,7 @@ namespace Barotrauma { if (State != InfectionState.Active) { - character.SetStun(Rand.Range(2, 4, Rand.RandSync.Server)); + character.SetStun(Rand.Range(2, 4)); } State = InfectionState.Active; ActivateHusk(); @@ -101,7 +102,7 @@ namespace Barotrauma foreach (Limb limb in character.AnimController.Limbs) { if (limb.IsSevered) { continue; } - float random = Rand.Value(Rand.RandSync.Server); + float random = Rand.Value(); huskInfection.Clear(); huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / limbCount)); character.LastDamageSource = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 39cae1adb..cb83175cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -68,13 +68,13 @@ namespace Barotrauma HuskedSpeciesName = element.GetAttributeString("huskedspeciesname", null).ToLowerInvariant(); if (HuskedSpeciesName == null) { - DebugConsole.NewMessage($"No 'huskedspeciesname' defined for the husk affliction ({Identifier}) in {element.ToString()}", Color.Orange); + DebugConsole.NewMessage($"No 'huskedspeciesname' defined for the husk affliction ({Identifier}) in {element}", Color.Orange); HuskedSpeciesName = "[speciesname]husk"; } TargetSpecies = element.GetAttributeStringArray("targets", new string[0] { }, trim: true, convertToLowerInvariant: true); if (TargetSpecies.Length == 0) { - DebugConsole.NewMessage($"No 'targets' defined for the husk affliction ({Identifier}) in {element.ToString()}", Color.Orange); + DebugConsole.NewMessage($"No 'targets' defined for the husk affliction ({Identifier}) in {element}", Color.Orange); TargetSpecies = new string[] { "human" }; } var attachElement = element.GetChildElement("attachlimb"); @@ -188,6 +188,30 @@ namespace Barotrauma } } + public class PeriodicEffect + { + public readonly List StatusEffects = new List(); + public readonly float MinInterval, MaxInterval; + + public PeriodicEffect(XElement element, string parentDebugName) + { + foreach (XElement subElement in element.Elements()) + { + StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); + } + + if (element.Attribute("interval") != null) + { + MinInterval = MaxInterval = Math.Max(element.GetAttributeFloat("interval", 1.0f), 1.0f); + } + else + { + MinInterval = Math.Max(element.GetAttributeFloat("mininterval", 1.0f), 1.0f); + MaxInterval = Math.Max(element.GetAttributeFloat("maxinterval", 1.0f), MinInterval); + } + } + } + public static AfflictionPrefab InternalDamage; public static AfflictionPrefab ImpactDamage; public static AfflictionPrefab Bleeding; @@ -267,8 +291,12 @@ namespace Barotrauma public readonly Color[] IconColors; private readonly List effects = new List(); + private readonly List periodicEffects = new List(); + public IEnumerable Effects => effects; + public IList PeriodicEffects => periodicEffects; + private readonly string typeName; private readonly ConstructorInfo constructor; @@ -304,10 +332,10 @@ namespace Barotrauma CharacterHealth.DamageOverlay = null; CharacterHealth.DamageOverlayFile = string.Empty; #endif - var prevPrefabs = Prefabs.ToList(); + var prevPrefabs = Prefabs.AllPrefabs.SelectMany(kvp => kvp.Value).ToList(); foreach (var prefab in prevPrefabs) { - prefab.Dispose(); + prefab?.Dispose(); } System.Diagnostics.Debug.Assert(Prefabs.Count() == 0, "All previous AfflictionPrefabs were not removed in AfflictionPrefab.LoadAll"); @@ -552,6 +580,9 @@ namespace Barotrauma case "effect": effects.Add(new Effect(subElement, Name)); break; + case "periodiceffect": + periodicEffects.Add(new PeriodicEffect(subElement, Name)); + break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 8ef1c7314..68bd64e00 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -170,12 +170,12 @@ namespace Barotrauma { get { - if (!Character.NeedsOxygen || Unkillable) { return 100.0f; } + if (!Character.NeedsOxygen || Unkillable || Character.GodMode) { return 100.0f; } return -oxygenLowAffliction.Strength + 100; } set { - if (!Character.NeedsOxygen || Unkillable) { return; } + if (!Character.NeedsOxygen || Unkillable || Character.GodMode) { return; } oxygenLowAffliction.Strength = MathHelper.Clamp(-value + 100, 0.0f, 200.0f); } } @@ -399,7 +399,7 @@ namespace Barotrauma public void ApplyAffliction(Limb targetLimb, Affliction affliction) { - if (Unkillable) { return; } + if (Unkillable || Character.GodMode) { return; } if (affliction.Prefab.LimbSpecific) { if (targetLimb == null) @@ -481,7 +481,7 @@ namespace Barotrauma public void ApplyDamage(Limb hitLimb, AttackResult attackResult) { - if (Unkillable) { return; } + if (Unkillable || Character.GodMode) { return; } if (hitLimb.HealthIndex < 0 || hitLimb.HealthIndex >= limbHealths.Count) { DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name + @@ -504,7 +504,7 @@ namespace Barotrauma public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount) { - if (Unkillable) { return; } + if (Unkillable || Character.GodMode) { return; } foreach (LimbHealth limbHealth in limbHealths) { limbHealth.Afflictions.RemoveAll(a => @@ -741,7 +741,7 @@ namespace Barotrauma public void CalculateVitality() { Vitality = MaxVitality; - if (Unkillable) { return; } + if (Unkillable || Character.GodMode) { return; } float damageResistanceMultiplier = 1f - GetResistance("damage"); @@ -777,7 +777,7 @@ namespace Barotrauma private void Kill() { - if (Unkillable) { return; } + if (Unkillable || Character.GodMode) { return; } var causeOfDeath = GetCauseOfDeath(); Character.Kill(causeOfDeath.First, causeOfDeath.Second); @@ -913,6 +913,11 @@ namespace Barotrauma msg.WriteRangedSingle( MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength), 0.0f, affliction.Prefab.MaxStrength, 8); + msg.Write((byte)affliction.Prefab.PeriodicEffects.Count()); + foreach (AfflictionPrefab.PeriodicEffect periodicEffect in affliction.Prefab.PeriodicEffects) + { + msg.WriteRangedSingle(affliction.PeriodicEffectTimers[periodicEffect], periodicEffect.MinInterval, periodicEffect.MaxInterval, 8); + } } limbAfflictions.Clear(); @@ -933,6 +938,11 @@ namespace Barotrauma msg.WriteRangedSingle( MathHelper.Clamp(limbAffliction.Second.Strength, 0.0f, limbAffliction.Second.Prefab.MaxStrength), 0.0f, limbAffliction.Second.Prefab.MaxStrength, 8); + msg.Write((byte)limbAffliction.Second.Prefab.PeriodicEffects.Count()); + foreach (AfflictionPrefab.PeriodicEffect periodicEffect in limbAffliction.Second.Prefab.PeriodicEffects) + { + msg.WriteRangedSingle(limbAffliction.Second.PeriodicEffectTimers[periodicEffect], periodicEffect.MinInterval, periodicEffect.MaxInterval, 8); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index 332fce7a7..5d23aa8ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -154,7 +154,10 @@ namespace Barotrauma if (item.Prefab.Identifier == "idcard" || item.Prefab.Identifier == "idcardwreck") { item.AddTag("name:" + character.Name); - item.ReplaceTag("wreck_id", Level.Loaded.GetWreckIDTag("wreck_id", submarine)); + if (Level.Loaded != null) + { + item.ReplaceTag("wreck_id", Level.Loaded.GetWreckIDTag("wreck_id", submarine)); + } var job = character.Info?.Job; if (job != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index 5a0418fd4..8818422e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -11,7 +11,8 @@ namespace Barotrauma { public string identifier; public string option; - public float priorityModifier; + public readonly float priorityModifier; + public readonly bool ignoreAtOutpost; public AutonomousObjective(XElement element) { @@ -26,6 +27,7 @@ namespace Barotrauma option = element.GetAttributeString("option", null); priorityModifier = element.GetAttributeFloat("prioritymodifier", 1); priorityModifier = MathHelper.Max(priorityModifier, 0); + ignoreAtOutpost = element.GetAttributeBool("ignoreatoutpost", false); } } @@ -64,7 +66,7 @@ namespace Barotrauma public readonly Dictionary> ItemIdentifiers = new Dictionary>(); public readonly Dictionary> ShowItemPreview = new Dictionary>(); public readonly List Skills = new List(); - public readonly List AutonomousObjective = new List(); + public readonly List AutonomousObjectives = new List(); public readonly List AppropriateOrders = new List(); [Serialize("1,1,1,1", false)] @@ -209,7 +211,7 @@ namespace Barotrauma } break; case "autonomousobjectives": - subElement.Elements().ForEach(order => AutonomousObjective.Add(new AutonomousObjective(order))); + subElement.Elements().ForEach(order => AutonomousObjectives.Add(new AutonomousObjective(order))); break; case "appropriateobjectives": case "appropriateorders": diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index e3ebd0ce4..2b4ecad93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -639,6 +639,7 @@ namespace Barotrauma } private readonly List appliedDamageModifiers = new List(); + private readonly List tempModifiers = new List(); private readonly List afflictionsCopy = new List(); public AttackResult AddDamage(Vector2 simPosition, IEnumerable afflictions, bool playSound) { @@ -646,6 +647,7 @@ namespace Barotrauma afflictionsCopy.Clear(); foreach (var affliction in afflictions) { + tempModifiers.Clear(); var newAffliction = affliction; float random = Rand.Value(Rand.RandSync.Unsynced); if (random > affliction.Probability) { continue; } @@ -660,8 +662,7 @@ namespace Barotrauma } if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition)) { - newAffliction = affliction.CreateMultiplied(damageModifier.DamageMultiplier); - appliedDamageModifiers.Add(damageModifier); + tempModifiers.Add(damageModifier); } } foreach (WearableSprite wearable in wearingItems) @@ -676,18 +677,49 @@ namespace Barotrauma } if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition)) { - newAffliction = affliction.CreateMultiplied(damageModifier.DamageMultiplier); - appliedDamageModifiers.Add(damageModifier); + tempModifiers.Add(damageModifier); } } } + float finalDamageModifier = 1.0f; + foreach (DamageModifier damageModifier in tempModifiers) + { + finalDamageModifier *= damageModifier.DamageMultiplier; + } + if (!MathUtils.NearlyEqual(finalDamageModifier, 1.0f)) + { + newAffliction = affliction.CreateMultiplied(finalDamageModifier); + } + if (applyAffliction) { afflictionsCopy.Add(newAffliction); } + appliedDamageModifiers.AddRange(tempModifiers); } var result = new AttackResult(afflictionsCopy, this, appliedDamageModifiers); AddDamageProjSpecific(playSound, result); + + float bleedingDamage = 0; + if (character.CharacterHealth.DoesBleed) + { + foreach (var affliction in result.Afflictions) + { + if (affliction is AfflictionBleeding) + { + bleedingDamage += affliction.GetVitalityDecrease(character.CharacterHealth); + } + } + if (bleedingDamage > 0) + { + float bloodDecalSize = MathHelper.Clamp(bleedingDamage / 5, 0.1f, 1.0f); + if (character.CurrentHull != null && !string.IsNullOrEmpty(character.BloodDecalName)) + { + character.CurrentHull.AddDecal(character.BloodDecalName, WorldPosition, MathHelper.Clamp(bloodDecalSize, 0.5f, 1.0f), true); + } + } + } + return result; } @@ -751,7 +783,7 @@ namespace Barotrauma Vector2 simPos = ragdoll.SimplePhysicsEnabled ? character.SimPosition : SimPosition; float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos)); bool wasRunning = attack.IsRunning; - attack.UpdateAttackTimer(deltaTime); + attack.UpdateAttackTimer(deltaTime, character); bool wasHit = false; Body structureBody = null; @@ -762,7 +794,11 @@ namespace Barotrauma case HitDetection.Distance: if (dist < attack.DamageRange) { - structureBody = Submarine.PickBody(simPos, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true); + structureBody = Submarine.PickBody(simPos, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true); + if (structureBody?.UserData as string == "ruinroom") + { + structureBody = null; + } if (damageTarget is Item i && i.GetComponent() != null) { // If the attack is aimed to an item and hits an item, it's successful. @@ -917,7 +953,7 @@ namespace Barotrauma StickTo(structureBody, from, to); }*/ attack.ResetAttackTimer(); - attack.SetCoolDown(); + attack.SetCoolDown(applyRandom: !character.IsPlayer); } private WeldJoint attachJoint; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index 52d3ba651..cab24ff0a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -20,17 +20,17 @@ namespace Barotrauma abstract class GroundedMovementParams : AnimationParams { - [Serialize("1.0, 1.0", true, description: "How big steps the character takes."), Editable(DecimalCount = 2)] + [Serialize("1.0, 1.0", true, description: "How big steps the character takes."), Editable(DecimalCount = 2, ValueStep = 0.01f)] public Vector2 StepSize { get; set; } - [Serialize(0f, true, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2)] + [Serialize(0f, true, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)] public float HeadPosition { get; set; } - [Serialize(0f, true, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2)] + [Serialize(0f, true, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)] public float TorsoPosition { get; set; } [Serialize(1f, true, description: "Separate multiplier for the head lift"), Editable(MinValueFloat = 0, MaxValueFloat = 2, ValueStep = 0.1f)] @@ -39,7 +39,10 @@ namespace Barotrauma [Serialize(0f, true, description: "How much the body raises when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 0.1f)] public float StepLiftAmount { get; set; } - [Serialize(-0.5f, true, description: "When does the body raise when taking a step. The default (0.5) is in the middle of the step."), Editable(MinValueFloat = -1, MaxValueFloat = 1, DecimalCount = 2, ValueStep = 0.1f)] + [Serialize(true, true), Editable] + public bool MultiplyByDir { get; set; } + + [Serialize(0.5f, true, description: "When does the body raise when taking a step. The default (0.5) is in the middle of the step."), Editable(MinValueFloat = -1, MaxValueFloat = 1, DecimalCount = 2, ValueStep = 0.1f)] public float StepLiftOffset { get; set; } [Serialize(2f, true, description: "How frequently the body raises when taking a step. The default is 2 (after every step)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f)] @@ -51,7 +54,7 @@ namespace Barotrauma abstract class SwimParams : AnimationParams { - [Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] + [Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 500, ValueStep = 1)] public float SteerTorque { get; set; } } @@ -63,11 +66,11 @@ namespace Barotrauma protected static Dictionary> allAnimations = new Dictionary>(); - [Serialize(1.0f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED)] + [Serialize(1.0f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)] public float MovementSpeed { get; set; } [Serialize(1.0f, true, description: "The speed of the \"animation cycle\", i.e. how fast the character takes steps or moves the tail/legs/arms (the outcome depends what the clip is about)"), - Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)] public float CycleSpeed { get; set; } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs index 275336876..981e704fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs @@ -87,19 +87,19 @@ namespace Barotrauma [Serialize(8.0f, true, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float FootMoveForce { get; set; } - [Serialize(50.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] + [Serialize(50.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float HeadTorque { get; set; } - [Serialize(50.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] + [Serialize(50.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float TorsoTorque { get; set; } - [Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] + [Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float TailTorque { get; set; } - [Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] + [Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float FootTorque { get; set; } - [Serialize(0.0f, true, description: "Optional torque that's constantly applied to legs."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] + [Serialize(0.0f, true, description: "Optional torque that's constantly applied to legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float LegTorque { get; set; } /// @@ -173,19 +173,19 @@ namespace Barotrauma [Editable, Serialize(true, true, description: "Should the character face towards the direction it's heading.")] public bool RotateTowardsMovement { get; set; } - [Serialize(25.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] + [Serialize(25.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float TorsoTorque { get; set; } - [Serialize(25.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] + [Serialize(25.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float HeadTorque { get; set; } - [Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] + [Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float TailTorque { get; set; } [Serialize(1f, true, description: "Multiplier applied based on the angle difference between the tail and the main limb. Increasing the value prevents snake-like characters from getting tangled on themselves. Default = 1 (no boost)"), Editable(MinValueFloat = 1, MaxValueFloat = 100)] public float TailTorqueMultiplier { get; set; } - [Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] + [Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float FootTorque { get; set; } [Serialize(null, true), Editable] diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs index ee705cc44..503de101f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Security.Cryptography; using System.Xml.Linq; using Barotrauma.Extensions; +using Barotrauma.Steam; namespace Barotrauma { @@ -23,7 +24,6 @@ namespace Barotrauma NPCSets, Factions, Text, - Executable, ServerExecutable, LocationTypes, MapGenerationParameters, @@ -54,7 +54,22 @@ namespace Barotrauma { public static string Folder = "Data/ContentPackages/"; - public static List List = new List(); + private static List regularPackages = new List(); + public static IReadOnlyList RegularPackages + { + get { return regularPackages; } + } + + private static List corePackages = new List(); + public static IReadOnlyList CorePackages + { + get { return corePackages; } + } + + public static IEnumerable AllPackages + { + get { return corePackages.Concat(regularPackages); } + } //these types of files are included in the MD5 hash calculation, //meaning that the players must have the exact same files to play together @@ -97,7 +112,6 @@ namespace Barotrauma ContentType.Wreck, ContentType.WreckAIConfig, ContentType.Text, - ContentType.Executable, ContentType.ServerExecutable, ContentType.LocationTypes, ContentType.MapGenerationParameters, @@ -128,7 +142,7 @@ namespace Barotrauma set; } - public string SteamWorkshopUrl; + public ulong SteamWorkshopId; public DateTime? InstallTime; public bool HideInWorkshopMenu @@ -160,10 +174,24 @@ namespace Barotrauma //core packages are content packages that are required for the game to work //e.g. they include the executable, some location types, level generation params and other files the game won't work without //one (and only one) core package must always be selected - public bool CorePackage + private bool isCorePackage; + public bool IsCorePackage { - get; - set; + get { return isCorePackage; } + set + { + isCorePackage = value; + if (isCorePackage && regularPackages.Contains(this)) + { + corePackages.Add(this); + regularPackages.Remove(this); + } + else if (!isCorePackage && corePackages.Contains(this)) + { + regularPackages.Add(this); + corePackages.Remove(this); + } + } } public Version GameVersion @@ -171,7 +199,31 @@ namespace Barotrauma get; set; } - public List Files; + + private List files; + private List filesToAdd; + private List filesToRemove; + + + public IReadOnlyList Files + { + get { return files; } + } + + public IEnumerable FilesUnsaved + { + get { return files.Where(f => !filesToRemove.Contains(f)).Concat(filesToAdd); } + } + + public IReadOnlyList FilesToAdd + { + get { return filesToAdd; } + } + + public IReadOnlyList FilesToRemove + { + get { return filesToRemove; } + } public bool HasMultiplayerIncompatibleContent { @@ -180,7 +232,9 @@ namespace Barotrauma private ContentPackage() { - Files = new List(); + files = new List(); + filesToAdd = new List(); + filesToRemove = new List(); } public ContentPackage(string filePath, string setPath = "") @@ -200,8 +254,13 @@ namespace Barotrauma Name = doc.Root.GetAttributeString("name", ""); HideInWorkshopMenu = doc.Root.GetAttributeBool("hideinworkshopmenu", false); - CorePackage = doc.Root.GetAttributeBool("corepackage", false); - SteamWorkshopUrl = doc.Root.GetAttributeString("steamworkshopurl", ""); + isCorePackage = doc.Root.GetAttributeBool("corepackage", false); + SteamWorkshopId = doc.Root.GetAttributeUInt64("steamworkshopid", 0); + string workshopUrl = doc.Root.GetAttributeString("steamworkshopurl", ""); + if (!string.IsNullOrEmpty(workshopUrl)) + { + SteamWorkshopId = SteamManager.GetWorkshopItemIDFromUrl(workshopUrl); + } GameVersion = new Version(doc.Root.GetAttributeString("gameversion", "0.0.0.0")); if (doc.Root.Attribute("installtime") != null) { @@ -211,12 +270,13 @@ namespace Barotrauma List errorMsgs = new List(); foreach (XElement subElement in doc.Root.Elements()) { + if (subElement.Name.ToString().Equals("executable", StringComparison.OrdinalIgnoreCase)) { continue; } if (!Enum.TryParse(subElement.Name.ToString(), true, out ContentType type)) { errorMsgs.Add("Error in content package \"" + Name + "\" - \"" + subElement.Name.ToString() + "\" is not a valid content type."); type = ContentType.None; } - Files.Add(new ContentFile(subElement.GetAttributeString("file", ""), type, this)); + files.Add(new ContentFile(subElement.GetAttributeString("file", ""), type, this)); } if (Files.Count == 0) @@ -227,7 +287,7 @@ namespace Barotrauma string folder = System.IO.Path.GetDirectoryName(filePath); if (File.Exists(System.IO.Path.Combine(folder, Name+".sub"))) { - Files.Add(new ContentFile(System.IO.Path.Combine(folder, Name + ".sub"), ContentType.Submarine, this)); + files.Add(new ContentFile(System.IO.Path.Combine(folder, Name + ".sub"), ContentType.Submarine, this)); } else { @@ -318,7 +378,6 @@ namespace Barotrauma { switch (file.Type) { - case ContentType.Executable: case ContentType.ServerExecutable: case ContentType.None: case ContentType.Outpost: @@ -351,7 +410,7 @@ namespace Barotrauma } } - if (CorePackage && !ContainsRequiredCorePackageFiles(out List missingContentTypes)) + if (IsCorePackage && !ContainsRequiredCorePackageFiles(out List missingContentTypes)) { errorMessages.Add(TextManager.GetWithVariables("ContentPackageCantMakeCorePackage", new string[2] { "[packagename]", "[missingfiletypes]" }, @@ -375,7 +434,6 @@ namespace Barotrauma foreach (ContentFile file in Files) { //TODO: determine executable extension on platform and check for the presence of the executables - if (file.Type == ContentType.Executable) { continue; } if (file.Type == ContentType.ServerExecutable) { continue; } if (!File.Exists(file.Path)) @@ -394,7 +452,7 @@ namespace Barotrauma { Name = name, Path = path, - CorePackage = corePackage, + isCorePackage = corePackage, GameVersion = GameMain.Version }; @@ -403,35 +461,91 @@ namespace Barotrauma public ContentFile AddFile(string path, ContentType type) { - if (Files.Find(file => file.Path == path && file.Type == type) != null) return null; + if (Files.Concat(FilesToAdd).Any(file => file.Path == path && file.Type == type)) return null; ContentFile cf = new ContentFile(path, type) { ContentPackage = this }; - Files.Add(cf); + filesToAdd.Add(cf); return cf; } - public void RemoveFile(ContentFile file) + public void AddFile(ContentFile file) { - Files.Remove(file); + if (filesToRemove.Contains(file)) { filesToRemove.Remove(file); } + if (Files.Concat(FilesToAdd).Any(f => f.Path == file.Path && f.Type == file.Type)) return; + + filesToAdd.Add(file); } - public void Save(string filePath) + public void RemoveFile(ContentFile file) { + if (filesToAdd.Contains(file)) { filesToAdd.Remove(file); } + if (files.Contains(file) && !filesToRemove.Contains(file)) { filesToRemove.Add(file); } + } + + public void Save(string filePath, bool reload = true) + { + var packagesToDeselect = corePackages.Concat(regularPackages).Where(p => p.Path.CleanUpPath() == Path.CleanUpPath()).ToList(); + bool refreshFiles = false; + + if (packagesToDeselect.Any()) + { + foreach (var p in packagesToDeselect) + { + if (p.IsCorePackage) + { + if (GameMain.Config.CurrentCorePackage == p) + { + refreshFiles = true; + } + corePackages.Remove(p); + } + else + { + if (GameMain.Config.EnabledRegularPackages.Contains(p)) + { + refreshFiles = true; + } + regularPackages.Remove(p); + } + } + if (IsCorePackage) + { + corePackages.Add(this); + } + else + { + regularPackages.Add(this); + } + + if (refreshFiles) + { + GameMain.Config.DisableContentPackageItems(filesToRemove); + GameMain.Config.EnableContentPackageItems(filesToAdd); + GameMain.Config.RefreshContentPackageItems(filesToRemove.Concat(filesToAdd).Distinct()); + } + } + files.RemoveAll(f => filesToRemove.Contains(f)); + files.AddRange(filesToAdd); + filesToRemove.Clear(); filesToAdd.Clear(); + XDocument doc = new XDocument(); doc.Add(new XElement("contentpackage", new XAttribute("name", Name), new XAttribute("path", Path.CleanUpPathCrossPlatform(correctFilenameCase: false)), - new XAttribute("corepackage", CorePackage))); + new XAttribute("corepackage", IsCorePackage))); doc.Root.Add(new XAttribute("gameversion", GameVersion.ToString())); - if (!string.IsNullOrEmpty(SteamWorkshopUrl)) + if (SteamWorkshopId != 0) { - doc.Root.Add(new XAttribute("steamworkshopurl", SteamWorkshopUrl)); + doc.Root.Add(new XAttribute("steamworkshopid", SteamWorkshopId.ToString())); +#if UNSTABLE + doc.Root.Add(new XAttribute("steamworkshopurl", $"http://steamcommunity.com/sharedfiles/filedetails/?source=Facepunch.Steamworks&id={SteamWorkshopId}")); +#endif } if (InstallTime != null) @@ -445,41 +559,6 @@ namespace Barotrauma } doc.SaveSafe(filePath); - - var packagesToDeselect = List.Where(p => p.Path.CleanUpPath() == Path.CleanUpPath()).ToList(); - bool reselectPackage = false; - - if (packagesToDeselect.Any()) - { - foreach (var p in packagesToDeselect) - { - if (GameMain.Config.SelectedContentPackages.Contains(p)) - { - reselectPackage = true; - if (p.CorePackage) - { - GameMain.Config.AutoSelectCorePackage(packagesToDeselect); - } - else - { - GameMain.Config.DeselectContentPackage(p); - } - } - List.Remove(p); - } - List.Add(this); - if (reselectPackage) - { - if (CorePackage) - { - GameMain.Config.SelectCorePackage(this); - } - else - { - GameMain.Config.SelectContentPackage(this); - } - } - } } public void CalculateHash(bool logging = false) @@ -633,9 +712,31 @@ namespace Barotrauma return Files.Where(f => f.Type == type).Select(f => f.Path); } + public static void AddPackage(ContentPackage newPackage) + { + if (corePackages.Concat(regularPackages).Any(p => p.Name.Equals(newPackage.Name, StringComparison.OrdinalIgnoreCase))) + { + DebugConsole.ThrowError($"Attempted to add \"{newPackage.Name}\" more than once!\n{Environment.StackTrace}"); + } + if (newPackage.IsCorePackage) + { + corePackages.Add(newPackage); + } + else + { + regularPackages.Add(newPackage); + } + } + + public static void RemovePackage(ContentPackage package) + { + if (package.IsCorePackage) { corePackages.Remove(package); } + else { regularPackages.Remove(package); } + } + public static void LoadAll() { - string folder = ContentPackage.Folder; + string folder = Folder; if (!Directory.Exists(folder)) { try @@ -651,11 +752,13 @@ namespace Barotrauma IEnumerable files = Directory.GetFiles(folder, "*.xml"); - List.Clear(); + corePackages.Clear(); + var prevRegularPackages = regularPackages.Select(p => p.Name.ToLowerInvariant()).ToList(); + regularPackages.Clear(); foreach (string filePath in files) { - List.Add(new ContentPackage(filePath)); + AddPackage(new ContentPackage(filePath)); } IEnumerable modDirectories = Directory.GetDirectories("Mods"); @@ -671,54 +774,39 @@ namespace Barotrauma } else if (File.Exists(modFilePath)) { - List.Add(new ContentPackage(modFilePath)); + AddPackage(new ContentPackage(modFilePath)); } } - - List = List - .OrderByDescending(p => p.CorePackage) - .ThenByDescending(p => GameMain.Config?.SelectedContentPackages.Contains(p)) - .ThenBy(p => GameMain.Config?.SelectedContentPackages.IndexOf(p)) - .ToList(); + SortContentPackages(p => prevRegularPackages.IndexOf(p.Name.ToLowerInvariant())); + GameMain.Config?.SortContentPackages(); } - public static void SortContentPackages() + public static void SortContentPackages(Func order, bool refreshAll = false) { - if (GameMain.Config != null) - { - List = List - .OrderByDescending(p => p.CorePackage) - .ThenBy(p => GameMain.Config.SelectedContentPackages.IndexOf(p)) - .ThenBy(p => List.IndexOf(p)) - .ToList(); - - var sortedSelected = GameMain.Config.SelectedContentPackages - .OrderByDescending(p => p.CorePackage) - .ThenBy(p => GameMain.Config.SelectedContentPackages.IndexOf(p)) - .ToList(); - GameMain.Config.SelectedContentPackages.Clear(); GameMain.Config.SelectedContentPackages.AddRange(sortedSelected); - - var reportList = GameMain.Config.SelectedContentPackages; - DebugConsole.NewMessage($"Content package load order: { string.Join(" | ", reportList.Select(cp => cp.Name)) }"); - } - else - { - List = List - .OrderByDescending(p => p.CorePackage) - .ThenBy(p => List.IndexOf(p)) - .ToList(); - } + var ordered = regularPackages + .OrderBy(p => order(p)) + .ThenBy(p => regularPackages.IndexOf(p)) + .ToList(); + regularPackages.Clear(); regularPackages.AddRange(ordered); + GameMain.Config?.SortContentPackages(refreshAll); } public void Delete() { try { - GameMain.Config.DeselectContentPackage(this); + if (IsCorePackage) + { + corePackages.Remove(this); + if (GameMain.Config.CurrentCorePackage == this) { GameMain.Config.AutoSelectCorePackage(null); } + } + else + { + regularPackages.Remove(this); + if (GameMain.Config.EnabledRegularPackages.Contains(this)) { GameMain.Config.DisableRegularPackage(this); } + } GameMain.Config.SaveNewPlayerConfig(); - List.Remove(this); File.Delete(Path); - SortContentPackages(); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 567a2451d..8e65cda85 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -258,7 +258,7 @@ namespace Barotrauma { HumanAIController.DisableCrewAI = true; NewMessage("Crew AI disabled", Color.Red); - })); + }, isCheat: true)); commands.Add(new Command("enablecrewai", "enablecrewai: Enable the AI of the NPCs in the crew.", (string[] args) => { @@ -509,7 +509,21 @@ namespace Barotrauma return new string[][] { ListCharacterNames() }; }, isCheat: true)); - commands.Add(new Command("godmode", "godmode: Toggle submarine godmode. Makes the main submarine invulnerable to damage.", (string[] args) => + commands.Add(new Command("godmode", "godmode [character name]: Toggle character godmode. Makes the targeted character invulnerable to damage. If the name parameter is omitted, the controlled character will receive godmode.", + (string[] args) => + { + Character targetCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, false); + + if (targetCharacter == null) { return; } + + targetCharacter.GodMode = !targetCharacter.GodMode; + }, + () => + { + return new string[][] { ListCharacterNames() }; + }, isCheat: true)); + + commands.Add(new Command("godmode_mainsub", "godmode_mainsub: Toggle submarine godmode. Makes the main submarine invulnerable to damage.", (string[] args) => { if (Submarine.MainSub == null) return; @@ -517,6 +531,17 @@ namespace Barotrauma NewMessage(Submarine.MainSub.GodMode ? "Godmode on" : "Godmode off", Color.White); }, isCheat: true)); + commands.Add(new Command("growthdelay", "growthdelay: Sets how long it takes for planters to attempt to advance a plant's growth.", (string[] args) => + { + if (args.Length > 0 && float.TryParse(args[0], out float value)) + { + Planter.GrowthTickDelay = value; + NewMessage($"Growth delay set to {value}.", Color.Green); + return; + } + NewMessage("Invalid value.", Color.Red); + }, isCheat: true)); + commands.Add(new Command("lock", "lock: Lock movement of the main submarine.", (string[] args) => { Submarine.LockX = !Submarine.LockX; @@ -1208,6 +1233,17 @@ namespace Barotrauma } })); + commands.Add(new Command("togglecampaignteleport", "Toggle on/off teleportation between campaign locations by double clicking on the campaign map.", args => + { + if (GameMain.GameSession?.Campaign == null) + { + ThrowError("No campaign active."); + return; + } + GameMain.GameSession.Map.AllowDebugTeleport = !GameMain.GameSession.Map.AllowDebugTeleport; + NewMessage((GameMain.GameSession.Map.AllowDebugTeleport ? "Enabled" : "Disabled") + " teleportation on the campaign map.", Color.White); + }, isCheat: true)); + commands.Add(new Command("money", "", args => { if (args.Length == 0) { return; } @@ -1238,14 +1274,14 @@ namespace Barotrauma NewMessage((GameSettings.VerboseLogging ? "Enabled" : "Disabled") + " verbose logging.", Color.White); }, isCheat: false)); - commands.Add(new Command("listtasks", "listtasks: Lists all asynchronous tasks currently in the task pool.", TaskPool.ListTasks)); + commands.Add(new Command("listtasks", "listtasks: Lists all asynchronous tasks currently in the task pool.", (string[] args) => { TaskPool.ListTasks(); })); commands.Add(new Command("calculatehashes", "calculatehashes [content package name]: Show the MD5 hashes of the files in the selected content package. If the name parameter is omitted, the first content package is selected.", (string[] args) => { if (args.Length > 0) { string packageName = string.Join(" ", args).ToLower(); - var package = GameMain.Config.SelectedContentPackages.FirstOrDefault(p => p.Name.ToLower() == packageName); + var package = GameMain.Config.AllEnabledPackages.FirstOrDefault(p => p.Name.ToLower() == packageName); if (package == null) { ThrowError("Content package \"" + packageName + "\" not found."); @@ -1257,14 +1293,14 @@ namespace Barotrauma } else { - GameMain.Config.SelectedContentPackages.First().CalculateHash(logging: true); + GameMain.Config.AllEnabledPackages.First().CalculateHash(logging: true); } }, () => { return new string[][] { - GameMain.Config.SelectedContentPackages.Select(cp => cp.Name).ToArray() + GameMain.Config.AllEnabledPackages.Select(cp => cp.Name).ToArray() }; })); @@ -1859,7 +1895,7 @@ namespace Barotrauma if (GameSettings.VerboseLogging) NewMessage(message, Color.Gray); } - public static void ThrowError(string error, Exception e = null, bool createMessageBox = false) + public static void ThrowError(string error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false) { if (e != null) { @@ -1869,6 +1905,10 @@ namespace Barotrauma error += "\n\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace; } } + else if (appendStackTrace) + { + error += "\n" + Environment.StackTrace; + } System.Diagnostics.Debug.WriteLine(error); #if CLIENT @@ -1888,27 +1928,7 @@ namespace Barotrauma public static void AddWarning(string warning) { System.Diagnostics.Debug.WriteLine(warning); -#if CLIENT - if (listBox == null) { NewMessage($"WARNING: {warning}", Color.Yellow); return; } - - var textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), style: "InnerFrame", color: Color.White) - { - CanBeFocused = false - }; - var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width - 5, 0), textContainer.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(2, 2) }, - warning, textAlignment: Alignment.TopLeft, font: GUI.SmallFont, wrap: true) - { - CanBeFocused = false, - TextColor = Color.Yellow - }; - textContainer.RectTransform.NonScaledSize = new Point(textContainer.RectTransform.NonScaledSize.X, textBlock.RectTransform.NonScaledSize.Y + 5); - textBlock.SetTextPos(); - - listBox.UpdateScrollBarSize(); - listBox.BarScroll = 1.0f; -#else NewMessage($"WARNING: {warning}", Color.Yellow); -#endif } #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Decals/Decal.cs b/Barotrauma/BarotraumaShared/SharedSource/Decals/Decal.cs new file mode 100644 index 000000000..6a304b697 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Decals/Decal.cs @@ -0,0 +1,167 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; + +namespace Barotrauma +{ + partial class Decal + { + public readonly DecalPrefab Prefab; + private Vector2 position; + + private float fadeTimer; + + public readonly Sprite Sprite; + + public float FadeTimer + { + get { return fadeTimer; } + set { fadeTimer = MathHelper.Clamp(value, 0.0f, LifeTime); } + } + + public float FadeInTime + { + get { return Prefab.FadeInTime; } + } + + public float FadeOutTime + { + get { return Prefab.FadeOutTime; } + } + + public float LifeTime + { + get { return Prefab.LifeTime; } + } + + private float baseAlpha = 1.0f; + public float BaseAlpha + { + get { return baseAlpha; } + } + + public Color Color + { + get; + set; + } + + public Vector2 WorldPosition + { + get + { + Vector2 worldPos = position + + clippedSourceRect.Size.ToVector2() / 2 * Scale + + hull.Rect.Location.ToVector2(); + if (hull.Submarine != null) { worldPos += hull.Submarine.DrawPosition; } + return worldPos; + } + } + + public Vector2 Position + { + get { return position; } + } + + public Vector2 NonClampedPosition + { + get; + private set; + } + + private readonly HashSet affectedSections; + + private readonly Hull hull; + + public readonly float Scale; + + private Rectangle clippedSourceRect; + + private bool cleaned = false; + + public Decal(DecalPrefab prefab, float scale, Vector2 worldPosition, Hull hull) + { + Prefab = prefab; + + this.hull = hull; + + //transform to hull-relative coordinates so we don't have to worry about the hull moving + NonClampedPosition = position = worldPosition - hull.WorldRect.Location.ToVector2(); + + Vector2 drawPos = position + hull.Rect.Location.ToVector2(); + + Sprite = prefab.Sprites[Rand.Range(0, prefab.Sprites.Count, Rand.RandSync.Unsynced)]; + Color = prefab.Color; + + Rectangle drawRect = new Rectangle( + (int)(drawPos.X - Sprite.size.X / 2 * scale), + (int)(drawPos.Y + Sprite.size.Y / 2 * scale), + (int)(Sprite.size.X * scale), + (int)(Sprite.size.Y * scale)); + + Rectangle overFlowAmount = new Rectangle( + (int)Math.Max(hull.Rect.X - drawRect.X, 0.0f), + (int)Math.Max(drawRect.Y - hull.Rect.Y, 0.0f), + (int)Math.Max(drawRect.Right - hull.Rect.Right, 0.0f), + (int)Math.Max((hull.Rect.Y - hull.Rect.Height) - (drawRect.Y - drawRect.Height), 0.0f)); + + clippedSourceRect = new Rectangle( + Sprite.SourceRect.X + (int)(overFlowAmount.X / scale), + Sprite.SourceRect.Y + (int)(overFlowAmount.Y / scale), + Sprite.SourceRect.Width - (int)((overFlowAmount.X + overFlowAmount.Width) / scale), + Sprite.SourceRect.Height - (int)((overFlowAmount.Y + overFlowAmount.Height) / scale)); + + position -= new Vector2(Sprite.size.X / 2 * scale - overFlowAmount.X, -Sprite.size.Y / 2 * scale + overFlowAmount.Y); + + this.Scale = scale; + + foreach (BackgroundSection section in hull.GetBackgroundSectionsViaContaining(new Rectangle((int)position.X, (int)position.Y - drawRect.Height, drawRect.Width, drawRect.Height))) + { + affectedSections ??= new HashSet(); + affectedSections.Add(section); + } + } + + public void Update(float deltaTime) + { + fadeTimer += deltaTime; + } + + public void ForceRefreshFadeTimer(float val) + { + cleaned = false; + fadeTimer = val; + } + + public void StopFadeIn() + { + Color *= GetAlpha(); + fadeTimer = Prefab.FadeInTime; + } + + public bool AffectsSection(BackgroundSection section) + { + return affectedSections != null && affectedSections.Contains(section); + } + + public void Clean(float val) + { + cleaned = true; + float sizeModifier = MathHelper.Clamp(Sprite.size.X * Sprite.size.Y * Scale / 10000, 1.0f, 25.0f); + baseAlpha -= val * -1 / sizeModifier; + } + + private float GetAlpha() + { + if (fadeTimer < Prefab.FadeInTime && !cleaned) + { + return baseAlpha * fadeTimer / Prefab.FadeInTime; + } + else if (cleaned || fadeTimer > Prefab.LifeTime - Prefab.FadeOutTime) + { + return baseAlpha * Math.Min((Prefab.LifeTime - fadeTimer) / Prefab.FadeOutTime, 1.0f); + } + return baseAlpha; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs new file mode 100644 index 000000000..56d3b76d1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs @@ -0,0 +1,124 @@ +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Xml.Linq; + +namespace Barotrauma +{ + class DecalManager + { + public PrefabCollection Prefabs { get; private set; } + + public readonly List GrimeSprites = new List(); + private Dictionary> grimeSpritesByFile = new Dictionary>(); + + public DecalManager() + { + Prefabs = new PrefabCollection(); + foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.Decals)) + { + LoadFromFile(configFile); + } + } + + public void LoadFromFile(ContentFile configFile) + { + XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); + if (doc == null) { return; } + + if (grimeSpritesByFile.ContainsKey(configFile.Path)) + { + foreach (Sprite sprite in grimeSpritesByFile[configFile.Path]) + { + sprite.Remove(); + GrimeSprites.Remove(sprite); + } + grimeSpritesByFile.Remove(configFile.Path); + } + + bool allowOverriding = false; + var mainElement = doc.Root; + if (doc.Root.IsOverride()) + { + mainElement = doc.Root.FirstElement(); + allowOverriding = true; + } + + foreach (XElement sourceElement in mainElement.Elements()) + { + var element = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement; + string name = element.Name.ToString().ToLowerInvariant(); + + switch (name) + { + case "grime": + if (!grimeSpritesByFile.ContainsKey(configFile.Path)) + { + grimeSpritesByFile.Add(configFile.Path, new List()); + } + var grimeSprite = new Sprite(element); + GrimeSprites.Add(grimeSprite); + grimeSpritesByFile[configFile.Path].Add(grimeSprite); + break; + default: + if (Prefabs.ContainsKey(name)) + { + if (allowOverriding || sourceElement.IsOverride()) + { + DebugConsole.NewMessage($"Overriding the existing decal prefab '{name}' using the file '{configFile.Path}'", Color.Yellow); + } + else + { + DebugConsole.ThrowError($"Error in '{configFile.Path}': Duplicate decal prefab '{name}' found in '{configFile.Path}'! Each decal prefab must have a unique name. " + + "Use tags to override prefabs."); + continue; + } + } + Prefabs.Add(new DecalPrefab(element, configFile), allowOverriding || sourceElement.IsOverride()); + break; + } + } + + using MD5 md5 = MD5.Create(); + foreach (DecalPrefab prefab in Prefabs) + { + prefab.UIntIdentifier = ToolBox.StringToUInt32Hash(prefab.Identifier, md5); + + //it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small + var collision = Prefabs.Find(p => p != prefab && p.UIntIdentifier == prefab.UIntIdentifier); + if (collision != null) + { + DebugConsole.ThrowError("Hashing collision when generating uint identifiers for Decals: " + prefab.Identifier + " has the same identifier as " + collision.Identifier + " (" + prefab.UIntIdentifier + ")"); + collision.UIntIdentifier++; + } + } + } + + public void RemoveByFile(string filePath) + { + Prefabs.RemoveByFile(filePath); + if (grimeSpritesByFile.ContainsKey(filePath)) + { + foreach (Sprite sprite in grimeSpritesByFile[filePath]) + { + sprite.Remove(); + GrimeSprites.Remove(sprite); + } + grimeSpritesByFile.Remove(filePath); + } + } + + public Decal CreateDecal(string decalName, float scale, Vector2 worldPosition, Hull hull) + { + if (!Prefabs.ContainsKey(decalName.ToLowerInvariant())) + { + DebugConsole.ThrowError("Decal prefab " + decalName + " not found!"); + return null; + } + + DecalPrefab prefab = Prefabs[decalName]; + + return new Decal(prefab, scale, worldPosition, hull); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalPrefab.cs new file mode 100644 index 000000000..fe55e454d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalPrefab.cs @@ -0,0 +1,74 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma +{ + class DecalPrefab : IPrefab, IDisposable + { + public readonly string Name; + + public string OriginalName { get { return Name; } } + + public string Identifier + { + get; + private set; + } + + /// + /// Unique identifier that's generated by hashing the prefab's string identifier. + /// Used to reduce the amount of bytes needed to write decal data into network messages in multiplayer. + /// + public uint UIntIdentifier; + + public string FilePath { get; private set; } + + public ContentPackage ContentPackage { get; private set; } + + public void Dispose() + { + foreach (Sprite spr in Sprites) + { + spr.Remove(); + } + Sprites.Clear(); + } + + public readonly List Sprites; + + public readonly Color Color; + + public readonly float LifeTime; + public readonly float FadeOutTime; + public readonly float FadeInTime; + + public DecalPrefab(XElement element, ContentFile file) + { + Name = element.Name.ToString(); + + Identifier = Name.ToLowerInvariant(); + + FilePath = file.Path; + + ContentPackage = file.ContentPackage; + + Sprites = new List(); + + foreach (XElement subElement in element.Elements()) + { + if (subElement.Name.ToString().Equals("sprite", StringComparison.OrdinalIgnoreCase)) + { + Sprites.Add(new Sprite(subElement)); + } + } + + Color = new Color(element.GetAttributeVector4("color", Vector4.One)); + + LifeTime = element.GetAttributeFloat("lifetime", 10.0f); + FadeOutTime = Math.Min(LifeTime, element.GetAttributeFloat("fadeouttime", 1.0f)); + FadeInTime = Math.Min(LifeTime - FadeOutTime, element.GetAttributeFloat("fadeintime", 0.0f)); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 3d70417af..626979d2c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -20,6 +20,7 @@ OnEating, OnDeath = OnBroken, OnDamaged, - OnSevered + OnSevered, + OnProduceSpawned } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/BinaryOptionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/BinaryOptionAction.cs index 5c863cc83..5eb38c547 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/BinaryOptionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/BinaryOptionAction.cs @@ -67,6 +67,11 @@ namespace Barotrauma return false; } + protected bool HasBeenDetermined() + { + return succeeded.HasValue; + } + public override bool SetGoToTarget(string goTo) { if (Success != null && Success.SetGoToTarget(goTo)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index 81020e191..b1ba89a2a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -34,11 +33,11 @@ namespace Barotrauma if (!(target is Character chr)) { continue; } if (chr.Inventory == null) { continue; } - if (itemTags.Any(tag => chr.Inventory.Items.Any(item => item != null && item.HasTag(tag)))) { return true; } + if (itemTags.Any(tag => chr.Inventory.FindItemByTag(tag, recursive: true) != null)) { return true; } foreach (var identifier in itemIdentifierSplit) { - if (chr.Inventory.Items.Any(it => it != null && it.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase))) + if (chr.Inventory.FindItemByIdentifier(identifier, recursive: true) != null) { return true; } @@ -50,15 +49,9 @@ namespace Barotrauma public override string ToDebugString() { - string subActionStr = ""; - if (succeeded.HasValue) - { - subActionStr = $"\n Sub action: {(succeeded.Value ? Success : Failure)?.CurrentSubAction.ColorizeObject()}"; - } - return $"{ToolBox.GetDebugSymbol(DetermineFinished())} {nameof(CheckItemAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " + + return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CheckItemAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " + $"ItemIdentifiers: {ItemIdentifiers.ColorizeObject()}" + - $"Succeeded: {(succeeded.HasValue ? succeeded.Value.ToString() : "not determined").ColorizeObject()})" + - subActionStr; + $"Succeeded: {succeeded.ColorizeObject()})"; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs new file mode 100644 index 000000000..f19e759a9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs @@ -0,0 +1,35 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + class CheckMoneyAction : BinaryOptionAction + { + [Serialize(0, true)] + public int Amount { get; set; } + + public CheckMoneyAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + { + } + + protected override bool? DetermineSuccess() + { + if (GameMain.GameSession?.GameMode is CampaignMode campaign) + { + return campaign.Money >= Amount; + } + return false; + } + + public override string ToDebugString() + { + string subActionStr = ""; + if (succeeded.HasValue) + { + subActionStr = $"\n Sub action: {(succeeded.Value ? Success : Failure)?.CurrentSubAction.ColorizeObject()}"; + } + return $"{ToolBox.GetDebugSymbol(DetermineFinished())} {nameof(CheckMoneyAction)} -> (Amount: {Amount.ColorizeObject()}" + + $" Succeeded: {(succeeded.HasValue ? succeeded.Value.ToString() : "not determined").ColorizeObject()})" + + subActionStr; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs index 68ffb7020..c42a96330 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs @@ -10,6 +10,15 @@ namespace Barotrauma [Serialize(AIObjectiveCombat.CombatMode.Offensive, true)] public AIObjectiveCombat.CombatMode CombatMode { get; set; } + [Serialize(false, true, description: "Did this NPC start the fight (as an aggressor)?")] + public bool IsInstigator { get; set; } + + [Serialize(AIObjectiveCombat.CombatMode.None, true)] + public AIObjectiveCombat.CombatMode GuardReaction { get; set; } + + [Serialize(AIObjectiveCombat.CombatMode.None, true)] + public AIObjectiveCombat.CombatMode WitnessReaction { get; set; } + [Serialize("", true)] public string NPCTag { get; set; } @@ -50,7 +59,8 @@ namespace Barotrauma } if (enemy == null) { continue; } - npc.TurnedHostileByEvent = true; + npc.CombatAction = this; + var objectiveManager = humanAiController.ObjectiveManager; foreach (var goToObjective in objectiveManager.GetActiveObjectives()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index e14e51a53..02ea64468 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -55,6 +55,7 @@ namespace Barotrauma private Character speaker; private OrderInfo? prevSpeakerOrder; + private AIObjective prevIdleObjective, prevGotoObjective; public List Options { get; private set; } @@ -169,13 +170,19 @@ namespace Barotrauma #if SERVER GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); #endif - if (prevSpeakerOrder != null) + var humanAI = speaker.AIController as HumanAIController; + if (humanAI != null) { - (speaker.AIController as HumanAIController)?.SetOrder(prevSpeakerOrder.Value.Order, prevSpeakerOrder.Value.OrderOption, orderGiver: null, speak: false); - } - else - { - (speaker.AIController as HumanAIController)?.SetOrder(null, string.Empty, orderGiver: null, speak: false); + if (prevSpeakerOrder != null) + { + humanAI.SetOrder(prevSpeakerOrder.Value.Order, prevSpeakerOrder.Value.OrderOption, orderGiver: null, speak: false); + } + else + { + humanAI.SetOrder(null, string.Empty, orderGiver: null, speak: false); + } + if (prevIdleObjective != null) { humanAI.ObjectiveManager.AddObjective(prevIdleObjective); } + if (prevGotoObjective != null) { humanAI.ObjectiveManager.AddObjective(prevGotoObjective); } } } @@ -246,9 +253,12 @@ namespace Barotrauma TryStartConversation(null); } } - else if (Options.Any()) + else { - Options[selectedOption].Update(deltaTime); + if (Options.Any()) + { + Options[selectedOption].Update(deltaTime); + } } } @@ -300,6 +310,8 @@ namespace Barotrauma { prevSpeakerOrder = new OrderInfo(humanAI.CurrentOrder, humanAI.CurrentOrderOption); } + prevIdleObjective = humanAI.ObjectiveManager.GetObjective(); + prevGotoObjective = humanAI.ObjectiveManager.GetObjective(); humanAI.SetOrder( Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase)), option: string.Empty, orderGiver: null, speak: false); @@ -334,25 +346,11 @@ namespace Barotrauma { if (!interrupt) { - SubactionGroup selOtion = null; - if (selectedOption >= 0 && Options.Count > selectedOption) - { - selOtion = Options[selectedOption]; - } - - EventAction subAction = null; - if (selOtion != null) - { - subAction = selOtion.CurrentSubAction; - } - - return $"{ToolBox.GetDebugSymbol(selectedOption > -1)} {nameof(ConversationAction)} -> (Selected option: {selOtion?.Text.ColorizeObject()})\n" + - $" Sub action: {subAction.ColorizeObject()}"; + return $"{ToolBox.GetDebugSymbol(selectedOption > -1, selectedOption < 0 && dialogOpened)} {nameof(ConversationAction)} -> (Selected option: {selectedOption.ColorizeObject()})"; } else { - return $"{ToolBox.GetDebugSymbol(true)} {nameof(ConversationAction)} -> (Interrupted)\n" + - $" Sub action: {Interrupted?.CurrentSubAction.ColorizeObject()}"; + return $"{ToolBox.GetDebugSymbol(true, selectedOption < 0 && dialogOpened)} {nameof(ConversationAction)} -> (Interrupted)"; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs index afaaf1a1c..f198ac6f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs @@ -42,7 +42,7 @@ namespace Barotrauma var targets = ParentEvent.GetTargets(TargetTag).Where(e => e is Character).Select(e => e as Character); foreach (var target in targets) { - target.Info?.IncreaseSkillLevel(Skill, Amount, target.WorldPosition + Vector2.UnitY * 150.0f); + target.Info?.IncreaseSkillLevel(Skill?.ToLowerInvariant(), Amount, target.WorldPosition + Vector2.UnitY * 150.0f); } isFinished = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs index 351280384..27197a60a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs @@ -19,6 +19,8 @@ namespace Barotrauma private List affectedNpcs = null; + private AIObjectiveGoTo gotoObjective; + public override void Update(float deltaTime) { if (isFinished) { return; } @@ -31,21 +33,18 @@ namespace Barotrauma if (Wait) { - var newObjective = new AIObjectiveGoTo(npc, npc, humanAiController.ObjectiveManager, repeat: true) + gotoObjective = new AIObjectiveGoTo(npc, npc, humanAiController.ObjectiveManager, repeat: true) { OverridePriority = 100.0f }; - humanAiController.ObjectiveManager.AddObjective(newObjective); + humanAiController.ObjectiveManager.AddObjective(gotoObjective); humanAiController.ObjectiveManager.WaitTimer = 0.0f; } else { - foreach (var goToObjective in humanAiController.ObjectiveManager.GetActiveObjectives()) + if (gotoObjective != null) { - if (goToObjective.Target == npc) - { - goToObjective.Abandon = true; - } + gotoObjective.Abandon = true; } } } @@ -64,13 +63,10 @@ namespace Barotrauma foreach (var npc in affectedNpcs) { if (npc.Removed || !(npc.AIController is HumanAIController humanAiController)) { continue; } - foreach (var goToObjective in humanAiController.ObjectiveManager.GetActiveObjectives()) + if (gotoObjective != null) { - if (goToObjective.Target == npc) - { - goToObjective.Abandon = true; - } - } + gotoObjective.Abandon = true; + } } affectedNpcs = null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs index a8e016e75..148ef7e9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs @@ -11,21 +11,18 @@ namespace Barotrauma public RNGAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + private bool isFinished; + protected override bool? DetermineSuccess() { + isFinished = true; return Rand.Range(0.0, 1.0) <= Chance; } public override string ToDebugString() { - string subActionStr = ""; - if (succeeded.HasValue) - { - subActionStr = $"\n Sub action: {(succeeded.Value ? Success : Failure)?.CurrentSubAction.ColorizeObject()}"; - } - return $"{ToolBox.GetDebugSymbol(DetermineFinished())} {nameof(RNGAction)} -> (Chance: {Chance.ColorizeObject()}, "+ - $"Succeeded: {(succeeded.HasValue ? succeeded.Value.ToString() : "not determined").ColorizeObject()})" + - subActionStr; + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(RNGAction)} -> (Chance: {Chance.ColorizeObject()}, "+ + $"Succeeded: {succeeded.ColorizeObject()})"; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs index f4899a4a3..716427172 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -15,7 +16,17 @@ namespace Barotrauma [Serialize(1, true)] public int Amount { get; set; } - public RemoveItemAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public RemoveItemAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + { + if (string.IsNullOrWhiteSpace(ItemIdentifier)) + { + ItemIdentifier = element.GetAttributeString("itemidentifiers", ""); + } + if (string.IsNullOrWhiteSpace(ItemIdentifier)) + { + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\" - RemoveItemAction without an item identifier."); + } + } private bool isFinished = false; @@ -32,25 +43,33 @@ namespace Barotrauma { if (isFinished) { return; } - var targets = ParentEvent.GetTargets(TargetTag) - .Where(t => t is Character chr && chr.Inventory != null) - .Select(t => t as Character).ToList(); - if (targets.Count <= 0) { return; } - - int count = Amount; - while (count > 0 && targets.Count > 0) + var targets = ParentEvent.GetTargets(TargetTag); + bool hasValidTargets = false; + foreach (Entity target in targets) { - var items = targets[0].Inventory.Items; - for (int i = 0; i < items.Length; i++) + if (target is Character character && character.Inventory != null) { - if (items[i] != null && items[i].Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase)) - { - Entity.Spawner.AddToRemoveQueue(items[i]); - count--; - if (count <= 0) { break; } - } + hasValidTargets = true; + break; + } + } + if (!hasValidTargets) { return; } + + List usedItems = new List(); + foreach (Entity target in targets) + { + Inventory inventory = (target as Character)?.Inventory; + if (inventory == null) { continue; } + while (usedItems.Count < Amount) + { + var item = inventory.FindItem(it => + it != null && + !usedItems.Contains(it) && + it.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase), recursive: true); + if (item == null) { break; } + Entity.Spawner.AddToRemoveQueue(item); + usedItems.Add(item); } - targets.RemoveAt(0); } isFinished = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs index aaec7eb29..d9e180e3a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs @@ -40,38 +40,42 @@ namespace Barotrauma if (GameMain.GameSession?.GameMode is CampaignMode campaign) { - object currentValue = campaign.CampaignMetadata.GetValue(Identifier); - object xmlValue = ConvertXMLValue(); - - float? originalValue = ConvertValueToFloat(currentValue ?? 0); - float? newValue = ConvertValueToFloat(xmlValue); - - if ((originalValue == null || newValue == null) && Operation != OperationType.Set) - { - DebugConsole.ThrowError($"Tried to perform numeric operations to a non number via SetDataAction (Existing: {currentValue?.GetType()}, New: {xmlValue.GetType()})"); - return; - } - - if (Identifier != null) - { - switch (Operation) - { - case OperationType.Set: - campaign.CampaignMetadata.SetValue(Identifier, xmlValue); - break; - case OperationType.Add: - campaign.CampaignMetadata.SetValue(Identifier, originalValue + newValue ?? 0); - break; - case OperationType.Multiply: - campaign.CampaignMetadata.SetValue(Identifier, originalValue * newValue ?? 0); - break; - } - } + object xmlValue = ConvertXMLValue(Value); + PerformOperation(campaign.CampaignMetadata, Identifier, xmlValue, Operation); } isFinished = true; } + public static void PerformOperation(CampaignMetadata metadata, string identifier, object value, OperationType operation) + { + if (metadata == null) { return; } + + object currentValue = metadata.GetValue(identifier); + + float? originalValue = ConvertValueToFloat(currentValue ?? 0); + float? newValue = ConvertValueToFloat(value); + + if ((originalValue == null || newValue == null) && operation != OperationType.Set) + { + DebugConsole.ThrowError($"Tried to perform numeric operations to a non number via SetDataAction (Existing: {currentValue?.GetType()}, New: {value.GetType()})"); + return; + } + + switch (operation) + { + case OperationType.Set: + metadata.SetValue(identifier, value); + break; + case OperationType.Add: + metadata.SetValue(identifier, originalValue + newValue ?? 0); + break; + case OperationType.Multiply: + metadata.SetValue(identifier, originalValue * newValue ?? 0); + break; + } + } + private static float? ConvertValueToFloat(object value) { if (value is float || value is int) @@ -82,24 +86,24 @@ namespace Barotrauma return null; } - private object ConvertXMLValue() + public static object ConvertXMLValue(string value) { - if (bool.TryParse(Value, out bool b)) + if (bool.TryParse(value, out bool b)) { return b; } - if (float.TryParse(Value, out float f)) + if (float.TryParse(value, out float f)) { return f; } - return Value; + return value; } public override string ToDebugString() { - return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(SetDataAction)} -> (Identifier: {Identifier.ColorizeObject()}, Value: {ConvertXMLValue().ColorizeObject()}, Operation: {Operation.ColorizeObject()})"; + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(SetDataAction)} -> (Identifier: {Identifier.ColorizeObject()}, Value: {ConvertXMLValue(Value).ColorizeObject()}, Operation: {Operation.ColorizeObject()})"; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs index 9bb3a5743..5f3a46771 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs @@ -32,15 +32,9 @@ namespace Barotrauma public override string ToDebugString() { - string subActionStr = ""; - if (succeeded.HasValue) - { - subActionStr = $"\n Sub action: {(succeeded.Value ? Success : Failure)?.CurrentSubAction.ColorizeObject()}"; - } - return $"{ToolBox.GetDebugSymbol(DetermineFinished())} {nameof(SkillCheckAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " + - $"Required skill: {RequiredSkill.ColorizeObject()}, Required level: {RequiredLevel.ColorizeObject()}, " + - $"Succeeded: {(succeeded.HasValue ? succeeded.Value.ToString() : "not determined").ColorizeObject()})" + - subActionStr; + return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(SkillCheckAction)} -> (Target: {TargetTag.ColorizeObject()}, " + + $"Skill: {RequiredSkill.ColorizeObject()}, Level: {RequiredLevel.ColorizeObject()}, " + + $"Succeeded: {succeeded.ColorizeObject()})"; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index 1732e530f..cefadd1d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -41,13 +41,18 @@ namespace Barotrauma } public override void Reset() { + isRunning = false; isFinished = false; } + public bool isRunning = false; + public override void Update(float deltaTime) { if (isFinished) { return; } + isRunning = true; + var targets1 = ParentEvent.GetTargets(Target1Tag); if (!targets1.Any()) { return; } @@ -155,6 +160,8 @@ namespace Barotrauma { ParentEvent.AddTarget(ApplyToTarget2, entity2); } + + isRunning = false; isFinished = true; } @@ -162,11 +169,11 @@ namespace Barotrauma { if (string.IsNullOrEmpty(TargetModuleType)) { - return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(TriggerAction)} -> (Distance: {((int)distance).ColorizeObject()}, Radius: {Radius.ColorizeObject()}, TargetTags: {Target1Tag.ColorizeObject()}, {Target2Tag.ColorizeObject()})"; + return $"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (Distance: {((int)distance).ColorizeObject()}, Radius: {Radius.ColorizeObject()}, TargetTags: {Target1Tag.ColorizeObject()}, {Target2Tag.ColorizeObject()})"; } else { - return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(TriggerAction)} -> (TargetTags: {Target1Tag.ColorizeObject()}, {TargetModuleType.ColorizeObject()})"; + return $"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (TargetTags: {Target1Tag.ColorizeObject()}, {TargetModuleType.ColorizeObject()})"; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index d94db4483..796e995d2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -19,6 +19,8 @@ namespace Barotrauma const float CalculateDistanceTraveledInterval = 5.0f; + const int MaxEventHistory = 20; + private Level level; private readonly List preloadedSprites = new List(); @@ -110,10 +112,25 @@ namespace Barotrauma if (level?.LevelData?.Type == LevelData.LevelType.Outpost) { - level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab)); - if (level.LevelData.EventHistory.Count > 10) + 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 - 10); + level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory); + } + AddChildEvents(initialEventSet); + void AddChildEvents(EventSet eventSet) + { + foreach (EventPrefab ep in eventSet.EventPrefabs.Select(e => e.First)) + { + if (!level.LevelData.NonRepeatableEvents.Contains(ep)) + { + level.LevelData.NonRepeatableEvents.Add(ep); + } + } + foreach (EventSet childSet in eventSet.ChildSets) + { + AddChildEvents(childSet); + } } } @@ -301,6 +318,7 @@ namespace Barotrauma private float CalculateCommonness(Pair eventPrefab) { + if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab.First)) { return 0.0f; } float retVal = eventPrefab.Second; if (level.LevelData.EventHistory.Contains(eventPrefab.First)) { retVal *= 0.1f; } return retVal; @@ -324,7 +342,13 @@ namespace Barotrauma { if (eventSet.EventPrefabs.Count > 0) { - MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed)); + int seed = ToolBox.StringToInt(level.Seed); + foreach (var previousEvent in level.LevelData.EventHistory) + { + seed |= ToolBox.StringToInt(previousEvent.Identifier); + } + + MTRandom rand = new MTRandom(seed); List> unusedEvents = new List>(eventSet.EventPrefabs); for (int j = 0; j < eventSet.EventCount; j++) { @@ -476,37 +500,42 @@ namespace Barotrauma eventThreshold += settings.EventThresholdIncrease * deltaTime; eventCoolDown -= deltaTime; - + if (currentIntensity < eventThreshold) { - //activate pending event sets that can be activated - for (int i = pendingEventSets.Count - 1; i >= 0; i--) + bool recheck = false; + do { - var eventSet = pendingEventSets[i]; - if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; } - - if (!CanStartEventSet(eventSet)) { continue; } - - eventThreshold = settings.DefaultEventThreshold; - eventCoolDown = settings.EventCooldown; - - pendingEventSets.RemoveAt(i); - - if (selectedEvents.ContainsKey(eventSet)) + recheck = false; + //activate pending event sets that can be activated + for (int i = pendingEventSets.Count - 1; i >= 0; i--) { - //start events in this set - foreach (Event ev in selectedEvents[eventSet]) + var eventSet = pendingEventSets[i]; + if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; } + + if (!CanStartEventSet(eventSet)) { continue; } + + pendingEventSets.RemoveAt(i); + + if (selectedEvents.ContainsKey(eventSet)) { - activeEvents.Add(ev); + //start events in this set + foreach (Event ev in selectedEvents[eventSet]) + { + activeEvents.Add(ev); + eventThreshold = settings.DefaultEventThreshold; + eventCoolDown = settings.EventCooldown; + } + } + + //add child event sets to pending + foreach (EventSet childEventSet in eventSet.ChildSets) + { + pendingEventSets.Add(childEventSet); + recheck = true; } } - - //add child event sets to pending - foreach (EventSet childEventSet in eventSet.ChildSets) - { - pendingEventSets.Add(childEventSet); - } - } + } while (recheck); } foreach (Event ev in activeEvents) @@ -568,7 +597,7 @@ namespace Barotrauma { //enemy outside and targeting the sub or something in it //moloch adds 0.24 to enemy danger, a crawler 0.02 - enemyDanger += enemyAI.CombatStrength / 5000.0f; + enemyDanger += enemyAI.CombatStrength / 2000.0f; } } enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 670acb09a..b93c047ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -86,6 +86,8 @@ namespace Barotrauma public readonly bool PerRuin; public readonly bool PerWreck; + public readonly bool OncePerOutpost; + public readonly Dictionary Commonness; //Pair.First: event prefab, Pair.Second: commonness @@ -134,6 +136,7 @@ namespace Barotrauma IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? false); PerRuin = element.GetAttributeBool("perruin", false); PerWreck = element.GetAttributeBool("perwreck", false); + OncePerOutpost = element.GetAttributeBool("perwreck", false); Commonness[""] = 1.0f; foreach (XElement subElement in element.Elements()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index de6116c75..2a1ae5069 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -131,11 +131,9 @@ namespace Barotrauma if (Submarine.MainSub != null && Submarine.MainSub.AtEndPosition) { int deliveredItemCount = items.Count(i => i.CurrentHull != null && !i.Removed && i.Condition > 0.0f); - if (deliveredItemCount >= requiredDeliveryAmount) { GiveReward(); - completed = true; } } @@ -145,6 +143,7 @@ namespace Barotrauma if (!item.Removed) { item.Remove(); } } items.Clear(); + failed = !completed; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 30c815d2d..2901498fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -10,7 +10,7 @@ namespace Barotrauma abstract partial class Mission { public readonly MissionPrefab Prefab; - protected bool completed; + protected bool completed, failed; protected int state; public int State { @@ -74,7 +74,12 @@ namespace Barotrauma get { return completed; } set { completed = value; } } - + + public bool Failed + { + get { return failed; } + } + public virtual bool AllowRespawn { get { return true; } @@ -219,6 +224,14 @@ namespace Barotrauma if (faction != null) { faction.Reputation.Value += reputationReward.Value; } } } + + if (Prefab.DataRewards != null) + { + foreach (var (identifier, value, operation) in Prefab.DataRewards) + { + SetDataAction.PerformOperation(campaign.CampaignMetadata, identifier, value, operation); + } + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index d643a14ea..f9d59ba4d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -54,7 +54,8 @@ namespace Barotrauma public readonly string AchievementIdentifier; - public readonly Dictionary ReputationRewards = new Dictionary(); + public readonly Dictionary ReputationRewards = new Dictionary(); + public readonly List> DataRewards = new List>(); public readonly int Commonness; @@ -178,6 +179,23 @@ namespace Barotrauma } } break; + case "metadata": + string identifier = subElement.GetAttributeString("identifier", string.Empty); + string stringValue = subElement.GetAttributeString("value", string.Empty); + if (!string.IsNullOrWhiteSpace(stringValue) && !string.IsNullOrWhiteSpace(identifier)) + { + object value = SetDataAction.ConvertXMLValue(stringValue); + SetDataAction.OperationType operation = SetDataAction.OperationType.Set; + + string operatingString = subElement.GetAttributeString("operation", string.Empty); + if (!string.IsNullOrWhiteSpace(operatingString)) + { + operation = (SetDataAction.OperationType) Enum.Parse(typeof(SetDataAction.OperationType), operatingString); + } + + DataRewards.Add(Tuple.Create(identifier, value, operation)); + } + break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 310fa7f9d..c8e802ad1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -37,7 +37,7 @@ namespace Barotrauma } else { - yield return item.WorldPosition; + yield return item.GetRootInventoryOwner()?.WorldPosition ?? item.WorldPosition; } } } @@ -241,7 +241,8 @@ namespace Barotrauma public override void End() { - if (item.CurrentHull?.Submarine == null || (!item.CurrentHull.Submarine.AtEndPosition && !item.CurrentHull.Submarine.AtStartPosition) || item.Removed) + var root = item.GetRootContainer() ?? item; + if (root.CurrentHull?.Submarine == null || (!root.CurrentHull.Submarine.AtEndPosition && !root.CurrentHull.Submarine.AtStartPosition) || item.Removed) { return; } @@ -250,6 +251,7 @@ namespace Barotrauma item = null; GiveReward(); completed = true; + failed = !completed && state > 0; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 5e131970e..79c965621 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -202,16 +202,40 @@ namespace Barotrauma foreach (var position in availablePositions) { Vector2 pos = position.Position.ToVector2(); - float dist = Vector2.DistanceSquared(pos, GetReferenceSub().WorldPosition); + Submarine refSub = GetReferenceSub(); + float dist = Vector2.DistanceSquared(pos, refSub.WorldPosition); foreach (Submarine sub in Submarine.Loaded) { if (sub.Info.Type != SubmarineType.Player) { continue; } + float minDistToSub = GetMinDistanceToSub(sub); - if (dist > minDistToSub * minDistToSub && dist < closestDist) + if (dist < minDistToSub * minDistToSub) { continue; } + + if (closestDist == float.PositiveInfinity) { closestDist = dist; chosenPosition = position; + continue; } + + //chosen position behind the sub -> override with anything that's closer or to the right + if (chosenPosition.Position.X < refSub.WorldPosition.X) + { + if (dist < closestDist || pos.X > refSub.WorldPosition.X) + { + closestDist = dist; + chosenPosition = position; + } + } + //chosen position ahead of the sub -> only override with a position that's also ahead + else if (chosenPosition.Position.X > refSub.WorldPosition.X) + { + if (dist < closestDist && pos.X > refSub.WorldPosition.X) + { + closestDist = dist; + chosenPosition = position; + } + } } } //only found a spawnpos that's very far from the sub, pick one that's closer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index ecc1c2d72..950f1f6a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -50,6 +50,27 @@ namespace Barotrauma.Extensions } } + public static T RandomElementByWeight(this IEnumerable source, Func weightSelector, Rand.RandSync randSync = Rand.RandSync.Unsynced) + { + float totalWeight = source.Sum(weightSelector); + + float itemWeightIndex = Rand.Range(0f, 1f, randSync) * totalWeight; + float currentWeightIndex = 0; + + foreach (T weightedItem in source) + { + float weight = weightSelector(weightedItem); + currentWeightIndex += weight; + + if (currentWeightIndex >= itemWeightIndex) + { + return weightedItem; + } + } + + return default; + } + /// /// Executes an action that modifies the collection on each element (such as removing items from the list). /// Creates a temporary list. diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalyticsManager.cs index 3975c1cd3..05096b9f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalyticsManager.cs @@ -63,16 +63,17 @@ namespace Barotrauma GameSettings.SendUserStatistics = false; return; } - - if (GameMain.Config?.SelectedContentPackages.Count > 0) + + var allPackages = GameMain.Config?.AllEnabledPackages.ToList(); + if (allPackages?.Count > 0) { StringBuilder sb = new StringBuilder("ContentPackage: "); int i = 0; - foreach (ContentPackage cp in GameMain.Config.SelectedContentPackages) + foreach (ContentPackage cp in allPackages) { string trimmedName = cp.Name.Replace(":", "").Replace(" ", ""); sb.Append(trimmedName.Substring(0, Math.Min(32, trimmedName.Length))); - if (i < GameMain.Config.SelectedContentPackages.Count - 1) { sb.Append(" "); } + if (i < allPackages.Count - 1) { sb.Append(" "); } } GameAnalytics.AddDesignEvent(sb.ToString()); } @@ -86,9 +87,9 @@ namespace Barotrauma if (!GameSettings.SendUserStatistics) { return; } if (sentEventIdentifiers.Contains(identifier)) { return; } - if (GameMain.SelectedPackages != null) + if (GameMain.Config.AllEnabledPackages != null) { - if (GameMain.VanillaContent == null || GameMain.SelectedPackages.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent)) + if (GameMain.VanillaContent == null || GameMain.Config.AllEnabledPackages.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent)) { message = "[MODDED] " + message; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 727acf3fb..8a30b0860 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -51,7 +51,7 @@ namespace Barotrauma private readonly CampaignMode campaign; - private Location location => campaign.Map.CurrentLocation; + private Location Location => campaign?.Map?.CurrentLocation; public Action OnItemsInBuyCrateChanged; public Action OnItemsInSellCrateChanged; @@ -120,7 +120,7 @@ namespace Barotrauma // Exchange money var itemValue = GetBuyValueAtCurrentLocation(item); campaign.Money -= itemValue; - campaign.Map.CurrentLocation.StoreCurrentBalance += itemValue; + Location.StoreCurrentBalance += itemValue; if (removeFromCrate) { @@ -136,11 +136,11 @@ namespace Barotrauma OnPurchasedItemsChanged?.Invoke(); } - public int GetBuyValueAtCurrentLocation(PurchasedItem item) => item?.ItemPrefab != null && campaign?.Map?.CurrentLocation != null ? - item.Quantity* campaign.Map.CurrentLocation.GetAdjustedItemBuyPrice(item.ItemPrefab) : 0; + public int GetBuyValueAtCurrentLocation(PurchasedItem item) => item?.ItemPrefab != null && Location != null ? + item.Quantity * Location.GetAdjustedItemBuyPrice(item.ItemPrefab) : 0; - public int GetSellValueAtCurrentLocation(ItemPrefab itemPrefab, int quantity = 1) => itemPrefab != null && campaign?.Map?.CurrentLocation != null ? - quantity * campaign.Map.CurrentLocation.GetAdjustedItemSellPrice(itemPrefab) : 0; + public int GetSellValueAtCurrentLocation(ItemPrefab itemPrefab, int quantity = 1) => itemPrefab != null && Location != null ? + quantity * Location.GetAdjustedItemSellPrice(itemPrefab) : 0; public void CreatePurchasedItems() { @@ -185,7 +185,7 @@ namespace Barotrauma float floorPos = cargoRoom.Rect.Y - cargoRoom.Rect.Height; Vector2 position = new Vector2( - Rand.Range(cargoRoom.Rect.X + 20, cargoRoom.Rect.Right - 20), + cargoRoom.Rect.Width > 40 ? Rand.Range(cargoRoom.Rect.X + 20, cargoRoom.Rect.Right - 20) : cargoRoom.Rect.Center.X, floorPos); //check where the actual floor structure is in case the bottom of the hull extends below it diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index a93d2e753..717ca3768 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -114,7 +114,7 @@ namespace Barotrauma } #if CLIENT AddCharacterToCrewList(character); - DisplayCharacterOrder(character, character.CurrentOrder, character.CurrentOrderOption); + AddCurrentOrderIcon(character, character.CurrentOrder, character.CurrentOrderOption); #endif } @@ -193,7 +193,8 @@ namespace Barotrauma #endif } - conversationTimer = Rand.Range(5.0f, 10.0f); + //longer delay in multiplayer to prevent the server from triggering NPC conversations while the players are still loading the round + conversationTimer = IsSinglePlayer ? Rand.Range(5.0f, 10.0f) : Rand.Range(45.0f, 60.0f); } public void FireCharacter(CharacterInfo characterInfo) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index b061ddc77..4864d1b08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -29,9 +29,17 @@ namespace Barotrauma public float Value { get => Math.Min(MaxReputation, Metadata.GetFloat(metaDataIdentifier, InitialReputation)); - set => Metadata.SetValue(metaDataIdentifier, Math.Clamp(value, MinReputation, MaxReputation)); + set + { + Metadata.SetValue(metaDataIdentifier, Math.Clamp(value, MinReputation, MaxReputation)); + OnReputationValueChanged?.Invoke(); + OnAnyReputationValueChanged?.Invoke(); + } } + public Action OnReputationValueChanged; + public static Action OnAnyReputationValueChanged; + public Reputation(CampaignMetadata metadata, string identifier, int minReputation, int maxReputation, int initialReputation) { System.Diagnostics.Debug.Assert(metadata != null); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index c7c0f5834..34568d0a3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -109,7 +109,7 @@ namespace Barotrauma { get { - if (Level.Loaded != null && !Level.Loaded.Generating && + if (Level.Loaded?.EndLocation != null && !Level.Loaded.Generating && Level.Loaded.Type == LevelData.LevelType.LocationConnection && GetAvailableTransition(out _, out _) == TransitionType.ProgressToNextEmptyLocation) { @@ -270,7 +270,9 @@ namespace Barotrauma { if (leavingSub.AtEndPosition) { - if (Map.EndLocation != null && map.SelectedLocation == Map.EndLocation) + if (Map.EndLocation != null && + map.SelectedLocation == Map.EndLocation && + Map.EndLocation.Connections.Any(c => c.LevelData == Level.Loaded.LevelData)) { nextLevel = map.StartLocation.LevelData; return TransitionType.End; @@ -463,8 +465,12 @@ namespace Barotrauma } } - foreach (CharacterInfo ci in CrewManager.CharacterInfos) + foreach (CharacterInfo ci in CrewManager.CharacterInfos.ToList()) { + if (ci.CauseOfDeath != null) + { + CrewManager.RemoveCharacterInfo(ci); + } ci?.ResetCurrentOrder(); } @@ -677,7 +683,7 @@ namespace Barotrauma public void OutpostNPCAttacked(Character npc, Character attacker, AttackResult attackResult) { - if (npc == null || attacker == null || npc.IsDead || npc.TurnedHostileByEvent) { return; } + if (npc == null || attacker == null || npc.IsDead || npc.IsInstigator) { return; } if (npc.TeamID != Character.TeamType.FriendlyNPC) { return; } if (!attacker.IsRemotePlayer && attacker != Character.Controlled) { return; } Location location = Map?.CurrentLocation; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs index c215ee74c..6faaf1b13 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs @@ -36,6 +36,8 @@ namespace Barotrauma get { return false; } } + public virtual void UpdateWhilePaused(float deltaTime) { } + public GameModePreset Preset { get { return preset; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 9c714a581..b6f891ac0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -178,11 +178,18 @@ namespace Barotrauma characterData.Clear(); string characterDataPath = GetCharacterDataSavePath(); - var characterDataDoc = XMLExtensions.TryLoadXml(characterDataPath); - if (characterDataDoc?.Root == null) return; - foreach (XElement subElement in characterDataDoc.Root.Elements()) + if (!File.Exists(characterDataPath)) { - characterData.Add(new CharacterCampaignData(subElement)); + DebugConsole.ThrowError($"Failed to load the character data for the campaign. Could not find the file \"{characterDataPath}\"."); + } + else + { + var characterDataDoc = XMLExtensions.TryLoadXml(characterDataPath); + if (characterDataDoc?.Root == null) { return; } + foreach (XElement subElement in characterDataDoc.Root.Elements()) + { + characterData.Add(new CharacterCampaignData(subElement)); + } } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index f0f92d516..bdac4f61e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -149,9 +149,9 @@ namespace Barotrauma GameMode = mpCampaign; if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { + mpCampaign.LoadNewLevel(); //save to ensure the campaign ID in the save file matches the one that got assigned to this campaign instance SaveUtil.SaveGame(saveFile); - mpCampaign.LoadNewLevel(); } break; } @@ -350,6 +350,7 @@ namespace Barotrauma } #endif } + private void InitializeLevel(Level level) { //make sure no status effects have been carried on from the next round @@ -436,6 +437,9 @@ namespace Barotrauma Submarine.MainSub.SetPosition(Vector2.Zero); return; } + + var originalSubPos = Submarine.WorldPosition; + if (level.StartOutpost != null) { //start by placing the sub below the outpost @@ -504,6 +508,18 @@ namespace Barotrauma Submarine.NeutralizeBallast(); Submarine.EnableMaintainPosition(); } + + // Make sure that linked subs which are NOT docked to the main sub + // (but still close enough to NOT be considered as 'left behind') + // are also moved to keep their relative position to the main sub + var linkedSubs = MapEntity.mapEntityList.FindAll(me => me is LinkedSubmarine); + foreach (LinkedSubmarine ls in linkedSubs) + { + if (ls.Sub == null || ls.Submarine != Submarine) { continue; } + if (!ls.LoadSub || ls.Sub.DockedTo.Contains(Submarine)) { continue; } + if (Submarine.Info.LeftBehindDockingPortIDs.Contains(ls.OriginalLinkedToID)) { continue; } + ls.Sub.SetPosition(ls.Sub.WorldPosition + (Submarine.WorldPosition - originalSubPos)); + } } public void Update(float deltaTime) @@ -562,7 +578,7 @@ namespace Barotrauma #endif } - public static bool IsCompatibleWithSelectedContentPackages(IList contentPackagePaths, out string errorMsg) + public static bool IsCompatibleWithEnabledContentPackages(IList contentPackagePaths, out string errorMsg) { errorMsg = ""; //no known content packages, must be an older save file @@ -571,13 +587,13 @@ namespace Barotrauma List missingPackages = new List(); foreach (string packagePath in contentPackagePaths) { - if (!GameMain.Config.SelectedContentPackages.Any(cp => cp.Path == packagePath)) + if (!GameMain.Config.AllEnabledPackages.Any(cp => cp.Path == packagePath)) { missingPackages.Add(packagePath); } } List excessPackages = new List(); - foreach (ContentPackage cp in GameMain.Config.SelectedContentPackages) + foreach (ContentPackage cp in GameMain.Config.AllEnabledPackages) { if (!cp.HasMultiplayerIncompatibleContent) { continue; } if (!contentPackagePaths.Any(p => p == cp.Path)) @@ -589,10 +605,10 @@ namespace Barotrauma bool orderMismatch = false; if (missingPackages.Count == 0 && missingPackages.Count == 0) { - var selectedPackages = GameMain.Config.SelectedContentPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList(); - for (int i = 0; i < contentPackagePaths.Count && i < selectedPackages.Count; i++) + var enabledPackages = GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList(); + for (int i = 0; i < contentPackagePaths.Count && i < enabledPackages.Count; i++) { - if (contentPackagePaths[i] != selectedPackages[i].Path) + if (contentPackagePaths[i] != enabledPackages[i].Path) { orderMismatch = true; break; @@ -653,7 +669,7 @@ namespace Barotrauma } doc.Root.Add(new XAttribute("mapseed", Map.Seed)); doc.Root.Add(new XAttribute("selectedcontentpackages", - string.Join("|", GameMain.Config.SelectedContentPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).Select(cp => cp.Path)))); + string.Join("|", GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).Select(cp => cp.Path)))); ((CampaignMode)GameMode).Save(doc.Root); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index fae4f91c1..49611be15 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -627,6 +627,7 @@ namespace Barotrauma pendingUpgrades.Add(new PurchasedUpgrade(prefab, category, level)); } +#if CLIENT if (isSingleplayer) { SetPendingUpgrades(pendingUpgrades); @@ -635,6 +636,9 @@ namespace Barotrauma { loadedUpgrades = pendingUpgrades; } +#else + SetPendingUpgrades(pendingUpgrades); +#endif } public static void LogError(string text, Dictionary data, Exception e = null) @@ -669,54 +673,6 @@ namespace Barotrauma return values; } - /// - /// Verifies that the client and the server are agreeing on the upgrade levels, if not something has gone wrong. - /// - /// - /// - public static void CompareUpgrades(Dictionary clientUpgrades, Dictionary serverUpgrades) - { - int mismatches = 0; - DebugLog("Comparing client upgrades to server upgrades...", Color.Orange); - foreach (var (key, value) in clientUpgrades) - { - if (!serverUpgrades.ContainsKey(key)) - { - DebugLog($"Client has an upgrade the server doesn't! {key} lvl. {value}.", Color.Red); - mismatches++; - continue; - } - - if (value != serverUpgrades[key]) - { - DebugLog($"Client's upgrade level doesn't match the server's! Client: {key} {value}, Server: {key} {serverUpgrades[key]}.", Color.Red); - mismatches++; - } - } - - DebugLog("...comparing server upgrades to client upgrades...", Color.Orange); - foreach (var (key, value) in serverUpgrades) - { - if (!clientUpgrades.ContainsKey(key)) - { - DebugLog($"Server has an upgrade the client doesn't! {key} lvl. {value}.", Color.Red); - mismatches++; - } - } - - if (mismatches == 0) - { - DebugLog("Everything ok!"); - } - else - { - DebugLog($"{mismatches} mismatches found! This means that the client and the server are disagreeing on upgrade levels and might cause desync.\n", Color.Red); -#if CLIENT - DebugConsole.IsOpen = true; -#endif - } - } - /// /// Used to sync the pending upgrades list in multiplayer. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs index ab993a425..6ce10a7f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs @@ -251,7 +251,24 @@ namespace Barotrauma set { TextManager.Language = value; } } - public readonly List SelectedContentPackages = new List(); + public ContentPackage CurrentCorePackage { get; private set; } + private readonly List enabledRegularPackages = new List(); + public IReadOnlyList EnabledRegularPackages + { + get { return enabledRegularPackages; } + } + + public IEnumerable AllEnabledPackages + { + get + { + yield return CurrentCorePackage; + foreach (var package in EnabledRegularPackages) + { + yield return package; + } + } + } public bool ContentPackageSelectionDirtyNotification { @@ -267,6 +284,8 @@ namespace Barotrauma public volatile bool SuppressModFolderWatcher; + public volatile bool WaitingForAutoUpdate; + #if DEBUG public bool AutomaticQuickStartEnabled { get; set; } public bool AutomaticCampaignLoadEnabled { get; set; } @@ -275,7 +294,7 @@ namespace Barotrauma private System.IO.FileSystemWatcher modsFolderWatcher; - private int ContentFileLoadOrder(ContentFile a) + private static int ContentFileLoadOrder(ContentFile a) { switch (a.Type) { @@ -292,62 +311,185 @@ namespace Barotrauma public void SelectCorePackage(ContentPackage contentPackage, bool forceReloadAll = false) { + if (!contentPackage.IsCorePackage) { return; } if (!contentPackage.ContainsRequiredCorePackageFiles(out _)) { return; } - ContentPackage otherCorePackage = SelectedContentPackages.Where(cp => cp.CorePackage).First(); + ContentPackage prevCorePackage = CurrentCorePackage; - SelectedContentPackages.Remove(otherCorePackage); - SelectedContentPackages.Add(contentPackage); + CurrentCorePackage = contentPackage; - ContentPackage.SortContentPackages(); + if (prevCorePackage != null) + { + List filesToRemove = prevCorePackage.Files.Where(f1 => forceReloadAll || + !contentPackage.Files.Any(f2 => + Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList(); - List filesToRemove = otherCorePackage.Files.Where(f1 => forceReloadAll || - !contentPackage.Files.Any(f2 => - Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList(); + List filesToAdd = contentPackage.Files.Where(f1 => forceReloadAll || + !prevCorePackage.Files.Any(f2 => + Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList(); - List filesToAdd = contentPackage.Files.Where(f1 => forceReloadAll || - !otherCorePackage.Files.Any(f2 => - Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList(); + DisableContentPackageItems(filesToRemove); + EnableContentPackageItems(filesToAdd); - DisableContentPackageItems(filesToRemove.OrderBy(ContentFileLoadOrder)); - EnableContentPackageItems(filesToAdd.OrderBy(ContentFileLoadOrder)); - - RefreshContentPackageItems(filesToAdd.Concat(filesToRemove)); + RefreshContentPackageItems(filesToAdd.Concat(filesToRemove)); + } + else + { + EnableContentPackageItems(contentPackage.Files); + RefreshContentPackageItems(contentPackage.Files); + } } public void AutoSelectCorePackage(IEnumerable toRemove) { - SelectCorePackage(ContentPackage.List.Find(cpp => - cpp.CorePackage && - !toRemove.Contains(cpp) && + SelectCorePackage(ContentPackage.CorePackages.Find(cpp => + (toRemove == null || !toRemove.Contains(cpp)) && cpp.ContainsRequiredCorePackageFiles(out _))); } - public void SelectContentPackage(ContentPackage contentPackage) - { - if (!SelectedContentPackages.Contains(contentPackage)) - { - SelectedContentPackages.Add(contentPackage); - ContentPackage.SortContentPackages(); + private List> backupModOrder; - EnableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder)); + public void SwapPackages(ContentPackage corePackage, List regularPackages) + { + backupModOrder = new List>(); + backupModOrder.Add(new Tuple(CurrentCorePackage, true)); + for (int i=0;i(p, EnabledRegularPackages.Contains(p))); + } + + List packagesToDisable = new List(); + packagesToDisable.Add(CurrentCorePackage); + packagesToDisable.AddRange(EnabledRegularPackages.Where(p => p.HasMultiplayerIncompatibleContent)); + List packagesToEnable = new List(); + packagesToEnable.Add(corePackage); + packagesToEnable.AddRange(regularPackages); + + IEnumerable filesOfDisabledPkgs = packagesToDisable.SelectMany(p => p.Files); + IEnumerable filesOfEnabledPkgs = packagesToEnable.SelectMany(p => p.Files); + + List filesToDisable = filesOfDisabledPkgs.Where(f1 => + !filesOfEnabledPkgs.Any(f2 => + Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList(); + + List filesToEnable = filesOfEnabledPkgs.Where(f1 => + !filesOfDisabledPkgs.Any(f2 => + Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList(); + + CurrentCorePackage = corePackage; + enabledRegularPackages.RemoveAll(p => p.HasMultiplayerIncompatibleContent); enabledRegularPackages.AddRange(regularPackages); + + DisableContentPackageItems(filesToDisable); + EnableContentPackageItems(filesToEnable); + + RefreshContentPackageItems(filesOfEnabledPkgs.Concat(filesToDisable)); + + ContentPackage.SortContentPackages(p => -regularPackages.IndexOf(p)); + } + + public void RestoreBackupPackages() + { + if (backupModOrder == null) { return; } + + SwapPackages( + backupModOrder[0].Item1, + backupModOrder.Skip(1).Where(p => p.Item2).Select(p => p.Item1).ToList()); + ContentPackage.SortContentPackages(p => backupModOrder.FindIndex(n => n.Item1 == p)); + + backupModOrder = null; + } + + public void EnableRegularPackage(ContentPackage contentPackage) + { + if (contentPackage.IsCorePackage) { return; } + if (!enabledRegularPackages.Contains(contentPackage)) + { + enabledRegularPackages.Add(contentPackage); + SortContentPackages(); + + EnableContentPackageItems(contentPackage.Files); RefreshContentPackageItems(contentPackage.Files); } } - public void DeselectContentPackage(ContentPackage contentPackage) + public void DisableRegularPackage(ContentPackage contentPackage) { - if (SelectedContentPackages.Contains(contentPackage)) + if (contentPackage.IsCorePackage) { return; } + if (enabledRegularPackages.Contains(contentPackage)) { - SelectedContentPackages.Remove(contentPackage); - ContentPackage.SortContentPackages(); - DisableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder)); + enabledRegularPackages.Remove(contentPackage); + SortContentPackages(); + + DisableContentPackageItems(contentPackage.Files); RefreshContentPackageItems(contentPackage.Files); } } - private void EnableContentPackageItems(IOrderedEnumerable files) + public void SortContentPackages(bool refreshAll = false) { + for (int i = enabledRegularPackages.Count - 1; i >= 0; i--) + { + var package = enabledRegularPackages[i]; + if (!ContentPackage.RegularPackages.Contains(package)) + { + ContentPackage replacement = ContentPackage.RegularPackages.Find(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase)); + if (replacement != null) + { + enabledRegularPackages[i] = replacement; + } + else + { + DisableRegularPackage(package); + } + } + } + + if (CurrentCorePackage == null) + { + AutoSelectCorePackage(null); + } + else if (!ContentPackage.CorePackages.Contains(CurrentCorePackage)) + { + ContentPackage replacement = ContentPackage.CorePackages.Find(p => p.Name.Equals(CurrentCorePackage.Name, StringComparison.OrdinalIgnoreCase)); + if (replacement != null) + { + SelectCorePackage(replacement); + } + else + { + AutoSelectCorePackage(null); + } + } + + var sortedSelected = enabledRegularPackages + .OrderBy(p => -ContentPackage.RegularPackages.IndexOf(p)) + .ToList(); + enabledRegularPackages.Clear(); enabledRegularPackages.AddRange(sortedSelected); + + CharacterPrefab.Prefabs.SortAll(); + AfflictionPrefab.Prefabs.SortAll(); + JobPrefab.Prefabs.SortAll(); + ItemPrefab.Prefabs.SortAll(); + CoreEntityPrefab.Prefabs.SortAll(); + ItemAssemblyPrefab.Prefabs.SortAll(); + StructurePrefab.Prefabs.SortAll(); + +#if CLIENT + GameMain.DecalManager?.Prefabs.SortAll(); + GameMain.ParticleManager?.Prefabs.SortAll(); +#endif + + if (refreshAll) + { + RefreshContentPackageItems(AllEnabledPackages.SelectMany(p => p.Files)); + } + } + + public void EnableContentPackageItems(IEnumerable unorderedFiles) + { + if (WaitingForAutoUpdate) { return; } + IOrderedEnumerable files = unorderedFiles.OrderBy(ContentFileLoadOrder); foreach (ContentFile file in files) { switch (file.Type) @@ -390,8 +532,10 @@ namespace Barotrauma } } - private void DisableContentPackageItems(IOrderedEnumerable files) + public void DisableContentPackageItems(IEnumerable unorderedFiles) { + if (WaitingForAutoUpdate) { return; } + IOrderedEnumerable files = unorderedFiles.OrderBy(ContentFileLoadOrder); foreach (ContentFile file in files) { switch (file.Type) @@ -434,9 +578,9 @@ namespace Barotrauma } } - private void RefreshContentPackageItems(IEnumerable files) + public void RefreshContentPackageItems(IEnumerable files) { - if (files.Any(f => f.Type == ContentType.LocationTypes)) { LocationType.Init(); } + if (WaitingForAutoUpdate) { return; } if (files.Any(f => f.Type == ContentType.Afflictions)) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); } if (files.Any(f => f.Type == ContentType.Submarine || f.Type == ContentType.Outpost || @@ -447,7 +591,13 @@ namespace Barotrauma if (files.Any(f => f.Type == ContentType.Factions)) { FactionPrefab.LoadFactions(); } if (files.Any(f => f.Type == ContentType.Item)) { ItemPrefab.InitFabricationRecipes(); } if (files.Any(f => f.Type == ContentType.RuinConfig)) { RuinGeneration.RuinGenerationParams.ClearAll(); } - if (files.Any(f => f.Type == ContentType.RandomEvents)) { EventSet.LoadPrefabs(); } + if (files.Any(f => f.Type == ContentType.RandomEvents || + f.Type == ContentType.LocationTypes)) + { + LocationType.List.Clear(); + EventSet.LoadPrefabs(); + LocationType.Init(); + } if (files.Any(f => f.Type == ContentType.Missions)) { MissionPrefab.Init(); } if (files.Any(f => f.Type == ContentType.LevelObjectPrefabs)) { LevelObjectPrefab.LoadAll(); } if (files.Any(f => f.Type == ContentType.MapGenerationParameters)) { MapGenerationParams.Init(); } @@ -515,49 +665,6 @@ namespace Barotrauma } } - public void ReorderSelectedContentPackages(Func orderFunction) - { - ContentPackage.List = ContentPackage.List - .OrderByDescending(p => p.CorePackage) - .ThenBy(orderFunction) - .ToList(); - - ContentPackage.SortContentPackages(); - - CharacterPrefab.Prefabs.SortAll(); - AfflictionPrefab.Prefabs.SortAll(); - JobPrefab.Prefabs.SortAll(); - ItemPrefab.Prefabs.SortAll(); - CoreEntityPrefab.Prefabs.SortAll(); - ItemAssemblyPrefab.Prefabs.SortAll(); - StructurePrefab.Prefabs.SortAll(); - - SubmarineInfo.RefreshSavedSubs(); - ItemPrefab.InitFabricationRecipes(); - RuinGeneration.RuinGenerationParams.ClearAll(); - EventSet.LoadPrefabs(); - MissionPrefab.Init(); - LevelObjectPrefab.LoadAll(); - LocationType.Init(); - MapGenerationParams.Init(); - LevelGenerationParams.LoadPresets(); - OutpostGenerationParams.LoadPresets(); - TraitorMissionPrefab.Init(); - Order.Init(); - EventManagerSettings.Init(); - WreckAIConfig.LoadAll(); - SkillSettings.Load(GameMain.Instance.GetFilesOfType(ContentType.SkillSettings)); - -#if CLIENT - GameMain.DecalManager.Prefabs.SortAll(); - GameMain.ParticleManager.Prefabs.SortAll(); - SoundPlayer.Init().ForEach(_ => { return; }); -#endif - } - - - private HashSet selectedContentPackagePaths = new HashSet(); - public string MasterServerUrl { get; set; } public string RemoteContentUrl { get; set; } public bool AutoCheckUpdates { get; set; } @@ -621,6 +728,7 @@ namespace Barotrauma private bool showTutorialSkipWarning = true; public static bool EnableSubmarineAutoSave { get; set; } + public static int MaximumAutoSaves { get; set; } public static Color SubEditorBackgroundColor { get; set; } public bool ShowTutorialSkipWarning @@ -662,54 +770,64 @@ namespace Barotrauma case System.IO.WatcherChangeTypes.Created: { string cpPath = Path.GetFullPath(Path.Combine(e.FullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath(); - if (File.Exists(cpPath) && !ContentPackage.List.Any(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath)) + if (File.Exists(cpPath) && + !ContentPackage.AllPackages.Any(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath)) { - var cp = new ContentPackage(cpPath); - ContentPackage.List.Add(cp); + ContentPackage.AddPackage(new ContentPackage(cpPath)); } } break; case System.IO.WatcherChangeTypes.Deleted: { string cpPath = Path.GetFullPath(Path.Combine(e.FullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath(); - var toRemove = ContentPackage.List.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath).ToList(); - var packagesToDeselect = GameMain.Config.SelectedContentPackages.Where(p => toRemove.Contains(p)).ToList(); - foreach (var cp in packagesToDeselect) - { - if (cp.CorePackage) - { - GameMain.Config.AutoSelectCorePackage(toRemove); - } - else - { - GameMain.Config.DeselectContentPackage(cp); - } - } - + var toRemove = ContentPackage.RegularPackages.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath).ToList(); foreach (var cp in toRemove) { - ContentPackage.List.Remove(cp); + if (enabledRegularPackages.Contains(cp)) { DisableRegularPackage(cp); } } + + toRemove.AddRange(ContentPackage.CorePackages.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath)); + bool reselectCore = false; + foreach (var cp in toRemove) + { + ContentPackage.RemovePackage(cp); + if (cp.IsCorePackage) + { + reselectCore = true; + } + } + if (reselectCore) { AutoSelectCorePackage(null); } } break; case System.IO.WatcherChangeTypes.Renamed: { System.IO.RenamedEventArgs renameArgs = e as System.IO.RenamedEventArgs; - string cpPath = Path.GetFullPath(Path.Combine(renameArgs.OldFullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath(); - var toRemove = ContentPackage.List.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath).ToList(); + string cpPath = Path.GetFullPath(Path.Combine(e.FullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath(); + var toRemove = ContentPackage.RegularPackages.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath).ToList(); foreach (var cp in toRemove) { - GameMain.Config.DeselectContentPackage(cp); - ContentPackage.List.Remove(cp); + if (enabledRegularPackages.Contains(cp)) { DisableRegularPackage(cp); } + } + + toRemove.AddRange(ContentPackage.CorePackages.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath)); + bool reselectCore = false; + foreach (var cp in toRemove) + { + ContentPackage.RemovePackage(cp); + if (cp.IsCorePackage) + { + reselectCore = true; + } } cpPath = Path.GetFullPath(Path.Combine(renameArgs.FullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath(); - if (File.Exists(cpPath) && !ContentPackage.List.Any(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath)) + if (File.Exists(cpPath) && + !ContentPackage.AllPackages.Any(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath)) { - var cp = new ContentPackage(cpPath); - ContentPackage.List.Add(cp); + ContentPackage.AddPackage(new ContentPackage(cpPath)); } + if (reselectCore) { AutoSelectCorePackage(null); } } break; } @@ -723,7 +841,7 @@ namespace Barotrauma GraphicsWidth = 1024; GraphicsHeight = 768; MasterServerUrl = ""; - SelectContentPackage(ContentPackage.List.Any() ? ContentPackage.List[0] : new ContentPackage("")); + SelectCorePackage(ContentPackage.CorePackages.FirstOrDefault()); jobPreferences = new List>(); return; } @@ -778,6 +896,7 @@ namespace Barotrauma new XAttribute("verboselogging", VerboseLogging), new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs), new XAttribute("submarineautosave", EnableSubmarineAutoSave), + new XAttribute("maxautosaves", MaximumAutoSaves), new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)), new XAttribute("enablesplashscreen", EnableSplashScreen), new XAttribute("usesteammatchmaking", UseSteamMatchmaking), @@ -831,11 +950,11 @@ namespace Barotrauma new XAttribute("hudscale", HUDScale), new XAttribute("inventoryscale", InventoryScale)); - foreach (ContentPackage contentPackage in SelectedContentPackages) + foreach (ContentPackage contentPackage in ContentPackage.CorePackages) { if (contentPackage.Path.Contains(VanillaContentPackagePath)) { - doc.Root.Add(new XElement("contentpackage", new XAttribute("path", contentPackage.Path))); + doc.Root.Add(new XElement("contentpackages", new XElement("core", new XAttribute("name", contentPackage.Name)))); break; } } @@ -978,112 +1097,6 @@ namespace Barotrauma return true; } - public void ReloadContentPackages() - { - LoadContentPackages(selectedContentPackagePaths); - } - - private void LoadContentPackages(IEnumerable contentPackagePaths) - { - var missingPackagePaths = new List(); - var incompatiblePackages = new List(); - var packagesWithErrors = new List(); - SelectedContentPackages.Clear(); - foreach (string path in contentPackagePaths) - { - var matchingContentPackage = ContentPackage.List.Find(cp => Barotrauma.IO.Path.GetFullPath(cp.Path).CleanUpPath() == path.CleanUpPath()); - - if (matchingContentPackage == null) - { - missingPackagePaths.Add(path); - } - else if (!matchingContentPackage.IsCompatible()) - { - DebugConsole.NewMessage( - $"Content package \"{matchingContentPackage.Name}\" is not compatible with this version of Barotrauma (game version: {GameMain.Version}, content package version: {matchingContentPackage.GameVersion})", - Color.Red); - incompatiblePackages.Add(matchingContentPackage); - } - else - { - if (!matchingContentPackage.CheckErrors(out List errorMessages)) - { - DebugConsole.NewMessage( - $"Errors found in content package \"{matchingContentPackage.Name}\": " + string.Join(", ", errorMessages), - Color.Red); - packagesWithErrors.Add(matchingContentPackage); - } - //add content packages with errors as they are generally able to load most of their assets - SelectedContentPackages.Add(matchingContentPackage); - } - } - - EnsureCoreContentPackageSelected(gameLoaded: false); - - ContentPackage.SortContentPackages(); - TextManager.LoadTextPacks(SelectedContentPackages); - - foreach (ContentPackage contentPackage in SelectedContentPackages) - { - foreach (ContentFile file in contentPackage.Files) - { - ToolBox.IsProperFilenameCase(file.Path); - } - } - - //save to get rid of the invalid selected packages in the config file - if (missingPackagePaths.Count > 0 || incompatiblePackages.Count > 0 || packagesWithErrors.Count > 0) { SaveNewPlayerConfig(); } - - //display error messages after all content packages have been loaded - //to make sure the package that contains text files has been loaded before we attempt to use TextManager - foreach (string missingPackagePath in missingPackagePaths) - { - DebugConsole.ThrowError(TextManager.GetWithVariable("ContentPackageNotFound", "[packagepath]", missingPackagePath)); - } - foreach (ContentPackage invalidPackage in packagesWithErrors) - { - DebugConsole.ThrowError(TextManager.GetWithVariable("ContentPackageHasErrors", "[packagename]", invalidPackage.Name), createMessageBox: true); - } - foreach (ContentPackage incompatiblePackage in incompatiblePackages) - { - DebugConsole.ThrowError(TextManager.GetWithVariables(incompatiblePackage.GameVersion <= new Version(0, 0, 0, 0) ? "IncompatibleContentPackageUnknownVersion" : "IncompatibleContentPackage", - new string[3] { "[packagename]", "[packageversion]", "[gameversion]" }, new string[3] { incompatiblePackage.Name, incompatiblePackage.GameVersion.ToString(), GameMain.Version.ToString() }), - createMessageBox: true); - } - } - - public void EnsureCoreContentPackageSelected(bool gameLoaded=true) - { - if (SelectedContentPackages.Any(cp => cp.CorePackage)) { return; } - - if (GameMain.VanillaContent != null) - { - if (gameLoaded) - { - SelectContentPackage(GameMain.VanillaContent); - } - else - { - SelectedContentPackages.Add(GameMain.VanillaContent); - } - } - else - { - var availablePackage = ContentPackage.List.FirstOrDefault(cp => cp.IsCompatible() && cp.CorePackage); - if (availablePackage != null) - { - if (gameLoaded) - { - SelectContentPackage(availablePackage); - } - else - { - SelectedContentPackages.Add(availablePackage); - } - } - } - } - #endregion #region Save PlayerConfig @@ -1106,6 +1119,7 @@ namespace Barotrauma new XAttribute("verboselogging", VerboseLogging), new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs), new XAttribute("submarineautosave", EnableSubmarineAutoSave), + new XAttribute("maxautosaves", MaximumAutoSaves), new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)), new XAttribute("enablesplashscreen", EnableSplashScreen), new XAttribute("usesteammatchmaking", UseSteamMatchmaking), @@ -1202,11 +1216,32 @@ namespace Barotrauma new XAttribute("hudscale", HUDScale), new XAttribute("inventoryscale", InventoryScale)); - foreach (ContentPackage contentPackage in SelectedContentPackages) + XElement contentPackagesElement = new XElement("contentpackages"); + + string corePackageName = (CurrentCorePackage ?? ContentPackage.CorePackages.FirstOrDefault()).Name; + contentPackagesElement.Add(new XElement("core", new XAttribute("name", corePackageName))); + + XElement regularPackagesElement = new XElement("regular"); + foreach (ContentPackage package in ContentPackage.RegularPackages) { - doc.Root.Add(new XElement("contentpackage", - new XAttribute("path", contentPackage.Path))); + XElement packageElement = new XElement("package", new XAttribute("name", package.Name)); + if (EnabledRegularPackages.Contains(package)) { packageElement.Add(new XAttribute("enabled", "true")); } + regularPackagesElement.Add(packageElement); } + contentPackagesElement.Add(regularPackagesElement); + + doc.Root.Add(contentPackagesElement); + +#if UNSTABLE + //TODO: remove at some point + foreach (ContentPackage package in AllEnabledPackages) + { + XElement compatibilityElement = new XElement("contentpackage"); + compatibilityElement.Add(new XAttribute("path", package.Path)); + + doc.Root.Add(compatibilityElement); + } +#endif #if CLIENT var keyMappingElement = new XElement("keymapping"); @@ -1318,6 +1353,7 @@ namespace Barotrauma sendUserStatistics = doc.Root.GetAttributeBool("senduserstatistics", sendUserStatistics); QuickStartSubmarineName = doc.Root.GetAttributeString("quickstartsub", QuickStartSubmarineName); EnableSubmarineAutoSave = doc.Root.GetAttributeBool("submarineautosave", true); + MaximumAutoSaves = doc.Root.GetAttributeInt("maxautosaves", 8); SubEditorBackgroundColor = doc.Root.GetAttributeColor("subeditorbackground", new Color(0.051f, 0.149f, 0.271f, 1.0f)); UseSteamMatchmaking = doc.Root.GetAttributeBool("usesteammatchmaking", UseSteamMatchmaking); RequireSteamAuthentication = doc.Root.GetAttributeBool("requiresteamauthentication", RequireSteamAuthentication); @@ -1441,18 +1477,79 @@ namespace Barotrauma private void LoadContentPackages(XDocument doc) { - selectedContentPackagePaths = new HashSet(); - foreach (XElement subElement in doc.Root.Elements()) + CurrentCorePackage = null; + enabledRegularPackages.Clear(); + + var contentPackagesElement = doc.Root.Element("contentpackages"); + if (contentPackagesElement != null) { - switch (subElement.Name.ToString().ToLowerInvariant()) + string coreName = contentPackagesElement.Element("core")?.GetAttributeString("name", ""); + ContentPackage corePackage = ContentPackage.CorePackages.Find(p => p.Name.Equals(coreName, StringComparison.OrdinalIgnoreCase)); + if (corePackage != null) { - case "contentpackage": - string path = Path.GetFullPath(subElement.GetAttributeString("path", "")); - selectedContentPackagePaths.Add(path); - break; + CurrentCorePackage = corePackage; + } + + XElement regularElement = contentPackagesElement.Element("regular"); + + List subElements = regularElement?.Elements()?.ToList(); + if (subElements != null) + { + ContentPackage.SortContentPackages(p => + { + int index = subElements.FindIndex(e => + { + string name = e.GetAttributeString("name", null); + return p.Name.Equals(name, StringComparison.OrdinalIgnoreCase); + }); + return index; + }); + + foreach (var subElement in subElements) + { + if (!bool.TryParse(subElement.GetAttributeString("enabled", "false"), out bool enabled) || !enabled) { continue; } + + string name = subElement.GetAttributeString("name", null); + if (string.IsNullOrEmpty(name)) { continue; } + + var package = ContentPackage.RegularPackages.Find(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + if (package == null) { continue; } + enabledRegularPackages.Add(package); + } } } - LoadContentPackages(selectedContentPackagePaths); + else + { + var enabledContentPackagePaths = new List(); + foreach (XElement subElement in doc.Root.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "contentpackage": + string path = subElement.GetAttributeString("path", ""); + enabledContentPackagePaths.Add(path.CleanUpPath().ToLowerInvariant()); + break; + } + } + + ContentPackage.SortContentPackages(p => enabledContentPackagePaths.IndexOf(p.Path.CleanUpPath().ToLowerInvariant())); + + foreach (string path in enabledContentPackagePaths) + { + ContentPackage package = ContentPackage.AllPackages + .FirstOrDefault(p => p.Path.CleanUpPath().Equals(path, StringComparison.OrdinalIgnoreCase)); + if (package == null) { continue; } + if (package.IsCorePackage) { CurrentCorePackage = package; } + else { enabledRegularPackages.Add(package); } + } + } + + if (CurrentCorePackage == null) + { + CurrentCorePackage = ContentPackage.CorePackages.First(); + } + + TextManager.LoadTextPacks(AllEnabledPackages); } #endregion diff --git a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs index b3ae7c280..09fab09a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs @@ -18,5 +18,10 @@ namespace Barotrauma Shoot, Command, ToggleInventory +#if DEBUG + , + NextFireMode, + PreviousFireMode +#endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index a7e44e200..afe84d033 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -209,15 +209,17 @@ namespace Barotrauma.Items.Components #if SERVER if (GameMain.Server != null && (!item.Submarine?.Loading ?? true)) { + originalDockingTargetID = DockingTarget.item.ID; item.CreateServerEvent(this); } #endif } + public void Lock(bool isNetworkMessage, bool forcePosition = false) { #if CLIENT - if (GameMain.Client != null && !isNetworkMessage) return; + if (GameMain.Client != null && !isNetworkMessage) { return; } #endif if (DockingTarget == null) @@ -251,6 +253,7 @@ namespace Barotrauma.Items.Components #if SERVER if (GameMain.Server != null && (!item.Submarine?.Loading ?? true)) { + originalDockingTargetID = DockingTarget.item.ID; item.CreateServerEvent(this); } #else @@ -332,20 +335,45 @@ namespace Barotrauma.Items.Components { if (DockingDir != 0) { return DockingDir; } - if (Door != null) + if (Door != null && Door.LinkedGap.linkedTo.Count > 0) { - if (Door.LinkedGap.linkedTo.Count == 1) + Hull refHull = null; + float largestHullSize = 0.0f; + foreach (MapEntity linked in Door.LinkedGap.linkedTo) + { + if (!(linked is Hull hull)) { continue; } + if (hull.Volume > largestHullSize) + { + refHull = hull; + largestHullSize = hull.Volume; + } + } + if (refHull != null) { return IsHorizontal ? - Math.Sign(Door.Item.WorldPosition.X - Door.LinkedGap.linkedTo[0].WorldPosition.X) : - Math.Sign(Door.Item.WorldPosition.Y - Door.LinkedGap.linkedTo[0].WorldPosition.Y); + Math.Sign(Door.Item.WorldPosition.X - refHull.WorldPosition.X) : + Math.Sign(Door.Item.WorldPosition.Y - refHull.WorldPosition.Y); } - else if (dockingTarget?.Door?.LinkedGap != null && dockingTarget.Door.LinkedGap.linkedTo.Count == 1) + } + if (dockingTarget?.Door?.LinkedGap != null && dockingTarget.Door.LinkedGap.linkedTo.Count > 0) + { + Hull refHull = null; + float largestHullSize = 0.0f; + foreach (MapEntity linked in dockingTarget.Door.LinkedGap.linkedTo) + { + if (!(linked is Hull hull)) { continue; } + if (hull.Volume > largestHullSize) + { + refHull = hull; + largestHullSize = hull.Volume; + } + } + if (refHull != null) { return IsHorizontal ? - Math.Sign(dockingTarget.Door.LinkedGap.linkedTo[0].WorldPosition.X - dockingTarget.Door.Item.WorldPosition.X) : - Math.Sign(dockingTarget.Door.LinkedGap.linkedTo[0].WorldPosition.Y - dockingTarget.Door.Item.WorldPosition.Y); - } + Math.Sign(refHull.WorldPosition.X - dockingTarget.Door.Item.WorldPosition.X) : + Math.Sign(refHull.WorldPosition.Y - dockingTarget.Door.Item.WorldPosition.Y); + } } if (dockingTarget != null) { @@ -838,6 +866,7 @@ namespace Barotrauma.Items.Components #if SERVER if (GameMain.Server != null && (!item.Submarine?.Loading ?? true)) { + originalDockingTargetID = Entity.NullEntityID; item.CreateServerEvent(this); } #endif @@ -1010,9 +1039,7 @@ namespace Barotrauma.Items.Components public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) { -#if CLIENT - if (GameMain.Client != null) return; -#endif + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } bool wasDocked = docked; DockingPort prevDockingTarget = DockingTarget; @@ -1020,7 +1047,10 @@ namespace Barotrauma.Items.Components switch (connection.Name) { case "toggle": - Docked = !docked; + if (signal != "0") + { + Docked = !docked; + } break; case "set_active": case "set_state": @@ -1044,16 +1074,5 @@ namespace Barotrauma.Items.Components } #endif } - - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) - { - msg.Write(docked); - - if (docked) - { - msg.Write(DockingTarget.item.ID); - msg.Write(hulls != null && hulls[0] != null && hulls[1] != null && gap != null); - } - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 109ce1553..11cc23a71 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -90,8 +90,10 @@ namespace Barotrauma.Items.Components get { return stuck; } set { - if (isOpen || isBroken || !CanBeWelded) return; + if (isOpen || isBroken || !CanBeWelded) { return; } stuck = MathHelper.Clamp(value, 0.0f, 100.0f); + //don't allow clients to make the door stuck unless the server says so (handled in ClientRead) + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (stuck <= 0.0f) { IsStuck = false; } if (stuck >= 99.0f) { IsStuck = true; } } @@ -366,12 +368,24 @@ namespace Barotrauma.Items.Components } else { - Body.Enabled = Impassable || openState < 1.0f; + bool wasEnabled = Body.Enabled; + Body.Enabled = Impassable || openState < 1.0f; + if (wasEnabled && !Body.Enabled && IsHorizontal) + { + //when opening a hatch, force characters above it to refresh the floor position + //(otherwise the character won't fall through the hatch until it moves) + foreach (Character c in Character.CharacterList) + { + if (c.WorldPosition.Y < item.WorldPosition.Y) { continue; } + if (c.WorldPosition.X < item.WorldRect.X || c.WorldPosition.X > item.WorldRect.Right) { continue; } + c.AnimController?.ForceRefreshFloorY(); + } + } } //don't use the predicted state here, because it might set //other items to an incorrect state if the prediction is wrong - item.SendSignal(0, (isOpen) ? "1" : "0", "state_out", null); + item.SendSignal(0, isOpen ? "1" : "0", "state_out", null); } partial void UpdateProjSpecific(float deltaTime); @@ -616,12 +630,13 @@ namespace Barotrauma.Items.Components public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) { - if (IsStuck) return; + if (IsStuck) { return; } bool wasOpen = PredictedState == null ? isOpen : PredictedState.Value; if (connection.Name == "toggle") { + if (signal == "0") { return; } if (toggleCooldownTimer > 0.0f && sender != lastUser) { OnFailedToOpen(); return; } if (IsStuck) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; } toggleCooldownTimer = ToggleCoolDown; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs new file mode 100644 index 000000000..3b44cb761 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -0,0 +1,832 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Xml.Linq; +using Barotrauma.Extensions; +using Barotrauma.Networking; +using FarseerPhysics; +using FarseerPhysics.Dynamics; +using Microsoft.Xna.Framework; +using Vector2 = Microsoft.Xna.Framework.Vector2; + +namespace Barotrauma.Items.Components +{ + internal class ProducedItem + { + [Serialize(0f, true)] + public float Probability { get; set; } + + public readonly List StatusEffects = new List(); + + public readonly ItemPrefab? Prefab; + + public ProducedItem(ItemPrefab prefab, float probability) + { + Prefab = prefab; + Probability = probability; + } + + public ProducedItem(XElement element) + { + SerializableProperty.DeserializeProperties(this, element); + + string itemIdentifier = element.GetAttributeString("identifier", string.Empty); + if (!string.IsNullOrWhiteSpace(itemIdentifier)) + { + Prefab = ItemPrefab.Find(null, itemIdentifier); + } + + LoadSubElements(element); + } + + private void LoadSubElements(XElement element) + { + if (!element.HasElements) { return; } + + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "statuseffect": + { + StatusEffect effect = StatusEffect.Load(subElement, Prefab?.Name); + if (effect.type != ActionType.OnProduceSpawned) + { + DebugConsole.ThrowError("Only OnProduceSpawned type can be used in ."); + continue; + } + + StatusEffects.Add(effect); + break; + } + } + } + } + } + + // ReSharper disable UnusedMember.Global + internal enum VineTileType + { + Stem = 0b0000, + CrossJunction = 0b1111, + VerticalLane = 0b1010, + HorizontalLane = 0b0101, + TurnTopRight = 0b1001, + TurnTopLeft = 0b0011, + TurnBottomLeft = 0b0110, + TurnBottomRight = 0b1100, + TSectionTop = 0b1011, + TSectionLeft = 0b0111, + TSectionBottom = 0b1110, + TSectionRight = 0b1101, + StumpTop = 0b0001, + StumpLeft = 0b0010, + StumpBottom = 0b0100, + StumpRight = 0b1000 + } + + [Flags] + internal enum TileSide + { + None = 0, + Top = 1 << 0, + Left = 1 << 1, + Bottom = 1 << 2, + Right = 1 << 3 + } + + internal struct FoliageConfig + { + public static FoliageConfig EmptyConfig = new FoliageConfig { Variant = -1, Rotation = 0f, Scale = 1.0f }; + public static readonly int EmptyConfigValue = EmptyConfig.Serialize(); + + public int Variant; + public float Rotation; + public float Scale; + + public readonly int Serialize() + { + int variant = Math.Min(Variant + 1, 15); + int scale = (int) (Scale * 10f); + int rotation = (int) (Rotation / MathHelper.TwoPi * 10f); + + return variant | (scale << 4) | (rotation << 8); + } + + public static FoliageConfig Deserialize(int value) + { + int variant = value & 0x00F; + int scale = (value & 0x0F0) >> 4; + int rotation = (value & 0xF00) >> 8; + + return new FoliageConfig { Variant = variant - 1, Scale = scale / 10f, Rotation = rotation / 10f * MathHelper.TwoPi }; + } + + public static FoliageConfig CreateRandomConfig(int maxVariants, float minScale, float maxScale, Random? random = null) + { + int flowerVariant = Growable.RandomInt(0, maxVariants, random); + float flowerScale = (float) Growable.RandomDouble(minScale, maxScale, random); + float flowerRotation = (float) Growable.RandomDouble(0, MathHelper.TwoPi, random); + return new FoliageConfig { Variant = flowerVariant, Scale = flowerScale, Rotation = flowerRotation }; + } + } + + internal partial class VineTile + { + public TileSide Sides = TileSide.None; + public TileSide BlockedSides = TileSide.None; + + public readonly FoliageConfig FlowerConfig; + public readonly FoliageConfig LeafConfig; + + public int FailedGrowthAttempts; + public Rectangle Rect; + public Vector2 Position; + public Color HealthColor = Color.Transparent; + public float DecayDelay; + + private float VineStep; + private float FlowerStep; + private float growthStep; + + public float GrowthStep + { + get => growthStep; + set + { + const float limit = 1.0f; + growthStep = value; + VineStep = Math.Min((float) Math.Pow(value, 2), limit); + if (value > limit) + { + FlowerStep = Math.Min((float) Math.Pow(value - limit, 2), limit); + } + } + } + + private readonly float diameter; + private Vector2 offset; + + private readonly Growable Parent; + public VineTileType Type; + + public readonly Dictionary AdjacentPositions; + + public static int Size = 32; + + public VineTile(Growable parent, Vector2 position, VineTileType type, FoliageConfig? flowerConfig = null, FoliageConfig? leafConfig = null, Rectangle? rect = null) + { + FlowerConfig = flowerConfig ?? FoliageConfig.EmptyConfig; + LeafConfig = leafConfig ?? FoliageConfig.EmptyConfig; + Position = position; + Rect = rect ?? CreatePlantRect(position); + Parent = parent; + Type = type; + diameter = Rect.Width / 2.0f; + + AdjacentPositions = new Dictionary + { + { TileSide.Top, new Vector2(Position.X, Position.Y + Rect.Height) }, + { TileSide.Bottom, new Vector2(Position.X, Position.Y - Rect.Height) }, + { TileSide.Left, new Vector2(Position.X - Rect.Width, Position.Y) }, + { TileSide.Right, new Vector2(Position.X + Rect.Width, Position.Y) } + }; + } + + public void UpdateScale(float deltaTime) + { + if (Parent.Decayed && GrowthStep > 1.0f) + { + if (DecayDelay > 0) + { + DecayDelay -= deltaTime; + } + else + { + GrowthStep -= 0.25f * deltaTime; + } + } + + if (GrowthStep >= 2.0f || Parent.Decayed) { return; } + + GrowthStep += deltaTime; + + if (GrowthStep < 1.0f) + { + // I don't know how or why this works + float offsetAmount = diameter * VineStep - diameter; + switch (Type) + { + case VineTileType.StumpLeft: + offset.X = offsetAmount; + break; + case VineTileType.StumpRight: + offset.X = -offsetAmount; + break; + case VineTileType.StumpTop: + offset.Y = offsetAmount; + break; + case VineTileType.Stem: + case VineTileType.StumpBottom: + offset.Y = -offsetAmount; + break; + default: + offset = Vector2.Zero; + break; + } + } + else + { + offset = Vector2.Zero; + } + } + + public Vector2 GetWorldPosition(Planter planter, Vector2 slotOffset) + { + return planter.Item.WorldPosition + slotOffset + Position; + } + + public void UpdateType() + { + if (Type == VineTileType.Stem) { return; } + + Type = (VineTileType) Sides; + } + + /// + /// Returns a random side that is not occupied. + /// + /// + /// There is probably a much better way of doing this than allocating memory with an array + /// but this felt like the most reliable approach I could come up with. + /// + /// + public TileSide GetRandomFreeSide(Random? random = null) + { + const int maxSides = 4; + TileSide occupiedSides = Sides | BlockedSides; + int setBits = occupiedSides.Count(); + if (setBits >= maxSides) { return TileSide.None; } + + int possible = maxSides - setBits; + int[] pool = new int[possible]; + + for (int i = 0, j = 0; i < maxSides; i++) + { + if (!occupiedSides.IsBitSet((TileSide) (1 << i))) + { + pool[j] = i; + j++; + } + } + + int value = pool[Growable.RandomInt(0, possible, random)]; + + return (TileSide) (1 << value); + } + + public bool CanGrowMore() => (Sides | BlockedSides).Count() < 4; + + public static Rectangle CreatePlantRect(Vector2 pos) => new Rectangle((int) pos.X - Size / 2, (int) pos.Y + Size / 2, Size, Size); + } + + internal static class GrowthSideExtension + { + // Enum.HasFlag() sucks + public static bool IsBitSet(this TileSide side, TileSide bit) + { + return ((int) side & (int) bit) != 0; + } + + // K&R algorithm for counting how many bits are set in a bit field + public static int Count(this TileSide side) + { + int n = (int) side; + int count = 0; + while (n != 0) + { + count += n & 1; + n >>= 1; + } + + return count; + } + } + + internal partial class Growable : ItemComponent, IServerSerializable + { + // used for debugging where a vine failed to grow + public readonly HashSet FailedRectangles = new HashSet(); + + [Serialize(1f, true, "How fast the plant grows.")] + public float GrowthSpeed { get; set; } + + [Serialize(100f, true, "How long the plant can go without watering.")] + public float MaxHealth { get; set; } + + [Serialize(1f, true, "How much damage the plant takes while in water.")] + public float FloodTolerance { get; set; } + + [Serialize(1f, true, "How much damage the plant takes while growing.")] + public float Hardiness { get; set; } + + [Serialize(0.01f, true, "How often a seed is produced.")] + public float SeedRate { get; set; } + + [Serialize(0.01f, true, "How often a product item is produced.")] + public float ProductRate { get; set; } + + [Serialize(0.5f, true, "Probability of an attribute being randomly modified in a newly produced seed.")] + public float MutationProbability { get; set; } + + [Serialize("1.0,1.0,1.0,1.0", true, "Color of the flowers.")] + public Color FlowerTint { get; set; } + + [Serialize(3, true, "Number of flowers drawn when fully grown")] + public int FlowerQuantity { get; set; } + + [Serialize(0.25f, true, "Size of the flower sprites.")] + public float BaseFlowerScale { get; set; } + + [Serialize(0.5f, true, "Size of the leaf sprites.")] + public float BaseLeafScale { get; set; } + + [Serialize("1.0,1.0,1.0,1.0", true, "Color of the leaves.")] + public Color LeafTint { get; set; } + + [Serialize(0.33f, true, "Chance of a leaf appearing behind a branch.")] + public float LeafProbability { get; set; } + + [Serialize("1.0,1.0,1.0,1.0", true, "Color of the vines.")] + public Color VineTint { get; set; } + + [Serialize(32, true, "Maximum number of vine tiles the plant can grow.")] + public int MaximumVines { get; set; } + + [Serialize(0.25f, true, "Size of the vine sprites.")] + public float VineScale { get; set; } + + [Serialize("0.26,0.27,0.29,1.0", true, "Tint of a dead plant.")] + public Color DeadTint { get; set; } + + private const float increasedDeathSpeed = 10f; + private bool accelerateDeath; + private float health; + private int flowerVariants; + private int leafVariants; + private int[] flowerTiles; + + public float Health + { + get => health; + set => health = Math.Clamp(value, 0, MaxHealth); + } + + public bool Decayed; + public bool FullyGrown; + + private const int maxProductDelay = 10, + maxVineGrowthDelay = 10; + + private int productDelay; + private int vineDelay; + + public readonly List ProducedItems = new List(); + public readonly List Vines = new List(); + private readonly ProducedItem ProducedSeed; + + private static float MinFlowerScale = 0.5f, MaxFlowerScale = 1.0f, MinLeafScale = 0.5f, MaxLeafScale = 1.0f; + private const int VineChunkSize = 32; + + public Growable(Item item, XElement element) : base(item, element) + { + SerializableProperty.DeserializeProperties(this, element); + + Health = MaxHealth; + + if (element.HasElements) + { + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "produceditem": + ProducedItems.Add(new ProducedItem(subElement)); + break; + case "vinesprites": + LoadVines(subElement); + break; + } + } + } + + ProducedSeed = new ProducedItem(this.item.Prefab, 1.0f); + flowerTiles = new int[FlowerQuantity]; + } + + public override void OnItemLoaded() + { + base.OnItemLoaded(); + if (flowerTiles.All(i => i == 0)) + { + GenerateFlowerTiles(); + } + } + + private void GenerateFlowerTiles(Random? random = null) + { + flowerTiles = new int[FlowerQuantity]; + List pool = new List(); + for (int i = 0; i < MaximumVines - 1; i++) { pool.Add(i); } + + for (int i = 0; i < flowerTiles.Length; i++) + { + int index = RandomInt(0, pool.Count, random); + flowerTiles[i] = pool[index]; + pool.RemoveAt(index); + } + } + + partial void LoadVines(XElement element); + + public void OnGrowthTick(Planter planter, PlantSlot slot) + { + if (Decayed) { return; } + + if (FullyGrown) + { + TryGenerateProduct(planter, slot); + } + + if (Health > 0) + { + GrowVines(planter, slot); + Health -= accelerateDeath ? Hardiness * increasedDeathSpeed : Hardiness; + + if (planter.Item.InWater) + { + Health -= FloodTolerance; + } + } + + CheckPlantState(); + +#if CLIENT + UpdateBranchHealth(); +#endif + } + + private void UpdateBranchHealth() + { + Color healthColor = Color.White * (1.0f - Health / MaxHealth); + foreach (VineTile vine in Vines) + { + vine.HealthColor = healthColor; + } + } + + private void TryGenerateProduct(Planter planter, PlantSlot slot) + { + productDelay++; + if (productDelay <= maxProductDelay) { return; } + + productDelay = 0; + + bool spawnProduct = Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < ProductRate, + spawnSeed = Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < SeedRate; + + Vector2 spawnPos; + + if (spawnProduct || spawnSeed) + { + VineTile vine = Vines.GetRandom(); + spawnPos = vine.GetWorldPosition(planter, slot.Offset); + } + else + { + return; + } + + if (spawnProduct && ProducedItems.Any()) + { + SpawnItem(ProducedItems.RandomElementByWeight(it => it.Probability), spawnPos); + return; + } + + if (spawnSeed) + { + SpawnItem(ProducedSeed, spawnPos); + } + + static void SpawnItem(ProducedItem producedItem, Vector2 pos) + { + if (producedItem.Prefab == null) { return; } + + Entity.Spawner?.AddToSpawnQueue(producedItem.Prefab, pos, onSpawned: it => + { + foreach (StatusEffect effect in producedItem.StatusEffects) + { + it.ApplyStatusEffect(effect, ActionType.OnProduceSpawned, 1.0f, isNetworkEvent: true); + } + + it.ApplyStatusEffects(ActionType.OnProduceSpawned, 1.0f, isNetworkEvent: true); + }); + } + } + + /// + /// Updates plant's state to fully grown or dead depending on its conditions. + /// + /// True if the plant has finished growing. + private bool CheckPlantState() + { + if (Decayed) { return true; } + + if (0 >= Health) + { + Decayed = true; +#if CLIENT + foreach (VineTile vine in Vines) + { + vine.DecayDelay = (float) RandomDouble(0f, 30f); + } +#endif +#if SERVER + item.CreateServerEvent(this); +#endif + return true; + } + + if (Vines.Count >= MaximumVines && !FullyGrown) + { + FullyGrown = true; +#if SERVER + item.CreateServerEvent(this); +#endif + return true; + } + + if (!FullyGrown && !accelerateDeath && Vines.Any() && Vines.All(tile => !tile.CanGrowMore())) + { + accelerateDeath = true; + } + + // if the player somehow finds a way to extract the seed out of a planter kill the plant + if (item.ParentInventory is CharacterInventory) + { + Decayed = true; +#if SERVER + item.CreateServerEvent(this); +#endif + return true; + } + + return false; + } + + public override void Update(float deltaTime, Camera cam) + { + base.Update(deltaTime, cam); + +#if CLIENT + foreach (VineTile vine in Vines) + { + vine.UpdateScale(deltaTime); + } +#endif + + CheckPlantState(); + } + + private void GrowVines(Planter planter, PlantSlot slot) + { + if (FullyGrown) { return; } + + vineDelay++; + if (vineDelay <= maxVineGrowthDelay / GrowthSpeed) { return; } + + vineDelay = 0; + + if (!Vines.Any()) + { + // generate first stem + GenerateStem(); + return; + } + + int count = Vines.Count; + + TryGenerateBranches(planter, slot); + + if (Vines.Count > count) + { +#if SERVER + for (int i = 0; i < Vines.Count; i += VineChunkSize) + { + GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), i }); + } +#elif CLIENT + ResetPlanterSize(); +#endif + } + } + + private void GenerateStem() + { + VineTile stem = new VineTile(this, Vector2.Zero, VineTileType.Stem) { BlockedSides = TileSide.Bottom | TileSide.Left | TileSide.Right }; + Vines.Add(stem); + } + + private void TryGenerateBranches(Planter planter, PlantSlot slot, Random? random = null, Random? flowerRandom = null) + { + List newList = new List(Vines); + foreach (VineTile oldVines in newList) + { + if (oldVines.FailedGrowthAttempts > 8 || !oldVines.CanGrowMore()) { continue; } + + if (RandomInt(0, Vines.Count(tile => tile.CanGrowMore()), random) != 0) { continue; } + + TileSide side = oldVines.GetRandomFreeSide(random); + + if (side == TileSide.None) { continue; } + + Vector2 pos = oldVines.AdjacentPositions[side]; + Rectangle rect = VineTile.CreatePlantRect(pos); + + if (CollidesWithWorld(rect, planter, slot)) + { + oldVines.BlockedSides |= side; + oldVines.FailedGrowthAttempts++; + continue; + } + + FoliageConfig flowerConfig = FoliageConfig.EmptyConfig; + FoliageConfig leafConfig = FoliageConfig.EmptyConfig; + + if (flowerTiles.Any(i => Vines.Count == i)) + { + flowerConfig = FoliageConfig.CreateRandomConfig(flowerVariants, MinFlowerScale, MaxFlowerScale, flowerRandom); + } + + if (LeafProbability >= RandomDouble(0d, 1.0d, flowerRandom) && leafVariants > 0) + { + leafConfig = FoliageConfig.CreateRandomConfig(leafVariants, MinLeafScale, MaxLeafScale, flowerRandom); + } + + VineTile newVine = new VineTile(this, pos, VineTileType.CrossJunction, flowerConfig, leafConfig, rect); + + foreach (VineTile otherVine in Vines) + { + var (distX, distY) = pos - otherVine.Position; + int absDistX = (int) Math.Abs(distX), absDistY = (int) Math.Abs(distY); + + // check if the tile is within the with or height distance from us but ignore diagonals + if (absDistX > newVine.Rect.Width || absDistY > newVine.Rect.Height || absDistX > 0 && absDistY > 0) { continue; } + + // determines what side the tile is relative to the new tile by comparing the X/Y distance values + // if the X value is bigger than Y it's to the left or right of us and then check if X is negative or positive to determine if it's right or left + TileSide connectingSide = absDistX > absDistY ? distX > 0 ? TileSide.Right : TileSide.Left : distY > 0 ? TileSide.Top : TileSide.Bottom; + + // We use log2 to find the index and offset that index by 2 since the opposite side is always 2 offsets away + TileSide oppositeSide = (TileSide) (1 << ((int) Math.Log2((int) connectingSide) + 2) % 4); + + if (otherVine.BlockedSides.IsBitSet(connectingSide)) + { + newVine.BlockedSides |= oppositeSide; + continue; + } + + if (otherVine != oldVines) + { + otherVine.BlockedSides |= connectingSide; + newVine.BlockedSides |= oppositeSide; + } + else + { + otherVine.Sides |= connectingSide; + newVine.Sides |= oppositeSide; + } + } + + Vines.Add(newVine); + + foreach (VineTile vine in Vines) + { + vine.UpdateType(); + } + } + } + + private bool CollidesWithWorld(Rectangle rect, Planter planter, PlantSlot slot) + { + if (Vines.Any(g => g.Rect.Contains(rect))) { return true; } + + Rectangle worldRect = rect; + worldRect.Location = planter.Item.WorldPosition.ToPoint() + slot.Offset.ToPoint() + worldRect.Location; + worldRect.Y -= worldRect.Height; + + Rectangle planterRect = planter.Item.WorldRect; + planterRect.Y -= planterRect.Height; + + if (planterRect.Intersects(worldRect)) + { +#if DEBUG + if (!FailedRectangles.Contains(worldRect)) + { + FailedRectangles.Add(worldRect); + } +#endif + return true; + } + + Vector2 topLeft = ConvertUnits.ToSimUnits(new Vector2(worldRect.Left, worldRect.Top)), + topRight = ConvertUnits.ToSimUnits(new Vector2(worldRect.Right, worldRect.Top)), + bottomLeft = ConvertUnits.ToSimUnits(new Vector2(worldRect.Left, worldRect.Bottom)), + bottomRight = ConvertUnits.ToSimUnits(new Vector2(worldRect.Right, worldRect.Bottom)); + + // ray casting a cross on the corners didn't seem to work so we are ray casting along the perimeter + bool hasCollision = planterRect.Intersects(worldRect) || LineCollides(topLeft, topRight) || LineCollides(topRight, bottomRight) || LineCollides(bottomRight, bottomLeft) || LineCollides(bottomLeft, topLeft); + +#if DEBUG + if (hasCollision) + { + if (!FailedRectangles.Contains(worldRect)) + { + FailedRectangles.Add(worldRect); + } + } +#endif + return hasCollision; + + static bool LineCollides(Vector2 point1, Vector2 point2) + { + const Category category = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel; + return Submarine.PickBody(point1, point2, collisionCategory: category, customPredicate: f => !(f.UserData is Hull) && f.CollidesWith.HasFlag(Physics.CollisionItem)) != null; + } + } + + public override XElement Save(XElement parentElement) + { + XElement element = base.Save(parentElement); + element.Add(new XAttribute("flowertiles", string.Join(",", flowerTiles))); + element.Add(new XAttribute("decayed", Decayed)); + foreach (VineTile vine in Vines) + { + XElement vineElement = new XElement("Vine"); + vineElement.Add(new XAttribute("sides", (int) vine.Sides)); + vineElement.Add(new XAttribute("blockedsides", (int) vine.BlockedSides)); + vineElement.Add(new XAttribute("pos", XMLExtensions.Vector2ToString(vine.Position))); + vineElement.Add(new XAttribute("tile", (int) vine.Type)); + vineElement.Add(new XAttribute("failedattempts", vine.FailedGrowthAttempts)); +#if SERVER + vineElement.Add(new XAttribute("growthscale", Decayed ? 1.0f : 2.0f)); +#else + vineElement.Add(new XAttribute("growthscale", vine.GrowthStep)); +#endif + vineElement.Add(new XAttribute("flowerconfig", vine.FlowerConfig.Serialize())); + vineElement.Add(new XAttribute("leafconfig", vine.LeafConfig.Serialize())); + + element.Add(vineElement); + } + + return element; + } + + public override void Load(XElement componentElement, bool usePrefabValues) + { + base.Load(componentElement, usePrefabValues); + flowerTiles = componentElement.GetAttributeIntArray("flowertiles", new int[0]); + Decayed = componentElement.GetAttributeBool("decayed", false); + + Vines.Clear(); + foreach (XElement element in componentElement.Elements()) + { + if (element.Name.ToString().Equals("vine", StringComparison.OrdinalIgnoreCase)) + { + VineTileType type = (VineTileType) element.GetAttributeInt("tile", 0); + Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero); + TileSide sides = (TileSide) element.GetAttributeInt("sides", 0); + TileSide blockedSides = (TileSide) element.GetAttributeInt("blockedsides", 0); + int failedAttempts = element.GetAttributeInt("failedattempts", 0); + float growthscale = element.GetAttributeFloat("growthscale", 0f); + int flowerConfig = element.GetAttributeInt("flowerconfig", FoliageConfig.EmptyConfigValue); + int leafConfig = element.GetAttributeInt("leafconfig", FoliageConfig.EmptyConfigValue); + + VineTile tile = new VineTile(this, pos, type, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafConfig)) + { + Sides = sides, BlockedSides = blockedSides, FailedGrowthAttempts = failedAttempts, GrowthStep = growthscale + }; + + Vines.Add(tile); + } + } + } + + private bool CanGrowMore() => Vines.Any(tile => tile.CanGrowMore()); + + public static int RandomInt(int min, int max, Random? random = null) => random?.Next(min, max) ?? Rand.Range(min, max); + public static double RandomDouble(double min, double max, Random? random = null) => random?.NextDouble() * (max - min) + min ?? Rand.Range(min, max); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 9added359..0f6427eef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -5,6 +5,7 @@ using FarseerPhysics.Dynamics.Contacts; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma.Items.Components @@ -392,6 +393,8 @@ namespace Barotrauma.Items.Components if (item.GetComponent() != null) { return true; } + if (item.GetComponent() is { } planter && planter.GrowableSeeds.Any(seed => seed != null)) { return false; } + //if the item has a connection panel and rewiring is disabled, don't allow deattaching var connectionPanel = item.GetComponent(); if (connectionPanel != null && (connectionPanel.Locked || !(GameMain.NetworkMember?.ServerSettings?.AllowRewiring ?? true))) @@ -476,12 +479,13 @@ namespace Barotrauma.Items.Components } } - var containedItems = item.ContainedItems; + var containedItems = item.OwnInventory?.Items; if (containedItems != null) { foreach (Item contained in containedItems) { - if (contained.body == null) continue; + if (contained == null) { continue; } + if (contained.body == null) { continue; } contained.SetTransform(item.SimPosition, contained.body.Rotation); } } @@ -573,7 +577,7 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (item.body == null || !item.body.Enabled) return; + if (item.body == null || !item.body.Enabled) { return; } if (picker == null || !picker.HasEquippedItem(item)) { if (Pusher != null) { Pusher.Enabled = false; } @@ -598,7 +602,10 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, picker); - if (item.body.Dir != picker.AnimController.Dir) Flip(); + if (item.body.Dir != picker.AnimController.Dir) + { + item.FlipX(relativeToSub: false); + } item.Submarine = picker.Submarine; @@ -635,11 +642,14 @@ namespace Barotrauma.Items.Components } } - public void Flip() + public override void FlipX(bool relativeToSub) { handlePos[0].X = -handlePos[0].X; handlePos[1].X = -handlePos[1].X; - item.body.Dir = -item.body.Dir; + if (item.body != null) + { + item.body.Dir = -item.body.Dir; + } } public override void OnItemLoaded() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 1bf97dadd..6c4d2e8e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -63,6 +63,13 @@ namespace Barotrauma.Items.Components item.RequireAimToUse = true; } + public override void Equip(Character character) + { + base.Equip(character); + reloadTimer = Math.Min(reload, 1.0f); + IsActive = true; + } + public override bool Use(float deltaTime, Character character = null) { if (character == null || reloadTimer > 0.0f) { return false; } @@ -151,7 +158,7 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, picker); - if (item.body.Dir != picker.AnimController.Dir) { Flip(); } + if (item.body.Dir != picker.AnimController.Dir) { item.FlipX(relativeToSub: false); } AnimController ac = picker.AnimController; @@ -366,13 +373,15 @@ 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 { GameMain.Server.CreateEntityEvent(item, new object[] { Networking.NetEntityEvent.Type.ApplyStatusEffect, - ActionType.OnUse, + success ? ActionType.OnUse : ActionType.OnFailure, null, //itemcomponent targetCharacter.ID, targetLimb }); @@ -389,7 +398,7 @@ namespace Barotrauma.Items.Components if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe?? { - ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: User); + ApplyStatusEffects(success ? ActionType.OnUse : ActionType.OnFailure, 1.0f, targetCharacter, targetLimb, user: User); } if (DeleteOnUse) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 8d793f906..6e4bc0134 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -61,7 +61,7 @@ namespace Barotrauma.Items.Components allowedSlots.Add(allowedSlot); } - canBePicked = true; + canBePicked = true; } public override bool Pick(Character picker) @@ -142,7 +142,8 @@ namespace Barotrauma.Items.Components this, item.WorldPosition, pickTimer / requiredTime, - GUI.Style.Red, GUI.Style.Green); + GUI.Style.Red, GUI.Style.Green, + !string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching"); #endif picker.AnimController.UpdateUseItem(true, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((pickTimer / 10.0f) % 0.1f)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index c11637145..107873037 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -72,6 +72,12 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(XElement element); + public override void Equip(Character character) + { + reloadTimer = Math.Min(reload, 1.0f); + IsActive = true; + } + public override void Update(float deltaTime, Camera cam) { reloadTimer -= deltaTime; @@ -180,22 +186,25 @@ namespace Barotrauma.Items.Components public Projectile FindProjectile(bool triggerOnUseOnContainers = false) { - var containedItems = item.ContainedItems; + var containedItems = item.OwnInventory?.Items; if (containedItems == null) { return null; } foreach (Item item in containedItems) { + if (item == null) { continue; } Projectile projectile = item.GetComponent(); if (projectile != null) { return projectile; } } //projectile not found, see if one of the contained items contains projectiles - foreach (Item item in containedItems) + foreach (Item it in containedItems) { - var containedSubItems = item.ContainedItems; + if (it == null) { continue; } + var containedSubItems = it.OwnInventory?.Items; if (containedSubItems == null) { continue; } foreach (Item subItem in containedSubItems) { + if (subItem == null) { continue; } Projectile projectile = subItem.GetComponent(); //apply OnUse statuseffects to the container in case it has to react to it somehow //(play a sound, spawn more projectiles, reduce condition...) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 532627fd8..577db9829 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -52,12 +52,16 @@ namespace Barotrauma.Items.Components { get; set; } + [Serialize(0.0f, false, description: "How much the item decreases the size of fires per second.")] public float ExtinguishAmount { get; set; } + [Serialize(0.0f, false, description: "How much water the item provides to planters per second.")] + public float WaterAmount { get; set; } + [Serialize("0.0,0.0", false, description: "The position of the barrel as an offset from the item's center (in pixels).")] public Vector2 BarrelPos { get; set; } @@ -82,13 +86,19 @@ namespace Barotrauma.Items.Components [Serialize(0.0f, false, description: "Force applied to the entity the ray hits.")] public float TargetForce { get; set; } + [Serialize(0.0f, false, description: "Rotation of the barrel in degrees."), Editable(MinValueFloat = 0, MaxValueFloat = 360, VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" })] + public float BarrelRotation + { + get; set; + } + public Vector2 TransformedBarrelPos { get { - Matrix bodyTransform = Matrix.CreateRotationZ(item.body.Rotation); + Matrix bodyTransform = Matrix.CreateRotationZ(item.body.Rotation + MathHelper.ToRadians(BarrelRotation)); Vector2 flippedPos = BarrelPos; - if (item.body.Dir < 0.0f) flippedPos.X = -flippedPos.X; + if (item.body.Dir < 0.0f) { flippedPos.X = -flippedPos.X; } return (Vector2.Transform(flippedPos, bodyTransform)); } } @@ -188,7 +198,7 @@ namespace Barotrauma.Items.Components } float spread = MathHelper.ToRadians(MathHelper.Lerp(UnskilledSpread, Spread, degreeOfSuccess)); - float angle = item.body.Rotation + spread * Rand.Range(-0.5f, 0.5f); + float angle = item.body.Rotation + MathHelper.ToRadians(BarrelRotation) + spread * Rand.Range(-0.5f, 0.5f); Vector2 rayEnd = rayStart + ConvertUnits.ToSimUnits(new Vector2( (float)Math.Cos(angle), @@ -276,7 +286,7 @@ namespace Barotrauma.Items.Components ignoreSensors: false, customPredicate: (Fixture f) => { - if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure) { return false; } + if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure || (f.Body?.UserData is Item it && it.GetComponent() != null)) { return false; } if (f.Body?.UserData as string == "ruinroom") { return false; } return true; }, @@ -373,6 +383,42 @@ namespace Barotrauma.Items.Components } } + if (WaterAmount > 0.0f && item.CurrentHull?.Submarine != null) + { + Vector2 pos = ConvertUnits.ToDisplayUnits(rayStart + item.Submarine.SimPosition); + + // Could probably be done much efficiently here + foreach (Item it in Item.ItemList) + { + if (it.Submarine == item.Submarine && it.GetComponent() is { } planter) + { + if (it.GetComponent() is { } holdable && holdable.Attachable && !holdable.Attached) { continue; } + + Rectangle collisionRect = it.WorldRect; + collisionRect.Y -= collisionRect.Height; + if (collisionRect.Left < pos.X && collisionRect.Right > pos.X && collisionRect.Bottom < pos.Y) + { + Body collision = Submarine.PickBody(rayStart, it.SimPosition, ignoredBodies, collisionCategories); + if (collision == null) + { + for (var i = 0; i < planter.GrowableSeeds.Length; i++) + { + Growable seed = planter.GrowableSeeds[i]; + if (seed == null || seed.Decayed) { continue; } + + seed.Health += WaterAmount * deltaTime; + +#if CLIENT + float barOffset = 10f * GUI.Scale; + Vector2 offset = planter.PlantSlots.ContainsKey(i) ? planter.PlantSlots[i].Offset : Vector2.Zero; + user.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxHealth, GUI.Style.Blue, GUI.Style.Blue, "progressbar.watering"); +#endif + } + } + } + } + } + } if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { @@ -464,7 +510,7 @@ namespace Barotrauma.Items.Components } else if (targetBody.UserData is Item targetItem) { - if (!HitItems) { return false; } + if (!HitItems || targetItem.NonInteractable) { return false; } var levelResource = targetItem.GetComponent(); if (levelResource != null && levelResource.Attached && @@ -477,8 +523,9 @@ namespace Barotrauma.Items.Components this, targetItem.WorldPosition, levelResource.DeattachTimer / levelResource.DeattachDuration, - GUI.Style.Red, GUI.Style.Green); + GUI.Style.Red, GUI.Style.Green, "progressbar.deattaching"); #endif + FixItemProjSpecific(user, deltaTime, targetItem); return true; } @@ -571,34 +618,31 @@ namespace Barotrauma.Items.Components character.AIController.SteeringManager.SteeringSeek(standPos); } } - else + if (dist < reach / 2) { - if (dist < reach / 2) + // Too close -> steer away + character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition)); + } + else if (dist < reach * 2) + { + // In or almost in range + character.CursorPosition = leak.Position; + character.CursorPosition += VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2); + if (character.AnimController.InWater) { - // Too close -> steer away - character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition)); - } - else if (dist <= reach) - { - // In range - character.CursorPosition = leak.Position; - character.CursorPosition += VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2); - if (character.AnimController.InWater) - { - var torso = character.AnimController.GetLimb(LimbType.Torso); - // Turn facing the target when not moving (handled in the animcontroller if not moving) - Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); - Vector2 diff = (mousePos - torso.SimPosition) * character.AnimController.Dir; - float newRotation = MathUtils.VectorToAngle(diff); - character.AnimController.Collider.SmoothRotate(newRotation, 5.0f); + var torso = character.AnimController.GetLimb(LimbType.Torso); + // Turn facing the target when not moving (handled in the animcontroller if not moving) + Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); + Vector2 diff = (mousePos - torso.SimPosition) * character.AnimController.Dir; + float newRotation = MathUtils.VectorToAngle(diff); + character.AnimController.Collider.SmoothRotate(newRotation, 5.0f); - if (VectorExtensions.Angle(VectorExtensions.Forward(torso.body.TransformedRotation), fromCharacterToLeak) < MathHelper.PiOver4) - { - // Swim past - Vector2 moveDir = leak.IsHorizontal ? Vector2.UnitY : Vector2.UnitX; - moveDir *= character.AnimController.Dir; - character.AIController.SteeringManager.SteeringManual(deltaTime, moveDir); - } + if (VectorExtensions.Angle(VectorExtensions.Forward(torso.body.TransformedRotation), fromCharacterToLeak) < MathHelper.PiOver4) + { + // Swim past + Vector2 moveDir = leak.IsHorizontal ? Vector2.UnitY : Vector2.UnitX; + moveDir *= character.AnimController.Dir; + character.AIController.SteeringManager.SteeringManual(deltaTime, moveDir); } } } @@ -674,9 +718,8 @@ namespace Barotrauma.Items.Components // A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml. foreach (ISerializableEntity target in targets) { - if (!(target is Door door)) { continue; } - - if (!door.CanBeWelded) { continue; } + if (!(target is Door door)) { continue; } + if (!door.CanBeWelded || door.Item.NonInteractable) { continue; } for (int i = 0; i < effect.propertyNames.Length; i++) { string propertyName = effect.propertyNames[i]; @@ -685,7 +728,7 @@ namespace Barotrauma.Items.Components object value = property.GetValue(target); if (door.Stuck > 0) { - var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White); + var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White, "progressbar.welding"); if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Sprayer.cs new file mode 100644 index 000000000..32a800d50 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Sprayer.cs @@ -0,0 +1,62 @@ +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + partial class Sprayer : RangedWeapon + { + [Serialize(0.0f, false, description: "The distance at which the item can spray walls.")] + public float Range { get; set; } + + [Serialize(1.0f, false, description: "How fast the item changes the color of the walls.")] + public float SprayStrength { get; set; } + + private readonly Dictionary liquidColors; + private ItemContainer liquidContainer; + + public Sprayer(Item item, XElement element) : base(item, element) + { + item.IsShootable = true; + item.RequireAimToUse = true; + + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "paintcolors": + { + liquidColors = new Dictionary(); + foreach (XElement paintElement in subElement.Elements()) + { + string paintName = paintElement.GetAttributeString("paintitem", string.Empty); + Color paintColor = paintElement.GetAttributeColor("color", Color.Transparent); + + if (paintName != string.Empty) + { + liquidColors.Add(paintName, paintColor); + } + } + } + break; + } + } + InitProjSpecific(element); + } + + public override void OnItemLoaded() + { + liquidContainer = item.GetComponent(); + } + + partial void InitProjSpecific(XElement element); + +#if SERVER + public override bool Use(float deltaTime, Character character = null) + { + return character != null || character.Removed; + } +#endif + + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 7cbe9fe91..62d7c6de6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -84,7 +84,7 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, picker); - if (item.body.Dir != picker.AnimController.Dir) { Flip(); } + if (item.body.Dir != picker.AnimController.Dir) { item.FlipX(relativeToSub: false); } AnimController ac = picker.AnimController; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 97c645c9c..4cb928a61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -76,6 +76,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize("", false, description: "What to display on the progress bar when this item is being picked.")] + public string PickingMsg + { + get; + set; + } + public Dictionary SerializableProperties { get; protected set; } public Action OnActiveStateChanged; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 414f50579..0a56c0efb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -44,6 +44,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(true, false, "Allow dragging and dropping items to deposit items into this inventory.")] + public bool AllowDragAndDrop + { + get; + set; + } + [Serialize(false, false, description: "If set to true, interacting with this item will make the character interact with the contained item(s), automatically picking them up if they can be picked up.")] public bool AutoInteractWithContained @@ -166,17 +173,21 @@ namespace Barotrauma.Items.Components public bool CanBeContained(Item item) { if (ContainableItems.Count == 0) { return true; } - return (ContainableItems.Find(c => c.MatchesItem(item)) != null); + return ContainableItems.Find(c => c.MatchesItem(item)) != null; } public bool CanBeContained(ItemPrefab itemPrefab) { if (ContainableItems.Count == 0) { return true; } - return (ContainableItems.Find(c => c.MatchesItem(itemPrefab)) != null); + return ContainableItems.Find(c => c.MatchesItem(itemPrefab)) != null; } public override void Update(float deltaTime, Camera cam) { - if (item.body != null && + if (item.ParentInventory is CharacterInventory) + { + item.SetContainedItemPositions(); + } + else if (item.body != null && item.body.Enabled && item.body.FarseerBody.Awake) { @@ -209,22 +220,6 @@ namespace Barotrauma.Items.Components } } - public override void OnItemLoaded() - { - base.OnItemLoaded(); - if (SpawnWithId.Length > 0) - { - ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId); - if (prefab != null) - { - if (Inventory != null && Inventory.Items.Any(it => it == null)) - { - Entity.Spawner?.AddToSpawnQueue(prefab, Inventory); - } - } - } - } - public override bool HasRequiredItems(Character character, bool addMessage, string msg = null) { return (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); @@ -284,6 +279,8 @@ namespace Barotrauma.Items.Components public override bool Combine(Item item, Character user) { + if (!AllowDragAndDrop && user != null) { return false; } + if (!ContainableItems.Any(x => x.MatchesItem(item))) { return false; } if (user != null && !user.CanAccessInventory(Inventory)) { return false; } @@ -354,16 +351,28 @@ namespace Barotrauma.Items.Components public override void OnMapLoaded() { - if (itemIds == null) { return; } - - for (ushort i = 0; i < itemIds.Length; i++) - { - if (!(Entity.FindEntityByID(itemIds[i]) is Item item)) { continue; } - if (i >= Inventory.Capacity) { continue; } - Inventory.TryPutItem(item, i, false, false, null, false); + if (itemIds != null) + { + for (ushort i = 0; i < itemIds.Length; i++) + { + if (!(Entity.FindEntityByID(itemIds[i]) is Item item)) { continue; } + if (i >= Inventory.Capacity) { continue; } + Inventory.TryPutItem(item, i, false, false, null, false); + } + itemIds = null; } - itemIds = null; + if (SpawnWithId.Length > 0) + { + ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId); + if (prefab != null) + { + if (Inventory != null && Inventory.Items.Any(it => it == null)) + { + Entity.Spawner?.AddToSpawnQueue(prefab, Inventory); + } + } + } } protected override void ShallowRemoveComponentSpecific() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemLabel.cs index beebb7b03..a523fbc60 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemLabel.cs @@ -1,8 +1,9 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; namespace Barotrauma.Items.Components { - partial class ItemLabel : ItemComponent, IDrawableComponent + partial class ItemLabel : ItemComponent, IDrawableComponent, IServerSerializable { public Vector2 DrawSize { @@ -10,12 +11,16 @@ namespace Barotrauma.Items.Components get { return Vector2.Zero; } } + partial void OnStateChanged(); + public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1) { switch (connection.Name) { case "set_text": + if (Text == signal) { return; } Text = signal; + OnStateChanged(); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 6fb9f2b01..0df74b078 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -151,6 +151,7 @@ namespace Barotrauma.Items.Components if (user == null || user.Removed || user.SelectedConstruction != item + || item.ParentInventory != null || !user.CanInteractWith(item) || (UsableIn == UseEnvironment.Water && !user.AnimController.InWater) || (UsableIn == UseEnvironment.Air && user.AnimController.InWater)) @@ -221,7 +222,7 @@ namespace Barotrauma.Items.Components user.AnimController.ResetPullJoints(); - if (dir != 0) user.AnimController.TargetDir = dir; + if (dir != 0) { user.AnimController.TargetDir = dir; } foreach (LimbPos lb in limbPositions) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index 171d8ba6e..423e59b46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Items.Components private float maxForce; - private Attack propellerDamage; + private readonly Attack propellerDamage; private float damageTimer; @@ -24,6 +24,8 @@ namespace Barotrauma.Items.Components private float controlLockTimer; + public Character User; + [Editable(0.0f, 10000000.0f), Serialize(2000.0f, true, description: "The amount of force exerted on the submarine when the engine is operating at full power.")] public float MaxForce @@ -106,6 +108,11 @@ namespace Barotrauma.Items.Components { //arbitrary multiplier that was added to changes in submarine mass without having to readjust all engines float forceMultiplier = 0.1f; + if (User != null) + { + forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100)); + } + float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage / MinVoltage, 1.0f); Vector2 currForce = new Vector2(force * maxForce * forceMultiplier * voltageFactor, 0.0f); //less effective when in a bad condition @@ -193,6 +200,7 @@ namespace Barotrauma.Items.Components { controlLockTimer = 0.1f; targetForce = MathHelper.Clamp(tempForce, -100.0f, 100.0f); + User = sender; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 49cf54621..9abbc812c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Xml.Linq; @@ -15,6 +16,9 @@ namespace Barotrauma.Items.Components private float timeUntilReady; private float requiredTime; + private string savedFabricatedItem; + private float savedTimeUntilReady, savedRequiredTime; + private bool hasPower; private Character user; @@ -46,6 +50,7 @@ namespace Barotrauma.Items.Components if (state == value) { return; } state = value; #if SERVER + serverEventId++; item.CreateServerEvent(this); #endif } @@ -158,8 +163,8 @@ namespace Barotrauma.Items.Components private void StartFabricating(FabricationRecipe selectedItem, Character user) { - if (selectedItem == null) return; - if (!outputContainer.Inventory.IsEmpty()) return; + if (selectedItem == null) { return; } + if (!outputContainer.Inventory.IsEmpty()) { return; } #if CLIENT itemList.Enabled = false; @@ -194,14 +199,23 @@ namespace Barotrauma.Items.Components private void CancelFabricating(Character user = null) { - if (fabricatedItem == null) { return; } - IsActive = false; - fabricatedItem = null; this.user = null; - currPowerConsumption = 0.0f; + progressState = 0.0f; + timeUntilReady = 0.0f; + inputContainer.Inventory.Locked = false; + outputContainer.Inventory.Locked = false; + + if (GameMain.NetworkMember?.IsServer ?? true) + { + State = FabricatorState.Stopped; + } + + if (fabricatedItem == null) { return; } + fabricatedItem = null; + #if CLIENT itemList.Enabled = true; if (activateButton != null) @@ -209,17 +223,6 @@ namespace Barotrauma.Items.Components activateButton.Text = TextManager.Get("FabricatorCreate"); } #endif - progressState = 0.0f; - - timeUntilReady = 0.0f; - - inputContainer.Inventory.Locked = false; - outputContainer.Inventory.Locked = false; - - if (GameMain.NetworkMember?.IsServer ?? true) - { - State = FabricatorState.Stopped; - } #if SERVER if (user != null) { @@ -279,13 +282,13 @@ namespace Barotrauma.Items.Components { for (int i = 0; i < ingredient.Amount; i++) { - var availableItem = availableIngredients.FirstOrDefault(it => it != null && it.Prefab == ingredient.ItemPrefab && it.Condition >= ingredient.ItemPrefab.Health * ingredient.MinCondition); + var availableItem = availableIngredients.FirstOrDefault(it => it != null && ingredient.ItemPrefabs.Contains(it.Prefab) && it.ConditionPercentage >= ingredient.MinCondition * 100.0f); if (availableItem == null) { continue; } //Item4 = use condition bool - if (ingredient.UseCondition && availableItem.Condition - ingredient.ItemPrefab.Health * ingredient.MinCondition > 0.0f) //Leave it behind with reduced condition if it has enough to stay above 0 + if (ingredient.UseCondition && availableItem.ConditionPercentage - ingredient.MinCondition * 100 > 0.0f) //Leave it behind with reduced condition if it has enough to stay above 0 { - availableItem.Condition -= ingredient.ItemPrefab.Health * ingredient.MinCondition; + availableItem.Condition -= availableItem.Prefab.Health * ingredient.MinCondition; continue; } availableIngredients.Remove(availableItem); @@ -366,7 +369,8 @@ namespace Barotrauma.Items.Components public float FabricationDegreeOfSuccess(Character character, List skills) { - if (skills.Count == 0) return 1.0f; + if (skills.Count == 0) { return 1.0f; } + if (character == null) { return 0.0f; } float skillSum = (from t in skills let characterLevel = character.GetSkillLevel(t.Identifier) select (characterLevel - (t.Level * SkillRequirementMultiplier))).Sum(); float average = skillSum / skills.Count; @@ -461,8 +465,53 @@ namespace Barotrauma.Items.Components { return item != null && - item.prefab == requiredItem.ItemPrefab && + requiredItem.ItemPrefabs.Contains(item.prefab) && item.Condition / item.Prefab.Health >= requiredItem.MinCondition; } + + public override XElement Save(XElement parentElement) + { + var componentElement = base.Save(parentElement); + if (fabricatedItem != null) + { + componentElement.Add(new XAttribute("fabricateditemidentifier", fabricatedItem.TargetItem.Identifier)); + componentElement.Add(new XAttribute("savedtimeuntilready", timeUntilReady.ToString("G", CultureInfo.InvariantCulture))); + componentElement.Add(new XAttribute("savedrequiredtime", requiredTime.ToString("G", CultureInfo.InvariantCulture))); + + } + return componentElement; + } + + public override void Load(XElement componentElement, bool usePrefabValues) + { + base.Load(componentElement, usePrefabValues); + savedFabricatedItem = componentElement.GetAttributeString("fabricateditemidentifier", ""); + savedTimeUntilReady = componentElement.GetAttributeFloat("savedtimeuntilready", 0.0f); + savedRequiredTime = componentElement.GetAttributeFloat("savedrequiredtime", 0.0f); + } + + public override void OnMapLoaded() + { + if (string.IsNullOrEmpty(savedFabricatedItem)) { return; } + + inputContainer?.OnMapLoaded(); + outputContainer?.OnMapLoaded(); + + var recipe = fabricationRecipes.Find(r => r.TargetItem.Identifier == savedFabricatedItem); + if (recipe == null) + { + DebugConsole.ThrowError("Error while loading a fabricator. Can't continue fabricating \"" + savedFabricatedItem + "\" (matching recipe not found)."); + } + else + { +#if CLIENT + SelectItem(null, recipe, savedRequiredTime); +#endif + StartFabricating(recipe, user: null); + timeUntilReady = savedTimeUntilReady; + requiredTime = savedRequiredTime; + } + savedFabricatedItem = null; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index b043bb4ff..cdc211703 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -11,7 +11,8 @@ namespace Barotrauma.Items.Components { partial class Reactor : Powered, IServerSerializable, IClientSerializable { - const float NetworkUpdateInterval = 0.5f; + const float NetworkUpdateIntervalHigh = 0.5f; + const float NetworkUpdateIntervalLow = 10.0f; //the rate at which the reactor is being run on (higher rate -> higher temperature) private float fissionRate; @@ -64,17 +65,18 @@ namespace Barotrauma.Items.Components } } - private Character lastAIUser; + public Character LastAIUser { get; private set; } private Character lastUser; - private Character LastUser + public Character LastUser { get { return lastUser; } - set + private set { - if (lastUser == value) return; + if (lastUser == value) { return; } lastUser = value; degreeOfSuccess = lastUser == null ? 0.0f : DegreeOfSuccess(lastUser); + LastUserWasPlayer = lastUser.IsPlayer; } } @@ -176,6 +178,8 @@ namespace Barotrauma.Items.Components [Serialize(0.0f, true)] public float AvailableFuel { get; set; } + public bool LastUserWasPlayer { get; private set; } + public Reactor(Item item, XElement element) : base(item, element) { @@ -207,13 +211,13 @@ namespace Barotrauma.Items.Components //if an AI character was using the item on the previous frame but not anymore, turn autotemp on // (= bots turn autotemp back on when leaving the reactor) - if (lastAIUser != null) + if (LastAIUser != null) { - if (lastAIUser.SelectedConstruction != item && lastAIUser.CanInteractWith(item)) + if (LastAIUser.SelectedConstruction != item && LastAIUser.CanInteractWith(item)) { AutoTemp = true; unsentChanges = true; - lastAIUser = null; + LastAIUser = null; } } @@ -309,10 +313,15 @@ namespace Barotrauma.Items.Components if (fissionRate > 0.0f) { - foreach (Item item in item.ContainedItems) + var containedItems = item.OwnInventory?.Items; + if (containedItems != null) { - if (!item.HasTag("reactorfuel")) continue; - item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime; + foreach (Item item in containedItems) + { + if (item == null) { continue; } + if (!item.HasTag("reactorfuel")) { continue; } + item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime; + } } if (item.CurrentHull != null) @@ -337,6 +346,7 @@ namespace Barotrauma.Items.Components item.SendSignal(0, ((int)(temperature * 100.0f)).ToString(), "temperature_out", null); item.SendSignal(0, ((int)-CurrPowerConsumption).ToString(), "power_value_out", null); item.SendSignal(0, ((int)load).ToString(), "load_value_out", null); + item.SendSignal(0, ((int)AvailableFuel).ToString(), "fuel_out", null); UpdateFailures(deltaTime); #if CLIENT @@ -344,9 +354,13 @@ namespace Barotrauma.Items.Components #endif AvailableFuel = 0.0f; - sendUpdateTimer = Math.Max(sendUpdateTimer - deltaTime, 0.0f); + sendUpdateTimer -= deltaTime; +#if CLIENT if (unsentChanges && sendUpdateTimer <= 0.0f) +#else + if (sendUpdateTimer < -NetworkUpdateIntervalLow || (unsentChanges && sendUpdateTimer <= 0.0f)) +#endif { #if SERVER if (GameMain.Server != null) @@ -360,7 +374,7 @@ namespace Barotrauma.Items.Components item.CreateClientEvent(this); } #endif - sendUpdateTimer = NetworkUpdateInterval; + sendUpdateTimer = NetworkUpdateIntervalHigh; unsentChanges = false; } } @@ -398,8 +412,8 @@ namespace Barotrauma.Items.Components private bool TooMuchFuel() { - var containedItems = item.ContainedItems; - if (containedItems != null && containedItems.Count() <= 1) { return false; } + var containedItems = item.OwnInventory?.Items; + if (containedItems != null && containedItems.Count(i => i != null) <= 1) { return false; } //get the amount of heat we'd generate if the fission rate was at the low end of the optimal range float minimumHeat = GetGeneratedHeat(optimalFissionRate.X); @@ -516,12 +530,12 @@ namespace Barotrauma.Items.Components fireTimer = 0.0f; meltDownTimer = 0.0f; - var containedItems = item.ContainedItems; + var containedItems = item.OwnInventory?.Items; if (containedItems != null) { foreach (Item containedItem in containedItems) { - if (containedItem == null) continue; + if (containedItem == null) { continue; } containedItem.Condition = 0.0f; } } @@ -583,15 +597,19 @@ namespace Barotrauma.Items.Components else if (TooMuchFuel()) { var container = item.GetComponent(); - foreach (Item item in item.ContainedItems) + var containedItems = item.OwnInventory?.Items; + if (containedItems != null) { - if (item != null && container.ContainableItems.Any(ri => ri.MatchesItem(item))) + foreach (Item item in containedItems) { - if (!character.Inventory.TryPutItem(item, character, allowedSlots: item.AllowedSlots)) + if (item != null && container.ContainableItems.Any(ri => ri.MatchesItem(item))) { - item.Drop(character); + if (!character.Inventory.TryPutItem(item, character, allowedSlots: item.AllowedSlots)) + { + item.Drop(character); + } + break; } - break; } } } @@ -599,7 +617,7 @@ namespace Barotrauma.Items.Components if (objective.Override) { - if (lastUser != null && lastUser != character && lastUser != lastAIUser) + if (lastUser != null && lastUser != character && lastUser != LastAIUser) { if (lastUser.SelectedConstruction == item) { @@ -607,8 +625,12 @@ namespace Barotrauma.Items.Components } } } + else if (LastUserWasPlayer) + { + return true; + } - LastUser = lastAIUser = character; + LastUser = LastAIUser = character; bool prevAutoTemp = autoTemp; bool prevPowerOn = _powerOn; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index d828f30fb..2576335f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -286,7 +286,8 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, null); float userSkill = 0.0f; - if (user != null && (user.SelectedConstruction == item || item.linkedTo.Contains(user.SelectedConstruction))) + if (user != null && controlledSub != null && + (user.SelectedConstruction == item || item.linkedTo.Contains(user.SelectedConstruction))) { userSkill = user.GetSkillLevel("helm") / 100.0f; } @@ -298,7 +299,9 @@ namespace Barotrauma.Items.Components } else { - if (user != null && user.Info != null && user.SelectedConstruction == item) + if (user != null && user.Info != null && + user.SelectedConstruction == item && + controlledSub != null && controlledSub.Velocity.LengthSquared() > 0.01f) { IncreaseSkillLevel(user, deltaTime); } @@ -320,13 +323,13 @@ namespace Barotrauma.Items.Components } } } - - item.SendSignal(0, targetVelocity.X.ToString(CultureInfo.InvariantCulture), "velocity_x_out", null); + + item.SendSignal(0, targetVelocity.X.ToString(CultureInfo.InvariantCulture), "velocity_x_out", user); float targetLevel = -targetVelocity.Y; targetLevel += (neutralBallastLevel - 0.5f) * 100.0f; - item.SendSignal(0, targetLevel.ToString(CultureInfo.InvariantCulture), "velocity_y_out", null); + item.SendSignal(0, targetLevel.ToString(CultureInfo.InvariantCulture), "velocity_y_out", user); } private void IncreaseSkillLevel(Character user, float deltaTime) @@ -335,12 +338,11 @@ namespace Barotrauma.Items.Components // Do not increase the helm skill when "steering" the sub in an outpost level if (GameMain.GameSession?.Campaign != null && Level.IsLoadedOutpost) { return; } - float userSkill = user.GetSkillLevel("helm") / 100.0f; + float userSkill = Math.Max(user.GetSkillLevel("helm"), 1.0f) / 100.0f; user.Info.IncreaseSkillLevel( "helm", - SkillSettings.Current.SkillIncreasePerSecondWhenSteering / Math.Max(userSkill, 1.0f) * deltaTime, + SkillSettings.Current.SkillIncreasePerSecondWhenSteering / userSkill * deltaTime, user.WorldPosition + Vector2.UnitY * 150.0f); - } private void UpdateAutoPilot(float deltaTime) @@ -599,6 +601,7 @@ namespace Barotrauma.Items.Components } break; case "navigateback": + if (Level.IsLoadedOutpost) { break; } if (DockingSources.Any(d => d.Docked)) { item.SendSignal(0, "1", "toggle_docking", sender: null); @@ -613,6 +616,7 @@ namespace Barotrauma.Items.Components } break; case "navigatetodestination": + if (Level.IsLoadedOutpost) { break; } if (DockingSources.Any(d => d.Docked)) { item.SendSignal(0, "1", "toggle_docking", sender: null); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs new file mode 100644 index 000000000..14bd26e93 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs @@ -0,0 +1,314 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Xml.Linq; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Items.Components +{ + internal enum PlantItemType + { + Seed, + Fertilizer + } + + internal readonly struct SuitablePlantItem + { + public readonly Item? Item; + public readonly PlantItemType Type; + public readonly string ProgressBarMessage; + + public SuitablePlantItem(Item item, PlantItemType type, string progressBarMessage) + { + Item = item; + Type = type; + ProgressBarMessage = progressBarMessage; + } + + public bool IsNull() => Item == null; + } + + internal struct PlantSlot + { + public Vector2 Offset; + public float Size; + + public PlantSlot(XElement element) + { + Offset = element.GetAttributeVector2("offset", Vector2.Zero); + Size = element.GetAttributeFloat("size", 0.5f); + } + + public PlantSlot(Vector2 offset, float size) + { + Offset = offset; + Size = size; + } + } + + internal partial class Planter : Pickable, IDrawableComponent + { + public static readonly PlantSlot NullSlot = new PlantSlot(); + public readonly Dictionary PlantSlots = new Dictionary(); + + private static readonly SuitablePlantItem NullItem = new SuitablePlantItem(); + private const string MsgFertilizer = "ItemMsgAddFertilizer"; + private const string MsgSeed = "ItemMsgPlantSeed"; + private const string MsgHarvest = "ItemMsgHarvest"; + private const string MsgUprooting = "progressbar.uprooting"; + private const string MsgFertilizing = "progressbar.fertilizing"; + private const string MsgPlanting = "progressbar.planting"; + public static float GrowthTickDelay = 1f; // 1 second + + private float fertilizer; + + [Serialize(0f, true, "How much fertilizer the planter has.")] + public float Fertilizer + { + get => fertilizer; + set => fertilizer = Math.Clamp(value, 0, FertilizerCapacity); + } + + [Serialize(100f, true, "How much fertilizer can the planter hold.")] + public float FertilizerCapacity { get; set; } + + public Growable?[] GrowableSeeds = new Growable?[0]; + + private readonly List SuitableFertilizer = new List(); + private readonly List SuitableSeeds = new List(); + private ItemContainer? container; + private float growthTickTimer; + + public Planter(Item item, XElement element) : base(item, element) + { + canBePicked = true; + SerializableProperty.DeserializeProperties(this, element); + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "plantslot": + PlantSlots.Add(subElement.GetAttributeInt("slot", 0), new PlantSlot(subElement)); + break; + case "suitablefertilizer": + SuitableFertilizer.Add(RelatedItem.Load(subElement, true, item.Name)); + break; + case "suitableseed": + SuitableSeeds.Add(RelatedItem.Load(subElement, true, item.Name)); + break; + } + } + } + + public override void OnItemLoaded() + { + base.OnItemLoaded(); + IsActive = true; +#if CLIENT + lightComponent = item.GetComponent(); + if (lightComponent != null) + { + lightComponent.Light.Enabled = false; + } +#endif + container = item.GetComponent(); + GrowableSeeds = new Growable[container.Capacity]; + } + + public override bool HasRequiredItems(Character character, bool addMessage, string msg = null) + { + if (container?.Inventory == null) { return false; } + + SuitablePlantItem plantItem = GetSuitableItem(character); + + if (!plantItem.IsNull()) + { + Msg = plantItem.Type switch + { + PlantItemType.Seed => MsgSeed, + PlantItemType.Fertilizer => MsgFertilizer, + _ => throw new ArgumentOutOfRangeException() + }; + ParseMsg(); + return true; + } + + if (HasAnyFinishedGrowing()) + { + Msg = MsgHarvest; + ParseMsg(); + return true; + } + + Msg = string.Empty; + ParseMsg(); + return false; + } + + public override bool Pick(Character character) + { + SuitablePlantItem plantItem = GetSuitableItem(character); + PickingMsg = plantItem.IsNull() ? MsgUprooting : plantItem.ProgressBarMessage; + + return base.Pick(character); + } + + public override bool OnPicked(Character character) + { + if (container?.Inventory == null) { return false; } + + SuitablePlantItem plantItem = GetSuitableItem(character); + if (plantItem.IsNull()) + { + return TryHarvest(character); + } + + switch (plantItem.Type) + { + case PlantItemType.Seed: + return container.Inventory.TryPutItem(plantItem.Item, character, new List { InvSlotType.Any }); + case PlantItemType.Fertilizer when plantItem.Item != null: + float canAdd = FertilizerCapacity - Fertilizer; + float maxAvailable = plantItem.Item.Condition; + float toAdd = Math.Min(canAdd, maxAvailable); + plantItem.Item.Condition -= toAdd; + fertilizer += toAdd; +#if CLIENT + character.UpdateHUDProgressBar(this, Item.DrawPosition, Fertilizer / FertilizerCapacity, Color.SaddleBrown, Color.SaddleBrown, "entityname.fertilizer"); +#endif + return false; + } + + return false; + } + + /// + /// Attempts to harvest a fully grown plant or removes a decayed plant if any + /// + /// The character who gets the produce or null if they should drop on the floor. + /// + private bool TryHarvest(Character? character) + { + Debug.Assert(container != null, "Tried to harvest a planter without an item container."); + + for (var i = 0; i < GrowableSeeds.Length; i++) + { + Growable? seed = GrowableSeeds[i]; + if (seed == null) { continue; } + + if (seed.Decayed || seed.FullyGrown) + { + container?.Inventory.RemoveItem(seed.Item); + GrowableSeeds[i] = null; + return true; + } + } + + return false; + } + + public override void Update(float deltaTime, Camera cam) + { + base.Update(deltaTime, cam); + +#if CLIENT + if (lightComponent != null) + { + bool hasSeed = false; + foreach (Growable? seed in GrowableSeeds) { hasSeed |= seed != null; } + + lightComponent.Light.Enabled = hasSeed; + } +#endif + + if (container?.Inventory == null) { return; } + + for (var i = 0; i < container.Inventory.Items.Length; i++) + { + if (i < 0 || GrowableSeeds.Length <= i) { continue; } + + Item containedItem = container.Inventory.Items[i]; + + Growable? growable = containedItem?.GetComponent(); + + if (growable != null) + { + GrowableSeeds[i] = growable; + growable.IsActive = true; + } + else + { + if (GrowableSeeds[i] is { } oldGrowable) + { + // Kill the plant if it's somehow removed + oldGrowable.Decayed = true; + oldGrowable.IsActive = false; + } + + GrowableSeeds[i] = null; + } + } + + // server handles this + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + + float delay = GrowthTickDelay; + if (Fertilizer > 0) + { + delay /= 2f; + Fertilizer -= deltaTime / 10f; + } + + if (growthTickTimer > delay) + { + for (var i = 0; i < GrowableSeeds.Length; i++) + { + PlantSlot slot = PlantSlots.ContainsKey(i) ? PlantSlots[i] : NullSlot; + Growable? seed = GrowableSeeds[i]; + seed?.OnGrowthTick(this, slot); + } + + growthTickTimer = 0; + } + else if (Item.ParentInventory == null) + { + if (item.GetComponent() is { } holdable) + { + if (holdable.Attachable && !holdable.Attached) + { + return; + } + } + + growthTickTimer += deltaTime; + } + } + + private SuitablePlantItem GetSuitableItem(Character character) + { + foreach (Item heldItem in character.SelectedItems) + { + if (heldItem == null) { continue; } + + if (container?.Inventory != null && !container.Inventory.IsFull()) + { + if (heldItem.GetComponent() != null && SuitableSeeds.Any(ri => ri.MatchesItem(heldItem))) + { + return new SuitablePlantItem(heldItem, PlantItemType.Seed, MsgPlanting); + } + } + + if (SuitableFertilizer.Any(ri => ri.MatchesItem(heldItem))) + { + return new SuitablePlantItem(heldItem, PlantItemType.Fertilizer, MsgFertilizing); + } + } + + return NullItem; + } + + private bool HasAnyFinishedGrowing() => GrowableSeeds.Any(seed => seed != null && (seed.FullyGrown || seed.Decayed)); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 74fc27815..b73217ed2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -121,7 +121,6 @@ namespace Barotrauma.Items.Components : base(item, element) { IsActive = true; - InitProjSpecific(); } @@ -184,23 +183,25 @@ namespace Barotrauma.Items.Components charge = 0.0f; return; } - - //output starts dropping when the charge is less than 10% - float maxOutputRatio = 1.0f; - if (chargeRatio < 0.1f) + else { - maxOutputRatio = Math.Max(chargeRatio * 10.0f, 0.0f); + //output starts dropping when the charge is less than 10% + float maxOutputRatio = 1.0f; + if (chargeRatio < 0.1f) + { + maxOutputRatio = Math.Max(chargeRatio * 10.0f, 0.0f); + } + + CurrPowerOutput += (gridLoad - gridPower) * deltaTime; + + float maxOutput = Math.Min(MaxOutPut * maxOutputRatio, gridLoad); + CurrPowerOutput = MathHelper.Clamp(CurrPowerOutput, 0.0f, maxOutput); + Charge -= CurrPowerOutput / 3600.0f; } - CurrPowerOutput += (gridLoad - gridPower) * deltaTime; - - float maxOutput = Math.Min(MaxOutPut * maxOutputRatio, gridLoad); - CurrPowerOutput = MathHelper.Clamp(CurrPowerOutput, 0.0f, maxOutput); - Charge -= CurrPowerOutput / 3600.0f; - item.SendSignal(0, ((int)Math.Round(Charge)).ToString(), "charge", null); - item.SendSignal(0, ((int)Math.Round((Charge / capacity) * 100)).ToString(), "charge_%", null); - item.SendSignal(0, ((int)Math.Round((RechargeSpeed / maxRechargeSpeed) * 100)).ToString(), "charge_rate", null); + item.SendSignal(0, ((int)Math.Round(Charge / capacity * 100)).ToString(), "charge_%", null); + item.SendSignal(0, ((int)Math.Round(RechargeSpeed / maxRechargeSpeed * 100)).ToString(), "charge_rate", null); } public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index a01f75343..a356035b3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -113,6 +113,19 @@ namespace Barotrauma.Items.Components powerLoad = 0.0f; currPowerConsumption = 0.0f; SetAllConnectionsDirty(); + foreach (HashSet recipientList in connectedRecipients.Values.ToList()) + { + foreach (Connection c in recipientList) + { + if (c.Item == item) { continue; } + var recipientPowerTransfer = c.Item.GetComponent(); + if (recipientPowerTransfer != null) + { + recipientPowerTransfer.SetAllConnectionsDirty(); + recipientPowerTransfer.RefreshConnections(); + } + } + } RefreshConnections(); isBroken = true; } @@ -185,29 +198,32 @@ namespace Barotrauma.Items.Components else if (!connectionDirty[c]) { continue; - } - - HashSet connected = new HashSet(); - if (!connectedRecipients.ContainsKey(c)) - { - connectedRecipients.Add(c, connected); - } - else - { - //mark all previous recipients as dirty - foreach (Connection recipient in connectedRecipients[c]) - { - var pt = recipient.Item.GetComponent(); - if (pt != null) pt.connectionDirty[recipient] = true; - } - } + } //find all connections that are connected to this one (directly or via another PowerTransfer) - connected.Add(c); - GetConnected(c, connected); + HashSet connected = new HashSet(); + if (item.Condition > 0.0f) + { + if (!connectedRecipients.ContainsKey(c)) + { + connectedRecipients.Add(c, connected); + } + else + { + //mark all previous recipients as dirty + foreach (Connection recipient in connectedRecipients[c]) + { + var pt = recipient.Item.GetComponent(); + if (pt != null) pt.connectionDirty[recipient] = true; + } + } + + connected.Add(c); + GetConnected(c, connected); + } connectedRecipients[c] = connected; - //go through all the PowerTransfers and we're connected to and set their connections to match the ones we just calculated + //go through all the PowerTransfers that we're connected to and set their connections to match the ones we just calculated //(no need to go through the recursive GetConnected method again) foreach (Connection recipient in connected) { @@ -232,10 +248,10 @@ namespace Barotrauma.Items.Components foreach (Connection recipient in recipients) { - if (recipient == null || connected.Contains(recipient)) continue; + if (recipient == null || connected.Contains(recipient)) { continue; } Item it = recipient.Item; - if (it == null || it.Condition <= 0.0f) continue; + if (it == null || it.Condition <= 0.0f) { continue; } connected.Add(recipient); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index b26795df8..9bff25e5f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -131,7 +131,7 @@ namespace Barotrauma.Items.Components { if (!powerOnSoundPlayed && powerOnSound != null) { - SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, item.CurrentHull); + SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, hullGuess: item.CurrentHull); powerOnSoundPlayed = true; } } @@ -250,7 +250,7 @@ namespace Barotrauma.Items.Components } if (powered is PowerContainer pc) { - if (pc.CurrPowerOutput <= 0.0f) { continue; } + if (pc.CurrPowerOutput <= 0.0f || pc.item.Condition <= 0.0f) { continue; } //providing power lastPowerProbeRecipients.Clear(); powered.powerOut?.SendPowerProbeSignal(powered.item, pc.CurrPowerOutput); @@ -282,7 +282,7 @@ namespace Barotrauma.Items.Components continue; } var pc = powerSource.Item.GetComponent(); - if (pc != null) + if (pc != null && pc.item.Condition > 0.0f) { float voltage = pc.CurrPowerOutput / Math.Max(powered.CurrPowerConsumption, 1.0f); powered.voltage += voltage; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 87a2f11bd..fed67f5c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -407,6 +407,16 @@ namespace Barotrauma.Items.Components return hits; } + public override void Drop(Character dropper) + { + if (dropper != null) + { + Deactivate(); + Unstick(); + } + base.Drop(dropper); + } + public override void Update(float deltaTime, Camera cam) { ApplyStatusEffects(ActionType.OnActive, deltaTime, null); @@ -422,10 +432,13 @@ namespace Barotrauma.Items.Components if (item.body.LinearVelocity.LengthSquared() < ContinuousCollisionThreshold * ContinuousCollisionThreshold) { item.body.FarseerBody.IsBullet = false; - //projectiles with a stickjoint don't become inactive until the stickjoint is detached - if (stickJoint == null) { IsActive = false; } } } + //projectiles with a stickjoint don't become inactive until the stickjoint is detached + if (stickJoint == null && !item.body.FarseerBody.IsBullet) + { + IsActive = false; + } if (stickJoint == null) { return; } @@ -511,9 +524,10 @@ namespace Barotrauma.Items.Components hits.Add(target.Body); impactQueue.Enqueue(new Impact(target, contact.Manifold.LocalNormal, item.body.LinearVelocity)); - if (hits.Count() >= MaxTargetsToHit) + IsActive = true; + if (hits.Count() >= MaxTargetsToHit || target.Body.UserData is VoronoiCell) { - item.body.FarseerBody.OnCollision -= OnProjectileCollision; + Deactivate(); return true; } else @@ -626,20 +640,9 @@ namespace Barotrauma.Items.Components target.Body.ApplyLinearImpulse(velocity * item.body.Mass); - if (hits.Count() >= MaxTargetsToHit) + if (hits.Count() >= MaxTargetsToHit || hits.LastOrDefault()?.UserData is VoronoiCell) { - item.body.FarseerBody.OnCollision -= OnProjectileCollision; - if ((item.Prefab.DamagedByProjectiles || item.Prefab.DamagedByMeleeWeapons) && item.Condition > 0) - { - item.body.CollisionCategories = Physics.CollisionCharacter; - item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile; - } - else - { - item.body.CollisionCategories = Physics.CollisionItem; - item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; - } - IgnoredBodies.Clear(); + Deactivate(); } if (attackResult.AppliedDamageModifiers != null && @@ -684,11 +687,12 @@ namespace Barotrauma.Items.Components item.body.LinearVelocity *= 0.5f; } - var containedItems = item.ContainedItems; + var containedItems = item.OwnInventory?.Items; if (containedItems != null) { foreach (Item contained in containedItems) { + if (contained == null) { continue; } if (contained.body != null) { contained.SetTransform(item.SimPosition, contained.body.Rotation); @@ -704,6 +708,22 @@ namespace Barotrauma.Items.Components return true; } + private void Deactivate() + { + item.body.FarseerBody.OnCollision -= OnProjectileCollision; + if ((item.Prefab.DamagedByProjectiles || item.Prefab.DamagedByMeleeWeapons) && item.Condition > 0) + { + item.body.CollisionCategories = Physics.CollisionCharacter; + item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile; + } + else + { + item.body.CollisionCategories = Physics.CollisionItem; + item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; + } + IgnoredBodies.Clear(); + } + private void StickToTarget(Body targetBody, Vector2 axis) { if (stickJoint != null) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 7b6371298..1f32402a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -206,9 +206,12 @@ namespace Barotrauma.Items.Components { if (!CheckCharacterSuccess(character)) { + GameServer.Log($"{GameServer.CharacterLogName(character)} failed to {(action == FixActions.Sabotage ? "sabotage" : "repair")} {item.Name}", ServerLog.MessageType.ItemInteraction); GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, character.ID }); return false; } + + GameServer.Log($"{GameServer.CharacterLogName(character)} started {(action == FixActions.Sabotage ? "sabotaging" : "repairing")} {item.Name}", ServerLog.MessageType.ItemInteraction); item.CreateServerEvent(this); } #else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs index b1c4a63b2..ab81a6d70 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs @@ -62,7 +62,10 @@ namespace Barotrauma.Items.Components timeSinceReceived[i] += deltaTime; } float output = Calculate(receivedSignal[0], receivedSignal[1]); - item.SendSignal(0, MathHelper.Clamp(output, ClampMin, ClampMax).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + if (MathUtils.IsValid(output)) + { + item.SendSignal(0, MathHelper.Clamp(output, ClampMin, ClampMax).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + } } protected abstract float Calculate(float signal1, float signal2); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs new file mode 100644 index 000000000..ab56fdf18 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs @@ -0,0 +1,17 @@ +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + class ConcatComponent : StringComponent + { + public ConcatComponent(Item item, XElement element) + : base(item, element) + { + } + + protected override string Calculate(string signal1, string signal2) + { + return signal1 + signal2; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DivideComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DivideComponent.cs index f15e523bf..e4efa15d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DivideComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DivideComponent.cs @@ -1,6 +1,4 @@ -using System; -using System.Globalization; -using System.Xml.Linq; +using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -13,6 +11,7 @@ namespace Barotrauma.Items.Components protected override float Calculate(float signal1, float signal2) { + if (MathUtils.NearlyEqual(signal2, 0)) { return float.NaN; } return signal1 / signal2; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs index 2c529781e..e1f02b23f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs @@ -56,8 +56,10 @@ namespace Barotrauma.Items.Components item.SendSignal(0, Math.Abs(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); break; case FunctionType.SquareRoot: - double square = value > 0 ? Math.Sqrt(value) : 0; - item.SendSignal(0, square.ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + if (value > 0) + { + item.SendSignal(0, Math.Sqrt(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + } break; default: throw new NotImplementedException($"Function {Function} has not been implemented."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index 7b25ed742..30d7419b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -208,7 +208,8 @@ namespace Barotrauma.Items.Components else { #if CLIENT - light.Rotation = -Rotation; + light.Rotation = -Rotation - MathHelper.ToRadians(item.Rotation); + light.LightSpriteEffect = item.SpriteEffects; #endif } @@ -265,11 +266,14 @@ namespace Barotrauma.Items.Components switch (connection.Name) { case "toggle": - if (!IgnoreContinuousToggle || lastToggleSignalTime < Timing.TotalTime - 0.1) + if (signal != "0") { - IsOn = !IsOn; + if (!IgnoreContinuousToggle || lastToggleSignalTime < Timing.TotalTime - 0.1) + { + IsOn = !IsOn; + } + lastToggleSignalTime = Timing.TotalTime; } - lastToggleSignalTime = Timing.TotalTime; break; case "set_state": IsOn = signal != "0"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs index 0fd1c5a51..3fbf67ac8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs @@ -1,14 +1,24 @@ -using System.Xml.Linq; +using Barotrauma.Networking; +using System.Xml.Linq; namespace Barotrauma.Items.Components { - class MemoryComponent : ItemComponent + partial class MemoryComponent : ItemComponent, IServerSerializable { + const int MaxValueLength = 256; + + + private string value; + [InGameEditable, Serialize("", true, description: "The currently stored signal the item outputs.", alwaysUseInstanceValues: true)] public string Value { - get; - set; + get { return value; } + set + { + if (value == null) { return; } + this.value = value.Length <= MaxValueLength ? value : value.Substring(0, MaxValueLength); + } } protected bool writeable = true; @@ -24,15 +34,22 @@ namespace Barotrauma.Items.Components item.SendSignal(0, Value, "signal_out", null); } + partial void OnStateChanged(); + public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) { switch (connection.Name) { case "signal_in": - if (writeable) { Value = signal; } + if (writeable) + { + if (Value == signal) { return; } + Value = signal; + OnStateChanged(); + } break; case "signal_store": - writeable = (signal == "1"); + writeable = signal == "1"; break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs new file mode 100644 index 000000000..875705036 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs @@ -0,0 +1,72 @@ +using Microsoft.Xna.Framework; +using System; +using System.Globalization; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + abstract class StringComponent : ItemComponent + { + //an array to keep track of how long ago a signal was received on both inputs + protected float[] timeSinceReceived; + + protected string[] receivedSignal; + + //the output is sent if both inputs have received a signal within the timeframe + protected float timeFrame; + + + [InGameEditable(DecimalCount = 2), + Serialize(0.0f, true, description: "The item must have received signals to both inputs within this timeframe to output the result." + + " If set to 0, the inputs must be received at the same time.", alwaysUseInstanceValues: true)] + public float TimeFrame + { + get { return timeFrame; } + set + { + timeFrame = Math.Max(0.0f, value); + } + } + + public StringComponent(Item item, XElement element) + : base(item, element) + { + timeSinceReceived = new float[] { Math.Max(timeFrame * 2.0f, 0.1f), Math.Max(timeFrame * 2.0f, 0.1f) }; + receivedSignal = new string[2]; + } + + sealed public override void Update(float deltaTime, Camera cam) + { + for (int i = 0; i < timeSinceReceived.Length; i++) + { + if (timeSinceReceived[i] > timeFrame) + { + IsActive = false; + return; + } + timeSinceReceived[i] += deltaTime; + } + string output = Calculate(receivedSignal[0], receivedSignal[1]); + item.SendSignal(0, output, "signal_out", null); + } + + protected abstract string Calculate(string signal1, string signal2); + + public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + { + switch (connection.Name) + { + case "signal_in1": + receivedSignal[0] = signal; + timeSinceReceived[0] = 0.0f; + IsActive = true; + break; + case "signal_in2": + receivedSignal[1] = signal; + timeSinceReceived[1] = 0.0f; + IsActive = true; + break; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs index 640a7f624..6b0953bc2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs @@ -62,9 +62,15 @@ namespace Barotrauma.Items.Components break; case FunctionType.Tan: if (!UseRadians) { value = MathHelper.ToRadians(value); } - item.SendSignal(0, ((float)Math.Tan(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + //tan is undefined if the value is (π / 2) + πk, where k is any integer + if (!MathUtils.NearlyEqual(value % MathHelper.Pi, MathHelper.PiOver2)) + { + item.SendSignal(0, ((float)Math.Tan(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + } break; case FunctionType.Asin: + //asin is only defined in the range [-1,1] + if (value >= -1.0f && value <= 1.0f) { float angle = (float)Math.Asin(value); if (!UseRadians) { angle = MathHelper.ToDegrees(angle); } @@ -72,6 +78,8 @@ namespace Barotrauma.Items.Components } break; case FunctionType.Acos: + //acos is only defined in the range [-1,1] + if (value >= -1.0f && value <= 1.0f) { float angle = (float)Math.Acos(value); if (!UseRadians) { angle = MathHelper.ToDegrees(angle); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index 7f9a670a9..f3f8911f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -12,6 +12,8 @@ namespace Barotrauma.Items.Components { private static readonly List list = new List(); + const int ChannelMemorySize = 10; + private float range; private int channel; @@ -20,6 +22,8 @@ namespace Barotrauma.Items.Components private string prevSignal; + private int[] channelMemory = new int[ChannelMemorySize]; + [Serialize(Character.TeamType.None, true, description: "WiFi components can only communicate with components that have the same Team ID.", alwaysUseInstanceValues: true)] public Character.TeamType TeamID { get; set; } @@ -36,7 +40,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize(1, true, description: "WiFi components can only communicate with components that use the same channel.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(0, true, description: "WiFi components can only communicate with components that use the same channel.", alwaysUseInstanceValues: true)] public int Channel { get { return channel; } @@ -83,6 +87,18 @@ namespace Barotrauma.Items.Components { list.Add(this); IsActive = true; + channelMemory = element.GetAttributeIntArray("channelmemory", new int[ChannelMemorySize]); + } + + public override void OnItemLoaded() + { + if (channelMemory.All(m => m == 0)) + { + for (int i = 0; i < channelMemory.Length; i++) + { + channelMemory[i] = i; + } + } } public bool CanTransmit() @@ -118,6 +134,24 @@ namespace Barotrauma.Items.Components } } + public int GetChannelMemory(int index) + { + if (index < 0 || index >= ChannelMemorySize) + { + return 0; + } + return channelMemory[index]; + } + + public void SetChannelMemory(int index, int value) + { + if (index < 0 || index >= ChannelMemorySize) + { + return; + } + channelMemory[index] = MathHelper.Clamp(value, 0, 10000); + } + public void TransmitSignal(int stepsTaken, string signal, Item source, Character sender, bool sendToChat, float signalStrength = 1.0f) { var senderComponent = source?.GetComponent(); @@ -220,5 +254,12 @@ namespace Barotrauma.Items.Components base.RemoveComponentSpecific(); list.Remove(this); } + + public override XElement Save(XElement parentElement) + { + var element = base.Save(parentElement); + element.Add(new XAttribute("channelmemory", string.Join(',', channelMemory))); + return element; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index f16568621..a84973c9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -87,7 +87,14 @@ namespace Barotrauma.Items.Components get; set; } - + + [Serialize(false, false, description: "If enabled, the wire will not be visible in connection panels outside the submarine editor.")] + public bool HiddenInGame + { + get; + set; + } + public Wire(Item item, XElement element) : base(item, element) { @@ -673,12 +680,13 @@ namespace Barotrauma.Items.Components closestDist = 0.0f; int closestIndex = -1; + maxDist *= maxDist; for (int i = 0; i < nodes.Count-1; i++) { if ((Math.Abs(nodes[i].X - nodes[i + 1].X)<5 || Math.Sign(mousePos.X - nodes[i].X) != Math.Sign(mousePos.X - nodes[i + 1].X)) && (Math.Abs(nodes[i].Y - nodes[i + 1].Y)<5 || Math.Sign(mousePos.Y - nodes[i].Y) != Math.Sign(mousePos.Y - nodes[i + 1].Y))) { - float dist = MathUtils.LineToPointDistance(nodes[i], nodes[i + 1], mousePos); + float dist = MathUtils.LineToPointDistanceSquared(nodes[i], nodes[i + 1], mousePos); if (dist > maxDist) continue; if (closestIndex == -1 || dist < closestDist) @@ -688,12 +696,15 @@ namespace Barotrauma.Items.Components } } } + closestDist = (float)Math.Sqrt(closestDist); return closestIndex; } public override void FlipX(bool relativeToSub) { + if (item.ParentInventory != null) { return; } + Vector2 refPos = item.Submarine == null ? Vector2.Zero : item.Position - item.Submarine.HiddenSubPosition; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index d9ed8f3ab..e56591c94 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -198,14 +198,14 @@ namespace Barotrauma.Items.Components private set; } - private float baseRotationRad; - [Editable(0.0f, 360.0f), Serialize(0.0f, true, description: "The angle of the turret's base in degrees.", alwaysUseInstanceValues: true)] + float prevBaseRotation; + [Serialize(0.0f, true, description: "The angle of the turret's base in degrees.", alwaysUseInstanceValues: true)] public float BaseRotation { - get { return MathHelper.ToDegrees(baseRotationRad); } + get { return item.Rotation; } set { - baseRotationRad = MathHelper.ToRadians(value); + item.Rotation = value; UpdateTransformedBarrelPos(); } } @@ -250,14 +250,15 @@ namespace Barotrauma.Items.Components private void UpdateTransformedBarrelPos() { - float flippedRotation = BaseRotation; - if (item.FlippedX) flippedRotation = -flippedRotation; + float flippedRotation = item.Rotation; + if (item.FlippedX) { flippedRotation = -flippedRotation; } //if (item.FlippedY) flippedRotation = 180.0f - flippedRotation; transformedBarrelPos = MathUtils.RotatePointAroundTarget(barrelPos * item.Scale, new Vector2(item.Rect.Width / 2, item.Rect.Height / 2), flippedRotation); #if CLIENT item.ResetCachedVisibleSize(); - item.SpriteRotation = MathHelper.ToRadians(flippedRotation); #endif + item.Rotation = flippedRotation; + prevBaseRotation = item.Rotation; } public override void OnItemLoaded() @@ -271,7 +272,7 @@ namespace Barotrauma.Items.Components if (lightComponent != null) { lightComponent.Parent = null; - lightComponent.Rotation = rotation; + lightComponent.Rotation = Rotation - MathHelper.ToRadians(item.Rotation); lightComponent.Light.Rotation = -rotation; } #endif @@ -283,6 +284,10 @@ namespace Barotrauma.Items.Components this.cam = cam; if (reload > 0.0f) { reload -= deltaTime; } + if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation)) + { + UpdateTransformedBarrelPos(); + } if (user != null && user.Removed) { @@ -344,7 +349,7 @@ namespace Barotrauma.Items.Components if (lightComponent != null) { - lightComponent.Rotation = rotation; + lightComponent.Rotation = Rotation - MathHelper.ToRadians(item.Rotation); } } @@ -504,6 +509,15 @@ namespace Barotrauma.Items.Components projectileComponent.Use((float)Timing.Step); projectile.GetComponent()?.Attach(item, projectile); projectileComponent.User = user; + + if (item.Submarine != null && projectile.body != null) + { + Vector2 velocitySum = item.Submarine.PhysicsBody.LinearVelocity + projectile.body.LinearVelocity; + if (velocitySum.LengthSquared() < NetConfig.MaxPhysicsBodyVelocity * NetConfig.MaxPhysicsBodyVelocity * 0.9f) + { + projectile.body.LinearVelocity = velocitySum; + } + } } if (projectile.Container != null) { projectile.Container.RemoveContained(projectile); } @@ -983,26 +997,8 @@ namespace Barotrauma.Items.Components public override void FlipY(bool relativeToSub) { - baseRotationRad = MathUtils.WrapAngleTwoPi(baseRotationRad - MathHelper.Pi); + BaseRotation = MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(MathHelper.ToRadians(BaseRotation - 180))); UpdateTransformedBarrelPos(); - - /*minRotation = -minRotation; - maxRotation = -maxRotation; - - var temp = minRotation; - minRotation = maxRotation; - maxRotation = temp; - - barrelPos.Y = item.Rect.Height / item.Scale - barrelPos.Y; - - while (minRotation < 0) - { - minRotation += MathHelper.TwoPi; - maxRotation += MathHelper.TwoPi; - } - rotation = (minRotation + maxRotation) / 2; - - UpdateTransformedBarrelPos();*/ } public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power, float signalStrength = 1.0f) @@ -1012,6 +1008,7 @@ namespace Barotrauma.Items.Components case "position_in": if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newRotation)) { + if (!MathUtils.IsValid(newRotation)) { return; } targetRotation = MathHelper.ToRadians(newRotation); IsActive = true; } @@ -1030,11 +1027,17 @@ namespace Barotrauma.Items.Components } break; case "toggle_light": - if (lightComponent != null) + if (lightComponent != null && signal != "0") { lightComponent.IsOn = !lightComponent.IsOn; } break; + case "set_light": + if (lightComponent != null) + { + lightComponent.IsOn = signal != "0"; + } + break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index e123830ce..20265e1e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -237,7 +237,7 @@ namespace Barotrauma #endif if (this is CharacterInventory) { - if (prevInventory != this) + if (prevInventory != this && prevOwnerInventory != this) { HumanAIController.ItemTaken(item, user); } @@ -405,7 +405,7 @@ namespace Barotrauma if (item == null) { continue; } if (item.OwnInventory != null) { - match = item.OwnInventory.FindItem(predicate, true); + match = item.OwnInventory.FindItem(predicate, recursive: true); if (match != null) { return match; @@ -416,6 +416,27 @@ namespace Barotrauma return match; } + public List FindAllItems(Func predicate, bool recursive = false, List list = null) + { + list ??= new List(); + foreach (var item in Items) + { + if (item == null) { continue; } + if (predicate(item)) + { + list.Add(item); + } + if (recursive) + { + if (item.OwnInventory != null) + { + item.OwnInventory.FindAllItems(predicate, recursive: true, list); + } + } + } + return list; + } + public Item FindItemByTag(string tag, bool recursive = false) { if (tag == null) { return null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 1b9df8c1f..9d89db215 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -167,19 +167,19 @@ namespace Barotrauma set; } - private float rotation; + private float rotationRad; [Editable(0.0f, 360.0f, DecimalCount = 1, ValueStep = 1f), Serialize(0.0f, true)] public float Rotation { get { - return MathHelper.ToDegrees(rotation); + return MathHelper.ToDegrees(rotationRad); } set { if (!Prefab.AllowRotatingInEditor) { return; } - rotation = MathHelper.ToRadians(value); + rotationRad = MathHelper.ToRadians(value); } } @@ -734,6 +734,7 @@ namespace Barotrauma case "preferredcontainer": case "upgrademodule": case "upgradeoverride": + case "minimapicon": break; case "staticbody": StaticBodyConfig = subElement; @@ -1070,6 +1071,12 @@ namespace Barotrauma public void Move(Vector2 amount, bool ignoreContacts) { + if (!MathUtils.IsValid(amount)) + { + DebugConsole.ThrowError($"Attempted to move an item by an invalid amount ({amount})\n{Environment.StackTrace}"); + return; + } + base.Move(amount); if (ItemList != null && body != null) @@ -1093,18 +1100,25 @@ namespace Barotrauma public Rectangle TransformTrigger(Rectangle trigger, bool world = false) { - return world ? + Rectangle baseRect = world ? WorldRect : Rect; + + Rectangle transformedRect = new Rectangle( - (int)(WorldRect.X + trigger.X * Scale), - (int)(WorldRect.Y + trigger.Y * Scale), - (trigger.Width == 0) ? Rect.Width : (int)(trigger.Width * Scale), - (trigger.Height == 0) ? Rect.Height : (int)(trigger.Height * Scale)) - : - new Rectangle( - (int)(Rect.X + trigger.X * Scale), - (int)(Rect.Y + trigger.Y * Scale), + (int)(baseRect.X + trigger.X * Scale), + (int)(baseRect.Y + trigger.Y * Scale), (trigger.Width == 0) ? Rect.Width : (int)(trigger.Width * Scale), (trigger.Height == 0) ? Rect.Height : (int)(trigger.Height * Scale)); + + if (FlippedX) + { + transformedRect.X = baseRect.X + (baseRect.Right - transformedRect.Right); + } + if (FlippedY) + { + transformedRect.Y = baseRect.Y + ((baseRect.Y - baseRect.Height) - (transformedRect.Y - transformedRect.Height)); + } + + return transformedRect; } /// @@ -1269,12 +1283,12 @@ namespace Barotrauma if (effect.HasTargetType(StatusEffect.TargetType.Contained)) { - var containedItems = ContainedItems; + var containedItems = ownInventory?.Items; if (containedItems != null) { foreach (Item containedItem in containedItems) { - if (containedItem == null) continue; + if (containedItem == null) { continue; } if (effect.TargetIdentifiers != null && !effect.TargetIdentifiers.Contains(containedItem.prefab.Identifier) && !effect.TargetIdentifiers.Any(id => containedItem.HasTag(id))) @@ -1534,10 +1548,12 @@ namespace Barotrauma body.SetTransform(body.SimPosition + prevSub.SimPosition - Submarine.SimPosition, body.Rotation); } - if (Submarine != prevSub && ContainedItems != null) + var containedItems = ownInventory?.Items; + if (Submarine != prevSub && containedItems != null) { foreach (Item containedItem in ContainedItems) { + if (containedItem == null) { continue; } containedItem.Submarine = Submarine; } } @@ -1621,11 +1637,12 @@ namespace Barotrauma #endif } - var containedItems = ContainedItems; + var containedItems = ownInventory?.Items; if (containedItems != null) { foreach (Item contained in containedItems) { + if (contained == null) { continue; } if (contained.body != null) { contained.HandleCollision(impact); } } } @@ -1687,7 +1704,7 @@ namespace Barotrauma } ConnectionPanel connectionPanel = GetComponent(); - if (connectionPanel == null) return connectedComponents; + if (connectionPanel == null) { return connectedComponents; } foreach (Connection c in connectionPanel.Connections) { @@ -1695,7 +1712,10 @@ namespace Barotrauma foreach (Connection recipient in recipients) { var component = recipient.Item.GetComponent(); - if (component != null) connectedComponents.Add(component); + if (component != null && !connectedComponents.Contains(component)) + { + connectedComponents.Add(component); + } } } @@ -1748,7 +1768,7 @@ namespace Barotrauma { if (alreadySearched.Contains(recipient)) { continue; } var component = recipient.Item.GetComponent(); - if (component != null) + if (component != null && !connectedComponents.Contains(component)) { connectedComponents.Add(component); } @@ -2143,8 +2163,6 @@ namespace Barotrauma public void Drop(Character dropper, bool createNetworkEvent = true) { - Inventory prevInventory = parentInventory; - if (createNetworkEvent) { if (parentInventory != null && !parentInventory.Owner.Removed && !Removed && @@ -2311,6 +2329,8 @@ namespace Barotrauma } } + private CoroutineHandle logPropertyChangeCoroutine; + private void ReadPropertyChange(IReadMessage msg, bool inGameEditableOnly, Client sender = null) { var allProperties = inGameEditableOnly ? GetProperties() : GetProperties(); @@ -2336,62 +2356,79 @@ namespace Barotrauma } Type type = property.PropertyType; + string logValue = ""; if (type == typeof(string)) { string val = msg.ReadString(); - if (allowEditing) property.TrySetValue(parentObject, val); + if (allowEditing) + { + property.TrySetValue(parentObject, val); + } } else if (type == typeof(float)) { float val = msg.ReadSingle(); - if (allowEditing) property.TrySetValue(parentObject, val); + logValue = val.ToString("G", CultureInfo.InvariantCulture); + if (allowEditing) { property.TrySetValue(parentObject, val); } } else if (type == typeof(int)) { int val = msg.ReadInt32(); - if (allowEditing) property.TrySetValue(parentObject, val); + logValue = val.ToString(); + if (allowEditing) { property.TrySetValue(parentObject, val); } } else if (type == typeof(bool)) { bool val = msg.ReadBoolean(); - if (allowEditing) property.TrySetValue(parentObject, val); + logValue = val.ToString(); + if (allowEditing) { property.TrySetValue(parentObject, val); } } else if (type == typeof(Color)) { Color val = new Color(msg.ReadByte(), msg.ReadByte(), msg.ReadByte(), msg.ReadByte()); - if (allowEditing) property.TrySetValue(parentObject, val); + logValue = XMLExtensions.ColorToString(val); + if (allowEditing) { property.TrySetValue(parentObject, val); } } else if (type == typeof(Vector2)) { Vector2 val = new Vector2(msg.ReadSingle(), msg.ReadSingle()); - if (allowEditing) property.TrySetValue(parentObject, val); + logValue = XMLExtensions.Vector2ToString(val); + if (allowEditing) { property.TrySetValue(parentObject, val); } } else if (type == typeof(Vector3)) { Vector3 val = new Vector3(msg.ReadSingle(), msg.ReadSingle(), msg.ReadSingle()); - if (allowEditing) property.TrySetValue(parentObject, val); + logValue = XMLExtensions.Vector3ToString(val); + if (allowEditing) { property.TrySetValue(parentObject, val); } } else if (type == typeof(Vector4)) { Vector4 val = new Vector4(msg.ReadSingle(), msg.ReadSingle(), msg.ReadSingle(), msg.ReadSingle()); - if (allowEditing) property.TrySetValue(parentObject, val); + logValue = XMLExtensions.Vector4ToString(val); + if (allowEditing) { property.TrySetValue(parentObject, val); } } else if (type == typeof(Point)) { Point val = new Point(msg.ReadInt32(), msg.ReadInt32()); - if (allowEditing) property.TrySetValue(parentObject, val); + logValue = XMLExtensions.PointToString(val); + if (allowEditing) { property.TrySetValue(parentObject, val); } } else if (type == typeof(Rectangle)) { Rectangle val = new Rectangle(msg.ReadInt32(), msg.ReadInt32(), msg.ReadInt32(), msg.ReadInt32()); - if (allowEditing) property.TrySetValue(parentObject, val); + logValue = XMLExtensions.RectToString(val); + if (allowEditing) { property.TrySetValue(parentObject, val); } } else if (typeof(Enum).IsAssignableFrom(type)) { int intVal = msg.ReadInt32(); try { - if (allowEditing) property.TrySetValue(parentObject, Enum.ToObject(type, intVal)); + if (allowEditing) + { + property.TrySetValue(parentObject, Enum.ToObject(type, intVal)); + logValue = property.GetValue(parentObject).ToString(); + } } catch (Exception e) { @@ -2408,7 +2445,22 @@ namespace Barotrauma { return; } - + +#if SERVER + if (allowEditing) + { + //the property change isn't logged until the value stays unchanged for 1 second to prevent log spam when a player adjusts a value + if (logPropertyChangeCoroutine != null) + { + CoroutineManager.StopCoroutines(logPropertyChangeCoroutine); + } + logPropertyChangeCoroutine = CoroutineManager.InvokeAfter(() => + { + GameServer.Log($"{sender.Character.Name} set the value \"{property.Name}\" of the item \"{Name}\" to \"{logValue}\".", ServerLog.MessageType.ItemInteraction); + }, delay: 1.0f); + } +#endif + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.ChangeProperty, property }); @@ -2595,7 +2647,7 @@ namespace Barotrauma if (linkedTo != null && linkedTo.Count > 0) { bool isOutpost = Submarine != null && Submarine.Info.IsOutpost; - var saveableLinked = linkedTo.Where(l => l.ShouldBeSaved && !l.Removed && (l.Submarine == null || l.Submarine.Info.IsOutpost == isOutpost)); + var saveableLinked = linkedTo.Where(l => l.ShouldBeSaved && (l.Removed == Removed) && (l.Submarine == null || l.Submarine.Info.IsOutpost == isOutpost)); element.Add(new XAttribute("linked", string.Join(",", saveableLinked.Select(l => l.ID.ToString())))); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index f9deaaf46..3d8982e08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -36,14 +36,22 @@ namespace Barotrauma { public class RequiredItem { - public readonly ItemPrefab ItemPrefab; + public readonly List ItemPrefabs; public int Amount; public readonly float MinCondition; public readonly bool UseCondition; public RequiredItem(ItemPrefab itemPrefab, int amount, float minCondition, bool useCondition) { - ItemPrefab = itemPrefab; + ItemPrefabs = new List() { itemPrefab }; + Amount = amount; + MinCondition = minCondition; + UseCondition = useCondition; + } + + public RequiredItem(IEnumerable itemPrefabs, int amount, float minCondition, bool useCondition) + { + ItemPrefabs = new List(itemPrefabs); Amount = amount; MinCondition = minCondition; UseCondition = useCondition; @@ -89,9 +97,10 @@ namespace Barotrauma case "item": case "requireditem": string requiredItemIdentifier = subElement.GetAttributeString("identifier", ""); - if (string.IsNullOrWhiteSpace(requiredItemIdentifier)) + string requiredItemTag = subElement.GetAttributeString("tag", ""); + if (string.IsNullOrWhiteSpace(requiredItemIdentifier) && string.IsNullOrEmpty(requiredItemTag)) { - DebugConsole.ThrowError("Error in fabricable item " + itemPrefab.Name + "! One of the required items has no identifier."); + DebugConsole.ThrowError("Error in fabricable item " + itemPrefab.Name + "! One of the required items has no identifier or tag."); continue; } @@ -100,26 +109,43 @@ namespace Barotrauma bool useCondition = subElement.GetAttributeBool("usecondition", true); int count = subElement.GetAttributeInt("count", 1); - - ItemPrefab requiredItem = MapEntityPrefab.Find(null, requiredItemIdentifier.Trim()) as ItemPrefab; - if (requiredItem == null) + if (!string.IsNullOrEmpty(requiredItemIdentifier)) { - DebugConsole.ThrowError("Error in fabricable item " + itemPrefab.Name + "! Required item \"" + requiredItemIdentifier + "\" not found."); - continue; - } + if (!(MapEntityPrefab.Find(null, requiredItemIdentifier.Trim()) is ItemPrefab requiredItem)) + { + DebugConsole.ThrowError("Error in fabricable item " + itemPrefab.Name + "! Required item \"" + requiredItemIdentifier + "\" not found."); + continue; + } - var existing = RequiredItems.Find(r => r.ItemPrefab == requiredItem); - if (existing == null) - { - RequiredItems.Add(new RequiredItem(requiredItem, count, minCondition, useCondition)); + var existing = RequiredItems.Find(r => r.ItemPrefabs.Count == 1 && r.ItemPrefabs[0] == requiredItem); + if (existing == null) + { + RequiredItems.Add(new RequiredItem(requiredItem, count, minCondition, useCondition)); + } + else + { + existing.Amount += count; + } } else { + var matchingItems = ItemPrefab.Prefabs.Where(ip => ip.Tags.Any(t => t.Equals(requiredItemTag, StringComparison.OrdinalIgnoreCase))); + if (!matchingItems.Any()) + { + DebugConsole.ThrowError("Error in fabricable item " + itemPrefab.Name + "! Could not find any items with the tag \"" + requiredItemTag + "\"."); + continue; + } - RequiredItems.Remove(existing); - RequiredItems.Add(new RequiredItem(requiredItem, existing.Amount + count, minCondition, useCondition)); + var existing = RequiredItems.Find(r => r.ItemPrefabs.SequenceEqual(matchingItems)); + if (existing == null) + { + RequiredItems.Add(new RequiredItem(matchingItems, count, minCondition, useCondition)); + } + else + { + existing.Amount += count; + } } - break; } } @@ -161,7 +187,7 @@ namespace Barotrauma partial class ItemPrefab : MapEntityPrefab { - private string name; + private readonly string name; public override string Name => name; public static readonly PrefabCollection Prefabs = new PrefabCollection(); @@ -405,6 +431,8 @@ namespace Barotrauma private set; } + [Serialize(null, false)] + public string EquipConfirmationText { get; set; } [Serialize(true, false, description: "Can the item be rotated in the sprite editor.")] public bool AllowRotatingInEditor { get; set; } @@ -717,12 +745,24 @@ namespace Barotrauma } #if CLIENT case "inventoryicon": - string iconFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) { - iconFolder = Path.GetDirectoryName(filePath); + string iconFolder = ""; + if (!subElement.GetAttributeString("texture", "").Contains("/")) + { + iconFolder = Path.GetDirectoryName(filePath); + } + InventoryIcon = new Sprite(subElement, iconFolder, lazyLoad: true); + } + break; + case "minimapicon": + { + string iconFolder = ""; + if (!subElement.GetAttributeString("texture", "").Contains("/")) + { + iconFolder = Path.GetDirectoryName(filePath); + } + MinimapIcon = new Sprite(subElement, iconFolder, lazyLoad: true); } - InventoryIcon = new Sprite(subElement, iconFolder, lazyLoad: true); break; case "brokensprite": string brokenSpriteFolder = ""; @@ -942,7 +982,7 @@ namespace Barotrauma public int? GetMinPrice() { - int? minPrice = locationPrices?.Values.Min(p => p.Price); + int? minPrice = locationPrices != null && locationPrices.Values.Any() ? locationPrices?.Values.Min(p => p.Price) : null; if (minPrice.HasValue) { if (defaultPrice != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index fa13d35f1..edf4bc59a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -134,7 +134,7 @@ namespace Barotrauma private bool CheckContained(Item parentItem) { - var containedItems = parentItem.ContainedItems; + var containedItems = parentItem.OwnInventory?.Items; if (containedItems == null) { return false; } if (MatchOnEmpty && !containedItems.Any(ci => ci != null)) @@ -221,9 +221,15 @@ namespace Barotrauma string typeStr = element.GetAttributeString("type", ""); if (string.IsNullOrEmpty(typeStr)) { - if (element.Name.ToString().Equals("containable", StringComparison.OrdinalIgnoreCase)) + switch (element.Name.ToString().ToLowerInvariant()) { - typeStr = "Contained"; + case "containable": + typeStr = "Contained"; + break; + case "suitablefertilizer": + case "suitableseed": + typeStr = "None"; + break; } } if (!Enum.TryParse(typeStr, true, out ri.type)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 979420832..4881e7835 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -23,8 +23,9 @@ namespace Barotrauma private readonly float screenColorRange, screenColorDuration; private bool sparks, shockwave, flames, smoke, flash, underwaterBubble; - private float flashDuration; - private float? flashRange; + private bool applyFireEffects; + private readonly float flashDuration; + private readonly float? flashRange; private readonly string decal; private readonly float decalSize; @@ -57,6 +58,8 @@ namespace Barotrauma underwaterBubble = element.GetAttributeBool("underwaterbubble", true); smoke = element.GetAttributeBool("smoke", true); + applyFireEffects = element.GetAttributeBool("applyfireeffects", flames); + flash = element.GetAttributeBool("flash", true); flashDuration = element.GetAttributeFloat("flashduration", 0.05f); if (element.Attribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); } @@ -64,7 +67,7 @@ namespace Barotrauma EmpStrength = element.GetAttributeFloat("empstrength", 0.0f); decal = element.GetAttributeString("decal", ""); - decalSize = element.GetAttributeFloat("decalSize", 1.0f); + decalSize = element.GetAttributeFloat(1.0f, "decalSize", "decalsize"); cameraShake = element.GetAttributeFloat("camerashake", attack.Range * 0.1f); cameraShakeRange = element.GetAttributeFloat("camerashakerange", attack.Range); @@ -98,9 +101,13 @@ namespace Barotrauma } Hull hull = Hull.FindHull(worldPosition); - ExplodeProjSpecific(worldPosition, hull); + if (hull != null && !string.IsNullOrWhiteSpace(decal) && decalSize > 0.0f) + { + hull.AddDecal(decal, worldPosition, decalSize, true); + } + float displayRange = attack.Range; Vector2 cameraPos = Character.Controlled != null ? Character.Controlled.WorldPosition : GameMain.GameScreen.Cam.Position; @@ -161,7 +168,7 @@ namespace Barotrauma { if (item.Condition <= 0.0f) { continue; } if (Vector2.Distance(item.WorldPosition, worldPosition) > attack.Range * 0.5f) { continue; } - if (flames && !item.FireProof) + if (applyFireEffects && !item.FireProof) { //don't apply OnFire effects if the item is inside a fireproof container //(or if it's inside a container that's inside a fireproof container, etc) @@ -231,8 +238,11 @@ namespace Barotrauma Dictionary distFactors = new Dictionary(); Dictionary damages = new Dictionary(); + List modifiedAfflictions = new List(); foreach (Limb limb in c.AnimController.Limbs) { + if (limb.IsSevered || limb.ignoreCollisions) { continue; } + float dist = Vector2.Distance(limb.WorldPosition, worldPosition); //calculate distance from the "outer surface" of the physics body @@ -244,19 +254,49 @@ namespace Barotrauma float distFactor = 1.0f - dist / attack.Range; - //solid obstacles between the explosion and the limb reduce the effect of the explosion by 90% - if (Submarine.CheckVisibility(limb.SimPosition, explosionPos) != null) + //solid obstacles between the explosion and the limb reduce the effect of the explosion + var obstacles = Submarine.PickBodies(limb.SimPosition, explosionPos, collisionCategory: Physics.CollisionItem | Physics.CollisionItemBlocking | Physics.CollisionWall); + foreach (var body in obstacles) { - distFactor *= 0.1f; + if (body.UserData is Item item) + { + var door = item.GetComponent(); + if (door != null && !door.IsBroken) { distFactor *= 0.01f; } + } + else if (body.UserData is Structure structure) + { + int sectionIndex = structure.FindSectionIndex(worldPosition, world: true, clamp: true); + if (structure.SectionBodyDisabled(sectionIndex)) + { + continue; + } + else if (structure.SectionIsLeaking(sectionIndex)) + { + distFactor *= 0.1f; + } + else + { + distFactor *= 0.01f; + } + } + else + { + distFactor *= 0.1f; + } } - + if (distFactor <= 0.05f) { continue; } + distFactors.Add(limb, distFactor); - - List modifiedAfflictions = new List(); - int limbCount = c.AnimController.Limbs.Count(l => !l.IsSevered && !l.ignoreCollisions); + + modifiedAfflictions.Clear(); foreach (Affliction affliction in attack.Afflictions.Keys) { - modifiedAfflictions.Add(affliction.CreateMultiplied(distFactor / limbCount)); + //previously the damage would be divided by the number of limbs (the intention was to prevent characters with more limbs taking more damage from explosions) + //that didn't work well on large characters like molochs and endworms: the explosions tend to only damage one or two of their limbs, and since the characters + //have lots of limbs, they tended to only take a fraction of the damage they should + + //now we just divide by 10, which keeps the damage to normal-sized characters roughly the same as before and fixes the large characters + modifiedAfflictions.Add(affliction.CreateMultiplied(distFactor / 10)); } c.LastDamageSource = damageSource; if (attacker == null) @@ -273,7 +313,8 @@ namespace Barotrauma //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 hitPos = limb.WorldPosition + (worldPosition - limb.WorldPosition) / dist * 0.01f; + 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); damages.Add(limb, attackResult.Damage); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs index 0e0b69192..29ff8295a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs @@ -29,9 +29,7 @@ namespace Barotrauma protected bool removed; -#if CLIENT - private List burnDecals = new List(); -#endif + private readonly List burnDecals = new List(); public Vector2 Position { @@ -136,10 +134,8 @@ namespace Barotrauma - leftEdge; fireSources[j].position.X = leftEdge; -#if CLIENT fireSources[j].burnDecals.AddRange(fireSources[i].burnDecals); fireSources[j].burnDecals.Sort((d1, d2) => { return Math.Sign(d1.WorldPosition.X - d2.WorldPosition.X); }); -#endif fireSources[i].Remove(); } } @@ -179,7 +175,33 @@ namespace Barotrauma LimitSize(); + if (size.X > 256.0f) + { + if (burnDecals.Count == 0) + { + var newDecal = hull.AddDecal("burnt", WorldPosition + size / 2, 1f, true); + if (newDecal != null) { burnDecals.Add(newDecal); } + } + else if (WorldPosition.X < burnDecals[0].WorldPosition.X - 256.0f) + { + var newDecal = hull.AddDecal("burnt", WorldPosition, 1f, true); + if (newDecal != null) { burnDecals.Insert(0, newDecal); } + } + else if (WorldPosition.X + size.X > burnDecals[burnDecals.Count - 1].WorldPosition.X + 256.0f) + { + var newDecal = hull.AddDecal("burnt", WorldPosition + Vector2.UnitX * size.X, 1f, true); + if (newDecal != null) { burnDecals.Add(newDecal); } + } + } + + foreach (Decal d in burnDecals) + { + //prevent the decals from fading out as long as the firesource is alive + d.ForceRefreshFadeTimer(Math.Min(d.FadeTimer, d.FadeInTime)); + } + UpdateProjSpecific(growModifier); + if (size.X < 1.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) { @@ -366,12 +388,11 @@ namespace Barotrauma #if CLIENT lightSource?.Remove(); lightSource = null; - +#endif foreach (Decal d in burnDecals) { d.StopFadeIn(); } -#endif hull?.RemoveFire(this); removed = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 8b96a4e2c..e53eebcfc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -160,9 +160,15 @@ namespace Barotrauma public override void Move(Vector2 amount) { + if (!MathUtils.IsValid(amount)) + { + DebugConsole.ThrowError($"Attempted to move a gap by an invalid amount ({amount})\n{Environment.StackTrace}"); + return; + } + base.Move(amount); - if (!DisableHullRechecks) FindHulls(); + if (!DisableHullRechecks) { FindHulls(); } } public static void UpdateHulls() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index f7115bd85..f277c6c52 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -4,11 +4,100 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Xml.Linq; namespace Barotrauma { + partial class BackgroundSection + { + public Rectangle Rect; + public int Index; + public int RowIndex; + + private Vector4 colorVector4; + private Color color; + + public readonly Vector2 Noise; + public readonly Color DirtColor; + + public float ColorStrength + { + get; + protected set; + } + + public Color Color + { + get { return color; } + protected set + { + color = value; + colorVector4 = new Vector4(value.R / 255.0f, value.G / 255.0f, value.B / 255.0f, value.A / 255.0f); + } + } + + public BackgroundSection(Rectangle rect, int index, int rowIndex) + { + Rect = rect; + Index = index; + ColorStrength = 0.0f; + RowIndex = rowIndex; + + Noise = new Vector2( + PerlinNoise.GetPerlin(Rect.X / 1000.0f, Rect.Y / 1000.0f), + PerlinNoise.GetPerlin(Rect.Y / 1000.0f + 0.5f, Rect.X / 1000.0f + 0.5f)); + + Color = DirtColor = Color.Lerp(new Color(10, 10, 10, 100), new Color(54, 57, 28, 200), Noise.X); + } + + public BackgroundSection(Rectangle rect, int index, float colorStrength, Color color, int rowIndex) + { + System.Diagnostics.Debug.Assert(rect.Width > 0 && rect.Height > 0); + + Rect = rect; + Index = index; + ColorStrength = colorStrength; + Color = color; + RowIndex = rowIndex; + + Noise = new Vector2( + PerlinNoise.GetPerlin(Rect.X / 1000.0f, Rect.Y / 1000.0f), + PerlinNoise.GetPerlin(Rect.Y / 1000.0f + 0.5f, Rect.X / 1000.0f + 0.5f)); + + Color = DirtColor = Color.Lerp(new Color(10, 10, 10, 100), new Color(54, 57, 28, 200), Noise.X); + } + + public bool SetColor(Color color) + { + if (Color == color) { return false; } + Color = color; + return true; + } + + public float SetColorStrength(float colorStrength) + { + if (ColorStrength == colorStrength) { return -1f; } + float previous = ColorStrength; + ColorStrength = colorStrength; + return previous; + } + + public bool LerpColor(Color to, float amount) + { + if (Color == to) { return false; } + colorVector4 = Vector4.Lerp(colorVector4, to.ToVector4(), amount); + color = new Color(colorVector4); + return true; + } + + public Color GetStrengthAdjustedColor() + { + return Color * ColorStrength; + } + } + partial class Hull : MapEntity, ISerializableEntity, IServerSerializable { public static List hullList = new List(); @@ -29,7 +118,11 @@ namespace Barotrauma //how much excess water the room can contain, relative to the volume of the room. //needed to make it possible for pressure to "push" water up through U-shaped hull configurations public const float MaxCompress = 1.05f; - + + public const int BackgroundSectionSize = 16; + + public const int BackgroundSectionsPerNetworkEvent = 16; + public readonly Dictionary properties; public Dictionary SerializableProperties { @@ -54,6 +147,11 @@ namespace Barotrauma private float[] leftDelta; private float[] rightDelta; + public const int MaxDecalsPerHull = 10; + + private readonly List decals = new List(); + + public readonly List ConnectedGaps = new List(); public override string Name @@ -140,6 +238,8 @@ namespace Barotrauma OxygenPercentage = prevOxygenPercentage; surface = drawSurface = rect.Y - rect.Height + WaterVolume / rect.Width; Pressure = surface; + + CreateBackgroundSections(); } } @@ -186,6 +286,8 @@ namespace Barotrauma get { return Submarine == null ? surface : surface + Submarine.Position.Y; } } + private float dirtiedVolume = 0.0f; + public float WaterVolume { get { return waterVolume; } @@ -193,8 +295,25 @@ namespace Barotrauma { if (!MathUtils.IsValid(value)) return; waterVolume = MathHelper.Clamp(value, 0.0f, Volume * MaxCompress); - if (waterVolume < Volume) Pressure = rect.Y - rect.Height + waterVolume / rect.Width; - if (waterVolume > 0.0f) update = true; + if (waterVolume < Volume) { Pressure = rect.Y - rect.Height + waterVolume / rect.Width; } + if (waterVolume > 0.0f) + { + update = true; + if (BackgroundSections != null) + { + float volumeMultiplier = Math.Clamp(waterVolume / Volume, 0f, 1f); + if (Math.Abs(volumeMultiplier - dirtiedVolume) > 0.075f) + { + RefreshSubmergedSections(new Rectangle(new Point(0, -rect.Height), new Point(rect.Width, (int)(rect.Height * volumeMultiplier)))); + dirtiedVolume = volumeMultiplier; + } + } + } + else + { + submergedSections.Clear(); + dirtiedVolume = 0.0f; + } } } @@ -238,6 +357,36 @@ namespace Barotrauma get { return waveVel; } } + // sections of a decorative background that can be painted + public List BackgroundSections + { + get; + private set; + } + + private readonly HashSet pendingSectionUpdates = new HashSet(); + + private readonly List submergedSections = new List(); + + public int xBackgroundMax, yBackgroundMax; + + public bool SupportsPaintedColors + { + get + { + return BackgroundSections != null; + } + } + + private const int sectorWidth = 4; + private const int sectorHeight = 4; + + private const float minColorStrength = 0.0f; + private const float maxColorStrength = 0.7f; + + private bool networkUpdatePending; + private float networkUpdateTimer; + public List FireSources { get; private set; } public Hull(MapEntityPrefab prefab, Rectangle rectangle) @@ -250,6 +399,8 @@ namespace Barotrauma : base (prefab, submarine) { rect = rectangle; + + if (BackgroundSections == null) { CreateBackgroundSections(); } OxygenPercentage = 100.0f; @@ -284,6 +435,8 @@ namespace Barotrauma Gap.UpdateHulls(); } + CreateBackgroundSections(); + WaterVolume = 0.0f; InsertToList(); @@ -399,6 +552,12 @@ namespace Barotrauma public override void Move(Vector2 amount) { + if (!MathUtils.IsValid(amount)) + { + DebugConsole.ThrowError($"Attempted to move a hull by an invalid amount ({amount})\n{Environment.StackTrace}"); + return; + } + rect.X += (int)amount.X; rect.Y += (int)amount.Y; @@ -471,6 +630,48 @@ namespace Barotrauma FireSources.Add(fireSource); } + public Decal AddDecal(UInt32 decalId, Vector2 worldPosition, float scale, bool isNetworkEvent) + { + //clients are only allowed to create decals when the server says so + if (!isNetworkEvent && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) + { + return null; + } + + var decal = GameMain.DecalManager.Prefabs.Find(p => p.UIntIdentifier == decalId); + if (decal == null) + { + DebugConsole.ThrowError($"Could not find a decal prefab with the UInt identifier {decalId}!"); + return null; + } + return AddDecal(decal.Name, worldPosition, scale, isNetworkEvent); + } + + + public Decal AddDecal(string decalName, Vector2 worldPosition, float scale, bool isNetworkEvent) + { + //clients are only allowed to create decals when the server says so + if (!isNetworkEvent && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) + { + return null; + } + + if (decals.Count >= MaxDecalsPerHull) { return null; } + + var decal = GameMain.DecalManager.CreateDecal(decalName, scale, worldPosition, this); + if (decal != null) + { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + { + GameMain.NetworkMember.CreateEntityEvent(this, new object[] { false }); + } + decals.Add(decal); + } + + return decal; + } + + public override void Update(float deltaTime, Camera cam) { base.Update(deltaTime, cam); @@ -480,6 +681,13 @@ namespace Barotrauma FireSource.UpdateAll(FireSources, deltaTime); + foreach (Decal decal in decals) + { + decal.Update(deltaTime); + } + decals.RemoveAll(d => d.FadeTimer >= d.LifeTime || d.BaseAlpha <= 0.001f); + + if (aiTarget != null) { aiTarget.SightRange = Submarine == null ? aiTarget.MinSightRange : Submarine.Velocity.Length() / 2 * aiTarget.MaxSightRange; @@ -596,6 +804,12 @@ namespace Barotrauma } } + //0.01 increase every ~1000 frames = reaches full dirtiness in ~27 minutes + if (submergedSections.Count > 0 && Submarine != null && Submarine.Info.Type == SubmarineType.Player && Rand.Int(1000) == 1) + { + DirtySections(submergedSections, 0.01f); + } + if (waterVolume < Volume) { LethalPressure -= 10.0f * deltaTime; @@ -906,9 +1120,180 @@ namespace Barotrauma return "RoomName.Sub" + roomPos.ToString(); } +#region BackgroundSections + private void CreateBackgroundSections() + { + int sectionWidth, sectionHeight; + + sectionWidth = sectionHeight = BackgroundSectionSize; + + xBackgroundMax = rect.Width / sectionWidth; + yBackgroundMax = rect.Height / sectionHeight; + + BackgroundSections = new List(xBackgroundMax * yBackgroundMax); + + int sections = xBackgroundMax * yBackgroundMax; + float xSectors = xBackgroundMax / (float)sectorWidth; + + for (int y = 0; y < yBackgroundMax; y++) + { + for (int x = 0; x < xBackgroundMax; x++) + { + int index = BackgroundSections.Count; + int sector = (int)Math.Floor(index / (float)sectorWidth - xSectors * y) + y / sectorHeight * (int)Math.Ceiling(xSectors); + BackgroundSections.Add(new BackgroundSection(new Rectangle(x * sectionWidth, y * -sectionHeight, sectionWidth, sectionHeight), index, y)); + } + } + +#if CLIENT + minimumPaintAmountToDraw = maxColorStrength / BackgroundSections.Count; +#endif + } + + public static Hull GetCleanTarget(Vector2 worldPosition) + { + foreach (Hull hull in hullList) + { + Rectangle worldRect = hull.WorldRect; + if (worldPosition.X < worldRect.X || worldPosition.X > worldRect.Right) { continue; } + if (worldPosition.Y > worldRect.Y || worldPosition.Y < worldRect.Y - worldRect.Height) { continue; } + return hull; + } + return null; + } + + public BackgroundSection GetBackgroundSection(Vector2 worldPosition) + { + if (!SupportsPaintedColors) { return null; } + + Vector2 subOffset = Submarine == null ? Vector2.Zero : Submarine.Position; + Vector2 relativePosition = new Vector2(worldPosition.X - subOffset.X - rect.X, worldPosition.Y - subOffset.Y - rect.Y); + + int xIndex = (int)Math.Floor(relativePosition.X / BackgroundSectionSize); + if (xIndex < 0 || xIndex >= xBackgroundMax) { return null; } + int yIndex = (int)Math.Floor(-relativePosition.Y / BackgroundSectionSize); + if (yIndex < 0 || yIndex >= yBackgroundMax) { return null; } + + return BackgroundSections[xIndex + yIndex * xBackgroundMax]; + } + + public IEnumerable GetBackgroundSectionsViaContaining(Rectangle rectArea) + { + if (BackgroundSections == null || BackgroundSections.Count == 0) + { + yield break; + } + else + { + int xMin = Math.Max(rectArea.X / BackgroundSectionSize, 0); + if (xMin >= xBackgroundMax) { yield break; } + int xMax = Math.Min(rectArea.Right / BackgroundSectionSize, xBackgroundMax - 1); + if (xMax < 0) { yield break; } + + int yMin = Math.Max(-rectArea.Bottom / BackgroundSectionSize, 0); + if (yMin >= yBackgroundMax) { yield break; } + int yMax = Math.Min(-rectArea.Y / BackgroundSectionSize, yBackgroundMax - 1); + if (yMax < 0) { yield break; } + + for (int x = xMin; x <= xMax; x++) + { + for (int y = yMin; y <= yMax; y++) + { + yield return BackgroundSections[x + y * xBackgroundMax]; + } + } + } + } + + public void RefreshSubmergedSections(Rectangle waterArea) + { + if (BackgroundSections == null) { return; } + + submergedSections.Clear(); + foreach (var section in GetBackgroundSectionsViaContaining(waterArea)) + { + submergedSections.Add(section); + } + } + + public bool DoesSectionMatch(int index, int row) + { + return index >= 0 && row >= 0 && BackgroundSections.Count > index && BackgroundSections[index] != null && BackgroundSections[index].RowIndex == row; + } + + public void SetSectionColorOrStrength(BackgroundSection section, Color? color, float? strength, bool requiresUpdate, bool isCleaning) + { + bool sectionUpdated = isCleaning; + if (color != null) + { + if (section.Color != color.Value && strength.HasValue) + { + //already painted with a different color -> interpolate towards the new one + + //an ad-hoc formula that makes the color changes faster when the current strength is low + //(-> a barely dirty wall gets recolored almost immediately, while a more heavily colored one takes a while) + float changeSpeed = strength.Value / Math.Max(section.ColorStrength * section.ColorStrength, 0.001f) * 0.1f; + if (section.LerpColor(color.Value, changeSpeed)) { sectionUpdated = true; } + } + else + { + if (section.SetColor(color.Value)) { sectionUpdated = true; } + } + } + + if (strength != null) + { + float previous = section.SetColorStrength(Math.Max(minColorStrength, Math.Min(maxColorStrength, section.ColorStrength + strength.Value))); + if (previous != -1f) + { +#if CLIENT + paintAmount = Math.Max(0, paintAmount + (section.ColorStrength - previous) / BackgroundSections.Count); +#endif + sectionUpdated = true; + } + } + + if (sectionUpdated && GameMain.NetworkMember != null && requiresUpdate) + { + networkUpdatePending = true; + pendingSectionUpdates.Add((int)Math.Floor(section.Index / (float)BackgroundSectionsPerNetworkEvent)); +#if CLIENT + serverUpdateDelay = 0.5f; +#endif + } + } + + public void DirtySections(List sections, float dirtyVal) + { + if (sections == null) { return; } + for (int i = 0; i < sections.Count; i++) + { + float sectionDirtyVal = dirtyVal; + SetSectionColorOrStrength(sections[i], sections[i].DirtColor, sectionDirtyVal, false, false); + } + } + + public void CleanSection(BackgroundSection section, float cleanVal, bool updateRequired) + { + bool decalsCleaned = false; + for (int i = 0; i < decals.Count; i++) + { + Decal decal = decals[i]; + if (decal.AffectsSection(section)) + { + decal.Clean(cleanVal); + decalsCleaned = true; + } + } + + if (section.ColorStrength == 0 && !decalsCleaned) { return; } + SetSectionColorOrStrength(section, null, cleanVal, updateRequired, true); + } +#endregion + public static Hull Load(XElement element, Submarine submarine) { - Rectangle rect = Rectangle.Empty; + Rectangle rect; if (element.Attribute("rect") != null) { rect = element.GetAttributeRect("rect", Rectangle.Empty); @@ -947,6 +1332,41 @@ namespace Barotrauma hull.OriginalAmbientLight = XMLExtensions.ParseColor(originalAmbientLight, false); } + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "decal": + string id = subElement.GetAttributeString("id", ""); + Vector2 pos = subElement.GetAttributeVector2("pos", Vector2.Zero); + float scale = subElement.GetAttributeFloat("scale", 1.0f); + float timer = subElement.GetAttributeFloat("timer", 1.0f); + var decal = hull.AddDecal(id, pos + hull.WorldRect.Location.ToVector2(), scale, true); + if (decal != null) + { + decal.FadeTimer = timer; + } + break; + } + } + + string backgroundSectionStr = element.GetAttributeString("backgroundsections", ""); + if (!string.IsNullOrEmpty(backgroundSectionStr)) + { + string[] backgroundSectionStrSplit = backgroundSectionStr.Split(';'); + foreach (string str in backgroundSectionStrSplit) + { + string[] backgroundSectionData = str.Split(':'); + if (backgroundSectionData.Length != 3) { continue; } + Color color = XMLExtensions.ParseColor(backgroundSectionData[1]); + if (int.TryParse(backgroundSectionData[0], out int index) && + float.TryParse(backgroundSectionData[2], NumberStyles.Any, CultureInfo.InvariantCulture, out float strength)) + { + hull.SetSectionColorOrStrength(hull.BackgroundSections[index], color, strength, false, false); + } + } + } + SerializableProperty.DeserializeProperties(hull, element); if (element.Attribute("oxygen") == null) { hull.Oxygen = hull.Volume; } @@ -976,7 +1396,7 @@ namespace Barotrauma if (linkedTo != null && linkedTo.Count > 0) { - var saveableLinked = linkedTo.Where(l => l.ShouldBeSaved && !l.Removed).ToList(); + var saveableLinked = linkedTo.Where(l => l.ShouldBeSaved && (l.Removed == Removed)).ToList(); element.Add(new XAttribute("linked", string.Join(",", saveableLinked.Select(l => l.ID.ToString())))); } @@ -985,6 +1405,25 @@ namespace Barotrauma element.Add(new XAttribute("originalambientlight", XMLExtensions.ColorToString(OriginalAmbientLight.Value))); } + if (BackgroundSections != null && BackgroundSections.Count > 0) + { + element.Add( + new XAttribute( + "backgroundsections", + string.Join(';', BackgroundSections.Where(b => b.ColorStrength > 0.01f).Select(b => b.Index + ":" + XMLExtensions.ColorToString(b.Color) + ":" + b.ColorStrength.ToString("G", CultureInfo.InvariantCulture))))); + } + + foreach (Decal decal in decals) + { + element.Add( + new XElement("decal", + new XAttribute("id", decal.Prefab.Identifier), + new XAttribute("pos", XMLExtensions.Vector2ToString(decal.NonClampedPosition)), + new XAttribute("scale", decal.Scale.ToString("G", CultureInfo.InvariantCulture)), + new XAttribute("timer", decal.FadeTimer.ToString("G", CultureInfo.InvariantCulture)) + )); + } + SerializableProperty.SerializeProperties(this, element); parentElement.Add(element); return element; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 16ce350bd..ebf16b2fe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -42,9 +42,15 @@ namespace Barotrauma XDocument doc = XMLExtensions.TryLoadXml(filePath); if (doc == null) { return; } - originalName = doc.Root.GetAttributeString("name", ""); - identifier = doc.Root.GetAttributeString("identifier", null) ?? originalName.ToLowerInvariant().Replace(" ", ""); - configElement = doc.Root; + XElement element = doc.Root; + if (element.IsOverride()) + { + element = element.Elements().First(); + } + + originalName = element.GetAttributeString("name", ""); + identifier = element.GetAttributeString("identifier", null) ?? originalName.ToLowerInvariant().Replace(" ", ""); + configElement = element; Category = MapEntityCategory.ItemAssembly; @@ -54,7 +60,7 @@ namespace Barotrauma Description = TextManager.Get("EntityDescription." + identifier, returnNull: true) ?? Description; List containedItemIDs = new List(); - foreach (XElement entityElement in doc.Root.Elements()) + foreach (XElement entityElement in element.Elements()) { var containerElement = entityElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("itemcontainer", StringComparison.OrdinalIgnoreCase)); if (containerElement == null) { continue; } @@ -66,7 +72,7 @@ namespace Barotrauma int minX = int.MaxValue, minY = int.MaxValue; int maxX = int.MinValue, maxY = int.MinValue; DisplayEntities = new List>(); - foreach (XElement entityElement in doc.Root.Elements()) + foreach (XElement entityElement in element.Elements()) { ushort id = (ushort)entityElement.GetAttributeInt("ID", 0); if (id > 0 && containedItemIDs.Contains(id)) { continue; } @@ -94,7 +100,7 @@ namespace Barotrauma new Rectangle(0, 0, 1, 1) : new Rectangle(minX, minY, maxX - minX, maxY - minY); - Prefabs.Add(this, false); + Prefabs.Add(this, doc.Root.IsOverride()); } public static void Remove(string filePath) @@ -110,17 +116,17 @@ namespace Barotrauma public List CreateInstance(Vector2 position, Submarine sub, bool selectPrefabs = false) { List entities = MapEntity.LoadAll(sub, configElement, FilePath); - if (entities.Count == 0) return entities; + if (entities.Count == 0) { return entities; } Vector2 offset = sub == null ? Vector2.Zero : sub.HiddenSubPosition; foreach (MapEntity me in entities) { me.Move(position); - Item item = me as Item; - if (item == null) continue; + me.Submarine = sub; + if (!(me is Item item)) { continue; } Wire wire = item.GetComponent(); - if (wire != null) wire.MoveNodes(position - offset); + if (wire != null) { wire.MoveNodes(position - offset); } } MapEntity.MapLoaded(entities, true); @@ -171,7 +177,7 @@ namespace Barotrauma } //find assembly files in selected content packages - foreach (ContentPackage cp in GameMain.Config.SelectedContentPackages) + foreach (ContentPackage cp in GameMain.Config.AllEnabledPackages) { foreach (string filePath in cp.GetFilesOfType(ContentType.ItemAssembly)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 044128ba0..2e4e4bc7f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -445,8 +445,11 @@ namespace Barotrauma if (mainPathCellCount > tunnel.Count / 2) continue; var newPathCells = CaveGenerator.GeneratePath(tunnel, cells, cellGrid, GridCellSize, pathBorders); - PositionsOfInterest.Add(new InterestingPosition(tunnel.Last(), PositionType.Cave)); - if (tunnel.Count > 4) PositionsOfInterest.Add(new InterestingPosition(tunnel[tunnel.Count / 2], PositionType.Cave)); + if (newPathCells.Any()) + { + PositionsOfInterest.Add(new InterestingPosition(newPathCells.Last().Center.ToPoint(), PositionType.Cave)); + if (newPathCells.Count > 4) { PositionsOfInterest.Add(new InterestingPosition(newPathCells[newPathCells.Count / 2].Center.ToPoint(), PositionType.Cave)); } + } validTunnels.Add(tunnel); pathCells.AddRange(newPathCells); } @@ -574,7 +577,7 @@ namespace Barotrauma Ruins = new List(); for (int i = 0; i < GenerationParams.RuinCount; i++) { - GenerateRuin(mainPath, this, mirror); + GenerateRuin(mainPath, mirror); } EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); @@ -1054,7 +1057,7 @@ namespace Barotrauma return tunnelNodes; } - private void GenerateRuin(List mainPath, Level level, bool mirror) + private void GenerateRuin(List mainPath, bool mirror) { var ruinGenerationParams = RuinGenerationParams.GetRandom(); @@ -1075,14 +1078,19 @@ namespace Barotrauma ruinPos.Y = Math.Min(ruinPos.Y, borders.Y + borders.Height - ruinSize.Y / 2); ruinPos.Y = Math.Max(ruinPos.Y, SeaFloorTopPos + ruinSize.Y / 2); - double minDist = ruinRadius * 2; - double minDistSqr = minDist * minDist; + double minMainPathDist = ruinRadius * 2; + double minMainPathDistSqr = minMainPathDist * minMainPathDist; + + double minOutpostDist = Math.Min(Math.Min(10000.0f, Size.X / 3), Size.Y / 3); + double minOutpostDistSqr = minOutpostDist * minOutpostDist; int iter = 0; - while (mainPath.Any(p => MathUtils.DistanceSquared(ruinPos.X, ruinPos.Y, p.Site.Coord.X, p.Site.Coord.Y) < minDistSqr) || + while (mainPath.Any(p => MathUtils.DistanceSquared(ruinPos.X, ruinPos.Y, p.Site.Coord.X, p.Site.Coord.Y) < minMainPathDistSqr) || Ruins.Any(r => r.Area.Intersects(new Rectangle(ruinPos - new Point(ruinSize.X / 2, ruinSize.Y / 2), ruinSize)) || - MathUtils.DistanceSquared(ruinPos.X, ruinPos.Y, StartPosition.X, StartPosition.Y) < minDistSqr || - MathUtils.DistanceSquared(ruinPos.X, ruinPos.Y, EndPosition.X, EndPosition.Y) < minDistSqr)) + MathUtils.DistanceSquared(ruinPos.X, ruinPos.Y, StartPosition.X, StartPosition.Y) < minOutpostDistSqr || + MathUtils.DistanceSquared(ruinPos.X, ruinPos.Y, StartPosition.X, Size.Y) < minOutpostDistSqr || + MathUtils.DistanceSquared(ruinPos.X, ruinPos.Y, EndPosition.X, EndPosition.Y) < minOutpostDistSqr) || + MathUtils.DistanceSquared(ruinPos.X, ruinPos.Y, EndPosition.X, Size.Y) < minOutpostDistSqr) { double weighedPathPosX = ruinPos.X; double weighedPathPosY = ruinPos.Y; @@ -1094,11 +1102,11 @@ namespace Barotrauma double diffY = i == 0 ? ruinPos.Y - StartPosition.Y : ruinPos.Y - StartPosition.Y; double distSqr = diffX * diffX + diffY * diffY; - if (distSqr < minDistSqr) + if (distSqr < minMainPathDistSqr) { double dist = Math.Sqrt(distSqr); - double moveAmountX = minDist * diffX / dist; - double moveAmountY = minDist * diffY / dist; + double moveAmountX = minMainPathDist * diffX / dist; + double moveAmountY = minMainPathDist * diffY / dist; weighedPathPosX += moveAmountX; weighedPathPosY += moveAmountY; weighedPathPosY = Math.Min(borders.Y + borders.Height - ruinSize.Y / 2, weighedPathPosY); @@ -1315,7 +1323,7 @@ namespace Barotrauma { holdable.AttachToWall(); #if CLIENT - item.SpriteRotation = -MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2; + item.Rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2); #endif } } @@ -1337,7 +1345,11 @@ namespace Barotrauma { Loaded.TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 startPos); - startPos += Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.Server), Rand.RandSync.Server); + Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.Server), Rand.RandSync.Server); + if (!cells.Any(c => c.IsPointInside(startPos + offset))) + { + startPos += offset; + } Vector2 endPos = startPos - Vector2.UnitY * Size.Y; @@ -1533,7 +1545,7 @@ namespace Barotrauma var totalSW = new Stopwatch(); var tempSW = new Stopwatch(); totalSW.Start(); - var wreckFiles = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Wreck).ToList(); + var wreckFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Wreck).ToList(); if (wreckFiles.None()) { DebugConsole.ThrowError("No wreck files found in the selected content packages!"); @@ -1827,7 +1839,7 @@ namespace Barotrauma private void CreateOutposts() { - var outpostFiles = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Outpost).ToList(); + var outpostFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Outpost).ToList(); if (!outpostFiles.Any() && !OutpostGenerationParams.Params.Any() && LevelData.ForceOutpostGenerationParams == null) { DebugConsole.ThrowError("No outpost files found in the selected content packages"); @@ -2080,7 +2092,7 @@ namespace Barotrauma job ??= selectedPrefab.GetJobPrefab(); if (job == null) { continue; } - var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job); + var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job, randSync: Rand.RandSync.Server); var corpse = Character.Create(CharacterPrefab.HumanConfigFile, worldPos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); corpse.AnimController.FindHull(worldPos, true); corpse.TeamID = Character.TeamType.None; @@ -2128,6 +2140,11 @@ namespace Barotrauma StartLocation = newStartLocation; } + public void DebugSetEndLocation(Location newEndLocation) + { + EndLocation = newEndLocation; + } + public override void Remove() { base.Remove(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index a61619390..62f44a084 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -31,6 +31,7 @@ namespace Barotrauma public readonly Point Size; public readonly List EventHistory = new List(); + public readonly List NonRepeatableEvents = new List(); public LevelData(string seed, float difficulty, float sizeFactor, LevelGenerationParams generationParams, Biome biome) { @@ -77,6 +78,10 @@ namespace Barotrauma string[] prefabNames = element.GetAttributeStringArray("eventhistory", new string[] { }); EventHistory.AddRange(EventSet.PrefabList.Where(p => prefabNames.Any(n => p.Identifier.Equals(n, StringComparison.InvariantCultureIgnoreCase)))); + + string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", new string[] { }); + NonRepeatableEvents.AddRange(EventSet.PrefabList.Where(p => prefabNames.Any(n => p.Identifier.Equals(n, StringComparison.InvariantCultureIgnoreCase)))); + } @@ -153,11 +158,17 @@ namespace Barotrauma new XAttribute("size", XMLExtensions.PointToString(Size)), new XAttribute("generationparams", GenerationParams.Identifier)); - if (Type == LevelType.Outpost && EventHistory.Any()) + if (Type == LevelType.Outpost) { - newElement.Add(new XAttribute("eventhistory", string.Join(',', EventHistory.Select(p => p.Identifier)))); + if (EventHistory.Any()) + { + newElement.Add(new XAttribute("eventhistory", string.Join(',', EventHistory.Select(p => p.Identifier)))); + } + if (NonRepeatableEvents.Any()) + { + newElement.Add(new XAttribute("nonrepeatableevents", string.Join(',', NonRepeatableEvents.Select(p => p.Identifier)))); + } } - parentElement.Add(newElement); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index d910ba1c6..98428a969 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -40,6 +40,7 @@ namespace Barotrauma private string filePath; private bool loadSub; + public bool LoadSub => loadSub; private Submarine sub; private ushort originalMyPortID; @@ -47,6 +48,7 @@ namespace Barotrauma //the ID of the docking port the sub was docked to in the original sub file //(needed when replacing a lost sub) private ushort originalLinkedToID; + public ushort OriginalLinkedToID => originalLinkedToID; private DockingPort originalLinkedPort; private bool purchasedLostShuttles; @@ -160,6 +162,7 @@ namespace Barotrauma wallVertices = MathUtils.GiftWrap(points); } + // LinkedSubmarine.Load() is called from MapEntity.LoadAll() public static LinkedSubmarine Load(XElement element, Submarine submarine) { Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero); @@ -172,17 +175,16 @@ namespace Barotrauma } else { + string levelSeed = element.GetAttributeString("location", ""); + LevelData levelData = GameMain.GameSession.Campaign?.NextLevel ?? GameMain.GameSession.Level?.LevelData; linkedSub = new LinkedSubmarine(submarine) { + purchasedLostShuttles = GameMain.GameSession.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles, saveElement = element }; - linkedSub.purchasedLostShuttles = GameMain.GameSession.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles; - string levelSeed = element.GetAttributeString("location", ""); - if (!string.IsNullOrWhiteSpace(levelSeed) && - GameMain.GameSession.Level != null && - GameMain.GameSession.Level.Seed != levelSeed && - !linkedSub.purchasedLostShuttles) + if (!string.IsNullOrWhiteSpace(levelSeed) && levelData != null && + levelData.Seed != levelSeed && !linkedSub.purchasedLostShuttles) { linkedSub.loadSub = false; } @@ -217,12 +219,17 @@ namespace Barotrauma } } - public override void OnMapLoaded() { if (!loadSub) { return; } SubmarineInfo info = new SubmarineInfo(Submarine.Info.FilePath, "", saveElement); + if (!info.SubmarineElement.HasElements) + { + DebugConsole.ThrowError("Failed to load a linked submarine (empty XML element). The save file may be corrupted."); + return; + } + sub = Submarine.Load(info, false); Vector2 worldPos = saveElement.GetAttributeVector2("worldpos", Vector2.Zero); @@ -294,9 +301,9 @@ namespace Barotrauma else { Vector2 portDiff = myPort.Item.WorldPosition - sub.WorldPosition; - Vector2 offset = (myPort.IsHorizontal ? - Vector2.UnitX * Math.Sign(linkedPort.Item.WorldPosition.X - myPort.Item.WorldPosition.X) : - Vector2.UnitY * Math.Sign(linkedPort.Item.WorldPosition.Y - myPort.Item.WorldPosition.Y)); + Vector2 offset = myPort.IsHorizontal ? + Vector2.UnitX * myPort.GetDir(linkedPort) : + Vector2.UnitY * myPort.GetDir(linkedPort); offset *= myPort.DockedDistance; sub.SetPosition((linkedPort.Item.WorldPosition - portDiff) - offset); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 0658a7d0f..5fc834dcb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -115,7 +115,7 @@ namespace Barotrauma { get { - availableMissions.RemoveAll(m => m.Completed); + availableMissions.RemoveAll(m => m.Completed || m.Failed); return availableMissions; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index 6dc178296..b5eaa3441 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -6,6 +6,7 @@ using System.Globalization; using Barotrauma.IO; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -207,6 +208,16 @@ namespace Barotrauma { List.Clear(); var locationTypeFiles = GameMain.Instance.GetFilesOfType(ContentType.LocationTypes); + if (!locationTypeFiles.Any()) + { + DebugConsole.ThrowError("No location types configured in any of the selected content packages. Attempting to load from the vanilla content package..."); + locationTypeFiles = ContentPackage.GetFilesOfType(GameMain.VanillaContent.ToEnumerable(), ContentType.LocationTypes); + if (!locationTypeFiles.Any()) + { + throw new Exception("No location types configured in any of the selected content packages. Please try uninstalling mods or reinstalling the game."); + } + } + foreach (ContentFile file in locationTypeFiles) { XDocument doc = XMLExtensions.TryLoadXml(file.Path); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index a36437403..83bbc2993 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -8,7 +8,9 @@ using Voronoi2; namespace Barotrauma { partial class Map - { + { + public bool AllowDebugTeleport; + private readonly MapGenerationParams generationParams; private Location furthestDiscoveredLocation; @@ -112,7 +114,15 @@ namespace Barotrauma } else { - DebugConsole.ThrowError("Error while loading the map (start location index out of bounds)."); + DebugConsole.AddWarning($"Error while loading the map. Start location index out of bounds (index: {startLocationindex}, location count: {Locations.Count})."); + foreach (Location location in Locations) + { + if (!location.Type.HasOutpost) { continue; } + if (StartLocation == null || location.MapPosition.X < StartLocation.MapPosition.X) + { + StartLocation = location; + } + } } int endLocationindex = element.GetAttributeInt("endlocation", -1); if (endLocationindex > 0 && endLocationindex < Locations.Count) @@ -121,7 +131,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError("Error while loading the map (end location index out of bounds)."); + DebugConsole.AddWarning($"Error while loading the map. End location index out of bounds (index: {endLocationindex}, location count: {Locations.Count})."); foreach (Location location in Locations) { if (EndLocation == null || location.MapPosition.X > EndLocation.MapPosition.X) @@ -166,6 +176,7 @@ namespace Barotrauma CurrentLocation = StartLocation = furthestDiscoveredLocation = location; } } + System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation."); CurrentLocation.CreateStore(); CurrentLocation.Discovered = true; @@ -640,7 +651,24 @@ namespace Barotrauma } } - public void ProgressWorld() + public void ProgressWorld(CampaignMode.TransitionType transitionType, float roundDuration) + { + //one step per 10 minutes of play time + int steps = (int)Math.Floor(roundDuration / (60.0f * 10.0f)); + if (transitionType == CampaignMode.TransitionType.ProgressToNextLocation || + transitionType == CampaignMode.TransitionType.ProgressToNextEmptyLocation) + { + //at least one step when progressing to the next location, regardless of how long the round took + steps = Math.Max(1, steps); + } + steps = Math.Min(steps, 5); + for (int i = 0; i < steps; i++) + { + ProgressWorld(); + } + } + + private void ProgressWorld() { foreach (Location location in Locations) { @@ -651,6 +679,8 @@ namespace Barotrauma furthestDiscoveredLocation = location; } + if (location == CurrentLocation || location == SelectedLocation) { continue; } + //find which types of locations this one can change to List allowedTypeChanges = new List(); List readyTypeChanges = new List(); @@ -844,6 +874,15 @@ namespace Barotrauma { SelectLocation(Connections[currentLocationConnection].OtherLocation(CurrentLocation)); } + else + { + //this should not be possible, you can't enter non-outpost locations (= natural formations) + if (CurrentLocation != null && !CurrentLocation.Type.HasOutpost && SelectedConnection == null) + { + DebugConsole.AddWarning($"Error while loading campaign map state. Submarine in a location with no outpost ({CurrentLocation.Name}). Loading the first adjacent connection..."); + SelectLocation(CurrentLocation.Connections[0].OtherLocation(CurrentLocation)); + } + } } public void Save(XElement element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs index cded6a02b..b745dff26 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs @@ -128,7 +128,7 @@ namespace Barotrauma public static void Init() { - var files = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.MapGenerationParameters); + var files = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.MapGenerationParameters); if (!files.Any()) { DebugConsole.ThrowError("No map generation parameters found in the selected content packages!"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index 9cd0006a5..99d83d7b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -2,7 +2,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index a1757098b..09b985907 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -3,7 +3,6 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; using System.Linq; namespace Barotrauma @@ -68,7 +67,7 @@ namespace Barotrauma private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false) { - var outpostModuleFiles = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.OutpostModule); + var outpostModuleFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.OutpostModule); //load the infos of the outpost module files List outpostModules = new List(); @@ -110,7 +109,7 @@ namespace Barotrauma selectedModules.Clear(); //select which module types the outpost should consist of - var pendingModuleFlags = onlyEntrance ? new List() : SelectModules(outpostModules, generationParams); + var pendingModuleFlags = onlyEntrance ? new List() { generationParams.ModuleCounts.First().Key } : SelectModules(outpostModules, generationParams); foreach (string flag in pendingModuleFlags.Distinct().ToList()) { if (flag.Equals("none", StringComparison.OrdinalIgnoreCase)) { continue; } @@ -132,13 +131,11 @@ namespace Barotrauma } } - //the first airlock is forced to spawn manually, remove it from the list of pending modules - if (pendingModuleFlags.Contains("airlock")) - { - pendingModuleFlags.Remove("airlock"); - } + //the first module is spawned separately, remove it from the list of pending modules + string initialModuleFlag = pendingModuleFlags.FirstOrDefault() ?? "airlock"; + pendingModuleFlags.Remove(initialModuleFlag); - var initialModule = GetRandomModule(outpostModules, "airlock", locationType); + var initialModule = GetRandomModule(outpostModules, initialModuleFlag, locationType); if (initialModule == null) { throw new Exception("Failed to generate an outpost (no airlock modules found)."); @@ -155,7 +152,7 @@ namespace Barotrauma } selectedModules.Add(new PlacedModule(initialModule, null, OutpostModuleInfo.GapPosition.None)); - selectedModules.Last().FulfilledModuleTypes.Add("airlock"); + selectedModules.Last().FulfilledModuleTypes.Add(initialModuleFlag); AppendToModule(selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, selectedModules, locationType); if (pendingModuleFlags.Any(flag => !flag.Equals("none", StringComparison.OrdinalIgnoreCase))) { @@ -202,7 +199,7 @@ namespace Barotrauma DebugConsole.NewMessage("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead..."); - var outpostFiles = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Outpost); + var outpostFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Outpost); if (!outpostFiles.Any()) { throw new Exception("Failed to generate an outpost. Could not generate an outpost from the available outpost modules and there are no pre-built outposts available."); @@ -354,6 +351,9 @@ namespace Barotrauma int totalModuleCount = generationParams.TotalModuleCount; var pendingModuleFlags = new List(); bool availableModulesFound = true; + + string initialModuleFlag = generationParams.ModuleCounts.FirstOrDefault().Key; + pendingModuleFlags.Add(initialModuleFlag); while (pendingModuleFlags.Count < totalModuleCount && availableModulesFound) { availableModulesFound = false; @@ -378,8 +378,13 @@ namespace Barotrauma //don't place "none" modules at the end because // a. "filler rooms" at the end of a hallway are pointless // b. placing the unnecessary filler rooms first give more options for the placement of the more important modules - pendingModuleFlags.Insert(Rand.Int(pendingModuleFlags.Count - 1, Rand.RandSync.Server),"none"); + pendingModuleFlags.Insert(Rand.Int(pendingModuleFlags.Count - 1, Rand.RandSync.Server), "none"); } + + //make sure the initial module is inserted first + pendingModuleFlags.Remove(initialModuleFlag); + pendingModuleFlags.Insert(0, initialModuleFlag); + return pendingModuleFlags; } @@ -615,7 +620,7 @@ namespace Barotrauma Vector2 moveDir = GetMoveDir(module.ThisGapPosition); Vector2 moveStep = moveDir * 50.0f; Vector2 currentMove = Vector2.Zero; - float maxMoveAmount = 1500.0f; + float maxMoveAmount = 2000.0f; List subsequentModules2 = new List(); GetSubsequentModules(module, movableModules, ref subsequentModules2); @@ -1342,7 +1347,7 @@ namespace Barotrauma Dictionary selectedCharacters = new Dictionary(); foreach (HumanPrefab humanPrefab in outpost.Info.OutpostGenerationParams.GetHumanPrefabs(Rand.RandSync.Server)) { - var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.Server)); + var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.Server), randSync: Rand.RandSync.Server); if (location != null && location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier())) { killedCharacters.Add(humanPrefab); @@ -1357,7 +1362,7 @@ namespace Barotrauma int tries = 0; while (tries < 100) { - var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: killedCharacter.GetJobPrefab(Rand.RandSync.Server)); + var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: killedCharacter.GetJobPrefab(Rand.RandSync.Server), randSync: Rand.RandSync.Server); if (!location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier())) { selectedCharacters.Add(killedCharacter, characterInfo); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 2af82000f..997cd6483 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -19,7 +19,7 @@ namespace Barotrauma public Rectangle rect; public float damage; public Gap gap; - + public WallSection(Rectangle rect) { System.Diagnostics.Debug.Assert(rect.Width > 0 && rect.Height > 0); @@ -58,7 +58,7 @@ namespace Barotrauma { get; private set; - } + } public override Sprite Sprite { @@ -215,6 +215,7 @@ namespace Barotrauma set { textureOffset = value; } } + private Rectangle defaultRect; /// /// Unscaled rect @@ -309,6 +310,12 @@ namespace Barotrauma public override void Move(Vector2 amount) { + if (!MathUtils.IsValid(amount)) + { + DebugConsole.ThrowError($"Attempted to move a structure by an invalid amount ({amount})\n{Environment.StackTrace}"); + return; + } + base.Move(amount); for (int i = 0; i < Sections.Length; i++) @@ -397,7 +404,7 @@ namespace Barotrauma if (StairDirection != Direction.None) { CreateStairBodies(); - } + } } } @@ -473,7 +480,7 @@ namespace Barotrauma { int xsections = 1, ysections = 1; int width = rect.Width, height = rect.Height; - + if (!HasBody) { if (FlippedX && IsHorizontal) @@ -623,11 +630,11 @@ namespace Barotrauma if (StairDirection == Direction.Left) { - return MathUtils.LineToPointDistance(new Vector2(WorldRect.X, WorldRect.Y), new Vector2(WorldRect.Right, WorldRect.Y - WorldRect.Height), position) < 40.0f; + return MathUtils.LineToPointDistanceSquared(new Vector2(WorldRect.X, WorldRect.Y), new Vector2(WorldRect.Right, WorldRect.Y - WorldRect.Height), position) < 1600.0f; } else { - return MathUtils.LineToPointDistance(new Vector2(WorldRect.X, WorldRect.Y - rect.Height), new Vector2(WorldRect.Right, WorldRect.Y), position) < 40.0f; + return MathUtils.LineToPointDistanceSquared(new Vector2(WorldRect.X, WorldRect.Y - rect.Height), new Vector2(WorldRect.Right, WorldRect.Y), position) < 1600.0f; } } } @@ -726,7 +733,7 @@ namespace Barotrauma return Sections[sectionIndex]; } - + public bool SectionBodyDisabled(int sectionIndex) { if (sectionIndex < 0 || sectionIndex >= Sections.Length) return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index d6e7bdb40..97036cc5f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -289,7 +289,6 @@ namespace Barotrauma public WreckAI WreckAI { get; private set; } public bool CreateWreckAI() { - MakeWreck(); WreckAI = new WreckAI(this); return WreckAI != null; } @@ -946,9 +945,29 @@ namespace Barotrauma { if (item.Submarine != this) { continue; } var steering = item.GetComponent(); - if (steering == null) { continue; } + if (steering == null || item.Connections == null) { continue; } + + //find all the engines and pumps the nav terminal is connected to + List connectedItems = new List(); + foreach (Connection c in item.Connections) + { + if (c.IsPower) { continue; } + connectedItems.AddRange(item.GetConnectedComponentsRecursive(c).Select(engine => engine.Item)); + connectedItems.AddRange(item.GetConnectedComponentsRecursive(c).Select(pump => pump.Item)); + } + + //if more than 50% of the connected engines/pumps are in another sub, + //assume this terminal is used to remotely control something and don't automatically enable autopilot + if (connectedItems.Count(it => it.Submarine != item.Submarine) > connectedItems.Count / 2) + { + continue; + } + steering.MaintainPos = true; steering.AutoPilot = true; +#if SERVER + steering.UnsentChanges = true; +#endif } } @@ -1369,8 +1388,17 @@ namespace Barotrauma foreach (MapEntity e in MapEntity.mapEntityList.OrderBy(e => e.ID)) { - if (e.Submarine != this || !e.ShouldBeSaved) continue; - if (e is Item item && item.FindParentInventory(inv => inv is CharacterInventory) != null) continue; + if (!e.ShouldBeSaved) { continue; } + if (e is Item item) + { + if (item.FindParentInventory(inv => inv is CharacterInventory) != null) { continue; } + if (e.Submarine != this && item.GetRootContainer()?.Submarine != this) { continue; } + } + else + { + if (e.Submarine != this) { continue; } + } + e.Save(element); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 94d312b6c..c3abe40e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -109,7 +109,7 @@ namespace Barotrauma this.submarine = sub; Body farseerBody = null; - if (!Hull.hullList.Any()) + if (!Hull.hullList.Any(h => h.Submarine == sub)) { farseerBody = GameMain.World.CreateRectangle(1.0f, 1.0f, 1.0f); if (showWarningMessages) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index 5586c5f6e..c00c87067 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -2,7 +2,11 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.ComponentModel; +#if DEBUG +using System.IO; +#else using Barotrauma.IO; +#endif using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -149,7 +153,7 @@ namespace Barotrauma get { if (requiredContentPackagesInstalled.HasValue) { return requiredContentPackagesInstalled.Value; } - return RequiredContentPackages.All(cp => GameMain.SelectedPackages.Any(cp2 => cp2.Name == cp)); + return RequiredContentPackages.All(cp => GameMain.Config.AllEnabledPackages.Any(cp2 => cp2.Name == cp)); } set { @@ -512,7 +516,7 @@ namespace Barotrauma public static void RefreshSavedSubs() { var contentPackageSubs = ContentPackage.GetFilesOfType( - GameMain.Config.SelectedContentPackages, + GameMain.Config.AllEnabledPackages, ContentType.Submarine, ContentType.Outpost, ContentType.OutpostModule, ContentType.Wreck); for (int i = savedSubmarines.Count - 1; i >= 0; i--) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 706d76c29..713775336 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -707,7 +707,7 @@ namespace Barotrauma int i = 0; foreach (MapEntity e in linkedTo) { - if (!e.ShouldBeSaved || e.Removed) { continue; } + if (!e.ShouldBeSaved || (e.Removed != Removed)) { continue; } if (e.Submarine?.Info.Type != Submarine?.Info.Type) { continue; } element.Add(new XAttribute("linkedto" + i, e.ID)); i += 1; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs index ab9db267c..4bc42b15c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs @@ -1,4 +1,5 @@ -using System; +using Barotrauma.Steam; +using System; using System.Collections.Generic; using System.Linq; @@ -7,11 +8,17 @@ namespace Barotrauma.Networking partial class BannedPlayer { public string Name; - public string IP; public bool IsRangeBan; + public string EndPoint; public bool IsRangeBan; public UInt64 SteamID; public string Reason; public DateTime? ExpirationTime; public UInt16 UniqueIdentifier; + + private void ParseEndPointAsSteamId() + { + ulong endPointAsSteamId = SteamManager.SteamIDStringToUInt64(EndPoint); + if (endPointAsSteamId != 0 && SteamID == 0) { SteamID = endPointAsSteamId; } + } } partial class BanList @@ -22,9 +29,9 @@ namespace Barotrauma.Networking get { return bannedPlayers.Select(bp => bp.Name); } } - public IEnumerable BannedIPs + public IEnumerable BannedEndPoints { - get { return bannedPlayers.Select(bp => bp.IP); } + get { return bannedPlayers.Select(bp => bp.EndPoint).Where(endPoint => !string.IsNullOrEmpty(endPoint)); } } partial void InitProjectSpecific(); @@ -38,6 +45,7 @@ namespace Barotrauma.Networking public static string ToRange(string ip) { + if (SteamManager.SteamIDStringToUInt64(ip) != 0) { return ip; } for (int i = ip.Length - 1; i > 0; i--) { if (ip[i] == '.') diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index 3d0ccd14e..37fd779ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -14,6 +14,8 @@ namespace Barotrauma.Networking public byte ID; public UInt64 SteamID; + public string Language; + public UInt16 Ping; public string PreferredJob; @@ -196,11 +198,13 @@ namespace Barotrauma.Networking { votes[i] = null; } + + kickVoters.Clear(); } public void AddKickVote(Client voter) { - if (!kickVoters.Contains(voter)) kickVoters.Add(voter); + if (voter != null && !kickVoters.Contains(voter)) { kickVoters.Add(voter); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index 0b3e9ebb4..69483c10b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -3,6 +3,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -10,13 +11,13 @@ namespace Barotrauma { private enum SpawnableType { Item, Character }; - interface IEntitySpawnInfo + public interface IEntitySpawnInfo { Entity Spawn(); void OnSpawned(Entity entity); } - class ItemSpawnInfo : IEntitySpawnInfo + public class ItemSpawnInfo : IEntitySpawnInfo { public readonly ItemPrefab Prefab; @@ -241,7 +242,7 @@ namespace Barotrauma public void AddToRemoveQueue(Entity entity) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (removeQueue.Contains(entity) || entity.Removed || entity == null) { return; } + if (removeQueue.Contains(entity) || entity.Removed || entity == null || entity.IdFreed) { return; } if (entity is Character) { Character character = entity as Character; @@ -263,13 +264,33 @@ namespace Barotrauma if (removeQueue.Contains(item) || item.Removed) { return; } removeQueue.Enqueue(item); - if (item.ContainedItems == null) return; - foreach (Item containedItem in item.ContainedItems) + var containedItems = item.OwnInventory?.Items; + if (containedItems == null) { return; } + foreach (Item containedItem in containedItems) { - if (containedItem != null) AddToRemoveQueue(containedItem); + if (containedItem != null) + { + AddToRemoveQueue(containedItem); + } } } + /// + /// Are there any entities in the spawn queue that match the given predicate + /// + public bool IsInSpawnQueue(Predicate predicate) + { + return spawnQueue.Any(s => predicate(s)); + } + + /// + /// How many entities in the spawn queue match the given predicate + /// + public int CountSpawnQueue(Predicate predicate) + { + return spawnQueue.Count(s => predicate(s)); + } + public void Update() { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs index 40c7a1104..ddc1107a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs @@ -721,10 +721,24 @@ namespace Barotrauma.Networking public class ReadWriteMessage : IWriteMessage, IReadMessage { - private byte[] buf = new byte[MsgConstants.InitialBufferSize]; + private byte[] buf; private int seekPos = 0; private int lengthBits = 0; + public ReadWriteMessage() + { + buf = new byte[MsgConstants.InitialBufferSize]; + seekPos = 0; + lengthBits = 0; + } + + public ReadWriteMessage(byte[] b, int sPos, int lBits, bool copyBuf) + { + buf = copyBuf ? (byte[])b.Clone() : b; + seekPos = sPos; + lengthBits = lBits; + } + public int BitPosition { get diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/LidgrenConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/LidgrenConnection.cs index 8ab15b2e1..94c63e5a4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/LidgrenConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/LidgrenConnection.cs @@ -33,5 +33,16 @@ namespace Barotrauma.Networking SteamID = steamId; EndPointString = IPString; } + + public override bool EndpointMatches(string endPoint) + { + if (IPEndPoint?.Address == null) { return false; } + if (!IPAddress.TryParse(endPoint, out IPAddress addr)) { return false; } + + IPAddress ip1 = IPEndPoint.Address.IsIPv4MappedToIPv6 ? IPEndPoint.Address.MapToIPv4() : IPEndPoint.Address; + IPAddress ip2 = addr.IsIPv4MappedToIPv6 ? addr.MapToIPv4() : addr; + + return ip1.ToString() == ip2.ToString(); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs index 63b8ba636..499d99223 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs @@ -27,6 +27,13 @@ namespace Barotrauma.Networking protected set; } + public string Language + { + get; set; + } + + public abstract bool EndpointMatches(string endPoint); + public NetworkConnectionStatus Status = NetworkConnectionStatus.Disconnected; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/PipeConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/PipeConnection.cs index 605ca7756..ef2fee23c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/PipeConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/PipeConnection.cs @@ -1,12 +1,19 @@ -using System; +using Barotrauma.Steam; +using System; namespace Barotrauma.Networking { public class PipeConnection : NetworkConnection { - public PipeConnection() + public PipeConnection(ulong steamId) { EndPointString = "PIPE"; + SteamID = steamId; + } + + public override bool EndpointMatches(string endPoint) + { + return SteamManager.SteamIDStringToUInt64(endPoint) == SteamID || endPoint == "PIPE"; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/SteamP2PConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/SteamP2PConnection.cs index 4b8f8b00d..a65506196 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/SteamP2PConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/SteamP2PConnection.cs @@ -1,4 +1,5 @@ -using System; +using Barotrauma.Steam; +using System; namespace Barotrauma.Networking { @@ -9,7 +10,7 @@ namespace Barotrauma.Networking public SteamP2PConnection(string name, UInt64 steamId) { SteamID = steamId; - EndPointString = SteamID.ToString(); + EndPointString = SteamManager.SteamIDUInt64ToString(SteamID); Name = name; Heartbeat(); } @@ -23,5 +24,10 @@ namespace Barotrauma.Networking { Timeout = TimeoutThreshold; } + + public override bool EndpointMatches(string endPoint) + { + return SteamManager.SteamIDStringToUInt64(endPoint) == SteamID; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index 2fe229d6f..230398ede 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -18,9 +18,9 @@ namespace Barotrauma.Networking Returning } - private NetworkMember networkMember; - private Steering shuttleSteering; - private List shuttleDoors; + private readonly NetworkMember networkMember; + private readonly Steering shuttleSteering; + private readonly List shuttleDoors; //items created during respawn //any respawn items left in the shuttle are removed when the shuttle despawns @@ -55,8 +55,6 @@ namespace Barotrauma.Networking public State CurrentState { get; private set; } - private DateTime despawnTime; - private float maxTransportTime; private float updateReturnTimer; @@ -187,8 +185,6 @@ namespace Barotrauma.Networking } } - partial void DispatchShuttle(); - partial void UpdateReturningProjSpecific(); private IEnumerable ForceShuttleToPos(Vector2 position, float speed) @@ -217,7 +213,10 @@ namespace Barotrauma.Networking private void ResetShuttle() { ReturnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, milliseconds: (int)(maxTransportTime * 1000)); + +#if SERVER despawnTime = ReturnTime + new TimeSpan(0, 0, seconds: 30); +#endif if (RespawnShuttle == null) return; @@ -244,29 +243,23 @@ namespace Barotrauma.Networking foreach (Structure wall in Structure.WallList) { - if (wall.Submarine != RespawnShuttle) continue; - + if (wall.Submarine != RespawnShuttle) { continue; } for (int i = 0; i < wall.SectionCount; i++) { wall.AddDamage(i, -100000.0f); } } - var shuttleGaps = Gap.GapList.FindAll(g => g.Submarine == RespawnShuttle && g.ConnectedWall != null); - shuttleGaps.ForEach(g => Spawner.AddToRemoveQueue(g)); - foreach (Hull hull in Hull.hullList) { - if (hull.Submarine != RespawnShuttle) continue; - + if (hull.Submarine != RespawnShuttle) { continue; } hull.OxygenPercentage = 100.0f; hull.WaterVolume = 0.0f; } foreach (Character c in Character.CharacterList) { - if (c.Submarine != RespawnShuttle) continue; - + if (c.Submarine != RespawnShuttle) { continue; } #if CLIENT if (Character.Controlled == c) Character.Controlled = null; #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs index a8f02444a..d3d364a3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs @@ -19,11 +19,11 @@ namespace Barotrauma.Networking { if (type.HasFlag(MessageType.Chat)) { - Text = $"[{DateTime.Now.ToString()}]\n {text}"; + Text = $"[{DateTime.Now}]\n {text}"; } else { - Text = $"[{DateTime.Now.ToString()}]\n {TextManager.GetServerMessage(text)}"; + Text = $"[{DateTime.Now}]\n {TextManager.GetServerMessage(text)}"; } RichData = RichTextData.GetRichTextData(Text, out SanitizedText); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index d4901fdf3..9b7fd2600 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -460,13 +460,6 @@ namespace Barotrauma.Networking } } - [Serialize(true, true)] - public bool EndRoundAtLevelEnd - { - get; - private set; - } - [Serialize(true, true)] public bool SaveServerLogs { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs index 8b45a594c..5da1564b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs @@ -148,7 +148,6 @@ namespace Barotrauma.Steam if (Steamworks.SteamServer.IsValid) { Steamworks.SteamServer.RunCallbacks(); } SteamAchievementManager.Update(deltaTime); - UpdateProjectSpecific(deltaTime); } public static void ShutDown() @@ -160,6 +159,56 @@ namespace Barotrauma.Steam isInitialized = false; } + public static IEnumerable ParseWorkshopIds(string workshopIdData) + { + string[] workshopIds = workshopIdData.Split(','); + foreach (string id in workshopIds) + { + if (ulong.TryParse(id, out ulong idCast)) + { + yield return idCast; + } + else + { + yield return 0; + } + } + } + + public static IEnumerable WorkshopUrlsToIds(IEnumerable urls) + { + return urls.Select((u) => + { + if (string.IsNullOrEmpty(u)) + { + return (ulong)0; + } + else + { + return GetWorkshopItemIDFromUrl(u); + } + }); + } + + public static ulong GetWorkshopItemIDFromUrl(string url) + { + try + { + Uri uri = new Uri(url); + string idStr = HttpUtility.ParseQueryString(uri.Query)["id"]; + if (ulong.TryParse(idStr, out ulong id)) + { + return id; + } + } + catch (Exception e) + { + DebugConsole.ThrowError("Failed to get Workshop item ID from the url \"" + url + "\"!", e); + } + + return 0; + } + public static UInt64 SteamIDStringToUInt64(string str) { if (string.IsNullOrWhiteSpace(str)) { return 0; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index 9dfad1d37..b252704ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -88,9 +88,8 @@ namespace Barotrauma DebugConsole.ThrowError($"Prefab \"{prefab.OriginalName}\" has no identifier!"); } - List list = null; List newList = null; - if (!prefabs.TryGetValue(prefab.Identifier, out list)) + if (!prefabs.TryGetValue(prefab.Identifier, out List list)) { newList = new List(); newList.Add(null); list = newList; @@ -177,7 +176,8 @@ namespace Barotrauma { if (list.Count <= 1) { return; } - var newList = list.Skip(1).OrderByDescending(p => GameMain.Config.SelectedContentPackages.IndexOf(p.ContentPackage)).ToList(); + var newList = list.Skip(1) + .OrderByDescending(p => GameMain.Config.EnabledRegularPackages.IndexOf(p.ContentPackage)).ToList(); list.RemoveRange(1, list.Count - 1); list.AddRange(newList); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index acd892ef6..6f02eb491 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -200,7 +200,7 @@ namespace Barotrauma { screenTargetPos.X = GameMain.GraphicsWidth * (CharacterHealth.OpenHealthWindow.Alignment == Alignment.Left ? 0.75f : 0.25f); } - else if (ConversationAction.IsDialogOpen != null) + else if (ConversationAction.IsDialogOpen) { screenTargetPos.Y = GameMain.GraphicsHeight * 0.4f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 7c4892667..0ea24c307 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -702,8 +702,8 @@ namespace Barotrauma { if (!subElement.Name.ToString().Equals("upgrade", StringComparison.OrdinalIgnoreCase)) { continue; } var upgradeVersion = new Version(subElement.GetAttributeString("gameversion", "0.0.0.0")); - if (savedVersion >= upgradeVersion) { continue; } - + if (savedVersion >= upgradeVersion) { continue; } + foreach (XAttribute attribute in subElement.Attributes()) { string attributeName = attribute.Name.ToString().ToLowerInvariant(); @@ -756,9 +756,17 @@ namespace Barotrauma if (entity is Item item2) { XElement componentElement = subElement.FirstElement(); - if (componentElement == null) continue; + if (componentElement == null) { continue; } ItemComponent itemComponent = item2.Components.First(c => c.Name == componentElement.Name.ToString()); - if (itemComponent == null) continue; + if (itemComponent == null) { continue; } + foreach (XAttribute attribute in componentElement.Attributes()) + { + string attributeName = attribute.Name.ToString().ToLowerInvariant(); + if (itemComponent.SerializableProperties.TryGetValue(attributeName, out SerializableProperty property)) + { + FixValue(property, itemComponent, attribute); + } + } foreach (XElement element in componentElement.Elements()) { switch (element.Name.ToString().ToLowerInvariant()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 4ed031b1f..feacc748a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -140,20 +140,20 @@ namespace Barotrauma if (element.Attribute(name) == null) continue; float val; - try { - if (!Single.TryParse(element.Attribute(name).Value, NumberStyles.Float, CultureInfo.InvariantCulture, out val)) + string strVal = element.Attribute(name).Value; + if (strVal.LastOrDefault() == 'f') { - continue; + strVal = strVal.Substring(0, strVal.Length - 1); } + val = float.Parse(strVal, CultureInfo.InvariantCulture); } catch (Exception e) { DebugConsole.ThrowError("Error in " + element + "!", e); continue; } - return val; } @@ -165,13 +165,14 @@ namespace Barotrauma if (element?.Attribute(name) == null) return defaultValue; float val = defaultValue; - try { - if (!Single.TryParse(element.Attribute(name).Value, NumberStyles.Float, CultureInfo.InvariantCulture, out val)) + string strVal = element.Attribute(name).Value; + if (strVal.LastOrDefault() == 'f') { - return defaultValue; + strVal = strVal.Substring(0, strVal.Length - 1); } + val = float.Parse(strVal, CultureInfo.InvariantCulture); } catch (Exception e) { @@ -189,7 +190,12 @@ namespace Barotrauma try { - val = Single.Parse(attribute.Value, CultureInfo.InvariantCulture); + string strVal = attribute.Value; + if (strVal.LastOrDefault() == 'f') + { + strVal = strVal.Substring(0, strVal.Length - 1); + } + val = float.Parse(strVal, CultureInfo.InvariantCulture); } catch (Exception e) { @@ -212,8 +218,12 @@ namespace Barotrauma { try { - float val = Single.Parse(splitValue[i], CultureInfo.InvariantCulture); - floatValue[i] = val; + string strVal = splitValue[i]; + if (strVal.LastOrDefault() == 'f') + { + strVal = strVal.Substring(0, strVal.Length - 1); + } + floatValue[i] = float.Parse(strVal, CultureInfo.InvariantCulture); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index 2233141e2..67069c0ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -1,7 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; namespace Barotrauma @@ -11,28 +13,39 @@ namespace Barotrauma public readonly DelayedEffect Parent; public readonly Entity Entity; public readonly Vector2? WorldPosition; + public readonly Vector2? StartPosition; public readonly List Targets; - public float StartTimer; + public float Delay; - public DelayedListElement(DelayedEffect parentEffect, Entity parentEntity, IEnumerable targets, float delay, Vector2? worldPosition) + public DelayedListElement(DelayedEffect parentEffect, Entity parentEntity, IEnumerable targets, float delay, Vector2? worldPosition, Vector2? startPosition) { Parent = parentEffect; Entity = parentEntity; Targets = new List(targets); - StartTimer = delay; + Delay = delay; WorldPosition = worldPosition; + StartPosition = startPosition; } } class DelayedEffect : StatusEffect { public static readonly List DelayList = new List(); + private enum DelayTypes { timer = 0, reachcursor = 1 } + + private DelayTypes delayType; private float delay; public DelayedEffect(XElement element, string parentDebugName) : base(element, parentDebugName) { - delay = element.GetAttributeFloat("delay", 1.0f); + delayType = (DelayTypes)Enum.Parse(typeof(DelayTypes), element.GetAttributeString("delaytype", "timer")); + switch (delayType) + { + case DelayTypes.timer: + delay = element.GetAttributeFloat("delay", 1.0f); + break; + } } public override void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition = null) @@ -42,14 +55,36 @@ namespace Barotrauma if (targetIdentifiers != null && !IsValidTarget(target)) { return; } if (!HasRequiredConditions(target.ToEnumerable())) { return; } - DelayList.Add(new DelayedListElement(this, entity, target.ToEnumerable(), delay, worldPosition)); + switch (delayType) + { + case DelayTypes.timer: + DelayList.Add(new DelayedListElement(this, entity, target.ToEnumerable(), delay, worldPosition, null)); + break; + case DelayTypes.reachcursor: + Projectile projectile = (entity as Item)?.GetComponent(); + if (projectile == null) + { + DebugConsole.NewMessage("Non-projectile using a delaytype of reachcursor", Color.Red, false, true); + return; + } + + if (projectile.User == null) + { + DebugConsole.NewMessage("Projectile: '" + projectile.Name + "' missing user to determine distance", Color.Red, false, true); + return; + } + + DelayList.Add(new DelayedListElement(this, entity, target.ToEnumerable(), Vector2.Distance(entity.WorldPosition, projectile.User.CursorWorldPosition), worldPosition, entity.WorldPosition)); + break; + } } public override void Apply(ActionType type, float deltaTime, Entity entity, IEnumerable targets, Vector2? worldPosition = null) { if (this.type != type || !HasRequiredItems(entity)) { return; } if (!Stackable && DelayList.Any(d => d.Parent == this && d.Targets.SequenceEqual(targets))) { return; } - + if (delayType == DelayTypes.reachcursor && Character.Controlled == null) return; + currentTargets.Clear(); foreach (ISerializableEntity target in targets) { @@ -63,7 +98,32 @@ namespace Barotrauma if (!HasRequiredConditions(currentTargets)) { return; } - DelayList.Add(new DelayedListElement(this, entity, currentTargets, delay, worldPosition)); + switch (delayType) + { + case DelayTypes.timer: + DelayList.Add(new DelayedListElement(this, entity, targets, delay, worldPosition, null)); + break; + case DelayTypes.reachcursor: + Projectile projectile = (entity as Item)?.GetComponent(); + if (projectile == null) + { +#if DEBUG + DebugConsole.NewMessage("Non-projectile using a delaytype of reachcursor", Color.Red, false, true); +#endif + return; + } + + if (projectile.User == null) + { +#if DEBUG + DebugConsole.NewMessage("Projectile " + projectile.Name + "missing user", Color.Red, false, true); +#endif + return; + } + + DelayList.Add(new DelayedListElement(this, entity, targets, Vector2.Distance(entity.WorldPosition, projectile.User.CursorWorldPosition), worldPosition, entity.WorldPosition)); + break; + } } public static void Update(float deltaTime) @@ -77,9 +137,16 @@ namespace Barotrauma continue; } - element.StartTimer -= deltaTime; - - if (element.StartTimer > 0.0f) { continue; } + switch (element.Parent.delayType) + { + case DelayTypes.timer: + element.Delay -= deltaTime; + if (element.Delay > 0.0f) { continue; } + break; + case DelayTypes.reachcursor: + if (Vector2.Distance(element.Entity.WorldPosition, element.StartPosition.Value) < element.Delay) continue; + break; + } element.Parent.Apply(deltaTime, element.Entity, element.Targets, element.WorldPosition); DelayList.Remove(element); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 99985975f..7032585fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -12,6 +12,11 @@ namespace Barotrauma { public readonly StatusEffect Parent; public readonly Entity Entity; + public float Duration + { + get; + private set; + } public readonly List Targets; public Character User { get; private set; } @@ -22,13 +27,13 @@ namespace Barotrauma Parent = parentEffect; Entity = parentEntity; Targets = new List(targets); - Timer = duration; + Timer = Duration = duration; User = user; } public void Reset(float duration, Character newUser) { - Timer = duration; + Timer = Duration = duration; User = newUser; } } @@ -126,7 +131,7 @@ namespace Barotrauma SerializableProperties = SerializableProperty.DeserializeProperties(this, element); if (string.IsNullOrEmpty(SpeciesName)) { - DebugConsole.ThrowError($"Invalid character spawn ({Name}) in StatusEffect \"{parentDebugName}\" - identifier not found in the element \"{element.ToString()}\""); + DebugConsole.ThrowError($"Invalid character spawn ({Name}) in StatusEffect \"{parentDebugName}\" - identifier not found in the element \"{element}\""); } } } @@ -165,7 +170,7 @@ namespace Barotrauma public readonly ActionType type = ActionType.OnActive; - private readonly Explosion explosion; + private readonly List explosions; private readonly List spawnItems; private readonly List spawnCharacters; @@ -224,7 +229,7 @@ namespace Barotrauma public static StatusEffect Load(XElement element, string parentDebugName) { - if (element.Attribute("delay") != null) + if (element.Attribute("delay") != null || element.Attribute("delaytype") != null) { return new DelayedEffect(element, parentDebugName); } @@ -238,6 +243,7 @@ namespace Barotrauma spawnItems = new List(); spawnCharacters = new List(); Afflictions = new List(); + explosions = new List(); reduceAffliction = new List>(); tags = new HashSet(element.GetAttributeString("tags", "").Split(',')); @@ -351,7 +357,7 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "explosion": - explosion = new Explosion(subElement, parentDebugName); + explosions.Add(new Explosion(subElement, parentDebugName)); break; case "fire": FireSize = subElement.GetAttributeFloat("size", 10.0f); @@ -600,28 +606,28 @@ namespace Barotrauma if (entity is Item item) { - if (targetIdentifiers.Contains("item")) return true; - if (item.HasTag(targetIdentifiers)) return true; - if (targetIdentifiers.Any(id => id == item.Prefab.Identifier)) return true; + if (targetIdentifiers.Contains("item")) { return true; } + if (item.HasTag(targetIdentifiers)) { return true; } + if (targetIdentifiers.Any(id => id.Equals(item.Prefab.Identifier, StringComparison.OrdinalIgnoreCase))) { return true; } } else if (entity is ItemComponent itemComponent) { - if (targetIdentifiers.Contains("itemcomponent")) return true; - if (itemComponent.Item.HasTag(targetIdentifiers)) return true; - if (targetIdentifiers.Any(id => id == itemComponent.Item.Prefab.Identifier)) return true; + if (targetIdentifiers.Contains("itemcomponent")) { return true; } + if (itemComponent.Item.HasTag(targetIdentifiers)) { return true; } + if (targetIdentifiers.Any(id => id.Equals(itemComponent.Item.Prefab.Identifier, StringComparison.OrdinalIgnoreCase))) { return true; } } else if (entity is Structure structure) { - if (targetIdentifiers.Contains("structure")) return true; - if (targetIdentifiers.Any(id => id == structure.Prefab.Identifier)) return true; + if (targetIdentifiers.Contains("structure")) { return true; } + if (targetIdentifiers.Any(id => id.Equals(structure.Prefab.Identifier, StringComparison.OrdinalIgnoreCase))) { return true; } } else if (entity is Character character) { - if (targetIdentifiers.Contains("character")) return true; - if (targetIdentifiers.Any(id => id == character.SpeciesName)) return true; + if (targetIdentifiers.Contains("character")) { return true; } + if (targetIdentifiers.Any(id => id.Equals(character.SpeciesName, StringComparison.OrdinalIgnoreCase))) { return true; } } - return targetIdentifiers.Any(id => id == entity.Name); + return targetIdentifiers.Any(id => id.Equals(entity.Name, StringComparison.OrdinalIgnoreCase)); } public void SetUser(Character user) @@ -688,6 +694,46 @@ namespace Barotrauma Apply(deltaTime, entity, currentTargets, worldPosition); } + private Hull GetHull(Entity entity) + { + Hull hull = null; + if (entity is Character character) + { + hull = character.AnimController.CurrentHull; + } + else if (entity is Item item) + { + hull = item.CurrentHull; + } + return hull; + } + + private Vector2 GetPosition(Entity entity, IEnumerable targets, Vector2? worldPosition = null) + { + Vector2 position = worldPosition ?? (entity.Removed ? Vector2.Zero : entity.WorldPosition); + if (worldPosition == null) + { + if (entity is Character c && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) + { + Limb limb = c.AnimController.GetLimb(l); + if (limb != null && !limb.Removed) + { + position = limb.WorldPosition; + } + } + else + { + var targetLimb = targets.FirstOrDefault(t => t is Limb) as Limb; + if (targetLimb != null && !targetLimb.Removed) + { + position = targetLimb.WorldPosition; + } + } + } + position += Offset; + return position; + } + protected void Apply(float deltaTime, Entity entity, IEnumerable targets, Vector2? worldPosition = null) { if (lifeTime > 0) @@ -696,30 +742,8 @@ namespace Barotrauma if (lifeTimer <= 0) { return; } } - Hull hull = null; - if (entity is Character) - { - hull = ((Character)entity).AnimController.CurrentHull; - } - else if (entity is Item) - { - hull = ((Item)entity).CurrentHull; - } - - Vector2 position = worldPosition ?? (entity.Removed ? Vector2.Zero : entity.WorldPosition); - if (worldPosition == null && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) - { - if (entity is Character c) - { - Limb limb = c.AnimController.GetLimb(l); - if (limb != null && !limb.Removed) - { - position = limb.WorldPosition; - } - } - } - position += Offset; - + Hull hull = GetHull(entity); + Vector2 position = GetPosition(entity, targets, worldPosition); foreach (ISerializableEntity serializableEntity in targets) { if (!(serializableEntity is Item item)) { continue; } @@ -783,9 +807,12 @@ namespace Barotrauma } } - if (explosion != null && entity != null) + if (entity != null) { - explosion.Explode(position, damageSource: entity, attacker: user); + foreach (Explosion explosion in explosions) + { + explosion.Explode(position, damageSource: entity, attacker: user); + } } foreach (ISerializableEntity target in targets) @@ -888,9 +915,10 @@ namespace Barotrauma break; case ItemSpawnInfo.SpawnPositionType.ThisInventory: { - if (entity is Character character) + if (entity is Character character && character.Inventory != null) { - if (character.Inventory != null && character.Inventory.Items.Any(it => it == null)) + int emptyCount = character.Inventory.Items.Count(it => it == null); + if (emptyCount - Entity.Spawner.CountSpawnQueue(spawnInfo => spawnInfo is EntitySpawner.ItemSpawnInfo itemSpawnInfo && itemSpawnInfo.Inventory == character.Inventory) > 0) { Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, character.Inventory); } @@ -898,9 +926,13 @@ namespace Barotrauma else if (entity is Item item) { var inventory = item?.GetComponent()?.Inventory; - if (inventory != null && inventory.Items.Any(it => it == null)) + if (inventory != null) { - Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, inventory); + int emptyCount = inventory.Items.Count(it => it == null); + if (emptyCount - Entity.Spawner.CountSpawnQueue(spawnInfo => spawnInfo is EntitySpawner.ItemSpawnInfo itemSpawnInfo && itemSpawnInfo.Inventory == inventory) > 0) + { + Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, inventory); + } } } } @@ -933,10 +965,10 @@ namespace Barotrauma } } - ApplyProjSpecific(deltaTime, entity, targets, hull, position); + ApplyProjSpecific(deltaTime, entity, targets, hull, position, playSound: true); } - partial void ApplyProjSpecific(float deltaTime, Entity entity, IEnumerable targets, Hull currentHull, Vector2 worldPosition); + partial void ApplyProjSpecific(float deltaTime, Entity entity, IEnumerable targets, Hull currentHull, Vector2 worldPosition, bool playSound); private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, object value, float deltaTime) { @@ -1060,6 +1092,13 @@ namespace Barotrauma } } + element.Parent.ApplyProjSpecific(deltaTime, + element.Entity, + element.Targets, + element.Parent.GetHull(element.Entity), + element.Parent.GetPosition(element.Entity, element.Targets), + playSound: element.Timer >= element.Duration); + element.Timer -= deltaTime; if (element.Timer > 0.0f) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 67b00acc3..7fa44d79e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -43,22 +43,22 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { Reactor reactor = item.GetComponent(); - if (reactor != null) roundData.Reactors.Add(reactor); + if (reactor != null) { roundData.Reactors.Add(reactor); } } } public static void Update(float deltaTime) { - if (GameMain.GameSession == null) return; + if (GameMain.GameSession == null) { return; } #if CLIENT - if (GameMain.Client != null) return; + if (GameMain.Client != null) { return; } #endif updateTimer -= deltaTime; - if (updateTimer > 0.0f) return; + if (updateTimer > 0.0f) { return; } updateTimer = UpdateInterval; - if (Level.Loaded != null) + if (Level.Loaded != null && roundData != null && Screen.Selected == GameMain.GameScreen) { if (GameMain.GameSession.EventManager.CurrentIntensity > 0.99f) { @@ -243,6 +243,14 @@ namespace Barotrauma UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName.Replace("boss", "") + "indoors"); } } + if (character.SpeciesName.EndsWith("_m")) + { + UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName.Replace("_m", "")); + if (character.CurrentHull != null) + { + UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName.Replace("_m", "") + "indoors"); + } + } if (character.HasEquippedItem("clownmask") && character.HasEquippedItem("clowncostume") && @@ -251,6 +259,11 @@ namespace Barotrauma UnlockAchievement(causeOfDeath.Killer, "killclown"); } + if (character.CharacterHealth?.GetAffliction("morbusinepoisoning") != null) + { + UnlockAchievement(causeOfDeath.Killer, "killpoison"); + } + if (causeOfDeath.DamageSource is Item item) { if (item.HasTag("tool")) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/HttpUtility.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/HttpUtility.cs new file mode 100644 index 000000000..1ec3d25a0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/HttpUtility.cs @@ -0,0 +1,57 @@ +// +// Formerly System.Web.HttpUtility, now it just vaguely resembles it +// +// Authors: +// Patrik Torstensson (Patrik.Torstensson@labs2.com) +// Wictor Wilén (decode/encode functions) (wictor@ibizkit.se) +// Tim Coleman (tim@timcoleman.com) +// Gonzalo Paniagua Javier (gonzalo@ximian.com) +// +// Copyright (C) 2005-2010 Novell, Inc (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System.Collections.Generic; + +namespace Barotrauma +{ + public sealed class HttpUtility + { + public static Dictionary ParseQueryString(string query) + { + Dictionary collection = new Dictionary(); + var splitGet = query.Split('?'); + if (splitGet.Length > 1) + { + var get = splitGet[1]; + foreach (string kvp in get.Split('&')) + { + var splitKeyValue = kvp.Split('='); + if (splitKeyValue.Length > 1) + { + collection.Add(splitKeyValue[0], splitKeyValue[1]); + } + } + } + return collection; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/IPExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/IPExtensions.cs index 954b6b127..e9934ec31 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/IPExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/IPExtensions.cs @@ -8,6 +8,7 @@ namespace Barotrauma { public static class IPExtensions { + //TODO: remove? //workaround for .NET Framework 4.5 bug; presumably fixed in later versions //see https://stackoverflow.com/questions/23608829/why-does-ipaddress-maptoipv4-throw-argumentoutofrangeexception public static IPAddress MapToIPv4NoThrow(this IPAddress address) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index d693662e4..46ea97dbc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -288,8 +288,13 @@ namespace Barotrauma return (r >= 0 && r <= 1) && (s >= 0 && s <= 1); } - // a1 is line1 start, a2 is line1 end, b1 is line2 start, b2 is line2 end public static bool GetLineIntersection(Vector2 a1, Vector2 a2, Vector2 b1, Vector2 b2, out Vector2 intersection) + { + return GetLineIntersection(a1, a2, b1, b2, false, out intersection); + } + + // a1 is line1 start, a2 is line1 end, b1 is line2 start, b2 is line2 end + public static bool GetLineIntersection(Vector2 a1, Vector2 a2, Vector2 b1, Vector2 b2, bool ignoreSegments, out Vector2 intersection) { intersection = Vector2.Zero; @@ -302,10 +307,10 @@ namespace Barotrauma Vector2 c = b1 - a1; float t = (c.X * d.Y - c.Y * d.X) / bDotDPerp; - if (t < 0 || t > 1) return false; + if ((t < 0 || t > 1) && !ignoreSegments) return false; float u = (c.X * b.Y - c.Y * b.X) / bDotDPerp; - if (u < 0 || u > 1) return false; + if ((u < 0 || u > 1) && !ignoreSegments) return false; intersection = a1 + t * b; return true; @@ -532,6 +537,21 @@ namespace Barotrauma Math.Sqrt(xDiff * xDiff + yDiff * yDiff)); } + public static float LineToPointDistanceSquared(Vector2 lineA, Vector2 lineB, Vector2 point) + { + float xDiff = lineB.X - lineA.X; + float yDiff = lineB.Y - lineA.Y; + + if (xDiff == 0 && yDiff == 0) + { + return Vector2.DistanceSquared(lineA, point); + } + + float numerator = xDiff * (lineA.Y - point.Y) - yDiff * (lineA.X - point.X); + return (numerator*numerator) / + (xDiff * xDiff + yDiff * yDiff); + } + public static bool CircleIntersectsRectangle(Vector2 circlePos, float radius, Rectangle rect) { int halfWidth = rect.Width / 2; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs index 5c0e9b27b..f5d9bf70e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs @@ -41,7 +41,11 @@ namespace Barotrauma { if (System.Threading.Thread.CurrentThread.ManagedThreadId != ThreadId) { +#if DEBUG throw new Exception("Unauthorized multithreaded access to RandSync.Server"); +#else + DebugConsole.ThrowError("Unauthorized multithreaded access to RandSync.Server\n" + Environment.StackTrace); +#endif } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReadOnlyListExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReadOnlyListExtensions.cs new file mode 100644 index 000000000..51d66c2b4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReadOnlyListExtensions.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + public static class ReadOnlyListExtensions + { + public static int IndexOf(this IReadOnlyList list, T elem) + { + for (int i = 0; i < list.Count; i++) + { + if (list[i].Equals(elem)) { return i; } + } + return -1; + } + + public static T Find(this IReadOnlyList list, Func predicate) + { + return list.FirstOrDefault(predicate); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index 509008557..fb5ae54bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -1,4 +1,3 @@ -using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -8,7 +7,9 @@ namespace Barotrauma.IO { static readonly string[] unwritableDirs = new string[] { "Content", "Data/ContentPackages" }; - public static bool CanWrite(string path, bool canWarn = true) + public static bool DevException; + + public static bool CanWrite(string path) { path = System.IO.Path.GetFullPath(path).CleanUpPath(); @@ -19,11 +20,7 @@ namespace Barotrauma.IO if (path.StartsWith(dir, StringComparison.InvariantCultureIgnoreCase)) { #if DEBUG - if (canWarn) - { - DebugConsole.NewMessage($"WARNING: writing to \"{path}\" is disallowed in Release builds!\n{Environment.StackTrace}", Color.Orange); - } - return true; + return DevException; #else return false; #endif @@ -46,6 +43,16 @@ namespace Barotrauma.IO doc.Save(path); } + public static void SaveSafe(this System.Xml.Linq.XElement element, string path) + { + if (!Validation.CanWrite(path)) + { + DebugConsole.ThrowError($"Cannot save XML element to \"{path}\": failed validation"); + return; + } + element.Save(path); + } + public static void SaveSafe(this System.Xml.Linq.XDocument doc, XmlWriter writer) { doc.WriteTo(writer); @@ -297,7 +304,7 @@ namespace Barotrauma.IO break; } return new FileStream(path, System.IO.File.Open(path, mode, - !Validation.CanWrite(path, false) ? + !Validation.CanWrite(path) ? System.IO.FileAccess.Read : access)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 9f584e76e..e7435178d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -35,7 +35,7 @@ namespace Barotrauma public static string MultiplayerSaveFolder = Path.Combine(SaveFolder, "Multiplayer"); public static readonly string SubmarineDownloadFolder = Path.Combine("Submarines", "Downloaded"); - public static readonly string CampaignDownloadFolder = Path.Combine("Data", "Saves", "Multiplayer"); + public static readonly string CampaignDownloadFolder = Path.Combine("Data", "Saves", "Multiplayer_Downloaded"); public delegate void ProgressDelegate(string sMessage); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs index a771801a6..05c7f975e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs @@ -18,20 +18,28 @@ namespace Barotrauma public object UserData; } - private static List taskActions = new List(); + private static readonly List taskActions = new List(); - public static void ListTasks(string[] args) + public static void ListTasks() { lock (taskActions) { DebugConsole.NewMessage($"Task count: {taskActions.Count}"); - for (int i=0;i t.Name == name); + } + } + private static void AddInternal(string name, Task task, Action onCompletion, object userdata) { lock (taskActions) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 15e2f5cb7..ebf4f2cdf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -212,15 +212,16 @@ namespace Barotrauma return inputType; } - + /// /// Returns either a green [x] or a red [o] /// /// + /// /// - public static string GetDebugSymbol(bool isFinished) + public static string GetDebugSymbol(bool isFinished, bool isRunning = false) { - return $"[‖color:{(isFinished ? "0,255,0‖x" : "255,0,0‖o")}‖color:end‖]"; + return isRunning ? "[‖color:243,162,50‖x‖color:end‖]" : $"[‖color:{(isFinished ? "0,255,0‖x" : "255,0,0‖o")}‖color:end‖]"; } /// diff --git a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub index 006e1b50e..956266192 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub and b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub index 7574235b3..11221a164 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub and b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub index 64fef2dbc..915132f19 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub and b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub b/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub index 5a18217fb..1725c25c4 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub and b/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub index a25c9d957..c0cfb9246 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub and b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub index 70ccb1471..01f5e42b1 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub and b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub index 89261d1f5..5a69da02f 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca.sub b/Barotrauma/BarotraumaShared/Submarines/Orca.sub index 7d1cd2376..9fce2b208 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Orca.sub and b/Barotrauma/BarotraumaShared/Submarines/Orca.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/R-29.sub b/Barotrauma/BarotraumaShared/Submarines/R-29.sub new file mode 100644 index 000000000..9a941a4ea Binary files /dev/null and b/Barotrauma/BarotraumaShared/Submarines/R-29.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora.sub b/Barotrauma/BarotraumaShared/Submarines/Remora.sub index 783856e86..6d94c2860 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub index e3b9bc39b..f27a05fe8 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub index 88338c5d4..124bd73b2 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub and b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub index 8bde79c7e..9d9a1af32 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub index 52eb30f67..bb99d09eb 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Venture.sub b/Barotrauma/BarotraumaShared/Submarines/Venture.sub index 0f0a7cb44..982a2248e 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Venture.sub and b/Barotrauma/BarotraumaShared/Submarines/Venture.sub differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 3a20c110f..b61e2d757 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,249 @@ +--------------------------------------------------------------------------------------------------------- +v0.10.5.1 +--------------------------------------------------------------------------------------------------------- + +- Fixed clients not receiving a message of their character dying if they die far away from the position they were last spectating, causing them to get stuck to a state where they appear alive but can't interact with anything. + +--------------------------------------------------------------------------------------------------------- +v0.10.5.0 +--------------------------------------------------------------------------------------------------------- + +Changes and additions: +- Added gardening: four different growable plants that produce specialed fruits which can be crafted into different items, or just thrown about and splattered on the hulls of the sub. +- Added cleaning: decals are now persistent and water gradually dirties up the submarine's walls. The dirt and the decals can be cleaned with a special tool that can also be used to paint the walls. +- New submarine, the heavy transportation ship R-29. Original design by "Rav2n". +- Overhauled mudraptor: new sprites, more varied attacks and heavier armor. +- Option to change the channel on the headset, making it possible to do things like secret communication on another channel or devices that react to commands sent on a specific channel. +- Added a verification prompt when trying to handcuff yourself. +- The inventory of handcuffed players isn't hidden completely, just locked (with a tooltip that explains why it's locked). Hiding the inventory was confusing to many new players, and it was easy to mistake it for a bug. +- Added directional markers that show the locations of the interactable NPCs in outposts. +- User's helm skill affects engine output. +- The game is paused when the campaign map is open in the single player campaign. +- The campaign map is automatically closed if the submarine gets too far from the end of the level while the map is open in multiplayer. +- Clients are allowed to view the campaign map even if they don't have the permissions to select the destination or the mission. +- The docking confirmation prompt is only shown when attempting to redock to the outpost you just left from. +- Wrecks are now properly initialized when spawned with "spawnsub" console command. +- Readjusted the attacks for the Husk and the Humanhusk. +- Added the option to select the minimum number of players required on the server for traitors to become active to the server settings menu. +- Log when clients modify the properties of an item (e.g. light colors, signal component settings). +- Allow selling items as long as their condition is over 90%. +- ID cards can be sold (as long as they're not in the player's own ID card slot). +- Disallowed selling items in equipment slots. +- Added cancel button to the "reconnecting to server" message box. +- DockingPort, Turret and LightComponent toggle connections ignore 0 signals. +- Traitor victims get a message saying they were killed by a traitor. +- Stack chat message popups when the chatbox is hidden instead of just showing one at a time. +- Switches now have lights that indicate whether they're on or off. +- Added "fuel_out" output to reactors. +- Added "set_light" connection to turrets. +- Made stun gun darts craftable from any type of wire. +- Outpost generator uses the first module type defined in the outpost config as the 1st module instead of forcing it to "airlock" (making it possible for modders to create outposts with another type of entrance module). +- Mission rounds end automatically if there are more living players inside the outpost than outside it, even if the sub is not at the end of the level. +- Made togglehud command usable in the sub editor for nicer screenshots. +- Characters don't gain helm skill when the sub isn't moving. Prevents grinding the helm skill when the sub is docked or maintaining position. +- Improved the way solid obstacles reduce explosion damage. Non-broken doors and walls nullify the damage almost completely. +- Fabricator continues fabricating when transitioning to a new level while it's running. +- Hid movement orders from the contextual command interface when there are orders of another type available. +- Made improvements to store interface tooltips. They now include the item name and should be displayed more consistently. +- Disabled automatic crew list sorting. The order now remains the same throughout the round. +- Added a second previous order icon to the crew list. +- Added character godmode, renamed mainsub godmode command to 'godmode_mainsub'. +- Increased sub editor auto save slots from one to 8 by default. +- Made logbooks sellable. +- Equipping a weapon activates it's reload time (capped to a maximum of 1 second), preventing bypassing the reload times by swapping between multiple weapons. +- Hide campaign NPC icon when the NPC is dead or incapacitated. +- Bots now prioritize stunning melee weapons based on their damage if they run out of battery. +- Made antidotes cheaper and easier to fabricate. +- Location reputations are shown when overing the cursor over a location in the campaign map screen. + +Modding improvements and fixes: +- The order of all installed mods (not just the enabled ones) is now saved into the config file. +- Workshop mods can now be downloaded and installed automatically before joining a modded server. +- Disabled the Executable content type due to issues with cross-platform support. +- Fixed Unsubscribe button sometimes failing to delete mod content. +- Fixed a crash when attempting to modify the currently selected core package when other core packages aren't available. +- Fixed a modding-related crash at startup. +- Increased the "SeverLimbsProbability" cap (for items) from 1 to 10. The value is multiplied together with the joint setting "SeveranceProbabilityModifier", and limiting the value to 1 makes it impossible to boost the probability of some weapons/attacks. +- Fixed negative rotation speeds not working on decorative sprites. +- Fixed ItemPrefabs loading after AfflictionPrefabs. +- Previous enabled mods are restored when leaving a server. +- Added support for "periodic effects" (effects that execute every x seconds) to Afflictions. See the new "nausea" affliction for a usage example. + +AI improvements: +- Using the fire extinguisher when there is a fire is no longer considered stealing by the NPCs. +- NPCs now spawn an oxygen tank if they can't find any. They don't spawn diving gear though. +- NPC guards now spawn more handcuffs if they don't have any and are trying to arrest a target. +- NPCs now spawn a welding fuel tank, if they can't find any. They don't spawn welding tools. +- Some adjustments to the logic on when a diving gear is required and when it's not. +- Bots now avoid idling in narrow hulls. Instead of taking the whole hull volume into account, they now give weight only for the width of the room. +- Bots should now avoid idling in flooding hulls even when they have proper equipment. +- Medics don't anymore try to rescue targets that are fixing leaks. +- Medics now try to replace empty oxygen tanks for their targets before dragging them into safety. They should also take the suit off when it's not needed. +- Security officers and those who are ordered to fight intruders now actively seek new weapons if they have a poor weapon or run out of ammo. +- Bots don't try to move while grabbed by a friendly character. +- Bots now detect and ignore leaks that other bots are targeting. +- Bots don't anymore try to decontain diving suits when the path to the cabinets is flooding. +- All crew members now try to keep the reactor running, unless the player orders someone to shut if down or turns it off manually. +- Bots shouldn't anymore try to operate items that other bots are targeting (previously they used to walk next to the device and only then continue). +- Removed the following autonomous objectives from medic and the security officer: fix leaks, repair systems, and pump water. +- Bots should now give a high priority on items that they are currently repairing, no matter if the decision to repair that item was made by them or the player controlling the character. +- Bots that operate/fix should now wait a few seconds before heading on their business (previously only in idle state). They are not allowed to wait in some circumstances, like when they are underwater or when there's a threat around. + +Bugfixes: +- Fixed items that are added outside hulls not getting saved in the sub editor. +- Fixed "missing entity" errors in multiplayer if the respawn shuttle despawns after its walls have started leaking. +- Fixed submarine upgrades, hired bots and purchased items disappearing if the round immediately after the purchases ends due to returning to the server lobby or the crew dying. +- Fixed characters spawning with a 100% husk affliction on the next round if they've become huskified during a multiplayer campaign round. +- Fixed dedicaters servers not setting a play style. +- Fixed servers with no play style showing up as "Serious". +- Fixed attachable items teleporting back to the player's inventory in multiplayer if they first equip it and then attach it immediately. +- Fixed duplicate saves in the multiplayer campaign's "load save" tab when the server is running on the same machine as the client. +- Fixed broken junction boxes carrying signals from other junction boxes. +- Fixed bots disappearing from the campaign save when loading a save that was done in a level between 2 uninhabited locations. +- Fixed mp campaign sometimes loading an outpost level with no outpost and forcing the player to the map view. Happened when a campaign save that had been done in a level between 2 natural formations was loaded, which would mess up the save and cause the bug when quitting and reloading the save. +- Fixed submarine voting not working in the server lobby. +- Fixed outpost events repeating more often than they should. +- Fixed dedicated servers always being public. +- Fixed right mouse button not behaving correctly in the campaign UI when using left-handed mode. +- Made endpoint checks in the banlist more robust. +- Certain audio errors no longer crash the game. +- More Fabricator syncing fixes. +- Fixed molochs bleeding when they take any damage on the brain/main body. +- Fixed attacks never being able to do more than 100 hp damage per affliction, which resulted for example nuclear explosions and railgun shots being too weak -> re-adjusted railgun and explosion damages. +- Fixed bots failing to get an item if their inventory is full. +- Fixed bots sometimes running towards a wall after extinguishing fires. +- Fixed Husks trying to target floors/ceilings. +- Fixed the security not reacting when being attacked by a friendly character with the husk stinger. +- Fixed the Husk infection not progressing properly in multiplayer (#3673.) +- Fixed taking more husk damage reducing the Husk infection when its strength was higher than 50 (#3673). +- Fixed creatures in the group "human" not being considered friendly, which prevented creating friendly non-human characters. +- Fixed bots not always prioritizing the selected (contextual) targets first when ordered. +- Fixed incorrect priority calculations for repair objectives that causes bots to make weird decisions on what to repair. +- Fixed latched monsters ignoring decoys. Also changed the latching logic a bit, which has some minor implications on the gameplay. +- Fixed a crash when "money" cheat command was used used in the main menu. +- Fixed bots suffocating in diving suits, because they were unable to take the suit off when they should. (#3333) +- Fixed bots sometimes being unable to hit with melee weapons, especially in the "arrest" mode or when the target is down. +- Fixed bots sometimes getting stuck when they are fixing leaks while they are a bit too far from the target. +- Fixed bots insisting on equipping the diving suit before accessing Kastrull's drone. +- Fixed bots rapidly turning left and right when they are positioned between a wall and another bot in the idle state. +- Fixed one of the Thalamus spawn organs always dying because the hull was not flooding in Wreck1. +- Fixed bots failing to take account all the items in the inventory. Addresses cases where bots are confused because there are some empty/broken items and some full/functional items in their inventory. +- Fixed bots getting stuck in the find safety state when they can't find any safe hull (e.g. all the hulls are flooding and they don't have a suit). +- Fixed bots avoiding lethal pressure in some cases even when they have a diving suit. +- Fixed bots being unable to get the diving gear when some of the hulls in the path are flooding. +- Fixed bots failing to switch oxygen tanks when the tanks are empty. They don't yet know how to use the oxygen generator for refilling the tanks (coming later). +- Fixed many AI objectives not resetting properly, causing many separate issues. +- Fixed bots trying to use stabilozine to treat internal damage even though it only treats poisonings. +- Incapacitated bots now forget what they were doing. Fixes bots shooting at the player when revived after being shot by the player. +- Fixes and improvements on NPC/bot reactions when being attacked by the player. +- Fixed the devotion not working right for the subobjectives of looping objectives. Should fix cases where the bots keep switching repair/fix targets, resulting in looping walking behavior. +- Fixed coils not triggering in some alien ruin rooms. +- Fixed misaligned oxygen tanks in the oxygen generator. +- Fixed misaligned railgun shells being in the single loaders. +- Fixed explosives not triggering inside depth charges. +- Fixed damage modifiers overriding each other instead of stacking, causing some protective items to make the characters less protected. +- Fixed skill checks in the "sound in the vent", "mediator" and "big brother" events always failing. +- Fixed conversation prompt staying open after finishing either of the "Mike the Idiot" events. +- Fixed attacks never being able to do more than 100 units of damage per affliction, making some weapons less effective than they should be. +- Fixed inability to override item assemblies with mods. +- Fixed items contained inside another item in an item assembly not getting saved in the sub editor. +- Fixed links between entities not getting saved in item assemblies, causing doors to create a duplicate gap when the assembly is placed. +- Fixed stabbing with a syringe bypassing the medical skill check. +- Fixed piercing coilgun bolts going through level walls. +- Fixed particles sometimes going through walls (example case: Typhon's ballast pumps). +- Fixed SMG magazines always spawning a bullet at the start of a round even if there's already one inside the mag. +- Fixed respawn shuttle sometimes spawning with upwards velocity. +- Fixed mining outposts sometimes failing to generate, causing the game to use one of the old pre-built outposts as a fallback. +- Fixed "acid party" traitor mission causing a crash if there's only one player on the server. +- Fixed ruins sometimes spawning close to the beginning/end of the level, blocking the way to the outpost. +- Fixed crew members that have been removed during the round (e.g. despawning, turning into a husk) reappearing on the next round. +- Fixed round summary displaying "new hire" instead of the status of recently hired characters who've died during the round. +- Fixed monsters often spawning behind the submarine even if there are valid spawnpoint ahead of it. +- Fixed duct blocks in vanilla subs being repairable with a welding tool instead of a wrench. +- Fixed giveperm, revokeperm, giverank, givecommandperm and revokecommandperm not working when executed on a player with spaces in their name by a client. +- Fixed occasional crashes at the end of the round due to a race condition where SteamManager tries to check for damaged walls while the sub is being unloaded. +- Fixed mudraptor eggs never hatching. +- Fixed the "giveaffliction" command not accepting the affliction identifier as a parameter in multiplayer. +- Fixed the contextual repair order being shown even if the item was not in need of repair. +- Fixed a networking bug that caused "index out of bounds" and "missing entity errors" (although there are probably other bugs remaining that can cause these error messages). Happened in multiplayer campaign when a purchased or player-owned item happened to take the ID of an existing structure during the start of the round. +- Fixed inability to give Operate Weapons order when there's only 1 turret. +- Fixed store total not updating properly when location reputation changes. +- Fixed bots not being able to reach leaks that are behind intact walls. +- Fixed pause menu staying open when clicking "Save and quit" in an outpost level. +- Fixed all saves getting hidden from the "load game" menu when deleting a save. +- Fixed projectiles staying active if picked up mid-flight, causing them to hit the character when dropped from the inventory. +- Fixed a crash when taking control of a previously player-controlled character that has turned into husk. +- Fixed attacks working unreliably when controlling a monster in multiplayer. +- Fixed grenade launcher's muzzle flash causing items' OnFire effects to trigger. +- Fixed clicking on the job name not selecting the job as a preference. +- Fixed bots running back and forth after reaching the target waypoint if there's nothing telling them otherwise. Now the bots should continue running towards the target as intended. +- Fixed "sound in the vent" and "giving directions" events not giving any rewards. +- Fixed prices not refreshing in the store screen if the crew's reputation changes while inside the outpost. +- Fixed characters not getting stunned when failing to repair an oxygen generator. +- Fixed missing fire extinguisher in securitymodule_03. +- Fixed secure steel cabinets not requiring a key card to open in securitymodule_01 and 02. +- Fixed campaign setup menu sometimes randomly selecting an invalid sub when opening the menu. +- Fixed monster attacks not working inside ruins. +- Fixed doors not working in AlienDoorAssembly2. +- Fixed ability to link hulls to each other multiple times. +- Fixed bots triggering the campaign map by undocking from outposts. +- Don't allow the current location or the destination to change it's type at the end of a round (i.e. an uninhabited location can't turn to an outpost as you're entering it). +- Bots don't try to power up the reactor unless ordered to in outposts. +- Fixed "invalid docking port network event" error if the ID of the port has changed after the event has been created (e.g. if one of the clients happens to have an item with the same ID in their inventory). +- Fixed changes to spawn point's text properties not saving in the sub editor without pressing enter. +- Fixed ability to "bypass" sonar disruptions by switching to passive sonar before the active ping reaches the disruption. +- Fixed incorrect diving suit deconstruction recipe. +- Fixed faraday artifact not playing the explosion effect in multiplayer. +- Fixed ability to bypass vote kicks by changing your name before you get kicked. +- Fixed killing a character with morbusine not unlocking the "Poisoner" achievement. +- Fixed killing a moloch that's the target of a mission not unlocking the "Killed a Moloch" achievement. +- Fixed Berilia's and Typhon's docking ports not being connected to the power grid. +- Fixed contained item positions not being updated if they're inside a non-equipped item in a character's inventory (e.g. a sonar beacon inside a toolbox). +- Fixed sonar beacon staying active after running out of battery. +- Fixed stationary batteries being able to provide power when broken. +- Fixed pumps being impossible to adjust manually after they've received a signal through the "set_targetlevel" connection in multiplayer. +- Fixed sub names and save times disappearing from the "load campaign" menu in the server lobby after deleting a save. +- Fixed autopilot being turned on automatically at the start of a round on nav terminals that control a shuttle/drone remotely. +- Fixed names not being color coded according to job in the server lobby when a round is running. +- Fixed non-mission artifacts sometimes spawning inside level walls. +- Signal components don't output anything if the result of an arithmetic operation is undefined (square root of a negative value, division by zero, inverse trig function outside the defined range). +- Fixed handcuffs becoming unequipped client-side on successive campaign rounds. +- Fixed copypasting a single docking port/hatch or a wire in the sub editor placing it very far from the submarine. +- Fixed characters not dying, but still being unable to move, if they get crushed by pressure when they have the vigor affliction. +- Fixed crashing if a turret receives a NaN signal in the position_in connection. +- Fixed crashing when trying to reselect the current preview image as the preview image for a workshop item. +- Fixed shadow-casting convex hulls being calculated incorrectly on hatches with windows (only affects mods that add hatches with windows). +- Fixed custom items disappearing when saving a sub if the sub is both the items and the sub are configured in the same content package. +- Fixed nullref exception if a client joins or disconnects when tab menu's crew tab hasn't been initialized. +- Fixed Dugong's oxygen generator being rewireable with a wrench instead of a screwdriver. +- Fixed particles not being emitted when cutting ores. +- Fixed unwired lamp in EngineeringModule_01. +- Fixed tunnel background being interactable in ResearchModule_02. +- Fixed inability to interact with hidden linked containers in multiplayer. +- Fixed undocked linked subs being positioned incorrectly when entering a new level in the campaign. +- Fixed players not getting notified about shuttles being left behind when docking with an outpost in the campaign. +- Fixed salvage missions not completing if the item is inside a subinventory (for example a toolbox). +- Fixed rejoining clients not gaining control of their previous character if they've changed their name. +- Fixed healthbar being positioned incorrectly on large resolutions. +- Fixed fabricator's hover text saying it's repairable with a screwdriver. +- Fixed last hit from a stun baton doing nothing. +- Fixed diving suit lockers that have been recolored in the sub editor reverting to default color when a suit is placed inside them. +- More reliable syncing of doors' stuck state. Should fix doors appearing usable client-side even though they're actually welded server-side or vice versa. +- More reliable memory component and text display syncing. +- Fixed events that require a specific item not taking items in subinventories into account. +- Fixed events where the player has to pay for something being possible to complete even if the player doesn't have enough money. +- Made "goodsamaritan" and "huskcultist" events remove the items from the player immediately after opting to give/use the item. Otherwise it's possible to keep the item by dropping it from the inventory before dismissing the conversation prompt. +- Fixed events that increase the medical skill giving the character a duplicate medical skill entry. +- Fixed light sprites not rotating when rotating an item. +- Fixed contained items not rotating when the container is rotated. +- Fixed legacy railgun controllers being fired with the Use key instead of the Shoot key. +- Fixed XP popups not showing up in multiplayer. +- Shuttle batteries can't be damaged by explosions because they can't be repaired. +- Fixed text overlapping with the icon in the "purchased supplies have been delivered" popup. +- Fixed mission notification appearing in front of the round summary. +- Fixed crawler walking/running animations when the creature is facing left. +- Fixed irrelevant damagemodifiers affecting the final damage when multiple damage modifiers exist. + --------------------------------------------------------------------------------------------------------- v0.10.4.0 --------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaShared/serversettings.xml b/Barotrauma/BarotraumaShared/serversettings.xml index 4830b170b..eb97bbb80 100644 --- a/Barotrauma/BarotraumaShared/serversettings.xml +++ b/Barotrauma/BarotraumaShared/serversettings.xml @@ -23,7 +23,6 @@ startwhenclientsready="False" startwhenclientsreadyratio="0.8" allowspectating="True" - endroundatlevelend="True" saveserverlogs="True" allowragdollbutton="True" allowfiletransfers="True" diff --git a/Libraries/Facepunch.Steamworks/SteamServer.cs b/Libraries/Facepunch.Steamworks/SteamServer.cs index 8bf333c86..1b1559d45 100644 --- a/Libraries/Facepunch.Steamworks/SteamServer.cs +++ b/Libraries/Facepunch.Steamworks/SteamServer.cs @@ -77,14 +77,13 @@ namespace Steamworks System.Environment.SetEnvironmentVariable( "SteamAppId", appid.ToString() ); System.Environment.SetEnvironmentVariable( "SteamGameId", appid.ToString() ); - var secure = (int)(init.Secure ? 3 : 2); // // Get other interfaces // - if ( !SteamInternal.GameServer_Init( ipaddress, init.SteamPort, init.GamePort, init.QueryPort, secure, init.VersionString ) ) + if ( !SteamInternal.GameServer_Init( ipaddress, init.SteamPort, init.GamePort, init.QueryPort, (int)init.Mode, init.VersionString ) ) { - throw new System.Exception( $"InitGameServer returned false ({ipaddress},{init.SteamPort},{init.GamePort},{init.QueryPort},{secure},\"{init.VersionString}\")" ); + throw new System.Exception( $"InitGameServer returned false ({ipaddress},{init.SteamPort},{init.GamePort},{init.QueryPort},{init.Mode},\"{init.VersionString}\")" ); } // diff --git a/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs b/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs index 02e360417..7664a9bb1 100644 --- a/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs +++ b/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs @@ -7,6 +7,14 @@ using System.Text; namespace Steamworks { + public enum InitServerMode + { + Invalid = 0, + NoAuthentication = 1, + Authentication = 2, + AuthenticationSecure = 3 + }; + /// /// Used to set up the server. /// The variables in here are all required to be set, and can't be changed once the server is created. @@ -17,7 +25,7 @@ namespace Steamworks public ushort SteamPort; public ushort GamePort; public ushort QueryPort; - public bool Secure; + public InitServerMode Mode; /// /// The version string is usually in the form x.x.x.x, and is used by the master server to detect when the server is out of date. @@ -49,7 +57,7 @@ namespace Steamworks GameDescription = gameDesc; GamePort = 27015; QueryPort = 27016; - Secure = true; + Mode = InitServerMode.Authentication; VersionString = "1.0.0.0"; IpAddress = null; SteamPort = 0; diff --git a/Libraries/Facepunch.Steamworks/Utility/Platform.cs b/Libraries/Facepunch.Steamworks/Utility/Platform.cs index 51a402e89..fe97970d7 100644 --- a/Libraries/Facepunch.Steamworks/Utility/Platform.cs +++ b/Libraries/Facepunch.Steamworks/Utility/Platform.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Runtime.InteropServices; -using System.Text; +using System.Runtime.InteropServices; namespace Steamworks { diff --git a/Libraries/OpenAL-Soft/oal_soft.diff b/Libraries/OpenAL-Soft/oal_soft.diff index 6ba24fc1a..b43055ca1 100644 --- a/Libraries/OpenAL-Soft/oal_soft.diff +++ b/Libraries/OpenAL-Soft/oal_soft.diff @@ -1,9 +1,9 @@ -diff --git a/Alc/alc.cpp b/Alc/alc.cpp -index fde655be..779727d1 100644 ---- a/Alc/alc.cpp -+++ b/Alc/alc.cpp -@@ -161,9 +161,11 @@ BackendInfo BackendList[] = { - { "qsa", QSABackendFactory::getFactory }, +diff --git a/alc/alc.cpp b/alc/alc.cpp +index 30d363af..f759a484 100644 +--- a/alc/alc.cpp ++++ b/alc/alc.cpp +@@ -197,9 +197,11 @@ BackendInfo BackendList[] = { + { "oss", OSSBackendFactory::getFactory }, #endif #ifdef HAVE_DSOUND +#error no dsound >:( @@ -14,16 +14,16 @@ index fde655be..779727d1 100644 { "winmm", WinMMBackendFactory::getFactory }, #endif #ifdef HAVE_PORTAUDIO -@@ -956,7 +958,7 @@ static void alc_initconfig(void) +@@ -1015,7 +1017,7 @@ void alc_initconfig(void) } TRACE("Supported backends: %s\n", names.c_str()); } - ReadALConfig(); + //ReadALConfig(); - str = getenv("__ALSOFT_SUSPEND_CONTEXT"); - if(str && *str) -@@ -2692,6 +2694,20 @@ START_API_FUNC + if(auto suspendmode = al::getenv("__ALSOFT_SUSPEND_CONTEXT")) + { +@@ -2580,6 +2582,20 @@ START_API_FUNC } END_API_FUNC @@ -42,9 +42,9 @@ index fde655be..779727d1 100644 +} +END_API_FUNC - /* alcSuspendContext - * -@@ -2705,7 +2721,9 @@ START_API_FUNC + ALC_API void ALC_APIENTRY alcSuspendContext(ALCcontext *context) + START_API_FUNC +@@ -2589,7 +2605,9 @@ START_API_FUNC ContextRef ctx{VerifyContext(context)}; if(!ctx) @@ -52,9 +52,9 @@ index fde655be..779727d1 100644 alcSetError(nullptr, ALC_INVALID_CONTEXT); + } else - ALCcontext_DeferUpdates(ctx.get()); + ctx->deferUpdates(); } -@@ -2723,7 +2741,9 @@ START_API_FUNC +@@ -2603,7 +2621,9 @@ START_API_FUNC ContextRef ctx{VerifyContext(context)}; if(!ctx) @@ -62,19 +62,9 @@ index fde655be..779727d1 100644 alcSetError(nullptr, ALC_INVALID_CONTEXT); + } else - ALCcontext_ProcessUpdates(ctx.get()); + ctx->processUpdates(); } -@@ -2824,7 +2844,9 @@ START_API_FUNC - case ALC_HRTF_SPECIFIER_SOFT: - dev = VerifyDevice(Device); - if(!dev) -+ { - alcSetError(nullptr, ALC_INVALID_DEVICE); -+ } - else - { - std::lock_guard _{dev->StateLock}; -@@ -2858,6 +2880,7 @@ static ALCsizei GetIntegerv(ALCdevice *device, ALCenum param, const al::span if(values.empty()) { @@ -82,9 +72,9 @@ index fde655be..779727d1 100644 alcSetError(device, ALC_INVALID_VALUE); return 0; } -@@ -3903,12 +3926,14 @@ START_API_FUNC +@@ -3717,12 +3738,14 @@ START_API_FUNC - if(!CaptureBackend.name) + if(!CaptureFactory) { + alcCallErrorReasonCallback("alcCaptureOpenDevice failed: CaptureBackend name is null"); alcSetError(nullptr, ALC_INVALID_VALUE); @@ -97,15 +87,15 @@ index fde655be..779727d1 100644 alcSetError(nullptr, ALC_INVALID_VALUE); return nullptr; } -@@ -3921,6 +3946,7 @@ START_API_FUNC - device->Frequency = frequency; - if(DecomposeDevFormat(format, &device->FmtChans, &device->FmtType) == AL_FALSE) +@@ -3739,6 +3762,7 @@ START_API_FUNC + auto decompfmt = DecomposeDevFormat(format); + if(!decompfmt) { + alcCallErrorReasonCallback("alcCaptureOpenDevice failed: DecomposeDevFormat failed"); alcSetError(nullptr, ALC_INVALID_ENUM); return nullptr; } -@@ -3945,6 +3971,7 @@ START_API_FUNC +@@ -3763,6 +3787,7 @@ START_API_FUNC } catch(al::backend_exception &e) { WARN("Failed to open capture device: %s\n", e.what()); @@ -113,9 +103,9 @@ index fde655be..779727d1 100644 alcSetError(nullptr, e.errorCode()); return nullptr; } -@@ -3968,11 +3995,13 @@ START_API_FUNC - auto iter = std::lower_bound(DeviceList.cbegin(), DeviceList.cend(), device); - if(iter == DeviceList.cend() || *iter != device) +@@ -3786,11 +3811,13 @@ START_API_FUNC + auto iter = std::lower_bound(DeviceList.begin(), DeviceList.end(), device); + if(iter == DeviceList.end() || *iter != device) { + alcCallErrorReasonCallback("alcCaptureCloseDevice failed: iterator couldn't find correct device"); alcSetError(nullptr, ALC_INVALID_DEVICE); @@ -127,7 +117,7 @@ index fde655be..779727d1 100644 alcSetError(*iter, ALC_INVALID_DEVICE); return ALC_FALSE; } -@@ -3998,19 +4027,24 @@ START_API_FUNC +@@ -3814,13 +3841,17 @@ START_API_FUNC DeviceRef dev{VerifyDevice(device)}; if(!dev || dev->Type != Capture) { @@ -144,15 +134,16 @@ index fde655be..779727d1 100644 + } else if(!dev->Flags.get()) { - if(dev->Backend->start()) + try { +@@ -3829,6 +3860,7 @@ START_API_FUNC dev->Flags.set(); - else - { -+ alcCallErrorReasonCallback("alcCaptureStart failed: backend start failed"); - aluHandleDisconnect(dev.get(), "Device start failure"); + } + catch(al::backend_exception& e) { ++ alcCallErrorReasonCallback(std::string("alcCaptureStart failed: backend start failed (")+e.what()+")"); + aluHandleDisconnect(dev.get(), "%s", e.what()); alcSetError(dev.get(), ALC_INVALID_DEVICE); } -@@ -4023,7 +4057,10 @@ START_API_FUNC +@@ -3841,7 +3873,10 @@ START_API_FUNC { DeviceRef dev{VerifyDevice(device)}; if(!dev || dev->Type != Capture) @@ -163,7 +154,7 @@ index fde655be..779727d1 100644 else { std::lock_guard _{dev->StateLock}; -@@ -4040,6 +4077,7 @@ START_API_FUNC +@@ -3858,6 +3893,7 @@ START_API_FUNC DeviceRef dev{VerifyDevice(device)}; if(!dev || dev->Type != Capture) { @@ -171,21 +162,26 @@ index fde655be..779727d1 100644 alcSetError(dev.get(), ALC_INVALID_DEVICE); return; } -diff --git a/Alc/backends/wasapi.cpp b/Alc/backends/wasapi.cpp -index 84e85fe6..33d6a74f 100644 ---- a/Alc/backends/wasapi.cpp -+++ b/Alc/backends/wasapi.cpp -@@ -46,6 +46,7 @@ +diff --git a/alc/backends/wasapi.cpp b/alc/backends/wasapi.cpp +index 8e43aa7c..0f313dea 100644 +--- a/alc/backends/wasapi.cpp ++++ b/alc/backends/wasapi.cpp +@@ -54,6 +54,12 @@ + #include #include #include - #include ++#include +#include - #include - #include - #include -@@ -57,6 +58,13 @@ - #include "compat.h" - #include "converter.h" ++#include ++#include ++#include ++#include + + #include "alcmain.h" + #include "alexcpt.h" +@@ -65,6 +71,13 @@ + #include "strutils.h" + #include "threads.h" +extern void alcCallErrorReasonCallback(std::string reason); + @@ -197,15 +193,128 @@ index 84e85fe6..33d6a74f 100644 /* Some headers seem to define these as macros for __uuidof, which is annoying * since some headers don't declare them at all. Hopefully the ifdef is enough -@@ -695,6 +703,7 @@ ALCenum WasapiPlayback::open(const ALCchar *name) +@@ -792,6 +805,7 @@ void WasapiPlayback::open(const ALCchar *name) mDevId.clear(); + alcCallErrorReasonCallback("WASAPI playback device init failed: HRESULT "+toStringHex(hr)); - ERR("Device init failed: 0x%08lx\n", hr); - return ALC_INVALID_VALUE; + throw al::backend_exception{ALC_INVALID_VALUE, "Device init failed: 0x%08lx", hr}; } -@@ -1222,7 +1231,9 @@ ALCenum WasapiCapture::open(const ALCchar *name) + } +@@ -897,7 +911,7 @@ HRESULT WasapiPlayback::resetProxy() + mDevice->FmtChans = DevFmtX51; + else if(chancount >= 6 && (chanmask&X51RearMask) == X5DOT1REAR) + mDevice->FmtChans = DevFmtX51Rear; +- else if(chancount >= 4 && (chanmask&QuadMask) == QUAD) ++ else if(chancount >= 4/* && (chanmask&QuadMask) == QUAD*/) + mDevice->FmtChans = DevFmtQuad; + else if(chancount >= 2 && (chanmask&StereoMask) == STEREO) + mDevice->FmtChans = DevFmtStereo; +@@ -1005,9 +1019,11 @@ HRESULT WasapiPlayback::resetProxy() + CoTaskMemFree(wfx); + wfx = nullptr; + +- mDevice->Frequency = OutputType.Format.nSamplesPerSec; +- const uint32_t chancount{OutputType.Format.nChannels}; +- const DWORD chanmask{OutputType.dwChannelMask}; ++ const WAVEFORMATEXTENSIBLE& constOutputType = OutputType; ++ ++ mDevice->Frequency = constOutputType.Format.nSamplesPerSec; ++ const uint32_t chancount{constOutputType.Format.nChannels}; ++ const DWORD chanmask{constOutputType.dwChannelMask}; + if(chancount >= 8 && (chanmask&X71Mask) == X7DOT1) + mDevice->FmtChans = DevFmtX71; + else if(chancount >= 7 && (chanmask&X61Mask) == X6DOT1) +@@ -1016,7 +1032,7 @@ HRESULT WasapiPlayback::resetProxy() + mDevice->FmtChans = DevFmtX51; + else if(chancount >= 6 && (chanmask&X51RearMask) == X5DOT1REAR) + mDevice->FmtChans = DevFmtX51Rear; +- else if(chancount >= 4 && (chanmask&QuadMask) == QUAD) ++ else if(chancount >= 4/* && (chanmask&QuadMask) == QUAD*/) + mDevice->FmtChans = DevFmtQuad; + else if(chancount >= 2 && (chanmask&StereoMask) == STEREO) + mDevice->FmtChans = DevFmtStereo; +@@ -1024,54 +1040,59 @@ HRESULT WasapiPlayback::resetProxy() + mDevice->FmtChans = DevFmtMono; + else + { +- ERR("Unhandled extensible channels: %d -- 0x%08lx\n", OutputType.Format.nChannels, +- OutputType.dwChannelMask); +- mDevice->FmtChans = DevFmtStereo; +- OutputType.Format.nChannels = 2; +- OutputType.dwChannelMask = STEREO; ++ ERR("Unhandled extensible channels: %d -- 0x%08lx\n", constOutputType.Format.nChannels, ++ constOutputType.dwChannelMask); ++ /*mDevice->FmtChans = DevFmtStereo; ++ constOutputType.Format.nChannels = 2; ++ constOutputType.dwChannelMask = STEREO;*/ + } + +- if(IsEqualGUID(OutputType.SubFormat, KSDATAFORMAT_SUBTYPE_PCM)) ++ if(IsEqualGUID(constOutputType.SubFormat, KSDATAFORMAT_SUBTYPE_PCM)) + { +- if(OutputType.Format.wBitsPerSample == 8) ++ if(constOutputType.Format.wBitsPerSample == 8) + mDevice->FmtType = DevFmtUByte; +- else if(OutputType.Format.wBitsPerSample == 16) ++ else if(constOutputType.Format.wBitsPerSample == 16) + mDevice->FmtType = DevFmtShort; +- else if(OutputType.Format.wBitsPerSample == 32) ++ else if(constOutputType.Format.wBitsPerSample == 32) + mDevice->FmtType = DevFmtInt; + else + { ++ ERR("Unhandled bits per sample: %d\n", constOutputType.Format.wBitsPerSample); + mDevice->FmtType = DevFmtShort; +- OutputType.Format.wBitsPerSample = 16; ++ //OutputType.Format.wBitsPerSample = 16; + } + } +- else if(IsEqualGUID(OutputType.SubFormat, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) ++ else if(IsEqualGUID(constOutputType.SubFormat, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) + { + mDevice->FmtType = DevFmtFloat; +- OutputType.Format.wBitsPerSample = 32; ++ if (constOutputType.Format.wBitsPerSample != 32) { ++ ERR("Incorrect bits per sample for SUBTYPE_IEEE_FLOAT: %d\n", constOutputType.Format.wBitsPerSample); ++ } + } + else + { +- ERR("Unhandled format sub-type: %s\n", GuidPrinter{OutputType.SubFormat}.c_str()); ++ ERR("Unhandled format sub-type: %s\n", GuidPrinter{constOutputType.SubFormat}.c_str()); + mDevice->FmtType = DevFmtShort; +- if(OutputType.Format.wFormatTag != WAVE_FORMAT_EXTENSIBLE) +- OutputType.Format.wFormatTag = WAVE_FORMAT_PCM; +- OutputType.Format.wBitsPerSample = 16; +- OutputType.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; ++ /*if(constOutputType.Format.wFormatTag != WAVE_FORMAT_EXTENSIBLE) ++ constOutputType.Format.wFormatTag = WAVE_FORMAT_PCM; ++ constOutputType.Format.wBitsPerSample = 16; ++ constOutputType.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;*/ + } + OutputType.Samples.wValidBitsPerSample = OutputType.Format.wBitsPerSample; + } +- mFrameStep = OutputType.Format.nChannels; ++ const WAVEFORMATEXTENSIBLE& constOutputType2 = OutputType; ++ ++ mFrameStep = constOutputType2.Format.nChannels; + + EndpointFormFactor formfactor{UnknownFormFactor}; + get_device_formfactor(mMMDev, &formfactor); + mDevice->IsHeadphones = (mDevice->FmtChans == DevFmtStereo + && (formfactor == Headphones || formfactor == Headset)); + +- setChannelOrderFromWFXMask(OutputType.dwChannelMask); ++ setChannelOrderFromWFXMask(constOutputType2.dwChannelMask); + + hr = mClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, +- buf_time.count(), 0, &OutputType.Format, nullptr); ++ buf_time.count(), 0, &constOutputType2.Format, nullptr); + if(FAILED(hr)) + { + ERR("Failed to initialize audio client: 0x%08lx\n", hr); +@@ -1326,7 +1347,9 @@ void WasapiCapture::open(const ALCchar *name) mNotifyEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr); if(mNotifyEvent == nullptr) { @@ -216,23 +325,22 @@ index 84e85fe6..33d6a74f 100644 hr = E_FAIL; } -@@ -1268,6 +1279,7 @@ ALCenum WasapiCapture::open(const ALCchar *name) +@@ -1373,12 +1396,14 @@ void WasapiCapture::open(const ALCchar *name) mDevId.clear(); + alcCallErrorReasonCallback(std::string("WASAPI capture open failed: HRESULT ")+toStringHex(hr)+" (1)"); - ERR("Device init failed: 0x%08lx\n", hr); - return ALC_INVALID_VALUE; + throw al::backend_exception{ALC_INVALID_VALUE, "Device init failed: 0x%08lx", hr}; } -@@ -1275,6 +1287,7 @@ ALCenum WasapiCapture::open(const ALCchar *name) + hr = pushMessage(MsgType::ResetDevice).get(); if(FAILED(hr)) { + alcCallErrorReasonCallback(std::string("WASAPI capture open failed: HRESULT ")+toStringHex(hr)+" (2)"); if(hr == E_OUTOFMEMORY) - return ALC_OUT_OF_MEMORY; - return ALC_INVALID_VALUE; -@@ -1308,6 +1321,7 @@ HRESULT WasapiCapture::openProxy() + throw al::backend_exception{ALC_OUT_OF_MEMORY, "Out of memory"}; + throw al::backend_exception{ALC_INVALID_VALUE, "Device reset failed"}; +@@ -1410,6 +1435,7 @@ HRESULT WasapiCapture::openProxy() if(FAILED(hr)) { @@ -240,7 +348,7 @@ index 84e85fe6..33d6a74f 100644 if(mMMDev) mMMDev->Release(); mMMDev = nullptr; -@@ -1337,6 +1351,7 @@ HRESULT WasapiCapture::resetProxy() +@@ -1439,6 +1465,7 @@ HRESULT WasapiCapture::resetProxy() HRESULT hr{mMMDev->Activate(IID_IAudioClient, CLSCTX_INPROC_SERVER, nullptr, &ptr)}; if(FAILED(hr)) { @@ -248,7 +356,7 @@ index 84e85fe6..33d6a74f 100644 ERR("Failed to reactivate audio client: 0x%08lx\n", hr); return hr; } -@@ -1418,6 +1433,7 @@ HRESULT WasapiCapture::resetProxy() +@@ -1521,6 +1548,7 @@ HRESULT WasapiCapture::resetProxy() hr = mClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &OutputType.Format, &wfx); if(FAILED(hr)) { @@ -256,15 +364,15 @@ index 84e85fe6..33d6a74f 100644 ERR("Failed to check format support: 0x%08lx\n", hr); return hr; } -@@ -1525,6 +1541,7 @@ HRESULT WasapiCapture::resetProxy() - 0, &OutputType.Format, nullptr); +@@ -1619,6 +1647,7 @@ HRESULT WasapiCapture::resetProxy() + buf_time.count(), 0, &OutputType.Format, nullptr); if(FAILED(hr)) { + alcCallErrorReasonCallback(std::string("WASAPI capture proxy reset failed: failed to initialize audio client (HRESULT ")+toStringHex(hr)+")"); ERR("Failed to initialize audio client: 0x%08lx\n", hr); return hr; } -@@ -1536,6 +1553,7 @@ HRESULT WasapiCapture::resetProxy() +@@ -1630,6 +1659,7 @@ HRESULT WasapiCapture::resetProxy() hr = mClient->GetBufferSize(&buffer_len); if(FAILED(hr)) { @@ -272,15 +380,7 @@ index 84e85fe6..33d6a74f 100644 ERR("Failed to get buffer size: 0x%08lx\n", hr); return hr; } -@@ -1547,6 +1565,7 @@ HRESULT WasapiCapture::resetProxy() - mRing = CreateRingBuffer(buffer_len, mDevice->frameSizeFromFmt(), false); - if(!mRing) - { -+ alcCallErrorReasonCallback(std::string("WASAPI capture proxy reset failed: failed to allocate capture ring buffer")); - ERR("Failed to allocate capture ring buffer\n"); - return E_OUTOFMEMORY; - } -@@ -1554,6 +1573,7 @@ HRESULT WasapiCapture::resetProxy() +@@ -1641,6 +1671,7 @@ HRESULT WasapiCapture::resetProxy() hr = mClient->SetEventHandle(mNotifyEvent); if(FAILED(hr)) { @@ -288,18 +388,18 @@ index 84e85fe6..33d6a74f 100644 ERR("Failed to set event handle: 0x%08lx\n", hr); return hr; } -@@ -1565,6 +1585,10 @@ HRESULT WasapiCapture::resetProxy() - ALCboolean WasapiCapture::start() +@@ -1653,7 +1684,10 @@ void WasapiCapture::start() { - HRESULT hr{pushMessage(MsgType::StartDevice).get()}; -+ if (FAILED(hr)) + const HRESULT hr{pushMessage(MsgType::StartDevice).get()}; + if(FAILED(hr)) + { + alcCallErrorReasonCallback(std::string("WASAPI capture start failed: HRESULT ")+toStringHex(hr)); + throw al::backend_exception{ALC_INVALID_DEVICE, "Failed to start recording: 0x%lx", hr}; + } - return SUCCEEDED(hr) ? ALC_TRUE : ALC_FALSE; } -@@ -1575,6 +1599,7 @@ HRESULT WasapiCapture::startProxy() + HRESULT WasapiCapture::startProxy() +@@ -1663,6 +1697,7 @@ HRESULT WasapiCapture::startProxy() HRESULT hr{mClient->Start()}; if(FAILED(hr)) { @@ -307,7 +407,7 @@ index 84e85fe6..33d6a74f 100644 ERR("Failed to start audio client: 0x%08lx\n", hr); return hr; } -@@ -1588,9 +1613,17 @@ HRESULT WasapiCapture::startProxy() +@@ -1676,9 +1711,17 @@ HRESULT WasapiCapture::startProxy() mKillNow.store(false, std::memory_order_release); mThread = std::thread{std::mem_fn(&WasapiCapture::recordProc), this}; } @@ -325,7 +425,7 @@ index 84e85fe6..33d6a74f 100644 ERR("Failed to start thread\n"); hr = E_FAIL; } -@@ -1649,6 +1682,12 @@ bool WasapiBackendFactory::init() +@@ -1737,6 +1780,12 @@ bool WasapiBackendFactory::init() InitResult = future.get(); } catch(...) { @@ -338,45 +438,3 @@ index 84e85fe6..33d6a74f 100644 } return SUCCEEDED(InitResult) ? ALC_TRUE : ALC_FALSE; -diff --git a/Alc/helpers.cpp b/Alc/helpers.cpp -index ee0bb2dc..2b17acce 100644 ---- a/Alc/helpers.cpp -+++ b/Alc/helpers.cpp -@@ -125,7 +125,6 @@ DEFINE_PROPERTYKEY(PKEY_AudioEndpoint_GUID, 0x1da5d803, 0xd492, 0x4edd, 0x8c, 0x - #include "compat.h" - #include "threads.h" - -- - #if defined(HAVE_GCC_GET_CPUID) && (defined(__i386__) || defined(__x86_64__) || \ - defined(_M_IX86) || defined(_M_X64)) - using reg_type = unsigned int; -@@ -482,6 +481,7 @@ void *GetSymbol(void *handle, const char *name) - return ret; - } - -+extern void alcCallErrorReasonCallback(std::string reason); - - void al_print(FILE *logfile, const char *fmt, ...) - { -@@ -502,6 +502,8 @@ void al_print(FILE *logfile, const char *fmt, ...) - va_end(args2); - va_end(args); - -+ alcCallErrorReasonCallback(str); -+ - std::wstring wstr{utf8_to_wstr(str)}; - fprintf(logfile, "%ls", wstr.c_str()); - fflush(logfile); -diff --git a/include/AL/alc.h b/include/AL/alc.h -index 5786bad2..d308fbeb 100644 ---- a/include/AL/alc.h -+++ b/include/AL/alc.h -@@ -187,6 +187,8 @@ ALC_API ALCboolean ALC_APIENTRY alcCloseDevice(ALCdevice *device); - */ - ALC_API ALCenum ALC_APIENTRY alcGetError(ALCdevice *device); - -+ALC_API void ALC_APIENTRY alcSetErrorReasonCallback(void (*c)(const char*)); -+ - /** - * Extension support. - *