diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index 618176093..69be757fd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -414,7 +414,8 @@ namespace Barotrauma if (playSound) { var damageSound = character.GetSound(s => s.Type == CharacterSound.SoundType.Damage); - SoundPlayer.PlayDamageSound(limbJoint.Params.BreakSound, 1.0f, limbJoint.LimbA.body.DrawPosition, range: damageSound != null ? damageSound.Range : 800); + float range = damageSound != null ? damageSound.Range * 2 : ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize().Length() * 10); + SoundPlayer.PlayDamageSound(limbJoint.Params.BreakSound, 1.0f, limbJoint.LimbA.body.DrawPosition, range: range); } } @@ -426,10 +427,10 @@ namespace Barotrauma if (Limbs == null) { - DebugConsole.ThrowError("Failed to draw a ragdoll, limbs have been removed. Character: \"" + character.Name + "\", removed: " + character.Removed + "\n" + Environment.StackTrace); + DebugConsole.ThrowError("Failed to draw a ragdoll, limbs have been removed. Character: \"" + character.Name + "\", removed: " + character.Removed + "\n" + Environment.StackTrace.CleanupStackTrace()); GameAnalyticsManager.AddErrorEventOnce("Ragdoll.Draw:LimbsRemoved", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Failed to draw a ragdoll, limbs have been removed. Character: \"" + character.Name + "\", removed: " + character.Removed + "\n" + Environment.StackTrace); + "Failed to draw a ragdoll, limbs have been removed. Character: \"" + character.Name + "\", removed: " + character.Removed + "\n" + Environment.StackTrace.CleanupStackTrace()); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index ec57f76ff..f35a9f860 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -559,6 +559,18 @@ namespace Barotrauma partial void UpdateProjSpecific(float deltaTime, Camera cam) { + if (InvisibleTimer > 0.0f) + { + if (Controlled == null || (Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f) + { + InvisibleTimer = 0.0f; + } + else + { + InvisibleTimer -= deltaTime; + } + } + if (!enabled) { return; } if (!IsDead && !IsIncapacitated) @@ -568,20 +580,29 @@ namespace Barotrauma soundTimer -= deltaTime; } else if (AIController != null) - { + { switch (AIController.State) { case AIState.Attack: PlaySound(CharacterSound.SoundType.Attack); break; default: - PlaySound(CharacterSound.SoundType.Idle); + var petBehavior = (AIController as EnemyAIController)?.PetBehavior; + if (petBehavior != null && petBehavior.Happiness < petBehavior.MaxHappiness * 0.25f) + { + PlaySound(CharacterSound.SoundType.Unhappy); + } + else + { + PlaySound(CharacterSound.SoundType.Idle); + + } break; } } } - if (info != null || Vitality < MaxVitality * 0.98f) + if (info != null || Vitality < MaxVitality * 0.98f || IsPet) { hudInfoTimer -= deltaTime; if (hudInfoTimer <= 0.0f) @@ -653,7 +674,7 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Camera cam) { - if (!Enabled) { return; } + if (!Enabled || InvisibleTimer > 0.0f) { return; } AnimController.Draw(spriteBatch, cam); } @@ -665,7 +686,7 @@ namespace Barotrauma public virtual void DrawFront(SpriteBatch spriteBatch, Camera cam) { - if (!Enabled) { return; } + if (!Enabled || InvisibleTimer > 0.0f) { return; } if (GameMain.DebugDraw) { @@ -760,49 +781,68 @@ namespace Barotrauma MathHelper.Clamp(1.0f - (cursorDist - (hoverRange - fadeOutRange)) / fadeOutRange, 0.2f, 1.0f) : 1.0f; - if (!GUI.DisableCharacterNames && hudInfoVisible && info != null && - (controlled == null || this != controlled.FocusedCharacter) && cam.Zoom > 0.4f) + if (!GUI.DisableCharacterNames && hudInfoVisible && + (controlled == null || this != controlled.FocusedCharacter || IsPet) && cam.Zoom > 0.4f) { - string name = Info.DisplayName; - if (controlled == null && name != Info.Name) { name += " " + TextManager.Get("Disguised"); } - - Vector2 nameSize = GUI.Font.MeasureString(name); - Vector2 namePos = new Vector2(pos.X, pos.Y - 10.0f - (5.0f / cam.Zoom)) - nameSize * 0.5f / cam.Zoom; - - Vector2 screenSize = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight); - Vector2 viewportSize = new Vector2(cam.WorldView.Width, cam.WorldView.Height); - namePos.X -= cam.WorldView.X; namePos.Y += cam.WorldView.Y; - namePos *= screenSize / viewportSize; - namePos.X = (float)Math.Floor(namePos.X); namePos.Y = (float)Math.Floor(namePos.Y); - namePos *= viewportSize / screenSize; - namePos.X += cam.WorldView.X; namePos.Y -= cam.WorldView.Y; - - Color nameColor = Color.White; - if (Controlled != null && TeamID != Controlled.TeamID) + if (info != null) { - nameColor = TeamID == TeamType.FriendlyNPC ? Color.SkyBlue : GUI.Style.Red; + string name = Info.DisplayName; + if (controlled == null && name != Info.Name) { name += " " + TextManager.Get("Disguised"); } + + Vector2 nameSize = GUI.Font.MeasureString(name); + Vector2 namePos = new Vector2(pos.X, pos.Y - 10.0f - (5.0f / cam.Zoom)) - nameSize * 0.5f / cam.Zoom; + + Vector2 screenSize = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + Vector2 viewportSize = new Vector2(cam.WorldView.Width, cam.WorldView.Height); + namePos.X -= cam.WorldView.X; namePos.Y += cam.WorldView.Y; + namePos *= screenSize / viewportSize; + namePos.X = (float)Math.Floor(namePos.X); namePos.Y = (float)Math.Floor(namePos.Y); + namePos *= viewportSize / screenSize; + namePos.X += cam.WorldView.X; namePos.Y -= cam.WorldView.Y; + + Color nameColor = Color.White; + if (Controlled != null && TeamID != Controlled.TeamID) + { + nameColor = TeamID == TeamType.FriendlyNPC ? Color.SkyBlue : GUI.Style.Red; + } + if (CampaignInteractionType != CampaignMode.InteractionType.None && AllowCustomInteract) + { + var iconStyle = GUI.Style.GetComponentStyle("CampaignInteractionBubble." + CampaignInteractionType); + if (iconStyle != null) + { + Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.WorldPosition ?? WorldPosition + Vector2.UnitY * 100.0f; + Vector2 iconPos = headPos; + iconPos.Y = -iconPos.Y; + nameColor = iconStyle.Color; + var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First(); + float iconScale = 30.0f / icon.Sprite.size.X / cam.Zoom; + icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale); + } + } + + GUI.Font.DrawString(spriteBatch, name, namePos + new Vector2(1.0f / cam.Zoom, 1.0f / cam.Zoom), Color.Black, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.001f); + GUI.Font.DrawString(spriteBatch, name, namePos, nameColor * hudInfoAlpha, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.0f); + if (GameMain.DebugDraw) + { + GUI.Font.DrawString(spriteBatch, ID.ToString(), namePos - new Vector2(0.0f, 20.0f), Color.White); + } } - if (CampaignInteractionType != CampaignMode.InteractionType.None && AllowCustomInteract) + + var petBehavior = (AIController as EnemyAIController)?.PetBehavior; + if (petBehavior != null && !IsDead && !IsUnconscious) { - var iconStyle = GUI.Style.GetComponentStyle("CampaignInteractionBubble." + CampaignInteractionType); + var petStatus = petBehavior.GetCurrentStatusIndicatorType(); + var iconStyle = GUI.Style.GetComponentStyle("PetIcon." + petStatus); if (iconStyle != null) { Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.WorldPosition ?? WorldPosition + Vector2.UnitY * 100.0f; Vector2 iconPos = headPos; iconPos.Y = -iconPos.Y; - nameColor = iconStyle.Color; var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First(); - float iconScale = 30.0f / icon.Sprite.size.X / cam.Zoom; + float iconScale = 30.0f / icon.Sprite.size.X / cam.Zoom; icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale); } } - - GUI.Font.DrawString(spriteBatch, name, namePos + new Vector2(1.0f / cam.Zoom, 1.0f / cam.Zoom), Color.Black, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.001f); - GUI.Font.DrawString(spriteBatch, name, namePos, nameColor * hudInfoAlpha, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.0f); - if (GameMain.DebugDraw) - { - GUI.Font.DrawString(spriteBatch, ID.ToString(), namePos - new Vector2(0.0f, 20.0f), Color.White); - } } if (IsDead) { return; } @@ -846,11 +886,12 @@ namespace Barotrauma private readonly List matchingSounds = new List(); private SoundChannel soundChannel; - public void PlaySound(CharacterSound.SoundType soundType) + public void PlaySound(CharacterSound.SoundType soundType, float soundIntervalFactor = 1.0f) { if (sounds == null || sounds.Count == 0) { return; } if (soundChannel != null && soundChannel.IsPlaying) { return; } if (GameMain.SoundManager?.Disabled ?? true) { return; } + if (soundTimer > soundInterval * soundIntervalFactor) { return; } matchingSounds.Clear(); foreach (var s in sounds) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 2b6d18c7e..ed15b154c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -11,7 +11,7 @@ namespace Barotrauma { class CharacterHUD { - private static Dictionary orderIndicatorCount = new Dictionary(); + private static Dictionary orderIndicatorCount = new Dictionary(); const float ItemOverlayDelay = 1.0f; private static Item focusedItem; private static float focusedItemOverlayTimer; @@ -321,6 +321,17 @@ namespace Barotrauma { character.SelectedConstruction.DrawHUD(spriteBatch, cam, Character.Controlled); } + if (Character.Controlled.Inventory != null) + { + foreach (Item item in Character.Controlled.Inventory.Items) + { + if (item == null) { continue; } + if (Character.Controlled.HasEquippedItem(item)) + { + item.DrawHUD(spriteBatch, cam, Character.Controlled); + } + } + } if (IsCampaignInterfaceOpen) { return; } @@ -415,7 +426,7 @@ namespace Barotrauma Vector2 startPos = character.DrawPosition + (character.FocusedCharacter.DrawPosition - character.DrawPosition) * 0.7f; startPos = cam.WorldToScreen(startPos); - string focusName = character.FocusedCharacter.DisplayName; + string focusName = character.FocusedCharacter.Info == null ? character.FocusedCharacter.DisplayName : character.FocusedCharacter.Info.DisplayName; Vector2 textPos = startPos; Vector2 textSize = GUI.Font.MeasureString(focusName); Vector2 largeTextSize = GUI.SubHeadingFont.MeasureString(focusName); @@ -431,6 +442,14 @@ namespace Barotrauma GUI.DrawString(spriteBatch, textPos, focusName, nameColor, Color.Black * 0.7f, 2, GUI.SubHeadingFont); textPos.X += 10.0f * GUI.Scale; textPos.Y += GUI.SubHeadingFont.MeasureString(focusName).Y; + + if (!character.FocusedCharacter.IsIncapacitated && character.FocusedCharacter.IsPet) + { + GUI.DrawString(spriteBatch, textPos, GetCachedHudText("PlayHint", GameMain.Config.KeyBindText(InputType.Use)), + GUI.Style.Green, Color.Black, 2, GUI.SmallFont); + textPos.Y += largeTextSize.Y; + } + if (character.FocusedCharacter.CanBeDragged) { GUI.DrawString(spriteBatch, textPos, GetCachedHudText("GrabHint", GameMain.Config.KeyBindText(InputType.Grab)), @@ -458,6 +477,8 @@ namespace Barotrauma private static void DrawOrderIndicator(SpriteBatch spriteBatch, Camera cam, Character character, Order order, float iconAlpha = 1.0f) { + if (order?.SymbolSprite == null) { return; } + if (order.TargetAllCharacters) { if (order.OrderGiver != character && !order.HasAppropriateJob(character)) @@ -466,7 +487,7 @@ namespace Barotrauma } } - Entity target = order.ConnectedController != null ? order.ConnectedController.Item : order.TargetEntity; + ISpatialEntity target = order.ConnectedController != null ? order.ConnectedController.Item : order.TargetSpatialEntity; if (target == null) { return; } //don't show the indicator if far away and not inside the same sub @@ -479,7 +500,9 @@ namespace Barotrauma if (!orderIndicatorCount.ContainsKey(target)) { orderIndicatorCount.Add(target, 0); } - Vector2 drawPos = target.DrawPosition + Vector2.UnitX * order.SymbolSprite.size.X * 1.5f * orderIndicatorCount[target]; + Vector2 drawPos = target is Entity ? (target as Entity).DrawPosition : + target.Submarine == null ? target.Position : target.Position + target.Submarine.DrawPosition; + drawPos += Vector2.UnitX * order.SymbolSprite.size.X * 1.5f * orderIndicatorCount[target]; GUI.DrawIndicator(spriteBatch, drawPos, cam, 100.0f, order.SymbolSprite, order.Color * iconAlpha); orderIndicatorCount[target] = orderIndicatorCount[target] + 1; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 87fecf8a9..e80f86d99 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -154,7 +154,7 @@ namespace Barotrauma GUI.Style.Green, textPopupPos, Vector2.UnitY * 10.0f, - playSound: Character.Controlled?.Info == this); + playSound: false); } else if (prevLevel % 0.1f > 0.05f && newLevel % 0.1f < 0.05f) { @@ -163,7 +163,7 @@ namespace Barotrauma GUI.Style.Green, textPopupPos, Vector2.UnitY * 10.0f, - playSound: Character.Controlled?.Info == this); + playSound: false); } if ((int)newLevel > (int)prevLevel) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index b1bca5ab8..6fc7e4290 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -1,4 +1,5 @@ using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; @@ -83,6 +84,10 @@ namespace Barotrauma { newMem.interact = FocusedCharacter.ID; } + else if (newMem.states.HasFlag(InputNetFlags.Use) && (FocusedCharacter?.IsPet ?? false)) + { + newMem.interact = FocusedCharacter.ID; + } else if (focusedItem != null && !CharacterInventory.DraggingItemToWorld && !newMem.states.HasFlag(InputNetFlags.Grab) && !newMem.states.HasFlag(InputNetFlags.Health)) { @@ -253,6 +258,7 @@ namespace Barotrauma if (readStatus) { ReadStatus(msg); + (AIController as EnemyAIController)?.PetBehavior?.ClientRead(msg); } msg.ReadPadBits(); @@ -444,14 +450,25 @@ namespace Barotrauma Entity targetEntity = FindEntityByID(inc.ReadUInt16()); Character orderGiver = inc.ReadBoolean() ? FindEntityByID(inc.ReadUInt16()) as Character : null; int orderOptionIndex = inc.ReadByte(); + OrderTarget targetPosition = null; + if (inc.ReadBoolean()) + { + var x = inc.ReadSingle(); + var y = inc.ReadSingle(); + var hull = FindEntityByID(inc.ReadUInt16()) as Hull; + targetPosition = new OrderTarget(new Vector2(x, y), hull, true); + } if (orderPrefabIndex >= 0 && orderPrefabIndex < Order.PrefabList.Count) { var orderPrefab = Order.PrefabList[orderPrefabIndex]; - if (!orderPrefab.MustSetTarget || (targetEntity != null && (targetEntity as Item).Components.Any(c => c?.GetType() == orderPrefab.ItemComponentType))) + var component = orderPrefab.GetTargetItemComponent(targetEntity as Item); + if (!orderPrefab.MustSetTarget || (targetEntity != null && component != null) || targetPosition != null) { - character.SetOrder( - new Order(orderPrefab, targetEntity, (targetEntity as Item)?.Components.FirstOrDefault(c => c?.GetType() == orderPrefab.ItemComponentType), orderGiver: orderGiver), + var order = targetPosition == null ? + new Order(orderPrefab, targetEntity, component, orderGiver: orderGiver) : + new Order(orderPrefab, targetPosition, orderGiver: orderGiver); + character.SetOrder(order, orderOptionIndex >= 0 && orderOptionIndex < orderPrefab.Options.Length ? orderPrefab.Options[orderOptionIndex] : null, orderGiver, speak: false); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs index e93c6e2b8..c247da8e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs @@ -6,7 +6,7 @@ namespace Barotrauma { public enum SoundType { - Idle, Attack, Die, Damage + Idle, Attack, Die, Damage, Happy, Unhappy } private readonly RoundSound roundSound; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs index 03ab84938..e08502aa9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs @@ -1,38 +1,26 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Barotrauma { partial class AfflictionPsychosis : Affliction { - class FakeFireSource - { - public Vector2 Size; - public Vector2 Position; - public Hull Hull; - - public float LifeTime; - } - const int MaxFakeFireSources = 10; const float MinFakeFireSourceInterval = 30.0f, MaxFakeFireSourceInterval = 240.0f; private float createFireSourceTimer; - private readonly List fakeFireSources = new List(); + private readonly List fakeFireSources = new List(); - enum FloodType + public enum FloodType { + None, Minor, Major, HideFlooding } - const float MinSoundInterval = 10.0f, MaxSoundInterval = 180.0f; + const float MinSoundInterval = 60.0f, MaxSoundInterval = 240.0f; private FloodType currentFloodType; private float soundTimer; @@ -44,12 +32,21 @@ namespace Barotrauma private float fakeBrokenInterval = 30.0f; private float fakeBrokenTimer = 0.0f; + private float invisibleCharacterInterval = 30.0f; + private float invisibleCharacterTimer = 0.0f; + + public FloodType CurrentFloodType + { + get { return currentFloodType; } + } + partial void UpdateProjSpecific(CharacterHealth characterHealth, Limb targetLimb, float deltaTime) { if (Character.Controlled != characterHealth.Character) return; UpdateFloods(deltaTime); UpdateSounds(characterHealth.Character, deltaTime); UpdateFires(characterHealth.Character, deltaTime); + UpdateInvisibleCharacters(deltaTime); UpdateFakeBroken(deltaTime); } @@ -77,10 +74,14 @@ namespace Barotrauma { case FloodType.Minor: currentFloodState += deltaTime; - //lerp the water surface in all hulls 50 units above the floor within 10 seconds + //lerp the water surface in all hulls 15 units above the floor within 10 seconds foreach (Hull hull in Hull.hullList) { - hull.DrawSurface = hull.Rect.Y - hull.Rect.Height + MathHelper.Lerp(0.0f, 50.0f, currentFloodState / 10.0f); + for (int i = hull.FakeFireSources.Count - 1; i >= 0; i--) + { + hull.FakeFireSources[i].Extinguish(deltaTime, 50.0f); + } + hull.DrawSurface = hull.Rect.Y - hull.Rect.Height + MathHelper.Lerp(0.0f, 15.0f, currentFloodState / 10.0f); } break; case FloodType.Major: @@ -88,6 +89,10 @@ namespace Barotrauma //create a full flood in 10 seconds foreach (Hull hull in Hull.hullList) { + for (int i = hull.FakeFireSources.Count - 1; i >= 0; i--) + { + hull.FakeFireSources[i].Extinguish(deltaTime, 200.0f); + } hull.DrawSurface = hull.Rect.Y - MathHelper.Lerp(hull.Rect.Height, 0.0f, currentFloodState / 10.0f); } break; @@ -104,6 +109,7 @@ namespace Barotrauma if (createFloodTimer < MathHelper.Lerp(MaxFloodInterval, MinFloodInterval, Strength / 100.0f)) { + currentFloodType = FloodType.None; createFloodTimer += deltaTime; return; } @@ -114,10 +120,12 @@ namespace Barotrauma if (Rand.Range(0.0f, 1.0f) < 0.5f) { currentFloodType = FloodType.HideFlooding; + currentFloodType = FloodType.Minor; } else { - currentFloodType = Strength < 50.0f ? FloodType.Minor : FloodType.Major; + //disabled Major flooding because it's too easy to tell it's fake + currentFloodType = FloodType.Minor;// Strength < 50.0f ? FloodType.Minor : FloodType.Major; } currentFloodDuration = Rand.Range(20.0f, 100.0f); } @@ -127,47 +135,46 @@ namespace Barotrauma private void UpdateFires(Character character, float deltaTime) { createFireSourceTimer += deltaTime; + fakeFireSources.RemoveAll(fs => fs.Removed); if (fakeFireSources.Count < MaxFakeFireSources && character.Submarine != null && createFireSourceTimer > MathHelper.Lerp(MaxFakeFireSourceInterval, MinFakeFireSourceInterval, Strength / 100.0f)) { Hull fireHull = Hull.hullList.GetRandom(h => h.Submarine == character.Submarine); - - fakeFireSources.Add(new FakeFireSource() + if (fireHull != null) { - Size = Vector2.One * 20.0f, - Hull = fireHull, - Position = new Vector2(Rand.Range(0.0f, fireHull.Rect.Width), 30.0f), - LifeTime = MathHelper.Lerp(10.0f, 100.0f, Strength / 100.0f) - }); - createFireSourceTimer = 0.0f; - } - - foreach (FakeFireSource fakeFireSource in fakeFireSources) - { - if (fakeFireSource.Hull.DrawSurface > fakeFireSource.Hull.Rect.Y - fakeFireSource.Hull.Rect.Height + fakeFireSource.Position.Y) - { - fakeFireSource.LifeTime -= deltaTime * 100.0f; + var fakeFire = new DummyFireSource(Vector2.One * 500.0f, new Vector2(Rand.Range(fireHull.WorldRect.X, fireHull.WorldRect.Right), fireHull.WorldPosition.Y), fireHull, isNetworkMessage: true) + { + CausedByPsychosis = true, + DamagesItems = false, + DamagesCharacters = false + }; + fakeFireSources.Add(fakeFire); + createFireSourceTimer = 0.0f; } + } + } - fakeFireSource.LifeTime -= deltaTime; - float growAmount = deltaTime * 5.0f; - fakeFireSource.Size.X += growAmount; - fakeFireSource.Position.X = MathHelper.Clamp(fakeFireSource.Position.X - growAmount / 2.0f, 0.0f, fakeFireSource.Hull.Rect.Width); - fakeFireSource.Position.Y = MathHelper.Clamp(fakeFireSource.Position.Y, 0.0f, fakeFireSource.Hull.Rect.Height); - fakeFireSource.Size.X = Math.Min(fakeFireSource.Hull.Rect.Width - fakeFireSource.Position.X, fakeFireSource.Size.X); - fakeFireSource.Size.Y = Math.Min(fakeFireSource.Hull.Rect.Height - fakeFireSource.Position.Y, fakeFireSource.Size.Y); + private void UpdateInvisibleCharacters(float deltaTime) + { + invisibleCharacterTimer -= deltaTime; + if (invisibleCharacterTimer > 0.0f) { return; } - FireSource.EmitParticles( - fakeFireSource.Size, - fakeFireSource.Hull.WorldRect.Location.ToVector2() + fakeFireSource.Position - Vector2.UnitY * fakeFireSource.Hull.Rect.Height, - fakeFireSource.Hull, - 0.5f); + foreach (Character c in Character.CharacterList) + { + if (c.IsDead || c == Character.Controlled) { continue; } + if (c.WorldPosition.X < GameMain.GameScreen.Cam.WorldView.X || c.WorldPosition.X > GameMain.GameScreen.Cam.WorldView.Right) { continue; } + if (c.WorldPosition.Y < GameMain.GameScreen.Cam.WorldView.Y - GameMain.GameScreen.Cam.WorldView.Height || c.WorldPosition.Y > GameMain.GameScreen.Cam.WorldView.Y) { continue; } + if (Rand.Range(0.0f, 500.0f) < Strength) + { + c.InvisibleTimer = 60.0f; + } } - fakeFireSources.RemoveAll(fs => fs.LifeTime <= 0.0f); + invisibleCharacterTimer = invisibleCharacterInterval; } + private void UpdateFakeBroken(float deltaTime) { fakeBrokenTimer -= deltaTime; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 6bdbfd16d..96b3eb799 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -225,7 +225,15 @@ namespace Barotrauma Character.Controlled.ResetInteract = true; if (openHealthWindow != null) { - openHealthWindow.characterName.Text = value.Character.Name; + if (value.Character.Info == null || Character.Controlled.HasEquippedItem("healthscanner")) + { + openHealthWindow.characterName.Text = value.Character.Name; + } + else + { + openHealthWindow.characterName.Text = value.Character.Info.DisplayName; + } + if (Character.Controlled.SelectedConstruction != null && Character.Controlled.SelectedConstruction.GetComponent() == null) { Character.Controlled.SelectedConstruction = null; @@ -545,7 +553,7 @@ namespace Barotrauma private void OnAttacked(Character attacker, AttackResult attackResult) { - if (Math.Abs(attackResult.Damage) < 0.01f && attackResult.Afflictions.Count == 0) { return; } + if (Math.Abs(attackResult.Damage) < 0.01f) { return; } DamageOverlayTimer = MathHelper.Clamp(attackResult.Damage / MaxVitality, DamageOverlayTimer, 1.0f); if (healthShadowDelay <= 0.0f) { healthShadowDelay = 1.0f; } @@ -611,17 +619,41 @@ namespace Barotrauma } } - partial void UpdateOxygenProjSpecific(float prevOxygen) + private float timeUntilNextHeartbeatSound = 0.0f; + private bool nextHeartbeatSoundIsSystole = true; + private const string diastoleSoundTag = "heartbeatdiastole", systoleSoundTag = "heartbeatsystole"; + + partial void UpdateOxygenProjSpecific(float prevOxygen, float deltaTime) { - if (prevOxygen > 0.0f && OxygenAmount <= 0.0f && - Character.Controlled == Character) + if (prevOxygen > 0.0f && OxygenAmount <= 0.0f && Character.Controlled == Character) { SoundPlayer.PlaySound(Character.Info != null && Character.Info.Gender == Gender.Female ? "drownfemale" : "drownmale"); } + + if (Character == Character.Controlled && !IsUnconscious && !Character.IsDead && OxygenAmount < LowOxygenThreshold) + { + timeUntilNextHeartbeatSound -= deltaTime; + if (timeUntilNextHeartbeatSound < 0.0f) + { + if (nextHeartbeatSoundIsSystole) + { + SoundPlayer.PlaySound(systoleSoundTag, 1.0f - (OxygenAmount / LowOxygenThreshold)); + timeUntilNextHeartbeatSound = MathHelper.Lerp(0.18f, 0.3f, Math.Clamp(OxygenAmount / InsufficientOxygenThreshold, 0.0f, 1.0f)); + } + else + { + SoundPlayer.PlaySound(diastoleSoundTag, 1.0f - (OxygenAmount / LowOxygenThreshold)); + timeUntilNextHeartbeatSound = MathHelper.Lerp(0.3f, 0.5f, Math.Clamp(OxygenAmount / InsufficientOxygenThreshold, 0.0f, 1.0f)); + } + nextHeartbeatSoundIsSystole = !nextHeartbeatSoundIsSystole; + } + } } partial void UpdateBleedingProjSpecific(AfflictionBleeding affliction, Limb targetLimb, float deltaTime) { + if (Character.InvisibleTimer > 0.0f) { return; } + bloodParticleTimer -= deltaTime * (affliction.Strength / 10.0f); if (bloodParticleTimer <= 0.0f) { @@ -664,7 +696,7 @@ namespace Barotrauma { forceAfflictionContainerUpdate = true; currentDisplayedAfflictions = GetAllAfflictions(mergeSameAfflictions: true) - .FindAll(a => a.Strength >= a.Prefab.ShowIconThreshold && a.Prefab.Icon != null); + .FindAll(a => a.ShouldShowIcon(Character) && a.Prefab.Icon != null); currentDisplayedAfflictions.Sort((a1, a2) => { int dmgPerSecond = Math.Sign(a2.DamagePerSecond - a1.DamagePerSecond); @@ -1147,7 +1179,7 @@ namespace Barotrauma afflictionIconContainer.Content.ClearChildren(); return; } - var currentAfflictions = GetMatchingAfflictions(selectedLimb, a => a.Strength >= a.Prefab.ShowIconThreshold); + var currentAfflictions = GetMatchingAfflictions(selectedLimb, a => a.ShouldShowIcon(Character)); var displayedAfflictions = afflictionIconContainer.Content.Children.Select(c => c.UserData as Affliction); if (currentAfflictions.Any(a => !displayedAfflictions.Contains(a)) || displayedAfflictions.Any(a => !currentAfflictions.Contains(a))) @@ -1675,9 +1707,9 @@ namespace Barotrauma var tempAfflictions = GetMatchingAfflictions(limbHealth, a => true); - float negativeEffect = tempAfflictions.Where(a => !a.Prefab.IsBuff && a.Strength >= a.Prefab.ShowIconThreshold).Sum(a => a.Strength); + float negativeEffect = tempAfflictions.Where(a => !a.Prefab.IsBuff && a.ShouldShowIcon(Character)).Sum(a => a.Strength); //float negativeMaxEffect = tempAfflictions.Where(a => !a.Prefab.IsBuff).Sum(a => a.Prefab.MaxStrength); - float positiveEffect = tempAfflictions.Where(a => a.Prefab.IsBuff && a.Strength >= a.Prefab.ShowIconThreshold).Sum(a => a.Strength * 0.2f); + float positiveEffect = tempAfflictions.Where(a => a.Prefab.IsBuff && a.ShouldShowIcon(Character)).Sum(a => a.Strength * 0.2f); //float positiveMaxEffect = tempAfflictions.Where(a => a.Prefab.IsBuff).Sum(a => a.Prefab.MaxStrength); float midPoint = (float)limbEffectiveArea.Center.Y / (float)limbHealth.IndicatorSprite.Texture.Height; @@ -1786,11 +1818,11 @@ namespace Barotrauma i = 0; foreach (LimbHealth limbHealth in limbHealths) { - IEnumerable thisAfflictions = limbHealth.Afflictions.Where(a => a.Strength >= a.Prefab.ShowIconThreshold); + IEnumerable thisAfflictions = limbHealth.Afflictions.Where(a => a.ShouldShowIcon(Character)); thisAfflictions = thisAfflictions.Concat(afflictions.Where(a => { Limb indicatorLimb = Character.AnimController.GetLimb(a.Prefab.IndicatorLimb); - return (indicatorLimb != null && indicatorLimb.HealthIndex == i && a.Strength >= a.Prefab.ShowIconThreshold); + return indicatorLimb != null && indicatorLimb.HealthIndex == i && a.ShouldShowIcon(Character); })); if (thisAfflictions.Count() <= 0) { i++; continue; } @@ -1837,12 +1869,14 @@ namespace Barotrauma private void DrawLimbAfflictionIcon(SpriteBatch spriteBatch, Affliction affliction, float iconScale, ref Vector2 iconPos) { - if (affliction.Strength < affliction.Prefab.ShowIconThreshold) return; - Vector2 iconSize = (affliction.Prefab.Icon.size * iconScale); + if (!affliction.ShouldShowIcon(Character)) { return; } + Vector2 iconSize = affliction.Prefab.Icon.size * iconScale; + + float showIconThreshold = Character.Controlled?.CharacterHealth == this ? affliction.Prefab.ShowIconThreshold : affliction.Prefab.ShowIconToOthersThreshold; //afflictions that have a strength of less than 10 are faded out slightly float alpha = MathHelper.Lerp(0.3f, 1.0f, - (affliction.Strength - affliction.Prefab.ShowIconThreshold) / Math.Min(affliction.Prefab.MaxStrength - affliction.Prefab.ShowIconThreshold, 10.0f)); + (affliction.Strength - showIconThreshold) / Math.Min(affliction.Prefab.MaxStrength - showIconThreshold, 10.0f)); affliction.Prefab.Icon.Draw(spriteBatch, iconPos - iconSize / 2.0f, GetAfflictionIconColor(affliction.Prefab, affliction) * alpha, 0, iconScale); iconPos += new Vector2(10.0f, 20.0f) * iconScale; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/DamageModifier.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/DamageModifier.cs index c9258a8a0..30833a339 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/DamageModifier.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/DamageModifier.cs @@ -8,5 +8,12 @@ get; private set; } + + [Serialize("", false), Editable] + public string DamageParticle + { + get; + private set; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index c258b3591..f452ddcb2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -278,17 +278,7 @@ namespace Barotrauma DamagedSprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.damagedSpriteParams)); break; case "conditionalsprite": - ISerializableEntity targetEntity; - string target = subElement.GetAttributeString("target", null); - if (string.Equals(target, "character", StringComparison.OrdinalIgnoreCase)) - { - targetEntity = character; - } - else - { - targetEntity = this; - } - var conditionalSprite = new ConditionalSprite(subElement, targetEntity, file: GetSpritePath(subElement, null)); + var conditionalSprite = new ConditionalSprite(subElement, GetConditionalTarget(), file: GetSpritePath(subElement, null)); ConditionalSprites.Add(conditionalSprite); if (conditionalSprite.DeformableSprite != null) { @@ -300,7 +290,7 @@ namespace Barotrauma CreateDeformations(subElement); break; case "lightsource": - LightSource = new LightSource(subElement) + LightSource = new LightSource(subElement, GetConditionalTarget()) { ParentBody = body, SpriteScale = Vector2.One * Scale * TextureScale @@ -310,6 +300,21 @@ namespace Barotrauma break; } + ISerializableEntity GetConditionalTarget() + { + ISerializableEntity targetEntity; + string target = subElement.GetAttributeString("target", null); + if (string.Equals(target, "character", StringComparison.OrdinalIgnoreCase)) + { + targetEntity = character; + } + else + { + targetEntity = this; + } + return targetEntity; + } + void CreateDeformations(XElement e) { foreach (XElement animationElement in e.GetChildElements("spritedeformation")) @@ -341,6 +346,7 @@ namespace Barotrauma } } } + LightSource?.CheckConditionals(); } public void RecreateSprites() @@ -459,18 +465,16 @@ namespace Barotrauma } } float damageMultiplier = 1; + float bleedingDamageMultiplier = 1; foreach (DamageModifier damageModifier in result.AppliedDamageModifiers) { - foreach (var afflictionPrefab in AfflictionPrefab.List) + if (damageModifier.MatchesAfflictionType("damage")) { - if (damageModifier.MatchesAffliction(afflictionPrefab.Identifier, afflictionPrefab.AfflictionType)) - { - if (afflictionPrefab.Effects.Any(e => e.MaxVitalityDecrease > 0)) - { - damageMultiplier *= damageModifier.DamageMultiplier; - break; - } - } + damageMultiplier *= damageModifier.DamageMultiplier; + } + else if (damageModifier.MatchesAfflictionType("bleeding")) + { + bleedingDamageMultiplier *= damageModifier.DamageMultiplier; } } if (playSound) @@ -488,20 +492,29 @@ namespace Barotrauma } // spawn damage particles - float damageParticleAmount = Math.Min(damage / 5, 1.0f) * damageMultiplier; + float damageParticleAmount = damage < 1 ? 0 : Math.Min(damage / 5, 1.0f) * damageMultiplier; if (damageParticleAmount > 0.001f) { foreach (ParticleEmitter emitter in character.DamageEmitters) { if (inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } if (!inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } - emitter.Emit(1.0f, WorldPosition, character.CurrentHull, amountMultiplier: damageParticleAmount); + ParticlePrefab overrideParticle = null; + foreach (DamageModifier damageModifier in result.AppliedDamageModifiers) + { + if (damageModifier.DamageMultiplier > 0 && !string.IsNullOrWhiteSpace(damageModifier.DamageParticle)) + { + overrideParticle = GameMain.ParticleManager?.FindPrefab(damageModifier.DamageParticle); + break; + } + } + emitter.Emit(1.0f, WorldPosition, character.CurrentHull, amountMultiplier: damageParticleAmount, overrideParticle: overrideParticle); } } if (bleedingDamage > 0) { - float bloodParticleAmount = Math.Min(bleedingDamage / 5, 1.0f) * damageMultiplier; + float bloodParticleAmount = Math.Min(bleedingDamage / 5, 1.0f) * bleedingDamageMultiplier; float bloodParticleSize = MathHelper.Clamp(bleedingDamage / 5, 0.1f, 1.0f); foreach (ParticleEmitter emitter in character.BloodEmitters) @@ -554,6 +567,11 @@ namespace Barotrauma } } + foreach (var conditionalSprite in ConditionalSprites) + { + conditionalSprite.CheckConditionals(); + } + if (LightSource != null) { LightSource.ParentSub = body.Submarine; @@ -566,6 +584,7 @@ namespace Barotrauma { LightSource.DeformableLightSprite.Sprite.Depth = ActiveSprite.Depth; } + LightSource.CheckConditionals(); } UpdateSpriteStates(deltaTime); @@ -930,6 +949,10 @@ namespace Barotrauma { depth -= depthStep; } + if (wearableItemComponent.AllowedSlots.Contains(InvSlotType.Bag)) + { + depth -= depthStep * 2; + } wearableColor = wearableItemComponent.Item.GetSpriteColor(); } float textureScale = wearable.InheritTextureScale ? TextureScale : 1; diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 20fa2e1ef..d9a19a6ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -2575,7 +2575,7 @@ namespace Barotrauma { string errorMsg = "Failed to spawn a submarine. Arguments: \"" + string.Join(" ", args) + "\"."; ThrowError(errorMsg, e); - GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnSubmarine:Error", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace); + GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnSubmarine:Error", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace.CleanupStackTrace()); } }, () => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index 9c2c4795a..a3270bafa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -271,7 +271,7 @@ namespace Barotrauma private static List CreateConversation(GUIListBox parentBox, string text, Character speaker, IEnumerable options, bool drawChathead = true) { - var content = new GUILayoutGroup(new RectTransform(Vector2.One, parentBox.Content.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) + var content = new GUILayoutGroup(new RectTransform(Vector2.One, parentBox.Content.RectTransform), childAnchor: Anchor.TopLeft, isHorizontal: true) { Stretch = true, CanBeFocused = true, @@ -289,7 +289,7 @@ namespace Barotrauma }); } - var textContent = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), content.RectTransform), childAnchor: Anchor.TopCenter) + var textContent = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0f), content.RectTransform), childAnchor: Anchor.TopCenter) { AbsoluteSpacing = GUI.IntScale(5) }; @@ -316,7 +316,7 @@ namespace Barotrauma content.Recalculate(); textContent.Recalculate(); textBlock.CalculateHeightFromText(); - textBlock.RectTransform.MinSize = new Point(0, (int)(textBlock.Rect.Height * 1.2f)); + textBlock.RectTransform.MinSize = new Point(0, textBlock.Rect.Height); foreach (GUIButton btn in buttons) { btn.TextBlock.SetTextPos(); @@ -324,10 +324,12 @@ namespace Barotrauma btn.RectTransform.MinSize = new Point(0, (int)(btn.TextBlock.Rect.Height * 1.2f)); } - textContent.RectTransform.MinSize = new Point(0, textContent.Children.Sum(c => c.Rect.Height + textContent.AbsoluteSpacing) + GUI.IntScale(16)); + textContent.RectTransform.MinSize = new Point(0, textContent.Children.Sum(c => c.Rect.Height) + GUI.IntScale(16)); + content.RectTransform.MinSize = new Point(0, content.Children.Sum(c => c.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(); + textBlock.TextAlignment = Alignment.TopLeft; //content.RectTransform.MinSize = new Point(0, textContent.Rect.Height); return buttons; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index 31a5b9add..41ac0aeba 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -109,7 +109,7 @@ namespace Barotrauma if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) { SetChannel(radio.Channel - 1, setText: true); - GUI.PlayUISound(GUISoundType.PopupMenu); + SoundPlayer.PlayUISound(GUISoundType.PopupMenu); } return true; } @@ -142,7 +142,7 @@ namespace Barotrauma { int.TryParse(channelText.Text, out int newChannel); SetChannel(newChannel, setText: true); - GUI.PlayUISound(GUISoundType.PopupMenu); + SoundPlayer.PlayUISound(GUISoundType.PopupMenu); }; var buttonRight = new GUIButton(new RectTransform(new Vector2(0.1f, 0.8f), channelSettingsContent.RectTransform), style: "DeviceButton") @@ -152,7 +152,7 @@ namespace Barotrauma if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) { SetChannel(radio.Channel + 1, setText: true); - GUI.PlayUISound(GUISoundType.PopupMenu); + SoundPlayer.PlayUISound(GUISoundType.PopupMenu); } return true; } @@ -194,7 +194,7 @@ namespace Barotrauma channelText.Flash(GUI.Style.Green); } SetChannel(radio.GetChannelMemory(index), setText: true); - GUI.PlayUISound(GUISoundType.PopupMenu); + SoundPlayer.PlayUISound(GUISoundType.PopupMenu); } return true; } @@ -469,7 +469,7 @@ namespace Barotrauma soundType = GUISoundType.DeadMessage; } - GUI.PlayUISound(soundType); + SoundPlayer.PlayUISound(soundType); } public void SetVisibility(bool visible) @@ -650,6 +650,8 @@ namespace Barotrauma if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) { radio.Channel = channel; + GameMain.Client?.CreateEntityEvent(radio.Item, new object[] { NetEntityEvent.Type.ChangeProperty, radio.SerializableProperties["channel"] }); + if (setText) { string text = radio.Channel.ToString().PadLeft(4, '0'); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 159c2218d..b84ba031d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -205,6 +205,7 @@ namespace Barotrauma }; validateHiresButton = new GUIButton(new RectTransform(new Vector2(1.0f / 3.0f, 1.0f), group.RectTransform), text: TextManager.Get("campaigncrew.validate")) { + ClickSound = GUISoundType.HireRepairClick, ForceUpperCase = true, OnClicked = (b, o) => ValidatePendingHires(true) }; @@ -390,12 +391,29 @@ namespace Barotrauma if (listBox == hireableList) { - new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementAddButton") + var hireButton = new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementAddButton") { UserData = characterInfo, Enabled = HasPermission, OnClicked = (b, o) => AddPendingHire(o as CharacterInfo) }; + hireButton.OnAddedToGUIUpdateList += (GUIComponent btn) => + { + if (PendingHires.Count + campaign.CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize) + { + if (btn.Enabled) + { + btn.ToolTip = TextManager.Get("canthiremorecharacters"); + btn.Enabled = false; + } + } + else if (!btn.Enabled) + { + btn.ToolTip = string.Empty; + btn.Enabled = true; + } + }; + } else if (listBox == pendingList) { @@ -514,6 +532,11 @@ namespace Barotrauma private bool AddPendingHire(CharacterInfo characterInfo, bool createNetworkMessage = true) { + if (PendingHires.Count + campaign.CrewManager.GetCharacters().Count() >= CrewManager.MaxCrewSize) + { + return false; + } + hireableList.Content.RemoveChild(hireableList.Content.FindChild(c => (c.UserData as Tuple).Item1 == characterInfo)); hireableList.UpdateScrollBarSize(); if (!PendingHires.Contains(characterInfo)) { PendingHires.Add(characterInfo); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index c77f2b94e..3b0cea052 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -27,7 +27,11 @@ namespace Barotrauma PickItem, PickItemFail, DropItem, - PopupMenu + PopupMenu, + DecreaseQuantity, + IncreaseQuantity, + HireRepairClick, + UISwitch } public enum CursorState @@ -135,7 +139,7 @@ namespace Barotrauma public static GraphicsDevice GraphicsDevice { get; private set; } private static List messages = new List(); - private static Sound[] sounds; + private static readonly Dictionary soundIdentifiers = new Dictionary(); private static bool pauseMenuOpen, settingsMenuOpen; public static GUIFrame PauseMenu { get; private set; } private static Sprite arrow; @@ -256,23 +260,13 @@ namespace Barotrauma } } - public static void LoadContent(bool loadSounds = true) + public static void LoadContent() { - if (loadSounds) + foreach (GUISoundType soundType in Enum.GetValues(typeof(GUISoundType))) { - sounds = new Sound[Enum.GetValues(typeof(GUISoundType)).Length]; - - sounds[(int)GUISoundType.UIMessage] = GameMain.SoundManager.LoadSound("Content/Sounds/UI/UImsg.ogg", false); - sounds[(int)GUISoundType.ChatMessage] = GameMain.SoundManager.LoadSound("Content/Sounds/UI/ChatMsg.ogg", false); - sounds[(int)GUISoundType.RadioMessage] = GameMain.SoundManager.LoadSound("Content/Sounds/UI/RadioMsg.ogg", false); - sounds[(int)GUISoundType.DeadMessage] = GameMain.SoundManager.LoadSound("Content/Sounds/UI/DeadMsg.ogg", false); - sounds[(int)GUISoundType.Click] = GameMain.SoundManager.LoadSound("Content/Sounds/UI/Click.ogg", false); - sounds[(int)GUISoundType.PopupMenu] = GameMain.SoundManager.LoadSound("Content/Sounds/UI/PopupMenu.ogg", false); - - sounds[(int)GUISoundType.PickItem] = GameMain.SoundManager.LoadSound("Content/Sounds/PickItem.ogg", false); - sounds[(int)GUISoundType.PickItemFail] = GameMain.SoundManager.LoadSound("Content/Sounds/PickItemFail.ogg", false); - sounds[(int)GUISoundType.DropItem] = GameMain.SoundManager.LoadSound("Content/Sounds/DropItem.ogg", false); + soundIdentifiers.Add(soundType, soundType.ToString().ToLowerInvariant()); } + // create 1x1 texture for line drawing CrossThread.RequestExecutionOnMainThread(() => { @@ -314,7 +308,7 @@ namespace Barotrauma #if UNSTABLE string line1 = "Barotrauma Unstable v" + GameMain.Version; - string line2 = "(" + AssemblyInfo.GetBuildString() + ", branch " + AssemblyInfo.GetGitBranch() + ", revision " + AssemblyInfo.GetGitRevision() + ")"; + string line2 = "(" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"; Rectangle watermarkRect = new Rectangle(-50, GameMain.GraphicsHeight - 80, 50 + (int)(Math.Max(LargeFont.MeasureString(line1).X, Font.MeasureString(line2).X) * 1.2f), 100); float alpha = 1.0f; @@ -2282,13 +2276,13 @@ namespace Barotrauma { if (messages.Any(msg => msg.Text == message)) { return; } messages.Add(new GUIMessage(message, color, lifeTime ?? MathHelper.Clamp(message.Length / 5.0f, 3.0f, 10.0f), font ?? LargeFont)); - if (playSound) PlayUISound(GUISoundType.UIMessage); + if (playSound) SoundPlayer.PlayUISound(GUISoundType.UIMessage); } - public static void AddMessage(string message, Color color, Vector2 worldPos, Vector2 velocity, float lifeTime = 3.0f, bool playSound = true) + public static void AddMessage(string message, Color color, Vector2 worldPos, Vector2 velocity, float lifeTime = 3.0f, bool playSound = true, GUISoundType soundType = GUISoundType.UIMessage) { messages.Add(new GUIMessage(message, color, worldPos, velocity, lifeTime, Alignment.Center, LargeFont)); - if (playSound) PlayUISound(GUISoundType.UIMessage); + if (playSound) SoundPlayer.PlayUISound(soundType); } public static void ClearMessages() @@ -2296,16 +2290,6 @@ namespace Barotrauma messages.Clear(); } - public static void PlayUISound(GUISoundType soundType) - { - if (sounds == null) { return; } - - int soundIndex = (int)soundType; - if (soundIndex < 0 || soundIndex >= sounds.Length) { return; } - - sounds[soundIndex]?.Play(null, "ui"); - } - public static bool IsFourByThree() { float aspectRatio = HorizontalAspectRatio; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs index 21acaa8f5..78af6642a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs @@ -157,6 +157,8 @@ namespace Barotrauma private float pulseTimer; private float pulseExpand; private bool flashed; + + public GUISoundType ClickSound { get; set; } = GUISoundType.Click; public GUIButton(RectTransform rectT, string text = "", Alignment textAlignment = Alignment.Center, string style = "", Color? color = null) : base(style, rectT) { @@ -247,7 +249,7 @@ namespace Barotrauma } else if (PlayerInput.PrimaryMouseButtonClicked()) { - GUI.PlayUISound(GUISoundType.Click); + SoundPlayer.PlayUISound(ClickSound); if (OnClicked != null) { if (OnClicked(this, UserData)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 7b6af817b..5f1c22356 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -404,7 +404,7 @@ namespace Barotrauma /// public void ScrollToElement(GUIComponent component) { - GUI.PlayUISound(GUISoundType.Click); + SoundPlayer.PlayUISound(GUISoundType.Click); List children = Content.Children.ToList(); int index = children.IndexOf(component); if (index < 0) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index 46cb7d19c..a2e32d741 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -17,7 +17,8 @@ namespace Barotrauma public GUITextBox TextBox { get; private set; } - private GUIButton plusButton, minusButton; + public GUIButton PlusButton { get; private set; } + public GUIButton MinusButton { get; private set; } private NumberType inputType; public NumberType InputType @@ -142,14 +143,14 @@ namespace Barotrauma get => base.Enabled; set { - plusButton.Enabled = true; - minusButton.Enabled = true; + PlusButton.Enabled = true; + MinusButton.Enabled = true; if (InputType == NumberType.Int) { ClampIntValue(); } else { ClampFloatValue(); } TextBox.Enabled = value; if (!value) { - plusButton.Enabled = false; - minusButton.Enabled = false; + PlusButton.Enabled = false; + MinusButton.Enabled = false; } } } @@ -193,19 +194,19 @@ namespace Barotrauma TextBox.OnTextChanged += TextChanged; var buttonArea = new GUIFrame(new RectTransform(new Vector2(_relativeButtonAreaWidth, 1.0f), LayoutGroup.RectTransform, Anchor.CenterRight), style: null); - plusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform), style: null); - GUI.Style.Apply(plusButton, "PlusButton", this); - plusButton.OnButtonDown += () => + PlusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform), style: null); + GUI.Style.Apply(PlusButton, "PlusButton", this); + PlusButton.OnButtonDown += () => { pressedTimer = pressedDelay; return true; }; - plusButton.OnClicked += (button, data) => + PlusButton.OnClicked += (button, data) => { IncreaseValue(); return true; }; - plusButton.OnPressed += () => + PlusButton.OnPressed += () => { if (!IsPressedTimerRunning) { @@ -214,19 +215,19 @@ namespace Barotrauma return true; }; - minusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform, Anchor.BottomRight), style: null); - GUI.Style.Apply(minusButton, "MinusButton", this); - minusButton.OnButtonDown += () => + MinusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform, Anchor.BottomRight), style: null); + GUI.Style.Apply(MinusButton, "MinusButton", this); + MinusButton.OnButtonDown += () => { pressedTimer = pressedDelay; return true; }; - minusButton.OnClicked += (button, data) => + MinusButton.OnClicked += (button, data) => { ReduceValue(); return true; }; - minusButton.OnPressed += () => + MinusButton.OnPressed += () => { if (!IsPressedTimerRunning) { @@ -279,17 +280,17 @@ namespace Barotrauma private void HidePlusMinusButtons() { - plusButton.Parent.Visible = false; - plusButton.Parent.IgnoreLayoutGroups = true; + PlusButton.Parent.Visible = false; + PlusButton.Parent.IgnoreLayoutGroups = true; TextBox.RectTransform.RelativeSize = Vector2.One; LayoutGroup.Recalculate(); } private void ShowPlusMinusButtons() { - plusButton.Parent.Visible = true; - plusButton.Parent.IgnoreLayoutGroups = false; - TextBox.RectTransform.RelativeSize = new Vector2(1.0f - plusButton.Parent.RectTransform.RelativeSize.X, 1.0f); + PlusButton.Parent.Visible = true; + PlusButton.Parent.IgnoreLayoutGroups = false; + TextBox.RectTransform.RelativeSize = new Vector2(1.0f - PlusButton.Parent.RectTransform.RelativeSize.X, 1.0f); LayoutGroup.Recalculate(); } @@ -369,12 +370,12 @@ namespace Barotrauma if (MinValueFloat != null) { floatValue = Math.Max(floatValue, MinValueFloat.Value); - minusButton.Enabled = floatValue > MinValueFloat; + MinusButton.Enabled = floatValue > MinValueFloat; } if (MaxValueFloat != null) { floatValue = Math.Min(floatValue, MaxValueFloat.Value); - plusButton.Enabled = floatValue < MaxValueFloat; + PlusButton.Enabled = floatValue < MaxValueFloat; } } @@ -390,8 +391,8 @@ namespace Barotrauma intValue = Math.Min(intValue, MaxValueInt.Value); UpdateText(); } - plusButton.Enabled = intValue < MaxValueInt; - minusButton.Enabled = intValue > MinValueInt; + PlusButton.Enabled = intValue < MaxValueInt; + MinusButton.Enabled = intValue > MinValueInt; } private void UpdateText() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs index 0a706871a..0c373187d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs @@ -31,7 +31,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce( "GUIProgressBar.BarSize_setter", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Attempted to set the BarSize of a GUIProgressBar to an invalid value (" + value + ")\n" + Environment.StackTrace); + "Attempted to set the BarSize of a GUIProgressBar to an invalid value (" + value + ")\n" + Environment.StackTrace.CleanupStackTrace()); return; } barSize = MathHelper.Clamp(value, 0.0f, 1.0f); @@ -106,7 +106,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce( "GUIProgressBar.Draw:GetProgress", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "ProgressGetter of a GUIProgressBar (" + ProgressGetter.Target.ToString() + " - " + ProgressGetter.Method.ToString() + ") returned an invalid value (" + newSize + ")\n" + Environment.StackTrace); + "ProgressGetter of a GUIProgressBar (" + ProgressGetter.Target.ToString() + " - " + ProgressGetter.Method.ToString() + ") returned an invalid value (" + newSize + ")\n" + Environment.StackTrace.CleanupStackTrace()); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index ee12d4702..cf2af2c55 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -664,7 +664,7 @@ namespace Barotrauma case (char)0x1: // ctrl-a SelectAll(); break; - case (char)0x1A when !Readonly: // ctrl-z + case (char)0x1A when !Readonly && !SubEditorScreen.IsSubEditor(): // ctrl-z text = memento.Undo(); if (text != Text) { @@ -674,7 +674,7 @@ namespace Barotrauma OnTextChanged?.Invoke(this, Text); } break; - case (char)0x12 when !Readonly: // ctrl-r + case (char)0x12 when !Readonly && !SubEditorScreen.IsSubEditor(): // ctrl-r text = memento.Redo(); if (text != Text) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs index eb719fe7f..ba3c56caf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs @@ -348,7 +348,7 @@ namespace Barotrauma spriteBatch.Draw(currSplashScreen.GetTexture(), new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); spriteBatch.End(); - if (DateTime.Now > videoStartTime + new TimeSpan(0, 0, 0, 0, milliseconds: 500) && GameMain.WindowActive && (PlayerInput.KeyHit(Keys.Space) || PlayerInput.KeyHit(Keys.Enter) || PlayerInput.PrimaryMouseButtonDown())) + if (DateTime.Now > videoStartTime + new TimeSpan(0, 0, 0, 0, milliseconds: 500) && GameMain.WindowActive && (PlayerInput.KeyHit(Keys.Escape) || PlayerInput.KeyHit(Keys.Space) || PlayerInput.KeyHit(Keys.Enter) || PlayerInput.PrimaryMouseButtonDown())) { currSplashScreen.Dispose(); currSplashScreen = null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs index 8f7c9d2bf..7355cdbc9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs @@ -519,7 +519,7 @@ namespace Barotrauma { if (displayErrors) { - DebugConsole.ThrowError("Parent null" + Environment.StackTrace); + DebugConsole.ThrowError("Parent null" + Environment.StackTrace.CleanupStackTrace()); } return false; } @@ -527,7 +527,7 @@ namespace Barotrauma { if (displayErrors) { - DebugConsole.ThrowError("The children of the parent does not contain this child. This should not be possible! " + Environment.StackTrace); + DebugConsole.ThrowError("The children of the parent does not contain this child. This should not be possible! " + Environment.StackTrace.CleanupStackTrace()); } return false; } @@ -535,7 +535,7 @@ namespace Barotrauma { if (displayErrors) { - DebugConsole.ThrowError("Unable to remove the child from the parent. " + Environment.StackTrace); + DebugConsole.ThrowError("Unable to remove the child from the parent. " + Environment.StackTrace.CleanupStackTrace()); } return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index b82efcead..56cf45761 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -1,4 +1,5 @@ using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -31,9 +32,13 @@ namespace Barotrauma private GUITextBlock shoppingCrateTotal; private GUIButton clearAllButton, confirmButton; + private bool needsRefresh, needsBuyingRefresh, needsSellingRefresh, needsItemsToSellRefresh; + private Point resolutionWhenCreated; private bool hadPermissions; + private Dictionary OwnedItems { get; } = new Dictionary(); + private CargoManager CargoManager => campaignUI.Campaign.CargoManager; private Location CurrentLocation => campaignUI.Campaign.Map?.CurrentLocation; private int PlayerMoney => campaignUI.Campaign.Money; @@ -70,27 +75,30 @@ namespace Barotrauma campaignUI.Campaign.Map.OnLocationChanged += UpdateLocation; if (CurrentLocation?.Reputation != null) { - CurrentLocation.Reputation.OnReputationValueChanged += Refresh; + CurrentLocation.Reputation.OnReputationValueChanged += () => { needsRefresh = true; }; } - campaignUI.Campaign.CargoManager.OnItemsInBuyCrateChanged += RefreshBuying; - campaignUI.Campaign.CargoManager.OnPurchasedItemsChanged += RefreshBuying; - campaignUI.Campaign.CargoManager.OnItemsInSellCrateChanged += RefreshSelling; + campaignUI.Campaign.CargoManager.OnItemsInBuyCrateChanged += () => { needsBuyingRefresh = true; }; + campaignUI.Campaign.CargoManager.OnPurchasedItemsChanged += () => { needsBuyingRefresh = true; }; + campaignUI.Campaign.CargoManager.OnItemsInSellCrateChanged += () => { needsSellingRefresh = true; }; campaignUI.Campaign.CargoManager.OnSoldItemsChanged += () => { - RefreshItemsToSell(); - RefreshSelling(); + needsItemsToSellRefresh = true; + needsSellingRefresh = true; }; } public void Refresh() { hadPermissions = HasPermissions; - RefreshBuying(); - RefreshSelling(); + UpdateOwnedItems(); + RefreshBuying(updateOwned: false); + RefreshSelling(updateOwned: false); + needsRefresh = false; } - private void RefreshBuying() + private void RefreshBuying(bool updateOwned = true) { + if (updateOwned) { UpdateOwnedItems(); } RefreshShoppingCrateBuyList(); //RefreshStoreDealsList(); RefreshStoreBuyList(); @@ -98,15 +106,18 @@ namespace Barotrauma //storeDealsList.Enabled = hasPermissions; storeBuyList.Enabled = hasPermissions; shoppingCrateBuyList.Enabled = hasPermissions; + needsBuyingRefresh = false; } - private void RefreshSelling() + private void RefreshSelling(bool updateOwned = true) { + if (updateOwned) { UpdateOwnedItems(); } RefreshShoppingCrateSellList(); RefreshStoreSellList(); var hasPermissions = HasPermissions; storeSellList.Enabled = hasPermissions; shoppingCrateSellList.Enabled = hasPermissions; + needsSellingRefresh = false; } private void CreateUI() @@ -400,6 +411,7 @@ namespace Barotrauma SetConfirmButtonBehavior(); clearAllButton = new GUIButton(new RectTransform(new Vector2(0.35f, 1.0f), buttonContainer.RectTransform), TextManager.Get("campaignstore.clearall")) { + ClickSound = GUISoundType.DecreaseQuantity, Enabled = HasPermissions, ForceUpperCase = true, OnClicked = (button, userData) => @@ -422,7 +434,7 @@ namespace Barotrauma if (prevLocation?.Reputation != null) { - prevLocation.Reputation.OnReputationValueChanged -= Refresh; + prevLocation.Reputation.OnReputationValueChanged = null; } foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) @@ -432,7 +444,7 @@ namespace Barotrauma ChangeStoreTab(StoreTab.Buy); if (newLocation?.Reputation != null) { - newLocation.Reputation.OnReputationValueChanged += Refresh; + newLocation.Reputation.OnReputationValueChanged += () => { needsRefresh = true; }; } return; } @@ -535,6 +547,7 @@ namespace Barotrauma { (itemFrame.UserData as PurchasedItem).Quantity = quantity; SetQuantityLabelText(StoreTab.Buy, itemFrame); + SetOwnedLabelText(itemFrame); SetItemFrameStatus(itemFrame, hasPermissions && quantity > 0); } existingItemFrames.Add(itemFrame); @@ -575,6 +588,7 @@ namespace Barotrauma { (itemFrame.UserData as PurchasedItem).Quantity = quantity; SetQuantityLabelText(StoreTab.Sell, itemFrame); + SetOwnedLabelText(itemFrame); SetItemFrameStatus(itemFrame, hasPermissions); } if (quantity < 1) { itemFrame.Visible = false; } @@ -617,6 +631,7 @@ namespace Barotrauma CargoManager.ModifyItemQuantityInSellCrate(crateItem.ItemPrefab, playerItemQuantity - crateItem.Quantity); } } + needsItemsToSellRefresh = false; } private void RefreshShoppingCrateList(List items, GUIListBox listBox) @@ -645,6 +660,7 @@ namespace Barotrauma numInput.UserData = item; numInput.Enabled = hasPermissions; } + SetOwnedLabelText(itemFrame); SetItemFrameStatus(itemFrame, hasPermissions); } existingItemFrames.Add(itemFrame); @@ -740,7 +756,7 @@ namespace Barotrauma 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") + GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, (int)(GUI.yScale * 80)), parent: listBox.Content.RectTransform), style: "ListBoxElement") { ToolTip = tooltip, UserData = pi @@ -775,7 +791,7 @@ namespace Barotrauma CanBeFocused = false, Stretch = true }; - GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), nameAndQuantityGroup.RectTransform), + GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), nameAndQuantityGroup.RectTransform), pi.ItemPrefab.Name, font: GUI.SubHeadingFont, textAlignment: Alignment.BottomLeft) { CanBeFocused = false, @@ -783,11 +799,12 @@ namespace Barotrauma TextScale = 0.85f, UserData = "name" }; + GUILayoutGroup shoppingCrateAmountGroup = null; GUINumberInput amountInput = null; if (listBox == storeBuyList || listBox == storeSellList) { - var block = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), nameAndQuantityGroup.RectTransform), - CreateQuantityLabelText(listBox == storeSellList ? StoreTab.Sell : StoreTab.Buy, pi.Quantity), font: GUI.Font, textAlignment: Alignment.TopLeft) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), nameAndQuantityGroup.RectTransform), + CreateQuantityLabelText(listBox == storeSellList ? StoreTab.Sell : StoreTab.Buy, pi.Quantity), font: GUI.Font, textAlignment: Alignment.BottomLeft) { CanBeFocused = false, TextColor = Color.White * (forceDisable ? 0.5f : 1.0f), @@ -797,7 +814,13 @@ namespace Barotrauma } else if (listBox == shoppingCrateBuyList || listBox == shoppingCrateSellList) { - amountInput = new GUINumberInput(new RectTransform(new Vector2(0.5f), nameAndQuantityGroup.RectTransform), GUINumberInput.NumberType.Int) + var relativePadding = nameBlock.Padding.X / nameBlock.Rect.Width; + shoppingCrateAmountGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - relativePadding, 0.6f), nameAndQuantityGroup.RectTransform) { RelativeOffset = new Vector2(relativePadding, 0) }, + isHorizontal: true) + { + RelativeSpacing = 0.02f + }; + amountInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), shoppingCrateAmountGroup.RectTransform), GUINumberInput.NumberType.Int) { MinValueInt = 0, MaxValueInt = GetMaxAvailable(pi.ItemPrefab, listBox == shoppingCrateBuyList ? StoreTab.Buy : StoreTab.Sell), @@ -818,9 +841,25 @@ namespace Barotrauma } AddToShoppingCrate(purchasedItem, quantity: numberInput.IntValue - purchasedItem.Quantity); }; + amountInput.PlusButton.ClickSound = GUISoundType.IncreaseQuantity; + amountInput.MinusButton.ClickSound = GUISoundType.DecreaseQuantity; frame.HoverColor = frame.SelectedColor = Color.Transparent; } + // Amount in players' inventories and on the sub + var rectTransform = shoppingCrateAmountGroup == null ? + new RectTransform(new Vector2(1.0f, 0.3f), nameAndQuantityGroup.RectTransform) : + new RectTransform(new Vector2(0.6f, 1.0f), shoppingCrateAmountGroup.RectTransform); + new GUITextBlock(rectTransform, CreateOwnedLabelText(OwnedItems.GetValueOrDefault(pi.ItemPrefab, 0)), font: GUI.Font, + textAlignment: shoppingCrateAmountGroup == null ? Alignment.TopLeft : Alignment.CenterLeft) + { + CanBeFocused = false, + TextColor = Color.White * (forceDisable ? 0.5f : 1.0f), + TextScale = 0.85f, + UserData = "owned" + }; + shoppingCrateAmountGroup?.Recalculate(); + var buttonRelativeWidth = (0.9f * mainGroup.Rect.Height) / mainGroup.Rect.Width; var priceBlock = new GUITextBlock(new RectTransform(new Vector2(priceAndButtonRelativeWidth - buttonRelativeWidth, 1.0f), mainGroup.RectTransform), "", font: GUI.SubHeadingFont, textAlignment: Alignment.Right) @@ -842,6 +881,7 @@ namespace Barotrauma { new GUIButton(new RectTransform(new Vector2(buttonRelativeWidth, 0.9f), mainGroup.RectTransform), style: "StoreAddToCrateButton") { + ClickSound = GUISoundType.IncreaseQuantity, Enabled = !forceDisable && pi.Quantity > 0, ForceUpperCase = true, UserData = "addbutton", @@ -852,6 +892,7 @@ namespace Barotrauma { new GUIButton(new RectTransform(new Vector2(buttonRelativeWidth, 0.9f), mainGroup.RectTransform), style: "StoreRemoveFromCrateButton") { + ClickSound = GUISoundType.DecreaseQuantity, Enabled = !forceDisable, ForceUpperCase = true, UserData = "removebutton", @@ -869,6 +910,39 @@ namespace Barotrauma return frame; } + private void UpdateOwnedItems() + { + OwnedItems.Clear(); + + // Add items on the sub(s) + Submarine.MainSub?.GetItems(true) + .Where(i => i.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached) && + i.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) + .ForEach(i => AddToOwnedItems(i.Prefab)); + + // Add items in character inventories + foreach (Character c in GameMain.GameSession.CrewManager.GetCharacters()) + { + Item.ItemList.Where(i => i != null && i.GetRootInventoryOwner() == c) + .ForEach(i => AddToOwnedItems(i.Prefab)); + } + + // Add items already purchased + CargoManager?.PurchasedItems?.ForEach(pi => AddToOwnedItems(pi.ItemPrefab, amount: pi.Quantity)); + + void AddToOwnedItems(ItemPrefab itemPrefab, int amount = 1) + { + if (OwnedItems.ContainsKey(itemPrefab)) + { + OwnedItems[itemPrefab] += amount; + } + else + { + OwnedItems.Add(itemPrefab, amount); + } + } + } + private void SetItemFrameStatus(GUIComponent itemFrame, bool enabled) { if (itemFrame == null || !(itemFrame.UserData is PurchasedItem pi)) { return; } @@ -901,6 +975,11 @@ namespace Barotrauma numberInput.Enabled = enabled; } + if (itemFrame.FindChild("owned", recursive: true) is GUITextBlock owned) + { + owned.TextColor = color; + } + if (itemFrame.FindChild("price", recursive: true) is GUITextBlock price) { price.TextColor = color; @@ -918,7 +997,8 @@ namespace Barotrauma private void SetQuantityLabelText(StoreTab mode, GUIComponent itemFrame) { - if (itemFrame?.FindChild("quantitylabel", recursive: true) is GUITextBlock label) + if (itemFrame == null) { return; } + if (itemFrame.FindChild("quantitylabel", recursive: true) is GUITextBlock label) { label.Text = CreateQuantityLabelText(mode, (itemFrame.UserData as PurchasedItem).Quantity); } @@ -928,6 +1008,23 @@ namespace Barotrauma TextManager.GetWithVariable("campaignstore.quantity", "[amount]", quantity.ToString()) : TextManager.GetWithVariable("campaignstore.instock", "[amount]", quantity.ToString()); + private void SetOwnedLabelText(GUIComponent itemComponent) + { + if (itemComponent == null) { return; } + var itemCount = 0; + if (itemComponent.UserData is PurchasedItem pi) + { + itemCount = OwnedItems.GetValueOrDefault(pi.ItemPrefab, itemCount); + } + if (itemComponent.FindChild("owned", recursive: true) is GUITextBlock label) + { + label.Text = CreateOwnedLabelText(itemCount); + } + } + + private string CreateOwnedLabelText(int itemCount) => itemCount > 0 ? + TextManager.GetWithVariable("campaignstore.owned", "[amount]", itemCount.ToString()) : ""; + private int GetMaxAvailable(ItemPrefab itemPrefab, StoreTab mode) { var list = mode == StoreTab.Sell ? itemsToSell : CurrentLocation.StoreStock; @@ -1103,11 +1200,12 @@ namespace Barotrauma if (GameMain.GraphicsWidth != resolutionWhenCreated.X || GameMain.GraphicsHeight != resolutionWhenCreated.Y) { CreateUI(); + needsRefresh = false; } - else if (hadPermissions != HasPermissions) - { - Refresh(); - } + if (needsRefresh || hadPermissions != HasPermissions) { Refresh(); } + if (needsBuyingRefresh) { RefreshBuying(); } + if (needsItemsToSellRefresh) { RefreshItemsToSell(); } + if (needsSellingRefresh) { RefreshSelling(); } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 8262bcb86..f3d6eb5d1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -491,7 +491,7 @@ namespace Barotrauma new GUITextBlock(rectT(1, 0, textLayout), title, font: GUI.SubHeadingFont) { CanBeFocused = false, AutoScaleHorizontal = true }; new GUITextBlock(rectT(1, 0, textLayout), FormatCurrency(price)); GUILayoutGroup buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, contentLayout), childAnchor: Anchor.Center) { UserData = "buybutton" }; - new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { Enabled = Campaign.Money >= price && !isDisabled, OnClicked = onPressed }; + new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { ClickSound = GUISoundType.HireRepairClick, Enabled = Campaign.Money >= price && !isDisabled, OnClicked = onPressed }; contentLayout.Recalculate(); buyButtonLayout.Recalculate(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 97a598368..4090a12e9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -79,7 +79,7 @@ namespace Barotrauma if (gameSession == value) { return; } if (value == null && Screen.Selected == GameScreen && gameSession.GameMode is CampaignMode) { - DebugConsole.AddWarning("GameSession set to null while in the game screen\n" + Environment.StackTrace); + DebugConsole.AddWarning("GameSession set to null while in the game screen\n" + Environment.StackTrace.CleanupStackTrace()); } if (gameSession?.GameMode != null && gameSession.GameMode != value?.GameMode) { @@ -669,7 +669,7 @@ namespace Barotrauma #if DEBUG DebugConsole.ThrowError($"Failed to parse a Steam friend's connect invitation command ({connectCommand})", e); #else - DebugConsole.Log($"Failed to parse a Steam friend's connect invitation command ({connectCommand})\n" + e.StackTrace); + DebugConsole.Log($"Failed to parse a Steam friend's connect invitation command ({connectCommand})\n" + e.StackTrace.CleanupStackTrace()); #endif ConnectName = null; ConnectEndpoint = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index f2ee9ebca..bd8ac4edc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -52,11 +52,11 @@ namespace Barotrauma i.GetRootInventoryOwner() == character && !i.SpawnedInOutpost && (i.ContainedItems == null || i.ContainedItems.None() || i.ContainedItems.All(ci => soldEntities.Any(se => se.Item == ci))) && - i.Condition >= 0.9f * i.MaxCondition && soldEntities.None(se => se.Item == i)); + (i.Condition >= 0.9f * i.MaxCondition || i.Prefab.AllowSellingWhenBroken) && soldEntities.None(se => se.Item == i)); // 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 equipmentSlots = new List() { InvSlotType.Head, InvSlotType.InnerClothes, InvSlotType.OuterClothes, InvSlotType.Headset, InvSlotType.Card }; + foreach (InvSlotType slot in equipmentSlots) { var index = character.Inventory.FindLimbSlot(slot); if (character.Inventory.Items[index] is Item item) @@ -69,18 +69,27 @@ namespace Barotrauma } } - // 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) + // Prevent selling items contained inside equipped items + foreach (InvSlotType slot in equipmentSlots) { 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) + RemoveContainedFromSellables(item); + } + } + + void RemoveContainedFromSellables(Item item) + { + foreach (Item containedItem in item.ContainedItems) + { + if (containedItem == null) { continue; } + if (containedItem.ContainedItems != null) { - sellables.Remove(containedItem); + RemoveContainedFromSellables(containedItem); } + sellables.Remove(containedItem); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 81061b83b..72234cabf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -27,7 +27,7 @@ namespace Barotrauma private GUIListBox crewList; private GUIButton commandButton, toggleCrewButton; private float crewListOpenState; - private bool toggleCrewListOpen = true; + private bool _isCrewMenuOpen = true; private Point crewListEntrySize; private GUIFrame contextMenu; @@ -42,16 +42,26 @@ namespace Barotrauma public bool AllowCharacterSwitch = true; - public bool ToggleCrewListOpen + /// + /// This property stores the preference in settings. Don't use for automatic logic. + /// Use AutoShowCrewList(), AutoHideCrewList(), and ResetCrewList(). + /// + public bool IsCrewMenuOpen { - get { return toggleCrewListOpen; } + get { return _isCrewMenuOpen; } set { - if (toggleCrewListOpen == value) { return; } - toggleCrewListOpen = GameMain.Config.CrewMenuOpen = value; + if (_isCrewMenuOpen == value) { return; } + _isCrewMenuOpen = GameMain.Config.CrewMenuOpen = value; } } + public bool AutoShowCrewList() => _isCrewMenuOpen = true; + + public void AutoHideCrewList() => _isCrewMenuOpen = false; + + public void ResetCrewList() => _isCrewMenuOpen = GameMain.Config.CrewMenuOpen; + const float CommandNodeAnimDuration = 0.2f; public List OrderOptionButtons = new List(); @@ -139,7 +149,7 @@ namespace Barotrauma { OnClicked = (GUIButton btn, object userdata) => { - ToggleCrewListOpen = !ToggleCrewListOpen; + IsCrewMenuOpen = !IsCrewMenuOpen; return true; } }; @@ -251,12 +261,7 @@ namespace Barotrauma var sub = Character.Controlled.Submarine; if (sub == null || sub.TeamID != Character.Controlled.TeamID || sub.Info.IsWreck) { return false; } SetCharacterOrder(null, order, null, Character.Controlled); - var visibleHulls = new List(Character.Controlled.GetVisibleHulls()); - foreach (var hull in visibleHulls) - { - HumanAIController.PropagateHullSafety(Character.Controlled, hull); - HumanAIController.RefreshTargets(Character.Controlled, order, hull); - } + if (IsSinglePlayer) { HumanAIController.ReportProblem(Character.Controlled, order); } return true; }, UserData = order, @@ -286,7 +291,7 @@ namespace Barotrauma screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); prevUIScale = GUI.Scale; - ToggleCrewListOpen = GameMain.Config.CrewMenuOpen; + _isCrewMenuOpen = GameMain.Config.CrewMenuOpen; dismissedOrderPrefab ??= Order.GetPrefab("dismissed"); } @@ -318,7 +323,7 @@ namespace Barotrauma { if (character == null) { - DebugConsole.ThrowError("Tried to remove a null character from CrewManager.\n" + Environment.StackTrace); + DebugConsole.ThrowError("Tried to remove a null character from CrewManager.\n" + Environment.StackTrace.CleanupStackTrace()); return; } characters.Remove(character); @@ -567,7 +572,7 @@ namespace Barotrauma { if (!IsSinglePlayer) { - DebugConsole.ThrowError("Cannot add messages to single player chat box in multiplayer mode!\n" + Environment.StackTrace); + DebugConsole.ThrowError("Cannot add messages to single player chat box in multiplayer mode!\n" + Environment.StackTrace.CleanupStackTrace()); return; } if (string.IsNullOrEmpty(text)) { return; } @@ -583,7 +588,7 @@ namespace Barotrauma { if (!IsSinglePlayer) { - DebugConsole.ThrowError("Cannot add messages to single player chat box in multiplayer mode!\n" + Environment.StackTrace); + DebugConsole.ThrowError("Cannot add messages to single player chat box in multiplayer mode!\n" + Environment.StackTrace.CleanupStackTrace()); return; } if (string.IsNullOrEmpty(message.Text)) { return; } @@ -675,8 +680,7 @@ namespace Barotrauma AddOrder(new Order(order.Prefab ?? order, hull, null, orderGiver), order.FadeOutTime); if (IsSinglePlayer) { - orderGiver.Speak( - order.GetChatMessage("", hull.DisplayName, givingOrderToSelf: character == orderGiver), ChatMessageType.Order); + orderGiver.Speak(order.GetChatMessage("", hull.DisplayName, givingOrderToSelf: character == orderGiver), ChatMessageType.Order); } else { @@ -692,12 +696,11 @@ namespace Barotrauma if (IsSinglePlayer) { character.SetOrder(order, option, orderGiver, speak: orderGiver != character); - orderGiver?.Speak( - order.GetChatMessage(character.Name, orderGiver.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option), null); + orderGiver?.Speak(order.GetChatMessage(character.Name, orderGiver.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option), null); } else if (orderGiver != null) { - OrderChatMessage msg = new OrderChatMessage(order, option, order?.TargetEntity ?? order?.TargetItemComponent?.Item, character, orderGiver); + OrderChatMessage msg = new OrderChatMessage(order, option, order?.TargetSpatialEntity ?? order?.TargetItemComponent?.Item as ISpatialEntity, character, orderGiver); GameMain.Client?.SendChatMessage(msg); } } @@ -1290,7 +1293,7 @@ namespace Barotrauma { CreateCommandUI(HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition) ? Character.Controlled : GUI.MouseOn?.UserData as Character); } - GUI.PlayUISound(GUISoundType.PopupMenu); + SoundPlayer.PlayUISound(GUISoundType.PopupMenu); clicklessSelectionActive = isOpeningClick = true; } @@ -1524,14 +1527,15 @@ namespace Barotrauma new Vector2(-crewArea.Rect.Width - HUDLayoutSettings.Padding, 0.0f), Vector2.Zero, crewListOpenState).ToPoint(); - crewListOpenState = ToggleCrewListOpen ? + + crewListOpenState = IsCrewMenuOpen ? Math.Min(crewListOpenState + deltaTime * 2.0f, 1.0f) : Math.Max(crewListOpenState - deltaTime * 2.0f, 0.0f); if (GUI.KeyboardDispatcher.Subscriber == null && PlayerInput.KeyHit(InputType.CrewOrders)) { - GUI.PlayUISound(GUISoundType.PopupMenu); - ToggleCrewListOpen = !ToggleCrewListOpen; + SoundPlayer.PlayUISound(GUISoundType.PopupMenu); + IsCrewMenuOpen = !IsCrewMenuOpen; } } @@ -1589,8 +1593,8 @@ namespace Barotrauma private Hull hullContext; private bool isContextual; private readonly List contextualOrders = new List(); - private Point shortcutCenterNodeOffset; - private const int maxShortcutNodeCount = 4; + private Point shorcutCenterNodeOffset; + private const int maxShortCutNodeCount = 4; private bool WasCommandInterfaceDisabledThisUpdate { get; set; } private bool CanIssueOrders @@ -1773,7 +1777,7 @@ namespace Barotrauma returnNodeMargin = returnNodeSize.X * 0.5f; nodeDistance = (int)(150 * GUI.Scale); - shortcutCenterNodeOffset = new Point(0, (int)(1.25f * nodeDistance)); + shorcutCenterNodeOffset = new Point(0, (int)(1.25f * nodeDistance)); } private List GetAvailableCategories() @@ -2057,7 +2061,7 @@ namespace Barotrauma shortcutNodes.Clear(); - if (shortcutNodes.Count < maxShortcutNodeCount && 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 @@ -2075,7 +2079,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 < maxShortcutNodeCount && (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) { @@ -2085,7 +2089,7 @@ namespace Barotrauma // If player is not a security officer AND invaders are reported // --> Create shorcut node for Fight Intruders order - if (shortcutNodes.Count < maxShortcutNodeCount && (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( @@ -2094,7 +2098,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 < maxShortcutNodeCount && (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( @@ -2103,7 +2107,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 < maxShortcutNodeCount && (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( @@ -2112,13 +2116,13 @@ namespace Barotrauma // If fire is reported // --> Create shortcut node for Extinguish Fires order - if (shortcutNodes.Count < maxShortcutNodeCount && 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 < maxShortcutNodeCount && 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) { @@ -2131,11 +2135,17 @@ namespace Barotrauma { shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, orderPrefab, -1)); } - if (shortcutNodes.Count >= maxShortcutNodeCount) { break; } + if (shortcutNodes.Count >= maxShortCutNodeCount) { break; } } } } + if (shortcutNodes.Count < maxShortCutNodeCount && characterContext != null && !characterContext.IsDismissed) + { + shortcutNodes.Add( + CreateOrderNode(shortcutNodeSize, null, Point.Zero, dismissedOrderPrefab, -1)); + } + if (shortcutNodes.Count < 1) { return; } shortcutCenterNode = new GUIButton( @@ -2148,7 +2158,7 @@ namespace Barotrauma c.PressedColor = c.Color; c.SelectedColor = c.Color; } - shortcutCenterNode.RectTransform.MoveOverTime(shortcutCenterNodeOffset, CommandNodeAnimDuration); + shortcutCenterNode.RectTransform.MoveOverTime(shorcutCenterNodeOffset, CommandNodeAnimDuration); var nodeCountForCalculations = shortcutNodes.Count * 2 + 2; var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, 0.75f * nodeDistance, nodeCountForCalculations); @@ -2156,7 +2166,7 @@ namespace Barotrauma for (int i = 0; i < shortcutNodes.Count; i++) { shortcutNodes[i].RectTransform.Parent = commandFrame.RectTransform; - shortcutNodes[i].RectTransform.MoveOverTime(shortcutCenterNodeOffset + offsets[firstOffsetIndex - i].ToPoint(), CommandNodeAnimDuration); + shortcutNodes[i].RectTransform.MoveOverTime(shorcutCenterNodeOffset + offsets[firstOffsetIndex - i].ToPoint(), CommandNodeAnimDuration); } } @@ -2171,7 +2181,7 @@ namespace Barotrauma { order = orders[i]; disableNode = !CanSomeoneHearCharacter() || - (order.MustSetTarget && (order.ItemComponentType != null || order.ItemIdentifiers.Length > 0) && order.GetMatchingItems(true).None()); + (order.MustSetTarget && (order.ItemComponentType != null || order.TargetItems.Length > 0) && order.GetMatchingItems(true).None()); optionNodes.Add(new Tuple( CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), order, (i + 1) % 10, disableNode: disableNode, checkIfOrderCanBeHeard: false), !disableNode ? Keys.D0 + (i + 1) % 10 : Keys.None)); @@ -2190,13 +2200,14 @@ namespace Barotrauma // Check if targeting an item or a hull if (itemContext != null && !itemContext.NonInteractable) { + ItemComponent targetComponent; foreach (Order p in Order.PrefabList) { - if ((p.ItemIdentifiers.Length > 0 && (p.ItemIdentifiers.Contains(itemContext.Prefab.Identifier) || itemContext.HasTag(p.ItemIdentifiers))) || - (p.ItemComponentType != null && itemContext.Components.Any(c => c?.GetType() == p.ItemComponentType))) + targetComponent = null; + if ((p.TargetItems.Length > 0 && (p.TargetItems.Contains(itemContext.Prefab.Identifier) || itemContext.HasTag(p.TargetItems))) || + p.TryGetTargetItemComponent(itemContext, out targetComponent)) { - contextualOrders.Add(p.HasOptions ? p : - new Order(p, itemContext, itemContext.Components.FirstOrDefault(c => c?.GetType() == p.ItemComponentType), Character.Controlled)); + contextualOrders.Add(p.HasOptions ? p : new Order(p, itemContext, targetComponent, Character.Controlled)); } } @@ -2205,8 +2216,8 @@ namespace Barotrauma var operateWeaponsPrefab = Order.GetPrefab(orderIdentifier); if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && itemContext.Components.Any(c => c is Controller)) { - var turret = itemContext.GetConnectedComponents().FirstOrDefault(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier)) ?? - itemContext.GetConnectedComponents(recursive: true).FirstOrDefault(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier)); + var turret = itemContext.GetConnectedComponents().FirstOrDefault(c => c.Item.HasTag(operateWeaponsPrefab.TargetItems)) ?? + itemContext.GetConnectedComponents(recursive: true).FirstOrDefault(c => c.Item.HasTag(operateWeaponsPrefab.TargetItems)); if (turret != null) { contextualOrders.Add(new Order(operateWeaponsPrefab, turret.Item, turret, Character.Controlled)); } } @@ -2214,14 +2225,17 @@ namespace Barotrauma orderIdentifier = "repairsystems"; 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")))) { - contextualOrders.Add(new Order(Order.GetPrefab("repairelectrical"), itemContext, null, Character.Controlled)); + contextualOrders.Add(new Order(Order.GetPrefab("repairelectrical"), itemContext, targetItem: null, Character.Controlled)); } else if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical")))) { - contextualOrders.Add(new Order(Order.GetPrefab("repairmechanical"), itemContext, null, Character.Controlled)); + contextualOrders.Add(new Order(Order.GetPrefab("repairmechanical"), itemContext, targetItem: null, Character.Controlled)); + } + else + { + contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), itemContext, targetItem: null, Character.Controlled)); } } @@ -2235,23 +2249,29 @@ namespace Barotrauma 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)) + orderIdentifier = "cleanupitems"; + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && AIObjectiveCleanupItems.IsValidTarget(itemContext, Character.Controlled)) { - contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), itemContext, null, Character.Controlled)); + contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), itemContext, targetItem: null, Character.Controlled)); } } } - else if(hullContext != null) + else if (hullContext != null) { - contextualOrders.Add(new Order(Order.GetPrefab("fixleaks"), hullContext, null, Character.Controlled)); + contextualOrders.Add(new Order(Order.GetPrefab("fixleaks"), hullContext, targetItem: null, Character.Controlled)); } - if (characters.Any(c => c != Character.Controlled)) + if (contextualOrders.None(o => o.Category != OrderCategory.Movement)) { - // Show 'follow' order only when there are no orders of other categories available - if (contextualOrders.None(o => o.Category != OrderCategory.Movement)) + orderIdentifier = "wait"; + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier))) + { + Vector2 position = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); + Hull hull = Hull.FindHull(position, guess: Character.Controlled?.CurrentHull); + contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), new OrderTarget(position, hull), Character.Controlled)); + } + + if (characters.Any(c => c != Character.Controlled)) { orderIdentifier = "follow"; if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier))) @@ -2259,14 +2279,14 @@ namespace Barotrauma 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)) && - characters.Any(c => c.CurrentOrder != null && !c.CurrentOrder.Identifier.Equals(orderIdentifier))) - { - contextualOrders.Add(Order.GetPrefab(orderIdentifier)); - } + } + + // Show 'dismiss' order only when there are crew members with active orders + orderIdentifier = "dismissed"; + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && + characters.Any(c => c.CurrentOrder != null && !c.CurrentOrder.Identifier.Equals(orderIdentifier))) + { + contextualOrders.Add(Order.GetPrefab(orderIdentifier)); } } @@ -2280,17 +2300,19 @@ namespace Barotrauma } } + // TODO: there's duplicate logic here and above -> would be better to refactor so that the conditions are only defined in one place public static bool DoesItemHaveContextualOrders(Item item) { - if (Order.PrefabList.Any(o => o.ItemIdentifiers.Length > 0 && o.ItemIdentifiers.Contains(item.Prefab.Identifier))) { return true; } - 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 (Order.PrefabList.Any(o => o.TargetItems.Length > 0 && o.TargetItems.Contains(item.Prefab.Identifier))) { return true; } + if (Order.PrefabList.Any(o => item.HasTag(o.TargetItems))) { return true; } + if (Order.PrefabList.Any(o => o.TryGetTargetItemComponent(item, out _))) { return true; } + if (AIObjectiveCleanupItems.IsValidTarget(item, Character.Controlled)) { 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)) || - item.GetConnectedComponents(recursive: true).Any(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier))); + (item.GetConnectedComponents().Any(c => c.Item.HasTag(operateWeaponsPrefab.TargetItems)) || + item.GetConnectedComponents(recursive: true).Any(c => c.Item.HasTag(operateWeaponsPrefab.TargetItems))); } private GUIButton CreateOrderNode(Point size, RectTransform parent, Point offset, Order order, int hotkey, bool disableNode = false, bool checkIfOrderCanBeHeard = true) @@ -2462,7 +2484,7 @@ namespace Barotrauma style: "GUITextBox") { UserData = new Tuple( - item == null ? order : new Order(order, item, item.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType)), + item == null ? order : new Order(order, item, order.GetTargetItemComponent(item)), order.Options[i]), Font = GUI.SmallFont, OnClicked = (_, userData) => @@ -2479,7 +2501,7 @@ namespace Barotrauma } else { - var userData = new Tuple(item == null ? order : new Order(order, item, item.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType)), ""); + var userData = new Tuple(item == null ? order : new Order(order, item, order.GetTargetItemComponent(item)), ""); optionElement = new GUIButton( new RectTransform( new Point((int)(50 * GUI.Scale)), @@ -2528,7 +2550,7 @@ namespace Barotrauma var item = itemContext != null ? (order.UseController ? itemContext.GetConnectedComponents().FirstOrDefault()?.Item ?? itemContext.GetConnectedComponents(recursive: true).FirstOrDefault()?.Item : itemContext) : (matchingItems.Count > 0 ? matchingItems[0] : null); - var o = item == null || !order.IsPrefab ? order : new Order(order, item, item.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType)); + var o = item == null || !order.IsPrefab ? order : new Order(order, item, order.GetTargetItemComponent(item)); var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, GetCircumferencePointCount(order.Options.Length), GetFirstNodeAngle(order.Options.Length)); @@ -2754,7 +2776,7 @@ namespace Barotrauma // Order icon GUIImage orderIcon; - if (character.CurrentOrder != null && !character.CurrentOrder.Identifier.Equals("dismissed")) + if (!character.IsDismissed) { orderIcon = new GUIImage(new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), character.CurrentOrder.SymbolSprite, scaleToFit: true); var tooltip = character.CurrentOrder.Name; @@ -2910,7 +2932,7 @@ namespace Barotrauma { bearing = GetBearing( centerNode.RectTransform.AnimTargetPos.ToVector2(), - shortcutCenterNodeOffset.ToVector2()); + shorcutCenterNodeOffset.ToVector2()); } return nodeCount % 2 > 0 ? MathHelper.ToRadians(bearing + 360.0f / nodeCount / 2) : @@ -2997,8 +3019,7 @@ namespace Barotrauma #endif if (order.Identifier == dismissedOrderPrefab.Identifier) { - return characters.FindAll(c => c.CurrentOrder != null && c.CurrentOrder.Identifier != dismissedOrderPrefab.Identifier) - .OrderBy(c => c.Info.DisplayName).ToList(); + return characters.FindAll(c => !c.IsDismissed).OrderBy(c => c.Info.DisplayName).ToList(); } return GetCharactersSortedForOrder(order, order.Identifier != "follow").ToList(); } @@ -3006,16 +3027,16 @@ namespace Barotrauma private IEnumerable GetCharactersSortedForOrder(Order order, bool includeSelf) { return characters.FindAll(c => Character.Controlled == null || ((includeSelf || c != Character.Controlled) && c.TeamID == Character.Controlled.TeamID)) - // 1. Prioritize those who are already ordered to operate the item target of the new 'operate' order - .OrderByDescending(c => c.CurrentOrder != null && order.Category == OrderCategory.Operate && c.CurrentOrder.Identifier == order.Identifier && c.CurrentOrder.TargetEntity == order.TargetEntity) - // 2. Prioritize those who are currently dismissed - .ThenByDescending(c => c.CurrentOrder == null || c.CurrentOrder.Identifier == dismissedOrderPrefab.Identifier) - // 3. Prioritize those who are not currently assigned with the same type of order (for example, when giving a 'Fix Leak' order, prioritize those who have a different order) - .ThenBy(c => c.CurrentOrder != null && c.CurrentOrder.Identifier == order.Identifier && c.CurrentOrder.TargetEntity == order.TargetEntity) - // 4. Prioritize those with the appropriate job for the order + // 1. Prioritize those who are on the same submarine than the controlled character + .OrderByDescending(c => Character.Controlled == null || c.Submarine == Character.Controlled.Submarine) + // 2. Prioritize those who are already ordered to operate the item target of the new 'operate' order, or given the same maintenance order as now issued + .ThenByDescending(c => c.CurrentOrder != null && c.CurrentOrder.Identifier == order.Identifier && (order.Category == OrderCategory.Maintenance || (order.Category == OrderCategory.Operate && c.CurrentOrder.TargetSpatialEntity == order.TargetSpatialEntity))) + // 3. Prioritize those with the appropriate job for the order .ThenByDescending(c => order.HasAppropriateJob(c)) - // 5. Prioritize those with the lowest "weight" of the current order - .ThenBy(c => c.CurrentOrder?.Weight) + // 4. Prioritize bots over player controlled characters + .ThenByDescending(c => c.IsBot) + // 5. Use the priority value of the current objective + .ThenBy(c => c.AIController?.ObjectiveManager.CurrentObjective?.Priority) // 6. Prioritize those with the best skill for the order .ThenByDescending(c => c.GetSkillLevel(order.AppropriateSkill)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 0df8733dc..e9f57df0c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -98,6 +98,9 @@ namespace Barotrauma case "pendingupgrades": UpgradeManager = new UpgradeManager(this, subElement, isSingleplayer: true); break; + case "pets": + petsElement = subElement; + break; } } @@ -213,6 +216,10 @@ namespace Barotrauma crewDead = false; endTimer = 5.0f; CrewManager.InitSinglePlayerRound(); + if (petsElement != null) + { + PetBehavior.LoadPets(petsElement); + } } protected override void LoadInitialLevel() @@ -413,8 +420,6 @@ namespace Barotrauma //-------------------------------------- - bool save = false; - if (success) { if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub)) @@ -707,6 +712,10 @@ namespace Barotrauma } } + petsElement = new XElement("pets"); + PetBehavior.SavePets(petsElement); + modeElement.Add(petsElement); + CrewManager.Save(modeElement); CampaignMetadata.Save(modeElement); Map.Save(modeElement); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs index 81cdd715a..98cc31663 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -161,7 +161,7 @@ namespace Barotrauma.Tutorials do { yield return null; } while (!captain_medicObjectiveSensor.MotionDetected); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(captain_medic.Info.DisplayName, TextManager.Get("Captain.Radio.Medic"), ChatMessageType.Radio, null); yield return new WaitForSeconds(2f, false); - GameMain.GameSession.CrewManager.ToggleCrewListOpen = true; + GameMain.GameSession.CrewManager.AutoShowCrewList(); GameMain.GameSession.CrewManager.AddCharacter(captain_medic); TriggerTutorialSegment(0, GameMain.Config.KeyBindText(InputType.Command)); do @@ -186,8 +186,7 @@ namespace Barotrauma.Tutorials // TODO: Rework order highlighting for new command UI // GameMain.GameSession.CrewManager.HighlightOrderButton(captain_mechanic, "repairsystems", highlightColor, new Vector2(5, 5)); //HighlightOrderOption("jobspecific"); - } - while (!HasOrder(captain_mechanic, "repairsystems")); + } while (!HasOrder(captain_mechanic, "repairsystems") && !HasOrder(captain_mechanic, "repairmechanical") && !HasOrder(captain_mechanic, "repairelectrical")); RemoveCompletedObjective(segments[1]); yield return new WaitForSeconds(2f, false); TriggerTutorialSegment(2, GameMain.Config.KeyBindText(InputType.Command)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs index 8932c49f5..01951839c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs @@ -275,7 +275,7 @@ namespace Barotrauma.Tutorials GameMain.GameSession.CrewManager.AllowCharacterSwitch = false; GameMain.GameSession.CrewManager.AddCharacter(doctor); GameMain.GameSession.CrewManager.AddCharacter(patient1); - GameMain.GameSession.CrewManager.ToggleCrewListOpen = true; + GameMain.GameSession.CrewManager.AutoShowCrewList(); patient1.CharacterHealth.UseHealthWindow = false; yield return new WaitForSeconds(3.0f, false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs index 94aea1fb7..09c22b475 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs @@ -87,9 +87,9 @@ namespace Barotrauma.Tutorials radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); engineer = Character.Controlled; - var toolbox = FindOrGiveItem(engineer, "toolbox"); - toolbox.Unequip(engineer); - engineer.Inventory.RemoveItem(toolbox); + var toolbelt = FindOrGiveItem(engineer, "toolbelt"); + toolbelt.Unequip(engineer); + engineer.Inventory.RemoveItem(toolbelt); var repairOrder = Order.GetPrefab("repairsystems"); engineer_repairIcon = repairOrder.SymbolSprite; @@ -123,7 +123,7 @@ namespace Barotrauma.Tutorials // Room 3 engineer_reactorObjectiveSensor = Item.ItemList.Find(i => i.HasTag("engineer_reactorobjectivesensor")).GetComponent(); - tutorial_oxygenGenerator = Item.ItemList.Find(i => i.HasTag("tutorial_oxygengenerator")).GetComponent(); + tutorial_oxygenGenerator = Item.ItemList.Find(i => i.HasTag("tutorial_oxygengenerator")).GetComponent(); engineer_reactor = Item.ItemList.Find(i => i.HasTag("engineer_reactor")).GetComponent(); engineer_reactor.FireDelay = engineer_reactor.MeltdownDelay = float.PositiveInfinity; engineer_reactor.FuelConsumptionRate = 0.0f; @@ -311,6 +311,8 @@ namespace Barotrauma.Tutorials } yield return null; } while (engineer_reactor.AvailableFuel == 0); + RemoveCompletedObjective(segments[1]); + TriggerTutorialSegment(2); CoroutineManager.StartCoroutine(ReactorOperatedProperly()); do { @@ -352,7 +354,7 @@ namespace Barotrauma.Tutorials } while (wait > 0.0f); engineer.SelectedConstruction = null; engineer_reactor.CanBeSelected = false; - RemoveCompletedObjective(segments[1]); + RemoveCompletedObjective(segments[2]); SetHighlight(engineer_reactor.Item, false); SetHighlight(engineer_brokenJunctionBox, true); SetDoorAccess(engineer_secondDoor, engineer_secondDoorLight, true); @@ -361,7 +363,7 @@ namespace Barotrauma.Tutorials do { yield return null; } while (!engineer_secondDoor.IsOpen); yield return new WaitForSeconds(1f, false); Repairable repairableJunctionBoxComponent = engineer_brokenJunctionBox.GetComponent(); - TriggerTutorialSegment(2, GameMain.Config.KeyBindText(InputType.Select)); // Repair the junction box + TriggerTutorialSegment(3, GameMain.Config.KeyBindText(InputType.Select)); // Repair the junction box do { if (!engineer.HasEquippedItem("screwdriver")) @@ -378,7 +380,7 @@ namespace Barotrauma.Tutorials yield return null; } while (engineer_brokenJunctionBox.Condition < repairableJunctionBoxComponent.RepairThreshold); // Wait until repaired SetHighlight(engineer_brokenJunctionBox, false); - RemoveCompletedObjective(segments[2]); + RemoveCompletedObjective(segments[3]); SetDoorAccess(engineer_thirdDoor, engineer_thirdDoorLight, true); for (int i = 0; i < engineer_disconnectedJunctionBoxes.Length; i++) { @@ -389,14 +391,14 @@ namespace Barotrauma.Tutorials do { yield return null; } while (!engineer_thirdDoor.IsOpen); GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Engineer.Radio.FaultyWiring"), ChatMessageType.Radio, null); yield return new WaitForSeconds(2f, false); - TriggerTutorialSegment(3, GameMain.Config.KeyBindText(InputType.Use), GameMain.Config.KeyBindText(InputType.Deselect)); // Connect the junction boxes + TriggerTutorialSegment(4, GameMain.Config.KeyBindText(InputType.Use), GameMain.Config.KeyBindText(InputType.Deselect)); // Connect the junction boxes do { CheckGhostWires(); HandleJunctionBoxWiringHighlights(); yield return null; } while (engineer_workingPump.Voltage < engineer_workingPump.MinVoltage); // Wait until connected all the way to the pump CheckGhostWires(); for (int i = 0; i < engineer_disconnectedJunctionBoxes.Length; i++) { SetHighlight(engineer_disconnectedJunctionBoxes[i].Item, false); } - RemoveCompletedObjective(segments[3]); + RemoveCompletedObjective(segments[4]); do { yield return null; } while (engineer_workingPump.Item.CurrentHull.WaterPercentage > waterVolumeBeforeOpening); // Wait until drained wiringActive = false; SetDoorAccess(engineer_fourthDoor, engineer_fourthDoorLight, true); @@ -406,7 +408,7 @@ namespace Barotrauma.Tutorials do { yield return null; } while (!tutorial_enteredSubmarineSensor.MotionDetected); GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Engineer.Radio.Submarine"), ChatMessageType.Radio, null); yield return new WaitForSeconds(2f, false); - TriggerTutorialSegment(4); // Repair junction box + TriggerTutorialSegment(5); // Repair junction box while (ContentRunning) yield return null; engineer.AddActiveObjectiveEntity(engineer_submarineJunctionBox_1, engineer_repairIcon, engineer_repairIconColor); engineer.AddActiveObjectiveEntity(engineer_submarineJunctionBox_2, engineer_repairIcon, engineer_repairIconColor); @@ -422,16 +424,16 @@ namespace Barotrauma.Tutorials // Remove highlights when each individual machine is repaired do { CheckJunctionBoxHighlights(repairableJunctionBoxComponent1, repairableJunctionBoxComponent2, repairableJunctionBoxComponent3); yield return null; } while (engineer_submarineJunctionBox_1.Condition < repairableJunctionBoxComponent1.RepairThreshold || engineer_submarineJunctionBox_2.Condition < repairableJunctionBoxComponent2.RepairThreshold || engineer_submarineJunctionBox_3.Condition < repairableJunctionBoxComponent3.RepairThreshold); CheckJunctionBoxHighlights(repairableJunctionBoxComponent1, repairableJunctionBoxComponent2, repairableJunctionBoxComponent3); - RemoveCompletedObjective(segments[4]); + RemoveCompletedObjective(segments[5]); yield return new WaitForSeconds(2f, false); - TriggerTutorialSegment(5); // Powerup reactor + TriggerTutorialSegment(6); // Powerup reactor SetHighlight(engineer_submarineReactor.Item, true); engineer.AddActiveObjectiveEntity(engineer_submarineReactor.Item, engineer_reactorIcon, engineer_reactorIconColor); do { yield return null; } while (!IsReactorPoweredUp(engineer_submarineReactor)); // Wait until ~matches load engineer.RemoveActiveObjectiveEntity(engineer_submarineReactor.Item); SetHighlight(engineer_submarineReactor.Item, false); - RemoveCompletedObjective(segments[5]); + RemoveCompletedObjective(segments[6]); GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Engineer.Radio.Complete"), ChatMessageType.Radio, null); yield return new WaitForSeconds(4f, false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs index 2720382d2..ea817521c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs @@ -87,9 +87,9 @@ namespace Barotrauma.Tutorials radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); mechanic = Character.Controlled; - var toolbox = FindOrGiveItem(mechanic, "toolbox"); - toolbox.Unequip(mechanic); - mechanic.Inventory.RemoveItem(toolbox); + var toolbelt = FindOrGiveItem(mechanic, "toolbelt"); + toolbelt.Unequip(mechanic); + mechanic.Inventory.RemoveItem(toolbelt); var crowbar = FindOrGiveItem(mechanic, "crowbar"); crowbar.Unequip(mechanic); @@ -141,7 +141,7 @@ namespace Barotrauma.Tutorials mechanic_brokenWall_1.SpriteColor = Color.White; for (int i = 0; i < mechanic_brokenWall_1.SectionCount; i++) { - mechanic_brokenWall_1.AddDamage(i, 165); + mechanic_brokenWall_1.AddDamage(i, 85); } mechanic_brokenhull_1 = mechanic_brokenWall_1.Sections[0].gap.FlowTargetHull; @@ -199,7 +199,7 @@ namespace Barotrauma.Tutorials mechanic_brokenWall_2.SpriteColor = Color.White; for (int i = 0; i < mechanic_brokenWall_2.SectionCount; i++) { - mechanic_brokenWall_2.AddDamage(i, 165); + mechanic_brokenWall_2.AddDamage(i, 85); } mechanic_brokenhull_2 = mechanic_brokenWall_2.Sections[0].gap.FlowTargetHull; SetDoorAccess(tutorial_submarineDoor, tutorial_submarineDoorLight, false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs index 67077870c..caaa10e45 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs @@ -362,10 +362,10 @@ namespace Barotrauma.Tutorials SetHighlight(officer_rangedWeaponHolder.Item, false); do { - HighlightInventorySlot(officer.Inventory, "harpoongun", highlightColor, 0.5f, 0.5f, 0f); + HighlightInventorySlot(officer.Inventory, "shotgun", highlightColor, 0.5f, 0.5f, 0f); yield return null; - } while (!officer.HasEquippedItem("harpoongun")); // Wait until equipped - ItemContainer harpoonGunChamber = officer.Inventory.FindItemByIdentifier("harpoongun").GetComponent(); + } while (!officer.HasEquippedItem("shotgun")); // Wait until equipped + ItemContainer shotGunChamber = officer.Inventory.FindItemByIdentifier("shotgun").GetComponent(); SetHighlight(officer_rangedWeaponCabinet.Item, true); do { @@ -376,7 +376,7 @@ namespace Barotrauma.Tutorials for (int i = 0; i < officer_rangedWeaponCabinet.Inventory.Items.Length; i++) { if (officer_rangedWeaponCabinet.Inventory.Items[i] == null) continue; - if (officer_rangedWeaponCabinet.Inventory.Items[i].Prefab.Identifier == "spear") + if (officer_rangedWeaponCabinet.Inventory.Items[i].Prefab.Identifier == "shotgunshell") { HighlightInventorySlot(officer_rangedWeaponCabinet.Inventory, i, highlightColor, 0.5f, 0.5f, 0f); } @@ -387,18 +387,18 @@ namespace Barotrauma.Tutorials for (int i = 0; i < officer.Inventory.Items.Length; i++) { if (officer.Inventory.Items[i] == null) continue; - if (officer.Inventory.Items[i].Prefab.Identifier == "spear") + if (officer.Inventory.Items[i].Prefab.Identifier == "shotgunshell") { HighlightInventorySlot(officer.Inventory, i, highlightColor, 0.5f, 0.5f, 0f); } } - if (officer.Inventory.FindItemByIdentifier("spear") != null || (IsSelectedItem(officer_rangedWeaponCabinet.Item) && officer_rangedWeaponCabinet.Inventory.FindItemByIdentifier("spear") != null)) + if (officer.Inventory.FindItemByIdentifier("shotgunshell") != null || (IsSelectedItem(officer_rangedWeaponCabinet.Item) && officer_rangedWeaponCabinet.Inventory.FindItemByIdentifier("shotgunshell") != null)) { - HighlightInventorySlot(officer.Inventory, "harpoongun", highlightColor, 0.5f, 0.5f, 0f); + HighlightInventorySlot(officer.Inventory, "shotgun", highlightColor, 0.5f, 0.5f, 0f); } yield return null; - } while (!harpoonGunChamber.Inventory.IsFull()); // Wait until all six harpoons loaded + } while (!shotGunChamber.Inventory.IsFull()); // Wait until all six harpoons loaded RemoveCompletedObjective(segments[5]); SetHighlight(officer_rangedWeaponCabinet.Item, false); SetDoorAccess(officer_fourthDoor, officer_fourthDoorLight, true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index a598561a3..1af887277 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -584,8 +584,8 @@ namespace Barotrauma.Tutorials } infoBlock.RectTransform.NonScaledSize = new Point(infoBlock.Rect.Width, (int)(infoContent.Children.Sum(c => c.Rect.Height + infoContent.AbsoluteSpacing) / infoContent.RectTransform.RelativeSize.Y)); - - GUI.PlayUISound(GUISoundType.UIMessage); + + SoundPlayer.PlayUISound(GUISoundType.UIMessage); return background; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index 4e7cf35f6..e3ddc5cf1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -1149,7 +1149,7 @@ namespace Barotrauma catch (Exception e) { DebugConsole.ThrowError("Failed to set voice capture mode.", e); - GameAnalyticsManager.AddErrorEventOnce("SetVoiceCaptureMode", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, "Failed to set voice capture mode. " + e.Message + "\n" + e.StackTrace); + GameAnalyticsManager.AddErrorEventOnce("SetVoiceCaptureMode", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, "Failed to set voice capture mode. " + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); VoiceSetting = VoiceMode.Disabled; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index c54105e9e..850a0a7c1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -51,12 +51,13 @@ namespace Barotrauma limbSlotIcons.Add(InvSlotType.LeftHand, new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(634, 0, 128, 128))); limbSlotIcons.Add(InvSlotType.RightHand, new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(762, 0, 128, 128))); limbSlotIcons.Add(InvSlotType.OuterClothes, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(256 + margin, 128 + margin, 128 - margin * 2, 128 - margin * 2))); + limbSlotIcons.Add(InvSlotType.Bag, new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(639, 926, 128,80))); } return limbSlotIcons; } } - public const InvSlotType PersonalSlots = InvSlotType.Card | InvSlotType.Headset | InvSlotType.InnerClothes | InvSlotType.OuterClothes | InvSlotType.Head; + public const InvSlotType PersonalSlots = InvSlotType.Card | InvSlotType.Bag | InvSlotType.Headset | InvSlotType.InnerClothes | InvSlotType.OuterClothes | InvSlotType.Head; private Point screenResolution; @@ -143,17 +144,13 @@ namespace Barotrauma protected override ItemInventory GetActiveEquippedSubInventory(int slotIndex) { var item = Items[slotIndex]; - if (item == null) return null; + if (item == null) { return null; } var container = item.GetComponent(); - if (container == null || - !character.CanAccessInventory(container.Inventory) || - !container.KeepOpenWhenEquipped || - !character.HasEquippedItem(container.Item)) + if (container == null || !container.KeepOpenWhenEquippedBy(character)) { return null; } - return container.Inventory; } @@ -625,7 +622,7 @@ namespace Barotrauma { var itemContainer = item.GetComponent(); if (itemContainer != null && - itemContainer.KeepOpenWhenEquipped && + itemContainer.KeepOpenWhenEquippedBy(character) && character.CanAccessInventory(itemContainer.Inventory) && !highlightedSubInventorySlots.Any(s => s.Inventory == itemContainer.Inventory)) { @@ -729,6 +726,7 @@ namespace Barotrauma { for (int i = 0; i < indicators.Length; i++) { + if (indicatorIndexes[i] < 0) { continue; } Item item = Items[indicatorIndexes[i]]; if (item != null) { @@ -922,12 +920,14 @@ namespace Barotrauma if (slotItem == item) { slot.ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.4f); - GUI.PlayUISound(GUISoundType.PickItem); + SoundPlayer.PlayUISound(GUISoundType.PickItem); break; } } } + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List { item }, true)); + item.Remove(); return; } @@ -1065,7 +1065,7 @@ namespace Barotrauma } draggingItem = null; - GUI.PlayUISound(success ? GUISoundType.PickItem : GUISoundType.PickItemFail); + SoundPlayer.PlayUISound(success ? GUISoundType.PickItem : GUISoundType.PickItemFail); } public void DrawOwn(SpriteBatch spriteBatch) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index c10bbb748..6443ffe97 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -261,7 +261,23 @@ namespace Barotrauma.Items.Components if (!isNetworkMessage || open != PredictedState) { StopPicking(null); - PlaySound(forcedOpen ? ActionType.OnPicked : ActionType.OnUse); + ActionType actionType = ActionType.OnUse; + if (forcedOpen) + { + actionType = ActionType.OnPicked; + } + else + { + if (open && HasSoundsOfType[(int)ActionType.OnOpen]) + { + actionType = ActionType.OnOpen; + } + else if (!open && HasSoundsOfType[(int)ActionType.OnClose]) + { + actionType = ActionType.OnClose; + } + } + PlaySound(actionType); if (isOpen) { stuck = MathHelper.Clamp(stuck - StuckReductionOnOpen, 0.0f, 100.0f); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs index 620434040..a1821a13f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs @@ -263,7 +263,7 @@ namespace Barotrauma.Items.Components { for (int i = 0; i < targetSections.Count; i++) { - targetHull.SetSectionColorOrStrength(targetSections[i], color, sizeAdjustedSprayStrength * deltaTime, true, false); + targetHull.IncreaseSectionColorOrStrength(targetSections[i], color, sizeAdjustedSprayStrength * deltaTime, true, false); } } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 516e6dfc4..8dd46bcd2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -52,6 +52,8 @@ namespace Barotrauma.Items.Components get { return sounds.Count > 0; } } + public bool[] HasSoundsOfType { get { return hasSoundsOfType; } } + private readonly bool[] hasSoundsOfType; private readonly Dictionary> sounds; private Dictionary soundSelectionModes; @@ -191,6 +193,12 @@ namespace Barotrauma.Items.Components private ItemSound loopingSound; private SoundChannel loopingSoundChannel; private List playingOneshotSoundChannels = new List(); + public ItemComponent ReplacedBy; + + public ItemComponent GetReplacementOrThis() + { + return ReplacedBy?.GetReplacementOrThis() ?? this; + } public void UpdateSounds() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index f961eba32..205a72010 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -70,6 +70,7 @@ namespace Barotrauma.Items.Components [Serialize(false, false, description: "Should the inventory of this item be kept open when the item is equipped by a character.")] public bool KeepOpenWhenEquipped { get; set; } + [Serialize(false, false, description: "Can the inventory of this item be moved around on the screen by the player.")] public bool MovableFrame { get; set; } @@ -162,6 +163,30 @@ namespace Barotrauma.Items.Components } } + public bool KeepOpenWhenEquippedBy(Character character) + { + if (!character.CanAccessInventory(Inventory) || + !KeepOpenWhenEquipped || + !character.HasEquippedItem(Item)) + { + return false; + } + + //if holding 2 different "always open" items in different hands, don't force them to stay open + if (character.SelectedItems[0] != null && + character.SelectedItems[1] != null && + character.SelectedItems[0] != character.SelectedItems[1]) + { + if ((character.SelectedItems[0].GetComponent()?.KeepOpenWhenEquipped ?? false) && + (character.SelectedItems[1].GetComponent()?.KeepOpenWhenEquipped ?? false)) + { + return false; + } + } + + return true; + } + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { if (hideItems || (item.body != null && !item.body.Enabled)) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs index 53ab9ddcf..994deb666 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs @@ -26,29 +26,17 @@ namespace Barotrauma.Items.Components if (isHUDsHidden == value) { return; } if (value == true) { - ToggleCrewArea(false, storeOriginalState: true); + GameMain.GameSession?.CrewManager?.AutoHideCrewList(); ToggleChatBox(false, storeOriginalState: true); } else { - ToggleCrewArea(crewAreaOriginalState, storeOriginalState: false); + GameMain.GameSession?.CrewManager?.ResetCrewList(); ToggleChatBox(chatBoxOriginalState, storeOriginalState: false); } isHUDsHidden = value; } - private void ToggleCrewArea(bool value, bool storeOriginalState) - { - var crewManager = GameMain.GameSession?.CrewManager; - if (crewManager == null) { return; } - - if (storeOriginalState) - { - crewAreaOriginalState = crewManager.ToggleCrewListOpen; - } - crewManager.ToggleCrewListOpen = value; - } - private void ToggleChatBox(bool value, bool storeOriginalState) { var crewManager = GameMain.GameSession?.CrewManager; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index 4b38ab997..6e61f3319 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -17,7 +17,16 @@ namespace Barotrauma.Items.Components private GUITickBox autoControlIndicator; private List> pumpOutEmitters = new List>(); - private List> pumpInEmitters = new List>(); + private List> pumpInEmitters = new List>(); + + public float CurrentBrokenVolume + { + get + { + if (item.ConditionPercentage > 10.0f || !IsActive) { return 0.0f; } + return (1.0f - item.ConditionPercentage / 10.0f) * 100.0f; + } + } partial void InitProjSpecific(XElement element) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index 7aa373710..c1bc2bc15 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs @@ -284,6 +284,7 @@ namespace Barotrauma.Items.Components { Enabled = false, Selected = AutoTemp, + ClickSound = GUISoundType.UISwitch, OnClicked = (button, data) => { AutoTemp = !AutoTemp; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 89ef58d14..b59955c76 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -181,6 +181,7 @@ namespace Barotrauma.Items.Components { Selected = false, Enabled = true, + ClickSound = GUISoundType.UISwitch, OnClicked = (button, data) => { button.Selected = !button.Selected; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 0d93814ad..559b44eac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -123,6 +123,7 @@ namespace Barotrauma.Items.Components { Selected = autoPilot, Enabled = true, + ClickSound = GUISoundType.UISwitch, OnClicked = (button, data) => { button.Selected = !button.Selected; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index b6bb282bb..70fa59879 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -342,7 +342,8 @@ namespace Barotrauma.Items.Components } else { - if (Vector2.DistanceSquared(nodeWorldPos, draggingWire.nodes[(int)highlightedNodeIndex]) > Submarine.GridSize.X * Submarine.GridSize.X || PlayerInput.IsShiftDown()) + if ((highlightedNodeIndex.HasValue && Vector2.DistanceSquared(nodeWorldPos, draggingWire.nodes[(int)highlightedNodeIndex]) > Submarine.GridSize.X * Submarine.GridSize.X) || + PlayerInput.IsShiftDown()) { selectedNodeIndex = highlightedNodeIndex; } @@ -535,6 +536,9 @@ namespace Barotrauma.Items.Components nodes = nodePositions.ToList(); UpdateSections(); Drawable = nodes.Any(); + IsActive = + (connections[0] == null ^ connections[1] == null) && + (item.ParentInventory is CharacterInventory characterInventory && ((characterInventory.Owner as Character)?.HasEquippedItem(item) ?? false)); } public void ClientWrite(IWriteMessage msg, object[] extraData = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 48a36b102..4ce13c0a2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -35,6 +35,7 @@ namespace Barotrauma.Items.Components private RoundSound startMoveSound, endMoveSound, moveSound; private SoundChannel moveSoundChannel; + private Vector2 oldRotation = Vector2.Zero; private Vector2 crosshairPos, crosshairPointerPos; @@ -285,7 +286,7 @@ namespace Barotrauma.Items.Components rotation + MathHelper.PiOver2, item.Scale, SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth)); - if (!editing || GUI.DisableHUD) { return; } + if (!editing || GUI.DisableHUD || !item.IsSelected) { return; } float widgetRadius = 60.0f; @@ -304,10 +305,12 @@ namespace Barotrauma.Items.Components drawPos + new Vector2((float)Math.Cos((maxRotation + minRotation) / 2), (float)Math.Sin((maxRotation + minRotation) / 2)) * widgetRadius, Color.LightGreen); - if (!item.IsSelected) { return; } - Widget minRotationWidget = GetWidget("minrotation", spriteBatch, size: 10, initMethod: (widget) => - { + { + widget.Selected += () => + { + oldRotation = RotationLimits; + }; widget.MouseDown += () => { widget.color = GUI.Style.Green; @@ -317,6 +320,10 @@ namespace Barotrauma.Items.Components { widget.color = Color.Yellow; item.CreateEditingHUD(); + if (SubEditorScreen.IsSubEditor()) + { + SubEditorScreen.StoreCommand(new PropertyCommand(this, "RotationLimits", RotationLimits, oldRotation)); + } }; widget.MouseHeld += (deltaTime) => { @@ -345,10 +352,14 @@ namespace Barotrauma.Items.Components widget.DrawPos = GetDrawPos() + new Vector2((float)Math.Cos(minRotation), (float)Math.Sin(minRotation)) * widgetRadius; widget.Update(deltaTime); }; - }); + }); Widget maxRotationWidget = GetWidget("maxrotation", spriteBatch, size: 10, initMethod: (widget) => { + widget.Selected += () => + { + oldRotation = RotationLimits; + }; widget.MouseDown += () => { widget.color = GUI.Style.Green; @@ -358,6 +369,10 @@ namespace Barotrauma.Items.Components { widget.color = Color.Yellow; item.CreateEditingHUD(); + if (SubEditorScreen.IsSubEditor()) + { + SubEditorScreen.StoreCommand(new PropertyCommand(this, "RotationLimits", RotationLimits, oldRotation)); + } }; widget.MouseHeld += (deltaTime) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 8a0e39f1e..3ba47d040 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -3,7 +3,6 @@ using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Linq; @@ -163,6 +162,8 @@ namespace Barotrauma public static Inventory DraggingInventory; + public Inventory ReplacedBy; + public Rectangle BackgroundFrame { get; protected set; } private ushort[] receivedItemIDs; @@ -317,6 +318,11 @@ namespace Barotrauma } } + public Inventory GetReplacementOrThiS() + { + return ReplacedBy?.GetReplacementOrThiS() ?? this; + } + public virtual void CreateSlots() { slots = new InventorySlot[capacity]; @@ -398,8 +404,8 @@ namespace Barotrauma container = (this as ItemInventory).Container; } - if (container == null) return false; - return owner.SelectedCharacter != null || !container.KeepOpenWhenEquipped || (!(owner is Character)) || !owner.HasEquippedItem(container.Item); + if (container == null) { return false; } + return owner.SelectedCharacter != null|| (!(owner is Character character)) || !container.KeepOpenWhenEquippedBy(character) || !owner.HasEquippedItem(container.Item); } protected virtual bool HideSlot(int i) @@ -465,12 +471,15 @@ namespace Barotrauma { if (item != null) { - if (mouseDrag) { item.OwnInventory?.DeleteAllItems(); } slot.ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.4f); if (!mouseDrag) { - GUI.PlayUISound(GUISoundType.PickItem); + SoundPlayer.PlayUISound(GUISoundType.PickItem); } + + SubEditorScreen.BulkItemBufferInUse = true; + SubEditorScreen.BulkItemBuffer.Add(new AddOrDeleteCommand(new List { item }, true)); + item.OwnInventory?.DeleteAllItems(); item.Remove(); } } @@ -993,13 +1002,23 @@ namespace Barotrauma { if (DraggingItemToWorld && Character.Controlled.FocusedItem?.OwnInventory != null && + (Character.Controlled.FocusedItem.GetComponent()?.HasRequiredItems(Character.Controlled, addMessage: false) ?? false) && Character.Controlled.FocusedItem.OwnInventory.CanBePut(draggingItem) && Character.Controlled.FocusedItem.OwnInventory.TryPutItem(draggingItem, Character.Controlled)) { - GUI.PlayUISound(GUISoundType.PickItem); + SoundPlayer.PlayUISound(GUISoundType.PickItem); } else { + if (Screen.Selected is SubEditorScreen) + { + if (draggingItem?.ParentInventory != null) + { + SubEditorScreen.StoreCommand(new InventoryPlaceCommand(draggingItem.ParentInventory, new List { draggingItem }, true)); + } + } + + SoundPlayer.PlayUISound(GUISoundType.DropItem); bool removed = false; if (Screen.Selected is SubEditorScreen editor) { @@ -1025,14 +1044,16 @@ namespace Barotrauma { draggingItem.Drop(Character.Controlled); } - - GUI.PlayUISound(removed ? GUISoundType.PickItem : GUISoundType.DropItem); + + SoundPlayer.PlayUISound(removed ? GUISoundType.PickItem : GUISoundType.DropItem); } } else if (selectedSlot.ParentInventory.Items[selectedSlot.SlotIndex] != draggingItem) { + Inventory oldInventory = draggingItem.ParentInventory; Inventory selectedInventory = selectedSlot.ParentInventory; int slotIndex = selectedSlot.SlotIndex; + int oldSlot = oldInventory == null ? 0 : Array.IndexOf(oldInventory.Items, draggingItem); //if attempting to drop into an invalid slot in the same inventory, try to move to the correct slot if (selectedInventory.Items[slotIndex] == null && @@ -1051,17 +1072,21 @@ namespace Barotrauma } selectedInventory.slots[slotIndex].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); } - GUI.PlayUISound(GUISoundType.PickItem); + SoundPlayer.PlayUISound(GUISoundType.PickItem); } else if (selectedInventory.TryPutItem(draggingItem, slotIndex, true, true, Character.Controlled)) { + if (SubEditorScreen.IsSubEditor()) + { + SubEditorScreen.StoreCommand(new InventoryMoveCommand(oldInventory, selectedInventory, draggingItem, oldSlot, slotIndex)); + } if (selectedInventory.slots != null) { selectedInventory.slots[slotIndex].ShowBorderHighlight(Color.White, 0.1f, 0.4f); } - GUI.PlayUISound(GUISoundType.PickItem); + SoundPlayer.PlayUISound(GUISoundType.PickItem); } else { if (selectedInventory.slots != null){ selectedInventory.slots[slotIndex].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); } - GUI.PlayUISound(GUISoundType.PickItemFail); + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); } selectedInventory.HideTimer = 2.0f; if (selectedSlot.ParentInventory?.Owner is Item parentItem && parentItem.ParentInventory != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index e17e0b092..dbca8e7a8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -62,7 +62,7 @@ namespace Barotrauma } } - public override bool DrawBelowWater => (!(Screen.Selected is SubEditorScreen editor) || !editor.WiringMode || !isWire) && base.DrawBelowWater; + public override bool DrawBelowWater => (!(Screen.Selected is SubEditorScreen editor) || !editor.WiringMode || !isWire) && (base.DrawBelowWater || ParentInventory is CharacterInventory); public override bool DrawOverWater => base.DrawOverWater || (IsSelected || Screen.Selected is SubEditorScreen editor && editor.WiringMode) && isWire; @@ -332,6 +332,7 @@ namespace Barotrauma var holdable = GetComponent(); if (holdable != null && holdable.Picker?.AnimController != null) { + if (!back) { return; } float depthStep = 0.000001f; if (holdable.Picker.SelectedItems[0] == this) { @@ -542,7 +543,7 @@ namespace Barotrauma Spacing = (int)(25 * GUI.Scale) }; - var itemEditor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUI.LargeFont); + var itemEditor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUI.LargeFont) { UserData = this }; itemEditor.Children.First().Color = Color.Black * 0.7f; if (!inGame) { @@ -663,7 +664,7 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), listBox.Content.RectTransform), style: "HorizontalLine"); - var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame, showName: !inGame, titleFont: GUI.SubHeadingFont); + var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame, showName: !inGame, titleFont: GUI.SubHeadingFont) { UserData = ic }; componentEditor.Children.First().Color = Color.Black * 0.7f; if (inGame) @@ -855,7 +856,7 @@ namespace Barotrauma public void UpdateHUD(Camera cam, Character character, float deltaTime) { bool editingHUDCreated = false; - if ((HasInGameEditableProperties && character.SelectedConstruction == this) || + if ((HasInGameEditableProperties && (character.SelectedConstruction == this || EditableWhenEquipped)) || Screen.Selected == GameMain.SubEditorScreen) { GUIComponent prevEditingHUD = editingHUD; @@ -956,7 +957,7 @@ namespace Barotrauma public void DrawHUD(SpriteBatch spriteBatch, Camera cam, Character character) { - if (HasInGameEditableProperties) + if (HasInGameEditableProperties && (character.SelectedConstruction == this || EditableWhenEquipped)) { DrawEditing(spriteBatch, cam); } @@ -1028,13 +1029,13 @@ namespace Barotrauma } else { - if (HasInGameEditableProperties) + if (HasInGameEditableProperties && Character.Controlled != null && (Character.Controlled.SelectedConstruction == this || EditableWhenEquipped)) { if (editingHUD != null && editingHUD.UserData == this) { editingHUD.AddToGUIUpdateList(); } } } - if (Character.Controlled != null && Character.Controlled?.SelectedConstruction != this) { return; } + if (Character.Controlled != null && Character.Controlled.SelectedConstruction != this) { return; } bool needsLayoutUpdate = false; foreach (ItemComponent ic in activeHUDs) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 3e701f41e..4e0f2271d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -97,10 +97,12 @@ namespace Barotrauma { if (potentialContainer?.OwnInventory?.TryPutItem(item, Character.Controlled) ?? false) { - GUI.PlayUISound(GUISoundType.PickItem); + SoundPlayer.PlayUISound(GUISoundType.PickItem); } } + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List {item}, false)); + placePosition = Vector2.Zero; return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs index 7cd74456b..16da4be6a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs @@ -110,5 +110,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } + + static partial void PlayTinnitusProjSpecific(float volume) => SoundPlayer.PlaySound("tinnitus", volume: volume); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs index 558615ef5..694e71f73 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs @@ -11,11 +11,18 @@ namespace Barotrauma partial void UpdateProjSpecific(float growModifier) { - EmitParticles(size, WorldPosition, hull, growModifier, OnChangeHull); + if (this is DummyFireSource) + { + EmitParticles(size, WorldPosition, hull, growModifier, null); + } + else + { + 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 (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; } } public static void EmitParticles(Vector2 size, Vector2 worldPosition, Hull hull, float growModifier, Particle.OnChangeHullHandler onChangeHull = null) @@ -32,6 +39,8 @@ namespace Barotrauma (particlePos.X - (worldPosition.X + size.X / 2.0f)), (float)Math.Sqrt(size.X) * Rand.Range(0.0f, 15.0f) * growModifier); + particleVel.X = MathHelper.Clamp(particleVel.X, -200.0f, 200.0f); + var particle = GameMain.ParticleManager.CreateParticle("flame", particlePos, particleVel, 0.0f, hull); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 31171fff0..a99f8e36f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -10,11 +10,30 @@ namespace Barotrauma { partial class Hull : MapEntity, ISerializableEntity, IServerSerializable, IClientSerializable { + private class RemoteDecal + { + public readonly UInt32 DecalId; + public readonly int SpriteIndex; + public Vector2 NormalizedPos; + public readonly float Scale; + + public RemoteDecal(UInt32 decalId, int spriteIndex, Vector2 normalizedPos, float scale) + { + DecalId = decalId; + SpriteIndex = spriteIndex; + NormalizedPos = normalizedPos; + Scale = scale; + } + } + private float serverUpdateDelay; private float remoteWaterVolume, remoteOxygenPercentage; private List remoteFireSources; private readonly List remoteBackgroundSections = new List(); + private readonly List remoteDecals = new List(); + private readonly HashSet pendingDecalUpdates = new HashSet(); + private double lastAmbientLightEditTime; public override bool SelectableInEditor @@ -94,20 +113,21 @@ namespace Barotrauma { if (entity == this || !entity.IsHighlighted) { continue; } if (!entity.IsMouseOn(position)) { continue; } - if (entity.linkedTo != null && entity.linkedTo.Contains(this)) + if (entity.linkedTo == null || !entity.Linkable) { continue; } + if (entity.linkedTo.Contains(this) || linkedTo.Contains(entity) || rClick) { - if (entity == this || !entity.IsHighlighted) continue; - if (!entity.IsMouseOn(position)) continue; - if (entity.Linkable && entity.linkedTo != null && !entity.linkedTo.Contains(this)) + if (entity == this || !entity.IsHighlighted) { continue; } + if (!entity.IsMouseOn(position)) { continue; } + if (entity.linkedTo.Contains(this)) { - entity.linkedTo.Add(this); - linkedTo.Add(entity); + entity.linkedTo.Remove(this); + linkedTo.Remove(entity); } } - else if (entity.Linkable && entity.linkedTo != null) + else { - entity.linkedTo.Add(this); - linkedTo.Add(entity); + if (!entity.linkedTo.Contains(this)) { entity.linkedTo.Add(this); } + if (!linkedTo.Contains(this)) { linkedTo.Add(entity); } } } } @@ -125,10 +145,14 @@ namespace Barotrauma networkUpdateTimer += deltaTime; if (networkUpdateTimer > 0.2f) { - if (!pendingSectionUpdates.Any()) + if (!pendingSectionUpdates.Any() && !pendingDecalUpdates.Any()) { GameMain.NetworkMember?.CreateEntityEvent(this); } + foreach (Decal decal in pendingDecalUpdates) + { + GameMain.NetworkMember?.CreateEntityEvent(this, new object[] { decal }); + } foreach (int pendingSectionUpdate in pendingSectionUpdates) { GameMain.NetworkMember?.CreateEntityEvent(this, new object[] { pendingSectionUpdate }); @@ -261,6 +285,13 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Rectangle(fireSourceRect.X - (int)fs.DamageRange, fireSourceRect.Y, fireSourceRect.Width + (int)fs.DamageRange * 2, fireSourceRect.Height), GUI.Style.Orange, false, 0, 5); //GUI.DrawRectangle(spriteBatch, new Rectangle((int)fs.LastExtinguishPos.X, (int)-fs.LastExtinguishPos.Y, 5,5), Color.Yellow, true); } + foreach (FireSource fs in FakeFireSources) + { + Rectangle fireSourceRect = new Rectangle((int)fs.WorldPosition.X, -(int)fs.WorldPosition.Y, (int)fs.Size.X, (int)fs.Size.Y); + GUI.DrawRectangle(spriteBatch, fireSourceRect, GUI.Style.Red, false, 0, 5); + GUI.DrawRectangle(spriteBatch, new Rectangle(fireSourceRect.X - (int)fs.DamageRange, fireSourceRect.Y, fireSourceRect.Width + (int)fs.DamageRange * 2, fireSourceRect.Height), GUI.Style.Orange, false, 0, 5); + //GUI.DrawRectangle(spriteBatch, new Rectangle((int)fs.LastExtinguishPos.X, (int)-fs.LastExtinguishPos.Y, 5,5), Color.Yellow, true); + } /*GUI.DrawLine(spriteBatch, new Vector2(drawRect.X, -WorldSurface), new Vector2(drawRect.Right, -WorldSurface), Color.Cyan * 0.5f); @@ -530,9 +561,9 @@ namespace Barotrauma public void ClientWrite(IWriteMessage msg, object[] extraData = null) { - msg.Write(extraData != null); if (extraData == null) { + msg.WriteRangedInteger(0, 0, 2); msg.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8); msg.Write(FireSources.Count > 0); @@ -552,8 +583,16 @@ namespace Barotrauma } } } + else if (extraData[0] is Decal decal) + { + msg.WriteRangedInteger(1, 0, 2); + int decalIndex = decals.IndexOf(decal); + msg.Write((byte)(decalIndex < 0 ? 255 : decalIndex)); + msg.WriteRangedSingle(decal.BaseAlpha, 0.0f, 1.0f, 8); + } else { + msg.WriteRangedInteger(2, 0, 2); int sectorToUpdate = (int)extraData[0]; int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent; int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1); @@ -598,11 +637,6 @@ namespace Barotrauma { 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) { @@ -619,16 +653,16 @@ namespace Barotrauma else { int decalCount = message.ReadRangedInteger(0, MaxDecalsPerHull); - decals.Clear(); + if (decalCount == 0) { decals.Clear(); } + remoteDecals.Clear(); for (int i = 0; i < decalCount; i++) { UInt32 decalId = message.ReadUInt32(); + int spriteIndex = message.ReadByte(); 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); + remoteDecals.Add(new RemoteDecal(decalId, spriteIndex, new Vector2(normalizedXPos, normalizedYPos), decalScale)); } } } @@ -642,11 +676,30 @@ namespace Barotrauma { foreach (BackgroundSection remoteBackgroundSection in remoteBackgroundSections) { + float prevColorStrength = BackgroundSections[remoteBackgroundSection.Index].ColorStrength; BackgroundSections[remoteBackgroundSection.Index].SetColor(remoteBackgroundSection.Color); BackgroundSections[remoteBackgroundSection.Index].SetColorStrength(remoteBackgroundSection.ColorStrength); + paintAmount = Math.Max(0, paintAmount + (BackgroundSections[remoteBackgroundSection.Index].ColorStrength - prevColorStrength) / BackgroundSections.Count); } remoteBackgroundSections.Clear(); + if (remoteDecals.Any()) + { + decals.Clear(); + foreach (RemoteDecal remoteDecal in remoteDecals) + { + float decalPosX = MathHelper.Lerp(rect.X, rect.Right, remoteDecal.NormalizedPos.X); + float decalPosY = MathHelper.Lerp(rect.Y - rect.Height, rect.Y, remoteDecal.NormalizedPos.Y); + if (Submarine != null) + { + decalPosX += Submarine.Position.X; + decalPosY += Submarine.Position.Y; + } + AddDecal(remoteDecal.DecalId, new Vector2(decalPosX, decalPosY), remoteDecal.Scale, isNetworkEvent: true, spriteIndex: remoteDecal.SpriteIndex); + } + remoteDecals.Clear(); + } + if (remoteFireSources == null) { return; } WaterVolume = remoteWaterVolume; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 576d16170..4f863f746 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -219,7 +219,7 @@ namespace Barotrauma.Lights { if (!light.IsBackground) { continue; } light.DrawSprite(spriteBatch, cam); - if (light.Color.A > 0 && light.Range > 0.0f) { light.DrawLightVolume(spriteBatch, lightEffect, transform); } + light.DrawLightVolume(spriteBatch, lightEffect, transform); } GameMain.ParticleManager.Draw(spriteBatch, true, null, Particles.ParticleBlendState.Additive); spriteBatch.End(); @@ -328,7 +328,7 @@ namespace Barotrauma.Lights foreach (LightSource light in activeLights) { if (light.IsBackground) { continue; } - if (light.Color.A > 0 && light.Range > 0.0f) { light.DrawLightVolume(spriteBatch, lightEffect, transform); } + light.DrawLightVolume(spriteBatch, lightEffect, transform); } lightEffect.World = transform; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 43655583c..438cbb3cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -1,3 +1,4 @@ +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -6,6 +7,7 @@ using System.Linq; using System.Text; using System.Xml.Linq; + namespace Barotrauma.Lights { class LightSourceParams : ISerializableEntity @@ -42,6 +44,12 @@ namespace Barotrauma.Lights } } + [Serialize(1f, true), Editable(minValue: 0.01f, maxValue: 100f, ValueStep = 0.1f, DecimalCount = 2)] + public float Scale { get; set; } + + [Serialize("0, 0", true), Editable(ValueStep = 1, DecimalCount = 1, MinValueFloat = -1000f, MaxValueFloat = 1000f)] + public Vector2 Offset { get; set; } + public float TextureRange { get; @@ -239,11 +247,13 @@ namespace Barotrauma.Lights } } + private Vector2 _spriteScale = Vector2.One; + public Vector2 SpriteScale { - get; - set; - } = Vector2.One; + get { return _spriteScale * lightSourceParams.Scale; } + set { _spriteScale = value; } + } public float? OverrideLightSpriteAlpha { @@ -278,7 +288,9 @@ namespace Barotrauma.Lights { get { return lightSourceParams.LightSprite; } } - + + private Vector2 OverrideLightTextureOrigin => OverrideLightTexture.Origin + LightSourceParams.Offset; + public Color Color { get { return lightSourceParams.Color; } @@ -331,16 +343,42 @@ namespace Barotrauma.Lights public bool Enabled = true; - public LightSource (XElement element) + private ISerializableEntity conditionalTarget; + private readonly PropertyConditional.Comparison comparison; + private readonly List conditionals = new List(); + + public LightSource (XElement element, ISerializableEntity conditionalTarget = null) : this(Vector2.Zero, 100.0f, Color.White, null) { lightSourceParams = new LightSourceParams(element); CastShadows = element.GetAttributeBool("castshadows", true); + string comparison = element.GetAttributeString("comparison", null); + if (comparison != null) + { + Enum.TryParse(comparison, ignoreCase: true, out this.comparison); + } if (lightSourceParams.DeformableLightSpriteElement != null) { DeformableLightSprite = new DeformableSprite(lightSourceParams.DeformableLightSpriteElement, invert: true); } + + this.conditionalTarget = conditionalTarget; + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "conditional": + foreach (XAttribute attribute in subElement.Attributes()) + { + if (PropertyConditional.IsValid(attribute)) + { + conditionals.Add(new PropertyConditional(attribute)); + } + } + break; + } + } } public LightSource(LightSourceParams lightSourceParams) @@ -363,7 +401,8 @@ namespace Barotrauma.Lights CastShadows = true; texture = LightTexture; diffToSub = new Dictionary(); - if (addLight) GameMain.LightManager.AddLight(this); + if (addLight) { GameMain.LightManager.AddLight(this); } + } /// @@ -493,7 +532,7 @@ namespace Barotrauma.Lights { return null; } - if (Range < 1.0f || Color.A < 0.01f) return null; + if (Range < 1.0f || Color.A < 1) { return null; } Vector2 drawPos = position; if (ParentSub != null) drawPos += ParentSub.DrawPosition; @@ -535,7 +574,7 @@ namespace Barotrauma.Lights var overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height); - Vector2 origin = OverrideLightTexture.Origin; + Vector2 origin = OverrideLightTextureOrigin; origin /= Math.Max(overrideTextureDims.X, overrideTextureDims.Y); origin -= Vector2.One * 0.5f; @@ -844,7 +883,7 @@ namespace Barotrauma.Lights { overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height); - Vector2 origin = OverrideLightTexture.Origin; + Vector2 origin = OverrideLightTextureOrigin; if (LightSpriteEffect == SpriteEffects.FlipHorizontally) { origin.X = OverrideLightTexture.SourceRect.Width - origin.X; } if (LightSpriteEffect == SpriteEffects.FlipVertically) { origin.Y = OverrideLightTexture.SourceRect.Height - origin.Y; } uvOffset = (origin / overrideTextureDims) - new Vector2(0.5f, 0.5f); @@ -1031,7 +1070,7 @@ namespace Barotrauma.Lights { var overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height); - Vector2 origin = OverrideLightTexture.Origin; + Vector2 origin = OverrideLightTextureOrigin; origin /= Math.Max(overrideTextureDims.X, overrideTextureDims.Y); origin *= TextureRange; @@ -1059,13 +1098,12 @@ namespace Barotrauma.Lights if (DeformableLightSprite != null) { - Vector2 origin = DeformableLightSprite.Origin; + Vector2 origin = DeformableLightSprite.Origin + LightSourceParams.Offset; Vector2 drawPos = position; if (ParentSub != null) { drawPos += ParentSub.DrawPosition; } - if (LightSpriteEffect == SpriteEffects.FlipHorizontally) { origin.X = DeformableLightSprite.Sprite.SourceRect.Width - origin.X; @@ -1084,7 +1122,7 @@ namespace Barotrauma.Lights if (LightSprite != null) { - Vector2 origin = LightSprite.Origin; + Vector2 origin = LightSprite.Origin + LightSourceParams.Offset; if ((LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) { origin.X = LightSprite.SourceRect.Width - origin.X; @@ -1128,12 +1166,27 @@ namespace Barotrauma.Lights GUI.DrawLine(spriteBatch, drawPos - Vector2.One * Range, drawPos + Vector2.One * Range, Color); GUI.DrawLine(spriteBatch, drawPos - new Vector2(1.0f, -1.0f) * Range, drawPos + new Vector2(1.0f, -1.0f) * Range, Color); } + } + } + + public void CheckConditionals() + { + if (conditionals.None()) { return; } + if (conditionalTarget == null) { return; } + if (comparison == PropertyConditional.Comparison.And) + { + Enabled = conditionals.All(c => c.Matches(conditionalTarget)); + } + else + { + Enabled = conditionals.Any(c => c.Matches(conditionalTarget)); } - } public void DrawLightVolume(SpriteBatch spriteBatch, BasicEffect lightEffect, Matrix transform) { + if (Range < 1.0f || Color.A < 1) { return; } + if (CastShadows) { CheckHullsInRange(); @@ -1158,7 +1211,6 @@ namespace Barotrauma.Lights return; } - if (NeedsRecalculation) { var verts = FindRaycastHits(); @@ -1168,7 +1220,6 @@ namespace Barotrauma.Lights NeedsRecalculation = false; } - Vector2 offset = ParentSub == null ? Vector2.Zero : ParentSub.DrawPosition; lightEffect.World = Matrix.CreateTranslation(-new Vector3(position, 0.0f)) * diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 90a63e7da..67b0c321f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -27,6 +27,9 @@ namespace Barotrauma private static bool resizing; private int resizeDirX, resizeDirY; + private Rectangle? prevRect; + + public static bool SelectionChanged; //which entities have been selected for editing private static List selectedList = new List(); @@ -105,6 +108,11 @@ namespace Barotrauma return true; } + /// + /// Used for undo/redo to determine what this item has been replaced with + /// + public MapEntity ReplacedBy; + public virtual void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { } /// @@ -147,13 +155,13 @@ namespace Barotrauma } if (GUI.KeyboardDispatcher.Subscriber == null) { - if (PlayerInput.KeyDown(Keys.Delete)) + if (PlayerInput.KeyHit(Keys.Delete)) { - selectedList.ForEach(e => + if (selectedList.Any()) { - //orphaned wires may already have been removed - if (!e.Removed) { e.Remove(); } - }); + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(selectedList, true)); + } + selectedList.ForEach(e => { if (!e.Removed) { e.Remove(); } }); selectedList.Clear(); } @@ -217,30 +225,6 @@ namespace Barotrauma } } } - else if (PlayerInput.KeyHit(Keys.Z)) - { - SetPreviousRects(e => e.rectMemento.Undo()); - } - else if (PlayerInput.KeyHit(Keys.R)) - { - SetPreviousRects(e => e.rectMemento.Redo()); - } - - void SetPreviousRects(Func memoryMethod) - { - foreach (var e in SelectedList) - { - if (e.rectMemento != null) - { - Point diff = memoryMethod(e).Location - e.Rect.Location; - // We have to call the move method, because there's a lot more than just storing the rect (in some cases) - // We also have to reassign the rect, because the move method does not set the width and height. They might have changed too. - // The Rect property is virtual and it's overridden for structs. Setting the rect via the property should automatically recreate the sections for resizable structures. - e.Move(diff.ToVector2()); - e.Rect = e.rectMemento.Current; - } - } - } } } @@ -343,35 +327,38 @@ namespace Barotrauma //clone if (PlayerInput.IsCtrlDown()) { - var clones = Clone(selectedList); + var clones = Clone(selectedList).Where(c => c != null).ToList(); selectedList = clones; + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(clones, false)); selectedList.ForEach(c => c.Move(moveAmount)); } else // move { + var oldRects = selectedList.Select(e => e.Rect).ToList(); List deposited = new List(); foreach (MapEntity e in selectedList) { - if (e.rectMemento == null) - { - e.rectMemento = new Memento(); - e.rectMemento.Store(e.Rect); - } e.Move(moveAmount); if (isShiftDown && e is Item item && targetContainer != null) { if (targetContainer.OwnInventory.TryPutItem(item, Character.Controlled)) { - GUI.PlayUISound(GUISoundType.DropItem); + SoundPlayer.PlayUISound(GUISoundType.DropItem); deposited.Add(item); } else { - GUI.PlayUISound(GUISoundType.PickItemFail); + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); } } - e.rectMemento.Store(e.Rect); + } + + SubEditorScreen.StoreCommand(new TransformCommand(new List(selectedList),selectedList.Select(entity => entity.Rect).ToList(), oldRects, false)); + if (deposited.Any() && deposited.Any(entity => entity is Item)) + { + var depositedItems = deposited.Where(entity => entity is Item).Cast().ToList(); + SubEditorScreen.StoreCommand(new InventoryPlaceCommand(targetContainer.OwnInventory, depositedItems, false)); } deposited.ForEach(entity => { selectedList.Remove(entity); }); @@ -492,6 +479,11 @@ namespace Barotrauma } } + public MapEntity GetReplacementOrThis() + { + return ReplacedBy?.GetReplacementOrThis() ?? this; + } + public static Item GetPotentialContainer(Vector2 position, List entities = null) { Item targetContainer = null; @@ -904,13 +896,12 @@ namespace Barotrauma public static void Cut(List entities) { if (entities.Count == 0) { return; } - + + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List(entities), true)); + CopyEntities(entities); - - entities.ForEach(e => - { - e.Remove(); - }); + + entities.ForEach(e => { if (!e.Removed) { e.Remove(); } }); entities.Clear(); } @@ -922,6 +913,7 @@ namespace Barotrauma Clone(copiedList); var clones = mapEntityList.Except(prevEntities).ToList(); + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(clones, false)); var nonWireClones = clones.Where(c => !(c is Item item) || item.GetComponent() == null); if (!nonWireClones.Any()) { nonWireClones = clones; } @@ -1031,12 +1023,11 @@ namespace Barotrauma if (resizing) { - if (rectMemento == null) + if (prevRect == null) { - rectMemento = new Memento(); - rectMemento.Store(Rect); + prevRect = new Rectangle(Rect.Location, Rect.Size); } - + Vector2 placePosition = new Vector2(rect.X, rect.Y); Vector2 placeSize = new Vector2(rect.Width, rect.Height); @@ -1079,9 +1070,15 @@ namespace Barotrauma if (!PlayerInput.PrimaryMouseButtonHeld()) { - rectMemento.Store(Rect); resizing = false; Resized?.Invoke(rect); + if (prevRect != null) + { + var newData = new List { Rect }; + var oldData = new List { prevRect.Value }; + SubEditorScreen.StoreCommand(new TransformCommand(new List { this }, newData, oldData, true)); + } + prevRect = null; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 3d3d46a0b..e4c9ee56c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -93,7 +93,7 @@ namespace Barotrauma int heightScaled = (int)(20 * GUI.Scale); editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.25f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) { UserData = this }; GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.95f, 0.8f), editingHUD.RectTransform, Anchor.Center), style: null); - var editor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUI.LargeFont); + var editor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUI.LargeFont) { UserData = this }; if (Submarine.MainSub?.Info?.Type == SubmarineType.OutpostModule) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs index 52961c361..6490192b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs @@ -56,11 +56,12 @@ namespace Barotrauma if (PlayerInput.PrimaryMouseButtonReleased()) { newRect.Location -= MathUtils.ToPoint(Submarine.MainSub.Position); - new Structure(newRect, this, Submarine.MainSub) + var structure = new Structure(newRect, this, Submarine.MainSub) { Submarine = Submarine.MainSub - }; - + }; + + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List { structure }, false)); selected = null; return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index ff303859b..41a8510e8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -95,7 +95,7 @@ namespace Barotrauma { string errorMsg = "Error when loading round sound (" + element + ") - file path not set"; DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Submarine.LoadRoundSound:FilePathEmpty" + element.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace); + GameAnalyticsManager.AddErrorEventOnce("Submarine.LoadRoundSound:FilePathEmpty" + element.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); return null; } @@ -107,7 +107,7 @@ namespace Barotrauma } else { - existingSound = roundSounds.Find(s => s.Filename == filename && s.Stream == stream)?.Sound; + existingSound = roundSounds.Find(s => s.Filename == filename && s.Stream == stream && !s.Sound.Disposed)?.Sound; } if (existingSound == null) @@ -121,7 +121,7 @@ namespace Barotrauma { string errorMsg = "Failed to load sound file \"" + filename + "\"."; DebugConsole.ThrowError(errorMsg, e); - GameAnalyticsManager.AddErrorEventOnce("Submarine.LoadRoundSound:FileNotFound" + filename, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace); + GameAnalyticsManager.AddErrorEventOnce("Submarine.LoadRoundSound:FileNotFound" + filename, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); return null; } } @@ -132,6 +132,26 @@ namespace Barotrauma return newSound; } + public static void ReloadRoundSound(RoundSound roundSound) + { + Sound existingSound = roundSounds?.Find(s => s.Filename == roundSound.Filename && s.Stream == roundSound.Stream && !s.Sound.Disposed)?.Sound; + if (existingSound == null) + { + try + { + existingSound = GameMain.SoundManager.LoadSound(roundSound.Filename, roundSound.Stream); + } + catch (System.IO.FileNotFoundException e) + { + string errorMsg = "Failed to load sound file \"" + roundSound.Filename + "\"."; + DebugConsole.ThrowError(errorMsg, e); + GameAnalyticsManager.AddErrorEventOnce("Submarine.LoadRoundSound:FileNotFound" + roundSound.Filename, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); + return; + } + } + roundSound.Sound = existingSound; + } + private static void RemoveRoundSound(RoundSound roundSound) { roundSound.Sound?.Dispose(); @@ -438,10 +458,15 @@ namespace Barotrauma public void CheckForErrors() { List errorMsgs = new List(); + List warnings = new List(); if (!Hull.hullList.Any()) { - errorMsgs.Add(TextManager.Get("NoHullsWarning")); + if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoWaypoints)) + { + errorMsgs.Add(TextManager.Get("NoHullsWarning")); + warnings.Add(SubEditorScreen.WarningType.NoHulls); + } } if (Info.Type != SubmarineType.OutpostModule || @@ -449,7 +474,11 @@ namespace Barotrauma { if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Path)) { - errorMsgs.Add(TextManager.Get("NoWaypointsWarning")); + if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoWaypoints)) + { + errorMsgs.Add(TextManager.Get("NoWaypointsWarning")); + warnings.Add(SubEditorScreen.WarningType.NoWaypoints); + } } } @@ -460,22 +489,38 @@ namespace Barotrauma if (item.GetComponent() == null) { continue; } if (!item.linkedTo.Any()) { - errorMsgs.Add(TextManager.Get("DisconnectedVentsWarning")); + if (!IsWarningSuppressed(SubEditorScreen.WarningType.DisconnectedVents)) + { + errorMsgs.Add(TextManager.Get("DisconnectedVentsWarning")); + warnings.Add(SubEditorScreen.WarningType.DisconnectedVents); + } break; } } if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Human)) { - errorMsgs.Add(TextManager.Get("NoHumanSpawnpointWarning")); + if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoHumanSpawnpoints)) + { + errorMsgs.Add(TextManager.Get("NoHumanSpawnpointWarning")); + warnings.Add(SubEditorScreen.WarningType.NoHumanSpawnpoints); + } } if (WayPoint.WayPointList.Find(wp => wp.SpawnType == SpawnType.Cargo) == null) { - errorMsgs.Add(TextManager.Get("NoCargoSpawnpointWarning")); + if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoCargoSpawnpoints)) + { + errorMsgs.Add(TextManager.Get("NoCargoSpawnpointWarning")); + warnings.Add(SubEditorScreen.WarningType.NoCargoSpawnpoints); + } } if (!Item.ItemList.Any(it => it.GetComponent() != null && it.HasTag("ballast"))) { - errorMsgs.Add(TextManager.Get("NoBallastTagsWarning")); + if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoBallastTag)) + { + errorMsgs.Add(TextManager.Get("NoBallastTagsWarning")); + warnings.Add(SubEditorScreen.WarningType.NoBallastTag); + } } } else if (Info.Type == SubmarineType.OutpostModule) @@ -503,7 +548,11 @@ namespace Barotrauma if (Gap.GapList.Any(g => g.linkedTo.Count == 0)) { - errorMsgs.Add(TextManager.Get("NonLinkedGapsWarning")); + if (!IsWarningSuppressed(SubEditorScreen.WarningType.NonLinkedGaps)) + { + errorMsgs.Add(TextManager.Get("NonLinkedGapsWarning")); + warnings.Add(SubEditorScreen.WarningType.NonLinkedGaps); + } } int disabledItemLightCount = 0; @@ -515,12 +564,35 @@ namespace Barotrauma int count = GameMain.LightManager.Lights.Count(l => l.CastShadows) - disabledItemLightCount; if (count > 45) { - errorMsgs.Add(TextManager.Get("subeditor.shadowcastinglightswarning")); + if (!IsWarningSuppressed(SubEditorScreen.WarningType.TooManyLights)) + { + errorMsgs.Add(TextManager.Get("subeditor.shadowcastinglightswarning")); + warnings.Add(SubEditorScreen.WarningType.TooManyLights); + } } if (errorMsgs.Any()) { - new GUIMessageBox(TextManager.Get("Warning"), string.Join("\n\n", errorMsgs), new Vector2(0.25f, 0.0f), new Point(400, 200)); + GUIMessageBox msgBox = new GUIMessageBox(TextManager.Get("Warning"), string.Join("\n\n", errorMsgs), new Vector2(0.25f, 0.0f), new Point(400, 200)); + if (warnings.Any()) + { + Point size = msgBox.RectTransform.NonScaledSize; + GUITickBox suppress = new GUITickBox(new RectTransform(new Vector2(1f, 0.33f), msgBox.Content.RectTransform), TextManager.Get("editor.suppresswarnings")); + msgBox.RectTransform.NonScaledSize = new Point(size.X, size.Y + suppress.RectTransform.NonScaledSize.Y); + + msgBox.Buttons[0].OnClicked += (button, obj) => + { + if (suppress.Selected) + { + foreach (SubEditorScreen.WarningType warning in warnings.Where(warning => !SubEditorScreen.SuppressedWarnings.Contains(warning))) + { + SubEditorScreen.SuppressedWarnings.Add(warning); + } + } + + return true; + }; + } } foreach (MapEntity e in MapEntity.mapEntityList) @@ -556,6 +628,11 @@ namespace Barotrauma } } + + bool IsWarningSuppressed(SubEditorScreen.WarningType type) + { + return SubEditorScreen.SuppressedWarnings.Contains(type); + } } public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index f6db39bdc..d11c022c5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -1,5 +1,5 @@ -using System; -using System.Linq; +using Microsoft.Xna.Framework; +using System; namespace Barotrauma.Networking { @@ -49,45 +49,53 @@ namespace Barotrauma.Networking Character targetCharacter = Entity.FindEntityByID(targetCharacterID) as Character; Entity targetEntity = Entity.FindEntityByID(msg.ReadUInt16()); int optionIndex = msg.ReadByte(); + OrderTarget orderTargetPosition = null; + if (msg.ReadBoolean()) + { + var x = msg.ReadSingle(); + var y = msg.ReadSingle(); + var hull = Entity.FindEntityByID(msg.ReadUInt16()) as Hull; + orderTargetPosition = new OrderTarget(new Vector2(x, y), hull, creatingFromExistingData: true); + } - Order order = null; + Order orderPrefab; if (orderIndex < 0 || orderIndex >= Order.PrefabList.Count) { DebugConsole.ThrowError("Invalid order message - order index out of bounds."); - if (NetIdUtils.IdMoreRecent(ID, LastID)) LastID = ID; + if (NetIdUtils.IdMoreRecent(ID, LastID)) { LastID = ID; } return; } else { - order = Order.PrefabList[orderIndex]; + orderPrefab = Order.PrefabList[orderIndex]; } string orderOption = ""; - if (optionIndex >= 0 && optionIndex < order.Options.Length) + if (optionIndex >= 0 && optionIndex < orderPrefab.Options.Length) { - orderOption = order.Options[optionIndex]; + orderOption = orderPrefab.Options[optionIndex]; } - txt = order.GetChatMessage(targetCharacter?.Name, senderCharacter?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == senderCharacter, orderOption: orderOption); + txt = orderPrefab.GetChatMessage(targetCharacter?.Name, senderCharacter?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == senderCharacter, orderOption: orderOption); if (GameMain.Client.GameStarted && Screen.Selected == GameMain.GameScreen) { + var order = orderTargetPosition == null ? + new Order(orderPrefab, targetEntity, orderPrefab.GetTargetItemComponent(targetEntity as Item), orderGiver: senderCharacter) : + new Order(orderPrefab, orderTargetPosition, orderGiver: senderCharacter); + if (order.TargetAllCharacters) { - GameMain.GameSession?.CrewManager?.AddOrder( - new Order(order.Prefab, targetEntity, (targetEntity as Item)?.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType), orderGiver: senderCharacter), - order.Prefab.FadeOutTime); + GameMain.GameSession?.CrewManager?.AddOrder(order, orderPrefab.FadeOutTime); } else if (targetCharacter != null) { - targetCharacter.SetOrder( - new Order(order.Prefab, targetEntity, (targetEntity as Item)?.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType), orderGiver: senderCharacter), - orderOption, senderCharacter); + targetCharacter.SetOrder(order, orderOption, senderCharacter); } } if (NetIdUtils.IdMoreRecent(ID, LastID)) { GameMain.Client.AddChatMessage( - new OrderChatMessage(order, orderOption, txt, targetEntity, targetCharacter, senderCharacter)); + new OrderChatMessage(orderPrefab, orderOption, txt, orderTargetPosition ?? targetEntity as ISpatialEntity, targetCharacter, senderCharacter)); LastID = ID; } return; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index d1a7c1502..df4da0256 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -608,10 +608,10 @@ namespace Barotrauma.Networking { string errorMsg = "Error while reading a message from server. {" + e + "}. "; if (GameMain.Client == null) { errorMsg += "Client disposed."; } - errorMsg += "\n" + e.StackTrace; + errorMsg += "\n" + e.StackTrace.CleanupStackTrace(); if (e.InnerException != null) { - errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace; + errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace(); } GameAnalyticsManager.AddErrorEventOnce("GameClient.Update:CheckServerMessagesException" + e.TargetSite.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); DebugConsole.ThrowError("Error while reading a message from server.", e); @@ -738,10 +738,10 @@ namespace Barotrauma.Networking } catch (Exception e) { - string errorMsg = "Error while reading an ingame update message from server. {" + e + "}\n" + e.StackTrace; + string errorMsg = "Error while reading an ingame update message from server. {" + e + "}\n" + e.StackTrace.CleanupStackTrace(); if (e.InnerException != null) { - errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace; + errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace(); } #if DEBUG DebugConsole.ThrowError("Error while reading an ingame update message from server.", e); @@ -755,7 +755,7 @@ namespace Barotrauma.Networking { string errorMsg = "Failed to read a voice packet from the server (VoipClient == null). "; if (GameMain.Client == null) { errorMsg += "Client disposed. "; } - errorMsg += "\n" + Environment.StackTrace; + errorMsg += "\n" + Environment.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce( "GameClient.ReadDataMessage:VoipClientNull", GameMain.Client == null ? GameAnalyticsSDK.Net.EGAErrorSeverity.Error : GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, @@ -1023,7 +1023,10 @@ namespace Barotrauma.Networking if (Enum.TryParse(splitMsg[0], out disconnectReason)) { disconnectReasonIncluded = true; } } - if (disconnectMsg == Lidgren.Network.NetConnection.NoResponseMessage) + if (disconnectMsg == Lidgren.Network.NetConnection.NoResponseMessage || + disconnectReason == DisconnectReason.Banned || + disconnectReason == DisconnectReason.Kicked || + disconnectReason == DisconnectReason.TooManyFailedLogins) { allowReconnect = false; } @@ -1124,7 +1127,7 @@ namespace Barotrauma.Networking else { DebugConsole.NewMessage("Not attempting to reconnect (DisconnectReason doesn't allow reconnection)."); - msg = TextManager.Get("DisconnectReason." + disconnectReason.ToString()); + msg = TextManager.Get("DisconnectReason." + disconnectReason.ToString()) + " "; for (int i = 1; i < splitMsg.Length; i++) { @@ -1966,7 +1969,12 @@ namespace Barotrauma.Networking string selectShuttleName = inc.ReadString(); string selectShuttleHash = inc.ReadString(); - string campaignSubmarineIndexes = inc.ReadString(); + UInt16 campaignSubmarineIndexCount = inc.ReadUInt16(); + List campaignSubIndices = new List(); + for (int i = 0; i< campaignSubmarineIndexCount; i++) + { + campaignSubIndices.Add(inc.ReadUInt16()); + } bool allowSubVoting = inc.ReadBoolean(); bool allowModeVoting = inc.ReadBoolean(); @@ -2022,22 +2030,16 @@ namespace Barotrauma.Networking if (GameMain.Client.IsServerOwner) RequestSelectMode(modeIndex); } - if (campaignSubmarineIndexes != null) + if (campaignSubIndices != null) { - string[] activeIndexes = campaignSubmarineIndexes.Split(';'); - GameMain.NetLobbyScreen.CampaignSubmarines = new List(); - for (int i = 0; i < activeIndexes.Length; i++) + foreach (UInt16 campaignSubIndex in campaignSubIndices) { - int index; - if (int.TryParse(activeIndexes[i], out index)) + SubmarineInfo sub = GameMain.Client.ServerSubmarines[campaignSubIndex]; + if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, "campaign")) { - SubmarineInfo sub = GameMain.Client.ServerSubmarines[index]; - if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, "campaign")) - { - GameMain.NetLobbyScreen.CampaignSubmarines.Add(sub); - } - } + GameMain.NetLobbyScreen.CampaignSubmarines.Add(sub); + } } if (HasPermission(ClientPermissions.ManageCampaign) && !gameStarted && GameMain.NetLobbyScreen?.CampaignSetupUI != null) @@ -2139,6 +2141,7 @@ namespace Barotrauma.Networking return; } + entities.Add(entity); if (entity != null && (entity is Item || entity is Character || entity is Submarine)) { entity.ClientRead(objHeader.Value, inc, sendingTime); @@ -2186,10 +2189,11 @@ namespace Barotrauma.Networking "Previous object was " + (prevBitLength) + " bits long (" + (prevByteLength) + " bytes)", " " }; - errorLines.Add(ex.StackTrace); + errorLines.Add(ex.StackTrace.CleanupStackTrace()); errorLines.Add(" "); if (prevObjHeader == ServerNetObject.ENTITY_EVENT || prevObjHeader == ServerNetObject.ENTITY_EVENT_INITIAL || - objHeader == ServerNetObject.ENTITY_EVENT || objHeader == ServerNetObject.ENTITY_EVENT_INITIAL) + objHeader == ServerNetObject.ENTITY_EVENT || objHeader == ServerNetObject.ENTITY_EVENT_INITIAL || + objHeader == ServerNetObject.ENTITY_POSITION || prevObjHeader == ServerNetObject.ENTITY_POSITION) { foreach (IServerSerializable ent in entities) { @@ -2721,7 +2725,7 @@ namespace Barotrauma.Networking MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign; if (campaign == null) { - DebugConsole.ThrowError("Failed send campaign state to the server (no campaign active).\n" + Environment.StackTrace); + DebugConsole.ThrowError("Failed send campaign state to the server (no campaign active).\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -2738,7 +2742,7 @@ namespace Barotrauma.Networking { if (string.IsNullOrWhiteSpace(command)) { - DebugConsole.ThrowError("Cannot send an empty console command to the server!\n" + Environment.StackTrace); + DebugConsole.ThrowError("Cannot send an empty console command to the server!\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -2778,7 +2782,7 @@ namespace Barotrauma.Networking if (subIndex < 0 || subIndex >= subList.Content.CountChildren) { - DebugConsole.ThrowError("Submarine index out of bounds (" + subIndex + ")\n" + Environment.StackTrace); + DebugConsole.ThrowError("Submarine index out of bounds (" + subIndex + ")\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -2818,7 +2822,7 @@ namespace Barotrauma.Networking if (!HasPermission(ClientPermissions.SelectMode)) return; if (modeIndex < 0 || modeIndex >= GameMain.NetLobbyScreen.ModeList.Content.CountChildren) { - DebugConsole.ThrowError("Gamemode index out of bounds (" + modeIndex + ")\n" + Environment.StackTrace); + DebugConsole.ThrowError("Gamemode index out of bounds (" + modeIndex + ")\n" + Environment.StackTrace.CleanupStackTrace()); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs index fe14cb666..b31e4f6e1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -53,12 +53,12 @@ namespace Barotrauma.Networking if (((Entity)entity).Removed) { - DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the entity has been removed.\n" + Environment.StackTrace); + DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the entity has been removed.\n" + Environment.StackTrace.CleanupStackTrace()); return; } if (((Entity)entity).IdFreed) { - DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the ID of the entity has been freed.\n" + Environment.StackTrace); + DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the ID of the entity has been freed.\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -254,7 +254,7 @@ namespace Barotrauma.Networking catch (Exception e) { - string errorMsg = "Failed to read event for entity \"" + entity.ToString() + "\" (" + e.Message + ")! (MidRoundSyncing: " + thisClient.MidRoundSyncing + ")\n" + e.StackTrace; + string errorMsg = "Failed to read event for entity \"" + entity.ToString() + "\" (" + e.Message + ")! (MidRoundSyncing: " + thisClient.MidRoundSyncing + ")\n" + e.StackTrace.CleanupStackTrace(); errorMsg += "\nPrevious entities:"; for (int j = entities.Count - 2; j >= 0; j--) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs index 84c5b31bd..a6f7279f3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs @@ -1,5 +1,4 @@ -using Lidgren.Network; -using System; +using System; namespace Barotrauma.Networking { @@ -11,10 +10,20 @@ namespace Barotrauma.Networking msg.Write(NetStateID); msg.Write((byte)ChatMessageType.Order); msg.Write((byte)Order.PrefabList.IndexOf(Order.Prefab)); - msg.Write(TargetCharacter == null ? (UInt16)0 : TargetCharacter.ID); - msg.Write(TargetEntity == null ? (UInt16)0 : TargetEntity.ID); + msg.Write(TargetEntity is Entity ? (TargetEntity as Entity).ID : (UInt16)0); msg.Write((byte)Array.IndexOf(Order.Prefab.Options, OrderOption)); + if (TargetEntity is OrderTarget orderTarget) + { + msg.Write(true); + msg.Write(orderTarget.Position.X); + msg.Write(orderTarget.Position.Y); + msg.Write(orderTarget.Hull == null ? (UInt16)0 : orderTarget.Hull.ID); + } + else + { + msg.Write(false); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index 38f0f6fb2..c2b239942 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -162,6 +162,7 @@ namespace Barotrauma.Networking disconnectMsg = $"DisconnectMessage.MissingContentPackages~[missingcontentpackages]={string.Join(", ", packageStrs)}"; } Close(disconnectMsg, disableReconnect: true); + OnDisconnectMessageReceived?.Invoke(DisconnectReason.MissingContentPackage + "/" + disconnectMsg); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs index e5dfe0417..063201ec6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs @@ -373,7 +373,17 @@ namespace Barotrauma.Networking info.GameMode = element.GetAttributeString("GameMode", ""); info.GameVersion = element.GetAttributeString("GameVersion", ""); - info.MaxPlayers = element.GetAttributeInt("MaxPlayers", 0); + + int maxPlayersElement = element.GetAttributeInt("MaxPlayers", 0); + + if (maxPlayersElement > NetConfig.MaxPlayers) + { + DebugConsole.IsOpen = true; + DebugConsole.NewMessage($"Setting the maximum amount of players to {maxPlayersElement} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead.", Color.Red); + maxPlayersElement = NetConfig.MaxPlayers; + } + + info.MaxPlayers = maxPlayersElement; if (Enum.TryParse(element.GetAttributeString("PlayStyle", ""), out PlayStyle playStyleTemp)) { info.PlayStyle = playStyleTemp; } if (bool.TryParse(element.GetAttributeString("UsingWhiteList", ""), out bool whitelistTemp)) { info.UsingWhiteList = whitelistTemp; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs index a78d7c1d0..658879440 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs @@ -478,11 +478,14 @@ namespace Barotrauma.Steam } if (rules.ContainsKey("gamestarted")) serverInfo.GameStarted = rules["gamestarted"] == "True"; - if (rules.ContainsKey("gamemode")) { serverInfo.GameMode = rules["gamemode"]; } + if (rules.ContainsKey("playstyle") && Enum.TryParse(rules["playstyle"], out PlayStyle playStyle)) + { + serverInfo.PlayStyle = playStyle; + } if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopIds.Count) @@ -1297,7 +1300,7 @@ namespace Barotrauma.Steam } catch (Exception e) { - errorMsg = "Disabling the workshop item \"" + item?.Title + "\" failed. " + e.Message + "\n" + e.StackTrace; + errorMsg = "Disabling the workshop item \"" + item?.Title + "\" failed. " + e.Message + "\n" + e.StackTrace.CleanupStackTrace(); if (!noLog) { DebugConsole.NewMessage(errorMsg, Microsoft.Xna.Framework.Color.Red); @@ -1492,7 +1495,7 @@ namespace Barotrauma.Steam GameAnalyticsManager.AddErrorEventOnce( "SteamManager.AutoUpdateWorkshopItems:" + e.Message, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Failed to autoupdate workshop item \"" + item.Title + "\". " + e.Message + "\n" + e.StackTrace); + "Failed to autoupdate workshop item \"" + item.Title + "\". " + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); }); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Decal.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Decal.cs deleted file mode 100644 index 41008a008..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Decal.cs +++ /dev/null @@ -1,131 +0,0 @@ -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using System; - -namespace Barotrauma.Particles -{ - class Decal - { - public readonly DecalPrefab Prefab; - private Vector2 position; - - public readonly Sprite Sprite; - - private float fadeTimer; - - 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; } - } - - 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; - } - } - - private Hull hull; - - private float scale; - - private Rectangle clippedSourceRect; - - 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 - 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; - } - - public void Update(float deltaTime) - { - fadeTimer += deltaTime; - } - - public void StopFadeIn() - { - Color *= GetAlpha(); - fadeTimer = Prefab.FadeInTime; - } - - 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); - } - - private float GetAlpha() - { - if (fadeTimer < Prefab.FadeInTime) - { - return fadeTimer / Prefab.FadeInTime; - } - else if (fadeTimer > Prefab.LifeTime - Prefab.FadeOutTime) - { - return (Prefab.LifeTime - fadeTimer) / Prefab.FadeOutTime; - } - return 1.0f; - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/DecalManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/DecalManager.cs deleted file mode 100644 index 6548582bf..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/DecalManager.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Microsoft.Xna.Framework; -using System.Collections.Generic; -using System.Xml.Linq; -using System.Linq; - -namespace Barotrauma.Particles -{ - class DecalManager - { - public PrefabCollection Prefabs { get; private set; } - - 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; } - - 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(); - 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()); - } - } - - public void RemoveByFile(string filePath) - { - Prefabs.RemoveByFile(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/BarotraumaClient/ClientSource/Particles/DecalPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/DecalPrefab.cs deleted file mode 100644 index da75fd497..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/DecalPrefab.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Xml.Linq; - -namespace Barotrauma.Particles -{ - class DecalPrefab : IPrefab, IDisposable - { - public readonly string Name; - - public string OriginalName { get { return Name; } } - - private string _identifier; - public string Identifier - { - get - { - if (_identifier == null) - { - _identifier = Name.ToLowerInvariant(); - } - return _identifier; - } - } - - 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(); - - 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/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index 8030da641..c33caf404 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -483,7 +483,7 @@ namespace Barotrauma.Particles if (prefab.GrowTime > 0.0f && totalLifeTime - lifeTime < prefab.GrowTime) { - drawSize *= ((totalLifeTime - lifeTime) / prefab.GrowTime); + drawSize *= MathUtils.SmoothStep((totalLifeTime - lifeTime) / prefab.GrowTime); } Color currColor = new Color(color.ToVector4() * ColorMultiplier); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index fd0a69875..0335ba0c7 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, Color? colorMultiplier = null) + 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, ParticlePrefab overrideParticle = 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, colorMultiplier); + Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle); 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, colorMultiplier); + Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle); } } - private void Emit(Vector2 position, Hull hullGuess, float angle, float particleRotation, float velocityMultiplier, float sizeMultiplier, Color? colorMultiplier = null) + private void Emit(Vector2 position, Hull hullGuess, float angle, float particleRotation, float velocityMultiplier, float sizeMultiplier, Color? colorMultiplier = null, ParticlePrefab overrideParticle = null) { angle += Rand.Range(Prefab.AngleMin, Prefab.AngleMax); @@ -55,13 +55,20 @@ namespace Barotrauma.Particles Vector2 velocity = dir * Rand.Range(Prefab.VelocityMin, Prefab.VelocityMax) * velocityMultiplier; position += dir * Rand.Range(Prefab.DistanceMin, Prefab.DistanceMax); - var particle = GameMain.ParticleManager.CreateParticle(Prefab.ParticlePrefab, position, velocity, particleRotation, hullGuess, Prefab.DrawOnTop); + var particle = GameMain.ParticleManager.CreateParticle(overrideParticle ?? Prefab.ParticlePrefab, position, velocity, particleRotation, hullGuess, Prefab.DrawOnTop); if (particle != null) { particle.Size *= Rand.Range(Prefab.ScaleMin, Prefab.ScaleMax) * sizeMultiplier; particle.HighQualityCollisionDetection = Prefab.HighQualityCollisionDetection; - if (colorMultiplier.HasValue) { particle.ColorMultiplier = colorMultiplier.Value.ToVector4(); } + if (colorMultiplier.HasValue) + { + particle.ColorMultiplier = colorMultiplier.Value.ToVector4(); + } + else if (Prefab.ColorMultiplier != Color.White) + { + particle.ColorMultiplier = Prefab.ColorMultiplier.ToVector4(); + } } } @@ -138,6 +145,8 @@ namespace Barotrauma.Particles public readonly bool CopyEntityAngle; + public readonly Color ColorMultiplier; + public bool DrawOnTop => forceDrawOnTop || ParticlePrefab.DrawOnTop; private readonly bool forceDrawOnTop; @@ -204,11 +213,12 @@ namespace Barotrauma.Particles } EmitInterval = element.GetAttributeFloat("emitinterval", 0.0f); - ParticlesPerSecond = element.GetAttributeInt("particlespersecond", 0); + ParticlesPerSecond = element.GetAttributeFloat("particlespersecond", 0); ParticleAmount = element.GetAttributeInt("particleamount", 0); HighQualityCollisionDetection = element.GetAttributeBool("highqualitycollisiondetection", false); CopyEntityAngle = element.GetAttributeBool("copyentityangle", false); - forceDrawOnTop = element.GetAttributeBool("drawontop", false); + forceDrawOnTop = element.GetAttributeBool("drawontop", false); + ColorMultiplier = element.GetAttributeColor("colormultiplier", Color.White); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index 234b15f6f..368eb8951 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -150,7 +150,7 @@ namespace Barotrauma } sb.AppendLine("\n"); sb.AppendLine("Game version " + GameMain.Version + - " (" + AssemblyInfo.GetBuildString() + ", branch " + AssemblyInfo.GetGitBranch() + ", revision " + AssemblyInfo.GetGitRevision() + ")"); + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"); if (GameMain.Config != null) { sb.AppendLine("Graphics mode: " + GameMain.Config.GraphicsWidth + "x" + GameMain.Config.GraphicsHeight + " (" + GameMain.Config.WindowMode.ToString() + ")"); @@ -219,7 +219,7 @@ namespace Barotrauma } sb.AppendLine("Stack trace: "); - sb.AppendLine(exception.StackTrace); + sb.AppendLine(exception.StackTrace.CleanupStackTrace()); sb.AppendLine("\n"); if (exception.InnerException != null) @@ -230,7 +230,7 @@ namespace Barotrauma sb.AppendLine("Target site: " + exception.InnerException.TargetSite.ToString()); } sb.AppendLine("Stack trace: "); - sb.AppendLine(exception.InnerException.StackTrace); + sb.AppendLine(exception.InnerException.StackTrace.CleanupStackTrace()); } sb.AppendLine("Last debug messages:"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index e570c3270..b3b0ecc64 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -374,6 +374,14 @@ namespace Barotrauma.CharacterEditor if (Wizard.instance != null) { return; } spriteSheetRect = CalculateSpritesheetRectangle(); // Handle shortcut keys + if (PlayerInput.KeyHit(Keys.F1)) + { + SetToggle(paramsToggle, !paramsToggle.Selected); + } + if (PlayerInput.KeyHit(Keys.F5)) + { + RecreateRagdoll(); + } if (GUI.KeyboardDispatcher.Subscriber == null) { if (PlayerInput.KeyHit(Keys.D1)) @@ -445,10 +453,6 @@ namespace Barotrauma.CharacterEditor { SetToggle(showCollidersToggle, !showCollidersToggle.Selected); } - if (PlayerInput.KeyHit(Keys.Tab)) - { - SetToggle(paramsToggle, !paramsToggle.Selected); - } if (PlayerInput.KeyHit(Keys.L)) { SetToggle(lightsToggle, !lightsToggle.Selected); @@ -469,10 +473,6 @@ namespace Barotrauma.CharacterEditor { SetToggle(ikToggle, !ikToggle.Selected); } - if (PlayerInput.KeyHit(Keys.F5)) - { - RecreateRagdoll(); - } } if (PlayerInput.KeyDown(InputType.Left) || PlayerInput.KeyDown(InputType.Right) || PlayerInput.KeyDown(InputType.Up) || PlayerInput.KeyDown(InputType.Down)) { @@ -809,7 +809,13 @@ namespace Barotrauma.CharacterEditor else if (showColliders) { character.AnimController.Collider.DebugDraw(spriteBatch, Color.White, forceColor: true); - character.AnimController.Limbs.ForEach(l => l.body.DebugDraw(spriteBatch, GUI.Style.Green, forceColor: true)); + foreach (var limb in character.AnimController.Limbs) + { + if (!limb.Hide) + { + limb.body.DebugDraw(spriteBatch, GUI.Style.Green, forceColor: true); + } + } } spriteBatch.End(); @@ -953,14 +959,14 @@ namespace Barotrauma.CharacterEditor { UpdateOtherLimbs(lastLimb, l => TryUpdateSubParam(l.Params, "spriteorientation", angle)); } - }, circleRadius: 40, widgetSize: 15, rotationOffset: MathHelper.Pi, autoFreeze: false, rounding: 10); + }, circleRadius: 40, widgetSize: 15, rotationOffset: 0, autoFreeze: false, rounding: 10); } else { var topLeft = spriteSheetControls.RectTransform.TopLeft; GUI.DrawString(spriteBatch, new Vector2(topLeft.X + 350 * GUI.xScale, GameMain.GraphicsHeight - 95 * GUI.yScale), GetCharacterEditorTranslation("SpriteSheetOrientation") + ":", Color.White, Color.Gray * 0.5f, 10, GUI.Font); DrawRadialWidget(spriteBatch, new Vector2(topLeft.X + 610 * GUI.xScale, GameMain.GraphicsHeight - 75 * GUI.yScale), RagdollParams.SpritesheetOrientation, string.Empty, Color.White, - angle => TryUpdateRagdollParam("spritesheetorientation", angle), circleRadius: 40, widgetSize: 15, rotationOffset: MathHelper.Pi, autoFreeze: false, rounding: 10); + angle => TryUpdateRagdollParam("spritesheetorientation", angle), circleRadius: 40, widgetSize: 15, rotationOffset: 0, autoFreeze: false, rounding: 10); } } // Debug @@ -2780,9 +2786,7 @@ namespace Barotrauma.CharacterEditor }; // Spacing new GUIFrame(new RectTransform(buttonSize / 2, layoutGroup.RectTransform), style: null) { CanBeFocused = false }; - Vector2 messageBoxRelSize = new Vector2(0.5f, 0.5f); - int messageBoxWidth = GameMain.GraphicsWidth / 2; - int messageBoxHeight = GameMain.GraphicsHeight / 2; + Vector2 messageBoxRelSize = new Vector2(0.5f, 0.7f); var saveRagdollButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("SaveRagdoll")); saveRagdollButton.OnClicked += (button, userData) => { @@ -2902,12 +2906,12 @@ namespace Barotrauma.CharacterEditor { var box = new GUIMessageBox(GetCharacterEditorTranslation("SaveAnimation"), string.Empty, new string[] { TextManager.Get("Cancel"), TextManager.Get("Save") }, messageBoxRelSize); var textArea = new GUIFrame(new RectTransform(new Vector2(1, 0.1f), box.Content.RectTransform) { MinSize = new Point(350, 30) }, style: null); - var inputLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), textArea.RectTransform) { MinSize = new Point(250, 30) }, $"{GetCharacterEditorTranslation("ProvideFileName")}: "); - var inputField = new GUITextBox(new RectTransform(new Vector2(0.5f, 1), textArea.RectTransform, Anchor.TopRight) { MinSize = new Point(100, 30) }, CurrentAnimation.Name); + var inputLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), textArea.RectTransform, Anchor.CenterLeft) { MinSize = new Point(250, 30) }, $"{GetCharacterEditorTranslation("ProvideFileName")}: "); + var inputField = new GUITextBox(new RectTransform(new Vector2(0.45f, 1), textArea.RectTransform, Anchor.CenterRight) { MinSize = new Point(100, 30) }, CurrentAnimation.Name); // Type filtering var typeSelectionArea = new GUIFrame(new RectTransform(new Vector2(1f, 0.1f), box.Content.RectTransform) { MinSize = new Point(0, 30) }, style: null); - var typeLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), typeSelectionArea.RectTransform, Anchor.TopCenter, Pivot.TopRight), $"{GetCharacterEditorTranslation("SelectAnimationType")}: "); - var typeDropdown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1), typeSelectionArea.RectTransform, Anchor.TopCenter, Pivot.TopLeft), elementCount: 4); + var typeLabel = new GUITextBlock(new RectTransform(new Vector2(0.45f, 1), typeSelectionArea.RectTransform, Anchor.CenterLeft), $"{GetCharacterEditorTranslation("SelectAnimationType")}: "); + var typeDropdown = new GUIDropDown(new RectTransform(new Vector2(0.45f, 1), typeSelectionArea.RectTransform, Anchor.CenterRight), elementCount: 4); foreach (object enumValue in Enum.GetValues(typeof(AnimationType))) { if (!(enumValue is AnimationType.NotDefined)) @@ -2958,8 +2962,8 @@ namespace Barotrauma.CharacterEditor deleteButton.Enabled = false; // Type filtering var typeSelectionArea = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.1f), loadBox.Content.RectTransform) { MinSize = new Point(0, 30) }, style: null); - var typeLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), typeSelectionArea.RectTransform, Anchor.TopCenter, Pivot.TopRight), $"{GetCharacterEditorTranslation("SelectAnimationType")}: "); - var typeDropdown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1), typeSelectionArea.RectTransform, Anchor.TopCenter, Pivot.TopLeft), elementCount: 4); + var typeLabel = new GUITextBlock(new RectTransform(new Vector2(0.45f, 1), typeSelectionArea.RectTransform, Anchor.CenterLeft), $"{GetCharacterEditorTranslation("SelectAnimationType")}: "); + var typeDropdown = new GUIDropDown(new RectTransform(new Vector2(0.45f, 1), typeSelectionArea.RectTransform, Anchor.CenterRight), elementCount: 4); foreach (object enumValue in Enum.GetValues(typeof(AnimationType))) { if (!(enumValue is AnimationType.NotDefined)) @@ -3499,12 +3503,8 @@ namespace Barotrauma.CharacterEditor limbJoint.UpperLimit += MathHelper.TwoPi; } } - - if (limbJoint.UpperLimit - limbJoint.LowerLimit > MathHelper.TwoPi) - { - limbJoint.LowerLimit = MathUtils.WrapAnglePi(limbJoint.LowerLimit); - limbJoint.UpperLimit = MathUtils.WrapAnglePi(limbJoint.UpperLimit); - } + limbJoint.LowerLimit = MathUtils.WrapAnglePi(limbJoint.LowerLimit); + limbJoint.UpperLimit = MathUtils.WrapAnglePi(limbJoint.UpperLimit); } private Limb GetClosestLimbOnRagdoll(Vector2 targetPos, Func filter = null) @@ -4392,7 +4392,7 @@ namespace Barotrauma.CharacterEditor if (!altDown && editJoints && selectedJoints.Any() && jointCreationMode == JointCreationMode.None) { - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 180, 250), GetCharacterEditorTranslation("HoldLeftAltToManipulateJoint"), Color.White, Color.Black * 0.5f, 10, GUI.Font); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 180, 100), GetCharacterEditorTranslation("HoldLeftAltToManipulateJoint"), Color.White, Color.Black * 0.5f, 10, GUI.Font); } foreach (Limb limb in character.AnimController.Limbs) @@ -4465,7 +4465,13 @@ namespace Barotrauma.CharacterEditor { if (joint.LimitEnabled && jointCreationMode == JointCreationMode.None) { - DrawJointLimitWidgets(spriteBatch, limb, joint, tformedJointPos, autoFreeze: true, allowPairEditing: true, rotationOffset: limb.Rotation, holdPosition: true); + var otherBody = limb == joint.LimbA ? joint.LimbB : joint.LimbA; + float rotation = -otherBody.Rotation + limb.Params.GetSpriteOrientation(); + if (character.AnimController.Dir < 0) + { + rotation -= MathHelper.Pi; + } + DrawJointLimitWidgets(spriteBatch, limb, joint, tformedJointPos, autoFreeze: true, allowPairEditing: true, rotationOffset: rotation, holdPosition: true); } // Is the direction inversed incorrectly? Vector2 to = tformedJointPos + VectorExtensions.ForwardFlipped(joint.LimbB.Rotation - joint.LimbB.Params.GetSpriteOrientation(), 20); @@ -4898,7 +4904,7 @@ namespace Barotrauma.CharacterEditor void RecalculateCollider(Limb l) { // We want the collider to be slightly smaller than the source rect, because the source rect is usually a bit bigger than the graphic. - float multiplier = 0.85f; + float multiplier = 0.9f; l.body.SetSize(new Vector2(ConvertUnits.ToSimUnits(width), ConvertUnits.ToSimUnits(height)) * l.Scale * RagdollParams.TextureScale * multiplier); TryUpdateLimbParam(l, "radius", ConvertUnits.ToDisplayUnits(l.body.radius / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); TryUpdateLimbParam(l, "width", ConvertUnits.ToDisplayUnits(l.body.width / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); @@ -5010,7 +5016,7 @@ namespace Barotrauma.CharacterEditor { if (joint.LimitEnabled && jointCreationMode == JointCreationMode.None) { - DrawJointLimitWidgets(spriteBatch, limb, joint, tformedJointPos, autoFreeze: false, allowPairEditing: true, holdPosition: false); + DrawJointLimitWidgets(spriteBatch, limb, joint, tformedJointPos, autoFreeze: false, allowPairEditing: true, holdPosition: false, rotationOffset: joint.LimbB.Params.GetSpriteOrientation()); } if (jointSelectionWidget.IsControlled) { @@ -5071,7 +5077,7 @@ namespace Barotrauma.CharacterEditor private void DrawJointLimitWidgets(SpriteBatch spriteBatch, Limb limb, LimbJoint joint, Vector2 drawPos, bool autoFreeze, bool allowPairEditing, bool holdPosition, float rotationOffset = 0) { - rotationOffset -= limb.Params.GetSpriteOrientation(); + bool clockWise = joint.Params.ClockWiseRotation; Color angleColor = joint.UpperLimit - joint.LowerLimit > 0 ? GUI.Style.Green * 0.5f : GUI.Style.Red; DrawRadialWidget(spriteBatch, drawPos, MathHelper.ToDegrees(joint.UpperLimit), $"{joint.Params.Name}: {GetCharacterEditorTranslation("UpperLimit")}", Color.Cyan, angle => { @@ -5111,7 +5117,7 @@ namespace Barotrauma.CharacterEditor DrawAngle(20, angleColor, 4); DrawAngle(40, Color.Cyan); GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: Color.Cyan, font: GUI.SmallFont); - }, circleRadius: 40, rotationOffset: rotationOffset, displayAngle: false, clockWise: false, holdPosition: holdPosition); + }, circleRadius: 40, rotationOffset: rotationOffset, displayAngle: false, clockWise: clockWise, holdPosition: holdPosition); DrawRadialWidget(spriteBatch, drawPos, MathHelper.ToDegrees(joint.LowerLimit), $"{joint.Params.Name}: {GetCharacterEditorTranslation("LowerLimit")}", Color.Yellow, angle => { joint.LowerLimit = MathHelper.ToRadians(angle); @@ -5150,12 +5156,12 @@ namespace Barotrauma.CharacterEditor DrawAngle(20, angleColor, 4); DrawAngle(25, Color.Yellow); GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: Color.Yellow, font: GUI.SmallFont); - }, circleRadius: 25, rotationOffset: rotationOffset, displayAngle: false, clockWise: false, holdPosition: holdPosition); + }, circleRadius: 25, rotationOffset: rotationOffset, displayAngle: false, clockWise: clockWise, holdPosition: holdPosition); void DrawAngle(float radius, Color color, float thickness = 5) { float angle = joint.UpperLimit - joint.LowerLimit; - ShapeExtensions.DrawSector(spriteBatch, drawPos, radius, angle, 40, color, - offset: -rotationOffset - joint.UpperLimit + MathHelper.PiOver2, thickness: thickness); + float offset = clockWise ? rotationOffset + joint.LowerLimit - MathHelper.PiOver2 : rotationOffset - joint.UpperLimit - MathHelper.PiOver2; + ShapeExtensions.DrawSector(spriteBatch, drawPos, radius, angle, 40, color, offset: offset, thickness: thickness); } } @@ -5244,8 +5250,8 @@ namespace Barotrauma.CharacterEditor { angle = 0; } - float drawAngle = clockWise ? -angle : angle; - var widgetDrawPos = drawPos + VectorExtensions.ForwardFlipped(MathHelper.ToRadians(drawAngle) + rotationOffset, circleRadius); + float drawAngle = clockWise ? angle : -angle; + var widgetDrawPos = drawPos + VectorExtensions.Forward(MathHelper.ToRadians(drawAngle) + rotationOffset - MathHelper.PiOver2, circleRadius); GUI.DrawLine(spriteBatch, drawPos, widgetDrawPos, color); DrawWidget(spriteBatch, widgetDrawPos, WidgetType.Rectangle, widgetSize, color, toolTip, () => { @@ -5253,8 +5259,8 @@ namespace Barotrauma.CharacterEditor ShapeExtensions.DrawCircle(spriteBatch, drawPos, circleRadius, 40, color, thickness: 1); Vector2 d = PlayerInput.MousePosition - drawPos; float newAngle = clockWise - ? MathUtils.VectorToAngle(d) - MathHelper.PiOver2 + rotationOffset - : -MathUtils.VectorToAngle(d) + MathHelper.PiOver2 - rotationOffset; + ? MathUtils.VectorToAngle(d) + MathHelper.PiOver2 - rotationOffset + : -MathUtils.VectorToAngle(d) - MathHelper.PiOver2 + rotationOffset; angle = MathHelper.ToDegrees(wrapAnglePi ? MathUtils.WrapAnglePi(newAngle) : MathUtils.WrapAngleTwoPi(newAngle)); angle = (float)Math.Round(angle / rounding) * rounding; if (angle >= 360 || angle <= -360) { angle = 0; } @@ -5263,7 +5269,7 @@ namespace Barotrauma.CharacterEditor GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: color, font: GUI.SmallFont); } onClick(angle); - var zeroPos = drawPos + VectorExtensions.ForwardFlipped(rotationOffset, circleRadius); + var zeroPos = drawPos + VectorExtensions.Forward(rotationOffset - MathHelper.PiOver2, circleRadius); GUI.DrawLine(spriteBatch, drawPos, zeroPos, GUI.Style.Red, width: 3); }, autoFreeze, holdPosition, onHovered: () => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index f60568d4c..b1193a13d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -75,6 +75,17 @@ namespace Barotrauma { Character.Controlled.SelectedConstruction.AddToGUIUpdateList(); } + if (Character.Controlled?.Inventory != null) + { + foreach (Item item in Character.Controlled.Inventory.Items) + { + if (item == null) { continue; } + if (Character.Controlled.HasEquippedItem(item)) + { + item.AddToGUIUpdateList(); + } + } + } if (GameMain.GameSession != null) GameMain.GameSession.AddToGUIUpdateList(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index b40d4efda..6eb05728d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -335,6 +335,7 @@ namespace Barotrauma { base.Select(); + GUI.PreventPauseMenuToggle = false; pointerLightSource = new LightSource(Vector2.Zero, 1000.0f, Color.White, submarine: null); GameMain.LightManager.AddLight(pointerLightSource); topPanel.ClearChildren(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 4716c4780..c57281405 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -424,6 +424,8 @@ namespace Barotrauma #region Selection public override void Select() { + GUI.PreventPauseMenuToggle = false; + base.Select(); if (GameMain.Client != null) @@ -806,19 +808,6 @@ namespace Barotrauma return true; } - private bool JoinServerClicked(GUIButton button, object obj) - { - GameMain.ServerListScreen.Select(); - return true; - } - - private bool SteamWorkshopClicked(GUIButton button, object obj) - { - if (!Steam.SteamManager.IsInitialized) { return false; } - GameMain.SteamWorkshopScreen.Select(); - return true; - } - private bool ChangeMaxPlayers(GUIButton button, object obj) { int.TryParse(maxPlayersBox.Text, out int currMaxPlayers); @@ -829,33 +818,10 @@ namespace Barotrauma return true; } - private bool HostServerClicked(GUIButton button, object obj) + private void StartServer() { string name = serverNameBox.Text; - if (string.IsNullOrEmpty(name)) - { - serverNameBox.Flash(); - return false; - } - /*if (!int.TryParse(portBox.Text, out int port) || port < 0 || port > 65535) - { - portBox.Text = NetConfig.DefaultPort.ToString(); - portBox.Flash(); - - return false; - } - - int queryPort = 0; -#if USE_STEAM - if (!int.TryParse(queryPortBox.Text, out queryPort) || queryPort < 0 || queryPort > 65535) - { - portBox.Text = NetConfig.DefaultQueryPort.ToString(); - portBox.Flash(); - return false; - } -#endif - */ GameMain.NetLobbyScreen?.Release(); GameMain.NetLobbyScreen = new NetLobbyScreen(); try @@ -913,14 +879,13 @@ namespace Barotrauma ChildServerRelay.Start(processInfo); Thread.Sleep(1000); //wait until the server is ready before connecting - GameMain.Client = new GameClient(name, System.Net.IPAddress.Loopback.ToString(), Steam.SteamManager.GetSteamID(), name, ownerKey, true); + GameMain.Client = new GameClient(string.IsNullOrEmpty(GameMain.Config.PlayerName) ? name : GameMain.Config.PlayerName, + System.Net.IPAddress.Loopback.ToString(), Steam.SteamManager.GetSteamID(), name, ownerKey, true); } catch (Exception e) { DebugConsole.ThrowError("Failed to start server", e); } - - return true; } private bool QuitClicked(GUIButton button, object obj) @@ -996,7 +961,7 @@ namespace Barotrauma GUI.Draw(Cam, spriteBatch); #if !UNSTABLE - string versionString = "Barotrauma v" + GameMain.Version + " (" + AssemblyInfo.GetBuildString() + ", branch " + AssemblyInfo.GetGitBranch() + ", revision " + AssemblyInfo.GetGitRevision() + ")"; + string versionString = "Barotrauma v" + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"; GUI.SmallFont.DrawString(spriteBatch, versionString, new Vector2(HUDLayoutSettings.Padding, GameMain.GraphicsHeight - GUI.SmallFont.MeasureString(versionString).Y - HUDLayoutSettings.Padding * 0.75f), Color.White * 0.7f); #endif if (selectedTab != Tab.Credits) @@ -1062,7 +1027,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce( "MainMenuScreen.StartGame:IOException" + selectedSub.Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Copying the file \"" + selectedSub.FilePath + "\" failed.\n" + e.Message + "\n" + Environment.StackTrace); + "Copying the file \"" + selectedSub.FilePath + "\" failed.\n" + e.Message + "\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -1139,7 +1104,16 @@ namespace Barotrauma { port = settingsDoc.Root.GetAttributeInt("port", port); queryPort = settingsDoc.Root.GetAttributeInt("queryport", queryPort); - maxPlayers = settingsDoc.Root.GetAttributeInt("maxplayers", maxPlayers); + + int maxPlayersElement = settingsDoc.Root.GetAttributeInt("maxplayers", maxPlayers); + if (maxPlayersElement > NetConfig.MaxPlayers) + { + DebugConsole.IsOpen = true; + DebugConsole.NewMessage($"Setting the maximum amount of players to {maxPlayersElement} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead.", Color.Red); + maxPlayersElement = NetConfig.MaxPlayers; + } + + maxPlayers = maxPlayersElement; karmaEnabled = settingsDoc.Root.GetAttributeBool("karmaenabled", true); selectedKarmaPreset = settingsDoc.Root.GetAttributeString("karmapreset", "default"); string playStyleStr = settingsDoc.Root.GetAttributeString("playstyle", "Casual"); @@ -1334,7 +1308,35 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.4f, 0.07f), content.RectTransform), TextManager.Get("StartServerButton"), style: "GUIButtonLarge") { - OnClicked = HostServerClicked + OnClicked = (btn, userdata) => + { + string name = serverNameBox.Text; + if (string.IsNullOrEmpty(name)) + { + serverNameBox.Flash(); + return false; + } + + if (ForbiddenWordFilter.IsForbidden(name, out string forbiddenWord)) + { + var msgBox = new GUIMessageBox("", + TextManager.GetWithVariables("forbiddenservernameverification", new string[] { "[forbiddenword]", "[servername]" }, new string[] { forbiddenWord, name }), + new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + msgBox.Buttons[0].OnClicked += (_, __) => + { + StartServer(); + msgBox.Close(); + return true; + }; + msgBox.Buttons[1].OnClicked += msgBox.Close; + } + else + { + StartServer(); + } + + return true; + } }; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 02547fe7a..4f0a13b24 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -1169,7 +1169,7 @@ namespace Barotrauma Character.Controlled = null; GameMain.LightManager.LosEnabled = false; - + GUI.PreventPauseMenuToggle = false; CampaignCharacterDiscarded = false; chatInput.Select(); @@ -2799,7 +2799,7 @@ namespace Barotrauma var variantButton = CreateJobVariantButton(jobPrefab, variantIndex, images.Length, jobButton); variantButton.OnClicked = (btn, obj) => { - currSelected.Selected = false; + if (currSelected != null) { currSelected.Selected = false; } int k = ((Pair)obj).Second; btn.Parent.UserData = obj; for (int j = 0; j < images.Length; j++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index b6eb664b3..73f672fdb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -153,6 +153,7 @@ namespace Barotrauma private GUITickBox filterVoip; private List playStyleTickBoxes; private List gameModeTickBoxes; + private GUITickBox filterOffensive; private string sortedBy; @@ -325,7 +326,7 @@ namespace Barotrauma filterSameVersion = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterSameVersion")) { - ToolTip = TextManager.Get("FilterSameVersion"), + UserData = TextManager.Get("FilterSameVersion"), Selected = true, OnSelected = (tickBox) => { FilterServers(); return true; } }; @@ -333,39 +334,47 @@ namespace Barotrauma filterPassword = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterPassword")) { - ToolTip = TextManager.Get("FilterPassword"), + UserData = TextManager.Get("FilterPassword"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterPassword.TextBlock); filterIncompatible = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterIncompatibleServers")) { - ToolTip = TextManager.Get("FilterIncompatibleServers"), + UserData = TextManager.Get("FilterIncompatibleServers"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterIncompatible.TextBlock); filterFull = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterFullServers")) { - ToolTip = TextManager.Get("FilterFullServers"), + UserData = TextManager.Get("FilterFullServers"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterFull.TextBlock); filterEmpty = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterEmptyServers")) { - ToolTip = TextManager.Get("FilterEmptyServers"), + UserData = TextManager.Get("FilterEmptyServers"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterEmpty.TextBlock); filterWhitelisted = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterWhitelistedServers")) { - ToolTip = TextManager.Get("FilterWhitelistedServers"), + UserData = TextManager.Get("FilterWhitelistedServers"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterWhitelisted.TextBlock); + filterOffensive = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterOffensiveServers")) + { + UserData = TextManager.Get("FilterOffensiveServers"), + ToolTip = TextManager.Get("FilterOffensiveServersToolTip"), + OnSelected = (tickBox) => { FilterServers(); return true; } + }; + filterTextList.Add(filterOffensive.TextBlock); + // Filter Tags new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("servertags"), font: GUI.SubHeadingFont) { @@ -374,35 +383,35 @@ namespace Barotrauma filterKarma = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.karma.true")) { - ToolTip = TextManager.Get("servertag.karma.true"), + UserData = TextManager.Get("servertag.karma.true"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterKarma.TextBlock); filterTraitor = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.traitors.true")) { - ToolTip = TextManager.Get("servertag.traitors.true"), + UserData = TextManager.Get("servertag.traitors.true"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterTraitor.TextBlock); filterFriendlyFire = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.friendlyfire.false")) { - ToolTip = TextManager.Get("servertag.friendlyfire.false"), + UserData = TextManager.Get("servertag.friendlyfire.false"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterFriendlyFire.TextBlock); filterVoip = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.voip.false")) { - ToolTip = TextManager.Get("servertag.voip.false"), + UserData = TextManager.Get("servertag.voip.false"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterVoip.TextBlock); filterModded = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.modded.true")) { - ToolTip = TextManager.Get("servertag.modded.true"), + UserData = TextManager.Get("servertag.modded.true"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterModded.TextBlock); @@ -449,7 +458,9 @@ namespace Barotrauma filters.Content.RectTransform.SizeChanged += () => { filters.Content.RectTransform.RecalculateChildren(true, true); - filterTextList.ForEach(t => t.Text = t.ToolTip); + filterTextList.ForEach(t => t.Text = t.Parent.Parent.UserData as string); + gameModeTickBoxes.ForEach(tb => tb.Text = tb.ToolTip); + playStyleTickBoxes.ForEach(tb => tb.Text = tb.ToolTip); GUITextBlock.AutoScaleAndNormalize(filterTextList, defaultScale: 1.0f); if (filterTextList[0].TextScale < 0.8f) { @@ -579,7 +590,7 @@ namespace Barotrauma { ClientNameBox.Flash(); ClientNameBox.Select(); - GUI.PlayUISound(GUISoundType.PickItemFail); + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); return false; } ShowDirectJoinPrompt(); @@ -1050,6 +1061,7 @@ namespace Barotrauma (!filterFull.Selected || serverInfo.PlayerCount < serverInfo.MaxPlayers) && (!filterEmpty.Selected || serverInfo.PlayerCount > 0) && (!filterWhitelisted.Selected || serverInfo.UsingWhiteList == true) && + (filterOffensive.Selected || !ForbiddenWordFilter.IsForbidden(serverInfo.ServerName)) && (!filterKarma.Selected || serverInfo.KarmaEnabled == true) && (!filterFriendlyFire.Selected || serverInfo.FriendlyFireEnabled == false) && (!filterTraitor.Selected || serverInfo.TraitorsEnabled == YesNoMaybe.Yes || serverInfo.TraitorsEnabled == YesNoMaybe.Maybe) && @@ -1406,7 +1418,7 @@ namespace Barotrauma #if DEBUG DebugConsole.ThrowError($"Failed to parse a Steam friend's connect command ({connectCommand})", e); #else - DebugConsole.Log($"Failed to parse a Steam friend's connect command ({connectCommand})\n" + e.StackTrace); + DebugConsole.Log($"Failed to parse a Steam friend's connect command ({connectCommand})\n" + e.StackTrace.CleanupStackTrace()); #endif info.ConnectName = null; info.ConnectEndpoint = null; @@ -2109,7 +2121,7 @@ namespace Barotrauma { ClientNameBox.Flash(); ClientNameBox.Select(); - GUI.PlayUISound(GUISoundType.PickItemFail); + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs index 5c9ad31fc..274a3da32 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs @@ -1156,7 +1156,7 @@ namespace Barotrauma if (itemContentPackage == null) { - string errorMsg = "Failed to edit workshop item (content package null)\n" + Environment.StackTrace; + string errorMsg = "Failed to edit workshop item (content package null)\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("SteamWorkshopScreen.ShowCreateItemFrame:ContentPackageNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 4b6c4a82e..c5eb94a9d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -33,6 +33,18 @@ namespace Barotrauma Default, Wiring } + + public enum WarningType + { + NoWaypoints, + NoHulls, + DisconnectedVents, + NoHumanSpawnpoints, + NoCargoSpawnpoints, + NoBallastTag, + NonLinkedGaps, + TooManyLights + } public static Vector2 MouseDragStart = Vector2.Zero; @@ -85,11 +97,19 @@ namespace Barotrauma private GUIFrame previouslyUsedPanel; private GUIListBox previouslyUsedList; + private GUIFrame undoBufferPanel; + private GUIListBox undoBufferList; + private GUIDropDown linkedSubBox; private static GUIComponent autoSaveLabel; private static int maxAutoSaves = GameSettings.MaximumAutoSaves; + public static bool BulkItemBufferInUse; + public static List BulkItemBuffer = new List(); + + public static List SuppressedWarnings = new List(); + //a Character used for picking up and manipulating items private Character dummyCharacter; @@ -109,12 +129,17 @@ namespace Barotrauma /// private Vector2 oldItemPosition; + /// + /// Global undo/redo state for the sub editor and a selector index for it + /// + /// + public static readonly List Commands = new List(); + private static int commandIndex; + private GUIFrame wiringToolPanel; private DateTime editorSelectedTime; - private const string containerDeleteTag = "containerdelete"; - private GUIImage previewImage; private GUILayoutGroup previewImageButtonHolder; @@ -273,6 +298,7 @@ namespace Barotrauma OnClicked = (btn, userData) => { previouslyUsedPanel.Visible = false; + undoBufferPanel.Visible = false; showEntitiesPanel.Visible = !showEntitiesPanel.Visible; showEntitiesPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(btn.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); return true; @@ -285,12 +311,26 @@ namespace Barotrauma OnClicked = (btn, userData) => { showEntitiesPanel.Visible = false; + undoBufferPanel.Visible = false; previouslyUsedPanel.Visible = !previouslyUsedPanel.Visible; previouslyUsedPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(btn.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); return true; } }; + var undoBufferButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "UndoHistoryButton") + { + ToolTip = TextManager.Get("Editor.UndoHistoryButton"), + OnClicked = (btn, userData) => + { + showEntitiesPanel.Visible = false; + previouslyUsedPanel.Visible = false; + undoBufferPanel.Visible = !undoBufferPanel.Visible; + undoBufferPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(btn.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); + return true; + } + }; + new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), paddedTopPanel.RectTransform), style: "VerticalLine"); subNameLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 0.9f), paddedTopPanel.RectTransform, Anchor.CenterLeft), @@ -407,6 +447,45 @@ namespace Barotrauma //----------------------------------------------- + undoBufferPanel = new GUIFrame(new RectTransform(new Vector2(0.15f, 0.2f), GUI.Canvas) { MinSize = new Point(200, 200) }) + { + Visible = false + }; + undoBufferList = new GUIListBox(new RectTransform(new Vector2(0.925f, 0.9f), undoBufferPanel.RectTransform, Anchor.Center)) + { + ScrollBarVisible = true, + OnSelected = (_, userData) => + { + int index; + if (userData is Command command) + { + index = Commands.IndexOf(command); + } + else + { + index = -1; + } + + int diff = index- commandIndex; + int amount = Math.Abs(diff); + + if (diff >= 0) + { + Redo(amount + 1); + } + else + { + Undo(amount - 1); + } + + return true; + } + }; + + UpdateUndoHistoryPanel(); + + //----------------------------------------------- + showEntitiesPanel = new GUIFrame(new RectTransform(new Vector2(0.08f, 0.5f), GUI.Canvas) { MinSize = new Point(170, 0) @@ -885,6 +964,7 @@ namespace Barotrauma { base.Select(); + GUI.PreventPauseMenuToggle = false; if (!Directory.Exists(autoSavePath)) { System.IO.DirectoryInfo e = Directory.CreateDirectory(autoSavePath); @@ -1043,6 +1123,7 @@ namespace Barotrauma MapEntity.DeselectAll(); MapEntity.SelectionGroups.Clear(); + ClearUndoBuffer(); SetMode(Mode.Default); @@ -1062,147 +1143,15 @@ namespace Barotrauma GameMain.World.ProcessChanges(); } - if (GUIMessageBox.MessageBoxes.Any(mbox => (mbox as GUIMessageBox)?.Tag == containerDeleteTag)) - { - for (int i = 0; i < GUIMessageBox.MessageBoxes.Count; i++) - { - GUIMessageBox box = GUIMessageBox.MessageBoxes[i] as GUIMessageBox; - if (box != null && box.Tag != containerDeleteTag) continue; - box?.Close(); - i--; // Take into account the message boxes removing themselves from the list when closed - } - } ClearFilter(); } - public void HandleContainerContentsDeletion(Item itemToDelete, Inventory itemInventory) - { - string itemNames = string.Empty; - - foreach (Item item in itemInventory.Items) - { - if (item == null) continue; - itemNames += item.Name + "\n"; - } - - if (itemNames.Length > 0) - { - // Multiple prompts open - if (GUIMessageBox.MessageBoxes.Any(mbox => (mbox as GUIMessageBox)?.Tag == containerDeleteTag)) - { - var msgBox = new GUIMessageBox(itemToDelete.Name, TextManager.Get("DeletingContainerWithItems") + itemNames, new[] { TextManager.Get("Yes"), TextManager.Get("No"), TextManager.Get("YesToAll"), TextManager.Get("NoToAll") }, tag: containerDeleteTag); - - // Yes - msgBox.Buttons[0].OnClicked = (btn, userdata) => - { - itemInventory.DeleteAllItems(); - msgBox.Close(); - return true; - }; - - // No - msgBox.Buttons[1].OnClicked = (btn, userdata) => - { - if (Selected == GameMain.SubEditorScreen) - { - foreach (Item item in itemInventory.Items) - { - item?.Drop(null); - } - } - else // If current screen is not subeditor, delete anyway to avoid lingering objects - { - itemInventory.DeleteAllItems(); - } - - msgBox.Close(); - return true; - }; - - // Yes to All - msgBox.Buttons[2].OnClicked = (btn, userdata) => - { - for (int i = 0; i < GUIMessageBox.MessageBoxes.Count; i++) - { - GUIMessageBox box = GUIMessageBox.MessageBoxes[i] as GUIMessageBox; - if (box?.Tag != msgBox.Tag || box == msgBox) continue; - GUIButton button = box?.Buttons[0]; - button?.OnClicked(button, button.UserData); - i--; // Take into account the message boxes removing themselves from the list when closed - } - - itemInventory.DeleteAllItems(); - msgBox.Close(); - return true; - }; - - // No to all - msgBox.Buttons[3].OnClicked = (btn, userdata) => - { - for (int i = 0; i < GUIMessageBox.MessageBoxes.Count; i++) - { - GUIMessageBox box = GUIMessageBox.MessageBoxes[i] as GUIMessageBox; - if (box?.Tag != msgBox.Tag || box == msgBox) continue; - GUIButton button = box?.Buttons[1]; - button?.OnClicked(button, button.UserData); - i--; // Take into account the message boxes removing themselves from the list when closed - } - - if (Selected == GameMain.SubEditorScreen) - { - foreach (Item item in itemInventory.Items) - { - item?.Drop(null); - } - } - else // If current screen is not subeditor, delete anyway to avoid lingering objects - { - itemInventory.DeleteAllItems(); - } - - msgBox.Close(); - return true; - }; - } - else // Single prompt - { - var msgBox = new GUIMessageBox(itemToDelete.Name, TextManager.Get("DeletingContainerWithItems") + itemNames, new[] { TextManager.Get("Yes"), TextManager.Get("No") }, tag: containerDeleteTag); - - // Yes - msgBox.Buttons[0].OnClicked = (btn, userdata) => - { - itemInventory.DeleteAllItems(); - msgBox.Close(); - return true; - }; - - // No - msgBox.Buttons[1].OnClicked = (btn, userdata) => - { - if (Selected == GameMain.SubEditorScreen) - { - foreach (Item item in itemInventory.Items) - { - item?.Drop(null); - } - } - else // If current screen is not subeditor, delete anyway to avoid lingering objects - { - itemInventory.DeleteAllItems(); - } - - msgBox.Close(); - return true; - }; - } - } - } - private void CreateDummyCharacter() { if (dummyCharacter != null) RemoveDummyCharacter(); dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", hasAi: false); + dummyCharacter.Info.Name = "Galldren"; //make space for the entity menu for (int i = 0; i < dummyCharacter.Inventory.SlotPositions.Length; i++) @@ -2460,7 +2409,6 @@ namespace Barotrauma 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) { - Enabled = File.Exists(Path.Combine(SubmarineInfo.SavePath, ".AutoSaves", "AutoSave.sub")), ToolTip = TextManager.Get("LoadAutoSaveTooltip"), UserData = "loadautosave", OnSelected = (button, o) => @@ -2609,7 +2557,7 @@ namespace Barotrauma var selectedSub = new Submarine(selectedSubInfo); Submarine.MainSub = selectedSub; Submarine.MainSub.UpdateTransform(interpolate: false); - + ClearUndoBuffer(); CreateDummyCharacter(); string name = Submarine.MainSub.Info.Name; @@ -2790,6 +2738,7 @@ namespace Barotrauma MapEntity.DeselectAll(); MapEntity.FilteredSelectedList.Clear(); + ClearUndoBuffer(); CreateDummyCharacter(); if (newMode == Mode.Wiring) @@ -2922,7 +2871,7 @@ namespace Barotrauma CreateBackgroundColorPicker(); break; case "selectsame": - IEnumerable matching = MapEntity.mapEntityList.Where(e => targets.Any(t => t.prefab.Identifier == e.prefab.Identifier) && !MapEntity.SelectedList.Contains(e)); + IEnumerable matching = MapEntity.mapEntityList.Where(e => e.prefab != null && targets.Any(t => t.prefab.Identifier == e.prefab.Identifier) && !MapEntity.SelectedList.Contains(e)); MapEntity.SelectedList.AddRange(matching); break; case "copy": @@ -2935,7 +2884,8 @@ namespace Barotrauma MapEntity.Paste(cam.ScreenToWorld(contextMenu.Rect.Location.ToVector2())); break; case "delete": - targets.ForEach(me => { me.Remove(); }); + StoreCommand(new AddOrDeleteCommand(targets, true)); + targets.ForEach(me => { if (!me.Removed) { me.Remove(); }}); break; case "open" when target != null: OpenItem(target); @@ -3234,7 +3184,13 @@ namespace Barotrauma } } }); - GUI.PlayUISound(spawnedItem ? GUISoundType.PickItem : GUISoundType.PickItemFail); + + List placedEntities = itemInstance.Where(it => !it.Removed).Cast().ToList(); + if (placedEntities.Any()) + { + StoreCommand(new AddOrDeleteCommand(placedEntities, false)); + } + SoundPlayer.PlayUISound(spawnedItem ? GUISoundType.PickItem : GUISoundType.PickItemFail); break; } case ItemPrefab itemPrefab when PlayerInput.IsShiftDown(): @@ -3243,12 +3199,17 @@ namespace Barotrauma if (!inv.TryPutItem(item, dummyCharacter)) { // We failed, remove the item so it doesn't stay at x:0,y:0 - GUI.PlayUISound(GUISoundType.PickItemFail); + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); item.Remove(); } else { - GUI.PlayUISound(GUISoundType.PickItem); + SoundPlayer.PlayUISound(GUISoundType.PickItem); + } + + if (!item.Removed) + { + StoreCommand(new AddOrDeleteCommand(new List { item }, false)); } break; } @@ -3257,7 +3218,7 @@ namespace Barotrauma { // Place the item into our hands DraggedItemPrefab = (MapEntityPrefab) obj; - GUI.PlayUISound(GUISoundType.PickItem); + SoundPlayer.PlayUISound(GUISoundType.PickItem); break; } } @@ -3265,7 +3226,7 @@ namespace Barotrauma } else { - GUI.PlayUISound(GUISoundType.PickItem); + SoundPlayer.PlayUISound(GUISoundType.PickItem); MapEntityPrefab.SelectPrefab(obj); } @@ -3611,6 +3572,7 @@ namespace Barotrauma EntityMenu.AddToGUIUpdateList(); showEntitiesPanel.AddToGUIUpdateList(); previouslyUsedPanel.AddToGUIUpdateList(); + undoBufferPanel.AddToGUIUpdateList(); entityCountPanel.AddToGUIUpdateList(); TopPanel.AddToGUIUpdateList(); @@ -3655,10 +3617,7 @@ namespace Barotrauma /// private bool IsMouseOnEditorGUI() { - if (GUI.MouseOn == null) - { - return false; - } + if (GUI.MouseOn == null) { return false; } return (EntityMenu?.MouseRect.Contains(PlayerInput.MousePosition) ?? false) || (entityCountPanel?.MouseRect.Contains(PlayerInput.MousePosition) ?? false) @@ -3666,6 +3625,91 @@ namespace Barotrauma || (TopPanel?.MouseRect.Contains(PlayerInput.MousePosition) ?? false); } + private static void Redo(int amount) + { + for (int i = 0; i < amount; i++) + { + if (commandIndex < Commands.Count) + { + Command command = Commands[commandIndex++]; + command.Execute(); + } + } + GameMain.SubEditorScreen.UpdateUndoHistoryPanel(); + } + + private static void Undo(int amount) + { + for (int i = 0; i < amount; i++) + { + if (commandIndex > 0) + { + Command command = Commands[--commandIndex]; + command.UnExecute(); + } + } + GameMain.SubEditorScreen.UpdateUndoHistoryPanel(); + } + + private static void ClearUndoBuffer() + { + SerializableEntityEditor.PropertyChangesActive = false; + SerializableEntityEditor.CommandBuffer = null; + Commands.ForEach(cmd => cmd.Cleanup()); + Commands.Clear(); + commandIndex = 0; + GameMain.SubEditorScreen.UpdateUndoHistoryPanel(); + } + + public static void StoreCommand(Command command) + { + if (commandIndex != Commands.Count) + { + Commands.RemoveRange(commandIndex, Commands.Count - commandIndex); + } + Commands.Add(command); + commandIndex++; + + // Start removing old commands + if (Commands.Count > Math.Clamp(GameSettings.SubEditorMaxUndoBuffer, 1, 10240)) + { + Commands.First()?.Cleanup(); + Commands.RemoveRange(0, 1); + commandIndex = Commands.Count; + } + + GameMain.SubEditorScreen.UpdateUndoHistoryPanel(); + } + + public void UpdateUndoHistoryPanel() + { + if (undoBufferPanel == null) { return; } + + undoBufferList.Content.Children.ForEachMod(component => + { + undoBufferList.Content.RemoveChild(component); + }); + + for (int i = 0; i < Commands.Count; i++) + { + Command command = Commands[i]; + string description = command.GetDescription(); + CreateTextBlock(description, description, i + 1, command).RectTransform.SetAsFirstChild(); + } + + CreateTextBlock(TextManager.Get("undo.beginning"), TextManager.Get("undo.beginningtooltip"), 0, null); + + GUITextBlock CreateTextBlock(string name, string description, int index, Command command) + { + return new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), undoBufferList.Content.RectTransform) { MinSize = new Point(0, 15) }, + ToolBox.LimitString(name, GUI.SmallFont, undoBufferList.Content.Rect.Width), font: GUI.SmallFont, textColor: index == commandIndex ? GUI.Style.Green : (Color?) null) + { + UserData = command, + ToolTip = description + }; + } + } + /// /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. @@ -3745,7 +3789,32 @@ namespace Barotrauma } } } - + + if (undoBufferPanel.Visible) + { + undoBufferList.Deselect(); + } + + if (GUI.KeyboardDispatcher.Subscriber == null + || MapEntity.EditingHUD != null + && GUI.KeyboardDispatcher.Subscriber is GUIComponent sub + && MapEntity.EditingHUD.Children.Contains(sub)) + { + if (PlayerInput.IsCtrlDown() && !WiringMode) + { + if (PlayerInput.KeyHit(Keys.Z)) + { + // Ctrl+Shift+Z redos while Ctrl+Z undos + if (PlayerInput.IsShiftDown()) { Redo(1); } else { Undo(1); } + } + + // ctrl+Y redo + if (PlayerInput.KeyHit(Keys.Y)) + { + Redo(1); + } + } + } if (GUI.KeyboardDispatcher.Subscriber == null) { @@ -4019,9 +4088,15 @@ namespace Barotrauma newItem.Remove(); } + if (!newItem.Removed) + { + BulkItemBufferInUse = true; + BulkItemBuffer.Add(new AddOrDeleteCommand(new List { newItem }, false)); + } + if (!dragginMouse) { - GUI.PlayUISound(spawnedItem ? GUISoundType.PickItem : GUISoundType.PickItemFail); + SoundPlayer.PlayUISound(spawnedItem ? GUISoundType.PickItem : GUISoundType.PickItemFail); } } } @@ -4083,14 +4158,41 @@ namespace Barotrauma } } } + + List placedEntities = itemInstance.Where(it => !it.Removed).Cast().ToList(); + if (placedEntities.Any()) + { + BulkItemBufferInUse = true; + BulkItemBuffer.Add(new AddOrDeleteCommand(placedEntities, false)); + } } } - GUI.PlayUISound(spawnedItems ? GUISoundType.PickItem : GUISoundType.PickItemFail); + + SoundPlayer.PlayUISound(spawnedItems ? GUISoundType.PickItem : GUISoundType.PickItemFail); break; } } } + if (BulkItemBufferInUse && PlayerInput.PrimaryMouseButtonReleased() && BulkItemBuffer.Any()) + { + AddOrDeleteCommand master = BulkItemBuffer[0]; + for (int i = 1; i < BulkItemBuffer.Count; i++) + { + AddOrDeleteCommand command = BulkItemBuffer[i]; + command.MergeInto(master); + } + + StoreCommand(master); + BulkItemBuffer.Clear(); + BulkItemBufferInUse = false; + } + + if (SerializableEntityEditor.PropertyChangesActive && (SerializableEntityEditor.NextCommandPush < DateTime.Now || MapEntity.EditingHUD == null)) + { + SerializableEntityEditor.CommitCommandBuffer(); + } + // Update our mouse dragging state so we can easily slide thru slots while holding the mouse button down to place lots of items if (PlayerInput.PrimaryMouseButtonHeld()) { @@ -4273,9 +4375,18 @@ namespace Barotrauma Submarine.DrawFront(spriteBatch, editing: true, e => ShowThalamus || !(e.prefab?.Category.HasFlag(MapEntityCategory.Thalamus) ?? false)); if (!WiringMode && !IsMouseOnEditorGUI()) { - MapEntityPrefab.Selected?.DrawPlacing(spriteBatch, cam); + MapEntityPrefab.Selected?.DrawPlacing(spriteBatch, cam); MapEntity.DrawSelecting(spriteBatch, cam); } + if (dummyCharacter != null && WiringMode) + { + for (int i = 0; i < dummyCharacter.SelectedItems.Length; i++) + { + if (dummyCharacter.SelectedItems[i] == null) { continue; } + if (i > 0 && dummyCharacter.SelectedItems[0] == dummyCharacter.SelectedItems[i]) { continue; } + dummyCharacter.SelectedItems[i].Draw(spriteBatch, editing: false, back: true); + } + } spriteBatch.End(); if (GameMain.LightManager.LightingEnabled && lightingEnabled) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 8a2082e58..9dca64179 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -18,6 +18,11 @@ namespace Barotrauma public static List MissingLocalizations = new List(); #endif + public static bool LockEditing; + public static bool PropertyChangesActive; + public static DateTime NextCommandPush; + public static Tuple CommandBuffer; + public int ContentHeight { get @@ -1091,10 +1096,67 @@ namespace Barotrauma private bool SetPropertyValue(SerializableProperty property, object entity, object value) { - MultiSetProperties(property, entity, value); + if (LockEditing) { return false; } + + object oldData = property.GetValue(entity); + // some properties have null as the default string value + if (oldData == null && value is string) { oldData = ""; } + if (entity is ISerializableEntity sEntity && Screen.Selected is SubEditorScreen && !Equals(oldData, value)) + { + List entities = new List { sEntity }; + Dictionary affected = MultiSetProperties(property, entity, value); + + Dictionary> oldValues = new Dictionary> {{ oldData, new List { sEntity }}}; + + affected.ForEach(aEntity => + { + var (item, oldVal) = aEntity; + entities.Add(item); + + if (!oldValues.ContainsKey(oldVal)) + { + oldValues.Add(oldVal, new List { item }); + } + else + { + oldValues[oldVal].Add(item); + } + }); + + PropertyCommand cmd = new PropertyCommand(entities, property.Name, value, oldValues); + if (CommandBuffer != null) + { + if (CommandBuffer.Item1 == property && CommandBuffer.Item2.PropertyCount == cmd.PropertyCount) + { + if (!CommandBuffer.Item2.MergeInto(cmd)) + { + CommitCommandBuffer(); + } + } + else + { + CommitCommandBuffer(); + } + } + + NextCommandPush = DateTime.Now.AddSeconds(1); + CommandBuffer = Tuple.Create(property, cmd); + PropertyChangesActive = true; + } + return property.TrySetValue(entity, value); } + public static void CommitCommandBuffer() + { + if (CommandBuffer != null) + { + SubEditorScreen.StoreCommand(CommandBuffer.Item2); + } + CommandBuffer = null; + PropertyChangesActive = false; + } + /// /// Sets common shared properties to all selected map entities in sub editor. /// Only works client side while in the sub editor and when parentObject is ItemComponent, Item or Structure. @@ -1103,10 +1165,12 @@ namespace Barotrauma /// /// /// The function has the same parameters as - private void MultiSetProperties(SerializableProperty property, object parentObject, object value) + private Dictionary MultiSetProperties(SerializableProperty property, object parentObject, object value) { - if (!(Screen.Selected is SubEditorScreen) || MapEntity.SelectedList.Count <= 1) { return; } - if (!(parentObject is ItemComponent || parentObject is Item || parentObject is Structure || parentObject is Hull)) { return; } + Dictionary affected = new Dictionary(); + + if (!(Screen.Selected is SubEditorScreen) || MapEntity.SelectedList.Count <= 1) { return affected; } + if (!(parentObject is ItemComponent || parentObject is Item || parentObject is Structure || parentObject is Hull)) { return affected; } foreach (var entity in MapEntity.SelectedList.Where(entity => entity != parentObject)) { @@ -1115,35 +1179,36 @@ namespace Barotrauma case Hull _: case Structure _: case Item _: - { if (entity.GetType() == parentObject.GetType()) - { + { + affected.Add((ISerializableEntity) entity, property.GetValue(entity)); property.PropertyInfo.SetValue(entity, value); } else if (entity is ISerializableEntity sEntity && sEntity.SerializableProperties != null) { var props = sEntity.SerializableProperties; - + if (props.TryGetValue(property.NameToLowerInvariant, out SerializableProperty foundProp)) { + affected.Add(sEntity, foundProp.GetValue(sEntity)); foundProp.PropertyInfo.SetValue(entity, value); } } break; - } case ItemComponent _ when entity is Item item: - { foreach (var component in item.Components) { if (component.GetType() == parentObject.GetType() && component != parentObject) - { + { + affected.Add(component, property.GetValue(component)); property.PropertyInfo.SetValue(component, value); } } break; - } } } + + return affected; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index b47541a36..48a2cc7f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -275,7 +275,7 @@ namespace Barotrauma.Sounds { if (value < 0.25f || value > 4.0f) { - DebugConsole.ThrowError($"Frequency multiplier out of range: {value}" + Environment.StackTrace); + DebugConsole.ThrowError($"Frequency multiplier out of range: {value}" + Environment.StackTrace.CleanupStackTrace()); } frequencyMultiplier = Math.Clamp(value, 0.25f, 4.0f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index 255a8e072..03420baa7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -730,6 +730,7 @@ namespace Barotrauma.Sounds else { playingChannels[i][j].Dispose(); + playingChannels[i][j] = null; } } else if (playingChannels[i][j].FadingOutAndDisposing) @@ -738,6 +739,7 @@ namespace Barotrauma.Sounds if (playingChannels[i][j].Gain <= 0.0f) { playingChannels[i][j].Dispose(); + playingChannels[i][j] = null; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 8709bd52d..e3eb85d5b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -74,12 +74,18 @@ namespace Barotrauma //ambience private static Sound waterAmbienceIn, waterAmbienceOut, waterAmbienceMoving; - private static SoundChannel[] waterAmbienceChannels = new SoundChannel[3]; + private static readonly SoundChannel[] waterAmbienceChannels = new SoundChannel[3]; private static float ambientSoundTimer; private static Vector2 ambientSoundInterval = new Vector2(20.0f, 40.0f); //x = min, y = max + private static SoundChannel hullSoundChannel; + private static Hull hullSoundSource; + private static float hullSoundTimer; + private static Vector2 hullSoundInterval = new Vector2(45.0f, 90.0f); //x = min, y = max + //misc + private static float[] targetFlowLeft, targetFlowRight; public static List FlowSounds = new List(); public static List SplashSounds = new List(); private static SoundChannel[] flowSoundChannels; @@ -94,11 +100,18 @@ namespace Barotrauma private static float[] fireVolumeRight; const float FireSoundRange = 1000.0f; + const float FireSoundMediumLimit = 100.0f; const float FireSoundLargeLimit = 200.0f; //switch to large fire sound when the size of a firesource is above this + const int fireSizes = 3; + private static string[] fireSoundTags = new string[fireSizes] { "fire", "firemedium", "firelarge" }; // TODO: could use a dictionary to split up the list into smaller lists of same type? private static List damageSounds; + private static Dictionary> guiSounds; + + private static bool firstTimeInMainMenu = true; + private static Sound startUpSound; public static bool Initialized; @@ -163,6 +176,7 @@ namespace Barotrauma List> miscSoundList = new List>(); damageSounds ??= new List(); musicClips ??= new List(); + guiSounds ??= new Dictionary>(); bool firstWaterAmbienceLoaded = false; @@ -250,6 +264,21 @@ namespace Barotrauma damageSoundType, soundElement.GetAttributeString("requiredtag", ""))); + break; + case "guisound": + Sound guiSound = GameMain.SoundManager.LoadSound(soundElement, stream: false); + if (guiSound == null) { continue; } + if (Enum.TryParse(soundElement.GetAttributeString("guisoundtype", null), true, out GUISoundType soundType)) + { + if (guiSounds.ContainsKey(soundType)) + { + guiSounds[soundType].Add(guiSound); + } + else + { + guiSounds.Add(soundType, new List() { guiSound }); + } + } break; default: Sound sound = GameMain.SoundManager.LoadSound(soundElement, false); @@ -294,20 +323,32 @@ namespace Barotrauma }); damageSounds.RemoveAll(s => s.sound.Disposed); + guiSounds.ForEach(kvp => + { + kvp.Value?.ForEach(s => + { + if (!soundElements.Any(e => SoundElementsEquivalent(s.XElement, e))) { s.Dispose(); } + }); + }); + guiSounds.ForEach(kvp => kvp.Value?.RemoveAll(s => s.Disposed)); + miscSounds?.ForEach(g => g.ForEach(s => { if (!soundElements.Any(e => SoundElementsEquivalent(s.XElement, e))) { s.Dispose(); } else { miscSoundList.Add(new KeyValuePair(g.Key, s)); } })); - + flowSoundChannels?.ForEach(ch => ch?.Dispose()); flowSoundChannels = new SoundChannel[FlowSounds.Count]; flowVolumeLeft = new float[FlowSounds.Count]; flowVolumeRight = new float[FlowSounds.Count]; + targetFlowLeft = new float[FlowSounds.Count]; + targetFlowRight = new float[FlowSounds.Count]; - fireSoundChannels = new SoundChannel[2]; - fireVolumeLeft = new float[2]; - fireVolumeRight = new float[2]; + fireSoundChannels?.ForEach(ch => ch?.Dispose()); + fireSoundChannels = new SoundChannel[fireSizes]; + fireVolumeLeft = new float[fireSizes]; + fireVolumeRight = new float[fireSizes]; miscSounds = miscSoundList.ToLookup(kvp => kvp.Key, kvp => kvp.Value); @@ -319,7 +360,6 @@ namespace Barotrauma } - public static void Update(float deltaTime) { if (!Initialized) { return; } @@ -355,6 +395,12 @@ namespace Barotrauma } fireVolumeLeft[0] = 0.0f; fireVolumeLeft[1] = 0.0f; fireVolumeRight[0] = 0.0f; fireVolumeRight[1] = 0.0f; + if (hullSoundChannel != null) + { + hullSoundChannel.FadeOutAndDispose(); + hullSoundChannel = null; + hullSoundSource = null; + } return; } @@ -372,12 +418,13 @@ namespace Barotrauma UpdateWaterAmbience(ambienceVolume, deltaTime); UpdateWaterFlowSounds(deltaTime); UpdateRandomAmbience(deltaTime); + UpdateHullSounds(deltaTime); UpdateFireSounds(deltaTime); } private static void UpdateWaterAmbience(float ambienceVolume, float deltaTime) { - if (GameMain.SoundManager.Disabled) { return; } + if (GameMain.SoundManager.Disabled || GameMain.GameScreen?.Cam == null) { return; } //how fast the sub is moving, scaled to 0.0 -> 1.0 float movementSoundVolume = 0.0f; @@ -385,6 +432,7 @@ namespace Barotrauma float insideSubFactor = 0.0f; foreach (Submarine sub in Submarine.Loaded) { + if (sub == null || sub.Removed) { continue; } float movementFactor = (sub.Velocity == Vector2.Zero) ? 0.0f : sub.Velocity.Length() / 10.0f; movementFactor = MathHelper.Clamp(movementFactor, 0.0f, 1.0f); @@ -429,6 +477,9 @@ namespace Barotrauma break; } + // Consider the volume set in sounds.xml + if (sound != null) { volume *= sound.BaseGain; } + if ((waterAmbienceChannels[i] == null || !waterAmbienceChannels[i].IsPlaying) && volume > 0.01f) { waterAmbienceChannels[i] = sound.Play(volume, "waterambience"); @@ -448,9 +499,12 @@ namespace Barotrauma private static void UpdateWaterFlowSounds(float deltaTime) { if (FlowSounds.Count == 0) { return; } - - float[] targetFlowLeft = new float[FlowSounds.Count]; - float[] targetFlowRight = new float[FlowSounds.Count]; + + for (int i = 0; i < targetFlowLeft.Length; i++) + { + targetFlowLeft[i] = 0.0f; + targetFlowRight[i] = 0.0f; + } Vector2 listenerPos = new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y); foreach (Gap gap in Gap.GapList) @@ -490,6 +544,20 @@ namespace Barotrauma } } + if (Character.Controlled?.CharacterHealth?.GetAffliction("psychosis") is AfflictionPsychosis psychosis) + { + if (psychosis.CurrentFloodType == AfflictionPsychosis.FloodType.Minor) + { + targetFlowLeft[0] = Math.Max(targetFlowLeft[0], 1.0f); + targetFlowRight[0] = Math.Max(targetFlowRight[0], 1.0f); + } + else if (psychosis.CurrentFloodType == AfflictionPsychosis.FloodType.Major) + { + targetFlowLeft[FlowSounds.Count - 1] = Math.Max(targetFlowLeft[FlowSounds.Count - 1], 1.0f); + targetFlowRight[FlowSounds.Count - 1] = Math.Max(targetFlowRight[FlowSounds.Count - 1], 1.0f); + } + } + for (int i = 0; i < FlowSounds.Count; i++) { flowVolumeLeft[i] = (targetFlowLeft[i] < flowVolumeLeft[i]) ? @@ -534,33 +602,11 @@ namespace Barotrauma { foreach (FireSource fs in hull.FireSources) { - Vector2 diff = fs.WorldPosition + fs.Size / 2 - listenerPos; - if (Math.Abs(diff.X) < FireSoundRange && Math.Abs(diff.Y) < FireSoundRange) - { - Vector2 diffLeft = (fs.WorldPosition + new Vector2(fs.Size.X, fs.Size.Y / 2)) - listenerPos; - if (Math.Abs(diff.X) < fs.Size.X / 2.0f) { diffLeft.X = 0.0f; } - if (diffLeft.X <= 0) - { - float distFallOffLeft = diffLeft.Length() / FireSoundRange; - if (distFallOffLeft < 0.99f) - { - fireVolumeLeft[0] += (1.0f - distFallOffLeft); - if (fs.Size.X > FireSoundLargeLimit) fireVolumeLeft[1] += (1.0f - distFallOffLeft) * ((fs.Size.X - FireSoundLargeLimit) / FireSoundLargeLimit); - } - } - - Vector2 diffRight = (fs.WorldPosition + new Vector2(0.0f, fs.Size.Y / 2)) - listenerPos; - if (Math.Abs(diff.X) < fs.Size.X / 2.0f) { diffRight.X = 0.0f; } - if (diffRight.X >= 0) - { - float distFallOffRight = diffRight.Length() / FireSoundRange; - if (distFallOffRight < 0.99f) - { - fireVolumeRight[0] += 1.0f - distFallOffRight; - if (fs.Size.X > FireSoundLargeLimit) fireVolumeRight[1] += (1.0f - distFallOffRight) * ((fs.Size.X - FireSoundLargeLimit) / FireSoundLargeLimit); - } - } - } + AddFireVolume(fs); + } + foreach (FireSource fs in hull.FakeFireSources) + { + AddFireVolume(fs); } } @@ -579,13 +625,58 @@ namespace Barotrauma Vector2 soundPos = new Vector2(GameMain.SoundManager.ListenerPosition.X + (fireVolumeRight[i] - fireVolumeLeft[i]) * 100, GameMain.SoundManager.ListenerPosition.Y); if (fireSoundChannels[i] == null || !fireSoundChannels[i].IsPlaying) { - fireSoundChannels[i] = GetSound(i == 0 ? "fire" : "firelarge").Play(1.0f, FlowSoundRange, soundPos); + fireSoundChannels[i] = GetSound(fireSoundTags[i]).Play(1.0f, FlowSoundRange, soundPos); fireSoundChannels[i].Looping = true; } fireSoundChannels[i].Gain = Math.Max(fireVolumeRight[i], fireVolumeLeft[i]); fireSoundChannels[i].Position = new Vector3(soundPos, 0.0f); } } + + void AddFireVolume(FireSource fs) + { + Vector2 diff = fs.WorldPosition + fs.Size / 2 - listenerPos; + if (Math.Abs(diff.X) < FireSoundRange && Math.Abs(diff.Y) < FireSoundRange) + { + Vector2 diffLeft = (fs.WorldPosition + new Vector2(fs.Size.X, fs.Size.Y / 2)) - listenerPos; + if (Math.Abs(diff.X) < fs.Size.X / 2.0f) { diffLeft.X = 0.0f; } + if (diffLeft.X <= 0) + { + float distFallOffLeft = diffLeft.Length() / FireSoundRange; + if (distFallOffLeft < 0.99f) + { + fireVolumeLeft[0] += (1.0f - distFallOffLeft); + if (fs.Size.X > FireSoundLargeLimit) + { + fireVolumeLeft[2] += (1.0f - distFallOffLeft) * ((fs.Size.X - FireSoundLargeLimit) / FireSoundLargeLimit); + } + else if (fs.Size.X > FireSoundMediumLimit) + { + fireVolumeLeft[1] += (1.0f - distFallOffLeft) * ((fs.Size.X - FireSoundMediumLimit) / FireSoundMediumLimit); + } + } + } + + Vector2 diffRight = (fs.WorldPosition + new Vector2(0.0f, fs.Size.Y / 2)) - listenerPos; + if (Math.Abs(diff.X) < fs.Size.X / 2.0f) { diffRight.X = 0.0f; } + if (diffRight.X >= 0) + { + float distFallOffRight = diffRight.Length() / FireSoundRange; + if (distFallOffRight < 0.99f) + { + fireVolumeRight[0] += 1.0f - distFallOffRight; + if (fs.Size.X > FireSoundLargeLimit) + { + fireVolumeRight[2] += (1.0f - distFallOffRight) * ((fs.Size.X - FireSoundLargeLimit) / FireSoundLargeLimit); + } + else if (fs.Size.X > FireSoundMediumLimit) + { + fireVolumeRight[1] += (1.0f - distFallOffRight) * ((fs.Size.X - FireSoundMediumLimit) / FireSoundMediumLimit); + } + } + } + } + } } private static void UpdateRandomAmbience(float deltaTime) @@ -606,6 +697,40 @@ namespace Barotrauma } } + private static void UpdateHullSounds(float deltaTime) + { + if (hullSoundChannel != null && hullSoundChannel.IsPlaying && hullSoundSource != null) + { + hullSoundChannel.Position = new Vector3(hullSoundSource.WorldPosition, 0.0f); + hullSoundChannel.Gain = GetHullSoundVolume(hullSoundSource.Submarine); + } + + if (hullSoundTimer > 0.0f) + { + hullSoundTimer -= deltaTime; + } + else + { + if (!Level.IsLoadedOutpost && Character.Controlled?.CurrentHull?.Submarine is Submarine sub && + sub.Info != null && !sub.Info.IsOutpost) + { + hullSoundSource = Character.Controlled.CurrentHull; + hullSoundChannel = PlaySound("hull", hullSoundSource.WorldPosition, volume: GetHullSoundVolume(sub), range: 1500.0f); + hullSoundTimer = Rand.Range(hullSoundInterval.X, hullSoundInterval.Y); + } + else + { + hullSoundTimer = 5.0f; + } + } + + static float GetHullSoundVolume(Submarine sub) + { + var depth = Level.Loaded == null ? 0.0f : Math.Abs(sub.Position.Y - Level.Loaded.Size.Y) * Physics.DisplayToRealWorldRatio; + return Math.Clamp((depth - 800.0f) / 1500.0f, 0.4f, 1.0f); + } + } + public static Sound GetSound(string soundTag) { var matchingSounds = miscSounds[soundTag].ToList(); @@ -637,8 +762,8 @@ namespace Barotrauma { if (sound == null) { - string errorMsg = "Error in SoundPlayer.PlaySound (sound was null)\n" + Environment.StackTrace; - GameAnalyticsManager.AddErrorEventOnce("SoundPlayer.PlaySound:SoundNull" + Environment.StackTrace, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + string errorMsg = "Error in SoundPlayer.PlaySound (sound was null)\n" + Environment.StackTrace.CleanupStackTrace(); + GameAnalyticsManager.AddErrorEventOnce("SoundPlayer.PlaySound:SoundNull" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return null; } @@ -815,7 +940,9 @@ namespace Barotrauma return "editor"; } - if (Screen.Selected != GameMain.GameScreen) { return "menu"; } + if (Screen.Selected != GameMain.GameScreen) { return firstTimeInMainMenu ? "menu" : "default"; } + + firstTimeInMainMenu = false; if (Character.Controlled != null) @@ -830,6 +957,18 @@ namespace Barotrauma { return "wreck"; } + + if (Level.IsLoadedOutpost && Character.Controlled.Submarine == Level.Loaded.StartOutpost) + { + // Only return music type for specific outpost types to not assume that + // every outpost type has an associated music track (switch-case for future tracks) + var locationType = Level.Loaded.StartLocation?.Type?.Identifier?.ToLowerInvariant(); + switch (locationType) + { + case "research": + return locationType; + } + } } Submarine targetSubmarine = Character.Controlled?.Submarine; @@ -845,12 +984,12 @@ namespace Barotrauma float totalArea = 0.0f; foreach (Hull hull in Hull.hullList) { - if (hull.Submarine != targetSubmarine) continue; + if (hull.Submarine != targetSubmarine) { continue; } floodedArea += hull.WaterVolume; totalArea += hull.Volume; } - if (totalArea > 0.0f && floodedArea / totalArea > 0.25f) return "flooded"; + if (totalArea > 0.0f && floodedArea / totalArea > 0.25f) { return "flooded"; } } float enemyDistThreshold = 5000.0f; @@ -863,7 +1002,7 @@ namespace Barotrauma foreach (Character character in Character.CharacterList) { if (character.IsDead || !character.Enabled) continue; - if (!(character.AIController is EnemyAIController enemyAI) || (!enemyAI.AttackHumans && !enemyAI.AttackRooms)) continue; + if (!(character.AIController is EnemyAIController enemyAI) || (!enemyAI.AttackHumans && !enemyAI.AttackRooms)) { continue; } if (targetSubmarine != null) { @@ -952,6 +1091,16 @@ namespace Barotrauma } } tempList.GetRandom().sound?.Play(1.0f, range, position, muffle: ShouldMuffleSound(Character.Controlled, position, range, null)); - } + } + + public static void PlayUISound(GUISoundType soundType) + { + if (guiSounds == null || guiSounds.Count < 1) { return; } + if (guiSounds.TryGetValue(soundType, out List sounds)) + { + if (sounds == null || sounds.Count < 1) { return; } + sounds.GetRandom()?.Play(null, "ui"); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs index c82285907..207c24449 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs @@ -99,6 +99,7 @@ namespace Barotrauma { Sprite = new Sprite(element, path, file, lazyLoad: lazyLoad); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + // TODO: what's the purpose of this? foreach (XElement subElement in element.Elements()) { List conditionalList = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index 5acb51127..98a5c6638 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -67,8 +67,8 @@ namespace Barotrauma { if (sound?.Sound == null) { - string errorMsg = $"Error in StatusEffect.ApplyProjSpecific1 (sound \"{sound?.Filename ?? "unknown"}\" was null)\n" + Environment.StackTrace; - GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull1" + Environment.StackTrace, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + string errorMsg = $"Error in StatusEffect.ApplyProjSpecific1 (sound \"{sound?.Filename ?? "unknown"}\" was null)\n" + Environment.StackTrace.CleanupStackTrace(); + GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull1" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; } soundChannel = SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, hullGuess: hull); @@ -93,14 +93,22 @@ namespace Barotrauma var selectedSound = sounds[selectedSoundIndex]; if (selectedSound?.Sound == null) { - string errorMsg = $"Error in StatusEffect.ApplyProjSpecific2 (sound \"{selectedSound?.Filename ?? "unknown"}\" was null)\n" + Environment.StackTrace; - GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull2" + Environment.StackTrace, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + string errorMsg = $"Error in StatusEffect.ApplyProjSpecific2 (sound \"{selectedSound?.Filename ?? "unknown"}\" was null)\n" + Environment.StackTrace.CleanupStackTrace(); + GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull2" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; } + if (selectedSound.Sound.Disposed) + { + Submarine.ReloadRoundSound(selectedSound); + } soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: hull); if (soundChannel != null) { soundChannel.Looping = loopSound; } } } + else + { + soundChannel.Position = new Vector3(worldPosition, 0.0f); + } if (soundChannel != null && soundChannel.Looping) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs new file mode 100644 index 000000000..492b2a8be --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs @@ -0,0 +1,559 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal readonly struct InventorySlotItem + { + public readonly int Slot; + public readonly Item Item; + + public InventorySlotItem(int slot, Item item) + { + Slot = slot; + Item = item; + } + + public void Deconstruct(out int slot, out Item item) + { + slot = Slot; + item = Item; + } + } + + internal abstract partial class Command + { + public abstract string GetDescription(); + } + + /// + /// A command for setting and reverting a MapEntity rectangle + /// + /// + /// + internal class TransformCommand : Command + { + private readonly List Receivers; + private readonly List NewData; + private readonly List OldData; + private readonly bool Resized; + + /// + /// A command for setting and reverting a MapEntity rectangle + /// + /// Entities whose rectangle has been altered + /// The new rectangle that is or will be applied to the map entity + /// Old rectangle the map entity had before + /// If the transform was resized or not + /// + /// All lists should be equal in length, for every receiver there should be a corresponding entry at the same position in newData and oldData. + /// + public TransformCommand(List receivers, List newData, List oldData, bool resized) + { + Receivers = receivers; + NewData = newData; + OldData = oldData; + Resized = resized; + } + + public override void Execute() => SetRects(NewData); + public override void UnExecute() => SetRects(OldData); + + public override void Cleanup() + { + NewData.Clear(); + OldData.Clear(); + Receivers.Clear(); + } + + private void SetRects(IReadOnlyList rects) + { + if (Receivers.Count != rects.Count) + { + DebugConsole.ThrowError($"Receivers.Count did not match Rects.Count ({Receivers.Count} vs {rects.Count})."); + return; + } + + for (int i = 0; i < rects.Count; i++) + { + MapEntity entity = Receivers[i].GetReplacementOrThis(); + Rectangle Rect = rects[i]; + Vector2 diff = Rect.Location.ToVector2() - entity.Rect.Location.ToVector2(); + entity.Move(diff); + entity.Rect = Rect; + } + } + + public override string GetDescription() + { + if (Resized) + { + return TextManager.GetWithVariable("Undo.ResizedItem", "[item]", Receivers.FirstOrDefault()?.Name); + } + + return Receivers.Count > 1 + ? TextManager.GetWithVariable("Undo.MovedItemsMultiple", "[count]", Receivers.Count.ToString()) + : TextManager.GetWithVariable("Undo.MovedItem", "[item]", Receivers.FirstOrDefault()?.Name); + } + } + + /// + /// A command that removes and unremoves map entities + /// + /// + /// + /// + internal class AddOrDeleteCommand : Command + { + private readonly Dictionary PreviousInventories = new Dictionary(); + private readonly List Receivers; + private readonly List CloneList; + private readonly bool WasDeleted; + private readonly List ContainedItemsCommand = new List(); + + /// + /// Creates a command where all entities share the same state. + /// + /// Entities that were deleted or added + /// Whether or not all entities are or are going to be deleted + public AddOrDeleteCommand(List receivers, bool wasDeleted) + { + WasDeleted = wasDeleted; + Receivers = receivers; + + try + { + foreach (MapEntity receiver in receivers) + { + if (receiver is Item it && it.ParentInventory != null) + { + PreviousInventories.Add(new InventorySlotItem(Array.IndexOf(it.ParentInventory.Items, it), it), it.ParentInventory); + } + } + + List clonedTargets = MapEntity.Clone(receivers); + + List itemsToDelete = new List(); + foreach (MapEntity receiver in Receivers) + { + if (receiver is Item it) + { + foreach (ItemContainer component in it.GetComponents()) + { + if (component.Inventory == null) { continue; } + + itemsToDelete.AddRange(component.Inventory.Items.Where(item => item != null && !item.Removed)); + } + } + } + + if (itemsToDelete.Any()) + { + ContainedItemsCommand.Add(new AddOrDeleteCommand(itemsToDelete, true)); + foreach (MapEntity item in itemsToDelete) + { + if (item != null && !item.Removed) + { + item.Remove(); + } + } + } + + foreach (MapEntity clone in clonedTargets) + { + clone.ShallowRemove(); + if (clone is Item it) + { + foreach (ItemContainer container in it.GetComponents()) + { + container.Inventory?.DeleteAllItems(); + } + } + } + + CloneList = clonedTargets; + } + // This should never happen except if we decide to make a new type of MapEntity that isn't finished yet + catch (Exception e) + { + Receivers = new List(); + CloneList = new List(); + DebugConsole.ThrowError("Could not store object", e); + } + } + + public override void Execute() + { + DeleteUndelete(true); + ContainedItemsCommand?.ForEach(cmd => cmd.Execute()); + } + + public override void UnExecute() + { + DeleteUndelete(false); + ContainedItemsCommand?.ForEach(cmd => cmd.UnExecute()); + } + + public override void Cleanup() + { + foreach (MapEntity entity in CloneList) + { + if (!entity.Removed) + { + entity.Remove(); + } + } + + CloneList?.Clear(); + Receivers.Clear(); + PreviousInventories?.Clear(); + ContainedItemsCommand?.ForEach(cmd => cmd.Cleanup()); + } + + private void DeleteUndelete(bool redo) + { + bool wasDeleted = WasDeleted; + + // We are redoing instead of undoing, flip the behavior + if (redo) { wasDeleted = !wasDeleted; } + + if (wasDeleted) + { + Debug.Assert(Receivers.All(entity => entity.GetReplacementOrThis().Removed), "Tried to redo a deletion but some items were not deleted"); + + List clones = MapEntity.Clone(CloneList); + for (int i = 0; i < Math.Min(Receivers.Count, clones.Count); i++) + { + MapEntity clone = clones[i]; + MapEntity receiver = Receivers[i]; + + if (receiver.GetReplacementOrThis() is Item item && clone is Item cloneItem) + { + foreach (ItemComponent ic in item.Components) + { + int index = item.GetComponentIndex(ic); + ItemComponent component = cloneItem.Components.ElementAtOrDefault(index); + switch (component) + { + case null: + continue; + case ItemContainer newContainer when newContainer.Inventory != null && ic is ItemContainer itemContainer && itemContainer.Inventory != null: + itemContainer.Inventory.GetReplacementOrThiS().ReplacedBy = newContainer.Inventory; + goto default; + default: + ic.GetReplacementOrThis().ReplacedBy = component; + break; + } + } + } + + receiver.GetReplacementOrThis().ReplacedBy = clone; + + if (clone is Item it) + { + foreach (var (slotRef, inventory) in PreviousInventories) + { + if (slotRef.Item == receiver) + { + inventory.GetReplacementOrThiS().TryPutItem(it, slotRef.Slot, false, false, null, createNetworkEvent: false); + } + } + } + } + + foreach (MapEntity clone in clones) + { + clone.Submarine = Submarine.MainSub; + } + } + else + { + foreach (MapEntity t in Receivers) + { + MapEntity receiver = t.GetReplacementOrThis(); + if (!receiver.Removed) + { + receiver.Remove(); + } + } + } + } + + public void MergeInto(AddOrDeleteCommand master) + { + master.Receivers.AddRange(Receivers); + master.CloneList.AddRange(CloneList); + master.ContainedItemsCommand.AddRange(ContainedItemsCommand); + foreach (var (slot, item) in PreviousInventories) + { + master.PreviousInventories.Add(slot, item); + } + } + + public override string GetDescription() + { + if (WasDeleted) + { + return Receivers.Count > 1 + ? TextManager.GetWithVariable("Undo.RemovedItemsMultiple", "[count]", Receivers.Count.ToString()) + : TextManager.GetWithVariable("Undo.RemovedItem", "[item]", Receivers.FirstOrDefault()?.Name); + } + + return Receivers.Count > 1 + ? TextManager.GetWithVariable("Undo.AddedItemsMultiple", "[count]", Receivers.Count.ToString()) + : TextManager.GetWithVariable("Undo.AddedItem", "[item]", Receivers.FirstOrDefault()?.Name); + } + } + + /// + /// A command that places or drops items out of inventories + /// + /// + /// + internal class InventoryPlaceCommand : Command + { + private readonly Inventory Inventory; + private readonly List Receivers; + private readonly bool wasDropped; + + public InventoryPlaceCommand(Inventory inventory, List items, bool dropped) + { + Inventory = inventory; + Receivers = items.Select(item => new InventorySlotItem(Array.IndexOf(inventory.Items, item), item)).ToList(); + wasDropped = dropped; + } + + public override void Execute() => ContainUncontain(false); + public override void UnExecute() => ContainUncontain(true); + + public override void Cleanup() + { + Receivers.Clear(); + } + + private void ContainUncontain(bool drop) + { + // flip the behavior if the item was dropped instead of inserted + if (wasDropped) { drop = !drop; } + + foreach (var (slot, receiver) in Receivers) + { + Item item = (Item) receiver.GetReplacementOrThis(); + + if (drop) + { + item.Drop(null, createNetworkEvent: false); + } + else + { + Inventory.GetReplacementOrThiS().TryPutItem(item, slot, false, false, null, createNetworkEvent: false); + } + } + } + + public override string GetDescription() + { + if (wasDropped) + { + return TextManager.GetWithVariable("Undo.DroppedItem", "[item]", Receivers.FirstOrDefault().Item.Name); + } + + string container = "[ERROR]"; + + if (Inventory.Owner is Item item) + { + container = item.Name; + } + + return Receivers.Count > 1 + ? TextManager.GetWithVariables("Undo.ContainedItemsMultiple", new[] { "[count]", "[container]" }, new[] { Receivers.Count.ToString(), container }) + : TextManager.GetWithVariables("Undo.ContainedItem", new[] { "[item]", "[container]" }, new[] { Receivers.FirstOrDefault().Item.Name, container }); + } + } + + /// + /// A command that sets item properties + /// + internal class PropertyCommand : Command + { + private Dictionary> OldProperties; + private readonly List Receivers; + private readonly string PropertyName; + private readonly object NewProperties; + private string sanitizedProperty; + + public readonly int PropertyCount; + + /// + /// A command that sets item properties + /// + /// Affected entities + /// Real property name, not all lowercase + /// + /// + public PropertyCommand(List receivers, string propertyName, object newData, Dictionary> oldData) + { + Receivers = receivers; + PropertyName = propertyName; + OldProperties = oldData; + NewProperties = newData; + PropertyCount = receivers.Count; + SanitizeProperty(); + } + + public PropertyCommand(ISerializableEntity receiver, string propertyName, object newData, object oldData) + { + Receivers = new List { receiver }; + PropertyName = propertyName; + OldProperties = new Dictionary> { { oldData, Receivers } }; + NewProperties = newData; + PropertyCount = 1; + SanitizeProperty(); + } + + public bool MergeInto(PropertyCommand master) + { + if (!master.Receivers.SequenceEqual(Receivers)) { return false; } + master.OldProperties = OldProperties; + return true; + } + + private void SanitizeProperty() + { + sanitizedProperty = NewProperties switch + { + float f => f.FormatSingleDecimal(), + Point point => XMLExtensions.PointToString(point), + Vector2 vector2 => vector2.FormatZeroDecimal(), + Vector3 vector3 => vector3.FormatSingleDecimal(), + Vector4 vector4 => vector4.FormatSingleDecimal(), + Color color => XMLExtensions.ColorToString(color), + Rectangle rectangle => XMLExtensions.RectToString(rectangle), + _ => NewProperties.ToString() + }; + } + + public override void Execute() => SetProperties(false); + public override void UnExecute() => SetProperties(true); + + public override void Cleanup() + { + Receivers.Clear(); + OldProperties.Clear(); + } + + private void SetProperties(bool undo) + { + foreach (ISerializableEntity t in Receivers) + { + ISerializableEntity receiver; + switch (t) + { + case MapEntity me when me.GetReplacementOrThis() is ISerializableEntity sEntity: + receiver = sEntity; + break; + case ItemComponent ic when ic.GetReplacementOrThis() is ISerializableEntity sItemComponent: + receiver = sItemComponent; + break; + default: + receiver = t; + break; + } + + object data = NewProperties; + + if (undo) + { + foreach (var (key, value) in OldProperties) + { + if (value.Contains(t)) { data = key; } + } + } + + if (receiver.SerializableProperties != null) + { + Dictionary props = receiver.SerializableProperties; + + if (props.TryGetValue(PropertyName.ToLowerInvariant(), out SerializableProperty prop)) + { + prop.TrySetValue(receiver, data); + // Update the editing hud + if (MapEntity.EditingHUD == null || (MapEntity.EditingHUD.UserData != receiver && (receiver is ItemComponent ic && MapEntity.EditingHUD.UserData != ic.Item))) { continue; } + + GUIListBox list = MapEntity.EditingHUD.GetChild(); + if (list == null) { continue; } + + IEnumerable editors = list.Content.FindChildren(comp => comp is SerializableEntityEditor).Cast(); + SerializableEntityEditor.LockEditing = true; + foreach (SerializableEntityEditor editor in editors) + { + if (editor.UserData == receiver && editor.Fields.TryGetValue(PropertyName, out GUIComponent[] _)) + { + editor.UpdateValue(prop, data); + } + } + + SerializableEntityEditor.LockEditing = false; + } + } + } + } + + public override string GetDescription() + { + return Receivers.Count > 1 + ? TextManager.GetWithVariables("Undo.ChangedPropertyMultiple", new[] { "[property]", "[count]", "[value]" }, new[] { PropertyName, Receivers.Count.ToString(), sanitizedProperty }) + : TextManager.GetWithVariables("Undo.ChangedProperty", new[] { "[property]", "[item]", "[value]" }, new[] { PropertyName, Receivers.FirstOrDefault()?.Name, sanitizedProperty }); + } + } + + /// + /// A command that moves items around in inventories + /// + /// + /// + internal class InventoryMoveCommand : Command + { + private readonly Inventory oldInventory; + private readonly Inventory newInventory; + private readonly int oldSlot; + private readonly int newSlot; + private readonly Item targetItem; + + public InventoryMoveCommand(Inventory oldInventory, Inventory newInventory, Item item, int oldSlot, int newSlot) + { + this.newInventory = newInventory; + this.oldInventory = oldInventory; + this.oldSlot = oldSlot; + this.newSlot = newSlot; + targetItem = item; + } + + public override void Execute() + { + if (targetItem.GetReplacementOrThis() is Item item) + { + newInventory?.GetReplacementOrThiS().TryPutItem(item, newSlot, true, false, null, createNetworkEvent: false); + } + } + + public override void UnExecute() + { + if (targetItem.GetReplacementOrThis() is Item item) + { + oldInventory?.GetReplacementOrThiS().TryPutItem(item, oldSlot, true, false, null, createNetworkEvent: false); + } + } + + public override void Cleanup() { } + + public override string GetDescription() + { + return TextManager.GetWithVariable("Undo.MovedItem", "[item]", targetItem.Name); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpUtility.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpUtility.cs deleted file mode 100644 index 9f0ee6910..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpUtility.cs +++ /dev/null @@ -1,773 +0,0 @@ -// -// System.Web.HttpUtility -// -// 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; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using Barotrauma.IO; -using System.Security.Permissions; -using System.Text; - -namespace RestSharp.Contrib -{ - - //#if !MONOTOUCH - // // CAS - no InheritanceDemand here as the class is sealed - // [AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)] - //#endif - public sealed class HttpUtility - { - sealed class HttpQSCollection : NameValueCollection - { - public override string ToString() - { - int count = Count; - if (count == 0) - return ""; - StringBuilder sb = new StringBuilder(); - string[] keys = AllKeys; - for (int i = 0; i < count; i++) - { - sb.AppendFormat("{0}={1}&", keys[i], this[keys[i]]); - } - if (sb.Length > 0) - sb.Length--; - return sb.ToString(); - } - } - - #region Constructors - - public HttpUtility() - { - } - - #endregion // Constructors - - #region Methods - - /* - public static void HtmlAttributeEncode(string s, TextWriter output) - { - if (output == null) - { -#if NET_4_0 - throw new ArgumentNullException ("output"); -#else - throw new NullReferenceException(".NET emulation"); -#endif - } -#if NET_4_0 - HttpEncoder.Current.HtmlAttributeEncode (s, output); -#else - output.Write(HttpEncoder.HtmlAttributeEncode(s)); -#endif - } - */ - - public static string HtmlAttributeEncode(string s) - { -#if NET_4_0 - if (s == null) - return null; - - using (var sw = new StringWriter ()) { - HttpEncoder.Current.HtmlAttributeEncode (s, sw); - return sw.ToString (); - } -#else - return HttpEncoder.HtmlAttributeEncode(s); -#endif - } - - public static string UrlDecode(string str) - { - return UrlDecode(str, Encoding.UTF8); - } - - static char[] GetChars(System.IO.MemoryStream b, Encoding e) - { - return e.GetChars(b.GetBuffer(), 0, (int)b.Length); - } - - static void WriteCharBytes(IList buf, char ch, Encoding e) - { - if (ch > 255) - { - foreach (byte b in e.GetBytes(new char[] { ch })) - buf.Add(b); - } - else - buf.Add((byte)ch); - } - - public static string UrlDecode(string s, Encoding e) - { - if (null == s) - return null; - - if (s.IndexOf('%') == -1 && s.IndexOf('+') == -1) - return s; - - if (e == null) - e = Encoding.UTF8; - - long len = s.Length; - var bytes = new List(); - int xchar; - char ch; - - for (int i = 0; i < len; i++) - { - ch = s[i]; - if (ch == '%' && i + 2 < len && s[i + 1] != '%') - { - if (s[i + 1] == 'u' && i + 5 < len) - { - // unicode hex sequence - xchar = GetChar(s, i + 2, 4); - if (xchar != -1) - { - WriteCharBytes(bytes, (char)xchar, e); - i += 5; - } - else - WriteCharBytes(bytes, '%', e); - } - else if ((xchar = GetChar(s, i + 1, 2)) != -1) - { - WriteCharBytes(bytes, (char)xchar, e); - i += 2; - } - else - { - WriteCharBytes(bytes, '%', e); - } - continue; - } - - if (ch == '+') - WriteCharBytes(bytes, ' ', e); - else - WriteCharBytes(bytes, ch, e); - } - - byte[] buf = bytes.ToArray(); - bytes = null; - return e.GetString(buf); - - } - - public static string UrlDecode(byte[] bytes, Encoding e) - { - if (bytes == null) - return null; - - return UrlDecode(bytes, 0, bytes.Length, e); - } - - static int GetInt(byte b) - { - char c = (char)b; - if (c >= '0' && c <= '9') - return c - '0'; - - if (c >= 'a' && c <= 'f') - return c - 'a' + 10; - - if (c >= 'A' && c <= 'F') - return c - 'A' + 10; - - return -1; - } - - static int GetChar(byte[] bytes, int offset, int length) - { - int value = 0; - int end = length + offset; - for (int i = offset; i < end; i++) - { - int current = GetInt(bytes[i]); - if (current == -1) - return -1; - value = (value << 4) + current; - } - - return value; - } - - static int GetChar(string str, int offset, int length) - { - int val = 0; - int end = length + offset; - for (int i = offset; i < end; i++) - { - char c = str[i]; - if (c > 127) - return -1; - - int current = GetInt((byte)c); - if (current == -1) - return -1; - val = (val << 4) + current; - } - - return val; - } - - public static string UrlDecode(byte[] bytes, int offset, int count, Encoding e) - { - if (bytes == null) - return null; - if (count == 0) - return String.Empty; - - if (bytes == null) - throw new ArgumentNullException("bytes"); - - if (offset < 0 || offset > bytes.Length) - throw new ArgumentOutOfRangeException("offset"); - - if (count < 0 || offset + count > bytes.Length) - throw new ArgumentOutOfRangeException("count"); - - StringBuilder output = new StringBuilder(); - System.IO.MemoryStream acc = new System.IO.MemoryStream(); - - int end = count + offset; - int xchar; - for (int i = offset; i < end; i++) - { - if (bytes[i] == '%' && i + 2 < count && bytes[i + 1] != '%') - { - if (bytes[i + 1] == (byte)'u' && i + 5 < end) - { - if (acc.Length > 0) - { - output.Append(GetChars(acc, e)); - acc.SetLength(0); - } - xchar = GetChar(bytes, i + 2, 4); - if (xchar != -1) - { - output.Append((char)xchar); - i += 5; - continue; - } - } - else if ((xchar = GetChar(bytes, i + 1, 2)) != -1) - { - acc.WriteByte((byte)xchar); - i += 2; - continue; - } - } - - if (acc.Length > 0) - { - output.Append(GetChars(acc, e)); - acc.SetLength(0); - } - - if (bytes[i] == '+') - { - output.Append(' '); - } - else - { - output.Append((char)bytes[i]); - } - } - - if (acc.Length > 0) - { - output.Append(GetChars(acc, e)); - } - - acc = null; - return output.ToString(); - } - - public static byte[] UrlDecodeToBytes(byte[] bytes) - { - if (bytes == null) - return null; - - return UrlDecodeToBytes(bytes, 0, bytes.Length); - } - - public static byte[] UrlDecodeToBytes(string str) - { - return UrlDecodeToBytes(str, Encoding.UTF8); - } - - public static byte[] UrlDecodeToBytes(string str, Encoding e) - { - if (str == null) - return null; - - if (e == null) - throw new ArgumentNullException("e"); - - return UrlDecodeToBytes(e.GetBytes(str)); - } - - public static byte[] UrlDecodeToBytes(byte[] bytes, int offset, int count) - { - if (bytes == null) - return null; - if (count == 0) - return new byte[0]; - - int len = bytes.Length; - if (offset < 0 || offset >= len) - throw new ArgumentOutOfRangeException("offset"); - - if (count < 0 || offset > len - count) - throw new ArgumentOutOfRangeException("count"); - - System.IO.MemoryStream result = new System.IO.MemoryStream(); - int end = offset + count; - for (int i = offset; i < end; i++) - { - char c = (char)bytes[i]; - if (c == '+') - { - c = ' '; - } - else if (c == '%' && i < end - 2) - { - int xchar = GetChar(bytes, i + 1, 2); - if (xchar != -1) - { - c = (char)xchar; - i += 2; - } - } - result.WriteByte((byte)c); - } - - return result.ToArray(); - } - - public static string UrlEncode(string str) - { - return UrlEncode(str, Encoding.UTF8); - } - - public static string UrlEncode(string s, Encoding Enc) - { - if (s == null) - return null; - - if (s == String.Empty) - return String.Empty; - - bool needEncode = false; - int len = s.Length; - for (int i = 0; i < len; i++) - { - char c = s[i]; - if ((c < '0') || (c < 'A' && c > '9') || (c > 'Z' && c < 'a') || (c > 'z')) - { - if (HttpEncoder.NotEncoded(c)) - continue; - - needEncode = true; - break; - } - } - - if (!needEncode) - return s; - - // avoided GetByteCount call - byte[] bytes = new byte[Enc.GetMaxByteCount(s.Length)]; - int realLen = Enc.GetBytes(s, 0, s.Length, bytes, 0); - return Encoding.ASCII.GetString(UrlEncodeToBytes(bytes, 0, realLen)); - } - - public static string UrlEncode(byte[] bytes) - { - if (bytes == null) - return null; - - if (bytes.Length == 0) - return String.Empty; - - return Encoding.ASCII.GetString(UrlEncodeToBytes(bytes, 0, bytes.Length)); - } - - public static string UrlEncode(byte[] bytes, int offset, int count) - { - if (bytes == null) - return null; - - if (bytes.Length == 0) - return String.Empty; - - return Encoding.ASCII.GetString(UrlEncodeToBytes(bytes, offset, count)); - } - - public static byte[] UrlEncodeToBytes(string str) - { - return UrlEncodeToBytes(str, Encoding.UTF8); - } - - public static byte[] UrlEncodeToBytes(string str, Encoding e) - { - if (str == null) - return null; - - if (str.Length == 0) - return new byte[0]; - - byte[] bytes = e.GetBytes(str); - return UrlEncodeToBytes(bytes, 0, bytes.Length); - } - - public static byte[] UrlEncodeToBytes(byte[] bytes) - { - if (bytes == null) - return null; - - if (bytes.Length == 0) - return new byte[0]; - - return UrlEncodeToBytes(bytes, 0, bytes.Length); - } - - public static byte[] UrlEncodeToBytes(byte[] bytes, int offset, int count) - { - if (bytes == null) - return null; -#if NET_4_0 - return HttpEncoder.Current.UrlEncode (bytes, offset, count); -#else - return HttpEncoder.UrlEncodeToBytes(bytes, offset, count); -#endif - } - - public static string UrlEncodeUnicode(string str) - { - if (str == null) - return null; - - return Encoding.ASCII.GetString(UrlEncodeUnicodeToBytes(str)); - } - - public static byte[] UrlEncodeUnicodeToBytes(string str) - { - if (str == null) - return null; - - if (str.Length == 0) - return new byte[0]; - - System.IO.MemoryStream result = new System.IO.MemoryStream(str.Length); - foreach (char c in str) - { - HttpEncoder.UrlEncodeChar(c, result, true); - } - return result.ToArray(); - } - - /// - /// Decodes an HTML-encoded string and returns the decoded string. - /// - /// The HTML string to decode. - /// The decoded text. - public static string HtmlDecode(string s) - { -#if NET_4_0 - if (s == null) - return null; - - using (var sw = new StringWriter ()) { - HttpEncoder.Current.HtmlDecode (s, sw); - return sw.ToString (); - } -#else - return HttpEncoder.HtmlDecode(s); -#endif - } - - /// - /// Decodes an HTML-encoded string and sends the resulting output to a TextWriter output stream. - /// - /// The HTML string to decode - /// The TextWriter output stream containing the decoded string. - /* - public static void HtmlDecode(string s, TextWriter output) - { - if (output == null) - { -#if NET_4_0 - throw new ArgumentNullException ("output"); -#else - throw new NullReferenceException(".NET emulation"); -#endif - } - - if (!String.IsNullOrEmpty(s)) - { -#if NET_4_0 - HttpEncoder.Current.HtmlDecode (s, output); -#else - output.Write(HttpEncoder.HtmlDecode(s)); -#endif - } - } - */ - - public static string HtmlEncode(string s) - { -#if NET_4_0 - if (s == null) - return null; - - using (var sw = new StringWriter ()) { - HttpEncoder.Current.HtmlEncode (s, sw); - return sw.ToString (); - } -#else - return HttpEncoder.HtmlEncode(s); -#endif - } - - /// - /// HTML-encodes a string and sends the resulting output to a TextWriter output stream. - /// - /// The string to encode. - /// The TextWriter output stream containing the encoded string. - /* - public static void HtmlEncode(string s, TextWriter output) - { - if (output == null) - { -#if NET_4_0 - throw new ArgumentNullException ("output"); -#else - throw new NullReferenceException(".NET emulation"); -#endif - } - - if (!String.IsNullOrEmpty(s)) - { -#if NET_4_0 - HttpEncoder.Current.HtmlEncode (s, output); -#else - output.Write(HttpEncoder.HtmlEncode(s)); -#endif - } - } - */ - -#if NET_4_0 - public static string HtmlEncode (object value) - { - if (value == null) - return null; - - IHtmlString htmlString = value as IHtmlString; - if (htmlString != null) - return htmlString.ToHtmlString (); - - return HtmlEncode (value.ToString ()); - } - - public static string JavaScriptStringEncode (string value) - { - return JavaScriptStringEncode (value, false); - } - - public static string JavaScriptStringEncode (string value, bool addDoubleQuotes) - { - if (String.IsNullOrEmpty (value)) - return addDoubleQuotes ? "\"\"" : String.Empty; - - int len = value.Length; - bool needEncode = false; - char c; - for (int i = 0; i < len; i++) { - c = value [i]; - - if (c >= 0 && c <= 31 || c == 34 || c == 39 || c == 60 || c == 62 || c == 92) { - needEncode = true; - break; - } - } - - if (!needEncode) - return addDoubleQuotes ? "\"" + value + "\"" : value; - - var sb = new StringBuilder (); - if (addDoubleQuotes) - sb.Append ('"'); - - for (int i = 0; i < len; i++) { - c = value [i]; - if (c >= 0 && c <= 7 || c == 11 || c >= 14 && c <= 31 || c == 39 || c == 60 || c == 62) - sb.AppendFormat ("\\u{0:x4}", (int)c); - else switch ((int)c) { - case 8: - sb.Append ("\\b"); - break; - - case 9: - sb.Append ("\\t"); - break; - - case 10: - sb.Append ("\\n"); - break; - - case 12: - sb.Append ("\\f"); - break; - - case 13: - sb.Append ("\\r"); - break; - - case 34: - sb.Append ("\\\""); - break; - - case 92: - sb.Append ("\\\\"); - break; - - default: - sb.Append (c); - break; - } - } - - if (addDoubleQuotes) - sb.Append ('"'); - - return sb.ToString (); - } -#endif - public static string UrlPathEncode(string s) - { -#if NET_4_0 - return HttpEncoder.Current.UrlPathEncode (s); -#else - return HttpEncoder.UrlPathEncode(s); -#endif - } - - public static NameValueCollection ParseQueryString(string query) - { - return ParseQueryString(query, Encoding.UTF8); - } - - public static NameValueCollection ParseQueryString(string query, Encoding encoding) - { - if (query == null) - throw new ArgumentNullException("query"); - if (encoding == null) - throw new ArgumentNullException("encoding"); - if (query.Length == 0 || (query.Length == 1 && query[0] == '?')) - return new NameValueCollection(); - if (query[0] == '?') - query = query.Substring(1); - - NameValueCollection result = new HttpQSCollection(); - ParseQueryString(query, encoding, result); - return result; - } - - internal static void ParseQueryString(string query, Encoding encoding, NameValueCollection result) - { - if (query.Length == 0) - return; - - string decoded = HtmlDecode(query); - int decodedLength = decoded.Length; - int namePos = 0; - bool first = true; - while (namePos <= decodedLength) - { - int valuePos = -1, valueEnd = -1; - for (int q = namePos; q < decodedLength; q++) - { - if (valuePos == -1 && decoded[q] == '=') - { - valuePos = q + 1; - } - else if (decoded[q] == '&') - { - valueEnd = q; - break; - } - } - - if (first) - { - first = false; - if (decoded[namePos] == '?') - namePos++; - } - - string name, value; - if (valuePos == -1) - { - name = null; - valuePos = namePos; - } - else - { - name = UrlDecode(decoded.Substring(namePos, valuePos - namePos - 1), encoding); - } - if (valueEnd < 0) - { - namePos = -1; - valueEnd = decoded.Length; - } - else - { - namePos = valueEnd + 1; - } - value = UrlDecode(decoded.Substring(valuePos, valueEnd - valuePos), encoding); - - result.Add(name, value); - if (namePos == -1) - break; - } - } - #endregion // Methods - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs index e7b4f6d47..38f1863cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -138,14 +138,15 @@ namespace Barotrauma public static Color GradientLerp(float t, params Color[] gradient) { + if (!MathUtils.IsValid(t)) { return Color.Purple; } System.Diagnostics.Debug.Assert(gradient.Length > 0, "Empty color array passed to the GradientLerp method"); if (gradient.Length == 0) { #if DEBUG - DebugConsole.ThrowError("Empty color array passed to the GradientLerp method.\n" + Environment.StackTrace); + DebugConsole.ThrowError("Empty color array passed to the GradientLerp method.\n" + Environment.StackTrace.CleanupStackTrace()); #endif GameAnalyticsManager.AddErrorEventOnce("ToolBox.GradientLerp:EmptyColorArray", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Empty color array passed to the GradientLerp method.\n" + Environment.StackTrace); + "Empty color array passed to the GradientLerp method.\n" + Environment.StackTrace.CleanupStackTrace()); return Color.Black; } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 28e99939f..c17176b2c 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.10.5.1 + 0.10.6.2 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma @@ -178,6 +178,10 @@ <_Parameter1>GitBranch <_Parameter2>$(BuildBranch) + + <_Parameter1>ProjectDir + <_Parameter2>$(ProjectDir) + diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index a0cd81d75..82243b5fd 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.10.5.1 + 0.10.6.2 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma @@ -180,6 +180,10 @@ <_Parameter1>GitBranch <_Parameter2>$(BuildBranch) + + <_Parameter1>ProjectDir + <_Parameter2>$(ProjectDir) + diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 6a8473788..bf3a1f2db 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.10.5.1 + 0.10.6.2 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma @@ -209,6 +209,10 @@ <_Parameter1>GitBranch <_Parameter2>$(BuildBranch) + + <_Parameter1>ProjectDir + <_Parameter2>$(ProjectDir) + diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 61f3d3ba2..8d4fbf757 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.10.5.1 + 0.10.6.2 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer @@ -127,6 +127,10 @@ <_Parameter1>GitBranch <_Parameter2>$(BuildBranch) + + <_Parameter1>ProjectDir + <_Parameter2>$(ProjectDir) + diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index bdbea03ac..1498a636e 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.10.5.1 + 0.10.6.2 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer @@ -140,6 +140,10 @@ <_Parameter1>GitBranch <_Parameter2>$(BuildBranch) + + <_Parameter1>ProjectDir + <_Parameter2>$(ProjectDir) + diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index a0595994a..0080258ba 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -420,6 +420,7 @@ namespace Barotrauma if (writeStatus) { WriteStatus(tempBuffer); + (AIController as EnemyAIController)?.PetBehavior?.ServerWrite(tempBuffer); HealthUpdatePending = false; } @@ -478,7 +479,7 @@ namespace Barotrauma msg.Write(Info == null); msg.Write(entityId); msg.Write(SpeciesName); - msg.Write(seed); + msg.Write(Seed); if (Removed) { @@ -528,19 +529,19 @@ namespace Barotrauma { msg.Write(true); msg.Write((byte)Order.PrefabList.IndexOf(info.CurrentOrder.Prefab)); - msg.Write(info.CurrentOrder.TargetEntity == null ? (UInt16)0 : - info.CurrentOrder.TargetEntity.ID); - if (info.CurrentOrder.OrderGiver != null) + msg.Write(info.CurrentOrder.TargetEntity == null ? (UInt16)0 : info.CurrentOrder.TargetEntity.ID); + var hasOrderGiver = info.CurrentOrder.OrderGiver != null; + msg.Write(hasOrderGiver); + if (hasOrderGiver) { msg.Write(info.CurrentOrder.OrderGiver.ID); } + msg.Write((byte)(string.IsNullOrWhiteSpace(info.CurrentOrderOption) ? 0 : Array.IndexOf(info.CurrentOrder.Prefab.Options, info.CurrentOrderOption))); + var hasTargetPosition = info.CurrentOrder.TargetPosition != null; + msg.Write(hasTargetPosition); + if (hasTargetPosition) { - msg.Write(true); - msg.Write(info.CurrentOrder.OrderGiver.ID); + msg.Write(info.CurrentOrder.TargetPosition.Position.X); + msg.Write(info.CurrentOrder.TargetPosition.Position.Y); + msg.Write(info.CurrentOrder.TargetPosition.Hull == null ? (UInt16)0 : info.CurrentOrder.TargetPosition.Hull.ID); } - else - { - msg.Write(false); - } - msg.Write((byte)(string.IsNullOrWhiteSpace(info.CurrentOrderOption) ? 0 : - Array.IndexOf(info.CurrentOrder.Prefab.Options, info.CurrentOrderOption))); } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 839cab056..3db355286 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -247,7 +247,7 @@ namespace Barotrauma catch (Exception e) { string errorMsg = "Failed to write input to command line (window width: " + Console.WindowWidth + ", window height: " + Console.WindowHeight + ")\n" - + e.Message + "\n" + e.StackTrace; + + e.Message + "\n" + e.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("DebugConsole.RewriteInputToCommandLine", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } } @@ -1119,6 +1119,12 @@ namespace Barotrauma } else { + if (maxPlayers > NetConfig.MaxPlayers) + { + NewMessage($"Setting the maximum amount of players to {maxPlayers} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead."); + maxPlayers = NetConfig.MaxPlayers; + } + GameMain.Server.ServerSettings.MaxPlayers = maxPlayers; NewMessage("Set the maximum player count to " + maxPlayers + "."); } @@ -1132,6 +1138,12 @@ namespace Barotrauma } else { + if (maxPlayers > NetConfig.MaxPlayers) + { + GameMain.Server.SendConsoleMessage($"Setting the maximum amount of players to {maxPlayers} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead.", client); + maxPlayers = NetConfig.MaxPlayers; + } + GameMain.Server.ServerSettings.MaxPlayers = maxPlayers; NewMessage(client.Name + " set the maximum player count to " + maxPlayers + "."); GameMain.Server.SendConsoleMessage("Set the maximum player count to " + maxPlayers + ".", client); @@ -1352,7 +1364,7 @@ namespace Barotrauma ServerEntityEvent ev = GameMain.Server.EntityEventManager.Events[Convert.ToUInt16(args[0])]; if (ev != null) { - NewMessage(ev.StackTrace, Color.Lime); + NewMessage(ev.StackTrace.CleanupStackTrace(), Color.Lime); } })); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MonsterMission.cs index e46a6bf95..35bbdcf39 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MonsterMission.cs @@ -15,7 +15,7 @@ namespace Barotrauma msg.Write((byte)monsters.Count); foreach (Character monster in monsters) { - monster.WriteSpawnData(msg, monster.ID, restrictMessageSize: false); + monster.WriteSpawnData(msg, monster.OriginalID, restrictMessageSize: false); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index f619c4b03..364f1d075 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -147,6 +147,14 @@ namespace Barotrauma c.InGame && (IsOwner(c) || c.HasPermission(ClientPermissions.ManageCampaign))); } + public void LoadPets() + { + if (petsElement != null) + { + PetBehavior.LoadPets(petsElement); + } + } + protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults) { lastUpdateID++; @@ -229,6 +237,9 @@ namespace Barotrauma c.Inventory.DeleteAllItems(); } + petsElement = new XElement("pets"); + PetBehavior.SavePets(petsElement); + yield return CoroutineStatus.Running; if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub)) @@ -694,6 +705,10 @@ namespace Barotrauma } pendingHireInfos.Add(match); + if (pendingHireInfos.Count + CrewManager.CharacterInfos.Count() >= CrewManager.MaxCrewSize) + { + break; + } } location.HireManager.PendingHires = pendingHireInfos; } @@ -764,6 +779,11 @@ namespace Barotrauma CargoManager?.SavePurchasedItems(modeElement); UpgradeManager?.SavePendingUpgrades(modeElement, UpgradeManager?.PendingUpgrades); + if (petsElement != null) + { + modeElement.Add(petsElement); + } + // save bots CrewManager.SaveMultiplayer(modeElement); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs index d29f38b98..3d6955aef 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs @@ -67,8 +67,26 @@ namespace Barotrauma.Items.Components if (!CheckCharacterSuccess(c.Character)) { item.CreateServerEvent(this); - c.Character.SelectedItems[0]?.GetComponent()?.CreateNetworkEvent(); - c.Character.SelectedItems[1]?.GetComponent()?.CreateNetworkEvent(); + c.Character.Inventory?.CreateNetworkEvent(); + for (int i = 0; i < 2; i++) + { + var selectedWire = c.Character.SelectedItems[i]?.GetComponent(); + if (selectedWire == null) { continue; } + + selectedWire.CreateNetworkEvent(); + var panel1 = selectedWire.Connections[0]?.ConnectionPanel; + if (panel1 != null && panel1 != this) { panel1.item.CreateServerEvent(panel1); } + var panel2 = selectedWire.Connections[1]?.ConnectionPanel; + if (panel2 != null && panel2 != this) { panel2.item.CreateServerEvent(panel2); } + + CoroutineManager.InvokeAfter(() => + { + item.CreateServerEvent(this); + if (panel1 != null && panel1 != this) { panel1.item.CreateServerEvent(panel1); } + if (panel2 != null && panel2 != this) { panel2.item.CreateServerEvent(panel2); } + if (!selectedWire.Item.Removed) { selectedWire.CreateNetworkEvent(); } + }, 1.0f); + } GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, c.Character.ID }); return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 6fce00726..2649fbbb4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -131,7 +131,7 @@ namespace Barotrauma case NetEntityEvent.Type.ChangeProperty: try { - WritePropertyChange(msg, extraData, false); + WritePropertyChange(msg, extraData, inGameEditableOnly: !GameMain.NetworkMember.IsServer); } catch (Exception e) { @@ -222,7 +222,7 @@ namespace Barotrauma break; case NetEntityEvent.Type.ChangeProperty: - ReadPropertyChange(msg, true, c); + ReadPropertyChange(msg, inGameEditableOnly: GameMain.NetworkMember.IsServer, sender: c); break; case NetEntityEvent.Type.Combine: UInt16 combineTargetID = msg.ReadUInt16(); @@ -367,23 +367,42 @@ namespace Barotrauma public void CreateServerEvent(T ic) where T : ItemComponent, IServerSerializable { - if (GameMain.Server == null) return; + if (GameMain.Server == null) { return; } if (!ItemList.Contains(this)) { - string errorMsg = "Attempted to create a network event for an item (" + Name + ") that hasn't been fully initialized yet.\n" + Environment.StackTrace; + string errorMsg = "Attempted to create a network event for an item (" + Name + ") that hasn't been fully initialized yet.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("Item.CreateServerEvent:EventForUninitializedItem" + Name + ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; } int index = components.IndexOf(ic); - if (index == -1) return; + if (index == -1) { return; } object[] extraData = new object[] { NetEntityEvent.Type.ComponentState, index }; ic.ServerAppendExtraData(ref extraData); GameMain.Server.CreateEntityEvent(this, extraData); } + + public void CreateServerEvent(T ic, object[] extraData) where T : ItemComponent, IServerSerializable + { + if (GameMain.Server == null) { return; } + + if (!ItemList.Contains(this)) + { + string errorMsg = "Attempted to create a network event for an item (" + Name + ") that hasn't been fully initialized yet.\n" + Environment.StackTrace.CleanupStackTrace(); + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Item.CreateServerEvent:EventForUninitializedItem" + Name + ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + return; + } + + int index = components.IndexOf(ic); + if (index == -1) { return; } + + object[] data = new object[] { NetEntityEvent.Type.ComponentState, index }.Concat(extraData).ToArray(); + GameMain.Server.CreateEntityEvent(this, data); + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index d8fe84369..043e85256 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -12,7 +12,7 @@ namespace Barotrauma private float lastSentVolume, lastSentOxygen, lastSentFireCount; private float sendUpdateTimer; - private bool decalsChanged; + private bool decalUpdatePending; public override bool IsMouseOn(Vector2 position) { @@ -32,27 +32,21 @@ 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 || pendingSectionUpdates.Count > 0 || - sendUpdateTimer < -NetConfig.SparseHullUpdateInterval) + sendUpdateTimer < -NetConfig.SparseHullUpdateInterval || + decalUpdatePending) { if (sendUpdateTimer < 0.0f) { + if (decalUpdatePending) + { + GameMain.NetworkMember.CreateEntityEvent(this, new object[] { false }); + } if (pendingSectionUpdates.Count > 0) { foreach (int pendingSectionUpdate in pendingSectionUpdates) @@ -120,8 +114,9 @@ namespace Barotrauma 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.Write((byte)decal.SpriteIndex); + float normalizedXPos = MathHelper.Clamp(MathUtils.InverseLerp(0.0f, rect.Width, decal.CenterPosition.X), 0.0f, 1.0f); + float normalizedYPos = MathHelper.Clamp(MathUtils.InverseLerp(-rect.Height, 0.0f, decal.CenterPosition.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); @@ -133,28 +128,8 @@ namespace Barotrauma //used when clients use the water/fire console commands or section / decal updates are received public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { - bool hasExtraData = msg.ReadBoolean(); - if (hasExtraData) - { - 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 + int messageType = msg.ReadRangedInteger(0, 2); + if (messageType == 0) { float newWaterVolume = msg.ReadRangedSingle(0.0f, 1.5f, 8) * Volume; @@ -210,6 +185,37 @@ namespace Barotrauma FireSources.RemoveAt(i); } } + } + else if (messageType == 1) + { + byte decalIndex = msg.ReadByte(); + float decalAlpha = msg.ReadRangedSingle(0.0f, 1.0f, 255); + if (decalIndex < 0 || decalIndex >= decals.Count) { return; } + if (c.Character != null && c.Character.AllowInput && c.Character.SelectedItems.Any(it => it?.GetComponent() != null)) + { + decals[decalIndex].BaseAlpha = decalAlpha; + } + decalUpdatePending = true; + } + else + { + 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); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index 008944852..505dd4621 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -332,7 +332,7 @@ namespace Barotrauma.Networking catch (Exception e) { - string errorMsg = "Error while writing banlist. {" + e + "}\n" + e.StackTrace; + string errorMsg = "Error while writing banlist. {" + e + "}\n" + e.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("Banlist.ServerAdminWrite", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); throw; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index 112c6af27..a8135919f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -1,7 +1,5 @@ -using Barotrauma.Items.Components; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System; -using System.Linq; using System.Text; namespace Barotrauma.Networking @@ -14,19 +12,25 @@ namespace Barotrauma.Networking UInt16 ID = msg.ReadUInt16(); ChatMessageType type = (ChatMessageType)msg.ReadByte(); - string txt = ""; + string txt; - int orderIndex = -1; Character orderTargetCharacter = null; Entity orderTargetEntity = null; - int orderOptionIndex = -1; OrderChatMessage orderMsg = null; + OrderTarget orderTargetPosition = null; if (type == ChatMessageType.Order) { - orderIndex = msg.ReadByte(); + int orderIndex = msg.ReadByte(); orderTargetCharacter = Entity.FindEntityByID(msg.ReadUInt16()) as Character; orderTargetEntity = Entity.FindEntityByID(msg.ReadUInt16()) as Entity; - orderOptionIndex = msg.ReadByte(); + int orderOptionIndex = msg.ReadByte(); + if (msg.ReadBoolean()) + { + var x = msg.ReadSingle(); + var y = msg.ReadSingle(); + var hull = Entity.FindEntityByID(msg.ReadUInt16()) as Hull; + orderTargetPosition = new OrderTarget(new Vector2(x, y), hull, true); + } if (orderIndex < 0 || orderIndex >= Order.PrefabList.Count) { @@ -37,7 +41,7 @@ namespace Barotrauma.Networking Order order = Order.PrefabList[orderIndex]; string orderOption = orderOptionIndex < 0 || orderOptionIndex >= order.Options.Length ? "" : order.Options[orderOptionIndex]; - orderMsg = new OrderChatMessage(order, orderOption, orderTargetEntity, orderTargetCharacter, c.Character); + orderMsg = new OrderChatMessage(order, orderOption, orderTargetPosition ?? orderTargetEntity as ISpatialEntity, orderTargetCharacter, c.Character); txt = orderMsg.Text; } else @@ -114,20 +118,18 @@ namespace Barotrauma.Networking if (type == ChatMessageType.Order) { - if (c.Character == null || c.Character.SpeechImpediment >= 100.0f || c.Character.IsDead) return; - - ChatMessageType messageType = CanUseRadio(orderMsg.Sender) ? ChatMessageType.Radio : ChatMessageType.Default; + if (c.Character == null || c.Character.SpeechImpediment >= 100.0f || c.Character.IsDead) { return; } if (orderMsg.Order.TargetAllCharacters) { - //do nothing + HumanAIController.ReportProblem(orderMsg.Sender, orderMsg.Order); } else if (orderTargetCharacter != null) { - orderTargetCharacter.SetOrder( - new Order(orderMsg.Order.Prefab, orderTargetEntity, (orderTargetEntity as Item)?.Components.FirstOrDefault(ic => ic.GetType() == orderMsg.Order.ItemComponentType)), - orderMsg.OrderOption, orderMsg.Sender); + var order = orderTargetPosition == null ? + new Order(orderMsg.Order.Prefab, orderTargetEntity, orderMsg.Order.Prefab?.GetTargetItemComponent(orderTargetEntity as Item), orderMsg.Sender) : + new Order(orderMsg.Order.Prefab, orderTargetPosition, orderMsg.Sender); + orderTargetCharacter.SetOrder(order, orderMsg.OrderOption, orderMsg.Sender); } - GameMain.Server.SendOrderChatMessage(orderMsg); } else diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs index 264253c0a..020725453 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs @@ -272,7 +272,7 @@ namespace Barotrauma.Networking GameAnalyticsManager.AddErrorEventOnce( "FileSender.Update:Exception", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "FileSender threw an exception when trying to send data:\n" + e.Message + "\n" + e.StackTrace); + "FileSender threw an exception when trying to send data:\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); transfer.Status = FileTransferStatus.Error; return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 71e3035e5..b607fd2b9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -615,14 +615,14 @@ namespace Barotrauma.Networking DebugConsole.ThrowError("Failed to write a network message for the client \"" + c.Name + "\"!", e); string errorMsg = "Failed to write a network message for the client \"" + c.Name + "\"! (MidRoundSyncing: " + c.NeedsMidRoundSync + ")\n" - + e.Message + "\n" + e.StackTrace; + + e.Message + "\n" + e.StackTrace.CleanupStackTrace(); if (e.InnerException != null) { - errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace; + errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace(); } GameAnalyticsManager.AddErrorEventOnce( - "GameServer.Update:ClientWriteFailed" + e.StackTrace, + "GameServer.Update:ClientWriteFailed" + e.StackTrace.CleanupStackTrace(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } @@ -1574,8 +1574,9 @@ namespace Barotrauma.Networking { //if docked to a sub with a smaller ID, don't send an update // (= update is only sent for the docked sub that has the smallest ID, doesn't matter if it's the main sub or a shuttle) - if (sub.Info.IsOutpost || sub.DockedTo.Any(s => s.ID < sub.ID)) continue; - if (!c.PendingPositionUpdates.Contains(sub)) c.PendingPositionUpdates.Enqueue(sub); + if (sub.Info.IsOutpost || sub.DockedTo.Any(s => s.ID < sub.ID)) { continue; } + if (sub.PhysicsBody == null || sub.PhysicsBody.BodyType == FarseerPhysics.BodyType.Static) { continue; } + if (!c.PendingPositionUpdates.Contains(sub)) { c.PendingPositionUpdates.Enqueue(sub); } } foreach (Item item in Item.ItemList) @@ -1796,7 +1797,7 @@ namespace Barotrauma.Networking outmsg.Write(GameMain.NetLobbyScreen.SelectedShuttle.Name); outmsg.Write(GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash.ToString()); - string campaignSubmarineIndexes = string.Empty; + List campaignSubIndices = new List(); if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) { List subList = GameMain.NetLobbyScreen.GetSubList(); @@ -1804,17 +1805,15 @@ namespace Barotrauma.Networking { if (GameMain.NetLobbyScreen.CampaignSubmarines.Contains(subList[i])) { - campaignSubmarineIndexes += i.ToString(); - campaignSubmarineIndexes += ";"; + campaignSubIndices.Add(i); } } - - if (campaignSubmarineIndexes.Length > 0) - { - campaignSubmarineIndexes.Trim(';'); - } } - outmsg.Write(campaignSubmarineIndexes); + outmsg.Write((UInt16)campaignSubIndices.Count); + foreach (int campaignSubIndex in campaignSubIndices) + { + outmsg.Write((UInt16)campaignSubIndex); + } outmsg.Write(serverSettings.Voting.AllowSubVoting); outmsg.Write(serverSettings.Voting.AllowModeVoting); @@ -2293,6 +2292,8 @@ namespace Barotrauma.Networking crewManager?.InitRound(); } + campaign?.LoadPets(); + foreach (Submarine sub in Submarine.MainSubs) { if (sub == null) continue; @@ -2361,7 +2362,7 @@ namespace Barotrauma.Networking bool missionAllowRespawn = campaign == null && (missionMode?.Mission == null || missionMode.Mission.AllowRespawn); bool outpostAllowRespawn = campaign != null && campaign.NextLevel?.Type == LevelData.LevelType.Outpost; - msg.Write(missionAllowRespawn || outpostAllowRespawn); + msg.Write(serverSettings.AllowRespawn && (missionAllowRespawn || outpostAllowRespawn)); msg.Write(serverSettings.AllowDisguises); msg.Write(serverSettings.AllowRewiring); msg.Write(serverSettings.AllowRagdollButton); @@ -2437,7 +2438,7 @@ namespace Barotrauma.Networking if (GameSettings.VerboseLogging) { - Log("Ending the round...\n" + Environment.StackTrace, ServerLog.MessageType.ServerMessage); + Log("Ending the round...\n" + Environment.StackTrace.CleanupStackTrace(), ServerLog.MessageType.ServerMessage); } else @@ -2772,7 +2773,7 @@ namespace Barotrauma.Networking { if (recipient == null) { - string errorMsg = "Attempted to send a chat message to a null client.\n" + Environment.StackTrace; + string errorMsg = "Attempted to send a chat message to a null client.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameServer.SendDirectChatMessage:ClientNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; @@ -3069,12 +3070,13 @@ namespace Barotrauma.Networking Client.UpdateKickVotes(connectedClients); + int minimumKickVotes = Math.Max(1, (int)(connectedClients.Count * serverSettings.KickVoteRequiredRatio)); var clientsToKick = connectedClients.FindAll(c => c.Connection != OwnerConnection && !c.HasPermission(ClientPermissions.Kick) && !c.HasPermission(ClientPermissions.Ban) && !c.HasPermission(ClientPermissions.Unban) && - c.KickVoteCount >= connectedClients.Count * serverSettings.KickVoteRequiredRatio); + c.KickVoteCount >= minimumKickVotes); foreach (Client c in clientsToKick) { var previousPlayer = previousPlayers.Find(p => p.MatchesClient(c)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index 48a031455..3ecf8c7b2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -32,7 +32,7 @@ namespace Barotrauma.Networking createTime = Timing.TotalTime; #if DEBUG - StackTrace = Environment.StackTrace.ToString(); + StackTrace = Environment.StackTrace.CleanupStackTrace(); #endif } @@ -121,12 +121,12 @@ namespace Barotrauma.Networking if (((Entity)entity).Removed && !(entity is Level)) { - DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the entity has been removed.\n"+Environment.StackTrace); + DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the entity has been removed.\n"+Environment.StackTrace.CleanupStackTrace()); return; } if (((Entity)entity).IdFreed) { - DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the ID of the entity has been freed.\n"+Environment.StackTrace); + DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the ID of the entity has been freed.\n"+Environment.StackTrace.CleanupStackTrace()); return; } @@ -197,12 +197,12 @@ namespace Barotrauma.Networking if (GameSettings.VerboseLogging) { string errorMsg = "Failed to read server event for entity \"" + entityName + "\"!"; - GameServer.Log(errorMsg + "\n" + e.StackTrace, ServerLog.MessageType.Error); + GameServer.Log(errorMsg + "\n" + e.StackTrace.CleanupStackTrace(), ServerLog.MessageType.Error); DebugConsole.ThrowError(errorMsg, e); } GameAnalyticsManager.AddErrorEventOnce("ServerEntityEventManager.Read:ReadFailed" + entityName, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Failed to read server event for entity \"" + entityName + "\"!\n" + e.StackTrace); + "Failed to read server event for entity \"" + entityName + "\"!\n" + e.StackTrace.CleanupStackTrace()); } bufferedEvent.IsProcessed = true; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs index 78d625989..eaf7012a1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace Barotrauma.Networking { @@ -21,8 +19,19 @@ namespace Barotrauma.Networking msg.Write((byte)Order.PrefabList.IndexOf(Order.Prefab)); msg.Write(TargetCharacter == null ? (UInt16)0 : TargetCharacter.ID); - msg.Write(TargetEntity == null ? (UInt16)0 : TargetEntity.ID); + msg.Write(TargetEntity is Entity ? (TargetEntity as Entity).ID : (UInt16)0); msg.Write((byte)Array.IndexOf(Order.Prefab.Options, OrderOption)); + if (TargetEntity is OrderTarget orderTarget) + { + msg.Write(true); + msg.Write(orderTarget.Position.X); + msg.Write(orderTarget.Position.Y); + msg.Write(orderTarget.Hull == null ? (UInt16)0 : orderTarget.Hull.ID); + } + else + { + msg.Write(false); + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index 6704bac5f..8d75fa8f4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -124,7 +124,7 @@ namespace Barotrauma.Networking catch (Exception e) { - string errorMsg = "Server failed to read an incoming message. {" + e + "}\n" + e.StackTrace; + string errorMsg = "Server failed to read an incoming message. {" + e + "}\n" + e.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("LidgrenServerPeer.Update:ClientReadException" + e.TargetSite.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index a92d04706..d8674f03d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -87,7 +87,7 @@ namespace Barotrauma.Networking if (!Client.IsValidName(name, serverSettings)) { - RemovePendingClient(pendingClient, DisconnectReason.InvalidName, "The name \"" + name + "\" is invalid"); + RemovePendingClient(pendingClient, DisconnectReason.InvalidName, ""); return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs index 56dda28c4..c53cdff42 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs @@ -101,7 +101,7 @@ namespace Barotrauma.Networking catch (Exception e) { - string errorMsg = "Server failed to read an incoming message. {" + e + "}\n" + e.StackTrace; + string errorMsg = "Server failed to read an incoming message. {" + e + "}\n" + e.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("SteamP2PServerPeer.Update:ClientReadException" + e.TargetSite.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); @@ -293,9 +293,9 @@ namespace Barotrauma.Networking if (!started) { return; } if (!(conn is SteamP2PConnection steamp2pConn)) { return; } + if (sendDisconnectMessage) { SendDisconnectMessage(steamp2pConn.SteamID, msg); } if (connectedClients.Contains(steamp2pConn)) { - if (sendDisconnectMessage) SendDisconnectMessage(steamp2pConn.SteamID, msg); steamp2pConn.Status = NetworkConnectionStatus.Disconnected; connectedClients.Remove(steamp2pConn); OnDisconnect?.Invoke(conn, msg); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index 2226c770b..2705bd8fa 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -41,7 +41,7 @@ namespace Barotrauma setLinuxEnv(); #endif Console.WriteLine("Barotrauma Dedicated Server " + GameMain.Version + - " (" + AssemblyInfo.GetBuildString() + ", branch " + AssemblyInfo.GetGitBranch() + ", revision " + AssemblyInfo.GetGitRevision() + ")"); + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"); string executableDir = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location); Directory.SetCurrentDirectory(executableDir); @@ -99,7 +99,7 @@ namespace Barotrauma sb.AppendLine("\n"); sb.AppendLine("Barotrauma seems to have crashed. Sorry for the inconvenience! "); sb.AppendLine("\n"); - sb.AppendLine("Game version " + GameMain.Version + " (" + AssemblyInfo.GetBuildString() + ", branch " + AssemblyInfo.GetGitBranch() + ", revision " + AssemblyInfo.GetGitRevision() + ")"); + sb.AppendLine("Game version " + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"); if (GameMain.Config != null) { sb.AppendLine("Language: " + (GameMain.Config.Language ?? "none")); @@ -124,7 +124,7 @@ namespace Barotrauma sb.AppendLine("Exception: " + exception.Message + " (" + exception.GetType().ToString() + ")"); sb.AppendLine("Target site: " +exception.TargetSite.ToString()); sb.AppendLine("Stack trace: "); - sb.AppendLine(exception.StackTrace); + sb.AppendLine(exception.StackTrace.CleanupStackTrace()); sb.AppendLine("\n"); if (exception.InnerException != null) @@ -135,7 +135,7 @@ namespace Barotrauma sb.AppendLine("Target site: " + exception.InnerException.TargetSite.ToString()); } sb.AppendLine("Stack trace: "); - sb.AppendLine(exception.InnerException.StackTrace); + sb.AppendLine(exception.InnerException.StackTrace.CleanupStackTrace()); } sb.AppendLine("Last debug messages:"); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index 14ff064fa..6be934070 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -261,7 +261,7 @@ namespace Barotrauma if (GameMain.Server.ServerSettings.SubSelectionMode == SelectionMode.Random) { - var nonShuttles = SubmarineInfo.SavedSubmarines.Where(c => !c.HasTag(SubmarineTag.Shuttle) && !c.HasTag(SubmarineTag.HideInMenus)).ToList(); + var nonShuttles = SubmarineInfo.SavedSubmarines.Where(c => !c.HasTag(SubmarineTag.Shuttle) && !c.HasTag(SubmarineTag.HideInMenus) && c.IsPlayer).ToList(); SelectedSub = nonShuttles[Rand.Range(0, nonShuttles.Count)]; } if (GameMain.Server.ServerSettings.ModeSelectionMode == SelectionMode.Random) diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 079efed39..7aa69fd27 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.10.5.1 + 0.10.6.2 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer @@ -135,6 +135,10 @@ <_Parameter1>GitBranch <_Parameter2>$(BuildBranch) + + <_Parameter1>ProjectDir + <_Parameter2>$(ProjectDir) + diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index 683e82d3c..ba7147768 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -52,11 +52,14 @@ + + + @@ -80,17 +83,22 @@ + + - + + + + diff --git a/Barotrauma/BarotraumaShared/Data/forbiddenwordlist.txt b/Barotrauma/BarotraumaShared/Data/forbiddenwordlist.txt new file mode 100644 index 000000000..10b9a44d3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Data/forbiddenwordlist.txt @@ -0,0 +1,32 @@ +adolf +anal +anus +ass +bitch +blowjob +clitoris +cock +cunt +dick +dildo +dyke +gay +fag +faggot +fuck +hitler +homo +jew +kike +nazi +mudslime +nig +nigger +nigga +penis +pussy +queer +slut +twat +vagina +whore \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index ac64ac356..6eb1cf197 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace Barotrauma { - public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect } + public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe, Freeze, Follow } abstract partial class AIController : ISteerable { @@ -147,6 +147,8 @@ namespace Barotrauma _selectedAiTarget = null; } + public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left; + protected virtual void OnStateChanged(AIState from, AIState to) { } protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs index 403f31dc0..0bcee48f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -38,7 +38,7 @@ namespace Barotrauma { if (float.IsNaN(value)) { - DebugConsole.ThrowError("Attempted to set the SoundRange of an AITarget to NaN.\n" + Environment.StackTrace); + DebugConsole.ThrowError("Attempted to set the SoundRange of an AITarget to NaN.\n" + Environment.StackTrace.CleanupStackTrace()); return; } soundRange = MathHelper.Clamp(value, MinSoundRange, MaxSoundRange); @@ -52,7 +52,7 @@ namespace Barotrauma { if (float.IsNaN(value)) { - DebugConsole.ThrowError("Attempted to set the SightRange of an AITarget to NaN.\n" + Environment.StackTrace); + DebugConsole.ThrowError("Attempted to set the SightRange of an AITarget to NaN.\n" + Environment.StackTrace.CleanupStackTrace()); return; } sightRange = MathHelper.Clamp(value, MinSightRange, MaxSightRange); @@ -74,7 +74,7 @@ namespace Barotrauma { if (!MathUtils.IsValid(value)) { - string errorMsg = "Invalid AITarget sector direction (" + value + ")\n" + Environment.StackTrace; + string errorMsg = "Invalid AITarget sector direction (" + value + ")\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("AITarget.SectorDir:" + entity?.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; @@ -113,11 +113,11 @@ namespace Barotrauma if (entity == null || entity.Removed) { #if DEBUG - DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace); + DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace()); #endif GameAnalyticsManager.AddErrorEventOnce("AITarget.WorldPosition:EntityRemoved", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Attempted to access a removed AITarget\n" + Environment.StackTrace); + "Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace()); return Vector2.Zero; } @@ -132,11 +132,11 @@ namespace Barotrauma if (entity == null || entity.Removed) { #if DEBUG - DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace); + DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace()); #endif GameAnalyticsManager.AddErrorEventOnce("AITarget.WorldPosition:EntityRemoved", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Attempted to access a removed AITarget\n" + Environment.StackTrace); + "Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace()); return Vector2.Zero; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index c6efd4fcb..80282e08a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -48,13 +48,13 @@ namespace Barotrauma private float attackLimbResetTimer; private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0; - public float CombatStrength => Character.Params.AI.CombatStrength; - private float Sight => Character.Params.AI.Sight; - private float Hearing => Character.Params.AI.Hearing; - private float FleeHealthThreshold => Character.Params.AI.FleeHealthThreshold; - private float AggressionGreed => Character.Params.AI.AggressionGreed; - private float AggressionHurt => Character.Params.AI.AggressionHurt; - private bool AggressiveBoarding => Character.Params.AI.AggressiveBoarding; + public float CombatStrength => AIParams.CombatStrength; + private float Sight => AIParams.Sight; + private float Hearing => AIParams.Hearing; + private float FleeHealthThreshold => AIParams.FleeHealthThreshold; + private bool AggressiveBoarding => AIParams.AggressiveBoarding; + + private FishAnimController FishAnimController => Character.AnimController as FishAnimController; //a point in a wall which the Character is currently targeting private WallTarget wallTarget; @@ -70,10 +70,6 @@ namespace Barotrauma _attackingLimb = value; attackVector = null; Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse; - if (Character.AnimController is FishAnimController fishController) - { - fishController.reverse = Reverse; - } } } @@ -92,14 +88,15 @@ namespace Barotrauma private readonly float priorityFearIncreasement = 2; private readonly float memoryFadeTime = 0.5f; - private readonly float avoidTime = 3; private float avoidTimer; + private float observeTimer; public bool StayInsideLevel = true; public LatchOntoAI LatchOntoAI { get; private set; } public SwarmBehavior SwarmBehavior { get; private set; } + public PetBehavior PetBehavior { get; private set; } public CharacterParams.TargetParams SelectedTargetingParams { get { return selectedTargetingParams; } } @@ -107,7 +104,7 @@ namespace Barotrauma { get { - var target = GetTarget(CharacterPrefab.HumanSpeciesName); + var target = GetTargetParams(CharacterPrefab.HumanSpeciesName); return target != null && target.Priority > 0.0f && (target.State == AIState.Attack || target.State == AIState.Aggressive); } } @@ -116,7 +113,7 @@ namespace Barotrauma { get { - var target = GetTarget("room"); + var target = GetTargetParams("room"); return target != null && target.Priority > 0.0f && (target.State == AIState.Attack || target.State == AIState.Aggressive); } } @@ -145,7 +142,19 @@ namespace Barotrauma public bool IsBeingChasedBy(Character c) => c.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity is Character && (enemyAI.State == AIState.Aggressive || enemyAI.State == AIState.Attack); private bool IsBeingChased => SelectedAiTarget?.Entity is Character targetCharacter && IsBeingChasedBy(targetCharacter); - public bool Reverse { get; private set; } + private bool reverse; + public bool Reverse + { + get { return reverse; } + private set + { + reverse = value; + if (FishAnimController != null) + { + FishAnimController.reverse = reverse; + } + } + } public EnemyAIController(Character c, string seed) : base(c) { @@ -196,6 +205,9 @@ namespace Barotrauma case "swarmbehavior": SwarmBehavior = new SwarmBehavior(subElement, this); break; + case "petbehavior": + PetBehavior = new PetBehavior(subElement, this); + break; } } @@ -213,8 +225,74 @@ namespace Barotrauma avoidLookAheadDistance = Math.Max(colliderWidth * 3, 1.5f); } - private CharacterParams.AIParams AIParams => Character.Params.AI; - private CharacterParams.TargetParams GetTarget(string targetTag) => AIParams.GetTarget(targetTag, false); + public CharacterParams.AIParams AIParams => Character.Params.AI; + private CharacterParams.TargetParams GetTargetParams(string targetTag) => AIParams.GetTarget(targetTag, false); + private CharacterParams.TargetParams GetTargetParams(AITarget aiTarget) => GetTargetParams(GetTargetingTag(aiTarget)); + private string GetTargetingTag(AITarget aiTarget) + { + if (aiTarget.Entity == null) { return null; } + string targetingTag = null; + if (aiTarget.Entity is Character targetCharacter) + { + if (targetCharacter.IsDead) + { + targetingTag = "dead"; + } + else if (PetBehavior != null && aiTarget.Entity == PetBehavior.Owner) + { + targetingTag = "owner"; + } + else if (AIParams.TryGetTarget(targetCharacter.SpeciesName, out CharacterParams.TargetParams tP)) + { + targetingTag = tP.Tag; + } + else if (targetCharacter.AIController is EnemyAIController enemy) + { + if (targetCharacter.IsHusk && AIParams.HasTag("husk")) + { + targetingTag = "husk"; + } + else + { + if (enemy.CombatStrength > CombatStrength) + { + targetingTag = "stronger"; + } + else if (enemy.CombatStrength < CombatStrength) + { + targetingTag = "weaker"; + } + } + } + } + else if (aiTarget.Entity is Item targetItem) + { + foreach (var prio in AIParams.Targets) + { + if (targetItem.HasTag(prio.Tag)) + { + targetingTag = prio.Tag; + break; + } + } + if (targetingTag == null) + { + if (targetItem.GetComponent() != null) + { + targetingTag = "sonar"; + } + } + } + else if (aiTarget.Entity is Structure) + { + targetingTag = "wall"; + } + else if (aiTarget.Entity is Hull) + { + targetingTag = "room"; + } + return targetingTag; + } public override void SelectTarget(AITarget target) => SelectTarget(target, 100); @@ -295,8 +373,9 @@ namespace Barotrauma updateMemoriesTimer = updateMemoriesInverval; } if (Character.HealthPercentage <= FleeHealthThreshold && SelectedAiTarget != null && - SelectedAiTarget.Entity is Character target && (target.IsPlayer || IsBeingChasedBy(target))) + SelectedAiTarget.Entity is Character target && (target.IsHuman && CanPerceive(SelectedAiTarget) || IsBeingChasedBy(target))) { + // Keep fleeing if being chased State = AIState.Flee; wallTarget = null; } @@ -306,7 +385,7 @@ namespace Barotrauma { updateTargetsTimer -= deltaTime; } - else + else if (avoidTimer <= 0) { CharacterParams.TargetParams targetingParams = null; UpdateTargets(Character, out targetingParams); @@ -315,11 +394,7 @@ namespace Barotrauma UpdateWallTarget(); } updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f); - if (avoidTimer > 0) - { - State = AIState.Escape; - } - else if (SelectedAiTarget == null) + if (SelectedAiTarget == null) { State = AIState.Idle; } @@ -348,9 +423,13 @@ namespace Barotrauma steeringManager = insideSteering; } + bool useSteeringLengthAsMovementSpeed = State == AIState.Idle && Character.AnimController.InWater; bool run = false; switch (State) { + case AIState.Freeze: + SteeringManager.Reset(); + break; case AIState.Idle: UpdateIdle(deltaTime); break; @@ -420,17 +499,21 @@ namespace Barotrauma } break; case AIState.Protect: + case AIState.Follow: if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed) { State = AIState.Idle; return; } - if (SelectedAiTarget.Entity is Character targetCharacter && targetCharacter.LastAttacker is Character attacker) + if (State == AIState.Protect) { - // Attack the character that attacked the target we are protecting - ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2); - SelectTarget(attacker.AiTarget); - return; + if (SelectedAiTarget.Entity is Character targetCharacter && targetCharacter.LastAttacker is Character attacker && !attacker.Removed && !attacker.IsDead) + { + // Attack the character that attacked the target we are protecting + ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2); + SelectTarget(attacker.AiTarget); + return; + } } float sqrDist = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition); float reactDist = selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget); @@ -446,6 +529,64 @@ namespace Barotrauma UpdateIdle(deltaTime); } break; + case AIState.Observe: + if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed) + { + State = AIState.Idle; + return; + } + run = false; + sqrDist = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition); + reactDist = selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget); + float halfReactDist = reactDist / 2; + float attackDist = selectedTargetingParams != null && selectedTargetingParams.AttackDistance > 0 ? selectedTargetingParams.AttackDistance : halfReactDist; + if (sqrDist > Math.Pow(reactDist, 2)) + { + // Too far to react + UpdateIdle(deltaTime); + } + else if (sqrDist < Math.Pow(attackDist + movementMargin, 2)) + { + movementMargin = attackDist; + SteeringManager.Reset(); + if (Character.AnimController.InWater) + { + useSteeringLengthAsMovementSpeed = true; + Vector2 dir = Vector2.Normalize(SelectedAiTarget.WorldPosition - Character.WorldPosition); + if (sqrDist < Math.Pow(attackDist * 0.75f, 2)) + { + // Keep the distance, if too close + dir = -dir; + useSteeringLengthAsMovementSpeed = false; + Reverse = true; + run = true; + } + else + { + Reverse = false; + } + SteeringManager.SteeringManual(deltaTime, dir * 0.2f); + } + else + { + // TODO: doesn't work right here + FaceTarget(SelectedAiTarget.Entity); + } + observeTimer -= deltaTime; + if (observeTimer < 0) + { + IgnoreTarget(SelectedAiTarget); + State = AIState.Idle; + ResetAITarget(); + } + } + else + { + run = sqrDist > Math.Pow(attackDist * 2, 2); + movementMargin = MathHelper.Clamp(movementMargin -= deltaTime, 0, attackDist); + UpdateFollow(deltaTime); + } + break; default: throw new NotImplementedException(); } @@ -465,7 +606,8 @@ namespace Barotrauma SteerInsideLevel(deltaTime); float speed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun); steeringManager.Update(speed); - Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, State == AIState.Idle && Character.AnimController.InWater ? Steering.Length() : speed); + float targetMovement = useSteeringLengthAsMovementSpeed ? Steering.Length() : speed; + Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, targetMovement); if (Character.CurrentHull != null && Character.AnimController.InWater) { // Halve the swimming speed inside the sub @@ -601,7 +743,7 @@ namespace Barotrauma { // Steer away from the target if in the same room Vector2 escapeDir = Vector2.Normalize(SelectedAiTarget != null ? WorldPosition - SelectedAiTarget.WorldPosition : Character.AnimController.TargetMovement); - if (!MathUtils.IsValid(escapeDir)) escapeDir = Vector2.UnitY; + if (!MathUtils.IsValid(escapeDir)) { escapeDir = Vector2.UnitY; } SteeringManager.SteeringManual(deltaTime, escapeDir); } else if (pathSteering != null) @@ -922,7 +1064,7 @@ namespace Barotrauma } if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls)) { - if (Vector2.DistanceSquared(Character.WorldPosition, attackWorldPos) < 2000 * 2000) + if (wallTarget == null && Vector2.DistanceSquared(Character.WorldPosition, attackWorldPos) < 2000 * 2000) { // Check that we are not bumping into a door or a wall Vector2 rayStart = SimPosition; @@ -1193,7 +1335,7 @@ namespace Barotrauma } if (AIParams.RandomAttack) { - selectedLimb = ToolBox.SelectWeightedRandom(attackLimbs, weights, Rand.RandSync.Server); + selectedLimb = ToolBox.SelectWeightedRandom(attackLimbs, weights, Rand.RandSync.Unsynced); attackLimbs.Clear(); weights.Clear(); } @@ -1268,7 +1410,7 @@ namespace Barotrauma LatchOntoAI?.SetAttachTarget(wall.Submarine.PhysicsBody.FarseerBody, wall.Submarine, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal); if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall)) { - if (AIParams.TargetOuterWalls || wall.prefab.Tags.Contains("inner")) + if (AIParams.TargetOuterWalls || wall.prefab.Tags.Contains("inner") || wall.Submarine != null && wall.Submarine == Character.Submarine) { wallTarget = new WallTarget(sectionPos, wall, sectionIndex); } @@ -1309,11 +1451,12 @@ namespace Barotrauma bool wasLatched = IsLatchedOnSub; Character.AnimController.ReleaseStuckLimbs(); LatchOntoAI?.DeattachFromBody(cooldown: 1); - if (attacker == null || attacker.AiTarget == null) { return; } + if (attacker == null || attacker.AiTarget == null || attacker.Removed || attacker.IsDead) { return; } bool isFriendly = IsFriendly(Character, attacker); if (wasLatched) { - avoidTimer = avoidTime * Rand.Range(0.75f, 1.25f); + State = AIState.Escape; + avoidTimer = AIParams.AvoidTime * Rand.Range(0.75f, 1.25f); if (!isFriendly) { SelectTarget(attacker.AiTarget); @@ -1386,7 +1529,7 @@ namespace Barotrauma } AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, true); - targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AggressionHurt; + targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AIParams.AggressionHurt; // Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown bool retaliate = !isFriendly && SelectedAiTarget != attacker.AiTarget && attacker.Submarine == Character.Submarine; @@ -1411,7 +1554,14 @@ namespace Barotrauma } else if (avoidGunFire) { - avoidTimer = avoidTime * Rand.Range(0.75f, 1.25f); + State = AIState.Escape; + avoidTimer = AIParams.AvoidTime * Rand.Range(0.75f, 1.25f); + SelectTarget(attacker.AiTarget); + } + if (Character.HealthPercentage <= FleeHealthThreshold) + { + State = AIState.Flee; + avoidTimer = AIParams.MinFleeTime * Rand.Range(0.75f, 1.25f); SelectTarget(attacker.AiTarget); } } @@ -1441,7 +1591,7 @@ namespace Barotrauma if (damageTarget.Health > 0) { // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon - selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * AggressionGreed; + selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * AIParams.AggressionGreed; } else { @@ -1484,7 +1634,7 @@ namespace Barotrauma State = AIState.Idle; return; } - if (SelectedAiTarget.Entity is Character target) + if (SelectedAiTarget.Entity is Character || SelectedAiTarget.Entity is Item) { Limb mouthLimb = Character.AnimController.GetLimb(LimbType.Head); if (mouthLimb == null) @@ -1494,12 +1644,38 @@ namespace Barotrauma return; } Vector2 mouthPos = Character.AnimController.SimplePhysicsEnabled ? SimPosition : Character.AnimController.GetMouthPosition().Value; - Vector2 attackSimPosition = Character.GetRelativeSimPosition(target); + Vector2 attackSimPosition = Character.GetRelativeSimPosition(SelectedAiTarget.Entity); Vector2 limbDiff = attackSimPosition - mouthPos; float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 2); if (limbDiff.LengthSquared() < extent * extent) { - Character.SelectCharacter(target); + if (SelectedAiTarget.Entity is Character targetCharacter) + { + Character.SelectCharacter(targetCharacter); + } + else if (SelectedAiTarget.Entity is Item item) + { + if (!item.Removed && item.body != null) + { + float itemBodyExtent = item.body.GetMaxExtent() * 2; + if (Math.Abs(limbDiff.X) < itemBodyExtent && + Math.Abs(limbDiff.Y) < Character.AnimController.Collider.GetMaxExtent() + Character.AnimController.ColliderHeightFromFloor) + { + item.body.LinearVelocity *= 0.9f; + item.body.LinearVelocity -= limbDiff * 0.25f; + + bool wasBroken = item.Condition <= 0.0f; + + item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.1f), deltaTime); + + if (item.Condition <= 0.0f) + { + if (!wasBroken) { PetBehavior?.OnEat(item.GetTags(), 1.0f); } + Entity.Spawner.AddToRemoveQueue(item); + } + } + } + } steeringManager.SteeringManual(deltaTime, Vector2.Normalize(limbDiff) * 3); Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, mouthPos); } @@ -1529,15 +1705,40 @@ namespace Barotrauma State = AIState.Idle; return; } - Vector2 dir = Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition); - if (!MathUtils.IsValid(dir)) + if (Character.CurrentHull != null && PathSteering != null) { - return; + // Inside + Character targetCharacter = SelectedAiTarget.Entity as Character; + if ((Character.AnimController.InWater || !Character.AnimController.CanWalk) && + (targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull) || Character.CanSeeTarget(SelectedAiTarget.Entity))) + { + // Steer towards the target if in the same room and swimming + Vector2 dir = Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition); + if (MathUtils.IsValid(dir)) + { + SteeringManager.SteeringManual(deltaTime, dir); + } + } + else + { + // Use path finding + SteeringManager.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), 2); + if (!PathSteering.IsPathDirty && PathSteering.CurrentPath.Unreachable) + { + // Can't reach + State = AIState.Idle; + return; + } + } } - steeringManager.SteeringManual(deltaTime, dir); - if (Character.AnimController.InWater) + else { - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); + // Outside + SteeringManager.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), 5); + if (Character.AnimController.InWater) + { + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); + } } } @@ -1557,6 +1758,7 @@ namespace Barotrauma foreach (AITarget aiTarget in AITarget.List) { if (!aiTarget.Enabled) { continue; } + if (aiTarget.Entity == null) { continue; } if (ignoredTargets.Contains(aiTarget)) { continue; } if (Level.Loaded != null && aiTarget.WorldPosition.Y > Level.Loaded.Size.Y) { @@ -1579,6 +1781,10 @@ namespace Barotrauma { targetingTag = "dead"; } + else if (PetBehavior != null && aiTarget.Entity == PetBehavior.Owner) + { + targetingTag = "owner"; + } else if (AIParams.TryGetTarget(targetCharacter.SpeciesName, out CharacterParams.TargetParams tP)) { targetingTag = tP.Tag; @@ -1626,7 +1832,7 @@ namespace Barotrauma } } } - else if (aiTarget.Entity != null) + else { // Ignore all structures and items inside wrecks if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.Info.IsWreck) { continue; } @@ -1784,8 +1990,10 @@ namespace Barotrauma { targetingTag = "door"; } - if (door.Item.Submarine == null) { continue;} + if (door.Item.Submarine == null) { continue; } bool isOutdoor = door.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom; + // Ignore inner doors when outside + if (character.CurrentHull == null && !isOutdoor) { continue; } bool isOpen = door.IsOpen || door.IsBroken; if (!isOpen && !canAttackDoors || (isOutdoor && !AIParams.TargetOuterWalls)) { @@ -1795,26 +2003,22 @@ namespace Barotrauma if (isOpen && (!Character.AnimController.CanEnterSubmarine || !AggressiveBoarding)) { // Ignore broken and open doors - // Aggressive boarders don't ignore open doors, because they use them for get in. + // Aggressive boarders don't ignore open doors, because they use them for getting in. continue; } if (AggressiveBoarding) { // Increase the priority if the character is outside and the door is from outside to inside - if (character.CurrentHull == null && isOutdoor) + if (character.CurrentHull == null) { valueModifier *= isOpen ? 5 : 1; } else { - // Inside + // Inside -> ignore open doors and outer doors valueModifier *= isOpen || isOutdoor ? 0 : 1; } } - else if (character.CurrentHull == null) - { - valueModifier = isOutdoor ? 1 : 0; - } } else if (aiTarget.Entity is IDamageable targetDamageable && targetDamageable.Health <= 0.0f) { @@ -1823,8 +2027,20 @@ namespace Barotrauma } if (targetingTag == null) { continue; } - var targetParams = GetTarget(targetingTag); + var targetParams = GetTargetParams(targetingTag); if (targetParams == null) { continue; } + if (targetParams.State == AIState.Observe || targetParams.State == AIState.Eat) + { + if (targetCharacter != null && targetCharacter.Submarine != Character.Submarine) + { + // Don't allow to target characters that are inside a different submarine / outside when we are inside. + continue; + } + } + if (aiTarget.Entity is Item targetItem && targetParams.IgnoreContained && targetItem.ParentInventory != null) + { + continue; + } valueModifier *= targetParams.Priority; if (valueModifier == 0.0f) { continue; } @@ -1894,6 +2110,17 @@ namespace Barotrauma } if (targetCharacter != null) { + if (Character.CurrentHull != null && targetCharacter.CurrentHull != Character.CurrentHull) + { + if (targetParams.State == AIState.Follow || targetParams.State == AIState.Protect || targetParams.State == AIState.Observe) + { + // Ignore targets that cannot see + if (!VisibleHulls.Contains(targetCharacter.CurrentHull)) + { + continue; + } + } + } if (targetCharacter.Submarine != Character.Submarine) { if (targetCharacter.Submarine != null) @@ -1939,7 +2166,7 @@ namespace Barotrauma newTarget = aiTarget; selectedTargetMemory = targetMemory; targetValue = valueModifier; - targetingParams = GetTarget(targetingTag); + targetingParams = GetTargetParams(targetingTag); } } @@ -1999,7 +2226,7 @@ namespace Barotrauma { _selectedAiTarget = null; } - else if (CanPerceive(_selectedAiTarget, distSquared: Vector2.DistanceSquared(Character.WorldPosition, _selectedAiTarget.WorldPosition))) + else if (CanPerceive(_selectedAiTarget)) { var memory = GetTargetMemory(_selectedAiTarget, false); if (memory != null) @@ -2040,7 +2267,7 @@ namespace Barotrauma removals.ForEach(r => targetMemories.Remove(r)); } - private readonly float targetIgnoreTime = 5; + private readonly float targetIgnoreTime = 10; private float targetIgnoreTimer; private readonly HashSet ignoredTargets = new HashSet(); public void IgnoreTarget(AITarget target) @@ -2168,6 +2395,17 @@ namespace Barotrauma } #endregion + protected override void OnTargetChanged(AITarget previousTarget, AITarget newTarget) + { + base.OnTargetChanged(previousTarget, newTarget); + if (newTarget == null) { return; } + var targetParams = GetTargetParams(newTarget); + if (targetParams != null) + { + observeTimer = targetParams.Timer * Rand.Range(0.75f, 1.25f); + } + } + protected override void OnStateChanged(AIState from, AIState to) { LatchOntoAI?.DeattachFromBody(); @@ -2189,13 +2427,17 @@ namespace Barotrauma private bool CanPerceive(AITarget target, float dist = -1, float distSquared = -1) { - if (distSquared > -1) + if (dist > 0) { - return distSquared <= MathUtils.Pow(target.SightRange * Sight, 2) || distSquared <= MathUtils.Pow(target.SoundRange * Hearing, 2); + return dist <= target.SightRange * Sight || dist <= target.SoundRange * Hearing; } else { - return dist <= target.SightRange * Sight || dist <= target.SoundRange * Hearing; + if (distSquared < 0) + { + distSquared = Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition); + } + return distSquared <= MathUtils.Pow(target.SightRange * Sight, 2) || distSquared <= MathUtils.Pow(target.SoundRange * Hearing, 2); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 9d2715b01..541f78e66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -35,6 +35,14 @@ namespace Barotrauma public readonly HashSet UnsafeHulls = new HashSet(); public readonly List IgnoredItems = new List(); + private float respondToAttackTimer; + private const float RespondToAttackInterval = 1.0f; + + /// + /// List of previous attacks done to this character + /// + private readonly Dictionary previousAttackResults = new Dictionary(); + private class HullSafety { public float safety; @@ -138,6 +146,17 @@ namespace Barotrauma } if (isIncapacitated) { return; } + respondToAttackTimer -= deltaTime; + if (respondToAttackTimer <= 0.0f) + { + foreach (var previousAttackResult in previousAttackResults) + { + RespondToAttack(previousAttackResult.Key, previousAttackResult.Value); + } + previousAttackResults.Clear(); + respondToAttackTimer = RespondToAttackInterval; + } + base.Update(deltaTime); foreach (var values in knownHulls) @@ -316,6 +335,7 @@ namespace Barotrauma { if (Character.LockHands) { return; } if (ObjectiveManager.CurrentObjective == null) { return; } + if (Character.CurrentHull == null) { return; } bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold; bool isCarrying = ObjectiveManager.HasActiveObjective() || ObjectiveManager.HasActiveObjective(); @@ -349,7 +369,9 @@ namespace Barotrauma if (!NeedsDivingGear(Character.CurrentHull, out bool needsSuit) || !needsSuit || oxygenLow) { bool shouldKeepTheGearOn = Character.AnimController.HeadInWater + || Character.Submarine.TeamID != Character.TeamID && Character.Submarine.TeamID != Character.TeamType.FriendlyNPC || ObjectiveManager.IsCurrentObjective() + || ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character // wait order || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn); if (oxygenLow && Character.CurrentHull.Oxygen > 0) { @@ -425,12 +447,14 @@ namespace Barotrauma { var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent()) { - DropIfFailsToContain = false + DropIfFails = false }; decontainObjective.Abandoned += () => { + ReequipUnequipped(); IgnoredItems.Add(targetContainer); }; + decontainObjective.Completed += () => ReequipUnequipped(); ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); return; } @@ -451,7 +475,7 @@ namespace Barotrauma { if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List() { InvSlotType.Any })) { - if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) + if (ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { mask.Drop(Character); } @@ -465,7 +489,12 @@ namespace Barotrauma if (targetContainer != null) { var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent()); - decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer); + decontainObjective.Abandoned += () => + { + ReequipUnequipped(); + IgnoredItems.Add(targetContainer); + }; + decontainObjective.Completed += () => ReequipUnequipped(); ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); return; } @@ -476,6 +505,10 @@ namespace Barotrauma } } } + else + { + ReequipUnequipped(); + } } } } @@ -504,7 +537,11 @@ namespace Barotrauma if (targetContainer != null) { var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent()); - decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer); + decontainObjective.Abandoned += () => + { + ReequipUnequipped(); + IgnoredItems.Add(targetContainer); + }; ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); return; } @@ -519,6 +556,18 @@ namespace Barotrauma } } + public void ReequipUnequipped() + { + foreach (var item in unequippedItems) + { + if (item != null && !item.Removed && Character.HasItem(item)) + { + TakeItem(item, Character.Inventory, equip: true, dropOtherIfCannotMove: true, allowSwapping: true, storeUnequipped: false); + } + } + unequippedItems.Clear(); + } + private enum FindItemState { None, @@ -528,10 +577,13 @@ namespace Barotrauma } private FindItemState findItemState; private int itemIndex; - public bool FindSuitableContainer(Item containableItem, out Item suitableContainer) + + public bool FindSuitableContainer(Item containableItem, out Item suitableContainer) => FindSuitableContainer(Character, containableItem, IgnoredItems, ref itemIndex, out suitableContainer); + + public static bool FindSuitableContainer(Character character, Item containableItem, List ignoredItems, ref int itemIndex, out Item suitableContainer) { suitableContainer = null; - if (Character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: IgnoredItems, customPriorityFunction: i => + if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredItems, customPriorityFunction: i => { var container = i.GetComponent(); if (container == null) { return 0; } @@ -663,6 +715,17 @@ namespace Barotrauma } } + public static void ReportProblem(Character reporter, Order order) + { + if (reporter == null || order == null) { return; } + var visibleHulls = new List(reporter.GetVisibleHulls()); + foreach (var hull in visibleHulls) + { + PropagateHullSafety(reporter, hull); + RefreshTargets(reporter, order, hull); + } + } + private void UpdateSpeaking() { if (Character.Oxygen < 20.0f) @@ -683,6 +746,38 @@ namespace Barotrauma public override void OnAttacked(Character attacker, AttackResult attackResult) { + if (Character.IsDead) { return; } + if (attacker == null || Character.IsPlayer) + { + // The player characters need to "respond" to the attack always, because the update loop doesn't run for them. + // Otherwise other NPCs totally ignore when player characters are attacked. + RespondToAttack(attacker, attackResult); + return; + } + if (previousAttackResults.ContainsKey(attacker)) + { + foreach (Affliction newAffliction in attackResult.Afflictions) + { + var matchingAffliction = previousAttackResults[attacker].Afflictions.Find(a => a.Prefab == newAffliction.Prefab && a.Source == newAffliction.Source); + if (matchingAffliction == null) + { + previousAttackResults[attacker].Afflictions.Add(newAffliction); + } + else + { + matchingAffliction.Strength += newAffliction.Strength; + } + } + previousAttackResults[attacker] = new AttackResult(previousAttackResults[attacker].Afflictions, previousAttackResults[attacker].HitLimb); + } + else + { + previousAttackResults.Add(attacker, attackResult); + } + } + + private void RespondToAttack(Character attacker, AttackResult attackResult) + { // excluding poisons etc float realDamage = attackResult.Damage; // including poisons etc @@ -691,7 +786,7 @@ namespace Barotrauma { totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength; } - if (totalDamage <= 0) { return; } + if (totalDamage <= 0.01f) { return; } if (Character.IsBot) { if (attacker != null) @@ -735,7 +830,7 @@ namespace Barotrauma return; } float cumulativeDamage = GetDamageDoneByAttacker(attacker); - if (!Character.IsSecurity && attacker.IsBot && !attacker.IsInstigator) + if (!Character.IsSecurity && attacker.IsBot && Character.CombatAction == null) { if (cumulativeDamage > 1) { @@ -834,7 +929,17 @@ namespace Barotrauma } else { - if (attacker.TeamID == Character.TeamType.FriendlyNPC) + // If there are any enemies around, just ignore the friendly fire + if (Character.CharacterList.Any(ch => ch.Submarine == Character.Submarine && !ch.Removed && !ch.IsDead && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull))) + { + return AIObjectiveCombat.CombatMode.None; + } + if (Character.IsInstigator && attacker.IsPlayer) + { + // The guards don't react when the player attacks instigators. + return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat); + } + else if (attacker.TeamID == Character.TeamType.FriendlyNPC) { if (c.IsSecurity) { @@ -847,11 +952,7 @@ namespace Barotrauma } else { - if (Character.IsInstigator) - { - return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat; - } - else if (cumulativeDamage > dmgThreshold) + if (cumulativeDamage > dmgThreshold) { if (c.IsSecurity) { @@ -1006,39 +1107,54 @@ namespace Barotrauma return true; } - public bool TryToMoveItem(Item item, Inventory targetInventory, bool dropIfCannotMove = true) + private readonly HashSet unequippedItems = new HashSet(); + public bool TakeItem(Item item, Inventory targetInventory, bool equip, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false) { var pickable = item.GetComponent(); - if (pickable == null) { return false; } - int targetSlot = -1; - //check if all the slots required by the item are free - foreach (InvSlotType slots in pickable.AllowedSlots) + if (item.ParentInventory is ItemInventory itemInventory) { - if (slots.HasFlag(InvSlotType.Any)) { continue; } - for (int i = 0; i < targetInventory.Items.Length; i++) + if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; } + } + if (equip) + { + int targetSlot = -1; + //check if all the slots required by the item are free + foreach (InvSlotType slots in pickable.AllowedSlots) { - if (targetInventory is CharacterInventory characterInventory) + if (slots.HasFlag(InvSlotType.Any)) { continue; } + for (int i = 0; i < targetInventory.Items.Length; i++) { - //slot not needed by the item, continue - if (!slots.HasFlag(characterInventory.SlotTypes[i])) { continue; } - } - targetSlot = i; - //slot free, continue - var otherItem = targetInventory.Items[i]; - if (otherItem == null) { continue; } - //try to move the existing item to LimbSlot.Any and continue if successful - if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, new List() { InvSlotType.Any })) - { - continue; - } - if (dropIfCannotMove) - { - //if everything else fails, simply drop the existing item - otherItem.Drop(Character); + if (targetInventory is CharacterInventory characterInventory) + { + //slot not needed by the item, continue + if (!slots.HasFlag(characterInventory.SlotTypes[i])) { continue; } + } + targetSlot = i; + //slot free, continue + var otherItem = targetInventory.Items[i]; + if (otherItem == null) { continue; } + //try to move the existing item to LimbSlot.Any and continue if successful + if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.anySlot)) + { + if (storeUnequipped && targetInventory.Owner == Character) + { + unequippedItems.Add(otherItem); + } + continue; + } + if (dropOtherIfCannotMove) + { + //if everything else fails, simply drop the existing item + otherItem.Drop(Character); + } } } + return targetInventory.TryPutItem(item, targetSlot, allowSwapping, allowCombine: false, Character); + } + else + { + return targetInventory.TryPutItem(item, Character, CharacterInventory.anySlot); } - return targetInventory.TryPutItem(item, targetSlot, false, false, Character); } public static bool NeedsDivingGear(Hull hull, out bool needsSuit) @@ -1107,7 +1223,7 @@ namespace Barotrauma } //if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; } if (!otherCharacter.CanSeeCharacter(thief)) { continue; } - if (!someoneSpoke) + if (!someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f) { if (!item.StolenDuringRound && GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) { @@ -1306,7 +1422,7 @@ namespace Barotrauma // Use the cached visible hulls visibleHulls = VisibleHulls; } - bool ignoreFire = objectiveManager.HasActiveObjective(); + bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective(); bool ignoreWater = HasDivingSuit(character); bool ignoreOxygen = ignoreWater || HasDivingMask(character); bool ignoreEnemies = ObjectiveManager.IsCurrentObjective(); @@ -1391,13 +1507,10 @@ namespace Barotrauma return hullSafety.safety; } - public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left; - public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false) { bool sameTeam = me.TeamID == other.TeamID; - // Only enemies are in the Team "None" - bool friendlyTeam = me.TeamID != Character.TeamType.None && other.TeamID != Character.TeamType.None; + bool friendlyTeam = IsOnFriendlyTeam(GameMain.GameSession?.GameMode, me, other); bool teamGood = sameTeam || friendlyTeam && !onlySameTeam; if (!teamGood) { return false; } bool speciesGood = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); @@ -1413,6 +1526,18 @@ namespace Barotrauma return true; } + private static bool IsOnFriendlyTeam(GameMode mode, Character me, Character other) + { + // Only enemies are in the Team "None" + bool friendlyTeam = me.TeamID != Character.TeamType.None && other.TeamID != Character.TeamType.None; + // When playing a combat mission, we need to be on the same team to be friendlies + if (friendlyTeam && mode is MissionMode mm && mm.Mission is CombatMission) + { + friendlyTeam = me.TeamID == other.TeamID; + } + return friendlyTeam; + } + public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious; public static bool IsTrueForAllCrewMembers(Character character, Func predicate) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index bca8de434..32c922a20 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -190,14 +190,45 @@ namespace Barotrauma } } pathFinder.InsideSubmarine = character.Submarine != null; + pathFinder.ApplyPenaltyToOutsideNodes = character.PressureProtection <= 0; var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility); - bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null; + bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.X) <= 0; 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); + // Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset). + if (IsIdenticalPath()) + { + useNewPath = false; + } + else + { + // Use the new path if it has significantly lower cost (don't change the path if it has marginally smaller cost. This reduces navigating backwards due to new path that is calculated from the node just behind us). + float t = (float)currentPath.CurrentIndex / (currentPath.Nodes.Count - 1); + useNewPath = newPath.Cost < currentPath.Cost * MathHelper.Lerp(0.95f, 0, t); + if (!useNewPath) + { + // 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 = Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2); + } + } + + bool IsIdenticalPath() + { + int nodeCount = newPath.Nodes.Count; + if (nodeCount == currentPath.Nodes.Count) + { + for (int i = 0; i < nodeCount - 1; i++) + { + if (newPath.Nodes[i] != currentPath.Nodes[i]) + { + return false; + } + } + return true; + } + return false; + } } if (useNewPath) { @@ -343,7 +374,7 @@ namespace Barotrauma } return diff; } - else if (!canClimb || character.AnimController.InWater) + else if (character.AnimController.InWater) { // If the character is underwater, we don't need the ladders anymore if (character.IsClimbing && isDiving) @@ -370,7 +401,7 @@ namespace Barotrauma } } } - else if (!IsNextLadderSameAsCurrent) + else if (!canClimb || !IsNextLadderSameAsCurrent) { // Walking horizontally Vector2 colliderBottom = character.AnimController.GetColliderBottom(); @@ -378,6 +409,8 @@ namespace Barotrauma Vector2 velocity = collider.LinearVelocity; // If the character is smaller than this, it would fail to use the waypoint nodes because they are always too high. float minHeight = 1; + // If the character is very thin, without a min value, it would often fail to reach the waypoints, because the horizontal distance is too small. + float minWidth = 0.17f; // Cannot use the head position, because not all characters have head or it can be below the total height of the character float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight); float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X); @@ -386,7 +419,7 @@ namespace Barotrauma var door = currentPath.CurrentNode.ConnectedDoor; bool blockedByDoor = door != null && !door.IsOpen && !door.IsBroken; float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 10, 0, 1)); - float targetDistance = collider.radius * margin; + float targetDistance = Math.Max(collider.radius * margin, minWidth); if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && !blockedByDoor) { currentPath.SkipToNextNode(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 13ab648d6..b63a77d34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -15,6 +15,7 @@ namespace Barotrauma public virtual bool IgnoreUnsafeHulls => false; public virtual bool AbandonWhenCannotCompleteSubjectives => true; public virtual bool AllowSubObjectiveSorting => false; + public virtual bool ForceOrderPriority => true; /// /// Can there be multiple objective instaces of the same type? @@ -34,6 +35,7 @@ namespace Barotrauma public virtual bool AllowAutomaticItemUnequipping => false; public virtual bool AllowOutsideSubmarine => false; public virtual bool AllowInFriendlySubs => false; + public virtual bool AllowInAnySub => false; protected readonly List subObjectives = new List(); private float _cumulatedDevotion; @@ -198,12 +200,10 @@ namespace Barotrauma { get { - if (AllowOutsideSubmarine) { return true; } - if (character.Submarine == null) { return false; } - return - character.Submarine.TeamID == character.TeamID || - (AllowInFriendlySubs && character.Submarine.TeamID == Character.TeamType.FriendlyNPC) || - character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID); + if (!AllowOutsideSubmarine && character.Submarine == null) { return false; } + if (AllowInAnySub) { return true; } + if (AllowInFriendlySubs && character.Submarine.TeamID == Character.TeamType.FriendlyNPC) { return true; } + return character.Submarine.TeamID == character.TeamID || character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID); } } @@ -212,12 +212,14 @@ namespace Barotrauma /// public virtual float GetPriority() { + bool isOrder = objectiveManager.CurrentOrder == this; if (!IsAllowed) { Priority = 0; + Abandon = !isOrder; return Priority; } - if (objectiveManager.CurrentOrder == this) + if (isOrder) { Priority = AIObjectiveManager.OrderPriority; } @@ -306,7 +308,7 @@ namespace Barotrauma return true; } #if DEBUG - DebugConsole.ThrowError("Attempted to add a duplicate subobjective!\n" + Environment.StackTrace); + DebugConsole.ThrowError("Attempted to add a duplicate subobjective!\n" + Environment.StackTrace.CleanupStackTrace()); #endif return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs new file mode 100644 index 000000000..feb1032b7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -0,0 +1,124 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + class AIObjectiveCleanupItem : AIObjective + { + public override string DebugTag => "cleanup item"; + public override bool KeepDivingGearOn => true; + public override bool AllowAutomaticItemUnequipping => false; + + public readonly Item item; + public bool IsPriority { get; set; } + + private readonly List ignoredContainers = new List(); + private AIObjectiveDecontainItem decontainObjective; + private int itemIndex = 0; + + public AIObjectiveCleanupItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) + : base(character, objectiveManager, priorityModifier) + { + this.item = item; + } + + public override float GetPriority() + { + if (!IsAllowed) + { + Priority = 0; + Abandon = true; + return Priority; + } + else + { + float distanceFactor = 0.9f; + if (!IsPriority && item.CurrentHull != character.CurrentHull) + { + 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(0.9f, 0, MathUtils.InverseLerp(0, 5000, dist)); + } + bool isSelected = character.HasItem(item); + 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 + (distanceFactor * PriorityModifier), 0, 1)); + } + return Priority; + } + + protected override void Act(float deltaTime) + { + // Only continue when the get item sub objectives have been completed. + if (subObjectives.Any()) { return; } + if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer)) + { + itemIndex = 0; + if (suitableContainer != null) + { + bool equip = item.HasTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR) || ( + item.GetComponent() == null && + item.AllowedSlots.None(s => + s == InvSlotType.Card || + s == InvSlotType.Head || + s == InvSlotType.Headset || + s == InvSlotType.InnerClothes || + s == InvSlotType.OuterClothes)); + TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent()) + { + Equip = equip, + DropIfFails = true + }, + onCompleted: () => + { + if (equip) + { + HumanAIController.ReequipUnequipped(); + } + IsCompleted = true; + }, + onAbandon: () => + { + if (equip) + { + HumanAIController.ReequipUnequipped(); + } + if (decontainObjective != null && decontainObjective.ContainObjective != null && decontainObjective.ContainObjective.CanBeCompleted) + { + ignoredContainers.Add(suitableContainer); + } + else + { + Abandon = true; + } + }); + } + else + { + Abandon = true; + } + } + else + { + objectiveManager.GetObjective().Wander(deltaTime); + } + } + + protected override bool Check() => IsCompleted; + + public override void Reset() + { + base.Reset(); + ignoredContainers.Clear(); + itemIndex = 0; + decontainObjective = null; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs new file mode 100644 index 000000000..c19f7afa5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -0,0 +1,86 @@ +using Barotrauma.Items.Components; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + class AIObjectiveCleanupItems : AIObjectiveLoop + { + public override string DebugTag => "cleanup items"; + public override bool KeepDivingGearOn => true; + public override bool AllowAutomaticItemUnequipping => false; + public override bool ForceOrderPriority => false; + + public readonly Item prioritizedItem; + + public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Item prioritizedItem = null) + : base(character, objectiveManager, priorityModifier) + { + this.prioritizedItem = prioritizedItem; + } + + protected override float TargetEvaluation() => Targets.Any() ? AIObjectiveManager.RunPriority - 1 : 0; + + protected override bool Filter(Item target) + { + // If the target was selected as a valid target, we'll have to accept it so that the objective can be completed. + // The validity changes when a character picks the item up. + if (!IsValidTarget(target, character)) { return Objectives.ContainsKey(target) && IsItemInsideValidSubmarine(target, character); } + if (target.CurrentHull.FireSources.Count > 0) { return false; } + // Don't repair items in rooms that have enemies inside. + if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } + return true; + } + + protected override IEnumerable GetList() => Item.ItemList; + + protected override AIObjective ObjectiveConstructor(Item item) + => new AIObjectiveCleanupItem(item, character, objectiveManager, priorityModifier: PriorityModifier) + { + IsPriority = prioritizedItem == item + }; + + protected override void OnObjectiveCompleted(AIObjective objective, Item target) + => HumanAIController.RemoveTargets(character, target); + + private static bool IsItemInsideValidSubmarine(Item item, Character character) + { + if (item.CurrentHull == null) { return false; } + if (item.Submarine == null) { return false; } + if (item.Submarine.TeamID != character.TeamID) { return false; } + if (character.Submarine != null) + { + if (!character.Submarine.IsConnectedTo(item.Submarine)) { return false; } + } + return true; + } + + public static bool IsValidTarget(Item item, Character character) + { + if (item == null) { return false; } + if (item.NonInteractable) { return false; } + if (item.ParentInventory != null) { return false; } + if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; } + //var rootContainer = item.GetRootContainer(); + //// Only target items lying on the ground (= not inside a container) (do we need this check?) + //if (rootContainer != null) { return false; } + var pickable = item.GetComponent(); + if (pickable == null) { return false; } + if (pickable is Holdable h && h.Attachable && h.Attached) { return false; } + var wire = item.GetComponent(); + if (wire != null) + { + if (wire.Connections.Any()) { return false; } + } + else + { + var connectionPanel = item.GetComponent(); + if (connectionPanel != null && connectionPanel.Connections.Any(c => c.Wires.Any(w => w != null))) + { + return false; + } + } + return item.Prefab.PreferredContainers.Any(); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 9a894bba1..7311434af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -15,6 +15,7 @@ namespace Barotrauma public override bool KeepDivingGearOn => true; public override bool IgnoreUnsafeHulls => true; public override bool AllowOutsideSubmarine => true; + public override bool AllowInAnySub => true; private readonly CombatMode initialMode; @@ -54,8 +55,8 @@ namespace Barotrauma if (_weaponComponent == null) { _weaponComponent = - Weapon.GetComponent() as ItemComponent ?? - Weapon.GetComponent() as ItemComponent ?? + Weapon.GetComponent() ?? + Weapon.GetComponent() ?? Weapon.GetComponent() as ItemComponent; } return _weaponComponent; @@ -144,6 +145,7 @@ namespace Barotrauma if (Enemy.Submarine == null || (Enemy.Submarine.TeamID != character.TeamID && Enemy.Submarine != character.Submarine)) { Priority = 0; + Abandon = true; return Priority; } } @@ -760,11 +762,7 @@ namespace Barotrauma 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); - } - else + if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true)) { #if DEBUG DebugConsole.NewMessage($"{character.Name}: Failed to handcuff the target.", Color.Red); @@ -777,7 +775,7 @@ namespace Barotrauma if (item.StolenDuringRound) { item.Drop(character); - character.Inventory.TryPutItem(item, character, new List() { InvSlotType.Any }); + character.Inventory.TryPutItem(item, character, CharacterInventory.anySlot); } } character.Speak(TextManager.Get("DialogTargetArrested"), null, 3.0f, "targetarrested", 30.0f); @@ -885,7 +883,11 @@ namespace Barotrauma private void Attack(float deltaTime) { - character.CursorPosition = Enemy.Position; + character.CursorPosition = Enemy.WorldPosition; + if (character.Submarine != null) + { + character.CursorPosition -= character.Submarine.Position; + } visibilityCheckTimer -= deltaTime; if (visibilityCheckTimer <= 0.0f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 9e282bb99..cde9a0e2e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -142,6 +142,7 @@ namespace Barotrauma } else { + // TODO: should we just use GetItem? TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear) { DialogueIdentifier = "dialogcannotreachtarget", @@ -153,27 +154,34 @@ namespace Barotrauma } else { - // No matching items in the inventory, try to get an item - TryAddSubObjective(ref getItemObjective, () => - new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, equip: Equip, checkInventory: checkInventory, spawnItemIfNotFound: spawnItemIfNotFound) - { - GetItemPriority = GetItemPriority, - ignoredContainerIdentifiers = ignoredContainerIdentifiers, - ignoredItems = containedItems, - AllowToFindDivingGear = AllowToFindDivingGear, - AllowDangerousPressure = AllowDangerousPressure, - TargetCondition = ConditionLevel - }, onAbandon: () => - { - Abandon = true; - }, onCompleted: () => - { - if (getItemObjective?.TargetItem != null) + if (character.Submarine == null) + { + Abandon = true; + } + else + { + // No matching items in the inventory, try to get an item + TryAddSubObjective(ref getItemObjective, () => + new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, equip: Equip, checkInventory: checkInventory, spawnItemIfNotFound: spawnItemIfNotFound) { - containedItems.Add(getItemObjective.TargetItem); - } - RemoveSubObjective(ref getItemObjective); - }); + GetItemPriority = GetItemPriority, + ignoredContainerIdentifiers = ignoredContainerIdentifiers, + ignoredItems = containedItems, + AllowToFindDivingGear = AllowToFindDivingGear, + AllowDangerousPressure = AllowDangerousPressure, + TargetCondition = ConditionLevel + }, onAbandon: () => + { + Abandon = true; + }, onCompleted: () => + { + if (getItemObjective?.TargetItem != null) + { + containedItems.Add(getItemObjective.TargetItem); + } + RemoveSubObjective(ref getItemObjective); + }); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index c155cc3bb..b00e588cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -16,9 +16,12 @@ namespace Barotrauma private ItemContainer targetContainer; private readonly Item targetItem; - private AIObjectiveGoTo goToObjective; + private AIObjectiveGetItem getItemObjective; private AIObjectiveContainItem containObjective; + public AIObjectiveGetItem GetItemObjective => getItemObjective; + public AIObjectiveContainItem ContainObjective => containObjective; + public bool Equip { get; set; } /// @@ -26,7 +29,7 @@ namespace Barotrauma /// In both cases abandons the objective. /// Note that has no effect if the target container was not defined (always drops) -> completes when the item is dropped. /// - public bool DropIfFailsToContain { get; set; } = true; + public bool DropIfFails { get; set; } = true; public AIObjectiveDecontainItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) @@ -74,54 +77,30 @@ namespace Barotrauma return; } } - else + else if (targetContainer.Inventory.Items.Contains(itemToDecontain)) { - if (targetContainer.Inventory.Items.Contains(itemToDecontain)) - { - IsCompleted = true; - return; - } + IsCompleted = true; + return; } - if (goToObjective == null && !itemToDecontain.IsOwnedBy(character)) + if (getItemObjective == null && !itemToDecontain.IsOwnedBy(character)) { - if (sourceContainer == null) - { - Abandon = true; - return; - } - if (!character.CanInteractWith(sourceContainer.Item, out _, checkLinked: false)) - { - TryAddSubObjective(ref goToObjective, - constructor: () => new AIObjectiveGoTo(sourceContainer.Item, character, objectiveManager) - { - // If the container changes, the item is no longer where it was - abortCondition = () => itemToDecontain.Container != sourceContainer.Item, - DialogueIdentifier = "dialogcannotreachtarget", - TargetName = sourceContainer.Item.Name - }, - onAbandon: () => Abandon = true); - return; - } + TryAddSubObjective(ref getItemObjective, + constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip), + onAbandon: () => Abandon = true); + return; } if (targetContainer != null) { TryAddSubObjective(ref containObjective, constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager) { - Equip = this.Equip, + Equip = Equip, RemoveEmpty = false, - GetItemPriority = this.GetItemPriority, + GetItemPriority = GetItemPriority, ignoredContainerIdentifiers = sourceContainer != null ? new string[] { sourceContainer.Item.Prefab.Identifier } : null }, onCompleted: () => IsCompleted = true, - onAbandon: () => - { - if (DropIfFailsToContain) - { - itemToDecontain.Drop(character); - } - Abandon = true; - }); + onAbandon: () => Abandon = true); } else { @@ -133,8 +112,17 @@ namespace Barotrauma public override void Reset() { base.Reset(); - goToObjective = null; + getItemObjective = null; containObjective = null; } + + protected override void OnAbandon() + { + base.OnAbandon(); + if (DropIfFails && targetItem != null && targetItem.IsOwnedBy(character)) + { + targetItem.Drop(character); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 82038d23b..e095d18cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -13,6 +13,8 @@ namespace Barotrauma public override bool ConcurrentObjectives => true; public override bool KeepDivingGearOn => true; + public override bool AllowInAnySub => true; + private readonly Hull targetHull; private AIObjectiveGetItem getExtinguisherObjective; @@ -30,12 +32,15 @@ namespace Barotrauma if (!IsAllowed) { Priority = 0; + Abandon = true; return Priority; } - if (!objectiveManager.IsCurrentOrder() - && Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) + bool isOrder = objectiveManager.IsCurrentOrder(); + if (!isOrder && Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { + // Don't go into rooms with any enemies, unless it's an order Priority = 0; + Abandon = true; } else { @@ -43,14 +48,22 @@ namespace Barotrauma yDist = yDist > 100 ? yDist * 3 : 0; float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist; float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist)); - if (targetHull == character.CurrentHull) + if (targetHull == character.CurrentHull || HumanAIController.VisibleHulls.Contains(targetHull)) { distanceFactor = 1; } float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull); - float severityFactor = MathHelper.Lerp(0, 1, severity / 100); - float devotion = CumulatedDevotion / 100; - Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (severityFactor * distanceFactor * PriorityModifier), 0, 1)); + if (severity > 0.5f && !isOrder) + { + // Ignore severe fires unless ordered. (Let the fire drain all the oxygen instead). + Priority = 0; + Abandon = true; + } + else + { + float devotion = CumulatedDevotion / 100; + Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); + } } return Priority; } @@ -131,13 +144,16 @@ namespace Barotrauma if (move) { //go to the first firesource - TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range / 2) + if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range / 2) + { + DialogueIdentifier = "dialogcannotreachfire", + TargetName = fs.Hull.DisplayName + }, + onAbandon: () => Abandon = true, + onCompleted: () => RemoveSubObjective(ref gotoObjective))) { - DialogueIdentifier = "dialogcannotreachfire", - TargetName = fs.Hull.DisplayName - }, - onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref gotoObjective)); + gotoObjective.requiredCondition = () => HumanAIController.VisibleHulls.Contains(fs.Hull); + } } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs index c34f28aa2..b877fd2ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs @@ -1,6 +1,8 @@ -using System.Linq; +using System; +using System.Linq; using System.Collections.Generic; using Barotrauma.Extensions; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -8,14 +10,22 @@ namespace Barotrauma { public override string DebugTag => "extinguish fires"; public override bool ForceRun => true; + public override bool AllowInAnySub => true; public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } protected override bool Filter(Hull hull) => IsValidTarget(hull, character); - protected override float TargetEvaluation() => Targets.Sum(t => GetFireSeverity(t)); + protected override float TargetEvaluation() => + // If any target is visible -> 100 priority + Targets.Any(t => t == character.CurrentHull || HumanAIController.VisibleHulls.Contains(t)) ? 100 : + // Else based on the fire severity + Targets.Sum(t => GetFireSeverity(t) * 100); - public static float GetFireSeverity(Hull hull) => hull.FireSources.Sum(fs => fs.Size.X); + /// + /// 0-1 based on the horizontal size of all of the fires in the hull. + /// + public static float GetFireSeverity(Hull hull) => MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, Math.Min(hull.Rect.Width, 1000), hull.FireSources.Sum(fs => fs.Size.X))); protected override IEnumerable GetList() => Hull.hullList; @@ -31,22 +41,7 @@ namespace Barotrauma if (hull.FireSources.None()) { return false; } if (hull.Submarine == null) { return false; } if (character.Submarine == null) { return false; } - if (!character.Submarine.IsConnectedTo(hull.Submarine)) { return false; } - if (character.AIController is HumanAIController humanAI) - { - 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; - } - } - } + if (!character.Submarine.IsEntityFoundOnThisSub(hull, includingConnectedSubs: true)) { 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 b3cd2cb0d..f33a2ed7f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -17,7 +17,7 @@ namespace Barotrauma private Item targetItem; public static float MIN_OXYGEN = 10; - public static string HEAVY_DIVING_GEAR = "heavydiving"; + public static string HEAVY_DIVING_GEAR = "deepdiving"; public static string LIGHT_DIVING_GEAR = "lightdiving"; public static string OXYGEN_SOURCE = "oxygensource"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 13d99b29f..1d98fcf52 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -14,8 +14,9 @@ namespace Barotrauma public override bool IgnoreUnsafeHulls => true; public override bool ConcurrentObjectives => true; public override bool AllowOutsideSubmarine => true; + public override bool AllowInAnySub => true; 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); } + public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); } // TODO: expose? const float priorityIncrease = 100; @@ -203,11 +204,15 @@ namespace Barotrauma { // 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 the target hull is inside a submarine that is not our main sub, just ignore it normally when it cannot be reached. This happens with outposts. + if (goToObjective != null) { if (goToObjective.Target is Hull hull) { - HumanAIController.UnreachableHulls.Add(hull); + if (currentHull != null || !Submarine.MainSubs.Contains(hull.Submarine)) + { + HumanAIController.UnreachableHulls.Add(hull); + } } } RemoveSubObjective(ref goToObjective); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 8723d0f1a..e95d3cabf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -12,6 +12,7 @@ namespace Barotrauma public override string DebugTag => "fix leak"; public override bool ForceRun => true; public override bool KeepDivingGearOn => true; + public override bool AllowInAnySub => true; public Gap Leak { get; private set; } @@ -35,11 +36,7 @@ namespace Barotrauma if (!IsAllowed) { Priority = 0; - return Priority; - } - if (Leak.Removed || Leak.Open <= 0) - { - Priority = 0; + Abandon = true; } else if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective()?.Leak == Leak)) { @@ -116,7 +113,7 @@ namespace Barotrauma { HumanAIController.AnimController.Crouching = true; } - float reach = repairTool.Range + ConvertUnits.ToDisplayUnits(((HumanoidAnimController)character.AnimController).ArmLength); + float reach = CalculateReach(repairTool, character); bool canOperate = toLeak.LengthSquared() < reach * reach; if (canOperate) { @@ -144,7 +141,7 @@ namespace Barotrauma onAbandon: () => { if (Check()) { IsCompleted = true; } - else if ((Leak.WorldPosition - character.WorldPosition).LengthSquared() > reach * reach * 2) + else if ((Leak.WorldPosition - character.WorldPosition).LengthSquared() > MathUtils.Pow(reach * 2, 2)) { // Too far Abandon = true; @@ -167,5 +164,14 @@ namespace Barotrauma gotoObjective = null; operateObjective = null; } + + public static float CalculateReach(RepairTool repairTool, Character character) + { + float armLength = ConvertUnits.ToDisplayUnits(((HumanoidAnimController)character.AnimController).ArmLength); + // This is an approximation, because we don't know the exact reach until the pose is taken. + // And even then the actual range depends on the direction we are aiming to. + // Found out that without any multiplier the value (209) is often too short. + return repairTool.Range + armLength * 1.2f; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index f77e9fb0c..60f172712 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -9,6 +9,7 @@ namespace Barotrauma public override string DebugTag => "fix leaks"; public override bool ForceRun => true; public override bool KeepDivingGearOn => true; + public override bool AllowInAnySub => true; private Hull PrioritizedHull { get; set; } public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Hull prioritizedHull = null) : base(character, objectiveManager, priorityModifier) @@ -71,12 +72,9 @@ namespace Barotrauma { if (gap == null) { return false; } if (gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null)) { return false; } - if (gap.Submarine == null) { return false; } - if (gap.Submarine.TeamID != character.TeamID) { return false; } - if (character.Submarine != null) - { - if (!character.Submarine.IsConnectedTo(gap.Submarine)) { return false; } - } + if (gap.Submarine == null || character.Submarine == null) { return false; } + // Don't allow going into another sub, unless it's connected and of the same team and type. + if (!character.Submarine.IsEntityFoundOnThisSub(gap, includingConnectedSubs: true)) { 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 4defd8d0c..4eda798d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -37,6 +37,7 @@ namespace Barotrauma public static float DefaultReach = 100; public bool AllowToFindDivingGear { get; set; } = true; + public bool MustBeSpecificItem { get; set; } public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) @@ -84,6 +85,11 @@ namespace Barotrauma Abandon = true; return; } + if (character.Submarine == null) + { + Abandon = true; + return; + } if (identifiersOrTags != null && !isDoneSeeking) { if (checkInventory) @@ -109,7 +115,10 @@ namespace Barotrauma } } FindTargetItem(); - objectiveManager.GetObjective().Wander(deltaTime); + if (!objectiveManager.IsCurrentOrder()) + { + objectiveManager.GetObjective().Wander(deltaTime); + } return; } } @@ -137,7 +146,8 @@ namespace Barotrauma if (originalTarget == null) { // Try again - Reset(); + ignoredItems.Add(targetItem); + ResetInternal(); } else { @@ -176,12 +186,8 @@ namespace Barotrauma return; } - if (HumanAIController.TryToMoveItem(targetItem, character.Inventory)) + if (HumanAIController.TakeItem(targetItem, character.Inventory, equip, storeUnequipped: true)) { - if (equip) - { - targetItem.Equip(character); - } IsCompleted = true; } else @@ -207,8 +213,16 @@ namespace Barotrauma }, onAbandon: () => { - ignoredItems.Add(targetItem); - Reset(); + if (originalTarget == null) + { + // Try again + ignoredItems.Add(targetItem); + ResetInternal(); + } + else + { + Abandon = true; + } }, onCompleted: () => RemoveSubObjective(ref goToObjective)); } @@ -235,14 +249,18 @@ namespace Barotrauma Submarine mySub = character.Submarine; if (itemSub == null) { 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 (!mySub.IsConnectedTo(itemSub)) { continue; } + // Don't allow going into another sub, unless it's connected and of the same team and type. + if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { continue; } if (character.IsItemTakenBySomeoneElse(item)) { continue; } + if (item.ParentInventory is ItemInventory itemInventory) + { + if (!itemInventory.Container.HasRequiredItems(character, addMessage: false)) { continue; } + } float itemPriority = 1; if (GetItemPriority != null) { @@ -272,7 +290,7 @@ namespace Barotrauma 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(", ", identifiersOrTags)}, 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) or tag(s): {string.Join(", ", identifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow); #endif Abandon = true; } @@ -291,7 +309,7 @@ namespace Barotrauma else { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow); + DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow); #endif Abandon = true; } @@ -330,11 +348,28 @@ namespace Barotrauma public override void Reset() { base.Reset(); + ResetInternal(); + } + + /// + /// Does not reset the ignored items list + /// + private void ResetInternal() + { goToObjective = null; targetItem = originalTarget; moveToTarget = targetItem?.GetRootInventoryOwner(); isDoneSeeking = false; currSearchIndex = 0; } + + protected override void OnAbandon() + { + base.OnAbandon(); + if (objectiveManager.CurrentOrder != null) + { + character.Speak(TextManager.Get("DialogCannotFindItem"), null, 0.0f, "cannotfinditem", 10.0f); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 1430b5155..fc5852c73 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -1,5 +1,6 @@ using Microsoft.Xna.Framework; using System; +using System.Collections.Generic; using System.Linq; namespace Barotrauma @@ -50,6 +51,7 @@ namespace Barotrauma public override bool AbandonWhenCannotCompleteSubjectives => !repeat; public override bool AllowOutsideSubmarine => AllowGoingOutside; + public override bool AllowInAnySub => true; public string DialogueIdentifier { get; set; } public string TargetName { get; set; } @@ -60,17 +62,27 @@ namespace Barotrauma public override float GetPriority() { + bool isOrder = objectiveManager.CurrentOrder == this; + if (!IsAllowed) + { + Priority = 0; + Abandon = !isOrder; + return Priority; + } if (followControlledCharacter && Character.Controlled == null) { Priority = 0; + Abandon = !isOrder; } if (Target is Entity e && e.Removed) { Priority = 0; + Abandon = !isOrder; } if (IgnoreIfTargetDead && Target is Character character && character.IsDead) { Priority = 0; + Abandon = !isOrder; } else { @@ -84,7 +96,7 @@ namespace Barotrauma } else { - Priority = objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : 10; + Priority = isOrder ? AIObjectiveManager.OrderPriority : 10; } } return Priority; @@ -93,7 +105,7 @@ namespace Barotrauma public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0) : base(character, objectiveManager, priorityModifier) { - this.Target = target; + Target = target; this.repeat = repeat; waitUntilPathUnreachable = 3.0f; this.getDivingGearIfNeeded = getDivingGearIfNeeded; @@ -260,61 +272,156 @@ namespace Barotrauma } } } + if (!character.AnimController.InWater) + { + useScooter = false; + checkScooterTimer = 0; + } + else if (checkScooterTimer <= 0) + { + useScooter = false; + checkScooterTimer = checkScooterTime; + string scooterTag = "scooter"; + string batteryTag = "mobilebattery"; + Item scooter = null; + bool isScooterEquipped = false; + float closeEnough = 250; + float squaredDistance = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition); + bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!mimic || + (Target is Character targetCharacter && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false)) || squaredDistance > Math.Pow(closeEnough * 2, 2)); + if (HumanAIController.HasItem(character, scooterTag, out IEnumerable equippedScooters, batteryTag, requireEquipped: true)) + { + scooter = equippedScooters.FirstOrDefault(); + isScooterEquipped = scooter != null; + } + else if (shouldUseScooter && HumanAIController.HasItem(character, scooterTag, out IEnumerable scooters, batteryTag, requireEquipped: false)) + { + scooter = scooters.FirstOrDefault(); + if (scooter != null) + { + isScooterEquipped = HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false); + } + } + if (scooter != null && isScooterEquipped) + { + if (shouldUseScooter) + { + useScooter = true; + } + else + { + // Unequip + character.Inventory.TryPutItem(scooter, character, CharacterInventory.anySlot); + } + } + } + else + { + checkScooterTimer -= deltaTime; + } if (SteeringManager == PathSteering) { Func nodeFilter = null; if (isInside && !AllowGoingOutside) { - nodeFilter = node => node.Waypoint.CurrentHull != null; + nodeFilter = n => n.Waypoint.CurrentHull != null; } - PathSteering.SteeringSeek(character.GetRelativeSimPosition(Target), 1, n => - { - if (n.Waypoint.isObstructed) { return false; } - return (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null); - }, endNodeFilter, nodeFilter, CheckVisibility); + + PathSteering.SteeringSeek(character.GetRelativeSimPosition(Target), 1, + startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null), + endNodeFilter, + nodeFilter, + CheckVisibility); + if (!isInside && PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable) { - SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition)); + if (useScooter) + { + UseScooter(Target.WorldPosition); + } + else + { + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition)); + if (character.AnimController.InWater) + { + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 2); + } + } + } + else if (useScooter && PathSteering.CurrentPath?.CurrentNode != null) + { + UseScooter(PathSteering.CurrentPath.CurrentNode.WorldPosition); + } + } + else + { + if (useScooter) + { + UseScooter(Target.WorldPosition); + } + else + { + SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10); if (character.AnimController.InWater) { SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15); } } } - else + } + + void UseScooter(Vector2 targetWorldPos) + { + SteeringManager.Reset(); + character.CursorPosition = targetWorldPos; + if (character.Submarine != null) { - SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10); - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15); + character.CursorPosition -= character.Submarine.Position; } + Vector2 dir = Vector2.Normalize(character.CursorPosition - character.Position); + if (!MathUtils.IsValid(dir)) { dir = Vector2.UnitY; } + SteeringManager.SteeringManual(1.0f, dir); + character.SetInput(InputType.Aim, false, true); + character.SetInput(InputType.Shoot, false, true); } } - public Hull GetTargetHull() + private bool useScooter; + private float checkScooterTimer; + private readonly float checkScooterTime = 0.2f; + + public Hull GetTargetHull() => GetTargetHull(Target); + + public static Hull GetTargetHull(ISpatialEntity target) { - if (Target is Hull h) + if (target is Hull h) { return h; } - else if (Target is Item i) + else if (target is Item i) { return i.CurrentHull; } - else if (Target is Character c) + else if (target is Character c) { return c.CurrentHull; } - else if (Target is Gap g) + else if (target is Gap g) { return g.FlowTargetHull; } - else if (Target is WayPoint wp) + else if (target is WayPoint wp) { return wp.CurrentHull; } - else if (Target is FireSource fs) + else if (target is FireSource fs) { return fs.Hull; } + else if (target is OrderTarget ot) + { + return ot.Hull; + } return null; } @@ -361,7 +468,7 @@ namespace Barotrauma { if (Target is Item item) { - if (character.CanInteractWith(item, out _, checkLinked: false)) { IsCompleted = true; } + if (!character.IsClimbing && character.CanInteractWith(item, out _, checkLinked: false)) { IsCompleted = true; } } else if (Target is Character targetCharacter) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 28ad9c764..87bca211c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; using System; @@ -11,14 +12,14 @@ namespace Barotrauma { public override string DebugTag => "idle"; public override bool AllowAutomaticItemUnequipping => true; - public override bool AllowOutsideSubmarine => true; + public override bool AllowInAnySub => true; private BehaviorType behavior; public BehaviorType Behavior { get { return behavior; } - set - { + set + { behavior = value; switch (behavior) { @@ -27,16 +28,13 @@ namespace Barotrauma newTargetIntervalMax = 20; standStillMin = 2; standStillMax = 10; - walkDurationMin = 5; - walkDurationMax = 10; break; case BehaviorType.Passive: + case BehaviorType.StayInHull: newTargetIntervalMin = 60; newTargetIntervalMax = 120; standStillMin = 30; standStillMax = 60; - walkDurationMin = 5; - walkDurationMax = 10; break; } } @@ -46,8 +44,8 @@ namespace Barotrauma private float newTargetIntervalMax; private float standStillMin; private float standStillMax; - private float walkDurationMin; - private float walkDurationMax; + private readonly float walkDurationMin = 5; + private readonly float walkDurationMax = 10; public enum BehaviorType { @@ -55,7 +53,7 @@ namespace Barotrauma Passive, StayInHull } - + public Hull TargetHull { get; set; } private Hull currentTarget; private float newTargetTimer; @@ -86,7 +84,7 @@ namespace Barotrauma protected override bool Check() => false; public override bool CanBeCompleted => true; - public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace); } + public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); } public readonly HashSet PreferredOutpostModuleTypes = new HashSet(); @@ -142,6 +140,8 @@ namespace Barotrauma timerMargin = 0; } + private bool IsSteeringFinished() => PathSteering.CurrentPath != null && (PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable); + protected override void Act(float deltaTime) { if (PathSteering == null) { return; } @@ -164,12 +164,30 @@ namespace Barotrauma character.DeselectCharacter(); } - if (behavior != BehaviorType.StayInHull) + if (!character.IsClimbing) { - bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || - (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); + character.SelectedConstruction = null; + } - bool IsSteeringFinished() => PathSteering.CurrentPath != null && (PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable); + CleanupItems(deltaTime); + + if (behavior == BehaviorType.StayInHull) + { + currentTarget = TargetHull; + bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing; + if (stayInHull) + { + Wander(deltaTime); + } + else if (currentTarget != null) + { + PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null); + } + } + else + { + bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || + (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); if (currentTarget != null && !currentTargetIsInvalid) { @@ -226,7 +244,7 @@ namespace Barotrauma searchingNewHull = true; return; } - else if (targetHulls.Count > 0) + else if (targetHulls.Any()) { //choose a random available hull currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced); @@ -242,12 +260,13 @@ namespace Barotrauma }); if (path.Unreachable) { - //can't go to this room, remove it from the list and try another room next frame + //can't go to this room, remove it from the list and try another room int index = targetHulls.IndexOf(currentTarget); targetHulls.RemoveAt(index); hullWeights.RemoveAt(index); PathSteering.Reset(); currentTarget = null; + SetTargetTimerLow(); return; } searchingNewHull = false; @@ -263,47 +282,25 @@ namespace Barotrauma { character.AIController.SelectTarget(currentTarget.AiTarget); string errorMsg = null; - #if DEBUG +#if DEBUG bool isRoomNameFound = currentTarget.DisplayName != null; errorMsg = "(Character " + character.Name + " idling, target " + (isRoomNameFound ? currentTarget.DisplayName : currentTarget.ToString()) + ")"; - #endif +#endif var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: errorMsg, nodeFilter: node => node.Waypoint.CurrentHull != null); PathSteering.SetPath(path); } SetTargetTimerNormal(); - } + } newTargetTimer -= deltaTime; - } - //wander randomly - // - if reached the end of the path - // - if the target is unreachable - // - if the path requires going outside - if (!character.IsClimbing) - { - if (behavior == BehaviorType.StayInHull || SteeringManager != PathSteering || (PathSteering.CurrentPath != null && - (PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes))) + if (!character.IsClimbing && IsSteeringFinished()) { Wander(deltaTime); - return; } - character.SelectedConstruction = null; - } - - if (currentTarget != null) - { - if (SteeringManager == PathSteering) + else if (currentTarget != null) { PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null); } - else - { - character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(currentTarget)); - } - } - else - { - Wander(deltaTime); } } @@ -328,7 +325,7 @@ namespace Barotrauma { //if there are characters too close on both sides, don't try to steer away from them //because it'll cause the character to spaz out trying to avoid both - if (tooCloseCharacter != null && + if (tooCloseCharacter != null && Math.Sign(tooCloseCharacter.WorldPosition.X - character.WorldPosition.X) != Math.Sign(c.WorldPosition.X - character.WorldPosition.X)) { tooCloseCharacter = null; @@ -336,7 +333,7 @@ namespace Barotrauma } tooCloseCharacter = c; } - HumanAIController.FaceTarget(c); + HumanAIController.FaceTarget(c); } } @@ -344,7 +341,7 @@ namespace Barotrauma { Vector2 diff = character.WorldPosition - tooCloseCharacter.WorldPosition; if (diff.LengthSquared() < 0.0001f) { diff = Rand.Vector(1.0f); } - if (Math.Abs(diff.X) > 0 && + 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 @@ -459,6 +456,48 @@ namespace Barotrauma } } + #region Cleaning + private readonly float checkItemsInterval = 1; + private float checkItemsTimer; + private readonly List itemsToClean = new List(); + private readonly List ignoredItems = new List(); + + private void CleanupItems(float deltaTime) + { + if (checkItemsTimer <= 0) + { + checkItemsTimer = checkItemsInterval * Rand.Range(0.9f, 1.1f); + var hull = character.CurrentHull; + if (hull != null) + { + itemsToClean.Clear(); + foreach (Item item in Item.ItemList) + { + if (item.CurrentHull != hull) { continue; } + if (AIObjectiveCleanupItems.IsValidTarget(item, character) && !ignoredItems.Contains(item)) + { + itemsToClean.Add(item); + } + } + if (itemsToClean.Any()) + { + var targetItem = itemsToClean.OrderBy(i => Math.Abs(character.WorldPosition.X - i.WorldPosition.X)).FirstOrDefault(); + if (targetItem != null) + { + var cleanupObjective = new AIObjectiveCleanupItem(targetItem, character, objectiveManager, PriorityModifier); + cleanupObjective.Abandoned += () => ignoredItems.Add(targetItem); + subObjectives.Add(cleanupObjective); + } + } + } + } + else + { + checkItemsTimer -= deltaTime; + } + } + #endregion + public static bool IsForbidden(Hull hull) { if (hull == null) { return true; } @@ -475,6 +514,9 @@ namespace Barotrauma tooCloseCharacter = null; targetHulls.Clear(); hullWeights.Clear(); + checkItemsTimer = 0;; + itemsToClean.Clear(); + ignoredItems.Clear(); autonomousObjectiveRetryTimer = 10; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index bde055907..a31401560 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 Barotrauma.Extensions; using Microsoft.Xna.Framework; namespace Barotrauma @@ -46,7 +45,7 @@ namespace Barotrauma public override bool AllowSubObjectiveSorting => true; public virtual bool InverseTargetEvaluation => false; - public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); } + public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace.CleanupStackTrace()); } public override void Update(float deltaTime) { @@ -79,7 +78,12 @@ namespace Barotrauma var target = objective.Key; if (!Targets.Contains(target)) { - subObjectives.Remove(objective.Value); + var subObjective = objective.Value; + if (CurrentSubObjective == subObjective) + { + CurrentSubObjective.Abandon = !CurrentSubObjective.IsCompleted; + } + subObjectives.Remove(subObjective); } } SyncRemovedObjectives(Objectives, GetList()); @@ -137,7 +141,7 @@ namespace Barotrauma { if (objectiveManager.CurrentOrder == this) { - Priority = AIObjectiveManager.OrderPriority; + Priority = ForceOrderPriority ? AIObjectiveManager.OrderPriority : targetValue; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 4682b8b37..b223a850d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -70,7 +70,7 @@ namespace Barotrauma if (objective == null) { #if DEBUG - DebugConsole.ThrowError("Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace); + DebugConsole.ThrowError("Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace.CleanupStackTrace()); #endif return; } @@ -128,8 +128,7 @@ namespace Barotrauma var orderPrefab = Order.GetPrefab(autonomousObjective.identifier); if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.identifier}'"); } var item = orderPrefab.MustSetTarget ? orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID)?.GetRandom() : null; - var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, - item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType), orderGiver: character); + var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), 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); @@ -147,7 +146,7 @@ namespace Barotrauma if (objective == null) { #if DEBUG - DebugConsole.ThrowError($"{character.Name}: Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace); + DebugConsole.ThrowError($"{character.Name}: Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace.CleanupStackTrace()); #endif return; } @@ -314,9 +313,10 @@ namespace Barotrauma }; break; case "wait": - newObjective = new AIObjectiveGoTo(order.TargetEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier) + newObjective = new AIObjectiveGoTo(order.TargetSpatialEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier) { - AllowGoingOutside = character.CurrentHull == null + AllowGoingOutside = order.TargetSpatialEntity == null ? character.CurrentHull == null : + character.Submarine == null || character.Submarine != order.TargetSpatialEntity.Submarine }; break; case "fixleaks": @@ -374,6 +374,32 @@ namespace Barotrauma Override = orderGiver != null && orderGiver.IsPlayer }; break; + case "setchargepct": + newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, false, priorityModifier: priorityModifier) + { + IsLoop = false, + Override = character.CurrentOrder != null, + completionCondition = () => + { + if (float.TryParse(option, out float pct)) + { + var targetRatio = Math.Clamp(pct, 0f, 1f); + var currentRatio = (order.TargetItemComponent as PowerContainer).RechargeRatio; + return Math.Abs(targetRatio - currentRatio) < 0.05f; + } + return true; + } + }; + break; + case "getitem": + newObjective = new AIObjectiveGetItem(character, order.TargetEntity as Item ?? order.TargetItemComponent?.Item, this, false, priorityModifier: priorityModifier) + { + MustBeSpecificItem = true + }; + break; + case "cleanupitems": + newObjective = new AIObjectiveCleanupItems(character, this, priorityModifier, order.TargetEntity as Item); + break; default: if (order.TargetItemComponent == null) { return null; } if (order.TargetItemComponent.Item.NonInteractable) { return null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 0e0cfbbca..bc42743ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -11,6 +11,7 @@ namespace Barotrauma public override string DebugTag => $"operate item {component.Name}"; public override bool AllowAutomaticItemUnequipping => true; public override bool AllowMultipleInstances => true; + public override bool AllowInAnySub => true; private ItemComponent component, controller; private Entity operateTarget; @@ -35,9 +36,11 @@ namespace Barotrauma public override float GetPriority() { + bool isOrder = objectiveManager.CurrentOrder == this; if (!IsAllowed || character.LockHands) { Priority = 0; + Abandon = !isOrder; return Priority; } if (component.Item.ConditionPercentage <= 0) @@ -46,7 +49,6 @@ namespace Barotrauma } else { - bool isOrder = objectiveManager.CurrentOrder == this; if (isOrder) { Priority = AIObjectiveManager.OrderPriority; @@ -172,7 +174,7 @@ namespace Barotrauma } if (target.CanBeSelected) { - if (character.CanInteractWith(target.Item, out _, checkLinked: false)) + if (!character.IsClimbing && character.CanInteractWith(target.Item, out _, checkLinked: false)) { HumanAIController.FaceTarget(target.Item); if (character.SelectedConstruction != target.Item) @@ -189,7 +191,8 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50) { DialogueIdentifier = "dialogcannotreachtarget", - TargetName = target.Item.Name + TargetName = target.Item.Name, + endNodeFilter = node => node.Waypoint.Ladders == null }, onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref goToObjective)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index b363f8999..a1f950d59 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -10,6 +10,8 @@ namespace Barotrauma { public override string DebugTag => "repair item"; + public override bool AllowInAnySub => true; + public Item Item { get; private set; } private AIObjectiveGoTo goToObjective; @@ -34,6 +36,7 @@ namespace Barotrauma if (!IsAllowed) { Priority = 0; + Abandon = true; return Priority; } // TODO: priority list? @@ -69,7 +72,7 @@ namespace Barotrauma IsCompleted = Item.IsFullCondition; if (IsCompleted && IsRepairing()) { - character?.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f); + character.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f); } return IsCompleted; } @@ -134,7 +137,7 @@ namespace Barotrauma return; } } - if (character.CanInteractWith(Item, out _, checkLinked: false)) + if (!character.IsClimbing && character.CanInteractWith(Item, out _, checkLinked: false)) { HumanAIController.FaceTarget(Item); if (repairTool != null) @@ -235,7 +238,11 @@ namespace Barotrauma private void OperateRepairTool(float deltaTime) { - character.CursorPosition = Item.Position; + character.CursorPosition = Item.WorldPosition; + if (character.Submarine != null) + { + character.CursorPosition -= character.Submarine.Position; + } if (repairTool.Item.RequireAimToUse) { character.SetInput(InputType.Aim, false, true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index 101f66cf9..7755bf7fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -24,11 +24,11 @@ namespace Barotrauma private readonly Item prioritizedItem; public override bool AllowMultipleInstances => true; + public override bool AllowInAnySub => true; public readonly static float RequiredSuccessFactor = 0.4f; - public override bool IsDuplicate(T otherObjective) => - (otherObjective as AIObjective) is AIObjectiveRepairItems repairObjective && repairObjective.RequireAdequateSkills == RequireAdequateSkills; + public override bool IsDuplicate(T otherObjective) => otherObjective is AIObjectiveRepairItems repairObjective && repairObjective.RequireAdequateSkills == RequireAdequateSkills; public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Item prioritizedItem = null) : base(character, objectiveManager, priorityModifier) @@ -151,13 +151,9 @@ namespace Barotrauma if (item.NonInteractable) { return false; } if (item.IsFullCondition) { return false; } if (item.CurrentHull == null) { return false; } - if (item.Submarine == null) { return false; } - if (item.Submarine.TeamID != character.TeamID) { return false; } + if (item.Submarine == null || character.Submarine == null) { return false; } + if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { return false; } if (item.Repairables.None()) { return false; } - if (character.Submarine != null) - { - 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 25512b72a..ebe7eb229 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -14,6 +14,7 @@ namespace Barotrauma public override bool KeepDivingGearOn => true; public override bool AllowOutsideSubmarine => true; + public override bool AllowInAnySub => true; const float TreatmentDelay = 0.5f; @@ -35,7 +36,7 @@ namespace Barotrauma { if (targetCharacter == null) { - string errorMsg = $"{character.Name}: Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace; + string errorMsg = $"{character.Name}: Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("AIObjectiveRescue:ctor:targetnull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); Abandon = true; @@ -392,11 +393,13 @@ namespace Barotrauma if (!IsAllowed) { Priority = 0; + Abandon = true; return Priority; } if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead) { Priority = 0; + Abandon = true; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 13e1b1c0f..b6a52e39c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -11,6 +11,7 @@ namespace Barotrauma public override bool ForceRun => true; public override bool InverseTargetEvaluation => true; public override bool AllowOutsideSubmarine => true; + public override bool AllowInAnySub => true; private const float vitalityThreshold = 75; private const float vitalityThresholdForOrders = 85; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index eaf61af4c..e1b8b47f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -38,7 +38,8 @@ namespace Barotrauma } public bool MatchesOrder(Order order, string option) => - order.Identifier == Order.Identifier && option == OrderOption && order.TargetEntity == Order.TargetEntity; + order.Identifier == Order.Identifier && + option == OrderOption; } class Order @@ -54,7 +55,7 @@ namespace Barotrauma } return order; } - + public Order Prefab { get; private set; } public readonly string Name; @@ -62,7 +63,8 @@ namespace Barotrauma public readonly Sprite SymbolSprite; public readonly Type ItemComponentType; - public readonly string[] ItemIdentifiers; + public readonly bool CanTypeBeSubclass; + public readonly string[] TargetItems; public readonly string Identifier; @@ -89,14 +91,14 @@ namespace Barotrauma color = value; } } - + //if true, the order is issued to all available characters public bool TargetAllCharacters; public readonly float FadeOutTime; - public Entity TargetEntity; + public Entity TargetEntity; public ItemComponent TargetItemComponent; public readonly bool UseController; public Controller ConnectedController; @@ -123,6 +125,18 @@ namespace Barotrauma public bool IsPrefab { get; private set; } public readonly bool MustManuallyAssign; + public readonly OrderTarget TargetPosition; + + private ISpatialEntity targetSpatialEntity; + public ISpatialEntity TargetSpatialEntity + { + get + { + if (targetSpatialEntity == null) { targetSpatialEntity = TargetEntity ?? TargetPosition as ISpatialEntity; } + return targetSpatialEntity; + } + } + public static void Init() { Prefabs = new Dictionary(); @@ -218,8 +232,9 @@ namespace Barotrauma DebugConsole.ThrowError("Error in the order definitions: item component type " + targetItemType + " not found", e); } } + CanTypeBeSubclass = orderElement.GetAttributeBool("cantypebesubclass", false); - ItemIdentifiers = orderElement.GetAttributeStringArray("targetitemidentifiers", new string[0], trim: true, convertToLowerInvariant: true); + TargetItems = orderElement.GetAttributeStringArray("targetitems", new string[0], trim: true, convertToLowerInvariant: true); color = orderElement.GetAttributeColor("color"); FadeOutTime = orderElement.GetAttributeFloat("fadeouttime", 0.0f); UseController = orderElement.GetAttributeBool("usecontroller", false); @@ -228,7 +243,6 @@ namespace Barotrauma 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); } - Weight = orderElement.GetAttributeFloat(0.0f, "weight"); MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false); AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null); @@ -279,7 +293,7 @@ namespace Barotrauma IsPrefab = true; MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false); } - + /// /// Constructor for order instances /// @@ -290,7 +304,8 @@ namespace Barotrauma Name = prefab.Name; Identifier = prefab.Identifier; ItemComponentType = prefab.ItemComponentType; - ItemIdentifiers = prefab.ItemIdentifiers; + CanTypeBeSubclass = prefab.CanTypeBeSubclass; + TargetItems = prefab.TargetItems; Options = prefab.Options; SymbolSprite = prefab.SymbolSprite; Color = prefab.Color; @@ -298,7 +313,6 @@ namespace Barotrauma TargetAllCharacters = prefab.TargetAllCharacters; AppropriateJobs = prefab.AppropriateJobs; FadeOutTime = prefab.FadeOutTime; - Weight = prefab.Weight; MustSetTarget = prefab.MustSetTarget; AppropriateSkill = prefab.AppropriateSkill; Category = prefab.Category; @@ -325,6 +339,11 @@ namespace Barotrauma IsPrefab = false; } + + public Order(Order prefab, OrderTarget target, Character orderGiver = null) : this(prefab, targetEntity: null, targetItem: null, orderGiver) + { + TargetPosition = target; + } public bool HasAppropriateJob(Character character) { @@ -358,15 +377,38 @@ namespace Barotrauma return msg; } + /// + /// Get the target item component based on the target item type + /// + public ItemComponent GetTargetItemComponent(Item item) + { + if (item?.Components == null || ItemComponentType == null) { return null; } + foreach (ItemComponent component in item.Components) + { + if (component?.GetType() is Type componentType) + { + if (componentType == ItemComponentType) { return component; } + if (CanTypeBeSubclass && componentType.IsSubclassOf(ItemComponentType)) { return component; } + } + } + return null; + } + + public bool TryGetTargetItemComponent(Item item, out ItemComponent firstMatchingComponent) + { + firstMatchingComponent = GetTargetItemComponent(item); + return firstMatchingComponent != null; + } + public List GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, Character.TeamType? requiredTeam = null) { List matchingItems = new List(); if (submarine == null) { return matchingItems; } - if (ItemComponentType != null || ItemIdentifiers.Length > 0) + if (ItemComponentType != null || TargetItems.Length > 0) { - matchingItems = ItemIdentifiers.Length > 0 ? - Item.ItemList.FindAll(it => ItemIdentifiers.Contains(it.Prefab.Identifier) || it.HasTag(ItemIdentifiers)) : - Item.ItemList.FindAll(it => it.Components.Any(ic => ic.GetType() == ItemComponentType)); + matchingItems = TargetItems.Length > 0 ? + Item.ItemList.FindAll(it => TargetItems.Contains(it.Prefab.Identifier) || it.HasTag(TargetItems)) : + Item.ItemList.FindAll(it => TryGetTargetItemComponent(it, out _)); if (mustBelongToPlayerSub) { matchingItems.RemoveAll(it => it.Submarine?.Info != null && it.Submarine.Info.Type != SubmarineType.Player); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index 920717e41..3bf84304a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs @@ -92,8 +92,10 @@ namespace Barotrauma public GetNodePenaltyHandler GetNodePenalty; private readonly List nodes; + public readonly bool IndoorsSteering; public bool InsideSubmarine { get; set; } + public bool ApplyPenaltyToOutsideNodes { get; set; } public PathFinder(List wayPoints, bool indoorsSteering = false) { @@ -104,7 +106,7 @@ namespace Barotrauma wp.linkedTo.CollectionChanged += WaypointLinksChanged; } - InsideSubmarine = indoorsSteering; + IndoorsSteering = indoorsSteering; } void WaypointLinksChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) @@ -178,7 +180,7 @@ namespace Barotrauma node.TempDistance = xDiff + (InsideSubmarine ? yDiff * 10.0f : yDiff); //higher cost for vertical movement when inside the sub //much higher cost to waypoints that are outside - if (node.Waypoint.CurrentHull == null && InsideSubmarine) { node.TempDistance *= 10.0f; } + if (node.Waypoint.CurrentHull == null && ApplyPenaltyToOutsideNodes) { node.TempDistance *= 10.0f; } //prefer nodes that are closer to the end position node.TempDistance += (Math.Abs(end.X - node.TempPosition.X) + Math.Abs(end.Y - node.TempPosition.Y)) / 100.0f; @@ -200,11 +202,13 @@ namespace Barotrauma if (nodeFilter != null && !nodeFilter(node)) { continue; } if (startNodeFilter != null && !startNodeFilter(node)) { continue; } //if searching for a path inside the sub, make sure the waypoint is visible - if (InsideSubmarine) + if (IndoorsSteering) { + if (node.Waypoint.isObstructed) { continue; } + // Always check the visibility for the start node var body = Submarine.PickBody( - start, node.TempPosition, null, + start, node.TempPosition, null, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs); if (body != null) { @@ -231,8 +235,11 @@ namespace Barotrauma node.TempDistance = Vector2.DistanceSquared(end, node.TempPosition); if (InsideSubmarine) { - //much higher cost to waypoints that are outside - if (node.Waypoint.CurrentHull == null) { node.TempDistance *= 10.0f; } + if (ApplyPenaltyToOutsideNodes) + { + //much higher cost to waypoints that are outside + if (node.Waypoint.CurrentHull == null) { node.TempDistance *= 10.0f; } + } //avoid stopping at a doorway if (node.Waypoint.ConnectedDoor != null) { node.TempDistance *= 10.0f; } //avoid stopping at a ladder @@ -255,17 +262,20 @@ namespace Barotrauma { if (nodeFilter != null && !nodeFilter(node)) { continue; } if (endNodeFilter != null && !endNodeFilter(node)) { continue; } - - //if searching for a path inside the sub, make sure the waypoint is visible - if (InsideSubmarine && checkVisibility) + if (IndoorsSteering) { - // 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 (node.Waypoint.isObstructed) { continue; } + //if searching for a path inside the sub, make sure the waypoint is visible + if (checkVisibility) { - if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; } - if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; } + // 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 Structure && !((Structure)body.UserData).IsPlatform) { continue; } + if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; } + } } } endNode = node; @@ -339,6 +349,7 @@ namespace Barotrauma foreach (PathNode node in nodes) { if (node.state != 1) { continue; } + if (IndoorsSteering && node.Waypoint.isObstructed) { continue; } if (filter != null && !filter(node)) { continue; } if (node.F < dist) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs new file mode 100644 index 000000000..53ca4f47f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs @@ -0,0 +1,429 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + class PetBehavior + { + public enum StatusIndicatorType + { + None, + Happy, + Sad, + Hungry + } + + private float hunger = 50.0f; + public float Hunger + { + get { return hunger; } + set { hunger = MathHelper.Clamp(value, 0.0f, MaxHunger); } + } + + private float happiness = 50.0f; + public float Happiness + { + get { return happiness; } + set { happiness = MathHelper.Clamp(value, 0.0f, MaxHappiness); } + } + + public float MaxHappiness { get; set; } + public float MaxHunger { get; set; } + + public float HappinessDecreaseRate { get; set; } + public float HungerIncreaseRate { get; set; } + + public float PlayForce { get; set; } + + public float PlayTimer { get; set; } + private float? unstunY { get; set; } + + public EnemyAIController AiController { get; private set; } = null; + + public Character Owner { get; set; } + + private class ItemProduction + { + public struct Item + { + public ItemPrefab Prefab; + public float Commonness; + } + public List Items; + public Vector2 HungerRange; + public Vector2 HappinessRange; + public float Rate; + public float HungerRate; + public float InvHungerRate; + public float HappinessRate; + public float InvHappinessRate; + + private readonly float totalCommonness; + private float timer; + + public ItemProduction(XElement element) + { + Items = new List(); + + HungerRate = element.GetAttributeFloat("hungerrate", 0.0f); + InvHungerRate = element.GetAttributeFloat("invhungerrate", 0.0f); + HappinessRate = element.GetAttributeFloat("happinessrate", 0.0f); + InvHappinessRate = element.GetAttributeFloat("invhappinessrate", 0.0f); + + string[] requiredHappinessStr = element.GetAttributeString("requiredhappiness", "0-100").Split('-'); + string[] requiredHungerStr = element.GetAttributeString("requiredhunger", "0-100").Split('-'); + HappinessRange = new Vector2(0, 100); + HungerRange = new Vector2(0, 100); + float tempF; + if (requiredHappinessStr.Length >= 2) + { + if (float.TryParse(requiredHappinessStr[0], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { HappinessRange.X = tempF; } + if (float.TryParse(requiredHappinessStr[1], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { HappinessRange.Y = tempF; } + } + if (requiredHungerStr.Length >= 2) + { + if (float.TryParse(requiredHungerStr[0], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { HungerRange.X = tempF; } + if (float.TryParse(requiredHungerStr[1], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { HungerRange.Y = tempF; } + } + Rate = element.GetAttributeFloat("rate", 0.016f); + totalCommonness = 0.0f; + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.LocalName.ToLowerInvariant()) + { + case "item": + string identifier = subElement.GetAttributeString("identifier", ""); + Item newItemToProduce = new Item + { + Prefab = string.IsNullOrEmpty(identifier) ? null : ItemPrefab.Find("", subElement.GetAttributeString("identifier", "")), + Commonness = subElement.GetAttributeFloat("commonness", 0.0f) + }; + totalCommonness += newItemToProduce.Commonness; + Items.Add(newItemToProduce); + break; + } + } + + timer = 1.0f; + } + + public void Update(PetBehavior pet, float deltaTime) + { + if (pet.Happiness < HappinessRange.X || pet.Happiness > HappinessRange.Y) { return; } + if (pet.Hunger < HungerRange.X || pet.Hunger > HungerRange.Y) { return; } + + float currentRate = Rate; + currentRate += HappinessRate * (pet.Happiness - HappinessRange.X) / (HappinessRange.Y - HappinessRange.X); + currentRate += InvHappinessRate * (1.0f - ((pet.Happiness - HappinessRange.X) / (HappinessRange.Y - HappinessRange.X))); + currentRate += HungerRate * (pet.Hunger - HungerRange.X) / (HungerRange.Y - HungerRange.X); + currentRate += InvHungerRate * (1.0f - ((pet.Hunger - HungerRange.X) / (HungerRange.Y - HungerRange.X))); + timer -= currentRate * deltaTime; + if (timer <= 0.0f) + { + timer = 1.0f; + float r = Rand.Range(0.0f, totalCommonness); + float aggregate = 0.0f; + for (int i = 0; i < Items.Count; i++) + { + aggregate += Items[i].Commonness; + if (aggregate >= r && Items[i].Prefab != null) + { + Entity.Spawner.AddToSpawnQueue(Items[i].Prefab, pet.AiController.Character.WorldPosition); + break; + } + } + } + } + } + + private class Food + { + public string Tag; + public Vector2 HungerRange; + public float Hunger; + public float Happiness; + public float Priority; + public bool IgnoreContained; + + public CharacterParams.TargetParams TargetParams = null; + } + + private readonly List itemsToProduce = new List(); + private readonly List foods = new List(); + + public PetBehavior(XElement element, EnemyAIController aiController) + { + AiController = aiController; + AiController.Character.CanBeDragged = true; + + MaxHappiness = element.GetAttributeFloat("maxhappiness", 100.0f); + MaxHunger = element.GetAttributeFloat("maxhunger", 100.0f); + + Happiness = MaxHappiness * 0.5f; + Hunger = MaxHunger * 0.5f; + + HappinessDecreaseRate = element.GetAttributeFloat("happinessdecreaserate", 0.1f); + HungerIncreaseRate = element.GetAttributeFloat("hungerincreaserate", 0.25f); + + PlayForce = element.GetAttributeFloat("playforce", 15.0f); + + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.LocalName.ToLowerInvariant()) + { + case "itemproduction": + itemsToProduce.Add(new ItemProduction(subElement)); + break; + case "eat": + Food food = new Food + { + Tag = subElement.GetAttributeString("tag", ""), + Hunger = subElement.GetAttributeFloat("hunger", -1), + Happiness = subElement.GetAttributeFloat("happiness", 1), + Priority = subElement.GetAttributeFloat("priority", 100), + IgnoreContained = subElement.GetAttributeBool("ignorecontained", true) + }; + string[] requiredHungerStr = subElement.GetAttributeString("requiredhunger", "0-100").Split('-'); + food.HungerRange = new Vector2(0, 100); + if (requiredHungerStr.Length >= 2) + { + if (float.TryParse(requiredHungerStr[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float tempF)) { food.HungerRange.X = tempF; } + if (float.TryParse(requiredHungerStr[1], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { food.HungerRange.Y = tempF; } + } + foods.Add(food); + break; + } + } + } + + public StatusIndicatorType GetCurrentStatusIndicatorType() + { + if (Hunger > MaxHunger * 0.5f) { return StatusIndicatorType.Hungry; } + if (Happiness > MaxHappiness * 0.8f) { return StatusIndicatorType.Happy; } + if (Happiness < MaxHappiness * 0.25f) { return StatusIndicatorType.Sad; } + return StatusIndicatorType.None; + } + + public bool OnEat(IEnumerable tags, float amount) + { + foreach (string tag in tags) + { + if (OnEat(tag, amount)) { return true; } + } + return false; + } + + public bool OnEat(string tag, float amount) + { + for (int i = 0; i < foods.Count; i++) + { + if (tag.Equals(foods[i].Tag, System.StringComparison.OrdinalIgnoreCase)) + { + Hunger += foods[i].Hunger * amount; + Happiness += foods[i].Happiness * amount; +#if CLIENT + AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f); +#endif + return true; + } + } + return false; + } + + public void Play(Character player) + { + if (PlayTimer > 0.0f) { return; } + if (Owner == null) { Owner = player; } + PlayTimer = 5.0f; + AiController.Character.IsRagdolled = true; + Happiness += 10.0f; + AiController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce); + unstunY = AiController.Character.SimPosition.Y; +#if CLIENT + AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f); +#endif + } + + public string GetTagName() + { + if (AiController.Character.Inventory != null) + { + var items = AiController.Character.Inventory.Items; + for (int i = 0; i < items.Length; i++) + { + var item = items[i]; + if (item == null) { continue; } + var tag = item.GetComponent(); + if (tag != null && !string.IsNullOrWhiteSpace(tag.WrittenName)) + { + return tag.WrittenName; + } + } + } + + return string.Empty; + } + + public void Update(float deltaTime) + { + var character = AiController.Character; + if (character?.Removed ?? true || character.IsDead) { return; } + + if (unstunY.HasValue) + { + if (PlayTimer > 4.0f) + { + float extent = character.AnimController.MainLimb.body.GetMaxExtent(); + if (character.SimPosition.Y < (unstunY.Value + extent * 3.0f) && + character.AnimController.MainLimb.body.LinearVelocity.Y < 0.0f) + { + character.IsRagdolled = false; + unstunY = null; + } + else + { + character.IsRagdolled = true; + } + } + else + { + character.IsRagdolled = false; + unstunY = null; + } + } + + PlayTimer -= deltaTime; + + if (GameMain.NetworkMember?.IsClient ?? false) { return; } + if (Owner != null && (Owner.Removed || Owner.IsDead)) { Owner = null; } + + Hunger += HungerIncreaseRate * deltaTime; + Happiness -= HappinessDecreaseRate * deltaTime; + + for (int i = 0; i < foods.Count; i++) + { + Food food = foods[i]; + if (Hunger >= food.HungerRange.X && Hunger <= food.HungerRange.Y) + { + if (food.TargetParams == null && + AiController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out CharacterParams.TargetParams targetParams)) + { + targetParams.IgnoreContained = food.IgnoreContained; + food.TargetParams = targetParams; + } + } + else if (food.TargetParams != null) + { + AiController.AIParams.RemoveTarget(food.TargetParams); + food.TargetParams = null; + } + } + + if (Hunger >= MaxHunger * 0.99f) + { + character.CharacterHealth.ApplyAffliction(character.AnimController.MainLimb, new Affliction(AfflictionPrefab.InternalDamage, 8.0f * deltaTime)); + } + else if (Hunger < MaxHunger * 0.1f) + { + character.CharacterHealth.ReduceAffliction(null, null, 8.0f * deltaTime); + } + + if (character.SelectedBy != null) + { + character.IsRagdolled = true; + unstunY = character.SimPosition.Y; + } + + for (int i = 0; i < itemsToProduce.Count; i++) + { + itemsToProduce[i].Update(this, deltaTime); + } + } + + public static void SavePets(XElement petsElement) + { + foreach (Character c in Character.CharacterList) + { + if (!c.IsPet || c.IsDead) { continue; } + if (c.Submarine == null) { continue; } + + var petBehavior = (c.AIController as EnemyAIController)?.PetBehavior; + if (petBehavior == null) { continue; } + + XElement petElement = new XElement("pet", + new XAttribute("speciesname", c.SpeciesName), + new XAttribute("ownerid", petBehavior.Owner?.ID ?? Entity.NullEntityID), + new XAttribute("seed", c.Seed)); + + var petBehaviorElement = new XElement("petbehavior", + new XAttribute("hunger", petBehavior.Hunger.ToString("G", CultureInfo.InvariantCulture)), + new XAttribute("happiness", petBehavior.Happiness.ToString("G", CultureInfo.InvariantCulture))); + petElement.Add(petBehaviorElement); + + var healthElement = new XElement("health"); + c.CharacterHealth.Save(healthElement); + petElement.Add(healthElement); + + if (c.Inventory != null) + { + var inventoryElement = new XElement("inventory"); + c.SaveInventory(c.Inventory, inventoryElement); + petElement.Add(inventoryElement); + } + + petsElement.Add(petElement); + } + } + + public static void LoadPets(XElement petsElement) + { + foreach (XElement subElement in petsElement.Elements()) + { + string speciesName = subElement.GetAttributeString("speciesname", ""); + string seed = subElement.GetAttributeString("seed", "123"); + ushort ownerID = (ushort)subElement.GetAttributeInt("ownerid", 0); + Vector2 spawnPos = Vector2.Zero; + Character owner = Entity.FindEntityByID(ownerID) as Character; + if (owner != null) + { + spawnPos = owner.WorldPosition; + } + else + { + var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandom(); + spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition; + } + var pet = Character.Create(speciesName, spawnPos, seed); + var petBehavior = (pet.AIController as EnemyAIController)?.PetBehavior; + if (petBehavior != null) + { + petBehavior.Owner = owner; + var petBehaviorElement = subElement.Attribute("petbehavior"); + if (petBehaviorElement != null) + { + petBehavior.Hunger = petBehaviorElement.GetAttributeFloat(50.0f); + petBehavior.Happiness = petBehaviorElement.GetAttributeFloat(50.0f); + } + } + } + } + + public void ServerWrite(IWriteMessage msg) + { + msg.WriteRangedSingle(Happiness, 0.0f, MaxHappiness, 8); + msg.WriteRangedSingle(Hunger, 0.0f, MaxHunger, 8); + } + + public void ClientRead(IReadMessage msg) + { + Happiness = msg.ReadRangedSingle(0.0f, MaxHappiness, 8); + Hunger = msg.ReadRangedSingle(0.0f, MaxHunger, 8); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs index e603b5b7c..c583b6dcf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs @@ -47,6 +47,10 @@ namespace Barotrauma base.Update(deltaTime, cam); if (!Enabled) { return; } + if (!IsRemotePlayer && AIController is EnemyAIController enemyAi) + { + enemyAi.PetBehavior?.Update(deltaTime); + } if (IsDead || Vitality <= 0.0f || Stun > 0.0f || IsIncapacitated) { //don't enable simple physics on dead/incapacitated characters diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 980aa32e0..6be176ce8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -381,6 +381,12 @@ namespace Barotrauma { //only one limb left, the character is now full eaten Entity.Spawner?.AddToRemoveQueue(target); + + if (Character.AIController is EnemyAIController enemyAi) + { + enemyAi.PetBehavior?.OnEat("dead", 1.0f); + } + character.SelectedCharacter = null; } else //sever a random joint @@ -423,113 +429,195 @@ namespace Barotrauma { WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5); mainLimb.PullJointWorldAnchorB = Collider.SimPosition; - return; - } - - Vector2 transformedMovement = reverse ? -movement : movement; - float movementAngle = MathUtils.VectorToAngle(transformedMovement) - MathHelper.PiOver2; - float mainLimbAngle = 0; - if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue) - { - mainLimbAngle = TorsoAngle.Value; - } - else if (mainLimb.type == LimbType.Head && HeadAngle.HasValue) - { - mainLimbAngle = HeadAngle.Value; - } - mainLimbAngle *= Dir; - while (mainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi) - { - movementAngle += MathHelper.TwoPi; - } - while (mainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi) - { - movementAngle -= MathHelper.TwoPi; - } - - if (CurrentSwimParams.RotateTowardsMovement) - { - Collider.SmoothRotate(movementAngle, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); - if (TorsoAngle.HasValue) - { - Limb torso = GetLimb(LimbType.Torso); - if (torso != null) - { - SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, mainLimb, TorsoTorque); - } - } - if (HeadAngle.HasValue) - { - Limb head = GetLimb(LimbType.Head); - if (head != null) - { - SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque); - } - } - if (TailAngle.HasValue) - { - Limb tail = GetLimb(LimbType.Tail); - if (tail != null) - { - float? mainLimbTargetAngle = null; - if (mainLimb.type == LimbType.Torso) - { - mainLimbTargetAngle = TorsoAngle; - } - else if (mainLimb.type == LimbType.Head) - { - mainLimbTargetAngle = HeadAngle; - } - float torque = TailTorque; - float maxMultiplier = CurrentSwimParams.TailTorqueMultiplier; - if (mainLimbTargetAngle.HasValue && maxMultiplier > 1) - { - float diff = Math.Abs(mainLimb.Rotation - tail.Rotation); - float offset = Math.Abs(mainLimbTargetAngle.Value - TailAngle.Value); - torque *= MathHelper.Lerp(1, maxMultiplier, MathUtils.InverseLerp(0, MathHelper.PiOver2, diff - offset)); - } - SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, torque); - } - } } else { - movementAngle = Dir > 0 ? -MathHelper.PiOver2 : MathHelper.PiOver2; - if (reverse) + Vector2 transformedMovement = reverse ? -movement : movement; + float movementAngle = MathUtils.VectorToAngle(transformedMovement) - MathHelper.PiOver2; + float mainLimbAngle = 0; + if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue) { - movementAngle = MathUtils.WrapAngleTwoPi(movementAngle - MathHelper.Pi); + mainLimbAngle = TorsoAngle.Value; } - if (mainLimb.type == LimbType.Head && HeadAngle.HasValue) + else if (mainLimb.type == LimbType.Head && HeadAngle.HasValue) { - Collider.SmoothRotate(HeadAngle.Value * Dir, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); + mainLimbAngle = HeadAngle.Value; } - else if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue) + mainLimbAngle *= Dir; + while (mainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi) { - Collider.SmoothRotate(TorsoAngle.Value * Dir, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); + movementAngle += MathHelper.TwoPi; } - if (TorsoAngle.HasValue) + while (mainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi) { - Limb torso = GetLimb(LimbType.Torso); - torso?.body.SmoothRotate(TorsoAngle.Value * Dir, TorsoTorque); + movementAngle -= MathHelper.TwoPi; } - if (HeadAngle.HasValue) + if (CurrentSwimParams.RotateTowardsMovement) { - Limb head = GetLimb(LimbType.Head); - head?.body.SmoothRotate(HeadAngle.Value * Dir, HeadTorque); - } - if (TailAngle.HasValue) - { - Limb tail = GetLimb(LimbType.Tail); - tail?.body.SmoothRotate(TailAngle.Value * Dir, TailTorque); - } - } + Collider.SmoothRotate(movementAngle, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); + if (TorsoAngle.HasValue) + { + Limb torso = GetLimb(LimbType.Torso); + if (torso != null) + { + SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, mainLimb, TorsoTorque); + } + } + if (HeadAngle.HasValue) + { + Limb head = GetLimb(LimbType.Head); + if (head != null) + { + SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque); + } + } + if (TailAngle.HasValue) + { + bool isAngleApplied = false; + foreach (var limb in Limbs) + { + if (limb.IsSevered) { continue; } + if (limb.type != LimbType.Tail) { continue; } + if (!limb.Params.ApplyTailAngle) { continue; } + RotateTail(limb); + isAngleApplied = true; + } + if (!isAngleApplied) + { + RotateTail(GetLimb(LimbType.Tail)); + } - var waveLength = Math.Abs(CurrentSwimParams.WaveLength * RagdollParams.JointScale); - var waveAmplitude = Math.Abs(CurrentSwimParams.WaveAmplitude * character.SpeedMultiplier); - if (waveLength > 0 && waveAmplitude > 0) - { - WalkPos -= transformedMovement.Length() / Math.Abs(waveLength); - WalkPos = MathUtils.WrapAngleTwoPi(WalkPos); + void RotateTail(Limb tail) + { + if (tail == null) { return; } + float? mainLimbTargetAngle = null; + if (mainLimb.type == LimbType.Torso) + { + mainLimbTargetAngle = TorsoAngle; + } + else if (mainLimb.type == LimbType.Head) + { + mainLimbTargetAngle = HeadAngle; + } + float torque = TailTorque; + float maxMultiplier = CurrentSwimParams.TailTorqueMultiplier; + if (mainLimbTargetAngle.HasValue && maxMultiplier > 1) + { + float diff = Math.Abs(mainLimb.Rotation - tail.Rotation); + float offset = Math.Abs(mainLimbTargetAngle.Value - TailAngle.Value); + torque *= MathHelper.Lerp(1, maxMultiplier, MathUtils.InverseLerp(0, MathHelper.PiOver2, diff - offset)); + } + SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, torque); + } + } + } + else + { + movementAngle = Dir > 0 ? -MathHelper.PiOver2 : MathHelper.PiOver2; + if (reverse) + { + movementAngle = MathUtils.WrapAngleTwoPi(movementAngle - MathHelper.Pi); + } + if (mainLimb.type == LimbType.Head && HeadAngle.HasValue) + { + Collider.SmoothRotate(HeadAngle.Value * Dir, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); + } + else if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue) + { + Collider.SmoothRotate(TorsoAngle.Value * Dir, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); + } + if (TorsoAngle.HasValue) + { + Limb torso = GetLimb(LimbType.Torso); + torso?.body.SmoothRotate(TorsoAngle.Value * Dir, TorsoTorque); + } + if (HeadAngle.HasValue) + { + Limb head = GetLimb(LimbType.Head); + head?.body.SmoothRotate(HeadAngle.Value * Dir, HeadTorque); + } + if (TailAngle.HasValue) + { + bool isAngleApplied = false; + foreach (var limb in Limbs) + { + if (limb.IsSevered) { continue; } + if (limb.type != LimbType.Tail) { continue; } + if (!limb.Params.ApplyTailAngle) { continue; } + RotateTail(limb); + isAngleApplied = true; + } + if (!isAngleApplied) + { + RotateTail(GetLimb(LimbType.Tail)); + } + + void RotateTail(Limb tail) + { + if (tail != null) + { + tail.body.SmoothRotate(TailAngle.Value * Dir, TailTorque); + } + } + } + } + + var waveLength = Math.Abs(CurrentSwimParams.WaveLength * RagdollParams.JointScale); + var waveAmplitude = Math.Abs(CurrentSwimParams.WaveAmplitude * character.SpeedMultiplier); + if (waveLength > 0 && waveAmplitude > 0) + { + WalkPos -= transformedMovement.Length() / Math.Abs(waveLength); + WalkPos = MathUtils.WrapAngleTwoPi(WalkPos); + } + + foreach (var limb in Limbs) + { + if (limb.IsSevered) { continue; } + switch (limb.type) + { + case LimbType.LeftFoot: + case LimbType.RightFoot: + if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.Params.ID)) + { + SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, mainLimb, FootTorque); + } + break; + case LimbType.Tail: + if (waveLength > 0 && waveAmplitude > 0) + { + float waveRotation = (float)Math.Sin(WalkPos * limb.Params.SineFrequencyMultiplier); + limb.body.ApplyTorque(waveRotation * limb.Mass * waveAmplitude * limb.Params.SineAmplitudeMultiplier); + } + break; + } + } + + for (int i = 0; i < Limbs.Length; i++) + { + var limb = Limbs[i]; + if (limb.IsSevered) { continue; } + if (limb.SteerForce <= 0.0f) { continue; } + if (!Collider.PhysEnabled) { continue; } + Vector2 pullPos = limb.PullJointWorldAnchorA; + limb.body.ApplyForce(movement * limb.SteerForce * limb.Mass * Math.Max(character.SpeedMultiplier, 1), pullPos); + } + + Vector2 mainLimbDiff = mainLimb.PullJointWorldAnchorB - mainLimb.SimPosition; + if (CurrentSwimParams.UseSineMovement) + { + mainLimb.PullJointWorldAnchorB = Vector2.SmoothStep( + mainLimb.PullJointWorldAnchorB, + Collider.SimPosition, + mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : (float)Math.Abs(Math.Sin(WalkPos))); + } + else + { + //mainLimb.PullJointWorldAnchorB = Collider.SimPosition; + mainLimb.PullJointWorldAnchorB = Vector2.Lerp( + mainLimb.PullJointWorldAnchorB, + Collider.SimPosition, + mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : 0.5f); + } } foreach (var limb in Limbs) @@ -537,55 +625,15 @@ namespace Barotrauma if (limb.IsSevered) { continue; } if (Math.Abs(limb.Params.ConstantTorque) > 0) { - limb.body.SmoothRotate(movementAngle + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Params.ConstantTorque, wrapAngle: true); + limb.body.SmoothRotate(MainLimb.Rotation + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Mass * limb.Params.ConstantTorque, wrapAngle: true); } - switch (limb.type) + if (limb.Params.BlinkFrequency > 0) { - case LimbType.LeftFoot: - case LimbType.RightFoot: - if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.Params.ID)) - { - SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, mainLimb, FootTorque); - } - break; - case LimbType.Tail: - if (waveLength > 0 && waveAmplitude > 0) - { - float waveRotation = (float)Math.Sin(WalkPos); - limb.body.ApplyTorque(waveRotation * limb.Mass * waveAmplitude); - } - break; + limb.Blink(deltaTime, MainLimb.Rotation); } } - for (int i = 0; i < Limbs.Length; i++) - { - var limb = Limbs[i]; - if (limb.IsSevered) { continue; } - if (limb.SteerForce <= 0.0f) { continue; } - if (!Collider.PhysEnabled) { continue; } - Vector2 pullPos = limb.PullJointWorldAnchorA; - limb.body.ApplyForce(movement * limb.SteerForce * limb.Mass * Math.Max(character.SpeedMultiplier, 1), pullPos); - } - - Vector2 mainLimbDiff = mainLimb.PullJointWorldAnchorB - mainLimb.SimPosition; - if (CurrentSwimParams.UseSineMovement) - { - mainLimb.PullJointWorldAnchorB = Vector2.SmoothStep( - mainLimb.PullJointWorldAnchorB, - Collider.SimPosition, - mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : (float)Math.Abs(Math.Sin(WalkPos))); - } - else - { - //mainLimb.PullJointWorldAnchorB = Collider.SimPosition; - mainLimb.PullJointWorldAnchorB = Vector2.Lerp( - mainLimb.PullJointWorldAnchorB, - Collider.SimPosition, - mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : 0.5f); - } - - floorY = Limbs[0].SimPosition.Y; + floorY = Limbs[0].SimPosition.Y; } void UpdateWalkAnim(float deltaTime) @@ -655,7 +703,7 @@ namespace Barotrauma if (head != null) { bool headFacingBackwards = false; - if (HeadAngle.HasValue) + if (HeadAngle.HasValue && head != mainLimb) { SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque); if (Math.Sign(head.SimPosition.X - mainLimb.SimPosition.X) != Math.Sign(Dir)) @@ -680,10 +728,26 @@ namespace Barotrauma if (TailAngle.HasValue) { - var tail = GetLimb(LimbType.Tail); - if (tail != null) + bool isAngleApplied = false; + foreach (var limb in Limbs) { - SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, TailTorque); + if (limb.IsSevered) { continue; } + if (limb.type != LimbType.Tail) { continue; } + if (!limb.Params.ApplyTailAngle) { continue; } + RotateTail(limb); + isAngleApplied = true; + } + if (!isAngleApplied) + { + RotateTail(GetLimb(LimbType.Tail)); + } + + void RotateTail(Limb tail) + { + if (tail != null) + { + SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, TailTorque); + } } } @@ -703,7 +767,11 @@ namespace Barotrauma if (limb.IsSevered) { continue; } if (Math.Abs(limb.Params.ConstantTorque) > 0) { - limb.body.SmoothRotate(movementAngle + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Params.ConstantTorque, wrapAngle: true); + limb.body.SmoothRotate(MainLimb.Rotation + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Mass * limb.Params.ConstantTorque, wrapAngle: true); + } + if (limb.Params.BlinkFrequency > 0) + { + limb.Blink(deltaTime, MainLimb.Rotation); } switch (limb.type) { @@ -785,11 +853,35 @@ namespace Barotrauma float noise = (PerlinNoise.GetPerlin(WalkPos * 0.002f, WalkPos * 0.003f) - 0.5f) * 5.0f; float animStrength = (1.0f - deathAnimTimer / deathAnimDuration); - Limb head = GetLimb(LimbType.Head); - if (head != null && head.IsSevered) { return; } + Limb baseLimb = GetLimb(LimbType.Head); + //if head is the main limb, it technically can't be severed - the rest of the limbs are considered severed if the head gets cut off + if (baseLimb == MainLimb) + { + int connectedToHeadCount = GetConnectedLimbs(baseLimb).Count; + //if there's nothing connected to the head, don't make it wiggle by itself + if (connectedToHeadCount == 1) { baseLimb = null; } + Limb torso = GetLimb(LimbType.Torso, excludeSevered: false); + if (torso != null) + { + //if there are more limbs connected to the torso than to the head, make the torso wiggle instead + int connectedToTorsoCount = GetConnectedLimbs(torso).Count; + if (connectedToTorsoCount > connectedToHeadCount) + { + baseLimb = torso; + } + } + } + else if (baseLimb == null) + { + baseLimb = GetLimb(LimbType.Torso, excludeSevered: true); + if (baseLimb == null) { return; } + } + + var connectedToBaseLimb = GetConnectedLimbs(baseLimb); + Limb tail = GetLimb(LimbType.Tail); - if (head != null && !head.IsSevered) head.body.ApplyTorque((float)(Math.Sqrt(head.Mass) * Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength); - if (tail != null && !tail.IsSevered) tail.body.ApplyTorque((float)(Math.Sqrt(tail.Mass) * -Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength); + if (baseLimb != null) { baseLimb.body.ApplyTorque((float)(Math.Sqrt(baseLimb.Mass) * Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength); } + if (tail != null && connectedToBaseLimb.Contains(tail)) { tail.body.ApplyTorque((float)(Math.Sqrt(tail.Mass) * -Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength); } WalkPos += deltaTime * 10.0f * animStrength; @@ -797,7 +889,7 @@ namespace Barotrauma foreach (Limb limb in Limbs) { - if (limb.IsSevered) { continue; } + if (!connectedToBaseLimb.Contains(limb)) { continue; } #if CLIENT if (limb.LightSource != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index d58854b36..c77df67a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -145,7 +145,7 @@ namespace Barotrauma private set; } - private LimbJoint shoulder; + private LimbJoint rightShoulder, leftShoulder; private float upperLegLength = 0.0f, lowerLegLength = 0.0f; @@ -167,7 +167,7 @@ namespace Barotrauma { get { - return Crouching ? CurrentGroundedParams.CrouchingTorsoPos * RagdollParams.JointScale : base.TorsoPosition; + return Crouching && !swimming ? CurrentGroundedParams.CrouchingTorsoPos * RagdollParams.JointScale : base.TorsoPosition; } } @@ -175,7 +175,7 @@ namespace Barotrauma { get { - return Crouching ? CurrentGroundedParams.CrouchingHeadPos * RagdollParams.JointScale : base.HeadPosition; + return Crouching && !swimming ? CurrentGroundedParams.CrouchingHeadPos * RagdollParams.JointScale : base.HeadPosition; } } @@ -183,7 +183,7 @@ namespace Barotrauma { get { - return Crouching ? MathHelper.ToRadians(CurrentGroundedParams.CrouchingTorsoAngle) : base.TorsoAngle; + return Crouching && !swimming ? MathHelper.ToRadians(CurrentGroundedParams.CrouchingTorsoAngle) : base.TorsoAngle; } } @@ -191,7 +191,7 @@ namespace Barotrauma { get { - return Crouching ? MathHelper.ToRadians(CurrentGroundedParams.CrouchingHeadAngle) : base.HeadAngle; + return Crouching && !swimming ? MathHelper.ToRadians(CurrentGroundedParams.CrouchingHeadAngle) : base.HeadAngle; } } @@ -241,12 +241,13 @@ namespace Barotrauma Limb rightHand = GetLimb(LimbType.RightHand); if (rightHand == null) { return; } - shoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.RightArm); + rightShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.RightArm); + leftShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.LeftArm); Vector2 localAnchorShoulder = Vector2.Zero; Vector2 localAnchorElbow = Vector2.Zero; - if (shoulder != null) + if (rightShoulder != null) { - localAnchorShoulder = shoulder.LimbA.type == LimbType.RightArm ? shoulder.LocalAnchorA : shoulder.LocalAnchorB; + localAnchorShoulder = rightShoulder.LimbA.type == LimbType.RightArm ? rightShoulder.LocalAnchorA : rightShoulder.LocalAnchorB; } LimbJoint rightElbow = rightForearm == null ? GetJointBetweenLimbs(LimbType.RightArm, LimbType.RightHand) : @@ -322,11 +323,12 @@ namespace Barotrauma if (MainLimb == null) { return; } levitatingCollider = true; - ColliderIndex = Crouching ? 1 : 0; - - if (character.SelectedConstruction?.GetComponent()?.ControlCharacterPose ?? false) + ColliderIndex = Crouching && !swimming ? 1 : 0; + if (character.SelectedConstruction?.GetComponent()?.ControlCharacterPose ?? false || + (ForceSelectAnimationType != AnimationType.Walk && ForceSelectAnimationType != AnimationType.NotDefined)) { Crouching = false; + ColliderIndex = 0; } else if (!Crouching && ColliderIndex == 1) { @@ -1611,7 +1613,7 @@ namespace Barotrauma pullLimb.PullJointMaxForce = 5000.0f; targetMovement *= MathHelper.Clamp(Mass / target.Mass, 0.5f, 1.0f); - Vector2 shoulderPos = shoulder.WorldAnchorA; + Vector2 shoulderPos = rightShoulder.WorldAnchorA; Vector2 dragDir = inWater ? Vector2.Normalize(targetLimb.SimPosition - shoulderPos) : Vector2.UnitY; targetAnchor = shoulderPos - dragDir * ConvertUnits.ToSimUnits(upperArmLength + forearmLength); @@ -1756,10 +1758,10 @@ namespace Barotrauma } else { - itemAngle = (torso.body.Rotation + holdAngle * Dir); + itemAngle = torso.body.Rotation + holdAngle * Dir; } - Vector2 transformedHoldPos = shoulder.WorldAnchorA; + Vector2 transformedHoldPos = rightShoulder.WorldAnchorA; if (itemPos == Vector2.Zero || isClimbing || usingController) { if (character.SelectedItems[0] == item) @@ -1780,15 +1782,17 @@ namespace Barotrauma if (character.SelectedItems[0] == item) { if (rightHand == null || rightHand.IsSevered) { return; } + transformedHoldPos = rightShoulder.WorldAnchorA; rightHand.Disabled = true; } if (character.SelectedItems[1] == item) { if (leftHand == null || leftHand.IsSevered) { return; } + transformedHoldPos = leftShoulder.WorldAnchorA; leftHand.Disabled = true; } - itemPos.X = itemPos.X * Dir; + itemPos.X *= Dir; transformedHoldPos += Vector2.Transform(itemPos, Matrix.CreateRotationZ(itemAngle)); } @@ -1857,18 +1861,21 @@ namespace Barotrauma private void HandIK(Limb hand, Vector2 pos, float force = 1.0f) { - if (shoulder == null) { return; } - Vector2 shoulderPos = shoulder.WorldAnchorA; + Vector2 shoulderPos; Limb arm, forearm; if (hand.type == LimbType.LeftHand) { + if (leftShoulder == null) { return; } + shoulderPos = leftShoulder.WorldAnchorA; arm = GetLimb(LimbType.LeftArm); forearm = GetLimb(LimbType.LeftForearm); LeftHandIKPos = pos; } else { + if (rightShoulder == null) { return; } + shoulderPos = rightShoulder.WorldAnchorA; arm = GetLimb(LimbType.RightArm); forearm = GetLimb(LimbType.RightForearm); RightHandIKPos = pos; @@ -1903,7 +1910,7 @@ namespace Barotrauma { if (!MathUtils.IsValid(pos)) { - string errorMsg = "Invalid foot position in FootIK (" + pos + ")\n" + Environment.StackTrace; + string errorMsg = "Invalid foot position in FootIK (" + pos + ")\n" + Environment.StackTrace.CleanupStackTrace(); #if DEBUG DebugConsole.ThrowError(errorMsg); #endif @@ -1937,7 +1944,7 @@ namespace Barotrauma float legAngle = MathUtils.VectorToAngle(pos - waistPos) + MathHelper.PiOver2; if (!MathUtils.IsValid(legAngle)) { - string errorMsg = "Invalid leg angle (" + legAngle + ") in FootIK. Waist pos: " + waistPos + ", target pos: " + pos + "\n" + Environment.StackTrace; + string errorMsg = "Invalid leg angle (" + legAngle + ") in FootIK. Waist pos: " + waistPos + ", target pos: " + pos + "\n" + Environment.StackTrace.CleanupStackTrace(); #if DEBUG DebugConsole.ThrowError(errorMsg); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index a4c12d631..50c0eb472 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -60,12 +60,12 @@ namespace Barotrauma if (!accessRemovedCharacterErrorShown) { string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this); - errorMsg += '\n' + Environment.StackTrace; + errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "Ragdoll.Limbs:AccessRemoved", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace); + "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace()); accessRemovedCharacterErrorShown = true; } return new Limb[0]; @@ -216,16 +216,18 @@ namespace Barotrauma get { Limb mainLimb = GetLimb(RagdollParams.MainLimb); - if (mainLimb == null) + if (!IsValid(mainLimb)) { Limb torso = GetLimb(LimbType.Torso); Limb head = GetLimb(LimbType.Head); mainLimb = torso ?? head; - if (mainLimb == null) + if (!IsValid(mainLimb)) { - mainLimb = Limbs.FirstOrDefault(l => !l.IsSevered && !l.ignoreCollisions); + mainLimb = Limbs.FirstOrDefault(l => IsValid(l)); } } + + bool IsValid(Limb limb) => limb != null && !limb.IsSevered && !limb.ignoreCollisions; return mainLimb; } } @@ -764,11 +766,10 @@ 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); + (limbJoint.LimbA.WorldPosition + limbJoint.LimbB.WorldPosition) / 2, MathHelper.Clamp(Math.Min(limbJoint.LimbA.Mass, limbJoint.LimbB.Mass), 0.5f, 2.0f), isNetworkEvent: false); } SeverLimbJointProjSpecific(limbJoint, playSound: true); @@ -781,6 +782,14 @@ namespace Barotrauma partial void SeverLimbJointProjSpecific(LimbJoint limbJoint, bool playSound); + protected List GetConnectedLimbs(Limb limb) + { + connectedLimbs.Clear(); + checkedJoints.Clear(); + GetConnectedLimbs(connectedLimbs, checkedJoints, limb); + return connectedLimbs; + } + private void GetConnectedLimbs(List connectedLimbs, List checkedJoints, Limb limb) { connectedLimbs.Add(limb); @@ -905,7 +914,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce( "Ragdoll.FindHull:InvalidPosition", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Attempted to find a hull at an invalid position (" + findPos + ")\n" + Environment.StackTrace); + "Attempted to find a hull at an invalid position (" + findPos + ")\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -1557,11 +1566,11 @@ namespace Barotrauma { if (!MathUtils.IsValid(simPosition)) { - DebugConsole.ThrowError("Attempted to move a ragdoll (" + character.Name + ") to an invalid position (" + simPosition + "). " + Environment.StackTrace); + DebugConsole.ThrowError("Attempted to move a ragdoll (" + character.Name + ") to an invalid position (" + simPosition + "). " + Environment.StackTrace.CleanupStackTrace()); GameAnalyticsManager.AddErrorEventOnce( "Ragdoll.SetPosition:InvalidPosition", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Attempted to move a ragdoll (" + character.Name + ") to an invalid position (" + simPosition + "). " + Environment.StackTrace); + "Attempted to move a ragdoll (" + character.Name + ") to an invalid position (" + simPosition + "). " + Environment.StackTrace.CleanupStackTrace()); return; } if (MainLimb == null) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 986992272..369b0fec5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -40,7 +40,11 @@ namespace Barotrauma struct AttackResult { - public readonly float Damage; + public float Damage + { + get; + private set; + } public readonly List Afflictions; public readonly Limb HitLimb; @@ -64,9 +68,7 @@ namespace Barotrauma { Damage = damage; HitLimb = null; - Afflictions = null; - AppliedDamageModifiers = appliedDamageModifiers; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CauseOfDeath.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CauseOfDeath.cs index 097e410fb..987bf64af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CauseOfDeath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CauseOfDeath.cs @@ -18,7 +18,7 @@ namespace Barotrauma { if (type == CauseOfDeathType.Affliction && affliction == null) { - string errorMsg = "Invalid cause of death (the type of the cause of death was Affliction, but affliction was not specified).\n" + Environment.StackTrace; + string errorMsg = "Invalid cause of death (the type of the cause of death was Affliction, but affliction was not specified).\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("InvalidCauseOfDeath", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); type = CauseOfDeathType.Unknown; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index f03c6ab7a..9d123be44 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -131,12 +131,14 @@ namespace Barotrauma protected float oxygenAvailable; //seed used to generate this character - private readonly string seed; + public readonly string Seed; protected Item focusedItem; private Character selectedCharacter, selectedBy; public Character LastAttacker; public Entity LastDamageSource; + public float InvisibleTimer; + private CharacterPrefab prefab; public readonly CharacterParams Params; @@ -181,6 +183,8 @@ namespace Barotrauma public bool IsTraitor; public string TraitorCurrentObjective = ""; public bool IsHuman => SpeciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase); + public bool IsMale => Info != null && Info.HasGenders && Info.Gender == Gender.Male; + public bool IsFemale => Info != null && Info.HasGenders && Info.Gender == Gender.Female; private float attackCoolDown; @@ -195,6 +199,7 @@ namespace Barotrauma if (Info != null) { Info.CurrentOrder = value; } } } + public string CurrentOrderOption { get @@ -207,10 +212,9 @@ namespace Barotrauma } } + public bool IsDismissed => Info != null && Info.IsDismissed; + private readonly List statusEffects = new List(); - private readonly List speedMultipliers = new List(); - private float greatestNegativeSpeedMultiplier = 1f; - private float greatestPositiveSpeedMultiplier = 1f; public Entity ViewTarget { @@ -267,6 +271,12 @@ namespace Barotrauma { get { + if (IsPet) + { + string petName = (AIController as EnemyAIController).PetBehavior.GetTagName(); + if (!string.IsNullOrEmpty(petName)) { return petName; } + } + if (info != null && !string.IsNullOrWhiteSpace(info.Name)) { return info.Name; } var displayName = Params.DisplayName; if (string.IsNullOrWhiteSpace(displayName)) @@ -466,6 +476,11 @@ namespace Barotrauma get { return CharacterHealth.IsUnconscious; } } + public bool IsPet + { + get { return AIController is EnemyAIController enemyController && enemyController.PetBehavior != null; } + } + public float Oxygen { get { return CharacterHealth.OxygenAmount; } @@ -481,7 +496,7 @@ namespace Barotrauma get { return oxygenAvailable; } set { oxygenAvailable = MathHelper.Clamp(value, 0.0f, 100.0f); } } - + public float Stun { get { return IsRagdolled ? 1.0f : CharacterHealth.StunTimer; } @@ -563,7 +578,28 @@ namespace Barotrauma get { return selectedItems; } } - public Item SelectedConstruction { get; set; } + private Item _selectedConstruction; + public Item SelectedConstruction + { + get => _selectedConstruction; + set + { + _selectedConstruction = value; +#if CLIENT + if (Controlled == this) + { + if (_selectedConstruction == null) + { + GameMain.GameSession?.CrewManager?.ResetCrewList(); + } + else if (_selectedConstruction.GetComponent() == null) + { + GameMain.GameSession?.CrewManager?.AutoHideCrewList(); + } + } +#endif + } + } public Item FocusedItem { @@ -584,6 +620,8 @@ namespace Barotrauma public bool IsDead { get; private set; } + public bool IsObserving => AIController is EnemyAIController enemyAI && enemyAI.Enabled && enemyAI.State == AIState.Observe; + public bool EnableDespawn { get; set; } = true; public CauseOfDeath CauseOfDeath @@ -608,7 +646,7 @@ namespace Barotrauma { if (!canBeDragged) { return false; } if (Removed || !AnimController.Draggable) { return false; } - return IsDead || Stun > 0.0f || LockHands || IsIncapacitated; + return IsDead || Stun > 0.0f || LockHands || IsIncapacitated || IsPet; } set { canBeDragged = value; } } @@ -662,12 +700,12 @@ namespace Barotrauma { errorMsg += " AnimController.Collider == null"; } - errorMsg += '\n' + Environment.StackTrace; + errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace(); DebugConsole.NewMessage(errorMsg, Color.Red); GameAnalyticsManager.AddErrorEventOnce( "Character.SimPosition:AccessRemoved", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - errorMsg + "\n" + Environment.StackTrace); + errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); accessRemovedCharacterErrorShown = true; } return Vector2.Zero; @@ -728,6 +766,13 @@ namespace Barotrauma { speciesName = Path.GetFileNameWithoutExtension(speciesName).ToLowerInvariant(); } + + if (CharacterPrefab.FindBySpeciesName(speciesName) == null) + { + DebugConsole.ThrowError($"Failed to create character \"{speciesName}\". Matching prefab not found.\n" + Environment.StackTrace); + return null; + } + Character newCharacter = null; if (!speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase)) { @@ -790,7 +835,7 @@ namespace Barotrauma { prefab = CharacterPrefab.FindBySpeciesName(speciesName); - this.seed = seed; + this.Seed = seed; MTRandom random = new MTRandom(ToolBox.StringToInt(seed)); selectedItems = new Item[2]; @@ -1188,10 +1233,13 @@ namespace Barotrauma return targetMovement; } + private float greatestNegativeSpeedMultiplier = 1f; + private float greatestPositiveSpeedMultiplier = 1f; + /// /// Can be used to modify the character's speed via StatusEffects /// - public float SpeedMultiplier { get; private set; } + public float SpeedMultiplier { get; private set; } = 1; public void StackSpeedMultiplier(float val) { @@ -1217,6 +1265,40 @@ namespace Barotrauma greatestNegativeSpeedMultiplier = 1f; } + private float greatestNegativeHealthMultiplier = 1f; + private float greatestPositiveHealthMultiplier = 1f; + + /// + /// Can be used to modify the character's health via StatusEffects + /// + public float HealthMultiplier { get; private set; } = 1; + + public void StackHealthMultiplier(float val) + { + if (val < 1f) + { + if (val < greatestNegativeHealthMultiplier) + { + greatestNegativeHealthMultiplier = val; + } + } + else + { + if (val > greatestPositiveHealthMultiplier) + { + greatestPositiveHealthMultiplier = val; + } + } + } + + private void CalculateHealthMultiplier() + { + HealthMultiplier = greatestPositiveHealthMultiplier - (1f - greatestNegativeHealthMultiplier); + // Reset, status effects should set the values again, if the conditions match + greatestPositiveHealthMultiplier = 1f; + greatestNegativeHealthMultiplier = 1f; + } + /// /// Speed reduction from the current limb specific damage. Min 0, max 1. /// @@ -1780,7 +1862,6 @@ namespace Barotrauma if (ignoreBroken && item.Condition <= 0) { continue; } if (Submarine != null) { - if (item.Submarine.Info.Type != Submarine.Info.Type) { continue; } if (!Submarine.IsEntityFoundOnThisSub(item, true)) { continue; } } if (customPredicate != null && !customPredicate(item)) { continue; } @@ -1813,7 +1894,7 @@ namespace Barotrauma public bool CanInteractWith(Character c, float maxDist = 200.0f, bool checkVisibility = true, bool skipDistanceCheck = false) { - if (c == this || Removed || !c.Enabled || !c.CanBeSelected) { return false; } + if (c == this || Removed || !c.Enabled || !c.CanBeSelected || c.InvisibleTimer > 0.0f) { return false; } if (!c.CharacterHealth.UseHealthWindow && !c.CanBeDragged && (c.onCustomInteract == null || !c.AllowCustomInteract)) { return false; } if (!skipDistanceCheck) @@ -2103,6 +2184,10 @@ namespace Barotrauma { SelectCharacter(FocusedCharacter); } + else if (FocusedCharacter != null && !FocusedCharacter.IsIncapacitated && IsKeyHit(InputType.Use) && FocusedCharacter.IsPet && CanInteract) + { + (FocusedCharacter.AIController as EnemyAIController).PetBehavior.Play(this); + } else if (FocusedCharacter != null && IsKeyHit(InputType.Health) && FocusedCharacter.CharacterHealth.UseHealthWindow && CanInteract && CanInteractWith(FocusedCharacter, 160f, false)) { if (FocusedCharacter == SelectedCharacter) @@ -2340,6 +2425,7 @@ namespace Barotrauma } ApplyStatusEffects(AnimController.InWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); + ApplyStatusEffects(ActionType.OnActive, deltaTime); UpdateControlled(deltaTime, cam); @@ -2348,6 +2434,8 @@ namespace Barotrauma { UpdateOxygen(deltaTime); } + + CalculateHealthMultiplier(); CharacterHealth.Update(deltaTime); if (IsIncapacitated) @@ -2654,7 +2742,7 @@ namespace Barotrauma if (orderGiver != null && !CanHearCharacter(orderGiver)) { return; } // If there's another character operating the same device, make them dismiss themself - if (order != null && order.Category == OrderCategory.Operate) + if (order != null && order.Category == OrderCategory.Operate && order.TargetEntity != null) { CharacterList.FindAll(c => c != this && c.TeamID == TeamID && c.CurrentOrder is Order characterOrder && characterOrder.Category == OrderCategory.Operate && characterOrder.Identifier.Equals(order.Identifier) && characterOrder.TargetEntity == order.TargetEntity)? @@ -2794,7 +2882,7 @@ namespace Barotrauma { if (Removed) { - string errorMsg = "Tried to apply an attack to a removed character (" + Name + ").\n" + Environment.StackTrace; + string errorMsg = "Tried to apply an attack to a removed character (" + Name + ").\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("Character.ApplyAttack:RemovedCharacter", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return new AttackResult(); @@ -2838,7 +2926,7 @@ namespace Barotrauma } #endif // Don't allow beheading for monster attacks, because it happens too frequently (crawlers/tigerthreshers etc attacking each other -> they will most often target to the head) - TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: AIController == null || AIController is HumanAIController); + TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: attacker == null || attacker.IsHuman || attacker.IsPlayer); return attackResult; } @@ -2864,7 +2952,8 @@ namespace Barotrauma foreach (LimbJoint joint in AnimController.LimbJoints) { if (!joint.CanBeSevered) { continue; } - if (joint.LimbA != targetLimb && joint.LimbB != targetLimb) { continue; } + // Limb A is where we usually create the joints from. Let's not allow severing when the "parent" limb is hit, or the head can pop off when we hit the torso, for example. + if (joint.LimbB != targetLimb) { continue; } float probability = severLimbsProbability; if (!IsDead) { @@ -2880,7 +2969,7 @@ namespace Barotrauma 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, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f); ApplyStatusEffects(ActionType.OnSevered, 1.0f); targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f); otherLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f); @@ -2937,7 +3026,7 @@ namespace Barotrauma // string errorMsg = $"Character {Name} received damage from outside the sub while inside (attacker: {attacker.Name})"; // GameAnalyticsManager.AddErrorEventOnce("Character.DamageLimb:DamageFromOutside" + Name + attacker.Name, // GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, -// errorMsg + "\n" + Environment.StackTrace); +// errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); //#if DEBUG // DebugConsole.ThrowError(errorMsg); //#endif @@ -2968,11 +3057,6 @@ namespace Barotrauma Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir); AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound); CharacterHealth.ApplyDamage(hitLimb, attackResult); - ApplyStatusEffects(ActionType.OnDamaged, 1.0f); - if (attackResult.Damage > 0) - { - hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f); - } if (attacker != this) { OnAttacked?.Invoke(attacker, attackResult); @@ -2982,12 +3066,15 @@ namespace Barotrauma TryAdjustAttackerSkill(attacker, -attackResult.Damage); } }; - - if (attacker != null && attackResult.Damage > 0.0f) + if (attackResult.Damage > 0) { - LastAttacker = attacker; + ApplyStatusEffects(ActionType.OnDamaged, 1.0f); + hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f); + if (attacker != null) + { + LastAttacker = attacker; + } } - return attackResult; } @@ -3045,7 +3132,7 @@ namespace Barotrauma { targets.Clear(); statusEffect.GetNearbyTargets(WorldPosition, targets); - statusEffect.Apply(ActionType.OnActive, deltaTime, this, targets); + statusEffect.Apply(actionType, deltaTime, this, targets); } else { @@ -3147,6 +3234,11 @@ namespace Barotrauma return; } + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + { + GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status }); + } + IsDead = true; ApplyStatusEffects(ActionType.OnDeath, 1.0f); @@ -3215,7 +3307,7 @@ namespace Barotrauma { if (Removed) { - DebugConsole.ThrowError("Attempting to revive an already removed character\n" + Environment.StackTrace); + DebugConsole.ThrowError("Attempting to revive an already removed character\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -3260,7 +3352,7 @@ namespace Barotrauma { if (Removed) { - DebugConsole.ThrowError("Attempting to remove an already removed character\n" + Environment.StackTrace); + DebugConsole.ThrowError("Attempting to remove an already removed character\n" + Environment.StackTrace.CleanupStackTrace()); return; } DebugConsole.Log("Removing character " + Name + " (ID: " + ID + ")"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index e4a39925f..71297fecd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -151,13 +151,13 @@ namespace Barotrauma public XElement HealthData; private static ushort idCounter; + private const string disguiseName = "???"; public string Name; public string DisplayName { get { - string disguiseName = "?"; if (Character == null || !Character.HideFace) { return Name; @@ -300,6 +300,7 @@ namespace Barotrauma public Order CurrentOrder { get; set; } public string CurrentOrderOption { get; set; } + public bool IsDismissed => CurrentOrder == null || CurrentOrder.Identifier.Equals("dismissed", StringComparison.OrdinalIgnoreCase); //unique ID given to character infos in MP //used by clients to identify which infos are the same to prevent duplicate characters in round summary diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index b1d2ddc12..62fe02062 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -37,7 +37,7 @@ namespace Barotrauma public string Identifier { get; private set; } [Serialize(1.0f, true, description: "The probability for the affliction to be applied."), Editable(minValue: 0f, maxValue: 1f)] - public float Probability { get; private set; } = 1.0f; + public float Probability { get; set; } = 1.0f; public float DamagePerSecond; public float DamagePerSecondTimer; @@ -264,5 +264,10 @@ namespace Barotrauma /// Ideally we would keep this private, but doing so would require too much refactoring. /// public void SetStrength(float strength) => _strength = strength; + + public bool ShouldShowIcon(Character afflictedCharacter) + { + return Strength >= (afflictedCharacter == Character.Controlled ? Prefab.ShowIconThreshold : Prefab.ShowIconToOthersThreshold); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index cb83175cd..39f9edb2b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -273,6 +273,8 @@ namespace Barotrauma public readonly float ActivationThreshold = 0.0f; //how high the strength has to be for the affliction icon to be shown in the UI public readonly float ShowIconThreshold = 0.05f; + //how high the strength has to be for the affliction icon to be shown to others with a health scanner or via the health interface + public readonly float ShowIconToOthersThreshold = 0.05f; public readonly float MaxStrength = 100.0f; //how high the strength has to be for the affliction icon to be shown with a health scanner @@ -555,6 +557,7 @@ namespace Barotrauma ActivationThreshold = element.GetAttributeFloat("activationthreshold", 0.0f); ShowIconThreshold = element.GetAttributeFloat("showiconthreshold", Math.Max(ActivationThreshold, 0.05f)); + ShowIconToOthersThreshold = element.GetAttributeFloat("showicontoothersthreshold", ShowIconThreshold); MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f); ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, 0.05f)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs index a65e14cc8..724184a11 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs @@ -1,12 +1,4 @@ -using Microsoft.Xna.Framework; -using System.Collections.Generic; -using Barotrauma.IO; -using System.Linq; -using Barotrauma.Extensions; -using System.Xml.Linq; -using System; - -namespace Barotrauma +namespace Barotrauma { partial class AfflictionPsychosis : Affliction { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 68bd64e00..9512f59a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -141,17 +141,17 @@ namespace Barotrauma { get { + float max = maxVitality; if (Character?.Info?.Job?.Prefab != null) { - return maxVitality + Character.Info.Job.Prefab.VitalityModifier; + max += Character.Info.Job.Prefab.VitalityModifier; } - return maxVitality; + return max * Character.HealthMultiplier; } set { maxVitality = Math.Max(0, value); } - } public float MinVitality @@ -450,9 +450,13 @@ namespace Barotrauma matchingAfflictions.AddRange(limbHealth.Afflictions); } } - matchingAfflictions.RemoveAll(a => - !a.Prefab.Identifier.Equals(affliction, StringComparison.OrdinalIgnoreCase) && - !a.Prefab.AfflictionType.Equals(affliction, StringComparison.OrdinalIgnoreCase)); + + if (!string.IsNullOrEmpty(affliction)) + { + matchingAfflictions.RemoveAll(a => + !a.Prefab.Identifier.Equals(affliction, StringComparison.OrdinalIgnoreCase) && + !a.Prefab.AfflictionType.Equals(affliction, StringComparison.OrdinalIgnoreCase)); + } if (matchingAfflictions.Count == 0) return; @@ -617,8 +621,8 @@ namespace Barotrauma private void AddAffliction(Affliction newAffliction) { - if (!DoesBleed && newAffliction is AfflictionBleeding) return; - if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return; + if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } + if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } if (newAffliction.Prefab.AfflictionType == "huskinfection") { var huskPrefab = newAffliction.Prefab as AfflictionPrefabHusk; @@ -636,7 +640,10 @@ namespace Barotrauma affliction.Strength = newStrength; affliction.Source = newAffliction.Source; CalculateVitality(); - if (Vitality <= MinVitality) Kill(); + if (Vitality <= MinVitality) + { + Kill(); + } return; } } @@ -650,7 +657,10 @@ namespace Barotrauma Character.HealthUpdateInterval = 0.0f; CalculateVitality(); - if (Vitality <= MinVitality) Kill(); + if (Vitality <= MinVitality) + { + Kill(); + } } @@ -707,7 +717,11 @@ namespace Barotrauma UpdateLimbAfflictionOverlays(); CalculateVitality(); - if (Vitality <= MinVitality) Kill(); + + if (Vitality <= MinVitality) + { + Kill(); + } } private void UpdateOxygen(float deltaTime) @@ -725,10 +739,10 @@ namespace Barotrauma OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? -5.0f : 10.0f), -100.0f, 100.0f); } - UpdateOxygenProjSpecific(prevOxygen); + UpdateOxygenProjSpecific(prevOxygen, deltaTime); } - partial void UpdateOxygenProjSpecific(float prevOxygen); + partial void UpdateOxygenProjSpecific(float prevOxygen, float deltaTime); partial void UpdateBleedingProjSpecific(AfflictionBleeding affliction, Limb targetLimb, float deltaTime); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index 5d23aa8ca..00d102c41 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -163,7 +163,13 @@ namespace Barotrauma { item.AddTag("job:" + job.Name); } + var idCardTags = itemElement.GetAttributeStringArray("tags", new string[0]); + foreach (string tag in idCardTags) + { + item.AddTag(tag); + } } + foreach (WifiComponent wifiComponent in item.GetComponents()) { wifiComponent.TeamID = character.TeamID; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 2b4ecad93..71ef86a5b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -203,7 +203,7 @@ namespace Barotrauma partial class Limb : ISerializableEntity, ISpatialEntity { //how long it takes for severed limbs to fade out - private const float SeveredFadeOutTime = 10.0f; + public float SeveredFadeOutTime => Params.SeveredFadeOutTime; public readonly Character character; /// @@ -327,10 +327,10 @@ namespace Barotrauma if (Removed) { #if DEBUG - DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace); + DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); #endif GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:SimPosition", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Attempted to access a removed limb.\n" + Environment.StackTrace); + "Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); return Vector2.Zero; } return body.SimPosition; @@ -344,10 +344,10 @@ namespace Barotrauma if (Removed) { #if DEBUG - DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace); + DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); #endif GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:SimPosition", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Attempted to access a removed limb.\n" + Environment.StackTrace); + "Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); return 0.0f; } return body.Rotation; @@ -364,10 +364,10 @@ namespace Barotrauma if (Removed) { #if DEBUG - DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace); + DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); #endif GameAnalyticsManager.AddErrorEventOnce("Limb.Mass:AccessRemoved", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Attempted to access a removed limb.\n" + Environment.StackTrace); + "Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); return 1.0f; } return body.Mass; @@ -383,10 +383,10 @@ namespace Barotrauma if (Removed) { #if DEBUG - DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace); + DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); #endif GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:AccessRemoved", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Attempted to access a removed limb.\n" + Environment.StackTrace); + "Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); return Vector2.Zero; } return body.LinearVelocity; @@ -428,7 +428,7 @@ namespace Barotrauma { if (!MathUtils.IsValid(value)) { - string errorMsg = "Attempted to set the anchor A of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace; + string errorMsg = "Attempted to set the anchor A of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:InvalidValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); @@ -442,7 +442,7 @@ namespace Barotrauma string errorMsg = "Attempted to move the anchor A of a limb's pull joint extremely far from the limb (diff: " + diff + ", limb enabled: " + body.Enabled + ", simple physics enabled: " + character.AnimController.SimplePhysicsEnabled + ")\n" - + Environment.StackTrace; + + Environment.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:ExcessiveValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); @@ -461,7 +461,7 @@ namespace Barotrauma { if (!MathUtils.IsValid(value)) { - string errorMsg = "Attempted to set the anchor B of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace; + string errorMsg = "Attempted to set the anchor B of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:InvalidValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); @@ -475,7 +475,7 @@ namespace Barotrauma string errorMsg = "Attempted to move the anchor B of a limb's pull joint extremely far from the limb (diff: " + diff + ", limb enabled: " + body.Enabled + ", simple physics enabled: " + character.AnimController.SimplePhysicsEnabled + ")\n" - + Environment.StackTrace; + + Environment.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:ExcessiveValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); @@ -715,7 +715,7 @@ namespace Barotrauma 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); + character.CurrentHull.AddDecal(character.BloodDecalName, WorldPosition, MathHelper.Clamp(bloodDecalSize, 0.5f, 1.0f), isNetworkEvent: false); } } } @@ -768,6 +768,18 @@ namespace Barotrauma { attack.UpdateCoolDown(deltaTime); } + + if (Params.BlinkFrequency > 0) + { + if (blinkTimer > -TotalBlinkDurationOut) + { + blinkTimer -= deltaTime; + } + else + { + blinkTimer = Params.BlinkFrequency; + } + } } partial void UpdateProjSpecific(float deltaTime); @@ -1039,6 +1051,45 @@ namespace Barotrauma } } + private float blinkTimer; + private float blinkPhase; + + private float TotalBlinkDurationOut => Params.BlinkDurationOut + Params.BlinkHoldTime; + + public void Blink(float deltaTime, float referenceRotation) + { + if (blinkTimer > -TotalBlinkDurationOut) + { + blinkPhase -= deltaTime; + if (blinkPhase > 0) + { + // in + float t = ToolBox.GetEasing(Params.BlinkTransitionIn, MathUtils.InverseLerp(1, 0, blinkPhase / Params.BlinkDurationIn)); + body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationIn) * Dir, Mass * Params.BlinkForce * t, wrapAngle: true); + } + else + { + if (Math.Abs(blinkPhase) < Params.BlinkHoldTime) + { + // hold + body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationIn) * Dir, Mass * Params.BlinkForce, wrapAngle: true); + } + else + { + // out + float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, -blinkPhase / TotalBlinkDurationOut)); + body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationOut) * Dir, Mass * Params.BlinkForce * t, wrapAngle: true); + } + } + } + else + { + // out + blinkPhase = Params.BlinkDurationIn; + body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationOut) * Dir, Mass * Params.BlinkForce, wrapAngle: true); + } + } + public void Remove() { body?.Remove(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index cab24ff0a..1d18cf6d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -305,7 +305,7 @@ namespace Barotrauma instance.Load(fullPath, speciesName); anims.Add(fileName, instance); DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite); - return instance as T; + return instance; } public bool Serialize() => base.Serialize(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 724d7514a..4b5530063 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -474,6 +474,12 @@ namespace Barotrauma [Serialize(true, true, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable] public bool AvoidGunfire { get; private set; } + [Serialize(3f, true, description: "How long the creature avoids gunfire. Also used when the creature is unlatched."), Editable(minValue: 0f, maxValue: 100f)] + public float AvoidTime { get; private set; } + + [Serialize(20f, true, description: "How long the creature flees before returning to normal state. When the creature sees the target or is being chased, it will always flee, if it's in the flee state."), Editable(minValue: 0f, maxValue: 100f)] + public float MinFleeTime { get; private set; } + [Serialize(false, true, description: "Does the character try to break inside the sub?"), Editable()] public bool AggressiveBoarding { get; private set; } @@ -571,12 +577,18 @@ namespace Barotrauma [Serialize(0f, true, description: "What base priority is given to the target?"), Editable(minValue: 0f, maxValue: 1000f, ValueStep = 1, DecimalCount = 0)] public float Priority { get; set; } - [Serialize(0f, true, description: "Generic distance that can be used for different purposes depending on the state. Eg. in Avoid state this defines the distance that the character tries to keep to the target. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)] + [Serialize(0f, true, description: "Generic distance that can be used for different purposes depending on the state. E.g. in Avoid state this defines the distance that the character tries to keep to the target. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)] public float ReactDistance { get; set; } [Serialize(0f, true, description: "Used for defining the attack distance for PassiveAggressive and Aggressive states. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)] public float AttackDistance { get; set; } + [Serialize(0f, true, description: "Generic timer that can be used for different purposes depending on the state. E.g. in Observe state this defines how long the character in general keeps staring the targets (Some random is always applied)."), Editable] + public float Timer { get; set; } + + [Serialize(false, true, description: "Should the target be ignored if it's inside a container/inventory. Only affects items."), Editable] + public bool IgnoreContained { get; set; } + public TargetParams(XElement element, CharacterParams character) : base(element, character) { } public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(tag, state, priority), character) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index ea95217df..9a02bf2a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -77,7 +77,7 @@ namespace Barotrauma [Serialize(LimbType.Torso, true), Editable] public LimbType MainLimb { get; set; } - private static Dictionary> allRagdolls = new Dictionary>(); + private readonly static Dictionary> allRagdolls = new Dictionary>(); public List Colliders { get; private set; } = new List(); public List Limbs { get; private set; } = new List(); @@ -464,7 +464,7 @@ namespace Barotrauma /// /// Should be converted to sim units. /// - [Serialize("1.0, 1.0", true, description: "Local position of the join in the Limb2."), Editable()] + [Serialize("1.0, 1.0", true, description: "Local position of the joint in the Limb2."), Editable()] public Vector2 Limb2Anchor { get; set; } [Serialize(true, true), Editable] @@ -500,6 +500,9 @@ namespace Barotrauma [Serialize(false, false), Editable(ReadOnly = true)] public bool WeldJoint { get; set; } + [Serialize(false, true), Editable] + public bool ClockWiseRotation { get; set; } + public JointParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { } } @@ -543,14 +546,17 @@ namespace Barotrauma [Serialize(LimbType.None, true, description: "The limb type affects many things, like the animations. Torso or Head are considered as the main limbs. Every character should have at least one Torso or Head."), Editable()] public LimbType Type { get; set; } - [Serialize(float.NaN, true, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings. Used mainly for animations and widgets."), Editable(-360, 360)] - public float SpriteOrientation { get; set; } - /// /// The orientation of the sprite as drawn on the sprite sheet (in radians). /// public float GetSpriteOrientation() => MathHelper.ToRadians(float.IsNaN(SpriteOrientation) ? Ragdoll.SpritesheetOrientation : SpriteOrientation); + [Serialize("", true), Editable] + public string Notes { get; set; } + + [Serialize(1f, true), Editable] + public float Scale { get; set; } + [Serialize(true, true, description: "Does the limb flip when the character flips?"), Editable()] public bool Flip { get; set; } @@ -563,8 +569,8 @@ namespace Barotrauma [Serialize(false, true, description: "Disable drawing for this limb."), Editable()] public bool Hide { get; set; } - [Serialize(1f, true, description: "Higher values make AI characters prefer attacking this limb."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10)] - public float AttackPriority { get; set; } + [Serialize(float.NaN, true, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings. Used mainly for animations and widgets."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)] + public float SpriteOrientation { get; set; } [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)] public float SteerForce { get; set; } @@ -587,6 +593,9 @@ namespace Barotrauma [Serialize(7f, true, description: "Increasing the damping makes the limb stop rotating more quickly."), Editable] public float AngularDamping { get; set; } + [Serialize(1f, true, description: "Higher values make AI characters prefer attacking this limb."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10)] + public float AttackPriority { get; set; } + [Serialize("0, 0", true, description: "The position which is used to lead the IK chain to the IK goal. Only applicable if the limb is hand or foot."), Editable()] public Vector2 PullPos { get; set; } @@ -599,24 +608,58 @@ namespace Barotrauma [Serialize("0, 0", true, description: "Relative offset for the mouth position (starting from the center). Only applicable for LimbType.Head. Used for eating."), Editable(DecimalCount = 2, MinValueFloat = -10f, MaxValueFloat = 10f)] public Vector2 MouthPos { get; set; } - [Serialize("", true), Editable] - public string Notes { get; set; } - [Serialize(0f, true), Editable] public float ConstantTorque { get; set; } [Serialize(0f, true), Editable] public float ConstantAngle { get; set; } - [Serialize(1f, true), Editable] - public float Scale { get; set; } - [Serialize(1f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 10)] public float AttackForceMultiplier { get; set; } [Serialize(1f, true, description:"How much damage must be done by the attack in order to be able to cut off the limb. Note that it's evaluated after the damage modifiers."), Editable(DecimalCount = 0, MinValueFloat = 0, MaxValueFloat = 1000)] public float MinSeveranceDamage { get; set; } + //how long it takes for severed limbs to fade out + [Serialize(10f, true, "How long it takes for the severed limb to fade out"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1)] + public float SeveredFadeOutTime { get; set; } = 10.0f; + + [Serialize(false, true, description: "Only applied when the limb is of type Tail. If none of the tails have been defined to use the angle and an angle is defined in the animation parameters, the first tail limb is used."), Editable] + public bool ApplyTailAngle { get; set; } + + [Serialize(1f, true), Editable(ValueStep = 0.1f, DecimalCount = 2)] + public float SineFrequencyMultiplier { get; set; } + + [Serialize(1f, true), Editable(ValueStep = 0.1f, DecimalCount = 2)] + public float SineAmplitudeMultiplier { get; set; } + + [Serialize(0f, true), Editable(0, 100, ValueStep = 1, DecimalCount = 1)] + public float BlinkFrequency { get; set; } + + [Serialize(0.2f, true), Editable(0.01f, 10, ValueStep = 1, DecimalCount = 2)] + public float BlinkDurationIn { get; set; } + + [Serialize(0.5f, true), Editable(0.01f, 10, ValueStep = 1, DecimalCount = 2)] + public float BlinkDurationOut { get; set; } + + [Serialize(0f, true), Editable(0, 10, ValueStep = 1, DecimalCount = 2)] + public float BlinkHoldTime { get; set; } + + [Serialize(0f, true), Editable(-360, 360, ValueStep = 1, DecimalCount = 0)] + public float BlinkRotationIn { get; set; } + + [Serialize(45f, true), Editable(-360, 360, ValueStep = 1, DecimalCount = 0)] + public float BlinkRotationOut { get; set; } + + [Serialize(50f, true), Editable] + public float BlinkForce { get; set; } + + [Serialize(TransitionMode.Linear, true), Editable] + public TransitionMode BlinkTransitionIn { get; private set; } + + [Serialize(TransitionMode.Linear, true), Editable] + public TransitionMode BlinkTransitionOut { get; private set; } + // Non-editable -> // TODO: make read-only [Serialize(0, true)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/CommandPattern.cs b/Barotrauma/BarotraumaShared/SharedSource/CommandPattern.cs new file mode 100644 index 000000000..7a563a0ec --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CommandPattern.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + /// + /// Implementation of the Command pattern. + /// + /// + /// + /// Created by Markus Isberg on 11th of March 2020 for the submarine editor. + /// "Implementing a global undo and redo with Memento pattern proved too difficult of a task for me so I implemented it with this pattern instead." + /// + internal abstract partial class Command + { + /// + /// A method that should apply a new state on an object or perform an action + /// + public abstract void Execute(); + + /// + /// A method that should revert Execute() method's actions + /// + public abstract void UnExecute(); + + /// + /// State no longer exists, clean up the lingering garbage + /// + public abstract void Cleanup(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs index 503de101f..35e0bead6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs @@ -543,9 +543,6 @@ namespace Barotrauma if (SteamWorkshopId != 0) { 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) diff --git a/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs b/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs index 6ef497afd..4091faa8c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs @@ -230,7 +230,7 @@ namespace Barotrauma #if CLIENT && WINDOWS if (e is SharpDX.SharpDXException) { throw; } #endif - DebugConsole.ThrowError("Coroutine " + handle.Name + " threw an exception: " + e.Message + "\n" + e.StackTrace.ToString()); + DebugConsole.ThrowError("Coroutine " + handle.Name + " threw an exception: " + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); handle.Exception = e; return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 8e65cda85..73298718e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -230,7 +230,7 @@ namespace Barotrauma { string errorMsg = "Failed to spawn an item. Arguments: \"" + string.Join(" ", args) + "\"."; ThrowError(errorMsg, e); - GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnItem:Error", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace); + GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnItem:Error", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace.CleanupStackTrace()); } }, () => @@ -1019,7 +1019,7 @@ namespace Barotrauma } catch (InvalidOperationException e) { - string errorMsg = "Error while executing the fixhulls command.\n" + e.StackTrace; + string errorMsg = "Error while executing the fixhulls command.\n" + e.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("DebugConsole.FixHulls", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } } @@ -1899,15 +1899,15 @@ namespace Barotrauma { if (e != null) { - error += " {" + e.Message + "}\n" + e.StackTrace; + error += " {" + e.Message + "}\n" + e.StackTrace.CleanupStackTrace(); if (e.InnerException != null) { - error += "\n\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace; + error += "\n\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace(); } } else if (appendStackTrace) { - error += "\n" + Environment.StackTrace; + error += "\n" + Environment.StackTrace.CleanupStackTrace(); } System.Diagnostics.Debug.WriteLine(error); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Decals/Decal.cs b/Barotrauma/BarotraumaShared/SharedSource/Decals/Decal.cs index 6a304b697..b42f1131c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Decals/Decal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Decals/Decal.cs @@ -34,11 +34,11 @@ namespace Barotrauma get { return Prefab.LifeTime; } } - private float baseAlpha = 1.0f; public float BaseAlpha { - get { return baseAlpha; } - } + get; + set; + } = 1.0f; public Color Color { @@ -58,9 +58,10 @@ namespace Barotrauma } } - public Vector2 Position + public Vector2 CenterPosition { - get { return position; } + get; + private set; } public Vector2 NonClampedPosition @@ -69,6 +70,12 @@ namespace Barotrauma private set; } + public int SpriteIndex + { + get; + private set; + } + private readonly HashSet affectedSections; private readonly Hull hull; @@ -79,7 +86,7 @@ namespace Barotrauma private bool cleaned = false; - public Decal(DecalPrefab prefab, float scale, Vector2 worldPosition, Hull hull) + public Decal(DecalPrefab prefab, float scale, Vector2 worldPosition, Hull hull, int? spriteIndex = null) { Prefab = prefab; @@ -90,7 +97,8 @@ namespace Barotrauma Vector2 drawPos = position + hull.Rect.Location.ToVector2(); - Sprite = prefab.Sprites[Rand.Range(0, prefab.Sprites.Count, Rand.RandSync.Unsynced)]; + SpriteIndex = spriteIndex ?? Rand.Range(0, prefab.Sprites.Count, Rand.RandSync.Unsynced); + Sprite = prefab.Sprites[SpriteIndex]; Color = prefab.Color; Rectangle drawRect = new Rectangle( @@ -111,6 +119,8 @@ namespace Barotrauma Sprite.SourceRect.Width - (int)((overFlowAmount.X + overFlowAmount.Width) / scale), Sprite.SourceRect.Height - (int)((overFlowAmount.Y + overFlowAmount.Height) / scale)); + CenterPosition = position; + position -= new Vector2(Sprite.size.X / 2 * scale - overFlowAmount.X, -Sprite.size.Y / 2 * scale + overFlowAmount.Y); this.Scale = scale; @@ -148,20 +158,20 @@ namespace Barotrauma { cleaned = true; float sizeModifier = MathHelper.Clamp(Sprite.size.X * Sprite.size.Y * Scale / 10000, 1.0f, 25.0f); - baseAlpha -= val * -1 / sizeModifier; + BaseAlpha -= val * -1 / sizeModifier; } private float GetAlpha() { if (fadeTimer < Prefab.FadeInTime && !cleaned) { - return baseAlpha * fadeTimer / Prefab.FadeInTime; + 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 * Math.Min((Prefab.LifeTime - fadeTimer) / Prefab.FadeOutTime, 1.0f); } - return baseAlpha; + return BaseAlpha; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs index 56d3b76d1..b107eac25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs @@ -108,7 +108,7 @@ namespace Barotrauma } } - public Decal CreateDecal(string decalName, float scale, Vector2 worldPosition, Hull hull) + public Decal CreateDecal(string decalName, float scale, Vector2 worldPosition, Hull hull, int? spriteIndex = null) { if (!Prefabs.ContainsKey(decalName.ToLowerInvariant())) { @@ -118,7 +118,7 @@ namespace Barotrauma DecalPrefab prefab = Prefabs[decalName]; - return new Decal(prefab, scale, worldPosition, hull); + return new Decal(prefab, scale, worldPosition, hull, spriteIndex); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalPrefab.cs index fe55e454d..3f80bbecd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalPrefab.cs @@ -62,9 +62,9 @@ namespace Barotrauma { Sprites.Add(new Sprite(subElement)); } - } - - Color = new Color(element.GetAttributeVector4("color", Vector4.One)); + } + + Color = element.GetAttributeColor("color", Color.White); LifeTime = element.GetAttributeFloat("lifetime", 10.0f); FadeOutTime = Math.Min(LifeTime, element.GetAttributeFloat("fadeouttime", 1.0f)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 626979d2c..f31a03108 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -21,6 +21,7 @@ OnDeath = OnBroken, OnDamaged, OnSevered, - OnProduceSpawned + OnProduceSpawned, + OnOpen, OnClose, } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs index 148ef7e9c..f08ec8423 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs @@ -9,7 +9,17 @@ namespace Barotrauma [Serialize(0.0f, true)] public float Chance { get; set; } - public RNGAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public RNGAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + { + if (Chance >= 1.0f) + { + DebugConsole.ThrowError($"Incorrectly configured RNG Action in event \"{parentEvent.Prefab.Identifier}\". Probability is 1.0 (100%) or more, the action will always succeed."); + } + else if (Chance <= 0.0f) + { + DebugConsole.ThrowError($"Incorrectly configured RNG Action in event \"{parentEvent.Prefab.Identifier}\". Probability is 0 or less, the action will never succeed."); + } + } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 796e995d2..28f60d666 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -120,6 +120,7 @@ namespace Barotrauma AddChildEvents(initialEventSet); void AddChildEvents(EventSet eventSet) { + if (eventSet == null) { return; } foreach (EventPrefab ep in eventSet.EventPrefabs.Select(e => e.First)) { if (!level.LevelData.NonRepeatableEvents.Contains(ep)) @@ -582,7 +583,7 @@ namespace Barotrauma enemyDanger = 0.0f; foreach (Character character in Character.CharacterList) { - if (character.IsDead || character.IsIncapacitated || !character.Enabled) continue; + if (character.IsDead || character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup("human")) { continue; } EnemyAIController enemyAI = character.AIController as EnemyAIController; if (enemyAI == null) continue; @@ -591,13 +592,13 @@ namespace Barotrauma (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine))) { //crawler inside the sub adds 0.1f to enemy danger, mantis 0.25f - enemyDanger += enemyAI.CombatStrength / 1000.0f; + enemyDanger += enemyAI.CombatStrength / 100.0f; } else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null) { //enemy outside and targeting the sub or something in it //moloch adds 0.24 to enemy danger, a crawler 0.02 - enemyDanger += enemyAI.CombatStrength / 2000.0f; + enemyDanger += enemyAI.CombatStrength / 1000.0f; } } enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f); @@ -653,12 +654,12 @@ namespace Barotrauma if (targetIntensity > currentIntensity) { //25 seconds for intensity to go from 0.0 to 1.0 - currentIntensity = MathHelper.Min(currentIntensity + 0.04f * IntensityUpdateInterval, targetIntensity); + currentIntensity = Math.Min(currentIntensity + 0.04f * IntensityUpdateInterval, targetIntensity); } else { //400 seconds for intensity to go from 1.0 to 0.0 - currentIntensity = MathHelper.Max(0.0025f * IntensityUpdateInterval, targetIntensity); + currentIntensity = Math.Max(currentIntensity - 0.0025f * IntensityUpdateInterval, targetIntensity); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringFormatter.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringFormatter.cs index dff706002..f16c75460 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringFormatter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringFormatter.cs @@ -63,6 +63,16 @@ namespace Barotrauma { return $"({value.X.FormatSingleDecimal()}, {value.Y.FormatSingleDecimal()})"; } + + public static string FormatSingleDecimal(this Vector3 value) + { + return $"({value.X.FormatSingleDecimal()}, {value.Y.FormatSingleDecimal()}, {value.Z.FormatSingleDecimal()})"; + } + + public static string FormatSingleDecimal(this Vector4 value) + { + return $"({value.X.FormatSingleDecimal()}, {value.Y.FormatSingleDecimal()}, {value.Z.FormatSingleDecimal()}, {value.W.FormatSingleDecimal()})"; + } public static string FormatDoubleDecimal(this Vector2 value) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/ForbiddenWordFilter.cs b/Barotrauma/BarotraumaShared/SharedSource/ForbiddenWordFilter.cs new file mode 100644 index 000000000..6cd5d2d46 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ForbiddenWordFilter.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Barotrauma +{ + static class ForbiddenWordFilter + { + static readonly string fileListPath = Path.Combine("Data", "forbiddenwordlist.txt"); + + private static readonly HashSet forbiddenWords; + + static ForbiddenWordFilter() + { + try + { + forbiddenWords = File.ReadAllLines(fileListPath).ToHashSet(); + } + catch (IOException e) + { + DebugConsole.ThrowError($"Failed to load the list of forbidden words from {fileListPath}.", e); + } + } + + public static bool IsForbidden(string text) + { + return IsForbidden(text, out _); + } + + public static bool IsForbidden(string text, out string forbiddenWord) + { + forbiddenWord = string.Empty; + if (forbiddenWords == null) + { + return false; + } + + char[] delimiters = new char[] { ' ', '-', '.', '_', ':', ';', '\'' }; + + HashSet words = new HashSet(); + foreach (char delimiter in delimiters) + { + foreach (string word in text.Split(delimiter)) + { + words.Add(word); + } + } + + foreach (string word in words) + { + if (forbiddenWords.Any(w => Homoglyphs.Compare(word, w) || Homoglyphs.Compare(word + 's', w))) + { + forbiddenWord = word; + return true; + } + } + return false; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 06098cb53..e8cc051b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -44,7 +44,7 @@ namespace Barotrauma { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { - DebugConsole.ThrowError("Clients are not allowed to use AutoItemPlacer.\n" + Environment.StackTrace); + DebugConsole.ThrowError("Clients are not allowed to use AutoItemPlacer.\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -135,7 +135,7 @@ namespace Barotrauma { if (itemPrefab == null) { - string errorMsg = "Error in AutoItemPlacer.SpawnItems - itemPrefab was null.\n"+Environment.StackTrace; + string errorMsg = "Error in AutoItemPlacer.SpawnItems - itemPrefab was null.\n"+Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("AutoItemPlacer.SpawnItems:ItemNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 717ca3768..d4c4a0e27 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -14,6 +14,8 @@ namespace Barotrauma private float conversationTimer, conversationLineTimer; private readonly List> pendingConversationLines = new List>(); + public const int MaxCrewSize = 16; + private readonly List characterInfos = new List(); private readonly List characters = new List(); @@ -40,7 +42,7 @@ namespace Barotrauma { if (order.TargetEntity == null) { - DebugConsole.ThrowError("Attempted to add an order with no target entity to CrewManager!\n" + Environment.StackTrace); + DebugConsole.ThrowError("Attempted to add an order with no target entity to CrewManager!\n" + Environment.StackTrace.CleanupStackTrace()); return false; } @@ -95,12 +97,12 @@ namespace Barotrauma { if (character.Removed) { - DebugConsole.ThrowError("Tried to add a removed character to CrewManager!\n" + Environment.StackTrace); + DebugConsole.ThrowError("Tried to add a removed character to CrewManager!\n" + Environment.StackTrace.CleanupStackTrace()); return; } if (character.IsDead) { - DebugConsole.ThrowError("Tried to add a dead character to CrewManager!\n" + Environment.StackTrace); + DebugConsole.ThrowError("Tried to add a dead character to CrewManager!\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -122,7 +124,7 @@ namespace Barotrauma { if (characterInfos.Contains(characterInfo)) { - DebugConsole.ThrowError("Tried to add the same character info to CrewManager twice.\n" + Environment.StackTrace); + DebugConsole.ThrowError("Tried to add the same character info to CrewManager twice.\n" + Environment.StackTrace.CleanupStackTrace()); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index 4864d1b08..08735675c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -31,6 +31,7 @@ namespace Barotrauma get => Math.Min(MaxReputation, Metadata.GetFloat(metaDataIdentifier, InitialReputation)); set { + if (MathUtils.NearlyEqual(Value, value)) { return; } Metadata.SetValue(metaDataIdentifier, Math.Clamp(value, MinReputation, MaxReputation)); OnReputationValueChanged?.Invoke(); OnAnyReputationValueChanged?.Invoke(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 34568d0a3..11cb7f46f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -30,6 +30,8 @@ namespace Barotrauma public CampaignMetadata CampaignMetadata; + protected XElement petsElement; + public enum TransitionType { None, @@ -188,7 +190,7 @@ namespace Barotrauma if (CoroutineManager.IsCoroutineRunning("LevelTransition")) { - DebugConsole.ThrowError("Level transition already running.\n" + Environment.StackTrace); + DebugConsole.ThrowError("Level transition already running.\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -208,7 +210,7 @@ namespace Barotrauma "leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " + "at start: " + (leavingSub?.AtStartPosition.ToString() ?? "null") + ", " + "at end: " + (leavingSub?.AtEndPosition.ToString() ?? "null") + ")\n" + - Environment.StackTrace); + Environment.StackTrace.CleanupStackTrace()); return; } if (nextLevel == null) @@ -220,7 +222,7 @@ namespace Barotrauma "leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " + "at start: " + (leavingSub?.AtStartPosition.ToString() ?? "null") + ", " + "at end: " + (leavingSub?.AtEndPosition.ToString() ?? "null") + ")\n" + - Environment.StackTrace); + Environment.StackTrace.CleanupStackTrace()); return; } #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index b6f891ac0..37039ee37 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -150,6 +150,9 @@ namespace Barotrauma case "cargo": CargoManager?.LoadPurchasedItems(subElement); break; + case "pets": + petsElement = subElement; + break; #if SERVER case "availablesubs": foreach (XElement availableSub in subElement.Elements()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index bdac4f61e..61b0d7c6f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -282,6 +282,11 @@ namespace Barotrauma DebugConsole.ThrowError("Couldn't start game session, submarine file corrupted."); return; } + if (SubmarineInfo.SubmarineElement.Elements().Count() == 0) + { + DebugConsole.ThrowError("Couldn't start game session, saved submarine is empty. The submarine file may be corrupted."); + return; + } LevelData = levelData; @@ -348,6 +353,8 @@ namespace Barotrauma GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Location"), StartLocation.Name), Color.CadetBlue, playSound: false); } } + + GUI.PreventPauseMenuToggle = false; #endif } @@ -542,6 +549,12 @@ namespace Barotrauma Mission == null ? "None" : Mission.GetType().ToString()); #if CLIENT + if (GUI.PauseMenuOpen) + { + GUI.TogglePauseMenu(); + } + GUI.PreventPauseMenuToggle = true; + if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null) { GUI.ClearMessages(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index 49611be15..75f401b7a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -262,7 +262,7 @@ namespace Barotrauma lastErrorSpeak = DateTime.Now; } #if CLIENT - GUI.PlayUISound(GUISoundType.PickItemFail); + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs index 6ce10a7f2..1f653b3c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs @@ -730,6 +730,7 @@ namespace Barotrauma public static bool EnableSubmarineAutoSave { get; set; } public static int MaximumAutoSaves { get; set; } public static Color SubEditorBackgroundColor { get; set; } + public static int SubEditorMaxUndoBuffer { get; set; } public bool ShowTutorialSkipWarning { @@ -898,6 +899,7 @@ namespace Barotrauma new XAttribute("submarineautosave", EnableSubmarineAutoSave), new XAttribute("maxautosaves", MaximumAutoSaves), new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)), + new XAttribute("subeditorundobuffer", SubEditorMaxUndoBuffer), new XAttribute("enablesplashscreen", EnableSplashScreen), new XAttribute("usesteammatchmaking", UseSteamMatchmaking), new XAttribute("quickstartsub", QuickStartSubmarineName), @@ -1033,7 +1035,7 @@ namespace Barotrauma { DebugConsole.ThrowError("Saving game settings failed.", e); GameAnalyticsManager.AddErrorEventOnce("GameSettings.Save:SaveFailed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace); + "Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); } } @@ -1119,6 +1121,7 @@ namespace Barotrauma new XAttribute("verboselogging", VerboseLogging), new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs), new XAttribute("submarineautosave", EnableSubmarineAutoSave), + new XAttribute("subeditorundobuffer", SubEditorMaxUndoBuffer), new XAttribute("maxautosaves", MaximumAutoSaves), new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)), new XAttribute("enablesplashscreen", EnableSplashScreen), @@ -1232,17 +1235,6 @@ namespace Barotrauma 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"); doc.Root.Add(keyMappingElement); @@ -1337,7 +1329,7 @@ namespace Barotrauma { DebugConsole.ThrowError("Saving game settings failed.", e); GameAnalyticsManager.AddErrorEventOnce("GameSettings.Save:SaveFailed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace); + "Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); } } #endregion @@ -1355,6 +1347,7 @@ namespace Barotrauma 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)); + SubEditorMaxUndoBuffer = doc.Root.GetAttributeInt("subeditorundobuffer", 32); UseSteamMatchmaking = doc.Root.GetAttributeBool("usesteammatchmaking", UseSteamMatchmaking); RequireSteamAuthentication = doc.Root.GetAttributeBool("requiresteamauthentication", RequireSteamAuthentication); EnableSplashScreen = doc.Root.GetAttributeBool("enablesplashscreen", EnableSplashScreen); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index ed58a87c6..8bd1a568c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -1,6 +1,4 @@ using Barotrauma.Items.Components; -using Barotrauma.Networking; -using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -11,7 +9,7 @@ namespace Barotrauma [Flags] public enum InvSlotType { - None = 0, Any = 1, RightHand = 2, LeftHand = 4, Head = 8, InnerClothes = 16, OuterClothes = 32, Headset = 64, Card = 128 + None = 0, Any = 1, RightHand = 2, LeftHand = 4, Head = 8, InnerClothes = 16, OuterClothes = 32, Headset = 64, Card = 128, Bag = 256 }; partial class CharacterInventory : Inventory @@ -24,6 +22,9 @@ namespace Barotrauma private set; } + + public static readonly List anySlot = new List() { InvSlotType.Any }; + protected bool[] IsEquipped; public bool AccessibleWhenAlive @@ -68,7 +69,7 @@ namespace Barotrauma #if CLIENT //clients don't create items until the server says so - if (GameMain.Client != null) return; + if (GameMain.Client != null) { return; } #endif foreach (XElement subElement in element.Elements()) @@ -76,8 +77,7 @@ namespace Barotrauma if (!subElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; } string itemIdentifier = subElement.GetAttributeString("identifier", ""); - ItemPrefab itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; - if (itemPrefab == null) + if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab)) { DebugConsole.ThrowError("Error in character inventory \"" + character.SpeciesName + "\" - item \"" + itemIdentifier + "\" not found."); continue; @@ -164,7 +164,7 @@ namespace Barotrauma #if DEBUG throw new Exception("Tried to put a removed item (" + item.Name + ") in an inventory"); #else - DebugConsole.ThrowError("Tried to put a removed item (" + item.Name + ") in an inventory.\n" + Environment.StackTrace); + DebugConsole.ThrowError("Tried to put a removed item (" + item.Name + ") in an inventory.\n" + Environment.StackTrace.CleanupStackTrace()); return false; #endif } @@ -284,7 +284,7 @@ namespace Barotrauma { if (index < 0 || index >= Items.Length) { - string errorMsg = "CharacterInventory.TryPutItem failed: index was out of range(" + index + ").\n" + Environment.StackTrace; + string errorMsg = "CharacterInventory.TryPutItem failed: index was out of range(" + index + ").\n" + Environment.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("CharacterInventory.TryPutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index afe84d033..ee9984e03 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -431,7 +431,7 @@ namespace Barotrauma.Items.Components string errorMsg = "Attempted to create a door body at an invalid position (item pos: " + item.Position + ", item world pos: " + item.WorldPosition - + ", docking target world pos: " + DockingTarget.Door.Item.WorldPosition + ")\n" + Environment.StackTrace; + + ", docking target world pos: " + DockingTarget.Door.Item.WorldPosition + ")\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index 3b44cb761..ea5ade1d9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -377,6 +377,8 @@ namespace Barotrauma.Items.Components private int flowerVariants; private int leafVariants; private int[] flowerTiles; + private const int serverHealthUpdateDelay = 10; + private int serverHealthUpdateTimer; public float Health { @@ -469,6 +471,20 @@ namespace Barotrauma.Items.Components { Health -= FloodTolerance; } +#if SERVER + if (FullyGrown) + { + if (serverHealthUpdateTimer > serverHealthUpdateDelay) + { + item.CreateServerEvent(this); + serverHealthUpdateTimer = 0; + } + else + { + serverHealthUpdateTimer++; + } + } +#endif } CheckPlantState(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 0f6427eef..c6c2f54af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -195,7 +195,9 @@ namespace Barotrauma.Items.Components } } } - } + } + + characterUsable = element.GetAttributeBool("characterusable", true); } private bool OnPusherCollision(Fixture sender, Fixture other, Contact contact) @@ -312,7 +314,7 @@ namespace Barotrauma.Items.Components if (item.Removed) { - DebugConsole.ThrowError($"Attempted to equip a removed item ({item.Name})\n" + Environment.StackTrace); + DebugConsole.ThrowError($"Attempted to equip a removed item ({item.Name})\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -416,7 +418,7 @@ namespace Barotrauma.Items.Components { if (item.Removed) { - DebugConsole.ThrowError($"Attempted to pick up a removed item ({item.Name})\n" + Environment.StackTrace); + DebugConsole.ThrowError($"Attempted to pick up a removed item ({item.Name})\n" + Environment.StackTrace.CleanupStackTrace()); return false; } @@ -514,9 +516,10 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character character = null) { - if (!attachable || item.body == null) { return character == null || character.IsKeyDown(InputType.Aim); } + if (!attachable || item.body == null) { return character == null || (character.IsKeyDown(InputType.Aim) && characterUsable); } if (character != null) { + if (!characterUsable && !attachable) { return false; } if (!character.IsKeyDown(InputType.Aim)) { return false; } if (!CanBeAttached(character)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 6e4bc0134..31e733992 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -104,14 +104,14 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker); #if CLIENT - if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) GUI.PlayUISound(GUISoundType.PickItem); + if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) SoundPlayer.PlayUISound(GUISoundType.PickItem); PlaySound(ActionType.OnPicked, picker); #endif return true; } #if CLIENT - if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) GUI.PlayUISound(GUISoundType.PickItemFail); + if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) SoundPlayer.PlayUISound(GUISoundType.PickItemFail); #endif return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 577db9829..f218cfcee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -248,6 +248,7 @@ namespace Barotrauma.Items.Components partial void UseProjSpecific(float deltaTime, Vector2 raystart); + private static readonly List hitBodies = new List(); private readonly HashSet hitCharacters = new HashSet(); private readonly List fireSourcesInRange = new List(); private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List ignoredBodies) @@ -291,10 +292,14 @@ namespace Barotrauma.Items.Components return true; }, allowInsideFixture: true); + + hitBodies.Clear(); + hitBodies.AddRange(bodies); + lastPickedFraction = Submarine.LastPickedFraction; Type lastHitType = null; hitCharacters.Clear(); - foreach (Body body in bodies) + foreach (Body body in hitBodies) { Type bodyType = body.UserData?.GetType(); if (!RepairThroughWalls && bodyType != null && bodyType != lastHitType) @@ -372,13 +377,23 @@ namespace Barotrauma.Items.Components fireSourcesInRange.Add(fs); } } + foreach (FireSource fs in hull.FakeFireSources) + { + if (fs.IsInDamageRange(displayPos, 100.0f) && !fireSourcesInRange.Contains(fs)) + { + fireSourcesInRange.Add(fs); + } + } } foreach (FireSource fs in fireSourcesInRange) { fs.Extinguish(deltaTime, ExtinguishAmount); #if SERVER - GameMain.Server.KarmaManager.OnExtinguishingFire(user, deltaTime); + if (!(fs is DummyFireSource)) + { + GameMain.Server.KarmaManager.OnExtinguishingFire(user, deltaTime); + } #endif } } @@ -562,13 +577,22 @@ namespace Barotrauma.Items.Components partial void FixItemProjSpecific(Character user, float deltaTime, Item targetItem); private float sinTime; + private float repairTimer; + private Gap previousGap; + private readonly float repairTimeOut = 5; public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (!(objective.OperateTarget is Gap leak)) { return true; } if (leak.Submarine == null) { return true; } + if (leak != previousGap) + { + sinTime = 0; + repairTimer = 0; + previousGap = leak; + } Vector2 fromCharacterToLeak = leak.WorldPosition - character.WorldPosition; float dist = fromCharacterToLeak.Length(); - float reach = Range + ConvertUnits.ToDisplayUnits(((HumanoidAnimController)character.AnimController).ArmLength); + float reach = AIObjectiveFixLeak.CalculateReach(this, character); //too far away -> consider this done and hope the AI is smart enough to move closer if (dist > reach * 2) { return true; } @@ -626,7 +650,11 @@ namespace Barotrauma.Items.Components else if (dist < reach * 2) { // In or almost in range - character.CursorPosition = leak.Position; + character.CursorPosition = leak.WorldPosition; + if (character.Submarine != null) + { + character.CursorPosition -= character.Submarine.Position; + } character.CursorPosition += VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2); if (character.AnimController.InWater) { @@ -656,6 +684,12 @@ namespace Barotrauma.Items.Components var angle = VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak); if (angle < MathHelper.PiOver4) { + if (Submarine.PickBody(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionWall, allowInsideFixture: true)?.UserData is Item i) + { + var door = i.GetComponent(); + // Hit a door, abandon so that we don't weld it shut. + return door != null && !door.IsOpen && !door.IsBroken; + } // Check that we don't hit any friendlies if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit => { @@ -669,17 +703,24 @@ namespace Barotrauma.Items.Components { character.SetInput(InputType.Shoot, false, true); Use(deltaTime, character); + repairTimer += deltaTime; + if (repairTimer > repairTimeOut) + { +#if DEBUG + DebugConsole.NewMessage($"{character.Name}: timed out while welding a leak in {leak.FlowTargetHull.DisplayName}.", color: Color.Yellow); +#endif + return true; + } } } bool leakFixed = (leak.Open <= 0.0f || leak.Removed) && (leak.ConnectedWall == null || leak.ConnectedWall.Sections.Average(s => s.damage) < 1); - if (leakFixed && leak.FlowTargetHull != null) + if (leakFixed && leak.FlowTargetHull?.DisplayName != null) { if (!leak.FlowTargetHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f)) - { - + { character.Speak(TextManager.GetWithVariable("DialogLeaksFixed", "[roomname]", leak.FlowTargetHull.DisplayName, true), null, 0.0f, "leaksfixed", 10.0f); } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 62d7c6de6..ac2f0ac8b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -6,18 +6,20 @@ namespace Barotrauma.Items.Components { class Throwable : Holdable { - private float throwForce, throwPos; + private float throwPos; private bool throwing, throwDone; private bool midAir; - [Serialize(1.0f, false, description: "The impulse applied to the physics body of the item when thrown. Higher values make the item be thrown faster.")] - public float ThrowForce + public Character CurrentThrower { - get { return throwForce; } - set { throwForce = value; } + get; + private set; } + [Serialize(1.0f, false, description: "The impulse applied to the physics body of the item when thrown. Higher values make the item be thrown faster.")] + public float ThrowForce { get; set; } + public Throwable(Item item, XElement element) : base(item, element) { @@ -60,6 +62,21 @@ namespace Barotrauma.Items.Components { if (item.body.LinearVelocity.LengthSquared() < 0.01f) { + CurrentThrower = null; + if (statusEffectLists?.ContainsKey(ActionType.OnImpact) ?? false) + { + foreach (var statusEffect in statusEffectLists[ActionType.OnImpact]) + { + statusEffect.SetUser(null); + } + } + if (statusEffectLists?.ContainsKey(ActionType.OnBroken) ?? false) + { + foreach (var statusEffect in statusEffectLists[ActionType.OnBroken]) + { + statusEffect.SetUser(null); + } + } item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform; midAir = false; } @@ -117,9 +134,24 @@ namespace Barotrauma.Items.Components #if SERVER GameServer.Log(GameServer.CharacterLogName(picker) + " threw " + item.Name, ServerLog.MessageType.ItemInteraction); #endif - Character thrower = picker; - item.Drop(thrower, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer); - item.body.ApplyLinearImpulse(throwVector * throwForce * item.body.Mass * 3.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + CurrentThrower = picker; + if (statusEffectLists?.ContainsKey(ActionType.OnImpact) ?? false) + { + foreach (var statusEffect in statusEffectLists[ActionType.OnImpact]) + { + statusEffect.SetUser(CurrentThrower); + } + } + if (statusEffectLists?.ContainsKey(ActionType.OnBroken) ?? false) + { + foreach (var statusEffect in statusEffectLists[ActionType.OnBroken]) + { + statusEffect.SetUser(CurrentThrower); + } + } + + item.Drop(CurrentThrower, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer); + item.body.ApplyLinearImpulse(throwVector * ThrowForce * item.body.Mass * 3.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); //disable platform collisions until the item comes back to rest again item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; @@ -135,12 +167,12 @@ namespace Barotrauma.Items.Components if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnSecondaryUse, this, thrower.ID }); + GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnSecondaryUse, this, CurrentThrower.ID }); } if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { //Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse" - ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, thrower, user: thrower); + ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, user: CurrentThrower); } throwing = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 4cb928a61..cddb7d1f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -146,7 +146,7 @@ namespace Barotrauma.Items.Components public bool DrawHudWhenEquipped { get; - private set; + protected set; } [Serialize(false, false, description: "Can the item be selected by interacting with it.")] @@ -604,7 +604,7 @@ namespace Barotrauma.Items.Components if (character == null) { - string errorMsg = "ItemComponent.DegreeOfSuccess failed (character was null).\n" + Environment.StackTrace; + string errorMsg = "ItemComponent.DegreeOfSuccess failed (character was null).\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("ItemComponent.DegreeOfSuccess:CharacterNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return 0.0f; @@ -846,7 +846,7 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError("Error while loading entity of the type " + t + ".", e.InnerException); GameAnalyticsManager.AddErrorEventOnce("ItemComponent.Load:TargetInvocationException" + item.Name + element.Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Error while loading entity of the type " + t + " (" + e.InnerException + ")\n" + Environment.StackTrace); + "Error while loading entity of the type " + t + " (" + e.InnerException + ")\n" + Environment.StackTrace.CleanupStackTrace()); } return ic; @@ -1014,8 +1014,8 @@ namespace Barotrauma.Items.Components var container = i.GetComponent(); if (container == null) { return 0; } if (container.Inventory.IsFull()) { return 0; } - // Ignore containers that are identical to the source container - if (sourceC != null && container.Item.Prefab == sourceC.Item.Prefab) { return 0; } + // Ignore containers that are identical to the source container + if (sourceC != null && container.Item.Prefab == sourceC.Item.Prefab) { return 0; } if (container.ShouldBeContained(containedItem, out bool isRestrictionsDefined)) { if (isRestrictionsDefined) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 0a56c0efb..3ffb755a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -328,10 +328,10 @@ namespace Barotrauma.Items.Components } catch (Exception e) { - DebugConsole.Log("SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace); + DebugConsole.Log("SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace()); GameAnalyticsManager.AddErrorEventOnce("ItemContainer.SetContainedItemPositions.InvalidPosition:" + contained.Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace); + "SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace()); } contained.body.Submarine = item.Submarine; } @@ -349,6 +349,14 @@ namespace Barotrauma.Items.Components } } + public override void OnItemLoaded() + { + if (item.Submarine == null || !item.Submarine.Loading) + { + SpawnAlwaysContainedItems(); + } + } + public override void OnMapLoaded() { if (itemIds != null) @@ -361,7 +369,11 @@ namespace Barotrauma.Items.Components } itemIds = null; } + SpawnAlwaysContainedItems(); + } + private void SpawnAlwaysContainedItems() + { if (SpawnWithId.Length > 0) { ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId); @@ -375,6 +387,7 @@ namespace Barotrauma.Items.Components } } + protected override void ShallowRemoveComponentSpecific() { } @@ -388,9 +401,9 @@ namespace Barotrauma.Items.Components inventoryBottomSprite?.Remove(); ContainedStateIndicator?.Remove(); - if (Screen.Selected == GameMain.SubEditorScreen && !Submarine.Unloading) + if (SubEditorScreen.IsSubEditor()) { - GameMain.SubEditorScreen.HandleContainerContentsDeletion(Item, Inventory); + Inventory.DeleteAllItems(); return; } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index 7164c8061..9db46cfbe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -34,7 +34,7 @@ namespace Barotrauma.Items.Components set { maxFlow = value; } } - [Editable, Serialize(false, false, alwaysUseInstanceValues: true)] + [Editable, Serialize(true, false, alwaysUseInstanceValues: true)] public bool IsOn { get { return IsActive; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/NameTag.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/NameTag.cs new file mode 100644 index 000000000..6cd9e1d39 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/NameTag.cs @@ -0,0 +1,21 @@ +using Barotrauma.Networking; +using System.Xml.Linq; +#if CLIENT +using Microsoft.Xna.Framework.Graphics; +#endif + +namespace Barotrauma.Items.Components +{ + class NameTag : ItemComponent + { + [InGameEditable, Serialize("", false, description: "Name written on the tag.", alwaysUseInstanceValues: true)] + public string WrittenName { get; set; } + + public NameTag(Item item, XElement element) : base(item, element) + { + AllowInGameEditing = true; + DrawHudWhenEquipped = true; + item.EditableWhenEquipped = true; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs index 14bd26e93..d9d66f667 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs @@ -201,6 +201,7 @@ namespace Barotrauma.Items.Components if (seed.Decayed || seed.FullyGrown) { container?.Inventory.RemoveItem(seed.Item); + Entity.Spawner?.AddToRemoveQueue(seed.Item); GrowableSeeds[i] = null; return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index b73217ed2..246217863 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -214,14 +214,15 @@ namespace Barotrauma.Items.Components } if (HasBeenTuned) { return true; } - if (string.IsNullOrEmpty(objective.Option) || objective.Option.Equals("charge", StringComparison.OrdinalIgnoreCase)) + float targetRatio = string.IsNullOrEmpty(objective.Option) || objective.Option.Equals("charge", StringComparison.OrdinalIgnoreCase) ? aiRechargeTargetRatio : -1; + if (targetRatio > 0 || float.TryParse(objective.Option, out targetRatio)) { - if (Math.Abs(rechargeSpeed - maxRechargeSpeed * aiRechargeTargetRatio) > 0.05f) + if (Math.Abs(rechargeSpeed - maxRechargeSpeed * targetRatio) > 0.05f) { #if SERVER item.CreateServerEvent(this); #endif - RechargeSpeed = maxRechargeSpeed * aiRechargeTargetRatio; + RechargeSpeed = maxRechargeSpeed * targetRatio; #if CLIENT if (rechargeSpeedSlider != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index fed67f5c4..88bfb72e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -639,6 +639,7 @@ namespace Barotrauma.Items.Components } target.Body.ApplyLinearImpulse(velocity * item.body.Mass); + target.Body.LinearVelocity = target.Body.LinearVelocity.ClampLength(NetConfig.MaxPhysicsBodyVelocity * 0.5f); if (hits.Count() >= MaxTargetsToHit || hits.LastOrDefault()?.UserData is VoronoiCell) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index eb17ab06e..2e748b8ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -271,7 +271,7 @@ namespace Barotrauma.Items.Components if (wires[i] == null) { continue; } Connection recipient = wires[i].OtherConnection(this); - if (recipient == null) { continue; } + if (recipient == null || !recipient.IsPower) { continue; } recipient.item.GetComponent()?.ReceivePowerProbeSignal(recipient, source, power); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs index 3fbf67ac8..8d1c64ced 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Items.Components { partial class MemoryComponent : ItemComponent, IServerSerializable { - const int MaxValueLength = 256; + const int MaxValueLength = ChatMessage.MaxLength; private string value; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs index a541bc98f..2cf176ad0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs @@ -11,6 +11,7 @@ namespace Barotrauma.Items.Components private string previousReceivedSignal; private bool previousResult; + private GroupCollection previousGroups; private Regex regex; @@ -19,6 +20,9 @@ namespace Barotrauma.Items.Components [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the regular expression.", alwaysUseInstanceValues: true)] public string Output { get; set; } + [InGameEditable, Serialize(false, true, description: "Should the component output a value of a capture group instead of a constant signal.", alwaysUseInstanceValues: true)] + public bool UseCaptureGroup { get; set; } + [Serialize("0", true, description: "The signal this item outputs when the received signal does not match the regular expression.", alwaysUseInstanceValues: true)] public string FalseOutput { get; set; } @@ -64,6 +68,7 @@ namespace Barotrauma.Items.Components { Match match = regex.Match(receivedSignal); previousResult = match.Success; + previousGroups = UseCaptureGroup && previousResult ? match.Groups : null; previousReceivedSignal = receivedSignal; } @@ -75,7 +80,30 @@ namespace Barotrauma.Items.Components } } - string signalOut = previousResult ? Output : FalseOutput; + string signalOut; + if (previousResult) + { + if (UseCaptureGroup) + { + if (previousGroups != null && previousGroups.TryGetValue(Output, out Group group)) + { + signalOut = group.Value; + } + else + { + signalOut = FalseOutput; + } + } + else + { + signalOut = Output; + } + } + else + { + signalOut = FalseOutput; + } + if (ContinuousOutput) { if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(0, signalOut, "signal_out", null); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs index 7f5019107..04e273922 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs @@ -37,7 +37,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(false, true, description: "Can the relay currently pass power and signals through it.", alwaysUseInstanceValues: true)] + [Editable, Serialize(true, true, description: "Can the relay currently pass power and signals through it.", alwaysUseInstanceValues: true)] public bool IsOn { get diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs index dad4b40fa..19599da8e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs @@ -1,10 +1,11 @@ -using System.Xml.Linq; +using Barotrauma.Networking; +using System.Xml.Linq; namespace Barotrauma.Items.Components { partial class Terminal : ItemComponent { - private const int MaxMessageLength = 150; + private const int MaxMessageLength = ChatMessage.MaxLength; public string DisplayedWelcomeMessage { @@ -21,7 +22,7 @@ namespace Barotrauma.Items.Components { if (welcomeMessage == value) { return; } welcomeMessage = value; - DisplayedWelcomeMessage = TextManager.Get(welcomeMessage, returnNull: true) ?? welcomeMessage; + DisplayedWelcomeMessage = TextManager.Get(welcomeMessage, returnNull: true) ?? welcomeMessage.Replace("\\n", "\n"); } } @@ -45,7 +46,9 @@ namespace Barotrauma.Items.Components { signal = signal.Substring(0, MaxMessageLength); } - ShowOnDisplay(signal); + + string inputSignal = signal.Replace("\\n", "\n"); + ShowOnDisplay(inputSignal); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index a84973c9c..344df8079 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -63,7 +63,7 @@ namespace Barotrauma.Items.Components public bool Hidden; - private float removeNodeDelay; + private float editNodeDelay; private bool locked; public bool Locked @@ -198,7 +198,21 @@ namespace Barotrauma.Items.Components int newNodeIndex = 0; if (nodes.Count > 1) { - if (Vector2.DistanceSquared(nodes[nodes.Count - 1], nodePos) < Vector2.DistanceSquared(nodes[0], nodePos)) + if (connections[0] != null && connections[0] != newConnection) + { + if (Vector2.DistanceSquared(nodes[0], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < Vector2.DistanceSquared(nodes[nodes.Count - 1], nodePos)) + { + newNodeIndex = nodes.Count; + } + } + else if (connections[1] != null && connections[1] != newConnection) + { + if (Vector2.DistanceSquared(nodes[0], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < Vector2.DistanceSquared(nodes[nodes.Count - 1], nodePos)) + { + newNodeIndex = nodes.Count; + } + } + else if (Vector2.DistanceSquared(nodes[nodes.Count - 1], nodePos) < Vector2.DistanceSquared(nodes[0], nodePos)) { newNodeIndex = nodes.Count; } @@ -276,7 +290,7 @@ namespace Barotrauma.Items.Components if (nodes.Count == 0) { return; } Character user = item.ParentInventory?.Owner as Character; - removeNodeDelay = (user?.SelectedConstruction == null) ? removeNodeDelay - deltaTime : 0.5f; + editNodeDelay = (user?.SelectedConstruction == null) ? editNodeDelay - deltaTime : 0.5f; Submarine sub = item.Submarine; if (connections[0] != null && connections[0].Item.Submarine != null) { sub = connections[0].Item.Submarine; } @@ -402,7 +416,9 @@ namespace Barotrauma.Items.Components #endif //clients communicate node addition/removal with network events if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { return false; } - if (newNodePos != Vector2.Zero && canPlaceNode && nodes.Count > 0 && Vector2.Distance(newNodePos, nodes[nodes.Count - 1]) > MinNodeDistance) + + if (newNodePos != Vector2.Zero && canPlaceNode && editNodeDelay <= 0.0f && nodes.Count > 0 && + Vector2.DistanceSquared(newNodePos, nodes[nodes.Count - 1]) > MinNodeDistance * MinNodeDistance) { if (nodes.Count >= MaxNodeCount) { @@ -426,6 +442,7 @@ namespace Barotrauma.Items.Components } #endif } + editNodeDelay = 0.1f; return true; } @@ -436,7 +453,7 @@ namespace Barotrauma.Items.Components //clients communicate node addition/removal with network events if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { return false; } - if (nodes.Count > 1 && removeNodeDelay <= 0.0f) + if (nodes.Count > 1 && editNodeDelay <= 0.0f) { nodes.RemoveAt(nodes.Count - 1); UpdateSections(); @@ -452,7 +469,7 @@ namespace Barotrauma.Items.Components } #endif } - removeNodeDelay = 0.1f; + editNodeDelay = 0.1f; Drawable = IsActive || sections.Count > 0; return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index e56591c94..2f91a277f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -384,7 +384,7 @@ namespace Barotrauma.Items.Components if (!flashLowPower && character != null && character == Character.Controlled) { flashLowPower = true; - GUI.PlayUISound(GUISoundType.PickItemFail); + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); } #endif return false; @@ -422,7 +422,7 @@ namespace Barotrauma.Items.Components { flashNoAmmo = true; failedLaunchAttempts = 0; - GUI.PlayUISound(GUISoundType.PickItemFail); + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); } #endif return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 20265e1e7..6963fd045 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -151,7 +151,7 @@ namespace Barotrauma { if (i < 0 || i >= Items.Length) { - string errorMsg = "Inventory.TryPutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace; + string errorMsg = "Inventory.TryPutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("Inventory.TryPutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return false; } @@ -192,7 +192,7 @@ namespace Barotrauma { if (i < 0 || i >= Items.Length) { - string errorMsg = "Inventory.PutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace; + string errorMsg = "Inventory.PutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("Inventory.PutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; } @@ -304,13 +304,16 @@ namespace Barotrauma for (int j = 0; j < otherInventory.capacity; j++) { - if (otherInventory.Items[j] == item) otherInventory.Items[j] = null; + if (otherInventory.Items[j] == item) { otherInventory.Items[j] = null; } } for (int j = 0; j < capacity; j++) { - if (Items[j] == existingItem) Items[j] = null; + if (Items[j] == existingItem) { Items[j] = null; } } + (otherInventory.Owner as Character)?.DeselectItem(item); + (otherInventory.Owner as Character)?.DeselectItem(existingItem); + bool swapSuccessful = false; if (otherIsEquipped) { @@ -486,6 +489,22 @@ namespace Barotrauma } Items[i].Remove(); } - } + } + + public List GetAllItems() + { + List deletedItems = new List(); + for (int i = 0; i < capacity; i++) + { + if (Items[i] == null) continue; + foreach (ItemContainer itemContainer in Items[i].GetComponents()) + { + deletedItems.AddRange(itemContainer.Inventory.GetAllItems()); + } + deletedItems.Add(Items[i]); + } + + return deletedItems; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 9d89db215..9459bc8cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -119,6 +119,8 @@ namespace Barotrauma } } + public bool EditableWhenEquipped { get; set; } = false; + //the inventory in which the item is contained in public Inventory ParentInventory { @@ -1016,7 +1018,7 @@ namespace Barotrauma { string errorMsg = "Attempted to move the item " + Name + - " to an invalid position (" + simPosition + ")\n" + Environment.StackTrace; + " to an invalid position (" + simPosition + ")\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( @@ -1073,7 +1075,7 @@ namespace Barotrauma { if (!MathUtils.IsValid(amount)) { - DebugConsole.ThrowError($"Attempted to move an item by an invalid amount ({amount})\n{Environment.StackTrace}"); + DebugConsole.ThrowError($"Attempted to move an item by an invalid amount ({amount})\n{Environment.StackTrace.CleanupStackTrace()}"); return; } @@ -1360,12 +1362,15 @@ namespace Barotrauma public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true) { - if (Indestructible) return new AttackResult(); + if (Indestructible) { return new AttackResult(); } float damageAmount = attack.GetItemDamage(deltaTime); Condition -= damageAmount; - ApplyStatusEffects(ActionType.OnDamaged, 1.0f); + if (damageAmount > 0) + { + ApplyStatusEffects(ActionType.OnDamaged, 1.0f); + } return new AttackResult(damageAmount, null); } @@ -1629,7 +1634,7 @@ namespace Barotrauma OnCollisionProjSpecific(impact); if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - if (ImpactTolerance > 0.0f && condition > 0.0f && impact > ImpactTolerance) + if (ImpactTolerance > 0.0f && condition > 0.0f && Math.Abs(impact) > ImpactTolerance) { ApplyStatusEffects(ActionType.OnImpact, 1.0f); #if SERVER @@ -1653,10 +1658,14 @@ namespace Barotrauma public override void FlipX(bool relativeToSub) { - if (!Prefab.CanFlipX) { return; } - + //call the base method even if the item can't flip, to handle repositioning when flipping the whole sub base.FlipX(relativeToSub); + if (!Prefab.CanFlipX) + { + flippedX = false; + return; + } #if CLIENT if (Prefab.CanSpriteFlipX) { @@ -1672,10 +1681,15 @@ namespace Barotrauma public override void FlipY(bool relativeToSub) { - if (!Prefab.CanFlipY) { return; } - + //call the base method even if the item can't flip, to handle repositioning when flipping the whole sub base.FlipY(relativeToSub); + if (!Prefab.CanFlipY) + { + flippedY = false; + return; + } + #if CLIENT if (Prefab.CanSpriteFlipY) { @@ -1980,17 +1994,14 @@ namespace Barotrauma { if (picker.SelectedConstruction == this) { - if (picker.IsKeyHit(InputType.Select) || forceSelectKey) picker.SelectedConstruction = null; + if (picker.IsKeyHit(InputType.Select) || forceSelectKey) + { + picker.SelectedConstruction = null; + } } else if (selected) { picker.SelectedConstruction = this; -#if CLIENT - if (GameMain.GameSession?.CrewManager != null && picker == Character.Controlled && GetComponent() == null) - { - GameMain.GameSession.CrewManager.ToggleCrewListOpen = false; - } -#endif } } @@ -2215,7 +2226,7 @@ namespace Barotrauma { if (Removed) { - DebugConsole.ThrowError($"Tried to equip a removed item ({Name}).\n{Environment.StackTrace}"); + DebugConsole.ThrowError($"Tried to equip a removed item ({Name}).\n{Environment.StackTrace.CleanupStackTrace()}"); return; } @@ -2257,7 +2268,7 @@ namespace Barotrauma var propertyOwner = allProperties.Find(p => p.Second == property); if (allProperties.Count > 1) { - msg.WriteRangedInteger(allProperties.FindIndex(p => p.Second == property), 0, allProperties.Count - 1); + msg.Write((byte)allProperties.FindIndex(p => p.Second == property)); } object value = property.GetValue(propertyOwner.First); @@ -2339,7 +2350,7 @@ namespace Barotrauma int propertyIndex = 0; if (allProperties.Count > 1) { - propertyIndex = msg.ReadRangedInteger(0, allProperties.Count - 1); + propertyIndex = msg.ReadByte(); } bool allowEditing = true; @@ -2360,6 +2371,7 @@ namespace Barotrauma if (type == typeof(string)) { string val = msg.ReadString(); + logValue = val; if (allowEditing) { property.TrySetValue(parentObject, val); @@ -2712,7 +2724,7 @@ namespace Barotrauma { if (Removed) { - DebugConsole.ThrowError("Attempting to remove an already removed item (" + Name + ")\n" + Environment.StackTrace); + DebugConsole.ThrowError("Attempting to remove an already removed item (" + Name + ")\n" + Environment.StackTrace.CleanupStackTrace()); return; } DebugConsole.Log("Removing item " + Name + " (ID: " + ID + ")"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs index 793062786..35a35d89d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs @@ -93,7 +93,7 @@ namespace Barotrauma { if (!Item.ItemList.Contains(container.Item)) { - string errorMsg = "Attempted to create a network event for an item (" + container.Item.Name + ") that hasn't been fully initialized yet.\n" + Environment.StackTrace; + string errorMsg = "Attempted to create a network event for an item (" + container.Item.Name + ") that hasn't been fully initialized yet.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "ItemInventory.CreateServerEvent:EventForUninitializedItem" + container.Item.Name + container.Item.ID, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 3d8982e08..0bbe1b03f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -319,6 +319,13 @@ namespace Barotrauma private set; } + [Serialize(false, false)] + public bool AllowSellingWhenBroken + { + get; + private set; + } + [Serialize(false, false)] public bool Indestructible { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/DummyFireSource.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/DummyFireSource.cs index fe8d445ec..a50646860 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/DummyFireSource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/DummyFireSource.cs @@ -7,14 +7,13 @@ namespace Barotrauma { private Vector2 maxSize; - public bool Removed - { - get { return removed; } - } + public bool CausedByPsychosis; public DummyFireSource(Vector2 maxSize, Vector2 worldPosition, Hull spawningHull = null, bool isNetworkMessage = false) : base(worldPosition, spawningHull, isNetworkMessage) { this.maxSize = maxSize; + DamagesItems = false; + DamagesCharacters = true; } public override float DamageRange @@ -24,11 +23,7 @@ namespace Barotrauma protected override void LimitSize() { - if (hull == null) return; - - position.X = Math.Max(hull.Rect.X, position.X); - position.Y = Math.Min(hull.Rect.Y, position.Y); - + base.LimitSize(); size.X = Math.Min(maxSize.X, size.X); size.Y = Math.Min(maxSize.Y, size.Y); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index d03cc4ede..0ab0abaf8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -51,13 +51,13 @@ namespace Barotrauma if (value == NullEntityID) { DebugConsole.ThrowError("Cannot set the ID of an entity to " + NullEntityID + - "! The value is reserved for entity events referring to a non-existent (e.g. removed) entity.\n" + Environment.StackTrace); + "! The value is reserved for entity events referring to a non-existent (e.g. removed) entity.\n" + Environment.StackTrace.CleanupStackTrace()); return; } if (value == EntitySpawnerID) { DebugConsole.ThrowError("Cannot set the ID of an entity to " + EntitySpawnerID + - "! The value is reserved for EntitySpawner.\n" + Environment.StackTrace); + "! The value is reserved for EntitySpawner.\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -183,7 +183,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce( "Entity.RemoveAll:Exception" + e.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Error while removing entity \"" + e.ToString() + " (" + exception.Message + ")\n" + exception.StackTrace); + "Error while removing entity \"" + e.ToString() + " (" + exception.Message + ")\n" + exception.StackTrace.CleanupStackTrace()); } } StringBuilder errorMsg = new StringBuilder(); @@ -266,7 +266,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce( "Entity.FreeID:EntityNotFound" + ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Entity " + ToString() + " (" + ID + ") not present in entity dictionary.\n" + Environment.StackTrace); + "Entity " + ToString() + " (" + ID + ") not present in entity dictionary.\n" + Environment.StackTrace.CleanupStackTrace()); } else if (existingEntity != this) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 4881e7835..439ccdcdf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -105,7 +105,7 @@ namespace Barotrauma if (hull != null && !string.IsNullOrWhiteSpace(decal) && decalSize > 0.0f) { - hull.AddDecal(decal, worldPosition, decalSize, true); + hull.AddDecal(decal, worldPosition, decalSize, isNetworkEvent: false); } float displayRange = attack.Range; @@ -340,6 +340,15 @@ namespace Barotrauma } } + if (c == Character.Controlled && !c.IsDead) + { + Limb head = c.AnimController.GetLimb(LimbType.Head); + if (damages.TryGetValue(head, out float headDamage) && headDamage > 0.0f && distFactors.TryGetValue(head, out float headFactor)) + { + PlayTinnitusProjSpecific(headFactor); + } + } + //sever joints if (attack.SeverLimbsProbability > 0.0f) { @@ -402,5 +411,7 @@ namespace Barotrauma return damagedStructures; } + + static partial void PlayTinnitusProjSpecific(float volume); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs index 29ff8295a..7da6b79b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs @@ -69,6 +69,23 @@ namespace Barotrauma get { return Math.Min((float)Math.Sqrt(size.X) * 10.0f, MaxDamageRange); } } + public bool DamagesItems + { + get; + set; + } = true; + + public bool DamagesCharacters + { + get; + set; + } = true; + + public bool Removed + { + get { return removed; } + } + public Hull Hull { get { return hull; } @@ -80,7 +97,7 @@ namespace Barotrauma if (hull == null || worldPosition.Y < hull.WorldSurface) return; #if CLIENT - if (!isNetworkMessage && GameMain.Client != null) return; + if (!isNetworkMessage && GameMain.Client != null) { return; } #endif hull.AddFireSource(this); @@ -140,9 +157,43 @@ namespace Barotrauma } } } - + + public static void UpdateAll(List fireSources, float deltaTime) + { + for (int i = fireSources.Count - 1; i >= 0; i--) + { + fireSources[i].Update(deltaTime); + } + + //combine overlapping fires + for (int i = fireSources.Count - 1; i >= 0; i--) + { + for (int j = i - 1; j >= 0; j--) + { + i = Math.Min(i, fireSources.Count - 1); + j = Math.Min(j, i - 1); + + if (!fireSources[i].CheckOverLap(fireSources[j])) { continue; } + + float leftEdge = Math.Min(fireSources[i].position.X, fireSources[j].position.X); + + fireSources[j].size.X = + Math.Max(fireSources[i].position.X + fireSources[i].size.X, fireSources[j].position.X + fireSources[j].size.X) + - leftEdge; + + fireSources[j].position.X = leftEdge; + fireSources[i].Remove(); + } + } + } + private bool CheckOverLap(FireSource fireSource) { + if (this is DummyFireSource != fireSource is DummyFireSource) + { + return false; + } + return !(position.X > fireSource.position.X + fireSource.size.X || position.X + size.X < fireSource.position.X); } @@ -152,8 +203,8 @@ namespace Barotrauma //the firesource will start to shrink if oxygen percentage is below 10 float growModifier = Math.Min((hull.OxygenPercentage / 10.0f) - 1.0f, 1.0f); - DamageCharacters(deltaTime); - DamageItems(deltaTime); + if (DamagesCharacters) { DamageCharacters(deltaTime); } + if (DamagesItems) { DamageItems(deltaTime); } if (hull.WaterVolume > 0.0f) { @@ -161,7 +212,10 @@ namespace Barotrauma if (removed) { return; } } - ReduceOxygen(deltaTime); + if (!(this is DummyFireSource)) + { + ReduceOxygen(deltaTime); + } AdjustXPos(growModifier, deltaTime); @@ -175,21 +229,21 @@ namespace Barotrauma LimitSize(); - if (size.X > 256.0f) + if (size.X > 256.0f && !(this is DummyFireSource)) { if (burnDecals.Count == 0) { - var newDecal = hull.AddDecal("burnt", WorldPosition + size / 2, 1f, true); + var newDecal = hull.AddDecal("burnt", WorldPosition + size / 2, 1f, isNetworkEvent: false); if (newDecal != null) { burnDecals.Add(newDecal); } } else if (WorldPosition.X < burnDecals[0].WorldPosition.X - 256.0f) { - var newDecal = hull.AddDecal("burnt", WorldPosition, 1f, true); + var newDecal = hull.AddDecal("burnt", WorldPosition, 1f, isNetworkEvent: false); 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); + var newDecal = hull.AddDecal("burnt", WorldPosition + Vector2.UnitX * size.X, 1f, isNetworkEvent: false); if (newDecal != null) { burnDecals.Add(newDecal); } } } @@ -281,14 +335,13 @@ namespace Barotrauma private void DamageItems(float deltaTime) { - if (size.X <= 0.0f) return; + if (size.X <= 0.0f) { return; } #if CLIENT - if (GameMain.Client != null) return; + if (GameMain.Client != null) { return; } #endif - foreach (Item item in Item.ItemList) { - if (item.CurrentHull != hull || item.FireProof || item.Condition <= 0.0f) continue; + if (item.CurrentHull != hull || item.FireProof || item.Condition <= 0.0f) { continue; } //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) @@ -300,8 +353,8 @@ namespace Barotrauma } float range = (float)Math.Sqrt(size.X) * 10.0f; - if (item.Position.X < position.X - range || item.Position.X > position.X + size.X + range) continue; - if (item.Position.Y < position.Y - size.Y || item.Position.Y > hull.Rect.Y) continue; + if (item.Position.X < position.X - range || item.Position.X > position.X + size.X + range) { continue; } + if (item.Position.Y < position.Y - size.Y || item.Position.Y > hull.Rect.Y) { continue; } item.ApplyStatusEffects(ActionType.OnFire, deltaTime); if (item.Condition <= 0.0f && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index e53eebcfc..daba696ec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -162,7 +162,7 @@ namespace Barotrauma { if (!MathUtils.IsValid(amount)) { - DebugConsole.ThrowError($"Attempted to move a gap by an invalid amount ({amount})\n{Environment.StackTrace}"); + DebugConsole.ThrowError($"Attempted to move a gap by an invalid amount ({amount})\n{Environment.StackTrace.CleanupStackTrace()}"); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index f277c6c52..2bfd020c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -65,8 +65,8 @@ namespace Barotrauma 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); + + DirtColor = Color.Lerp(new Color(10, 10, 10, 100), new Color(54, 57, 28, 200), Noise.X); } public bool SetColor(Color color) @@ -389,6 +389,8 @@ namespace Barotrauma public List FireSources { get; private set; } + public List FakeFireSources { get; private set; } + public Hull(MapEntityPrefab prefab, Rectangle rectangle) : this (prefab, rectangle, Submarine.MainSub) { @@ -405,6 +407,7 @@ namespace Barotrauma OxygenPercentage = 100.0f; FireSources = new List(); + FakeFireSources = new List(); properties = SerializableProperty.GetProperties(this); @@ -554,7 +557,7 @@ namespace Barotrauma { if (!MathUtils.IsValid(amount)) { - DebugConsole.ThrowError($"Attempted to move a hull by an invalid amount ({amount})\n{Environment.StackTrace}"); + DebugConsole.ThrowError($"Attempted to move a hull by an invalid amount ({amount})\n{Environment.StackTrace.CleanupStackTrace()}"); return; } @@ -583,11 +586,13 @@ namespace Barotrauma } List fireSourcesToRemove = new List(FireSources); + fireSourcesToRemove.AddRange(FakeFireSources); foreach (FireSource fireSource in fireSourcesToRemove) { fireSource.Remove(); } FireSources.Clear(); + FakeFireSources.Clear(); if (EntityGrids != null) { @@ -627,10 +632,17 @@ namespace Barotrauma public void AddFireSource(FireSource fireSource) { - FireSources.Add(fireSource); + if (fireSource is DummyFireSource dummyFire) + { + FakeFireSources.Add(dummyFire); + } + else + { + FireSources.Add(fireSource); + } } - public Decal AddDecal(UInt32 decalId, Vector2 worldPosition, float scale, bool isNetworkEvent) + public Decal AddDecal(UInt32 decalId, Vector2 worldPosition, float scale, bool isNetworkEvent, int? spriteIndex = null) { //clients are only allowed to create decals when the server says so if (!isNetworkEvent && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) @@ -644,11 +656,11 @@ namespace Barotrauma DebugConsole.ThrowError($"Could not find a decal prefab with the UInt identifier {decalId}!"); return null; } - return AddDecal(decal.Name, worldPosition, scale, isNetworkEvent); + return AddDecal(decal.Name, worldPosition, scale, isNetworkEvent, spriteIndex); } - public Decal AddDecal(string decalName, Vector2 worldPosition, float scale, bool isNetworkEvent) + public Decal AddDecal(string decalName, Vector2 worldPosition, float scale, bool isNetworkEvent, int? spriteIndex = null) { //clients are only allowed to create decals when the server says so if (!isNetworkEvent && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) @@ -658,7 +670,7 @@ namespace Barotrauma if (decals.Count >= MaxDecalsPerHull) { return null; } - var decal = GameMain.DecalManager.CreateDecal(decalName, scale, worldPosition, this); + var decal = GameMain.DecalManager.CreateDecal(decalName, scale, worldPosition, this, spriteIndex); if (decal != null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) @@ -679,7 +691,19 @@ namespace Barotrauma Oxygen -= OxygenDeteriorationSpeed * deltaTime; + if ((Character.Controlled?.CharacterHealth?.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f) + { + for (int i = FakeFireSources.Count - 1; i >= 0; i--) + { + if (FakeFireSources[i].CausedByPsychosis) + { + FakeFireSources[i].Remove(); + } + } + } + FireSource.UpdateAll(FireSources, deltaTime); + FireSource.UpdateAll(FakeFireSources, deltaTime); foreach (Decal decal in decals) { @@ -687,7 +711,6 @@ namespace Barotrauma } 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; @@ -842,17 +865,31 @@ namespace Barotrauma } } - public void Extinguish(float deltaTime, float amount, Vector2 position) + public void Extinguish(float deltaTime, float amount, Vector2 position, bool extinguishRealFires = true, bool extinguishFakeFires = true) { - for (int i = FireSources.Count - 1; i >= 0; i--) + if (extinguishRealFires) { - FireSources[i].Extinguish(deltaTime, amount, position); + for (int i = FireSources.Count - 1; i >= 0; i--) + { + FireSources[i].Extinguish(deltaTime, amount, position); + } + } + if (extinguishFakeFires) + { + for (int i = FakeFireSources.Count - 1; i >= 0; i--) + { + FakeFireSources[i].Extinguish(deltaTime, amount, position); + } } } public void RemoveFire(FireSource fire) { FireSources.Remove(fire); + if (fire is DummyFireSource dummyFire) + { + FakeFireSources.Remove(dummyFire); + } } private readonly HashSet adjacentHulls = new HashSet(); @@ -1221,7 +1258,7 @@ namespace Barotrauma 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) + public void IncreaseSectionColorOrStrength(BackgroundSection section, Color? color, float? strength, bool requiresUpdate, bool isCleaning) { bool sectionUpdated = isCleaning; if (color != null) @@ -1263,13 +1300,31 @@ namespace Barotrauma } } + public void SetSectionColorOrStrength(BackgroundSection section, Color? color, float? strength) + { + if (color != null) + { + section.SetColor(color.Value); + } + + 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 + } + } + } + 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); + IncreaseSectionColorOrStrength(sections[i], sections[i].DirtColor, dirtyVal, false, false); } } @@ -1283,11 +1338,17 @@ namespace Barotrauma { decal.Clean(cleanVal); decalsCleaned = true; +#if SERVER + decalUpdatePending = true; +#elif CLIENT + pendingDecalUpdates.Add(decal); + networkUpdatePending = true; +#endif } } if (section.ColorStrength == 0 && !decalsCleaned) { return; } - SetSectionColorOrStrength(section, null, cleanVal, updateRequired, true); + IncreaseSectionColorOrStrength(section, null, cleanVal, updateRequired, true); } #endregion @@ -1341,10 +1402,12 @@ namespace Barotrauma Vector2 pos = subElement.GetAttributeVector2("pos", Vector2.Zero); float scale = subElement.GetAttributeFloat("scale", 1.0f); float timer = subElement.GetAttributeFloat("timer", 1.0f); + float baseAlpha = subElement.GetAttributeFloat("alpha", 1.0f); var decal = hull.AddDecal(id, pos + hull.WorldRect.Location.ToVector2(), scale, true); if (decal != null) { decal.FadeTimer = timer; + decal.BaseAlpha = baseAlpha; } break; } @@ -1362,7 +1425,7 @@ namespace Barotrauma 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); + hull.SetSectionColorOrStrength(hull.BackgroundSections[index], color, strength); } } } @@ -1377,7 +1440,7 @@ namespace Barotrauma { if (Submarine == null) { - string errorMsg = "Error - tried to save a hull that's not a part of any submarine.\n" + Environment.StackTrace; + string errorMsg = "Error - tried to save a hull that's not a part of any submarine.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("Hull.Save:WorldHull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return null; @@ -1420,7 +1483,8 @@ namespace Barotrauma 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)) + new XAttribute("timer", decal.FadeTimer.ToString("G", CultureInfo.InvariantCulture)), + new XAttribute("alpha", decal.BaseAlpha.ToString("G", CultureInfo.InvariantCulture)) )); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index ebf16b2fe..aee63270b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -110,7 +110,13 @@ namespace Barotrauma protected override void CreateInstance(Rectangle rect) { - CreateInstance(rect.Location.ToVector2(), Submarine.MainSub); + var loaded = CreateInstance(rect.Location.ToVector2(), Submarine.MainSub); +#if CLIENT + if (Screen.Selected is SubEditorScreen) + { + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(loaded, false)); + } +#endif } public List CreateInstance(Vector2 position, Submarine sub, bool selectPrefabs = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 2e4e4bc7f..a714f13dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -1398,7 +1398,7 @@ namespace Barotrauma } if (!suitablePositions.Any()) { - string errorMsg = "Could not find a suitable position of interest. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace; + string errorMsg = "Could not find a suitable position of interest. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("Level.TryGetInterestingPosition:PositionTypeNotFound", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); @@ -1418,7 +1418,7 @@ namespace Barotrauma } if (!farEnoughPositions.Any()) { - string errorMsg = "Could not find a position of interest far enough from the submarines. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace; + string errorMsg = "Could not find a position of interest far enough from the submarines. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("Level.TryGetInterestingPosition:TooCloseToSubs", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 98428a969..c25e2a8e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -132,7 +132,15 @@ namespace Barotrauma public override MapEntity Clone() { - return CreateDummy(Submarine, filePath, Position); + + var path = filePath; + if (string.IsNullOrEmpty(path)) + { + var linkedSubmarine = CreateDummy(Submarine, saveElement, Position); + linkedSubmarine.saveElement = saveElement; + return linkedSubmarine; + } + return CreateDummy(Submarine, path, Position); } private void GenerateWallVertices(XElement rootElement) @@ -176,7 +184,7 @@ namespace Barotrauma else { string levelSeed = element.GetAttributeString("location", ""); - LevelData levelData = GameMain.GameSession.Campaign?.NextLevel ?? GameMain.GameSession.Level?.LevelData; + LevelData levelData = GameMain.GameSession.Campaign?.NextLevel ?? GameMain.GameSession.LevelData; linkedSub = new LinkedSubmarine(submarine) { purchasedLostShuttles = GameMain.GameSession.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 5fc834dcb..6541c07df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -424,12 +424,12 @@ namespace Barotrauma { if (!Type.HasHireableCharacters) { - DebugConsole.ThrowError("Cannot hire a character from location \"" + Name + "\" - the location has no hireable characters.\n" + Environment.StackTrace); + DebugConsole.ThrowError("Cannot hire a character from location \"" + Name + "\" - the location has no hireable characters.\n" + Environment.StackTrace.CleanupStackTrace()); return; } if (HireManager == null) { - DebugConsole.ThrowError("Cannot hire a character from location \"" + Name + "\" - hire manager has not been instantiated.\n" + Environment.StackTrace); + DebugConsole.ThrowError("Cannot hire a character from location \"" + Name + "\" - hire manager has not been instantiated.\n" + Environment.StackTrace.CleanupStackTrace()); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 83bbc2993..f5c8ad934 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -524,12 +524,12 @@ namespace Barotrauma { if (SelectedConnection == null) { - DebugConsole.ThrowError("Could not move to the next location (no connection selected).\n"+Environment.StackTrace); + DebugConsole.ThrowError("Could not move to the next location (no connection selected).\n"+Environment.StackTrace.CleanupStackTrace()); return; } if (SelectedLocation == null) { - DebugConsole.ThrowError("Could not move to the next location (no location selected).\n" + Environment.StackTrace); + DebugConsole.ThrowError("Could not move to the next location (no location selected).\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -833,7 +833,7 @@ namespace Barotrauma if (location.Discovered) { #if CLIENT - RemoveFogOfWar(StartLocation); + RemoveFogOfWar(location); #endif if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index a6fa07e10..43ef0f908 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -50,7 +50,7 @@ namespace Barotrauma //observable collection because some entities may need to be notified when the collection is modified public readonly ObservableCollection linkedTo = new ObservableCollection(); - private bool flippedX, flippedY; + protected bool flippedX, flippedY; public bool FlippedX { get { return flippedX; } } public bool FlippedY { get { return flippedY; } } @@ -354,7 +354,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce( "MapEntity.Clone:" + e.Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Cloning entity \"" + e.Name + "\" failed (" + ex.Message + ").\n" + ex.StackTrace); + "Cloning entity \"" + e.Name + "\" failed (" + ex.Message + ").\n" + ex.StackTrace.CleanupStackTrace()); return clones; } Debug.Assert(clones.Last() != null); @@ -534,7 +534,7 @@ namespace Barotrauma public virtual void FlipX(bool relativeToSub) { flippedX = !flippedX; - if (!relativeToSub || Submarine == null) return; + if (!relativeToSub || Submarine == null) { return; } Vector2 relative = WorldPosition - Submarine.WorldPosition; relative.Y = 0.0f; @@ -548,7 +548,7 @@ namespace Barotrauma public virtual void FlipY(bool relativeToSub) { flippedY = !flippedY; - if (!relativeToSub || Submarine == null) return; + if (!relativeToSub || Submarine == null) { return; } Vector2 relative = WorldPosition - Submarine.WorldPosition; relative.X = 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index 827235a77..0c60d7b65 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -284,7 +284,7 @@ namespace Barotrauma if (showErrorMessages) { - DebugConsole.ThrowError("Failed to find a matching MapEntityPrefab (name: \"" + name + "\", identifier: \"" + identifier + "\").\n" + Environment.StackTrace); + DebugConsole.ThrowError("Failed to find a matching MapEntityPrefab (name: \"" + name + "\", identifier: \"" + identifier + "\").\n" + Environment.StackTrace.CleanupStackTrace()); } return null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs index 13c31e4a1..8f9d7b13d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs @@ -44,7 +44,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.NewMessage($"Failed to load hash cache: {e.Message}\n{e.StackTrace}", Microsoft.Xna.Framework.Color.Orange); + DebugConsole.NewMessage($"Failed to load hash cache: {e.Message}\n{e.StackTrace.CleanupStackTrace()}", Microsoft.Xna.Framework.Color.Orange); cache.Clear(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/OrderTarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/OrderTarget.cs new file mode 100644 index 000000000..5194cf291 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/OrderTarget.cs @@ -0,0 +1,22 @@ +using FarseerPhysics; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + class OrderTarget : ISpatialEntity + { + public Vector2 Position { get; private set; } + public Hull Hull { get; private set; } + + public Vector2 WorldPosition => Submarine == null ? Position : Position + Submarine.Position; + public Vector2 SimPosition => ConvertUnits.ToSimUnits(Position); + public Submarine Submarine => Hull?.Submarine; + + public OrderTarget(Vector2 position, Hull hull, bool creatingFromExistingData = false) + { + if (!creatingFromExistingData && hull?.Submarine != null) { position -= hull.Submarine.Position; } + Position = position; + Hull = hull; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 09b985907..8d78b85f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -1407,11 +1407,16 @@ namespace Barotrauma if (item != null) { item.SpawnedInOutpost = true; } } npc.GiveIdCardTags(gotoTarget as WayPoint); - var humanAI = npc.AIController as HumanAIController; - if (humanAI != null) + if (npc.AIController is HumanAIController humanAI) { var idleObjective = humanAI.ObjectiveManager.GetObjective(); - if (idleObjective != null) + if (humanPrefab.CampaignInteractionType != CampaignMode.InteractionType.None) + { + idleObjective.Behavior = AIObjectiveIdle.BehaviorType.StayInHull; + idleObjective.TargetHull = AIObjectiveGoTo.GetTargetHull(gotoTarget); + (GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(npc, humanPrefab.CampaignInteractionType); + } + else { idleObjective.Behavior = humanPrefab.BehaviorType; foreach (string moduleType in humanPrefab.PreferredOutpostModuleTypes) @@ -1420,21 +1425,6 @@ namespace Barotrauma } } } - if (humanPrefab.CampaignInteractionType != CampaignMode.InteractionType.None) - { - if (humanAI != null) - { - Hull goToHull = gotoTarget as Hull ?? (gotoTarget as WayPoint)?.CurrentHull ?? (gotoTarget as Item)?.CurrentHull; - var goToObjective = new AIObjectiveGoTo(gotoTarget, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200); - if (goToHull != null) - { - goToObjective.priorityGetter = () => npc.CurrentHull == goToHull ? 0.0f : AIObjectiveManager.OrderPriority; - } - humanAI.ObjectiveManager.SetOrder(goToObjective); - humanAI.ObjectiveManager.GetObjective().Behavior = AIObjectiveIdle.BehaviorType.StayInHull; - } - (GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(npc, humanPrefab.CampaignInteractionType); - } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 997cd6483..2dcad4753 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -312,7 +312,7 @@ namespace Barotrauma { if (!MathUtils.IsValid(amount)) { - DebugConsole.ThrowError($"Attempted to move a structure by an invalid amount ({amount})\n{Environment.StackTrace}"); + DebugConsole.ThrowError($"Attempted to move a structure by an invalid amount ({amount})\n{Environment.StackTrace.CleanupStackTrace()}"); return; } @@ -986,12 +986,20 @@ namespace Barotrauma { diffFromCenter = (gapRect.Center.X - this.rect.Center.X) / (float)this.rect.Width * BodyWidth; if (BodyWidth > 0.0f) { gapRect.Width = (int)(BodyWidth * (gapRect.Width / (float)this.rect.Width)); } - if (BodyHeight > 0.0f) { gapRect.Height = (int)BodyHeight; } + if (BodyHeight > 0.0f) + { + gapRect.Y = (gapRect.Y - gapRect.Height / 2) + (int)(BodyHeight / 2 + BodyOffset.Y * scale); + gapRect.Height = (int)BodyHeight; + } } else { diffFromCenter = ((gapRect.Y - gapRect.Height / 2) - (this.rect.Y - this.rect.Height / 2)) / (float)this.rect.Height * BodyHeight; - if (BodyWidth > 0.0f) { gapRect.Width = (int)BodyWidth; } + if (BodyWidth > 0.0f) + { + gapRect.X = gapRect.Center.X + (int)(-BodyWidth / 2 + BodyOffset.X * scale); + gapRect.Width = (int)BodyWidth; + } if (BodyHeight > 0.0f) { gapRect.Height = (int)(BodyHeight * (gapRect.Height / (float)this.rect.Height)); } } if (FlippedX) { diffFromCenter = -diffFromCenter; } @@ -1001,7 +1009,7 @@ namespace Barotrauma Vector2 structureCenter = Position; Vector2 gapPos = structureCenter + new Vector2( (float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation), - (float)Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)) * diffFromCenter; + (float)Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)) * diffFromCenter + BodyOffset * scale; gapRect = new Rectangle((int)(gapPos.X - gapRect.Width / 2), (int)(gapPos.Y + gapRect.Height / 2), gapRect.Width, gapRect.Height); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 97036cc5f..471148e4a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1565,14 +1565,17 @@ namespace Barotrauma if (connectedWp.isObstructed) { continue; } Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition) - otherSub.SimPosition; Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition) - otherSub.SimPosition; - var body = Submarine.PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: false); - if (body != null && body.UserData is Structure && !((Structure)body.UserData).IsPlatform) + var body = PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: true); + if (body != null) { - connectedWp.isObstructed = true; - wp.isObstructed = true; - obstructedNodes.Add(node); - obstructedNodes.Add(connection); - break; + if (body.UserData is Structure wall && !wall.IsPlatform || body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) + { + connectedWp.isObstructed = true; + wp.isObstructed = true; + obstructedNodes.Add(node); + obstructedNodes.Add(connection); + break; + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index c00c87067..03a22bc07 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -661,7 +661,7 @@ namespace Barotrauma catch (System.IO.FileNotFoundException e) { exception = e; - DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (File not found) " + Environment.StackTrace, e); + DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (File not found) " + Environment.StackTrace.CleanupStackTrace(), e); return null; } catch (Exception e) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index fd8e579b2..39cba8d86 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Networking partial class ChatMessage { - public const int MaxLength = 150; + public const int MaxLength = 200; public const int MaxMessagesPerPacket = 10; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index 69483c10b..26058cca5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -179,7 +179,7 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (itemPrefab == null) { - string errorMsg = "Attempted to add a null item to entity spawn queue.\n" + Environment.StackTrace; + string errorMsg = "Attempted to add a null item to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue1:ItemPrefabNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; @@ -192,7 +192,7 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (itemPrefab == null) { - string errorMsg = "Attempted to add a null item to entity spawn queue.\n" + Environment.StackTrace; + string errorMsg = "Attempted to add a null item to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue2:ItemPrefabNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; @@ -205,7 +205,7 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (itemPrefab == null) { - string errorMsg = "Attempted to add a null item to entity spawn queue.\n" + Environment.StackTrace; + string errorMsg = "Attempted to add a null item to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue3:ItemPrefabNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; @@ -218,7 +218,7 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (string.IsNullOrEmpty(speciesName)) { - string errorMsg = "Attempted to add an empty/null species name to entity spawn queue.\n" + Environment.StackTrace; + string errorMsg = "Attempted to add an empty/null species name to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; @@ -231,7 +231,7 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (string.IsNullOrEmpty(speciesName)) { - string errorMsg = "Attempted to add an empty/null species name to entity spawn queue.\n" + Environment.StackTrace; + string errorMsg = "Attempted to add an empty/null species name to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue5:SpeciesNameNullOrEmpty", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs index dcc0d4f82..2b69aea0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs @@ -32,7 +32,7 @@ namespace Barotrauma.Networking DebugConsole.ThrowError("Failed to write an event for the entity \"" + e.Entity + "\"", exception); GameAnalyticsManager.AddErrorEventOnce("NetEntityEventManager.Write:WriteFailed" + e.Entity.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Failed to write an event for the entity \"" + e.Entity + "\"\n" + exception.StackTrace); + "Failed to write an event for the entity \"" + e.Entity + "\"\n" + exception.StackTrace.CleanupStackTrace()); //write an empty event to avoid messing up IDs //(otherwise the clients might read the next event in the message and think its ID diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index 76243edbc..2451008d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Barotrauma.Networking +namespace Barotrauma.Networking { partial class OrderChatMessage : ChatMessage { @@ -12,19 +8,20 @@ namespace Barotrauma.Networking public readonly Character TargetCharacter; //which entity is this order referring to (hull, reactor, railgun controller, etc) - public readonly Entity TargetEntity; + public readonly ISpatialEntity TargetEntity; //additional instructions (power up, fire at will, etc) public readonly string OrderOption; - public OrderChatMessage(Order order, string orderOption, Entity targetEntity, Character targetCharacter, Character sender) + public OrderChatMessage(Order order, string orderOption, ISpatialEntity targetEntity, Character targetCharacter, Character sender) : this(order, orderOption, - order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == sender, orderOption: orderOption), - targetEntity, targetCharacter, sender) + order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == sender, orderOption: orderOption), + targetEntity, targetCharacter, sender) { + } - public OrderChatMessage(Order order, string orderOption, string text, Entity targetEntity, Character targetCharacter, Character sender) + public OrderChatMessage(Order order, string orderOption, string text, ISpatialEntity targetEntity, Character targetCharacter, Character sender) : base(sender?.Name, text, ChatMessageType.Order, sender, GameMain.NetworkMember.ConnectedClients.Find(c => c.Character == sender)) { Order = order; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 9b7fd2600..a9d262fa9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -966,19 +966,16 @@ namespace Barotrauma.Networking for (int i = 0; i < count; i++) { string prefabIdentifier = msg.ReadString(); - string prefabName = msg.ReadString(); byte amount = msg.ReadByte(); - var itemPrefab = string.IsNullOrEmpty(prefabIdentifier) ? - MapEntityPrefab.Find(prefabName, null, showErrorMessages: false) as ItemPrefab : - MapEntityPrefab.Find(prefabName, prefabIdentifier, showErrorMessages: false) as ItemPrefab; + var itemPrefab = MapEntityPrefab.Find(null, prefabIdentifier, showErrorMessages: false) as ItemPrefab; if (itemPrefab != null && amount > 0) { - if (changed || !ExtraCargo.ContainsKey(itemPrefab) || ExtraCargo[itemPrefab] != amount) changed = true; + if (changed || !ExtraCargo.ContainsKey(itemPrefab) || ExtraCargo[itemPrefab] != amount) { changed = true; } extraCargo.Add(itemPrefab, amount); } } - if (changed) ExtraCargo = extraCargo; + if (changed) { ExtraCargo = extraCargo; } return changed; } @@ -994,7 +991,6 @@ namespace Barotrauma.Networking foreach (KeyValuePair kvp in ExtraCargo) { msg.Write(kvp.Key.Identifier ?? ""); - msg.Write(kvp.Key.OriginalName ?? ""); msg.Write((byte)kvp.Value); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index bed982de1..1fd780dcd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -532,7 +532,7 @@ namespace Barotrauma { errorMsg += GameMain.NetworkMember.IsClient ? " Playing as a client." : " Hosting a server."; } - errorMsg += "\n" + Environment.StackTrace; + errorMsg += "\n" + Environment.StackTrace.CleanupStackTrace(); if (GameSettings.VerboseLogging) DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( @@ -559,7 +559,7 @@ namespace Barotrauma { errorMsg += GameMain.NetworkMember.IsClient ? " Playing as a client." : " Hosting a server."; } - errorMsg += "\n" + Environment.StackTrace; + errorMsg += "\n" + Environment.StackTrace.CleanupStackTrace(); if (GameSettings.VerboseLogging) DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 0ea24c307..4f87592ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -415,7 +415,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Error in SerializableProperty.TrySetValue", e); + DebugConsole.ThrowError("Error in SerializableProperty.GetValue", e); return false; } } @@ -480,6 +480,9 @@ namespace Barotrauma case "IsDead": { if (parentObject is Character character) { return character.IsDead; } } break; + case "IsHuman": + { if (parentObject is Character character) { return character.IsHuman; } } + break; case "IsOn": { if (parentObject is LightComponent lightComponent) { return lightComponent.IsOn; } } break; @@ -543,6 +546,9 @@ namespace Barotrauma case "SpeedMultiplier": { if (parentObject is Character character && value is float) { character.StackSpeedMultiplier((float)value); return true; } } break; + case "HealthMultiplier": + { if (parentObject is Character character && value is float) { character.StackHealthMultiplier((float)value); return true; } } + break; case "IsOn": { if (parentObject is LightComponent lightComponent && value is bool) { lightComponent.IsOn = (bool)value; return true; } } break; @@ -708,6 +714,26 @@ namespace Barotrauma { string attributeName = attribute.Name.ToString().ToLowerInvariant(); if (attributeName == "gameversion") { continue; } + + if (attributeName == "refreshrect") + { + if (entity is Structure structure) + { + if (!structure.ResizeHorizontal) + { + structure.Rect = structure.DefaultRect = new Rectangle(structure.Rect.X, structure.Rect.Y, + (int)structure.Prefab.ScaledSize.X, + structure.Rect.Height); + } + if (!structure.ResizeVertical) + { + structure.Rect = structure.DefaultRect = new Rectangle(structure.Rect.X, structure.Rect.Y, + structure.Rect.Width, + (int)structure.Prefab.ScaledSize.Y); + } + } + } + if (entity.SerializableProperties.TryGetValue(attributeName, out SerializableProperty property)) { FixValue(property, entity, attribute); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs index 4b93e0dc1..b1f25b2c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs @@ -8,14 +8,7 @@ namespace Barotrauma partial class ConditionalSprite { public readonly List conditionals = new List(); - public bool IsActive - { - get - { - if (Target == null) { return false; } - return Comparison == PropertyConditional.Comparison.And ? conditionals.All(c => c.Matches(Target)) : conditionals.Any(c => c.Matches(Target)); - } - } + public bool IsActive { get; private set; } = true; public readonly PropertyConditional.Comparison Comparison; public readonly bool Exclusive; @@ -55,5 +48,17 @@ namespace Barotrauma } } } + + public void CheckConditionals() + { + if (Target == null) + { + IsActive = false; + } + else + { + IsActive = Comparison == PropertyConditional.Comparison.And ? conditionals.All(c => c.Matches(Target)) : conditionals.Any(c => c.Matches(Target)); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 7032585fd..72e556013 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -68,6 +68,8 @@ namespace Barotrauma public readonly SpawnPositionType SpawnPosition; public readonly float Speed; public readonly float Rotation; + public readonly int Count; + public readonly float Spread; public ItemSpawnInfo(XElement element, string parentDebugName) { @@ -99,7 +101,9 @@ namespace Barotrauma } Speed = element.GetAttributeFloat("speed", 0.0f); - Rotation = MathHelper.ToRadians(element.GetAttributeFloat("rotation", 0.0f)); + Rotation = element.GetAttributeFloat("rotation", 0.0f); + Count = element.GetAttributeInt("count", 1); + Spread = element.GetAttributeFloat("spread", 0f); string spawnTypeStr = element.GetAttributeString("spawnposition", "This"); if (!Enum.TryParse(spawnTypeStr, out SpawnPosition)) @@ -416,8 +420,9 @@ namespace Barotrauma } } - float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength"); - Afflictions.Add(afflictionPrefab.Instantiate(afflictionStrength)); + Affliction afflictionInstance = afflictionPrefab.Instantiate(subElement.GetAttributeFloat(1.0f, "amount", "strength")); + afflictionInstance.Probability = subElement.GetAttributeFloat(1.0f, "probability"); + Afflictions.Add(afflictionInstance); break; case "reduceaffliction": @@ -480,20 +485,16 @@ namespace Barotrauma public virtual bool HasRequiredItems(Entity entity) { - if (requiredItems == null) return true; + if (entity == null) { return true; } foreach (RelatedItem requiredItem in requiredItems) { - if (entity == null) + if (entity is Item item) { - return false; - } - else if (entity is Item item) - { - if (!requiredItem.CheckRequirements(null, item)) return false; + if (!requiredItem.CheckRequirements(null, item)) { return false; } } else if (entity is Character character) { - if (!requiredItem.CheckRequirements(character, null)) return false; + if (!requiredItem.CheckRequirements(character, null)) { return false; } } } return true; @@ -507,12 +508,10 @@ namespace Barotrauma foreach (Character c in Character.CharacterList) { if (!c.Enabled || c.Removed || !IsValidTarget(c)) { continue; } - float xDiff = Math.Abs(c.WorldPosition.X - worldPosition.X); - if (xDiff > Range) { continue; } - float yDiff = Math.Abs(c.WorldPosition.Y - worldPosition.Y); - if (yDiff > Range) { continue; } - - if (xDiff * xDiff + yDiff * yDiff < Range * Range) { targets.Add(c); } + if (CheckDistance(c)) + { + targets.Add(c); + } } } if (HasTargetType(TargetType.NearbyItems)) @@ -520,14 +519,25 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { if (item.Removed || !IsValidTarget(item)) { continue; } - float xDiff = Math.Abs(item.WorldPosition.X - worldPosition.X); - if (xDiff > Range) { continue; } - float yDiff = Math.Abs(item.WorldPosition.Y - worldPosition.Y); - if (yDiff > Range) { continue; } - - if (xDiff * xDiff + yDiff * yDiff < Range * Range) { targets.Add(item); } + if (CheckDistance(item)) + { + targets.Add(item); + } } } + + bool CheckDistance(ISpatialEntity e) + { + float xDiff = Math.Abs(e.WorldPosition.X - worldPosition.X); + if (xDiff > Range) { return false; } + float yDiff = Math.Abs(e.WorldPosition.Y - worldPosition.Y); + if (yDiff > Range) { return false; } + if (xDiff * xDiff + yDiff * yDiff < Range * Range) + { + return true; + } + return false; + } } public bool HasRequiredConditions(IEnumerable targets) @@ -710,7 +720,7 @@ namespace Barotrauma private Vector2 GetPosition(Entity entity, IEnumerable targets, Vector2? worldPosition = null) { - Vector2 position = worldPosition ?? (entity.Removed ? Vector2.Zero : entity.WorldPosition); + Vector2 position = worldPosition ?? (entity == null || entity.Removed ? Vector2.Zero : entity.WorldPosition); if (worldPosition == null) { if (entity is Character c && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) @@ -753,12 +763,15 @@ namespace Barotrauma { foreach (var target in targets) { - if (target is Limb limb && limb.character != null && !limb.character.Removed) targetCharacter = ((Limb)target).character; + if (target is Limb limb && limb.character != null && !limb.character.Removed) + { + targetCharacter = ((Limb)target).character; + } } } for (int i = 0; i < useItemCount; i++) { - if (item.Removed) continue; + if (item.Removed) { continue; } item.Use(deltaTime, targetCharacter, targets.FirstOrDefault(t => t is Limb) as Limb); } } @@ -786,6 +799,8 @@ namespace Barotrauma { foreach (ISerializableEntity target in targets) { + if (target == null) { continue; } + if (target is Entity targetEntity) { if (targetEntity.Removed) { continue; } @@ -807,18 +822,17 @@ namespace Barotrauma } } - if (entity != null) + foreach (Explosion explosion in explosions) { - foreach (Explosion explosion in explosions) - { - explosion.Explode(position, damageSource: entity, attacker: user); - } + explosion.Explode(position, damageSource: entity, attacker: user); } foreach (ISerializableEntity target in targets) { + if (target == null) { continue; } foreach (Affliction affliction in Afflictions) { + if (Rand.Value(Rand.RandSync.Unsynced) > affliction.Probability) { continue; } Affliction multipliedAffliction = affliction; if (!disableDeltaTime) { @@ -898,6 +912,13 @@ namespace Barotrauma Entity.Spawner.AddToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Server) + characterSpawnInfo.Offset, onSpawn: newCharacter => { + if (newCharacter.AIController is EnemyAIController enemyAi && + enemyAi.PetBehavior != null && + entity is Item item && + item.ParentInventory is CharacterInventory inv) + { + enemyAi.PetBehavior.Owner = inv.Owner as Character; + } characters.Add(newCharacter); if (characters.Count == characterSpawnInfo.Count) { @@ -908,59 +929,66 @@ namespace Barotrauma } foreach (ItemSpawnInfo itemSpawnInfo in spawnItems) { - switch (itemSpawnInfo.SpawnPosition) + for (int i = 0; i < itemSpawnInfo.Count; i++) { - case ItemSpawnInfo.SpawnPositionType.This: - Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, position); - break; - case ItemSpawnInfo.SpawnPositionType.ThisInventory: - { - if (entity is Character character && character.Inventory != null) + switch (itemSpawnInfo.SpawnPosition) + { + case ItemSpawnInfo.SpawnPositionType.This: + Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, position + Rand.Vector(itemSpawnInfo.Spread, Rand.RandSync.Server), onSpawned: newItem => { - 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); - } - } - else if (entity is Item item) + newItem.body?.ApplyLinearImpulse(Rand.Vector(1) * itemSpawnInfo.Speed); + newItem.Rotation = itemSpawnInfo.Rotation; + }); + break; + case ItemSpawnInfo.SpawnPositionType.ThisInventory: { - var inventory = item?.GetComponent()?.Inventory; - if (inventory != null) + if (entity is Character character && character.Inventory != null) { - int emptyCount = inventory.Items.Count(it => it == null); - if (emptyCount - Entity.Spawner.CountSpawnQueue(spawnInfo => spawnInfo is EntitySpawner.ItemSpawnInfo itemSpawnInfo && itemSpawnInfo.Inventory == inventory) > 0) + 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, inventory); + Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, character.Inventory); + } + } + else if (entity is Item item) + { + var inventory = item?.GetComponent()?.Inventory; + if (inventory != null) + { + 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); + } } } } - } - break; - case ItemSpawnInfo.SpawnPositionType.ContainedInventory: - { - Inventory thisInventory = null; - if (entity is Character character) + break; + case ItemSpawnInfo.SpawnPositionType.ContainedInventory: { - thisInventory = character.Inventory; - } - else if (entity is Item item) - { - thisInventory = item?.GetComponent()?.Inventory; - } - if (thisInventory != null) - { - foreach (Item item in thisInventory.Items) + Inventory thisInventory = null; + if (entity is Character character) { - if (item == null) continue; - Inventory containedInventory = item.GetComponent()?.Inventory; - if (containedInventory == null || !containedInventory.Items.Any(i => i == null)) continue; - Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, containedInventory); - break; + thisInventory = character.Inventory; + } + else if (entity is Item item) + { + thisInventory = item?.GetComponent()?.Inventory; + } + if (thisInventory != null) + { + foreach (Item item in thisInventory.Items) + { + if (item == null) continue; + Inventory containedInventory = item.GetComponent()?.Inventory; + if (containedInventory == null || !containedInventory.Items.Any(i => i == null)) continue; + Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, containedInventory); + break; + } } } - } - break; + break; + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs index db622c204..6c844bb98 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs @@ -317,7 +317,7 @@ namespace Barotrauma if (variableValues[i] == null) { #if DEBUG - DebugConsole.ThrowError("Error in TextManager.GetWithVariables (variable " + i + " was null).\n" + Environment.StackTrace); + DebugConsole.ThrowError("Error in TextManager.GetWithVariables (variable " + i + " was null).\n" + Environment.StackTrace.CleanupStackTrace()); #endif continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/AssemblyInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/AssemblyInfo.cs index 4e891145a..404e8b571 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/AssemblyInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/AssemblyInfo.cs @@ -1,46 +1,48 @@ -using System.Linq; +using System; +using System.Linq; using System.Reflection; public static class AssemblyInfo { - /// Gets the git hash value from the assembly - /// or null if it cannot be found. - public static string GetGitRevision() + public static readonly string GitRevision; + public static readonly string GitBranch; + public static readonly string ProjectDir; + public static readonly string BuildString; + + static AssemblyInfo() { var asm = typeof(AssemblyInfo).Assembly; var attrs = asm.GetCustomAttributes(); - return attrs.FirstOrDefault(a => a.Key == "GitRevision")?.Value; - } - /// Gets the git branch name from the assembly - /// or null if it cannot be found. - public static string GetGitBranch() - { - var asm = typeof(AssemblyInfo).Assembly; - var attrs = asm.GetCustomAttributes(); - return attrs.FirstOrDefault(a => a.Key == "GitBranch")?.Value; - } + GitRevision = attrs.FirstOrDefault(a => a.Key == "GitRevision")?.Value; - /// Gets the build platform and configuration - public static string GetBuildString() - { - string retVal = "Unknown"; + GitBranch = attrs.FirstOrDefault(a => a.Key == "GitBranch")?.Value; + + ProjectDir = attrs.FirstOrDefault(a => a.Key == "ProjectDir")?.Value; + if (ProjectDir.Last() == '/' || ProjectDir.Last() == '\\') { ProjectDir = ProjectDir.Substring(0, ProjectDir.Length - 1); } + string[] dirSplit = ProjectDir.Split('/', '\\'); + ProjectDir = string.Join(ProjectDir.Contains('/') ? '/' : '\\', dirSplit.Take(dirSplit.Length - 2)); + + BuildString = "Unknown"; #if WINDOWS - retVal = "Windows"; + BuildString = "Windows"; #elif OSX - retVal = "Mac"; + BuildString = "Mac"; #elif LINUX - retVal = "Linux"; + BuildString = "Linux"; #endif #if DEBUG - retVal = "Debug" + retVal; + BuildString = "Debug" + BuildString; #elif UNSTABLE - retVal = "Unstable" + retVal; + BuildString = "Unstable" + BuildString; #else - retVal = "Release" + retVal; + BuildString = "Release" + BuildString; #endif + } - return retVal; + public static string CleanupStackTrace(this string stackTrace) + { + return stackTrace.Replace(ProjectDir, ""); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs index f5d9bf70e..e9d8f3727 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs @@ -44,7 +44,7 @@ namespace Barotrauma #if DEBUG throw new Exception("Unauthorized multithreaded access to RandSync.Server"); #else - DebugConsole.ThrowError("Unauthorized multithreaded access to RandSync.Server\n" + Environment.StackTrace); + DebugConsole.ThrowError("Unauthorized multithreaded access to RandSync.Server\n" + Environment.StackTrace.CleanupStackTrace()); #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs index 05c7f975e..921725254 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs @@ -87,7 +87,7 @@ namespace Barotrauma DebugConsole.ThrowError(msg); foreach (Exception e in task.Exception.InnerExceptions) { - DebugConsole.ThrowError(e.Message + "\n" + e.StackTrace); + DebugConsole.ThrowError(e.Message + "\n" + e.StackTrace.CleanupStackTrace()); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index ebf4f2cdf..3a90ce26c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -406,7 +406,7 @@ namespace Barotrauma if (objects.Count != weights.Count) { - DebugConsole.ThrowError("Error in SelectWeightedRandom, number of objects does not match the number of weights.\n" + Environment.StackTrace); + DebugConsole.ThrowError("Error in SelectWeightedRandom, number of objects does not match the number of weights.\n" + Environment.StackTrace.CleanupStackTrace()); return objects[0]; } diff --git a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub index 956266192..065f88283 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 11221a164..ed6d8292a 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 915132f19..db7711f8b 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 1725c25c4..5b6e5d0a8 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 c0cfb9246..1e07c93fa 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 01f5e42b1..0b1a4f5b7 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub and b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca.sub b/Barotrauma/BarotraumaShared/Submarines/Orca.sub index 9fce2b208..c10d5de24 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 index 9a941a4ea..1fe91b36e 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/R-29.sub and b/Barotrauma/BarotraumaShared/Submarines/R-29.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora.sub b/Barotrauma/BarotraumaShared/Submarines/Remora.sub index 6d94c2860..12d7aac5c 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub index 124bd73b2..4840c9d73 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 9d9a1af32..a8377211d 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 bb99d09eb..ace353ab4 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 982a2248e..97d16a3a3 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 b61e2d757..d29dab26c 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,199 @@ +--------------------------------------------------------------------------------------------------------- +v0.10.6.2 +--------------------------------------------------------------------------------------------------------- + +- Adjusted pets' item production rates and hunger/happiness thresholds. +- Fixed inability to pick up chitin chunks. +- Fixed event manager considering pets to be enemies, leading to monster spawns being delayed or disabled altogether when there are pets inside the sub. +- Fixed bots having difficulties in entering/exiting the airlocks. +- Fixed doors not obstructing waypoints after docking. +- Fixes to pet syncing. +- Fixes to pets disappearing when transitioning between levels. +- Removed small crawler egg (was only intended for testing). +- Kastrull: Fixed the airlock waypoints not being linked to the doors, causing the bots not being able to operate them. + +--------------------------------------------------------------------------------------------------------- +v0.10.6.1 +--------------------------------------------------------------------------------------------------------- + +- Added sounds for pets. +- Most fruits can be fed to pets. +- Increased the time it takes for pets to get hungry. +- Decreased pets' item production rate. +- The number of monsters now increases the event intensity more than previously, which should spawn monsters less frequently. +- Added some unarmored mudraptors to low-difficulty levels. +- Fixed crashing when a character takes damage from something else than another character attacking them (e.g. volcanoes). +- Fixed some steering issues where bots would return to the last waypoint instead of continuing with their current path when they should. +- Fixed occasional "velocity invalid" error messages when a character gets hit by a very fast projectile (e.g. coilgun bolt). +- Fixed severed tiger thresher heads doing the "death wiggle" animation. +- Fixed water flow sounds not disappearing over time. +- Fixed throwing an item that has no status effects (such as a flare) causing a crash. +- Fixed chitin helmet spawning in armory cabinets. +- Fixed event intensity going down immediately when the situation gets less intense instead of gradually returning to normal. Should make it happen less that more monsters are spawned soon after the player survives the previous wave of monsters. +- Fixed monsters using the Escape state when they should use the Flee state. +- Fixed Threshers not dying after being beheaded. +- Fixed heads or other extruding limbs being severed also when their root body takes damage. +- Fixed bots trying to get inside outposts (from outside) that they can't and shouldn't be able to enter to. +- Fixed very small creatures getting stuck on waypoints. +- Test changing the assignment logic for maintenance/operate orders: if the player doesn't specify the target character, use the bot who already is following the same order. The intention is to make it easier to change the target item of the order. The draw back is that ordering multiple bots to man the turrets now requires an extra step: specify the target character. + +--------------------------------------------------------------------------------------------------------- +v0.10.6.0 +--------------------------------------------------------------------------------------------------------- + +Changes and additions: +- Reworked Watcher. +- Added pets (can be obtained by buying eggs from outposts). The pets produce items that can be used for crafting if they're kept happy and well-fed. +- Added a new AI behaviors: Observe, Follow and Freeze. +- Added toolbelt (a wearable container with a capacity of 12 and it's own dedicated slot) as a replacement for the toolbox. +- Improvements to the effects caused by psychosis: the affliction icon is not visible to the psychotic character, the fake fires and floods are a bit more convincing, the affliction plays random sounds and can cause other characters to become invisible. +- Added capture group support to RegEx component. +- Added black and white paint. +- Added optional "forbidden word filter" to the server browser. The filter is off by default. The list of forbidden words can be modified by editing the file "Data/forbiddenwordlist.txt" in the game folder. +- Increased the damage of 40mm grenades. +- Misc improvements to the tutorials. +- Added undo and redo functionality to the submarine editor. +- Added animations and lights to pump, fabricator, deconstructor and engines. +- All explosions now inflict explosion damage instead or in addition to lacerations. +- Nerfed the mudraptor slightly: the head and the tail armor should now break more easily; increased the cooldown of some attacks. +- Mudraptor's armor now explodes when destroyed. +- Added an unarmored variation of mudraptor. +- Bonethresher's head is now vulnerable to gunshotwounds and lacerations. +- Adjusted the appropriate jobs for bots. +- Changed the automatic crew selection logic for orders. +- Only show either electrical or mechanical repair order for the contextual selection, depending on which skills the item requires. +- Previous order icons of the same type are no more created, no matter if the target was different than the current. +- Tigerthreshers can now attack inner walls of the submarine. They still need help to get inside. +- Changes to the Tigertherthresher's main collider that should improve the overall movement. +- Option to make particle emitters modify the color of the particles. +- Set a limit to how many bots you can hire in the campaign (currently 16). +- Explosions caused by sodium, potassium, magnesium and lithium being exposed to water don't trigger additional explosions (e.g. oxygen tanks don't explode if they happen to be next to a sodium explosion). +- Pumps and relays placed in the sub editor are on by default. +- Added sounds for the nausea affliction. +- Nausea now inflicts a minor stun and internal damage when the character throws up. +- Monsters now stop fleeing after a while, if they are not being chased and can't perceive the target anymore. +- Implement spread, speed, and rotation for the spawn item status effects. +- Minor damage (less than 1 hp) doesn't spawn particles anymore. +- Rebalanced upgrade parameters, allowing for more noticable benefits. +- Allow closing the splash screens with esc. +- Added more copper to chalcopyrite and bornite deconstruct recipe. +- More calcium for aragonite, adjusted prices. +- Made large monsters immune to paralyzant (mudraptor is the largest affected monster). +- Use player name instead of server name for the server owner when hosting a server. +- Don't draw turret range indicators in the sub editor when the turret isn't selected. +- Adjusted Hammerhead's posture. +- Minor adjustments to Mudraptor's animations. + +Modding: +- Added Scale and Offset to Light Sprite's parameters. +- Fixed the Constant Torque parameter not working right. +- Allow to enable/disable tail angles per limb. Previously the angle was only applied to the first limb of type Tail. +- Added per limb multipliers for sine animations (fish tail movement). +- Exposed the fleeing and avoiding times on the monster AI parameters. +- The Light Sources on characters can now be defined with conditionals. +- Added HealthMultiplier parameter that can be used in StatusEffects like SpeedMultiplier. + +Character Editor: +- Fixed a number of issues with the joint limit widgets. Also allowed to set a joint to rotate clockwise, which inverses the widget direction. Useful for heads or other limbs that extrude right from the main body. +- The colliders of the hidden limbs are now hidden in the game view. +- Changed the hotkey for toggling the parameter editor from "Tab" to "F1" and fix the inability to toggle the editor when a text field is selected. +- Fixed load and save interfaces being broken on lower resolutions. + +Sounds: +- Added 2 new background music tracks +- Added a new, separate sounds for opening and closing the alien doors +- Added a new sound for medium-sized fires +- Added new heartbeat sound for suffering from low oxygen +- Added new sounds for the hull creaking +- Added a new sound for a broken pump +- Added a new sound for tinnitus after an explosion +- Added a new sound for alien artifacts +- Added a new sound for electrocution +- Added a new sound for husk footsteps +- Added new misc. UI sounds +- Added a new variations for some existing sounds +- Improved some existing sounds +- Exposed GUI sound definitions in the sounds.xml file + +AI improvements and fixes: +- Added a new AI behavior and order: cleanup items. +- Fixed severe performance hit when damaging outpost NPCs with a weapon that does continuous damage (for example a flamer). +- Reworked the contextual "Wait" order: the order now targets a position instead of an entity. +- Bots are now allowed to more things even they are not inside the player sub. For example they can be told to extinguish fires in the outpost or rescue crew members inside a wreck (when they are at the same place). WIP. +- Bots now try to put oxygen tanks to oxygen tank containers, before the oxygen generator or supplycab. +- Bots now re-equip hats, helmets, and other clothing that they have unequipped for being able to wear diving gear. +- Bots now use the underwater scooter when they have one in the inventory. +- Bots should now be slightly smarter when putting off fires. +- Bots don't anymore retaliate if they are being shot while there is an enemy around. Fixes #3840. +- Fixed a number of cases where bots would get stuck or fail to fix a leak. +- Fixed bots with steering order targeting the shuttle nav terminal instead of the main nav terminal. +- Fixed NPCs getting aggroed by certain drugs (e.g. anaparalyzant). +- Fixed bots not prioritizing the wall mounts for containing the extinguishers. +- Fixed monsters targeting inner doors when they shouldn't (#3891). +- Fixed outpost guards attacking the player for defending themselves against the aggressors on the event "mediator". +- Fixed NPCs not reacting when the player is attacked. +- Fixed AI considering characters on the opposing combat mission team as friendly. + +Misc fixes: +- Fixed clients always getting the generic "could not connect" error message when connecting to a server fails, even if there's a specific reason to the connection failing (e.g. disallowed symbols in the player's name, mismatching content packages or game version). +- Fixed yet another cause for "missing entity" errors. Occasionally happened in monster missions when a monster happened to get assigned the same ID as an item in a player's inventory. +- Fixed clients spawning a respawn shuttle in non-campaign missions even if the server has disabled respawning, leading to "missing entity" errors. +- Fixed planters dropping removed seeds after a save is reloaded. +- Fixed plants not updating the health after being fully grown in multiplayer. +- Fixed decal syncing working unreliably in multiplayer. +- Fixed "level doesn't match" errors after leaving a shuttle/drone behind in the multiplayer campaign. +- Fixed painted/dirty walls going to full opacity when transitioning to a new level in the campaign. +- Fixed crashing when trying to render a light whose range is less than 1 pixel. +- Fixed crashing when using a repair tool causes an explosion (e.g. when killing a terminal cell with a plasma cutter). +- Mudraptor: Adjusted and fixed the animations. +- Fixed irrelevant damage modifiers (like bleeding) affecting the damageemitter's particles. In practice, this caused damagemodifiers with bleeding modifier 0 not spawn any particle effects when the limb was hit. +- Fixed extinguish fires not working with extinguish fire orders if the room has not enough oxygen. +- Fixed the user set state of the crew list not being respected; autohiding/showing shouldn't change the state set by the player. +- Fixed significant lag spikes at outposts with many NPCs. Caused by too frequent pathfinding calls, when an NPC can't find a path to the target. +- Fixed AIState.Protect throwing exceptions when an attacker has been removed. +- Fixed status effects targeting nearby characters/items failing to launch, unless they used the "Active" effect type. +- Fixed crashing when trying to crouch while swimming in the character editor. +- Fixed pause menu being togglable during level transitions, causing errors if quitting the game during a transition. +- Fixed wire disappearing from the character's inventory and from the walls when getting shocked from attaching the 2nd end of a wire in multiplayer. +- Fixed airlock warning light in Kastrull not turning off when the airlock door is closed. +- Made plants non-combineable. +- Fixed random submarine selection setting for servers occasionally selecting non-submarines. +- Fixed dedicated servers still showing up as "playstyle-less" in the server browser. +- Fixed sounds defined in affliction status effects not working. +- Require at least 1 kick vote before kicking someone, no matter how low KickVoteRequiredRatio is set to. +- Fixed gaps being misplaced on certain wall structures, preventing certain walls from leaking in some subs (for example the wall above Typhon's junction compartment). +- Fixed "Select Matching Items" crashing in the editor when used with submarines with a linked submarine. +- Fixed hull above Kastrull's drone not being connected to the rest of the hulls, preventing oxygen from flowing between the drone and the sub when docked. +- Fixed junction boxes' signal connections passing power/load information. +- Fixed crash when trying to clone a linked sub that you don't have the submarine file for in the sub editor. +- Fixed incorrectly scaled items in the diving gear cabinet/locker assemblies. +- Fixed inability to sell faraday artifacts when the condition of the artifact is 0. +- Fixed monsters not fleeing if the attacker is a bot. +- Fixed OnImpact statuseffects not launching if the impact was negative (e.g. when a moving sub hits an item that's below the sub). +- OnDamaged status effects now launch only when there's any damage. Not when the damage is zero. +- Fixed health bar pulsating even when no damage is done by an attack/status effect. +- Fixed affliction probability not having any effect when used in status effects. +- Fixed EventManager crashing if there are no event sets configured for the current location type (only affected mods that add new location types without adding any events for them). +- Fixed items held in the left hand being drawn in front of the characters. +- Fixed ability to drag and drop items into outpost reactors. +- Set terminal's maximum message length to match maximum chat message length (otherwise chat-linked terminals work differently in multiplayer). +- Fixed diving suit's low oxygen warning beep not following the player wearing the suit. +- Fixed "propaganda" and "clown outbreak" outpost events never triggering. +- Fixed "clown brutality" event getting stuck after the NPCs have been spawned. +- Fixed "impromptu engineering" event giving only one coilgun ammo box despite the text saying 2. +- Fixed "black market" event always giving the player the alien pistol. +- Fixed incendium bars exploding when pressing the Use key while holding one. +- Fixed outpost security not reacting to players throwing items that explode on impact (e.g. flash powder or nitroglycerin). +- Fixed outpost security reacting to raptor bane extract injections with lethal force. +- Fixed mantis' animation. +- Fixed multiple wire nodes occasionally getting placed with one click when rewiring. +- Fixed fire and water flow sounds staying active when returning from a multiplayer session to the main menu. +- Fixed previously discovered map tiles becoming undiscovered when saving and loading a campaign. +- Fixed 1st shot from SMG magazines spawned with console commands doing nothing. +- Fixed calyxanide not damaging huskified humans or crawlers. +- Fixed explosives exploding when combining them results in one being removed. +- Fixed items that don't flip horizontally being positioned incorrectly in mirrored subs. + --------------------------------------------------------------------------------------------------------- v0.10.5.1 --------------------------------------------------------------------------------------------------------- diff --git a/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs b/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs index 3cf44fb41..e78c07bf0 100644 --- a/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs +++ b/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs @@ -123,6 +123,7 @@ namespace Steamworks /// public static async Task WaitForPingDataAsync( float maxAgeInSeconds = 60 * 5 ) { + await Task.Yield(); if ( Internal.CheckPingDataUpToDate( maxAgeInSeconds ) ) return;