diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 460cf9c78..342e85237 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,9 @@ --- name: Bug report about: Found a bug? Help us squash it by making a bug report! +title: '' +labels: '' +assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/release-checklist.md b/.github/ISSUE_TEMPLATE/release-checklist.md new file mode 100644 index 000000000..598d41dfd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release-checklist.md @@ -0,0 +1,33 @@ +--- +name: Release checklist +about: A checklist that should be gone through when releasing a hotfix/patch/update +title: v0.0.0.0 +labels: Code, CM +assignees: '' + +--- + +**Code:** +- [ ] Build and upload dedicated server +- [ ] Verify that Vanilla content package hashes match between Windows/Mac/Linux +- [ ] Run "checkmissingloca" command to make sure localization files are up-to-date. +- [ ] Install Trick or Trauma and check you can start a round with no obvious issues/errors (to make sure we didn't unintentionally break compatibility with older mods). +- [ ] Prepare new main menu content (changelog) +- [ ] Prepare public github repo for pushing the new changes + +**CM:** +- [ ] Prepare Steam announcement +- [ ] Prepare Discord announcement (if needed) +- [ ] Prepare blog post (if needed) + +**Code (when everything above is ready):** +- [ ] Publish +- [ ] Upload new main menu content +- [ ] Update public github repo +- [ ] Merge release to master and active branches + +**CM (after the update is live):** +- [ ] Post Steam announcement +- [ ] Post Discord announcement (if needed) +- [ ] Blog post (if needed) +- [ ] Clean up Trello diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 1afe18bd0..48648f15b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -335,7 +335,8 @@ namespace Barotrauma //an ad-hoc way of allowing the players to have roughly the same maximum view distance regardless of the resolution, //while still keeping the zoom around 1.0 when not looking further away (because otherwise we'd always be downsampling //on lower resolutions, which doesn't look that good) - float newZoom = MathHelper.Lerp(unscaledZoom, scaledZoom, (float)Math.Sqrt(zoomOutAmount)); + float newZoom = MathHelper.Lerp(unscaledZoom, scaledZoom, + (GameMain.Config == null || GameMain.Config.EnableMouseLook) ? (float)Math.Sqrt(zoomOutAmount) : 0.3f); Zoom += (newZoom - zoom) / ZoomSmoothness; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs index a237a6a1e..a1be8d950 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs @@ -9,7 +9,7 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch) { - if (!ShowAITargets) return; + if (!ShowAITargets) { return; } var pos = new Vector2(WorldPosition.X, -WorldPosition.Y); if (soundRange > 0.0f) { @@ -43,8 +43,8 @@ namespace Barotrauma } else { - color = Color.WhiteSmoke; - // disable the indicators for structures, because they clutter the debug view + //color = Color.WhiteSmoke; + // disable the indicators for structures and hulls, because they clutter the debug view return; } ShapeExtensions.DrawCircle(spriteBatch, pos, SightRange, 100, color, thickness: 1 / Screen.Selected.Cam.Zoom); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index d33b41d27..a9ff967ca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -17,7 +17,7 @@ namespace Barotrauma if (State == AIState.Idle && PreviousState == AIState.Attack) { var target = _selectedAiTarget ?? _lastAiTarget; - if (target != null) + if (target != null && target.Entity != null) { var memory = GetTargetMemory(target); Vector2 targetPos = memory.Location; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs index 726985bb1..fdf426063 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs @@ -16,11 +16,6 @@ namespace Barotrauma }*/ } - partial void SetOrderProjSpecific(Order order, string option) - { - GameMain.GameSession.CrewManager.DisplayCharacterOrder(Character, order, option); - } - public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch) { Vector2 pos = Character.WorldPosition; @@ -40,7 +35,7 @@ namespace Barotrauma var currentOrder = ObjectiveManager.CurrentOrder; if (currentOrder != null) { - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 20), $"ORDER: {currentOrder.DebugTag} ({currentOrder.GetPriority().FormatZeroDecimal()})", Color.White, Color.Black); + GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 20), $"ORDER: {currentOrder.DebugTag} ({currentOrder.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } else if (ObjectiveManager.WaitTimer > 0) { @@ -51,17 +46,17 @@ namespace Barotrauma { if (currentOrder == null) { - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 20), $"MAIN OBJECTIVE: {currentObjective.DebugTag} ({currentObjective.GetPriority().FormatZeroDecimal()})", Color.White, Color.Black); + GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 20), $"MAIN OBJECTIVE: {currentObjective.DebugTag} ({currentObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } - var subObjective = currentObjective.SubObjectives.FirstOrDefault(); + var subObjective = currentObjective.CurrentSubObjective; if (subObjective != null) { - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 40), $"SUBOBJECTIVE: {subObjective.DebugTag} ({subObjective.GetPriority().FormatZeroDecimal()})", Color.White, Color.Black); + GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 40), $"SUBOBJECTIVE: {subObjective.DebugTag} ({subObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } var activeObjective = ObjectiveManager.GetActiveObjective(); if (activeObjective != null) { - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 60), $"ACTIVE OBJECTIVE: {activeObjective.DebugTag} ({activeObjective.GetPriority().FormatZeroDecimal()})", Color.White, Color.Black); + GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 60), $"ACTIVE OBJECTIVE: {activeObjective.DebugTag} ({activeObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index 63d963175..eb622fe34 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -286,7 +286,7 @@ namespace Barotrauma { bool inWater = limb.inWater; if (character.CurrentHull != null && - character.CurrentHull.Surface > character.CurrentHull.Rect.Y - character.CurrentHull.Rect.Height && + character.CurrentHull.Surface > character.CurrentHull.Rect.Y - character.CurrentHull.Rect.Height + 5.0f && limb.SimPosition.Y < ConvertUnits.ToSimUnits(character.CurrentHull.Rect.Y - character.CurrentHull.Rect.Height) + limb.body.GetMaxExtent()) { inWater = true; @@ -370,6 +370,7 @@ namespace Barotrauma foreach (var deformation in SpriteDeformations) { if (character.IsDead && deformation.Params.StopWhenHostIsDead) { continue; } + if (!character.AnimController.InWater && deformation.Params.OnlyInWater) { continue; } if (deformation.Params.UseMovementSine) { if (this is AnimController animator) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index a2e3ff53f..7522954e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -365,7 +365,7 @@ namespace Barotrauma } } - partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction) + partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log) { if (GameMain.NetworkMember != null && controlled == this) { @@ -466,7 +466,7 @@ namespace Barotrauma //modify the distance based on the size of the trigger (preferring smaller items) distanceToItem *= MathHelper.Lerp(0.05f, 2.0f, (transformedTrigger.Width + transformedTrigger.Height) / 250.0f); } - else + else if (!item.Prefab.RequireCursorInsideTrigger) { Rectangle itemDisplayRect = new Rectangle(item.InteractionRect.X, item.InteractionRect.Y - item.InteractionRect.Height, item.InteractionRect.Width, item.InteractionRect.Height); @@ -551,7 +551,7 @@ namespace Barotrauma { if (!enabled) { return; } - if (!IsDead && !IsUnconscious) + if (!IsDead && !IsIncapacitated) { if (soundTimer > 0) { @@ -603,6 +603,11 @@ namespace Barotrauma } } + partial void SetOrderProjSpecific(Order order, string orderOption) + { + GameMain.GameSession?.CrewManager?.DisplayCharacterOrder(this, order, orderOption); + } + public static void AddAllToGUIUpdateList() { for (int i = 0; i < CharacterList.Count; i++) @@ -638,8 +643,7 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Camera cam) { - if (!Enabled) return; - + if (!Enabled) { return; } AnimController.Draw(spriteBatch, cam); } @@ -656,8 +660,6 @@ namespace Barotrauma if (GameMain.DebugDraw) { AnimController.DebugDraw(spriteBatch); - - if (aiTarget != null) aiTarget.Draw(spriteBatch); } if (GUI.DisableHUD) return; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 2f2b23ece..8ccf31dcc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -61,7 +61,7 @@ namespace Barotrauma { if (GUI.DisableHUD) return; - if (!character.IsUnconscious && character.Stun <= 0.0f) + if (!character.IsIncapacitated && character.Stun <= 0.0f) { if (character.Inventory != null) { @@ -90,9 +90,9 @@ namespace Barotrauma { if (GUI.DisableHUD) { return; } - if (!character.IsUnconscious && character.Stun <= 0.0f) + if (!character.IsIncapacitated && character.Stun <= 0.0f) { - if (character.Info != null && !character.ShouldLockHud()) + if (character.Info != null && !character.ShouldLockHud() && character.SelectedCharacter == null) { bool mouseOnPortrait = HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition) && GUI.MouseOn == null; if (mouseOnPortrait && PlayerInput.PrimaryMouseButtonClicked()) @@ -154,6 +154,7 @@ namespace Barotrauma brokenItemsCheckTimer = 1.0f; foreach (Item item in Item.ItemList) { + if (item.Submarine == null || item.Submarine.TeamID != character.TeamID || item.Submarine.Info.IsWreck) { continue; } if (!item.Repairables.Any(r => item.ConditionPercentage <= r.AIRepairThreshold)) { continue; } if (Submarine.VisibleEntities != null && !Submarine.VisibleEntities.Contains(item)) { continue; } @@ -201,7 +202,7 @@ namespace Barotrauma Color.Lerp(GUI.Style.Red, GUI.Style.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha); } - if (!character.IsUnconscious && character.Stun <= 0.0f) + if (!character.IsIncapacitated && character.Stun <= 0.0f) { if (character.FocusedCharacter != null && character.FocusedCharacter.CanBeSelected) { @@ -309,7 +310,7 @@ namespace Barotrauma (int)(HUDLayoutSettings.BottomRightInfoArea.Y + HUDLayoutSettings.BottomRightInfoArea.Height * 0.1f), (int)(HUDLayoutSettings.BottomRightInfoArea.Width / 2), (int)(HUDLayoutSettings.BottomRightInfoArea.Height * 0.7f))); - character.Info.DrawPortrait(spriteBatch, HUDLayoutSettings.PortraitArea.Location.ToVector2(), new Vector2((int)(-4 * GUI.Scale), (int)(2 * GUI.Scale)), targetWidth: HUDLayoutSettings.PortraitArea.Width, true); + character.Info.DrawPortrait(spriteBatch, HUDLayoutSettings.PortraitArea.Location.ToVector2(), new Vector2(-12 * GUI.Scale, 4 * GUI.Scale), targetWidth: HUDLayoutSettings.PortraitArea.Width, true); } mouseOnPortrait = HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition) && !character.ShouldLockHud(); if (mouseOnPortrait) @@ -327,7 +328,7 @@ namespace Barotrauma } } - if (!character.IsUnconscious && character.Stun <= 0.0f) + if (!character.IsIncapacitated && character.Stun <= 0.0f) { if (character.IsHumanoid && character.SelectedCharacter != null && character.SelectedCharacter.Inventory != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 9758686d7..4a93fc785 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -10,12 +10,12 @@ namespace Barotrauma { partial class CharacterInfo { - public const float BgScale = 1.2f; private static Sprite infoAreaPortraitBG; public static void Init() { - infoAreaPortraitBG = new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(833, 298, 142, 98), null, 0); + infoAreaPortraitBG = GUI.Style.GetComponentStyle("InfoAreaPortraitBG")?.Sprites[GUIComponent.ComponentState.None][0].Sprite; + new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(833, 298, 142, 98), null, 0); } @@ -171,6 +171,7 @@ namespace Barotrauma public void DrawBackground(SpriteBatch spriteBatch) { + if (infoAreaPortraitBG == null) { return; } infoAreaPortraitBG.Draw(spriteBatch, HUDLayoutSettings.BottomRightInfoArea.Location.ToVector2(), Color.White, Vector2.Zero, 0.0f, scale: new Vector2( HUDLayoutSettings.BottomRightInfoArea.Width / (float)infoAreaPortraitBG.SourceRect.Width, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 28ca04741..5726215f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Linq; @@ -384,6 +385,37 @@ namespace Barotrauma character = Create(speciesName, position, seed, info, GameMain.Client.ID != ownerId, hasAi); character.ID = id; character.TeamID = (TeamType)teamID; + + // Check if the character has a current order + if (inc.ReadBoolean()) + { + int orderPrefabIndex = inc.ReadByte(); + Entity targetEntity = FindEntityByID(inc.ReadUInt16()); + Character orderGiver = inc.ReadBoolean() ? FindEntityByID(inc.ReadUInt16()) as Character : null; + int orderOptionIndex = inc.ReadByte(); + + if (orderPrefabIndex >= 0 && orderPrefabIndex < Order.PrefabList.Count) + { + var orderPrefab = Order.PrefabList[orderPrefabIndex]; + if ((orderPrefab.ItemComponentType == null && orderPrefab.ItemIdentifiers.None()) || + (targetEntity != null && (targetEntity as Item).Components.Any(c => c?.GetType() == orderPrefab.ItemComponentType))) + { + character.SetOrder( + new Order(orderPrefab, targetEntity, (targetEntity as Item)?.Components.FirstOrDefault(c => c?.GetType() == orderPrefab.ItemComponentType), orderGiver: orderGiver), + orderOptionIndex >= 0 && orderOptionIndex < orderPrefab.Options.Length ? orderPrefab.Options[orderOptionIndex] : null, + orderGiver, speak: false); + } + else + { + DebugConsole.ThrowError("Could not set order \"" + orderPrefab.Identifier + "\" for character \"" + character.Name + "\" because required target entity was not found."); + } + } + else + { + DebugConsole.ThrowError("Invalid order prefab index - index (" + orderPrefabIndex + ") out of bounds."); + } + } + bool containsStatusData = inc.ReadBoolean(); if (containsStatusData) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs index a096c3cc9..e93c6e2b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs @@ -14,8 +14,8 @@ namespace Barotrauma public SoundType Type => Params.State; public Gender Gender => Params.Gender; - public float Volume => roundSound.Volume; - public float Range => roundSound.Range; + public float Volume => roundSound == null ? 0.0f : roundSound.Volume; + public float Range => roundSound == null ? 0.0f : roundSound.Range; public Sound Sound => roundSound?.Sound; public CharacterSound(CharacterParams.SoundParams soundParams) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs index 11f03da17..1ba700e72 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs @@ -8,26 +8,25 @@ namespace Barotrauma { partial class AfflictionHusk : Affliction { - partial void UpdateMessages(float prevStrength, Character character) + partial void UpdateMessages() { - if (Strength < Prefab.MaxStrength * 0.5f) + switch (State) { - if (prevStrength % 10.0f > 0.05f && Strength % 10.0f < 0.05f) - { + case InfectionState.Dormant: GUI.AddMessage(TextManager.Get("HuskDormant"), GUI.Style.Red); - } - } - else if (Strength < Prefab.MaxStrength) - { - if (state == InfectionState.Dormant && Character.Controlled == character) - { + break; + case InfectionState.Transition: GUI.AddMessage(TextManager.Get("HuskCantSpeak"), GUI.Style.Red); - } - } - else if (state != InfectionState.Active && Character.Controlled == character) - { - GUI.AddMessage(TextManager.GetWithVariable("HuskActivate", "[Attack]", GameMain.Config.KeyBindText(InputType.Attack)), - GUI.Style.Red); + break; + case InfectionState.Active: + if (character.Params.UseHuskAppendage) + { + GUI.AddMessage(TextManager.GetWithVariable("HuskActivate", "[Attack]", GameMain.Config.KeyBindText(InputType.Attack)), GUI.Style.Red); + } + break; + case InfectionState.Final: + default: + break; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 360e81399..f2aaecdd4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -132,7 +132,7 @@ namespace Barotrauma private List heartratePositions; private float currentHeartrateTime; private float heartbeatTimer; - private Texture2D heartrateFade; + private static Texture2D heartrateFade; private readonly HeartratePosition[] heartbeatPattern = { @@ -246,11 +246,13 @@ namespace Barotrauma } private GUIFrame healthBarHolder; + private Point healthBarOffset { get { - return new Point(5 - (int)Math.Ceiling(1 - 1 * GUI.Scale), (int)Math.Min(Math.Ceiling(17 * GUI.Scale), 20)); + // 0.38775510204f = percentage of offset before reaching the healthbar portion of the graphic going from bottom upwards + return new Point(2, (int)(HUDLayoutSettings.HealthBarArea.Size.Y * 0.38775510204f)); } } @@ -258,7 +260,7 @@ namespace Barotrauma { get { - return new Point(healthBarHolder.Rect.Width - (int)Math.Ceiling(Math.Min(46 * GUI.Scale, 53)), (int)(healthBarHolder.Rect.Height - Math.Min(23 * GUI.Scale, 25)) / 2); + return new Point((int)Math.Ceiling(HUDLayoutSettings.HealthBarArea.Size.X - 45 * GUI.Scale), (int)(healthBarHolder.Rect.Height - Math.Min(23 * GUI.Scale, 25)) / 2); } } @@ -303,7 +305,7 @@ namespace Barotrauma healthShadowSize = 1.0f; healthBar = new GUIProgressBar(new RectTransform(healthBarSize, healthBarHolder.RectTransform, Anchor.BottomRight), - barSize: 1.0f, color: GUIColorSettings.HealthBarColorHigh, style: horizontal ? "CharacterHealthBarSlider" : "GUIProgressBarVertical", showFrame: false) + barSize: 1.0f, color: GUI.Style.HealthBarColorHigh, style: horizontal ? "CharacterHealthBarSlider" : "GUIProgressBarVertical", showFrame: false) { HoverCursor = CursorState.Hand, Enabled = true, @@ -478,7 +480,7 @@ namespace Barotrauma cprFrame = new GUIFrame(new RectTransform(new Vector2(0.7f, 1.0f), cprLayout.RectTransform), style: "GUIFrameListBox"); - heartrateFade = TextureLoader.FromFile("Content/UI/Health/HeartrateFade.png"); + heartrateFade ??= TextureLoader.FromFile("Content/UI/Health/HeartrateFade.png"); new GUICustomComponent(new RectTransform(Vector2.One * 0.95f, cprFrame.RectTransform, Anchor.Center), DrawHeartrate, UpdateHeartrate); @@ -520,8 +522,10 @@ namespace Barotrauma UpdateAlignment(); - suicideButton = new GUIButton(new RectTransform(new Vector2(0.06f, 0.02f), GUI.Canvas, Anchor.TopCenter) - { MinSize = new Point(150, 20), RelativeOffset = new Vector2(0.0f, 0.01f) }, + suicideButton = new GUIButton(new RectTransform(new Vector2(0.1f, 0.02f), GUI.Canvas, Anchor.TopCenter) + { + MinSize = new Point(150, 20), RelativeOffset = new Vector2(0.0f, 0.01f) + }, TextManager.Get("GiveInButton"), style: "GUIButtonLarge") { ToolTip = TextManager.Get(GameMain.NetworkMember == null ? "GiveInHelpSingleplayer" : "GiveInHelpMultiplayer"), @@ -816,9 +820,7 @@ namespace Barotrauma if (highlightedLimbIndex < 0 && selectedLimbIndex < 0) { // If no limb is selected or highlighted, select the one with the most critical afflictions. - var affliction = GetAllAfflictions(a => a.Prefab.IndicatorLimb != LimbType.None) - .OrderByDescending(a => a.DamagePerSecond) - .ThenByDescending(a => a.Strength).FirstOrDefault(); + var affliction = SortAfflictionsBySeverity(GetAllAfflictions(a => a.Prefab.IndicatorLimb != LimbType.None)).FirstOrDefault(); if (affliction.DamagePerSecond > 0 || affliction.Strength > 0) { var limbHealth = GetMatchingLimbHealth(affliction); @@ -849,7 +851,7 @@ namespace Barotrauma } else { - healthBar.Color = healthWindowHealthBar.Color = ToolBox.GradientLerp(DisplayedVitality / MaxVitality, GUIColorSettings.HealthBarColorLow, GUIColorSettings.HealthBarColorMedium, GUIColorSettings.HealthBarColorHigh); + healthBar.Color = healthWindowHealthBar.Color = ToolBox.GradientLerp(DisplayedVitality / MaxVitality, GUI.Style.HealthBarColorLow, GUI.Style.HealthBarColorMedium, GUI.Style.HealthBarColorHigh); healthBar.HoverColor = healthWindowHealthBar.HoverColor = healthBar.Color * 2.0f; healthBar.BarSize = healthWindowHealthBar.BarSize = (DisplayedVitality > 0.0f) ? @@ -936,7 +938,7 @@ namespace Barotrauma healthBar.State = GUIComponent.ComponentState.None; } - suicideButton.Visible = Character == Character.Controlled && Character.IsUnconscious && !Character.IsDead; + suicideButton.Visible = Character == Character.Controlled && !Character.IsDead && Character.IsIncapacitated; cprButton.Visible = Character == Character.Controlled?.SelectedCharacter @@ -1134,11 +1136,11 @@ namespace Barotrauma { if (prefab.IsBuff) { - return ToolBox.GradientLerp(affliction.Strength / prefab.MaxStrength, GUIColorSettings.BuffColorLow, GUIColorSettings.BuffColorMedium, GUIColorSettings.BuffColorHigh); + return ToolBox.GradientLerp(affliction.Strength / prefab.MaxStrength, GUI.Style.BuffColorLow, GUI.Style.BuffColorMedium, GUI.Style.BuffColorHigh); } else { - return ToolBox.GradientLerp(affliction.Strength / prefab.MaxStrength, GUIColorSettings.DebuffColorLow, GUIColorSettings.DebuffColorMedium, GUIColorSettings.DebuffColorHigh); + return ToolBox.GradientLerp(affliction.Strength / prefab.MaxStrength, GUI.Style.DebuffColorLow, GUI.Style.DebuffColorMedium, GUI.Style.DebuffColorHigh); } } else @@ -1185,7 +1187,8 @@ namespace Barotrauma Dictionary treatmentSuitability = new Dictionary(); GetSuitableTreatments(treatmentSuitability, normalize: true, randomization: randomVariance); - Affliction mostSevereAffliction = afflictions.FirstOrDefault(a => !a.Prefab.IsBuff && !afflictions.Any(a2 => !a2.Prefab.IsBuff && a2.Strength > a.Strength)) ?? afflictions.FirstOrDefault(); + //Affliction mostSevereAffliction = afflictions.FirstOrDefault(a => !a.Prefab.IsBuff && !afflictions.Any(a2 => !a2.Prefab.IsBuff && a2.Strength > a.Strength)) ?? afflictions.FirstOrDefault(); + Affliction mostSevereAffliction = SortAfflictionsBySeverity(afflictions).FirstOrDefault(); GUIButton buttonToSelect = null; foreach (Affliction affliction in afflictions) @@ -1811,8 +1814,8 @@ namespace Barotrauma float iconScale = 0.25f * scale; Vector2 iconPos = highlightArea.Center.ToVector2(); - Affliction mostSevereAffliction = thisAfflictions.FirstOrDefault(a => !a.Prefab.IsBuff && !thisAfflictions.Any(a2 => !a2.Prefab.IsBuff && a2.Strength > a.Strength)) ?? thisAfflictions.FirstOrDefault(); - + //Affliction mostSevereAffliction = thisAfflictions.FirstOrDefault(a => !a.Prefab.IsBuff && !thisAfflictions.Any(a2 => !a2.Prefab.IsBuff && a2.Strength > a.Strength)) ?? thisAfflictions.FirstOrDefault(); + Affliction mostSevereAffliction = SortAfflictionsBySeverity(thisAfflictions).FirstOrDefault(); if (mostSevereAffliction != null) { DrawLimbAfflictionIcon(spriteBatch, mostSevereAffliction, iconScale, ref iconPos); } if (thisAfflictions.Count() > 1) @@ -1871,10 +1874,11 @@ namespace Barotrauma healthBarHolder.Visible = value; } + private readonly List> newAfflictions = new List>(); + private readonly List> newLimbAfflictions = new List>(); public void ClientRead(IReadMessage inc) { - List> newAfflictions = new List>(); - + newAfflictions.Clear(); byte afflictionCount = inc.ReadByte(); for (int i = 0; i < afflictionCount; i++) { @@ -1914,7 +1918,7 @@ namespace Barotrauma } } - List> newLimbAfflictions = new List>(); + newLimbAfflictions.Clear(); byte limbAfflictionCount = inc.ReadByte(); for (int i = 0; i < limbAfflictionCount; i++) { @@ -1992,6 +1996,9 @@ namespace Barotrauma } } + medUIExtra?.Remove(); + medUIExtra = null; + limbIndicatorOverlay?.Remove(); limbIndicatorOverlay = null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 021685c5d..fea4134be 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -83,22 +83,22 @@ namespace Barotrauma // pos.Y = -pos.Y; // ShapeExtensions.DrawPoint(spriteBatch, pos, GUI.Style.Red, size: 5); //} - return; + // A debug visualisation on the bezier curve between limbs. - var start = LimbA.WorldPosition; + /*var start = LimbA.WorldPosition; var end = LimbB.WorldPosition; var jointAPos = ConvertUnits.ToDisplayUnits(LocalAnchorA); var control = start + Vector2.Transform(jointAPos, Matrix.CreateRotationZ(LimbA.Rotation)); start.Y = -start.Y; end.Y = -end.Y; control.Y = -control.Y; - //GUI.DrawRectangle(spriteBatch, start, Vector2.One * 5, Color.White, true); - //GUI.DrawRectangle(spriteBatch, end, Vector2.One * 5, Color.Black, true); - //GUI.DrawRectangle(spriteBatch, control, Vector2.One * 5, Color.Black, true); - //GUI.DrawLine(spriteBatch, start, end, Color.White); - //GUI.DrawLine(spriteBatch, start, control, Color.Black); - //GUI.DrawLine(spriteBatch, control, end, Color.Black); - GUI.DrawBezierWithDots(spriteBatch, start, end, control, 1000, GUI.Style.Red); + GUI.DrawRectangle(spriteBatch, start, Vector2.One * 5, Color.White, true); + GUI.DrawRectangle(spriteBatch, end, Vector2.One * 5, Color.Black, true); + GUI.DrawRectangle(spriteBatch, control, Vector2.One * 5, Color.Black, true); + GUI.DrawLine(spriteBatch, start, end, Color.White); + GUI.DrawLine(spriteBatch, start, control, Color.Black); + GUI.DrawLine(spriteBatch, control, end, Color.Black); + GUI.DrawBezierWithDots(spriteBatch, start, end, control, 1000, GUI.Style.Red);*/ } } @@ -220,9 +220,18 @@ namespace Barotrauma } set { - if (HuskSprite != null && value != enableHuskSprite) + if (enableHuskSprite == value) { return; } + enableHuskSprite = value; + if (enableHuskSprite) { - if (value) + if (HuskSprite == null) + { + LoadHuskSprite(); + } + } + if (HuskSprite != null) + { + if (enableHuskSprite) { List otherWearablesWithHusk = new List() { HuskSprite }; otherWearablesWithHusk.AddRange(OtherWearables); @@ -235,7 +244,6 @@ namespace Barotrauma UpdateWearableTypesToHide(); } } - enableHuskSprite = value; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 70b15fe92..fd4394742 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -195,6 +195,34 @@ namespace Barotrauma } } + private static bool IsCommandPermitted(string command, GameClient client) + { + switch (command) + { + case "kick": + return client.HasPermission(ClientPermissions.Kick); + case "ban": + case "banip": + case "banendpoint": + return client.HasPermission(ClientPermissions.Ban); + case "unban": + case "unbanip": + return client.HasPermission(ClientPermissions.Unban); + case "netstats": + case "help": + case "dumpids": + case "admin": + case "entitylist": + case "togglehud": + case "toggleupperhud": + case "togglecharacternames": + case "fpscounter": + return true; + default: + return client.HasConsoleCommandPermission(command); + } + } + public static void DequeueMessages() { while (queuedMessages.Count > 0) @@ -367,6 +395,7 @@ namespace Barotrauma NewMessage("Steam achievements have been disabled during this play session.", Color.Red); #endif })); + AssignRelayToServer("enablecheats", true); commands.Add(new Command("mainmenu|menu", "mainmenu/menu: Go to the main menu.", (string[] args) => { @@ -394,7 +423,8 @@ namespace Barotrauma { if (args.Length > 0) { - Submarine.Load(string.Join(" ", args), true); + var subInfo = new SubmarineInfo(string.Join(" ", args)); + Submarine.MainSub = Submarine.Load(subInfo, true); } GameMain.SubEditorScreen.Select(); }, isCheat: true)); @@ -440,6 +470,7 @@ namespace Barotrauma AssignRelayToServer("ban", false); AssignRelayToServer("banid", false); AssignRelayToServer("dumpids", false); + AssignRelayToServer("dumptofile", false); AssignRelayToServer("findentityids", false); AssignRelayToServer("campaigninfo", false); AssignRelayToServer("help", false); @@ -805,7 +836,7 @@ namespace Barotrauma return; } - if (Submarine.SaveCurrent(System.IO.Path.Combine(Submarine.SavePath, fileName + ".sub"))) + if (Submarine.MainSub.SaveAs(System.IO.Path.Combine(SubmarineInfo.SavePath, fileName + ".sub"))) { NewMessage("Sub saved", Color.Green); } @@ -814,7 +845,8 @@ namespace Barotrauma commands.Add(new Command("load|loadsub", "load [submarine name]: Load a submarine.", (string[] args) => { if (args.Length == 0) return; - Submarine.Load(string.Join(" ", args), true); + SubmarineInfo subInfo = new SubmarineInfo(string.Join(" ", args)); + Submarine.Load(subInfo, true); })); commands.Add(new Command("cleansub", "", (string[] args) => @@ -1057,23 +1089,69 @@ namespace Barotrauma if (args.Length != 2 || Screen.Selected != GameMain.SubEditorScreen) { return; } foreach (MapEntity me in MapEntity.SelectedList) { - if (me is ISerializableEntity serializableEntity) + bool propertyFound = false; + if (!(me is ISerializableEntity serializableEntity)) { continue; } + if (serializableEntity.SerializableProperties == null) { continue; } + + if (serializableEntity.SerializableProperties.TryGetValue(args[0].ToLowerInvariant(), out SerializableProperty property)) { - if (serializableEntity.SerializableProperties == null) + propertyFound = true; + object prevValue = property.GetValue(me); + if (property.TrySetValue(me, args[1])) { - continue; + NewMessage($"Changed the value \"{args[0]}\" from {(prevValue?.ToString() ?? null)} to {args[1]} on entity \"{me.ToString()}\".", Color.LightGreen); } - if (!serializableEntity.SerializableProperties.TryGetValue(args[0].ToLowerInvariant(), out SerializableProperty property)) + else { - NewMessage("Property \"" + args[0] + "\" not found in the entity \"" + me.ToString() + "\".", Color.Orange); - continue; + NewMessage($"Failed to set the value of \"{args[0]}\" to \"{args[1]}\" on the entity \"{me.ToString()}\".", Color.Orange); } - if (!property.TrySetValue(me, args[1])) + } + if (me is Item item) + { + foreach (ItemComponent ic in item.Components) { - NewMessage("Failed to set the value of \"" + args[0] + "\" to \"" + args[1] + "\" on the entity \"" + me.ToString() + "\".", Color.Orange); + ic.SerializableProperties.TryGetValue(args[0].ToLowerInvariant(), out SerializableProperty componentProperty); + if (componentProperty == null) { continue; } + propertyFound = true; + object prevValue = componentProperty.GetValue(ic); + if (componentProperty.TrySetValue(ic, args[1])) + { + NewMessage($"Changed the value \"{args[0]}\" from {prevValue} to {args[1]} on item \"{me.ToString()}\", component \"{ic.GetType().Name}\".", Color.LightGreen); + } + else + { + NewMessage($"Failed to set the value of \"{args[0]}\" to \"{args[1]}\" on the item \"{me.ToString()}\", component \"{ic.GetType().Name}\".", Color.Orange); + } + } + } + if (!propertyFound) + { + NewMessage($"Property \"{args[0]}\" not found in the entity \"{me.ToString()}\".", Color.Orange); + } + } + }, + () => + { + List propertyList = new List(); + foreach (MapEntity me in MapEntity.SelectedList) + { + if (!(me is ISerializableEntity serializableEntity)) { continue; } + if (serializableEntity.SerializableProperties == null) { continue; } + propertyList.AddRange(serializableEntity.SerializableProperties.Select(p => p.Key)); + if (me is Item item) + { + foreach (ItemComponent ic in item.Components) + { + propertyList.AddRange(ic.SerializableProperties.Select(p => p.Key)); } } } + + return new string[][] + { + propertyList.Distinct().ToArray(), + new string[0] + }; })); commands.Add(new Command("checkmissingloca", "", (string[] args) => @@ -1106,7 +1184,7 @@ namespace Barotrauma } } - foreach (Submarine sub in Submarine.SavedSubmarines) + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { string nameIdentifier = "submarine.name." + sub.Name.ToLowerInvariant(); if (!tags[language].Contains(nameIdentifier)) @@ -2113,6 +2191,11 @@ namespace Barotrauma commands.Add(new Command("flipx", "flipx: mirror the main submarine horizontally", (string[] args) => { + if (GameMain.NetworkMember != null) + { + ThrowError("Cannot use the flipx command while playing online."); + return; + } Submarine.MainSub?.FlipX(); }, isCheat: true)); @@ -2235,7 +2318,8 @@ namespace Barotrauma } try { - Submarine spawnedSub = Submarine.Load(args[0], false); + SubmarineInfo subInfo = new SubmarineInfo(args[0]); + Submarine spawnedSub = Submarine.Load(subInfo, false); spawnedSub.SetPosition(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition)); } catch (Exception e) @@ -2313,7 +2397,7 @@ namespace Barotrauma switch (firstArg) { case "name": - var sprites = Sprite.LoadedSprites.Where(s => s.Name?.ToLowerInvariant() == secondArg.ToLowerInvariant()); + var sprites = Sprite.LoadedSprites.Where(s => s.Name != null && s.Name.Equals(secondArg, StringComparison.OrdinalIgnoreCase)); if (sprites.Any()) { foreach (var s in sprites) @@ -2329,7 +2413,7 @@ namespace Barotrauma } case "identifier": case "id": - sprites = Sprite.LoadedSprites.Where(s => s.EntityID?.ToLowerInvariant() == secondArg.ToLowerInvariant()); + sprites = Sprite.LoadedSprites.Where(s => s.EntityID != null && s.EntityID.Equals(secondArg, StringComparison.OrdinalIgnoreCase)); if (sprites.Any()) { foreach (var s in sprites) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs index 58d480b8a..878c814b7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs @@ -24,7 +24,7 @@ namespace Barotrauma { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "icon") { continue; } + if (!subElement.Name.ToString().Equals("icon", StringComparison.OrdinalIgnoreCase)) { continue; } Icon = new Sprite(subElement); IconColor = subElement.GetAttributeColor("color", Color.White); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs index b455fea48..8dd4e5862 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs @@ -7,10 +7,23 @@ namespace Barotrauma { public override void ClientReadInitial(IReadMessage msg) { - item = Item.ReadSpawnData(msg); - if (item == null) + bool usedExistingItem = msg.ReadBoolean(); + if (usedExistingItem) { - throw new System.Exception("Error in SalvageMission.ClientReadInitial: spawned item was null (mission: " + Prefab.Identifier + ")"); + ushort id = msg.ReadUInt16(); + item = Entity.FindEntityByID(id) as Item; + if (item == null) + { + throw new System.Exception("Error in SalvageMission.ClientReadInitial: failed to find item " + id + " (mission: " + Prefab.Identifier + ")"); + } + } + else + { + item = Item.ReadSpawnData(msg); + if (item == null) + { + throw new System.Exception("Error in SalvageMission.ClientReadInitial: spawned item was null (mission: " + Prefab.Identifier + ")"); + } } item.body.FarseerBody.BodyType = BodyType.Kinematic; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index d3e45860b..39494a661 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -135,31 +135,33 @@ namespace Barotrauma } } + Color textColor = textBox.Color; switch (command) { case "r": case "radio": - textBox.TextColor = ChatMessage.MessageColor[(int)ChatMessageType.Radio]; + textColor = ChatMessage.MessageColor[(int)ChatMessageType.Radio]; break; case "d": case "dead": - textBox.TextColor = ChatMessage.MessageColor[(int)ChatMessageType.Dead]; + textColor = ChatMessage.MessageColor[(int)ChatMessageType.Dead]; break; default: if (Character.Controlled != null && (Character.Controlled.IsDead || Character.Controlled.SpeechImpediment >= 100.0f)) { - textBox.TextColor = ChatMessage.MessageColor[(int)ChatMessageType.Dead]; + textColor = ChatMessage.MessageColor[(int)ChatMessageType.Dead]; } else if (command != "") //PMing { - textBox.TextColor = ChatMessage.MessageColor[(int)ChatMessageType.Private]; + textColor = ChatMessage.MessageColor[(int)ChatMessageType.Private]; } else { - textBox.TextColor = ChatMessage.MessageColor[(int)ChatMessageType.Default]; + textColor = ChatMessage.MessageColor[(int)ChatMessageType.Default]; } break; } + textBox.TextColor = textBox.TextBlock.SelectedTextColor = textColor; return true; } @@ -252,21 +254,29 @@ namespace Barotrauma Visible = false, CanBeFocused = false }; - var senderText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), popupMsg.RectTransform, Anchor.TopRight), - senderName, textColor: senderColor, font: GUI.SmallFont, textAlignment: Alignment.TopRight) + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), popupMsg.RectTransform, Anchor.Center)); + Vector2 senderTextSize = Vector2.Zero; + if (!string.IsNullOrEmpty(senderName)) + { + var senderText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), + senderName, textColor: senderColor, style: null, font: GUI.SmallFont) + { + CanBeFocused = false + }; + senderTextSize = senderText.Font.MeasureString(senderText.WrappedText); + senderText.RectTransform.MinSize = new Point(0, senderText.Rect.Height); + } + var msgPopupText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), + displayedText, textColor: message.Color, font: GUI.SmallFont, textAlignment: Alignment.BottomLeft, style: null, wrap: true) { CanBeFocused = false }; - var msgPopupText = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.0f), popupMsg.RectTransform, Anchor.TopRight) - { AbsoluteOffset = new Point(0, senderText.Rect.Height) }, - displayedText, textColor: message.Color, font: GUI.SmallFont, textAlignment: Alignment.TopRight, style: null, wrap: true) - { - CanBeFocused = false - }; - int textWidth = (int)Math.Max( - msgPopupText.Font.MeasureString(msgPopupText.WrappedText).X, - senderText.Font.MeasureString(senderText.WrappedText).X); - popupMsg.RectTransform.Resize(new Point(textWidth + 20, msgPopupText.Rect.Bottom - senderText.Rect.Y), resizeChildren: false); + msgPopupText.RectTransform.MinSize = new Point(0, msgPopupText.Rect.Height); + Vector2 msgSize = msgPopupText.Font.MeasureString(msgPopupText.WrappedText); + int textWidth = (int)Math.Max(msgSize.X + msgPopupText.Padding.X + msgPopupText.Padding.Z, senderTextSize.X) + 10; + popupMsg.RectTransform.Resize(new Point((int)(textWidth / content.RectTransform.RelativeSize.X) , (int)((senderTextSize.Y + msgSize.Y) / content.RectTransform.RelativeSize.Y)), resizeChildren: true); + popupMsg.RectTransform.IsFixedSize = true; + content.Recalculate(); popupMessages.Enqueue(popupMsg); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs index ec39f671c..9069091ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs @@ -5,16 +5,6 @@ using System.Xml.Linq; namespace Barotrauma { - public enum TransitionMode - { - Linear, - Smooth, - Smoother, - EaseIn, - EaseOut, - Exponential - } - public enum SpriteFallBackState { None, @@ -159,7 +149,7 @@ namespace Barotrauma Point size = new Point(0, 0); foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "size") { continue; } + if (!subElement.Name.ToString().Equals("size", StringComparison.OrdinalIgnoreCase)) { continue; } Point maxResolution = subElement.GetAttributePoint("maxresolution", new Point(int.MaxValue, int.MaxValue)); if (GameMain.GraphicsWidth <= maxResolution.X && GameMain.GraphicsHeight <= maxResolution.Y) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index e03665277..5e3cba69a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -26,7 +26,8 @@ namespace Barotrauma Click, PickItem, PickItemFail, - DropItem + DropItem, + PopupMenu } public enum CursorState @@ -242,6 +243,7 @@ namespace Barotrauma 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); @@ -1922,6 +1924,19 @@ namespace Barotrauma return true; }; } + else if (GameMain.GameSession.GameMode is SubTestMode) + { + button = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), text: TextManager.Get("PauseMenuReturnToEditor")) + { + OnClicked = (btn, userdata) => + { + GameMain.GameSession.GameMode.End(""); + + return true; + } + }; + button.OnClicked += TogglePauseMenu; + } else if (!GameMain.GameSession.GameMode.IsSinglePlayer && GameMain.Client != null && GameMain.Client.HasPermission(ClientPermissions.ManageRound)) { new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), text: TextManager.Get("EndRound")) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index 77bb51dc1..d733439b0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -524,23 +524,9 @@ namespace Barotrauma }; } - private float GetEasing(TransitionMode easing, float t) - { - return easing switch - { - TransitionMode.Smooth => MathUtils.SmoothStep(t), - TransitionMode.Smoother => MathUtils.SmootherStep(t), - TransitionMode.EaseIn => MathUtils.EaseIn(t), - TransitionMode.EaseOut => MathUtils.EaseOut(t), - TransitionMode.Exponential => t * t, - TransitionMode.Linear => t, - _ => t, - }; - } - protected Color GetBlendedColor(Color targetColor, ref Color blendedColor) { - blendedColor = ColorCrossFadeTime > 0 ? Color.Lerp(blendedColor, targetColor, MathUtils.InverseLerp(ColorCrossFadeTime, 0, GetEasing(ColorTransition, colorFadeTimer))) : targetColor; + blendedColor = ColorCrossFadeTime > 0 ? Color.Lerp(blendedColor, targetColor, MathUtils.InverseLerp(ColorCrossFadeTime, 0, ToolBox.GetEasing(ColorTransition, colorFadeTimer))) : targetColor; return blendedColor; } @@ -598,7 +584,7 @@ namespace Barotrauma foreach (UISprite uiSprite in previousSprites) { float alphaMultiplier = SpriteCrossFadeTime > 0 && (uiSprite.CrossFadeOut || currentSprites != null && currentSprites.Any(s => s.CrossFadeIn)) - ? MathUtils.InverseLerp(0, SpriteCrossFadeTime, GetEasing(uiSprite.TransitionMode, spriteFadeTimer)) : 0; + ? MathUtils.InverseLerp(0, SpriteCrossFadeTime, ToolBox.GetEasing(uiSprite.TransitionMode, spriteFadeTimer)) : 0; if (alphaMultiplier > 0) { uiSprite.Draw(spriteBatch, rect, previousColor * alphaMultiplier, SpriteEffects); @@ -612,7 +598,7 @@ namespace Barotrauma foreach (UISprite uiSprite in currentSprites) { float alphaMultiplier = SpriteCrossFadeTime > 0 && (uiSprite.CrossFadeIn || previousSprites != null && previousSprites.Any(s => s.CrossFadeOut)) - ? MathUtils.InverseLerp(SpriteCrossFadeTime, 0, GetEasing(uiSprite.TransitionMode, spriteFadeTimer)) : (_currentColor.A / 255.0f); + ? MathUtils.InverseLerp(SpriteCrossFadeTime, 0, ToolBox.GetEasing(uiSprite.TransitionMode, spriteFadeTimer)) : (_currentColor.A / 255.0f); if (alphaMultiplier > 0) { uiSprite.Draw(spriteBatch, rect, _currentColor * alphaMultiplier, SpriteEffects); @@ -801,8 +787,7 @@ namespace Barotrauma foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "conditional" && - !CheckConditional(subElement)) + if (subElement.Name.ToString().Equals("conditional", StringComparison.OrdinalIgnoreCase) && !CheckConditional(subElement)) { return null; } @@ -851,7 +836,7 @@ namespace Barotrauma { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "conditional") { continue; } + if (subElement.Name.ToString().Equals("conditional", StringComparison.OrdinalIgnoreCase)) { continue; } FromXML(subElement, component is GUIListBox listBox ? listBox.Content.RectTransform : component.RectTransform); } @@ -1019,7 +1004,7 @@ namespace Barotrauma private static GUIFrame LoadGUIFrame(XElement element, RectTransform parent) { - string style = element.GetAttributeString("style", element.Name.ToString().ToLowerInvariant() == "spacing" ? null : ""); + string style = element.GetAttributeString("style", element.Name.ToString().Equals("spacing", StringComparison.OrdinalIgnoreCase) ? null : ""); if (style == "null") { style = null; } return new GUIFrame(RectTransform.Load(element, parent), style: style); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index 0169c7bf5..f4b41a3e8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -95,6 +95,15 @@ namespace Barotrauma set { button.TextColor = value; } } + public override ScalableFont Font + { + get { return button?.Font ?? base.Font; } + set + { + if (button != null) { button.Font = value; } + } + } + public void ReceiveTextInput(char inputChar) { GUI.KeyboardDispatcher.Subscriber = null; @@ -168,12 +177,11 @@ namespace Barotrauma Anchor listAnchor = dropAbove ? Anchor.TopCenter : Anchor.BottomCenter; Pivot listPivot = dropAbove ? Pivot.BottomCenter : Pivot.TopCenter; listBox = new GUIListBox(new RectTransform(new Point(Rect.Width, Rect.Height * MathHelper.Clamp(elementCount, 2, 10)), rectT, listAnchor, listPivot) - { IsFixedSize = false }, style: null) { - Enabled = !selectMultiple, - OnSelected = SelectItem + Enabled = !selectMultiple }; + if (!selectMultiple) { listBox.OnSelected = SelectItem; } GUI.Style.Apply(listBox, "GUIListBox", this); GUI.Style.Apply(listBox.ContentBackground, "GUIListBox", this); @@ -245,7 +253,7 @@ namespace Barotrauma ToolTip = toolTip }; - new GUITickBox(new RectTransform(new Point((int)(button.Rect.Height * 0.8f)), frame.RectTransform, anchor: Anchor.CenterLeft), text) + new GUITickBox(new RectTransform(new Vector2(1.0f, 0.8f), frame.RectTransform, anchor: Anchor.CenterLeft) { MaxSize = new Point(int.MaxValue, (int)(button.Rect.Height * 0.8f)) }, text) { UserData = userData, ToolTip = toolTip, diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index 96191e3be..01e36a545 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -59,11 +59,36 @@ namespace Barotrauma /// public Color Blue { get; private set; } = Color.Blue; + public Color ColorInventoryEmpty { get; private set; } = Color.Red; + public Color ColorInventoryHalf { get; private set; } = Color.Orange; + public Color ColorInventoryFull { get; private set; } = Color.LightGreen; + public Color ColorInventoryBackground { get; private set; } = Color.Gray; + public Color TextColor { get; private set; } = Color.White * 0.8f; public Color TextColorBright { get; private set; } = Color.White * 0.9f; public Color TextColorDark { get; private set; } = Color.Black * 0.9f; public Color TextColorDim { get; private set; } = Color.White * 0.6f; + // Inventory + public Color EquipmentSlotIconColor { get; private set; } = new Color(99, 70, 64); + + // Health HUD + public Color BuffColorLow { get; private set; } = Color.LightGreen; + public Color BuffColorMedium { get; private set; } = Color.Green; + public Color BuffColorHigh { get; private set; } = Color.DarkGreen; + + public Color DebuffColorLow { get; private set; } = Color.DarkSalmon; + public Color DebuffColorMedium { get; private set; } = Color.Red; + public Color DebuffColorHigh { get; private set; } = Color.DarkRed; + + public Color HealthBarColorLow { get; private set; } = Color.Red; + public Color HealthBarColorMedium { get; private set; } = Color.Orange; + public Color HealthBarColorHigh { get; private set; } = new Color(78, 114, 88); + + public Color EquipmentIndicatorNotEquipped { get; private set; } = Color.Gray; + public Color EquipmentIndicatorEquipped { get; private set; } = new Color(105, 202, 125); + public Color EquipmentIndicatorRunningOut { get; private set; } = new Color(202, 105, 105); + public static Point ItemFrameMargin => new Point(50, 56).Multiply(GUI.SlicedSpriteScale); public static Point ItemFrameOffset => new Point(0, 3).Multiply(GUI.SlicedSpriteScale); @@ -103,10 +128,22 @@ namespace Barotrauma case "blue": Blue = subElement.GetAttributeColor("color", Blue); break; + case "colorinventoryempty": + ColorInventoryEmpty = subElement.GetAttributeColor("color", ColorInventoryEmpty); + break; + case "colorinventoryhalf": + ColorInventoryHalf = subElement.GetAttributeColor("color", ColorInventoryHalf); + break; + case "colorinventoryfull": + ColorInventoryFull = subElement.GetAttributeColor("color", ColorInventoryFull); + break; + case "colorinventorybackground": + ColorInventoryBackground = subElement.GetAttributeColor("color", ColorInventoryBackground); + break; case "textcolordark": TextColorDark = subElement.GetAttributeColor("color", TextColorDark); break; - case "TextColorBright": + case "textcolorbright": TextColorBright = subElement.GetAttributeColor("color", TextColorBright); break; case "textcolordim": @@ -116,6 +153,45 @@ namespace Barotrauma case "textcolor": TextColor = subElement.GetAttributeColor("color", TextColor); break; + case "equipmentsloticoncolor": + EquipmentSlotIconColor = subElement.GetAttributeColor("color", EquipmentSlotIconColor); + break; + case "buffcolorlow": + BuffColorLow = subElement.GetAttributeColor("color", BuffColorLow); + break; + case "buffcolormedium": + BuffColorMedium = subElement.GetAttributeColor("color", BuffColorMedium); + break; + case "buffcolorhigh": + BuffColorHigh = subElement.GetAttributeColor("color", BuffColorHigh); + break; + case "debuffcolorlow": + DebuffColorLow = subElement.GetAttributeColor("color", DebuffColorLow); + break; + case "debuffcolormedium": + DebuffColorMedium = subElement.GetAttributeColor("color", DebuffColorMedium); + break; + case "debuffcolorhigh": + DebuffColorHigh = subElement.GetAttributeColor("color", DebuffColorHigh); + break; + case "healthbarcolorlow": + HealthBarColorLow = subElement.GetAttributeColor("color", HealthBarColorLow); + break; + case "healthbarcolormedium": + HealthBarColorMedium = subElement.GetAttributeColor("color", HealthBarColorMedium); + break; + case "healthbarcolorhigh": + HealthBarColorHigh = subElement.GetAttributeColor("color", HealthBarColorHigh); + break; + case "equipmentindicatornotequipped": + EquipmentIndicatorNotEquipped = subElement.GetAttributeColor("color", EquipmentIndicatorNotEquipped); + break; + case "equipmentindicatorequipped": + EquipmentIndicatorEquipped = subElement.GetAttributeColor("color", EquipmentIndicatorEquipped); + break; + case "equipmentindicatorrunningout": + EquipmentIndicatorRunningOut = subElement.GetAttributeColor("color", EquipmentIndicatorRunningOut); + break; case "uiglow": UIGlow = new UISprite(subElement); break; @@ -250,9 +326,8 @@ namespace Barotrauma //check if any of the language override fonts want to override the font size as well foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "override") { continue; } - string language = subElement.GetAttributeString("language", "").ToLowerInvariant(); - if (GameMain.Config.Language.ToLowerInvariant() == language) + if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } + if (GameMain.Config.Language.Equals(subElement.GetAttributeString("language", ""), StringComparison.OrdinalIgnoreCase)) { uint overrideFontSize = GetFontSize(subElement, 0); if (overrideFontSize > 0) { return overrideFontSize; } @@ -261,7 +336,7 @@ namespace Barotrauma foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "size") { continue; } + if (!subElement.Name.ToString().Equals("size", StringComparison.OrdinalIgnoreCase)) { continue; } Point maxResolution = subElement.GetAttributePoint("maxresolution", new Point(int.MaxValue, int.MaxValue)); if (GameMain.GraphicsWidth <= maxResolution.X && GameMain.GraphicsHeight <= maxResolution.Y) { @@ -275,9 +350,8 @@ namespace Barotrauma { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "override") { continue; } - string language = subElement.GetAttributeString("language", "").ToLowerInvariant(); - if (GameMain.Config.Language.ToLowerInvariant() == language) + if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } + if (GameMain.Config.Language.Equals(subElement.GetAttributeString("language", ""), StringComparison.OrdinalIgnoreCase)) { return subElement.GetAttributeString("file", ""); } @@ -289,9 +363,8 @@ namespace Barotrauma { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "override") { continue; } - string language = subElement.GetAttributeString("language", "").ToLowerInvariant(); - if (GameMain.Config.Language.ToLowerInvariant() == language) + if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } + if (GameMain.Config.Language.Equals(subElement.GetAttributeString("language", ""), StringComparison.OrdinalIgnoreCase)) { return subElement.GetAttributeBool("dynamicloading", false); } @@ -303,9 +376,8 @@ namespace Barotrauma { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "override") { continue; } - string language = subElement.GetAttributeString("language", "").ToLowerInvariant(); - if (GameMain.Config.Language.ToLowerInvariant() == language) + if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } + if (GameMain.Config.Language.Equals(subElement.GetAttributeString("language", ""), StringComparison.OrdinalIgnoreCase)) { return subElement.GetAttributeBool("iscjk", false); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index 37fd18873..73daf81f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -328,11 +328,7 @@ namespace Barotrauma { if (text == null) { return; } - censoredText = ""; - for (int i = 0; i < text.Length; i++) - { - censoredText += "\u2022"; - } + censoredText = string.IsNullOrEmpty(text) ? "" : new string('\u2022', text.Length); var rect = Rect; @@ -446,9 +442,10 @@ namespace Barotrauma Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; if (overflowClipActive) { - spriteBatch.End(); Rectangle scissorRect = new Rectangle(rect.X + (int)padding.X, rect.Y, rect.Width - (int)padding.X - (int)padding.Z, rect.Height); - spriteBatch.GraphicsDevice.ScissorRectangle = scissorRect; + if (!scissorRect.Intersects(prevScissorRect)) { return; } + spriteBatch.End(); + spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, scissorRect); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index c2aacfb6e..2b67a0674 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -69,6 +69,11 @@ namespace Barotrauma private readonly Memento memento = new Memento(); + public GUIFrame Frame + { + get { return frame; } + } + public GUITextBlock.TextGetterHandler TextGetter { get { return textBlock.TextGetter; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index a67480d28..8df88614e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -112,16 +112,16 @@ namespace Barotrauma //slice from the top of the screen for misc buttons (info, end round, server controls) ButtonAreaTop = new Rectangle(Padding, Padding, GameMain.GraphicsWidth - Padding * 2, (int)(50 * GUI.Scale)); - int infoAreaWidth = (int)(142 * GUI.Scale * CharacterInfo.BgScale); - int infoAreaHeight = (int)(98 * GUI.Scale * CharacterInfo.BgScale); - int portraitSize = (int)(125 * GUI.Scale); + int infoAreaWidth = (int)(142 * GUI.Scale); + int infoAreaHeight = (int)(98 * GUI.Scale); + int portraitSize = (int)(infoAreaHeight * 0.95f); BottomRightInfoArea = new Rectangle(GameMain.GraphicsWidth - Padding * 2 - infoAreaWidth, GameMain.GraphicsHeight - Padding * 2 - infoAreaHeight, infoAreaWidth, infoAreaHeight); - PortraitArea = new Rectangle(GameMain.GraphicsWidth - Padding - portraitSize, GameMain.GraphicsHeight - Padding - portraitSize, portraitSize, portraitSize); + PortraitArea = new Rectangle(GameMain.GraphicsWidth - portraitSize, BottomRightInfoArea.Bottom - portraitSize + Padding / 2, portraitSize, portraitSize); //horizontal slices at the corners of the screen for health bar and affliction icons int afflictionAreaHeight = (int)(50 * GUI.Scale); - int healthBarWidth = (int)((BottomRightInfoArea.Width + CharacterInventory.SlotSize.X + CharacterInventory.Spacing) * 1.1f); - int healthBarHeight = (int)Math.Max(50f * GUI.Scale, 25f); + int healthBarWidth = BottomRightInfoArea.Width + CharacterInventory.SlotSize.X + CharacterInventory.Spacing * 2 + CharacterInventory.HideButtonWidth; + int healthBarHeight = (int)(50f * GUI.Scale); HealthBarArea = new Rectangle(BottomRightInfoArea.X - (healthBarWidth - BottomRightInfoArea.Width) + (int)(2 * GUI.Scale), BottomRightInfoArea.Y - healthBarHeight + (int)(10 * GUI.Scale), healthBarWidth, healthBarHeight); AfflictionAreaLeft = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 8d86b74b2..03351a772 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -66,7 +66,7 @@ namespace Barotrauma if (vanillaContent == null) { // TODO: Dynamic method for defining and finding the vanilla content package. - vanillaContent = ContentPackage.List.SingleOrDefault(cp => Path.GetFileName(cp.Path).ToLowerInvariant() == "vanilla 0.9.xml"); + vanillaContent = ContentPackage.List.SingleOrDefault(cp => Path.GetFileName(cp.Path).Equals("vanilla 0.9.xml", StringComparison.OrdinalIgnoreCase)); } return vanillaContent; } @@ -189,6 +189,11 @@ namespace Barotrauma Instance = this; + if (!Directory.Exists(Content.RootDirectory)) + { + throw new Exception("Content folder not found. If you are trying to compile the game from the source code and own a legal copy of the game, you can copy the Content folder from the game's files to BarotraumaShared/Content."); + } + Config = new GameSettings(); Md5Hash.LoadCache(); @@ -380,6 +385,13 @@ namespace Barotrauma } } + public class LoadingException : Exception + { + public LoadingException(Exception e) : base("Loading was interrupted due to an error.", innerException: e) + { + } + } + private IEnumerable Load(bool isSeparateThread) { if (GameSettings.VerboseLogging) @@ -397,7 +409,7 @@ namespace Barotrauma SoundManager.SetCategoryGainMultiplier("ui", Config.SoundVolume, 0); SoundManager.SetCategoryGainMultiplier("waterambience", Config.SoundVolume, 0); SoundManager.SetCategoryGainMultiplier("music", Config.MusicVolume, 0); - SoundManager.SetCategoryGainMultiplier("voip", Config.VoiceChatVolume * 20.0f, 0); + SoundManager.SetCategoryGainMultiplier("voip", Config.VoiceChatVolume, 0); if (Config.EnableSplashScreen && !ConsoleArguments.Contains("-skipintro")) { @@ -421,17 +433,27 @@ namespace Barotrauma GUI.Init(Window, Config.SelectedContentPackages, GraphicsDevice); DebugConsole.Init(); - CrossThread.RequestExecutionOnMainThread(() => + if (Config.AutoUpdateWorkshopItems) { - if (Config.AutoUpdateWorkshopItems) + bool waitingForWorkshopUpdates = true; + bool result = false; + TaskPool.Add(SteamManager.AutoUpdateWorkshopItemsAsync(), (task) => { - if (SteamManager.AutoUpdateWorkshopItems()) + result = task.Result; + waitingForWorkshopUpdates = false; + }); + + while (waitingForWorkshopUpdates) { yield return CoroutineStatus.Running; } + + if (result) + { + CrossThread.RequestExecutionOnMainThread(() => { ContentPackage.LoadAll(); Config.ReloadContentPackages(); - } + }); } - }); + } if (SelectedPackages.None()) @@ -488,6 +510,7 @@ namespace Barotrauma Tutorials.Tutorial.Init(); MapGenerationParams.Init(); LevelGenerationParams.LoadPresets(); + WreckAIConfig.LoadAll(); ScriptedEventSet.LoadPrefabs(); AfflictionPrefab.LoadAll(GetFilesOfType(ContentType.Afflictions)); SkillSettings.Load(GetFilesOfType(ContentType.SkillSettings)); @@ -503,6 +526,7 @@ namespace Barotrauma yield return CoroutineStatus.Running; JobPrefab.LoadAll(GetFilesOfType(ContentType.Jobs)); + CorpsePrefab.LoadAll(GetFilesOfType(ContentType.Corpses)); NPCConversation.LoadAll(GetFilesOfType(ContentType.NPCConversations)); @@ -512,7 +536,7 @@ namespace Barotrauma GameModePreset.Init(); - Submarine.RefreshSavedSubs(); + SubmarineInfo.RefreshSavedSubs(); TitleScreen.LoadState = 65.0f; yield return CoroutineStatus.Running; @@ -649,6 +673,8 @@ 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); #endif ConnectName = null; ConnectEndpoint = null; @@ -741,12 +767,7 @@ namespace Barotrauma if (!hasLoaded && !CoroutineManager.IsCoroutineRunning(loadingCoroutine)) { - string errMsg = "Loading was interrupted due to an error"; - if (loadingCoroutine.Exception != null) - { - errMsg += ": " + loadingCoroutine.Exception.Message + "\n" + loadingCoroutine.Exception.StackTrace; - } - throw new Exception(errMsg); + throw new LoadingException(loadingCoroutine.Exception); } } else if (hasLoaded) @@ -1086,7 +1107,10 @@ namespace Barotrauma UserData = "https://steamcommunity.com/app/602960/discussions/1/", OnClicked = (btn, userdata) => { - SteamManager.OverlayCustomURL(userdata as string); + if (!SteamManager.OverlayCustomURL(userdata as string)) + { + ShowOpenUrlInWebBrowserPrompt(userdata as string); + } msgBox.Close(); return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 9e87390d5..bb2bb7897 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -34,6 +34,7 @@ namespace Barotrauma private GUIButton commandButton, toggleCrewButton; private float crewListOpenState; private bool toggleCrewListOpen = true; + private Point crewListEntrySize; /// /// Present only in single player games. In multiplayer. The chatbox is found from GameSession.Client. @@ -51,7 +52,6 @@ namespace Barotrauma { if (toggleCrewListOpen == value) { return; } toggleCrewListOpen = GameMain.Config.CrewMenuOpen = value; - toggleCrewButton.Children.ForEach(c => c.SpriteEffects = toggleCrewListOpen ? SpriteEffects.None : SpriteEffects.FlipHorizontally); } } @@ -70,13 +70,13 @@ namespace Barotrauma { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "character") continue; + if (!subElement.Name.ToString().Equals("character", StringComparison.OrdinalIgnoreCase)) { continue; } var characterInfo = new CharacterInfo(subElement); characterInfos.Add(characterInfo); foreach (XElement invElement in subElement.Elements()) { - if (invElement.Name.ToString().ToLowerInvariant() != "inventory") continue; + if (!invElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; } characterInfo.InventoryData = invElement; break; } @@ -90,6 +90,8 @@ namespace Barotrauma CanBeFocused = false }; + #region Crew Area + var crewAreaWithButtons = new GUIFrame( HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.CrewArea, guiFrame.RectTransform), style: null, @@ -98,11 +100,13 @@ namespace Barotrauma CanBeFocused = false }; - var buttonHeight = (int)(GUI.Scale * 40); + var commandButtonHeight = (int)(GUI.Scale * 40); + var buttonSize = new Point((int)(182f / 99f * commandButtonHeight), commandButtonHeight); + var crewListToggleButtonHeight = (int)(64f * buttonSize.X / 175f); crewArea = new GUIFrame( new RectTransform( - new Point(crewAreaWithButtons.Rect.Width, crewAreaWithButtons.Rect.Height - (int)(1.5f * buttonHeight) - 2 * HUDLayoutSettings.Padding), + new Point(crewAreaWithButtons.Rect.Width, crewAreaWithButtons.Rect.Height - commandButtonHeight - crewListToggleButtonHeight - 2 * HUDLayoutSettings.Padding), crewAreaWithButtons.RectTransform, Anchor.BottomLeft), style: null, @@ -111,7 +115,6 @@ namespace Barotrauma CanBeFocused = false }; - var buttonSize = new Point((int)(182.0f / 99.0f * buttonHeight), buttonHeight); commandButton = new GUIButton( new RectTransform(buttonSize, parent: crewAreaWithButtons.RectTransform), style: "CommandButton") @@ -139,14 +142,13 @@ namespace Barotrauma Spacing = (int)(GUI.Scale * 10) }; + buttonSize.Y = crewListToggleButtonHeight; toggleCrewButton = new GUIButton( - new RectTransform( - new Point(buttonSize.X, (int)(0.5f * buttonHeight)), - parent: crewAreaWithButtons.RectTransform) + new RectTransform(buttonSize, parent: crewAreaWithButtons.RectTransform) { - AbsoluteOffset = new Point(0, buttonHeight + HUDLayoutSettings.Padding) + AbsoluteOffset = new Point(0, commandButtonHeight + HUDLayoutSettings.Padding) }, - style: "UIToggleButton") + style: "CrewListToggleButton") { OnClicked = (GUIButton btn, object userdata) => { @@ -159,6 +161,17 @@ namespace Barotrauma previousOrderArrow = new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(128, 512, 128, 128)); cancelIcon = new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(512, 384, 128, 128)); + // Calculate and store crew list entry size so it doesn't have to be calculated for every entry + crewListEntrySize = new Point(crewList.Content.Rect.Width - HUDLayoutSettings.Padding, 0); + int crewListEntryMinHeight = 32; + crewListEntrySize.Y = Math.Max(crewListEntryMinHeight, (int)(crewListEntrySize.X / 8f)); + float charactersPerView = crewList.Content.Rect.Height / (float)(crewListEntrySize.Y + crewList.Spacing); + int adjustedHeight = (int)Math.Ceiling(crewList.Content.Rect.Height / Math.Round(charactersPerView)) - crewList.Spacing; + if (adjustedHeight < crewListEntryMinHeight) { adjustedHeight = (int)Math.Ceiling(crewList.Content.Rect.Height / Math.Floor(charactersPerView)) - crewList.Spacing; } + crewListEntrySize.Y = adjustedHeight; + + #endregion + #region Chatbox if (IsSinglePlayer) @@ -212,7 +225,7 @@ namespace Barotrauma var chatBox = ChatBox ?? GameMain.Client?.ChatBox; if (chatBox != null) { - chatBox.ToggleButton = new GUIButton(new RectTransform(new Point((int)(182f * GUI.Scale * 0.4f), (int)(99f * GUI.Scale * 0.4f)), guiFrame.RectTransform), style: "ChatToggleButton"); + chatBox.ToggleButton = new GUIButton(new RectTransform(new Point((int)(182f * GUI.Scale * 0.4f), (int)(99f * GUI.Scale * 0.4f)), chatBox.GUIFrame.Parent.RectTransform), style: "ChatToggleButton"); chatBox.ToggleButton.RectTransform.AbsoluteOffset = new Point(0, HUDLayoutSettings.ChatBoxArea.Height - chatBox.ToggleButton.Rect.Height); chatBox.ToggleButton.OnClicked += (GUIButton btn, object userdata) => { @@ -248,6 +261,8 @@ namespace Barotrauma OnClicked = (GUIButton button, object userData) => { if (!CanIssueOrders) { return false; } + 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) @@ -330,17 +345,7 @@ namespace Barotrauma } AddCharacterToCrewList(character); - - if (character is AICharacter) - { - var ai = character.AIController as HumanAIController; - if (ai == null) - { - DebugConsole.ThrowError("Error in crewmanager - attempted to give orders to a character with no HumanAIController"); - return; - } - character.SetOrder(ai.CurrentOrder, "", null, false); - } + DisplayCharacterOrder(character, character.CurrentOrder, character.CurrentOrderOption); } public void AddCharacterInfo(CharacterInfo characterInfo) @@ -383,16 +388,14 @@ namespace Barotrauma { if (character == null) { return; } - int width = crewList.Content.Rect.Width - HUDLayoutSettings.Padding; - int height = Math.Max(32, (int)((1.0f / 8.0f) * width)); var background = new GUIFrame( - new RectTransform(new Point(width, height), parent: crewList.Content.RectTransform, anchor: Anchor.TopRight), + new RectTransform(crewListEntrySize, parent: crewList.Content.RectTransform, anchor: Anchor.TopRight), style: "CrewListBackground") { UserData = character }; - var iconRelativeWidth = (float)height / background.Rect.Width; + var iconRelativeWidth = (float)crewListEntrySize.Y / background.Rect.Width; var layoutGroup = new GUILayoutGroup( new RectTransform(Vector2.One, parent: background.RectTransform), @@ -788,11 +791,11 @@ namespace Barotrauma characterFrame.SetAsFirstChild(); } - private void DisplayPreviousCharacterOrder(Character character, GUILayoutGroup characterComponent, OrderInfo currentOrderInfo) + private void DisplayPreviousCharacterOrder(Character character, GUILayoutGroup characterComponent, OrderInfo orderInfo) { - if (currentOrderInfo.Order == null || currentOrderInfo.Order.Identifier == dismissedOrderPrefab.Identifier) { return; } + if (orderInfo.Order == null || orderInfo.Order.Identifier == dismissedOrderPrefab.Identifier) { return; } - var previousOrderInfo = new OrderInfo(currentOrderInfo); + var previousOrderInfo = new OrderInfo(orderInfo); var prevOrderFrame = new GUIButton( new RectTransform( characterComponent.GetChildByUserData("job").RectTransform.RelativeSize, @@ -835,12 +838,12 @@ namespace Barotrauma private GUIComponent GetCurrentOrderComponent(GUILayoutGroup characterComponent) { - return characterComponent.FindChild(c => c.UserData is OrderInfo orderInfo && orderInfo.ComponentIdentifier == "currentorder"); + return characterComponent?.FindChild(c => c?.UserData is OrderInfo orderInfo && orderInfo.ComponentIdentifier == "currentorder"); } private GUIComponent GetPreviousOrderComponent(GUILayoutGroup characterComponent) { - return characterComponent.FindChild(c => c.UserData is OrderInfo orderInfo && orderInfo.ComponentIdentifier == "previousorder"); + return characterComponent?.FindChild(c => c?.UserData is OrderInfo orderInfo && orderInfo.ComponentIdentifier == "previousorder"); } private struct OrderInfo @@ -910,7 +913,11 @@ namespace Barotrauma { if (!(c.UserData is Character character) || character.IsDead || character.Removed) { continue; } AddCharacter(character); - DisplayCharacterOrder(character, character.CurrentOrder, (character.AIController as HumanAIController)?.CurrentOrderOption); + if (GetPreviousOrderComponent(c.GetChild())?.UserData is OrderInfo prevInfo && + crewList.Content.Children.FirstOrDefault(c => c?.UserData == character)?.GetChild() is GUILayoutGroup newLayoutGroup) + { + DisplayPreviousCharacterOrder(character, newLayoutGroup, prevInfo); + } } } @@ -944,6 +951,7 @@ namespace Barotrauma { Character.Controlled.AIController.ObjectiveManager.WaitTimer = CharacterWaitOnSwitch; } + DisableCommandUI(); Character.Controlled = character; } @@ -988,6 +996,7 @@ namespace Barotrauma commandFrame == null && !clicklessSelectionActive && CanIssueOrders) { CreateCommandUI(HUDLayoutSettings.PortraitArea.Contains(PlayerInput.MousePosition) ? Character.Controlled : GUI.MouseOn?.UserData as Character); + GUI.PlayUISound(GUISoundType.PopupMenu); clicklessSelectionActive = isOpeningClick = true; } @@ -1176,19 +1185,22 @@ namespace Barotrauma if (PlayerInput.KeyHit(InputType.RadioChat) && !ChatBox.InputBox.Selected) { - ChatBox.InputBox.AddToGUIUpdateList(); - ChatBox.GUIFrame.Flash(Color.YellowGreen, 0.5f); - if (!ChatBox.ToggleOpen) + if (Character.Controlled == null || Character.Controlled.SpeechImpediment < 100) { - ChatBox.CloseAfterMessageSent = !ChatBox.ToggleOpen; - ChatBox.ToggleOpen = true; - } + ChatBox.InputBox.AddToGUIUpdateList(); + ChatBox.GUIFrame.Flash(Color.YellowGreen, 0.5f); + if (!ChatBox.ToggleOpen) + { + ChatBox.CloseAfterMessageSent = !ChatBox.ToggleOpen; + ChatBox.ToggleOpen = true; + } - if (!ChatBox.InputBox.Text.StartsWith(ChatBox.RadioChatString)) - { - ChatBox.InputBox.Text = ChatBox.RadioChatString; + if (!ChatBox.InputBox.Text.StartsWith(ChatBox.RadioChatString)) + { + ChatBox.InputBox.Text = ChatBox.RadioChatString; + } + ChatBox.InputBox.Select(ChatBox.InputBox.Text.Length); } - ChatBox.InputBox.Select(ChatBox.InputBox.Text.Length); } } } @@ -1234,6 +1246,7 @@ namespace Barotrauma if (GUI.KeyboardDispatcher.Subscriber == null && PlayerInput.KeyHit(InputType.CrewOrders)) { + GUI.PlayUISound(GUISoundType.PopupMenu); ToggleCrewListOpen = !ToggleCrewListOpen; } @@ -1293,13 +1306,21 @@ namespace Barotrauma { get { +#if DEBUG + return Character.Controlled == null || Character.Controlled.Info != null && Character.Controlled.SpeechImpediment < 100.0f; +#else return Character.Controlled != null && Character.Controlled.SpeechImpediment < 100.0f; +#endif } } private bool CanSomeoneHearCharacter() { +#if DEBUG + return true; +#else return Character.Controlled != null && characters.Any(c => c != Character.Controlled && c.CanHearCharacter(Character.Controlled)); +#endif } private void CreateCommandUI(Character characterContext = null) @@ -1505,17 +1526,7 @@ namespace Barotrauma shortcutCenterNode = null; } CreateNodes(userData); - if (returnNode != null && returnNode.Visible) - { - var hotkey = optionNodes.Count + 1; - if (expandNode != null && expandNode.Visible) { hotkey += 1; } - CreateHotkeyIcon(returnNode.RectTransform, hotkey % 10, true); - returnNodeHotkey = Keys.D0 + hotkey % 10; - } - else - { - returnNodeHotkey = Keys.None; - } + CreateReturnNodeHotkey(); return true; } @@ -1539,10 +1550,20 @@ namespace Barotrauma returnNode = null; } CreateNodes(userData); + CreateReturnNodeHotkey(); + return true; + } + + private void CreateReturnNodeHotkey() + { if (returnNode != null && returnNode.Visible) { - var hotkey = optionNodes.Count + 1; - if (expandNode != null && expandNode.Visible) { hotkey += 1; } + var hotkey = 1; + if (targetFrame == null || !targetFrame.Visible) + { + hotkey = optionNodes.Count + 1; + if (expandNode != null && expandNode.Visible) { hotkey += 1; } + } CreateHotkeyIcon(returnNode.RectTransform, hotkey % 10, true); returnNodeHotkey = Keys.D0 + hotkey % 10; } @@ -1550,7 +1571,6 @@ namespace Barotrauma { returnNodeHotkey = Keys.None; } - return true; } private void SetCenterNode(GUIButton node) @@ -1822,7 +1842,7 @@ namespace Barotrauma Item.ItemList.FindAll(it => it.Components.Any(ic => ic.GetType() == order.ItemComponentType)); matchingItems.RemoveAll(it => it.Submarine != submarine && !submarine.DockedTo.Contains(it.Submarine)); - matchingItems.RemoveAll(it => it.Submarine != null && it.Submarine.IsOutpost); + matchingItems.RemoveAll(it => it.Submarine != null && it.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player); } //more than one target item -> create a minimap-like selection with a pic of the sub @@ -2148,7 +2168,11 @@ namespace Barotrauma ToolTip = character.Info.DisplayName + " (" + character.Info.Job.Name + ")" }; +#if DEBUG + bool canHear = true; +#else bool canHear = character.CanHearCharacter(Character.Controlled); +#endif if (!canHear) { node.CanBeFocused = icon.CanBeFocused = false; @@ -2271,8 +2295,14 @@ namespace Barotrauma private Character GetBestCharacterForOrder(Order order) { +#if !DEBUG if (Character.Controlled == null) { return null; } - return characters.FindAll(c => c != Character.Controlled && c.TeamID == Character.Controlled.TeamID) +#endif + if (order.Category == OrderCategory.Operate && HumanAIController.IsItemOperatedByAnother(null, order.TargetItemComponent, out Character operatingCharacter)) + { + return operatingCharacter; + } + return characters.FindAll(c => Character.Controlled == null || (c != Character.Controlled && c.TeamID == Character.Controlled.TeamID)) .OrderByDescending(c => c.CurrentOrder == null || c.CurrentOrder.Identifier == dismissedOrderPrefab.Identifier) .ThenByDescending(c => order.HasAppropriateJob(c)) .ThenBy(c => c.CurrentOrder?.Weight) @@ -2281,16 +2311,18 @@ namespace Barotrauma private List GetCharactersSortedForOrder(Order order) { +#if !DEBUG if (Character.Controlled == null) { return new List(); } +#endif if (order.Identifier == "follow") { - return characters.FindAll(c => c != Character.Controlled && c.TeamID == Character.Controlled.TeamID) + return characters.FindAll(c => Character.Controlled == null || (c != Character.Controlled && c.TeamID == Character.Controlled.TeamID)) .OrderByDescending(c => c.CurrentOrder == null || c.CurrentOrder.Identifier == dismissedOrderPrefab.Identifier) .ToList(); } else { - return characters.FindAll(c => c.TeamID == Character.Controlled.TeamID) + return characters.FindAll(c => Character.Controlled == null || c.TeamID == Character.Controlled.TeamID) .OrderByDescending(c => c.CurrentOrder == null || c.CurrentOrder.Identifier == dismissedOrderPrefab.Identifier) .ThenByDescending(c => order.HasAppropriateJob(c)) .ThenBy(c => c.CurrentOrder?.Weight) @@ -2411,12 +2443,15 @@ namespace Barotrauma var reportButtonParent = ChatBox ?? GameMain.Client?.ChatBox; if (reportButtonParent == null) { return; } + var sub = Character.Controlled.Submarine; + if (sub == null || sub.TeamID != Character.Controlled.TeamID || sub.Info.IsWreck) { return; } + ReportButtonFrame.RectTransform.AbsoluteOffset = new Point(reportButtonParent.GUIFrame.Rect.Right + (int)(10 * GUI.Scale), reportButtonParent.GUIFrame.Rect.Y); bool hasFires = Character.Controlled.CurrentHull.FireSources.Count > 0; ToggleReportButton("reportfire", hasFires); - bool hasLeaks = Character.Controlled.CurrentHull.Submarine != null && Character.Controlled.CurrentHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f); + bool hasLeaks = Character.Controlled.CurrentHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f); ToggleReportButton("reportbreach", hasLeaks); bool hasIntruders = Character.CharacterList.Any(c => c.CurrentHull == Character.Controlled.CurrentHull && AIObjectiveFightIntruders.IsValidTarget(c, Character.Controlled)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 833b5693b..f9a62b2bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -91,7 +91,7 @@ namespace Barotrauma if (Character.Controlled.Submarine != outpost) { return null; } //if there's a sub docked to the outpost, we can leave the level - if (outpost.DockedTo.Count > 0) + if (outpost.DockedTo.Any()) { var dockedSub = outpost.DockedTo.FirstOrDefault(); return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; @@ -277,6 +277,9 @@ namespace Barotrauma c.Info.InventoryData = inventoryElement; c.Inventory?.DeleteAllItems(); } + + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + SaveUtil.SaveGame(GameMain.GameSession.SavePath); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SubTestMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SubTestMode.cs new file mode 100644 index 000000000..cfddadd7e --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SubTestMode.cs @@ -0,0 +1,80 @@ +using Barotrauma.Tutorials; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + class SubTestMode : GameMode + { + public SubTestMode(GameModePreset preset, object param) + : base(preset, param) + { + foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) + { + for (int i = 0; i < jobPrefab.InitialCount; i++) + { + var variant = Rand.Range(0, jobPrefab.Variants); + CrewManager.AddCharacterInfo(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: jobPrefab, variant: variant)); + } + } + } + + public override void Start() + { + base.Start(); + + isRunning = true; + CrewManager.InitSinglePlayerRound(); + + Submarine.MainSub.SetPosition(Vector2.Zero); + } + + public override void Draw(SpriteBatch spriteBatch) + { + if (!isRunning|| GUI.DisableHUD || GUI.DisableUpperHUD) return; + + if (Submarine.MainSub == null) return; + } + + public override void AddToGUIUpdateList() + { + if (!isRunning) return; + + base.AddToGUIUpdateList(); + CrewManager.AddToGUIUpdateList(); + } + + public override void Update(float deltaTime) + { + if (!isRunning) { return; } + + base.Update(deltaTime); + } + + public override void End(string endMessage = "") + { + isRunning = false; + + GameMain.GameSession.EndRound(""); + + CrewManager.EndRound(); + + Submarine.Unload(); + + GameMain.SubEditorScreen.Select(); + } + + private bool EndRound(Submarine leavingSub) + { + isRunning = false; + + End(""); + + return true; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs index 711b190ac..89c5fbf6c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; using Barotrauma.Networking; @@ -221,7 +222,7 @@ namespace Barotrauma.Tutorials { //captain_navConsoleCustomInterface.HighlightElement(0, uiHighlightColor, duration: 1.0f, pulsateAmount: 0.0f); yield return new WaitForSeconds(1.0f, false); - } while (Submarine.MainSub.DockedTo.Count > 0); + } while (Submarine.MainSub.DockedTo.Any()); RemoveCompletedObjective(segments[4]); yield return new WaitForSeconds(2f, false); TriggerTutorialSegment(5); // Navigate to destination @@ -245,7 +246,7 @@ namespace Barotrauma.Tutorials { //captain_navConsoleCustomInterface.HighlightElement(0, uiHighlightColor, duration: 1.0f, pulsateAmount: 0.0f); yield return new WaitForSeconds(1.0f, false); - } while (!Submarine.MainSub.AtEndPosition || Submarine.MainSub.DockedTo.Count == 0); + } while (!Submarine.MainSub.AtEndPosition || Submarine.MainSub.DockedTo.Any()); RemoveCompletedObjective(segments[6]); yield return new WaitForSeconds(3f, false); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.GetWithVariable("Captain.Radio.Complete", "[OUTPOSTNAME]", GameMain.GameSession.EndLocation.Name), ChatMessageType.Radio, null); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs index 8434baceb..d659ef6ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs @@ -21,8 +21,8 @@ namespace Barotrauma.Tutorials private string levelSeed; private string levelParams; - private Submarine startOutpost = null; - private Submarine endOutpost = null; + private SubmarineInfo startOutpost = null; + private SubmarineInfo endOutpost = null; private bool currentTutorialCompleted = false; private float fadeOutTime = 3f; protected float waitBeforeFade = 4f; @@ -56,13 +56,13 @@ namespace Barotrauma.Tutorials private IEnumerable Loading() { - Submarine.MainSub = Submarine.Load(submarinePath, "", true); + SubmarineInfo subInfo = new SubmarineInfo(submarinePath); LevelGenerationParams generationParams = LevelGenerationParams.LevelParams.Find(p => p.Name == levelParams); yield return CoroutineStatus.Running; - GameMain.GameSession = new GameSession(Submarine.MainSub, "", + GameMain.GameSession = new GameSession(subInfo, "", GameModePreset.List.Find(g => g.Identifier == "tutorial")); (GameMain.GameSession.GameMode as TutorialMode).Tutorial = this; @@ -72,12 +72,12 @@ namespace Barotrauma.Tutorials if (startOutpostPath != string.Empty) { - startOutpost = Submarine.Load(startOutpostPath, "", false); + startOutpost = new SubmarineInfo(startOutpostPath); } if (endOutpostPath != string.Empty) { - endOutpost = Submarine.Load(endOutpostPath, "", false); + endOutpost = new SubmarineInfo(endOutpostPath); } Level tutorialLevel = new Level(levelSeed, 0, 0, generationParams, biome, startOutpost, endOutpost); @@ -160,11 +160,11 @@ namespace Barotrauma.Tutorials switch (this.spawnSub) { case "startoutpost": - spawnSub = startOutpost; + spawnSub = Level.Loaded.StartOutpost; break; case "endoutpost": - spawnSub = endOutpost; + spawnSub = Level.Loaded.EndOutpost; break; default: diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index aab3afeef..903f55d0f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -13,6 +13,7 @@ namespace Barotrauma private GUIFrame infoFrameContent; public RoundSummary RoundSummary { get; private set; } + public static bool IsInfoFrameOpen => GameMain.GameSession?.infoFrame != null; private bool ToggleInfoFrame() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 40a5ca72f..78b8f52fe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -24,7 +24,7 @@ namespace Barotrauma public GUIFrame CreateSummaryFrame(string endMessage) { bool singleplayer = GameMain.NetworkMember == null; - bool gameOver = gameSession.CrewManager.GetCharacters().All(c => c.IsDead || c.IsUnconscious); + bool gameOver = gameSession.CrewManager.GetCharacters().All(c => c.IsDead || c.IsIncapacitated); bool progress = Submarine.MainSub.AtEndPosition; if (!singleplayer) { @@ -55,7 +55,7 @@ namespace Barotrauma string summaryText = TextManager.GetWithVariables(gameOver ? "RoundSummaryGameOver" : (progress ? "RoundSummaryProgress" : "RoundSummaryReturn"), new string[2] { "[sub]", "[location]" }, - new string[2] { Submarine.MainSub.Name, progress ? GameMain.GameSession.EndLocation.Name : GameMain.GameSession.StartLocation.Name }); + new string[2] { Submarine.MainSub.Info.Name, progress ? GameMain.GameSession.EndLocation.Name : GameMain.GameSession.StartLocation.Name }); var infoText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoTextBox.Content.RectTransform), summaryText, wrap: true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index 8c16da963..0bea0a5b6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -47,7 +47,7 @@ namespace Barotrauma { keyMapping = new KeyOrMouse[Enum.GetNames(typeof(InputType)).Length]; keyMapping[(int)InputType.Run] = new KeyOrMouse(Keys.LeftShift); - keyMapping[(int)InputType.Attack] = new KeyOrMouse(MouseButton.MiddleMouse); + keyMapping[(int)InputType.Attack] = new KeyOrMouse(Keys.R); keyMapping[(int)InputType.Crouch] = new KeyOrMouse(Keys.LeftControl); keyMapping[(int)InputType.Grab] = new KeyOrMouse(Keys.G); keyMapping[(int)InputType.Health] = new KeyOrMouse(Keys.H); @@ -1182,7 +1182,7 @@ namespace Barotrauma { RelativeOffset = new Vector2(0.02f, 0.02f) }) { RelativeSpacing = 0.01f }; - var automaticQuickStartTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "Enable automatic quickstart", style: "GUITickBox"); + var automaticQuickStartTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "Automatic quickstart enabled", style: "GUITickBox"); automaticQuickStartTickBox.Selected = AutomaticQuickStartEnabled; automaticQuickStartTickBox.ToolTip = "Will the game automatically move on to Quickstart when the game is launched"; automaticQuickStartTickBox.OnSelected = (tickBox) => @@ -1192,7 +1192,7 @@ namespace Barotrauma return true; }; - var showSplashScreenTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "Show splash screen", style: "GUITickBox"); + var showSplashScreenTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "Splash screen enabled", style: "GUITickBox"); showSplashScreenTickBox.Selected = EnableSplashScreen; showSplashScreenTickBox.ToolTip = "Are the splash screens shown when the game is launched"; showSplashScreenTickBox.OnSelected = (tickBox) => @@ -1202,7 +1202,7 @@ namespace Barotrauma return true; }; - var verboseLoggingTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "Enable verbose logging", style: "GUITickBox"); + var verboseLoggingTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "Verbose logging enabled", style: "GUITickBox"); verboseLoggingTickBox.Selected = VerboseLogging; verboseLoggingTickBox.ToolTip = "Should verbose logging be used"; verboseLoggingTickBox.OnSelected = (tickBox) => @@ -1211,6 +1211,16 @@ namespace Barotrauma UnsavedSettings = true; return true; }; + + var textManagerDebugModeTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "TextManager debug mode enabled", style: "GUITickBox"); + textManagerDebugModeTickBox.Selected = TextManagerDebugModeEnabled; + textManagerDebugModeTickBox.ToolTip = "Does the TextManager return the text tags for debug purposes?"; + textManagerDebugModeTickBox.OnSelected = (tickBox) => + { + TextManagerDebugModeEnabled = tickBox.Selected; + UnsavedSettings = true; + return true; + }; #endif UnsavedSettings = false; // Reset unsaved settings to false once the UI has been created @@ -1279,7 +1289,7 @@ namespace Barotrauma string[] prefixes = { "OpenAL Soft on " }; foreach (string prefix in prefixes) { - if (name.StartsWith(prefix, StringComparison.InvariantCulture)) + if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { return name.Remove(0, prefix.Length); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index a910bbb99..42de38bdd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -43,6 +43,7 @@ namespace Barotrauma public Vector2[] SlotPositions; public static Point SlotSize; public static int Spacing; + public static int HideButtonWidth; private Layout layout; public Layout CurrentLayout @@ -71,22 +72,44 @@ namespace Barotrauma { get { return personalSlotArea; } } - + + private GUIImage[] indicators = new GUIImage[5]; + private int[] indicatorIndexes = new int[5]; + private Vector2 indicatorSpriteSize; + private GUILayoutGroup indicatorGroup; + partial void InitProjSpecific(XElement element) { Hidden = true; - hideButton = new GUIButton(new RectTransform(new Point((int)(30 * GUI.Scale), (int)(60 * GUI.Scale)), GUI.Canvas) + hideButton = new GUIButton(new RectTransform(new Point((int)(31f * (HUDLayoutSettings.BottomRightInfoArea.Height / 100f)), HUDLayoutSettings.BottomRightInfoArea.Height), GUI.Canvas) { AbsoluteOffset = HUDLayoutSettings.CrewArea.Location }, - "", style: "UIToggleButton"); - hideButton.Children.ForEach(c => c.SpriteEffects = SpriteEffects.FlipHorizontally); + "", style: "EquipmentToggleButton"); + + indicatorGroup = new GUILayoutGroup(new RectTransform(Point.Zero, hideButton.RectTransform)) { IsHorizontal = false }; + indicatorGroup.ChildAnchor = Anchor.TopCenter; + indicatorSpriteSize = GUI.Style.GetComponentStyle("EquipmentIndicatorDivingSuit").Sprites[GUIComponent.ComponentState.None][0].Sprite.size; + + indicators[0] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorDivingSuit"); + indicators[1] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorID"); + indicators[2] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorOutfit"); + indicators[3] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorHeadwear"); + indicators[4] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorHeadphones"); + + indicatorIndexes[0] = FindLimbSlot(InvSlotType.OuterClothes); + indicatorIndexes[1] = FindLimbSlot(InvSlotType.Card); + indicatorIndexes[2] = FindLimbSlot(InvSlotType.InnerClothes); + indicatorIndexes[3] = FindLimbSlot(InvSlotType.Head); + indicatorIndexes[4] = FindLimbSlot(InvSlotType.Headset); + + for (int i = 0; i < indicators.Length; i++) + { + indicators[i].CanBeFocused = false; + } + hideButton.OnClicked += (GUIButton btn, object userdata) => { hidePersonalSlots = !hidePersonalSlots; - foreach (GUIComponent child in btn.Children) - { - child.SpriteEffects = hidePersonalSlots ? SpriteEffects.None : SpriteEffects.FlipHorizontally; - } return true; }; @@ -245,6 +268,26 @@ namespace Barotrauma return false; } + + private void SetIndicatorSizes() + { + indicatorGroup.RectTransform.AbsoluteOffset = new Point((int)Math.Round(4 * GUI.Scale), (int)Math.Round(7 * GUI.Scale)); + indicatorGroup.RectTransform.NonScaledSize = new Point(hideButton.Rect.Width - indicatorGroup.RectTransform.AbsoluteOffset.X * 2, hideButton.Rect.Height - indicatorGroup.RectTransform.AbsoluteOffset.Y * 2); + indicatorGroup.AbsoluteSpacing = (int)Math.Ceiling(2 * GUI.Scale); + + int indicatorHeight = (indicatorGroup.RectTransform.NonScaledSize.Y - indicatorGroup.AbsoluteSpacing * (indicators.Length - 1)) / indicators.Length; + int indicatorWidth = (int)(indicatorSpriteSize.X / (indicatorSpriteSize.Y / indicatorHeight)); + + if (HideButtonWidth % 2 != indicatorWidth % 2) indicatorWidth++; + + Point indicatorSize = new Point(indicatorWidth, indicatorHeight); + + for (int i = 0; i < indicators.Length; i++) + { + indicators[i].RectTransform.NonScaledSize = indicatorSize; + } + } + private void SetSlotPositions(Layout layout) { bool isFourByThree = GUI.IsFourByThree(); @@ -257,6 +300,8 @@ namespace Barotrauma Spacing = (int)(8 * UIScale); } + HideButtonWidth = (int)(31f * (HUDLayoutSettings.BottomRightInfoArea.Height / 100f)); + SlotSize = !isFourByThree ? (SlotSpriteSmall.size * UIScale).ToPoint() : (SlotSpriteSmall.size * UIScale * .925f).ToPoint(); int bottomOffset = SlotSize.Y + Spacing * 2 + ContainedIndicatorHeight; @@ -272,8 +317,7 @@ namespace Barotrauma int normalSlotCount = SlotTypes.Count(s => !PersonalSlots.HasFlag(s)); int x = GameMain.GraphicsWidth / 2 - normalSlotCount * (SlotSize.X + Spacing) / 2; - int upperX = HUDLayoutSettings.BottomRightInfoArea.X - Spacing * 2 - SlotSize.X - SlotSize.X / 2; - //int upperX = GameMain.GraphicsWidth - personalSlotCount * (slotSize.X + spacing) + (int)(11 * GUI.Scale) + spacing; + int upperX = HUDLayoutSettings.BottomRightInfoArea.X - SlotSize.X - Spacing * 4 - HideButtonWidth; //make sure the rightmost normal slot doesn't overlap with the personal slots x -= Math.Max((x + normalSlotCount * (SlotSize.X + Spacing)) - (upperX - personalSlotCount * (SlotSize.X + Spacing)), 0); @@ -300,11 +344,13 @@ namespace Barotrauma if (hideButtonSlotIndex > -1) { hideButton.RectTransform.SetPosition(Anchor.TopLeft, Pivot.TopLeft); - hideButton.RectTransform.NonScaledSize = new Point(SlotSize.X / 2, HUDLayoutSettings.BottomRightInfoArea.Height); + hideButton.RectTransform.NonScaledSize = new Point(HideButtonWidth, HUDLayoutSettings.BottomRightInfoArea.Height); hideButton.RectTransform.AbsoluteOffset = new Point( - personalSlotArea.Right + Spacing, + personalSlotArea.Right + Spacing * 2, HUDLayoutSettings.BottomRightInfoArea.Y); hideButton.Visible = true; + + SetIndicatorSizes(); } } break; @@ -457,7 +503,8 @@ namespace Barotrauma hidePersonalSlotsState = hidePersonalSlots ? Math.Min(hidePersonalSlotsState + deltaTime * 5.0f, 1.0f) : Math.Max(hidePersonalSlotsState - deltaTime * 5.0f, 0.0f); - + + bool personalSlotsMoving = hidePersonalSlotsState > 0 && hidePersonalSlotsState < 1f; for (int i = 0; i < slots.Length; i++) { if (!PersonalSlots.HasFlag(SlotTypes[i])) { continue; } @@ -466,6 +513,7 @@ namespace Barotrauma if (selectedSlot?.Slot == slots[i]) { selectedSlot = null; } highlightedSubInventorySlots.RemoveWhere(s => s.Slot == slots[i]); } + slots[i].IsMoving = personalSlotsMoving; slots[i].DrawOffset = Vector2.Lerp(Vector2.Zero, new Vector2(personalSlotArea.Width, 0.0f), hidePersonalSlotsState); } } @@ -551,6 +599,8 @@ namespace Barotrauma if (character == Character.Controlled && character.SelectedCharacter == null) // Permanently open subinventories only available when the default UI layout is in use -> not when grabbing characters { + UpdateEquipmentIndicators(); + //remove the highlighted slots of other characters' inventories when not grabbing anyone highlightedSubInventorySlots.RemoveWhere(s => s.ParentInventory != this && s.ParentInventory?.Owner is Character); @@ -651,6 +701,39 @@ namespace Barotrauma } } } + + private void UpdateEquipmentIndicators() + { + for (int i = 0; i < indicators.Length; i++) + { + Item item = Items[indicatorIndexes[i]]; + if (item != null) + { + Wearable wearable = item.GetComponent(); + if (wearable != null && wearable.DisplayContainedStatus) + { + float conditionPercentage = item.GetContainedItemConditionPercentage(); + + if (conditionPercentage != -1) + { + indicators[i].Color = ToolBox.GradientLerp(conditionPercentage, GUI.Style.EquipmentIndicatorRunningOut, GUI.Style.EquipmentIndicatorEquipped); + } + else + { + indicators[i].Color = GUI.Style.EquipmentIndicatorRunningOut; + } + } + else + { + indicators[i].Color = GUI.Style.EquipmentIndicatorEquipped; + } + } + else + { + indicators[i].Color = GUI.Style.EquipmentIndicatorNotEquipped; + } + } + } private void ShowSubInventory(SlotReference slotRef, float deltaTime, Camera cam, List hideSubInventories, bool isEquippedSubInventory) { @@ -964,7 +1047,7 @@ namespace Barotrauma if (limbSlotIcons.ContainsKey(SlotTypes[i])) { var icon = limbSlotIcons[SlotTypes[i]]; - icon.Draw(spriteBatch, slots[i].Rect.Center.ToVector2() + slots[i].DrawOffset, GUIColorSettings.EquipmentSlotIconColor, origin: icon.size / 2, scale: slots[i].Rect.Width / icon.size.X); + icon.Draw(spriteBatch, slots[i].Rect.Center.ToVector2() + slots[i].DrawOffset, GUI.Style.EquipmentSlotIconColor, origin: icon.size / 2, scale: slots[i].Rect.Width / icon.size.X); } continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 7ec738285..9a76f7419 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -52,10 +52,14 @@ namespace Barotrauma.Items.Components get { return sounds.Count > 0; } } - private bool[] hasSoundsOfType; - private Dictionary> sounds; + private readonly bool[] hasSoundsOfType; + private readonly Dictionary> sounds; private Dictionary soundSelectionModes; + protected float correctionTimer; + + public float IsActiveTimer; + public GUILayoutSettings DefaultLayout { get; protected set; } public GUILayoutSettings AlternativeLayout { get; protected set; } @@ -230,20 +234,23 @@ namespace Barotrauma.Items.Components if (loopingSound != null) { - float targetGain = 0.0f; if (Vector3.DistanceSquared(GameMain.SoundManager.ListenerPosition, new Vector3(item.WorldPosition, 0.0f)) > loopingSound.Range * loopingSound.Range || - (targetGain = GetSoundVolume(loopingSound)) <= 0.0001f) + (GetSoundVolume(loopingSound)) <= 0.0001f) { if (loopingSoundChannel != null) { - loopingSoundChannel.FadeOutAndDispose(); loopingSoundChannel = null; + loopingSoundChannel.FadeOutAndDispose(); + loopingSoundChannel = null; + loopingSound = null; } return; } if (loopingSoundChannel != null && loopingSoundChannel.Sound != loopingSound.RoundSound.Sound) { - loopingSoundChannel.FadeOutAndDispose(); loopingSoundChannel = null; + loopingSoundChannel.FadeOutAndDispose(); + loopingSoundChannel = null; + loopingSound = null; } if (loopingSoundChannel == null || !loopingSoundChannel.IsPlaying) { @@ -258,8 +265,7 @@ namespace Barotrauma.Items.Components } return; } - - ItemSound itemSound = null; + var matchingSounds = sounds[type]; if (loopingSoundChannel == null || !loopingSoundChannel.IsPlaying) { @@ -277,7 +283,7 @@ namespace Barotrauma.Items.Components { foreach (ItemSound sound in matchingSounds) { - PlaySound(sound, item.WorldPosition, user); + PlaySound(sound, item.WorldPosition); } return; } @@ -286,13 +292,12 @@ namespace Barotrauma.Items.Components index = Rand.Int(matchingSounds.Count); } - itemSound = matchingSounds[index]; - PlaySound(matchingSounds[index], item.WorldPosition, user); + PlaySound(matchingSounds[index], item.WorldPosition); } } - private void PlaySound(ItemSound itemSound, Vector2 position, Character user = null) + private void PlaySound(ItemSound itemSound, Vector2 position) { if (Vector2.DistanceSquared(new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y), position) > itemSound.Range * itemSound.Range) { @@ -301,8 +306,7 @@ namespace Barotrauma.Items.Components if (itemSound.Loop) { - loopingSound = itemSound; - if (loopingSoundChannel != null && loopingSoundChannel.Sound != loopingSound.RoundSound.Sound) + if (loopingSoundChannel != null && loopingSoundChannel.Sound != itemSound.RoundSound.Sound) { loopingSoundChannel.FadeOutAndDispose(); loopingSoundChannel = null; } @@ -310,6 +314,7 @@ namespace Barotrauma.Items.Components { float volume = GetSoundVolume(itemSound); if (volume <= 0.0001f) { return; } + loopingSound = itemSound; loopingSoundChannel = loopingSound.RoundSound.Sound.Play( new Vector3(position.X, position.Y, 0.0f), 0.01f, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 317e98266..51a3a14fd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -3,11 +3,17 @@ using Barotrauma.Lights; using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using System.Collections.Generic; namespace Barotrauma.Items.Components { partial class LightComponent : Powered, IServerSerializable, IDrawableComponent { + private bool? lastReceivedState; + + private CoroutineHandle resetPredictionCoroutine; + private float resetPredictionTimer; + public Vector2 DrawSize { get { return new Vector2(light.Range * 2, light.Range * 2); } @@ -32,6 +38,12 @@ namespace Barotrauma.Items.Components light.Color = LightColor.Multiply(brightness); } + public override void OnItemLoaded() + { + base.OnItemLoaded(); + SetLightSourceState(IsActive, lightBrightness); + } + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { if (light.LightSprite != null && (item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn) @@ -49,9 +61,36 @@ namespace Barotrauma.Items.Components } } + partial void OnStateChanged() + { + if (GameMain.Client == null || !lastReceivedState.HasValue) { return; } + //reset to last known server state after the state hasn't changed in 1.0 seconds client-side + resetPredictionTimer = 1.0f; + if (resetPredictionCoroutine == null || !CoroutineManager.IsCoroutineRunning(resetPredictionCoroutine)) + { + resetPredictionCoroutine = CoroutineManager.StartCoroutine(ResetPredictionAfterDelay()); + } + } + + /// + /// Reset client-side prediction of the light's state to the last known state sent by the server after resetPredictionTimer runs out + /// + private IEnumerable ResetPredictionAfterDelay() + { + while (resetPredictionTimer > 0.0f) + { + resetPredictionTimer -= CoroutineManager.DeltaTime; + yield return CoroutineStatus.Running; + } + if (lastReceivedState.HasValue) { IsActive = lastReceivedState.Value; } + resetPredictionCoroutine = null; + yield return CoroutineStatus.Success; + } + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { - IsOn = msg.ReadBoolean(); + IsActive = msg.ReadBoolean(); + lastReceivedState = IsActive; } protected override void RemoveComponentSpecific() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 10240011e..89165ecc0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -140,11 +140,11 @@ namespace Barotrauma.Items.Components var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 1f), bottomFrame.RectTransform, Anchor.BottomCenter), isHorizontal: true, childAnchor: Anchor.BottomLeft); // === INPUT SLOTS === // - inputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(0.8f, 1f), inputArea.RectTransform), style: null); + inputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(0.7f, 1f), inputArea.RectTransform), style: null); new GUICustomComponent(new RectTransform(Vector2.One, inputInventoryHolder.RectTransform), DrawInputOverLay) { CanBeFocused = false }; // === ACTIVATE BUTTON === // - var buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.2f, 0.8f), inputArea.RectTransform), childAnchor: Anchor.CenterRight); + var buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 0.8f), inputArea.RectTransform), childAnchor: Anchor.CenterRight); activateButton = new GUIButton(new RectTransform(new Vector2(1f, 0.6f), buttonFrame.RectTransform), TextManager.Get("FabricatorCreate"), style: "DeviceButton") { @@ -154,7 +154,7 @@ namespace Barotrauma.Items.Components }; // === POWER WARNING === // inSufficientPowerWarning = new GUITextBlock(new RectTransform(Vector2.One, activateButton.RectTransform), - TextManager.Get("FabricatorNoPower"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow") + TextManager.Get("FabricatorNoPower"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow", wrap: true) { HoverColor = Color.Black, IgnoreLayoutGroups = true, @@ -356,6 +356,8 @@ namespace Barotrauma.Items.Components private void DrawOutputOverLay(SpriteBatch spriteBatch, GUICustomComponent overlayComponent) { overlayComponent.RectTransform.SetAsLastChild(); + + if (outputContainer.Inventory.Items.First() != null) { return; } FabricationRecipe targetItem = fabricatedItem ?? selectedItem; if (targetItem != null) @@ -584,15 +586,20 @@ namespace Barotrauma.Items.Components public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { + FabricatorState newState = (FabricatorState)msg.ReadByte(); + float newTimeUntilReady = msg.ReadSingle(); int itemIndex = msg.ReadRangedInteger(-1, fabricationRecipes.Count - 1); UInt16 userID = msg.ReadUInt16(); Character user = Entity.FindEntityByID(userID) as Character; - if (itemIndex == -1 || user == null) + State = newState; + timeUntilReady = newTimeUntilReady; + + if (newState == FabricatorState.Stopped || itemIndex == -1 || user == null) { CancelFabricating(); } - else + else if (newState == FabricatorState.Active || newState == FabricatorState.Paused) { //if already fabricating the selected item, return if (fabricatedItem != null && fabricationRecipes.IndexOf(fabricatedItem) == itemIndex) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 746fef710..6114dc576 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -129,8 +129,8 @@ namespace Barotrauma.Items.Components { if (child.UserData is Hull hull) { - if (hull.Submarine == null || !hull.Submarine.IsOutpost) { continue; } - string text = TextManager.GetWithVariable("MiniMapOutpostDockingInfo", "[outpost]", hull.Submarine.Name); + if (hull.Submarine == null || !hull.Submarine.Info.IsOutpost) { continue; } + string text = TextManager.GetWithVariable("MiniMapOutpostDockingInfo", "[outpost]", hull.Submarine.Info.Name); Vector2 textSize = GUI.Font.MeasureString(text); Vector2 textPos = child.Center; if (textPos.X + textSize.X / 2 > submarineContainer.Rect.Right) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index f0384d4b5..069231f64 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -99,6 +99,10 @@ namespace Barotrauma.Items.Components Step = 0.05f, OnMoved = (GUIScrollBar scrollBar, float barScroll) => { + if (pumpSpeedLockTimer <= 0.0f) + { + targetLevel = null; + } float newValue = barScroll * 200.0f - 100.0f; if (Math.Abs(newValue - FlowPercentage) < 0.1f) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index ffc0fc194..2282053b9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -490,7 +490,7 @@ namespace Barotrauma.Items.Components disruptedDirections.Clear(); foreach (AITarget t in AITarget.List) { - if (t.SoundRange <= 0.0f || !t.Enabled || float.IsNaN(t.SoundRange) || float.IsInfinity(t.SoundRange)) { continue; } + if (t.SoundRange <= 0.0f || float.IsNaN(t.SoundRange) || float.IsInfinity(t.SoundRange)) { continue; } float distSqr = Vector2.DistanceSquared(t.WorldPosition, transducerCenter); if (distSqr > t.SoundRange * t.SoundRange * 2) { continue; } @@ -647,6 +647,8 @@ namespace Barotrauma.Items.Components if (GameMain.GameSession == null) { return; } + if (Level.Loaded == null) { return; } + DrawMarker(spriteBatch, GameMain.GameSession.StartLocation.Name, "outpost", @@ -699,8 +701,8 @@ namespace Barotrauma.Items.Components if (sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } DrawMarker(spriteBatch, - sub.Name, - sub.HasTag(SubmarineTag.Shuttle) ? "shuttle" : "submarine", + sub.Info.Name, + sub.Info.HasTag(SubmarineTag.Shuttle) ? "shuttle" : "submarine", sub.WorldPosition - transducerCenter, displayScale, center, DisplayRadius * 0.95f); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 4254f0534..6a51dc250 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -46,6 +46,8 @@ namespace Barotrauma.Items.Components private float checkConnectedPortsTimer; private const float CheckConnectedPortsInterval = 1.0f; + public DockingPort ActiveDockingSource, DockingTarget; + private Vector2 keyboardInput = Vector2.Zero; private float inputCumulation; @@ -665,7 +667,7 @@ namespace Barotrauma.Items.Components if (Vector2.DistanceSquared(PlayerInput.MousePosition, steerArea.Rect.Center.ToVector2()) < steerRadius * steerRadius) { - if (PlayerInput.PrimaryMouseButtonHeld() && !CrewManager.IsCommandInterfaceOpen) + if (PlayerInput.PrimaryMouseButtonHeld() && !CrewManager.IsCommandInterfaceOpen && !GameSession.IsInfoFrameOpen) { Vector2 displaySubPos = (-sonar.DisplayOffset * sonar.Zoom) / sonar.Range * sonar.DisplayRadius * sonar.Zoom; displaySubPos.Y = -displaySubPos.Y; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs index c59e2571f..a72a5ed3e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs @@ -106,11 +106,10 @@ namespace Barotrauma.Items.Components { if (indicatorSize.X <= 1.0f || indicatorSize.Y <= 1.0f) { return; } - GUI.DrawRectangle(spriteBatch, - new Vector2( - item.DrawPosition.X - item.Sprite.SourceRect.Width / 2 * item.Scale + indicatorPosition.X * item.Scale, - -item.DrawPosition.Y - item.Sprite.SourceRect.Height / 2 * item.Scale + indicatorPosition.Y * item.Scale), - indicatorSize * item.Scale, Color.Black, depth: item.SpriteDepth - 0.00001f); + Vector2 itemSize = new Vector2(item.Sprite.SourceRect.Width, item.Sprite.SourceRect.Height) * item.Scale; + Vector2 indicatorPos = -itemSize / 2 + indicatorPosition * item.Scale; + if (item.FlippedX && item.Prefab.CanSpriteFlipX) { indicatorPos.X = -indicatorPos.X - indicatorSize.X * item.Scale; } + if (item.FlippedY && item.Prefab.CanSpriteFlipY) { indicatorPos.Y = -indicatorPos.Y - indicatorSize.Y * item.Scale; } if (charge > 0) { @@ -118,25 +117,23 @@ namespace Barotrauma.Items.Components if (!isHorizontal) { GUI.DrawRectangle(spriteBatch, - new Vector2( - item.DrawPosition.X - item.Sprite.SourceRect.Width / 2 * item.Scale + indicatorPosition.X * item.Scale + 1, - -item.DrawPosition.Y - item.Sprite.SourceRect.Height / 2 * item.Scale + indicatorPosition.Y * item.Scale + 1 + ((indicatorSize.Y * item.Scale) * (1.0f - charge / capacity))), - new Vector2(indicatorSize.X * item.Scale - 2, (indicatorSize.Y * item.Scale - 2) * (charge / capacity)), indicatorColor, true, - depth: item.SpriteDepth - 0.00001f); + new Vector2(item.DrawPosition.X, -item.DrawPosition.Y + ((indicatorSize.Y * item.Scale) * (1.0f - charge / capacity))) + indicatorPos, + new Vector2(indicatorSize.X * item.Scale, (indicatorSize.Y * item.Scale) * (charge / capacity)), indicatorColor, true, + depth: item.SpriteDepth - 0.00001f); } else { GUI.DrawRectangle(spriteBatch, - new Vector2( - item.DrawPosition.X - item.Sprite.SourceRect.Width / 2 * item.Scale + indicatorPosition.X * item.Scale + 1 , - -item.DrawPosition.Y - item.Sprite.SourceRect.Height / 2 * item.Scale + indicatorPosition.Y * item.Scale + 1), - new Vector2((indicatorSize.X * item.Scale - 2) * (charge / capacity), indicatorSize.Y * item.Scale - 2), indicatorColor, true, - depth: item.SpriteDepth - 0.00001f); + new Vector2(item.DrawPosition.X, -item.DrawPosition.Y) + indicatorPos, + new Vector2((indicatorSize.X * item.Scale) * (charge / capacity), indicatorSize.Y * item.Scale), indicatorColor, true, + depth: item.SpriteDepth - 0.00001f); } } - + GUI.DrawRectangle(spriteBatch, + new Vector2(item.DrawPosition.X, -item.DrawPosition.Y) + indicatorPos, + indicatorSize * item.Scale, Color.Black, depth: item.SpriteDepth - 0.00001f); } - + public void ClientWrite(IWriteMessage msg, object[] extraData) { msg.WriteRangedInteger((int)(rechargeSpeed / MaxRechargeSpeed * 10), 0, 10); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs new file mode 100644 index 000000000..58e1a8209 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -0,0 +1,66 @@ +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + partial class Projectile : ItemComponent + { + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + bool isStuck = msg.ReadBoolean(); + if (isStuck) + { + Vector2 simPosition = new Vector2( + msg.ReadSingle(), + msg.ReadSingle()); + Vector2 axis = new Vector2( + msg.ReadSingle(), + msg.ReadSingle()); + UInt16 entityID = msg.ReadUInt16(); + Entity entity = Entity.FindEntityByID(entityID); + item.body.SetTransform(simPosition, item.body.Rotation); + if (entity is Character character) + { + byte limbIndex = msg.ReadByte(); + if (limbIndex >= character.AnimController.Limbs.Length) + { + DebugConsole.ThrowError($"Failed to read a projectile update from the server. Limb index out of bounds ({limbIndex}, character: {character.ToString()})"); + return; + } + var limb = character.AnimController.Limbs[limbIndex]; + StickToTarget(limb.body.FarseerBody, axis); + } + else if (entity is Structure structure) + { + byte bodyIndex = msg.ReadByte(); + if (bodyIndex >= structure.Bodies.Count) + { + DebugConsole.ThrowError($"Failed to read a projectile update from the server. Structure body index out of bounds ({bodyIndex}, structure: {structure.ToString()})"); + return; + } + var body = structure.Bodies[bodyIndex]; + StickToTarget(body, axis); + } + else if (entity is Item item) + { + StickToTarget(item.body.FarseerBody, axis); + } + else if (entity is Submarine sub) + { + StickToTarget(sub.PhysicsBody.FarseerBody, axis); + } + else + { + DebugConsole.ThrowError($"Failed to read a projectile update from the server. Invalid stick target ({entity?.ToString() ?? "null"}, {entityID})"); + } + } + else + { + Unstick(); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 8caf52a49..998db3672 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -236,15 +236,7 @@ namespace Barotrauma.Items.Components DeteriorateAlways = msg.ReadBoolean(); ushort currentFixerID = msg.ReadUInt16(); currentFixerAction = (FixActions)msg.ReadRangedInteger(0, 2); - - if (currentFixerID == 0) - { - CurrentFixer = null; - } - else - { - CurrentFixer = Entity.FindEntityByID(currentFixerID) as Character; - } + CurrentFixer = currentFixerID != 0 ? Entity.FindEntityByID(currentFixerID) as Character : null; } public void ClientWrite(IWriteMessage msg, object[] extraData = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs new file mode 100644 index 000000000..8bd5decf7 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -0,0 +1,163 @@ +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + partial class Rope : ItemComponent, IDrawableComponent + { + private Sprite sprite, startSprite, endSprite; + + [Serialize(5, false)] + public int SpriteWidth + { + get; + set; + } + + [Serialize("255,255,255,255", false)] + public Color SpriteColor + { + get; + set; + } + + [Serialize(false, false)] + public bool Tile + { + get; + set; + } + + public Vector2 DrawSize + { + get + { + if (target == null || source == null) { return Vector2.Zero; } + return new Vector2( + Math.Abs(target.DrawPosition.X - source.DrawPosition.X), + Math.Abs(target.DrawPosition.Y - source.DrawPosition.Y)) * 1.5f; + } + } + + partial void InitProjSpecific(XElement element) + { + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "sprite": + sprite = new Sprite(subElement); + break; + case "startsprite": + startSprite = new Sprite(subElement); + break; + case "endsprite": + endSprite = new Sprite(subElement); + break; + } + } + } + + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + { + if (target == null) { return; } + + Vector2 startPos = new Vector2(source.DrawPosition.X, -source.DrawPosition.Y); + Vector2 endPos = new Vector2(target.DrawPosition.X, -target.DrawPosition.Y); + + if (Snapped) + { + float snapState = 1.0f - snapTimer / SnapAnimDuration; + Vector2 diff = target.DrawPosition - source.DrawPosition; + diff.Y = -diff.Y; + + int width = (int)(SpriteWidth * snapState); + if (width > 0.0f) + { + DrawRope(spriteBatch, endPos - diff * snapState * 0.5f, endPos, width); + DrawRope(spriteBatch, startPos, startPos + diff * snapState * 0.5f, width); + } + } + else + { + DrawRope(spriteBatch, startPos, endPos, SpriteWidth); + } + + if (startSprite != null || endSprite != null) + { + Vector2 dir = endPos - startPos; + float angle = (float)Math.Atan2(dir.Y, dir.X); + if (startSprite != null) + { + float depth = Math.Min(item.GetDrawDepth() + (startSprite.Depth - item.Sprite.Depth), 0.999f); + startSprite?.Draw(spriteBatch, startPos, SpriteColor, angle, depth: depth); + } + if (endSprite != null) + { + float depth = Math.Min(item.GetDrawDepth() + (endSprite.Depth - item.Sprite.Depth), 0.999f); + endSprite?.Draw(spriteBatch, endPos, SpriteColor, angle, depth: depth); + } + } + } + + private void DrawRope(SpriteBatch spriteBatch, Vector2 startPos, Vector2 endPos, int width) + { + float depth = sprite == null ? + item.Sprite.Depth + 0.001f : + Math.Min(item.GetDrawDepth() + (sprite.Depth - item.Sprite.Depth), 0.999f); + + if (sprite?.Texture == null) + { + GUI.DrawLine(spriteBatch, + startPos, + endPos, + SpriteColor, depth: depth, width: width); + return; + } + + if (Tile) + { + float length = Vector2.Distance(startPos, endPos); + Vector2 dir = (endPos - startPos) / length; + float x; + for (x = 0.0f; x <= length - sprite.size.X; x += sprite.size.X) + { + GUI.DrawLine(spriteBatch, sprite, + startPos + dir * (x - 5.0f), + startPos + dir * (x + sprite.size.X), + SpriteColor, depth: depth, width: width); + } + float leftOver = length - x; + if (leftOver > 0.0f) + { + GUI.DrawLine(spriteBatch, sprite, + startPos + dir * (x - 5.0f), + endPos, + SpriteColor, depth: depth, width: width); + } + } + else + { + GUI.DrawLine(spriteBatch, sprite, + startPos, + endPos, + SpriteColor, depth: depth, width: width); + } + } + + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + snapped = msg.ReadBoolean(); + } + + protected override void RemoveComponentSpecific() + { + sprite?.Remove(); sprite = null; + startSprite?.Remove(); startSprite = null; + endSprite?.Remove(); endSprite = null; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index 214ac75b3..be7b5a42f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -45,13 +45,48 @@ namespace Barotrauma.Items.Components float elementSize = Math.Min(1.0f / visibleElements.Count(), 1); foreach (CustomInterfaceElement ciElement in visibleElements) { - if (ciElement.ContinuousSignal) + if (!string.IsNullOrEmpty(ciElement.PropertyName)) + { + var layoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementSize), uiElementContainer.RectTransform), isHorizontal: true) + { + RelativeSpacing = 0.02f, + UserData = ciElement + }; + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), + TextManager.Get(ciElement.Label, returnNull: true) ?? ciElement.Label); + var textBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), "", style: "GUITextBoxNoIcon") + { + OverflowClip = true, + UserData = ciElement + }; + //reset size restrictions set by the Style to make sure the elements can fit the interface + textBox.RectTransform.MinSize = textBox.Frame.RectTransform.MinSize = new Point(0, 0); + textBox.RectTransform.MaxSize = textBox.Frame.RectTransform.MaxSize = new Point(int.MaxValue, int.MaxValue); + textBox.OnDeselected += (tb, key) => + { + if (GameMain.Client == null) + { + TextChanged(tb.UserData as CustomInterfaceElement, textBox.Text); + } + else + { + item.CreateClientEvent(this); + } + }; + + textBox.OnEnterPressed += (tb, text) => + { + tb.Deselect(); + return true; + }; + uiElements.Add(textBox); + } + else if (ciElement.ContinuousSignal) { var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementSize), uiElementContainer.RectTransform) { MaxSize = ElementMaxSize - }, - TextManager.Get(ciElement.Label, returnNull: true) ?? ciElement.Label) + }, TextManager.Get(ciElement.Label, returnNull: true) ?? ciElement.Label) { UserData = ciElement }; @@ -148,7 +183,7 @@ namespace Barotrauma.Items.Components foreach (var uiElement in uiElements) { if (!(uiElement.UserData is CustomInterfaceElement element)) { continue; } - bool visible = Screen.Selected == GameMain.SubEditorScreen || element.StatusEffects.Any() || (element.Connection != null && element.Connection.Wires.Any(w => w != null)); + bool visible = Screen.Selected == GameMain.SubEditorScreen || element.StatusEffects.Any() || !string.IsNullOrEmpty(element.PropertyName) || (element.Connection != null && element.Connection.Wires.Any(w => w != null)); if (visible) { visibleElementCount++; } if (uiElement.Visible != visible) { @@ -188,6 +223,22 @@ namespace Barotrauma.Items.Components customInterfaceElementList[i].Label; tickBox.TextBlock.Wrap = tickBox.Text.Contains(' '); } + if (uiElements[i] is GUITextBox textBox) + { + var textBlock = textBox.Parent.GetChild(); + textBlock.Text = string.IsNullOrWhiteSpace(customInterfaceElementList[i].Label) ? + TextManager.GetWithVariable("connection.signaloutx", "[num]", (i + 1).ToString()) : + customInterfaceElementList[i].Label; + textBlock.Wrap = textBlock.Text.Contains(' '); + + foreach (ISerializableEntity e in item.AllPropertyObjects) + { + if (e.SerializableProperties.ContainsKey(customInterfaceElementList[i].PropertyName)) + { + textBox.Text = e.SerializableProperties[customInterfaceElementList[i].PropertyName].GetValue(e) as string; + } + } + } } uiElementContainer.Recalculate(); @@ -206,6 +257,10 @@ namespace Barotrauma.Items.Components { textBlocks.Add(tickBox.TextBlock); } + else if (element is GUILayoutGroup) + { + textBlocks.Add(element.GetChild()); + } } uiElementContainer.Recalculate(); GUITextBlock.AutoScaleAndNormalize(textBlocks); @@ -216,7 +271,11 @@ namespace Barotrauma.Items.Components //extradata contains an array of buttons clicked by the player (or nothing if the player didn't click anything) for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (customInterfaceElementList[i].ContinuousSignal) + if (!string.IsNullOrEmpty(customInterfaceElementList[i].PropertyName)) + { + msg.Write(((GUITextBox)uiElements[i]).Text); + } + else if (customInterfaceElementList[i].ContinuousSignal) { msg.Write(((GUITickBox)uiElements[i]).Selected); } @@ -231,15 +290,22 @@ namespace Barotrauma.Items.Components { for (int i = 0; i < customInterfaceElementList.Count; i++) { - bool elementState = msg.ReadBoolean(); - if (customInterfaceElementList[i].ContinuousSignal) + if (!string.IsNullOrEmpty(customInterfaceElementList[i].PropertyName)) { - ((GUITickBox)uiElements[i]).Selected = elementState; - TickBoxToggled(customInterfaceElementList[i], elementState); + TextChanged(customInterfaceElementList[i], msg.ReadString()); } - else if (elementState) + else { - ButtonClicked(customInterfaceElementList[i]); + bool elementState = msg.ReadBoolean(); + if (customInterfaceElementList[i].ContinuousSignal) + { + ((GUITickBox)uiElements[i]).Selected = elementState; + TickBoxToggled(customInterfaceElementList[i], elementState); + } + else if (elementState) + { + ButtonClicked(customInterfaceElementList[i]); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index 7deb2705e..bf7dd68a1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -20,10 +20,10 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, Wire wire, Color color, Vector2 offset, float depth, float width = 0.3f) { spriteBatch.Draw(wire.wireSprite.Texture, - new Vector2(start.X + offset.X, -(start.Y + offset.Y)), null, color, + new Vector2(start.X + offset.X, -(start.Y + offset.Y)), wire.wireSprite.SourceRect, color, -angle, new Vector2(0.0f, wire.wireSprite.size.Y / 2.0f), - new Vector2(length / wire.wireSprite.Texture.Width, width), + new Vector2(length / wire.wireSprite.size.X, width), SpriteEffects.None, depth); } @@ -34,10 +34,10 @@ namespace Barotrauma.Items.Components end.Y = -end.Y; spriteBatch.Draw(wire.wireSprite.Texture, - start, null, color, + start, wire.wireSprite.SourceRect, color, MathUtils.VectorToAngle(end - start), new Vector2(0.0f, wire.wireSprite.size.Y / 2.0f), - new Vector2((Vector2.Distance(start, end)) / wire.wireSprite.Texture.Width, width), + new Vector2((Vector2.Distance(start, end)) / wire.wireSprite.size.X, width), SpriteEffects.None, depth); } @@ -50,6 +50,13 @@ namespace Barotrauma.Items.Components private static int? selectedNodeIndex; private static int? highlightedNodeIndex; + [Serialize(0.3f, false)] + public float Width + { + get; + set; + } + public Vector2 DrawSize { get { return sectionExtents; } @@ -72,7 +79,7 @@ namespace Barotrauma.Items.Components foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "wiresprite") + if (subElement.Name.ToString().Equals("wiresprite", StringComparison.OrdinalIgnoreCase)) { overrideSprite = new Sprite(subElement); break; @@ -109,20 +116,20 @@ namespace Barotrauma.Items.Components { foreach (WireSection section in sections) { - section.Draw(spriteBatch, this, Screen.Selected == GameMain.GameScreen ? higlightColor : editorHighlightColor, drawOffset, depth + 0.00001f, 0.7f); + section.Draw(spriteBatch, this, Screen.Selected == GameMain.GameScreen ? higlightColor : editorHighlightColor, drawOffset, depth + 0.00001f, Width * 2.0f); } } else if (item.IsSelected) { foreach (WireSection section in sections) { - section.Draw(spriteBatch, this, editorSelectedColor, drawOffset, depth + 0.00001f, 0.7f); + section.Draw(spriteBatch, this, editorSelectedColor, drawOffset, depth + 0.00001f, Width * 2.0f); } } foreach (WireSection section in sections) { - section.Draw(spriteBatch, this, item.Color, drawOffset, depth, 0.3f); + section.Draw(spriteBatch, this, item.Color, drawOffset, depth, Width); } if (nodes.Count > 0) @@ -167,13 +174,13 @@ namespace Barotrauma.Items.Components spriteBatch, this, new Vector2(nodes[nodes.Count - 1].X, nodes[nodes.Count - 1].Y) + drawOffset, new Vector2(newNodePos.X, newNodePos.Y) + drawOffset, - item.Color, 0.0f, 0.3f); + item.Color, 0.0f, Width); WireSection.Draw( spriteBatch, this, new Vector2(newNodePos.X, newNodePos.Y) + drawOffset, item.DrawPosition, - item.Color, itemDepth, 0.3f); + item.Color, itemDepth, Width); GUI.DrawRectangle(spriteBatch, new Vector2(newNodePos.X + drawOffset.X, -(newNodePos.Y + drawOffset.Y)) - Vector2.One * 3, Vector2.One * 6, item.Color); } @@ -183,7 +190,7 @@ namespace Barotrauma.Items.Components spriteBatch, this, new Vector2(nodes[nodes.Count - 1].X, nodes[nodes.Count - 1].Y) + drawOffset, item.DrawPosition, - item.Color, 0.0f, 0.3f); + item.Color, 0.0f, Width); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index c2371bcb7..d61908320 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -142,7 +142,7 @@ namespace Barotrauma.Items.Components private void DrawCharacterInfo(SpriteBatch spriteBatch, Character target, float alpha = 1.0f) { - Vector2 hudPos = GameMain.GameScreen.Cam.WorldToScreen(target.WorldPosition); + Vector2 hudPos = GameMain.GameScreen.Cam.WorldToScreen(target.DrawPosition); hudPos += Vector2.UnitX * 50.0f; List texts = new List(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index ba394badc..4b35cf0e6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -223,11 +223,14 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { Vector2 drawPos = new Vector2(item.Rect.X + transformedBarrelPos.X, item.Rect.Y - transformedBarrelPos.Y); - if (item.Submarine != null) drawPos += item.Submarine.DrawPosition; + if (item.Submarine != null) + { + drawPos += item.Submarine.DrawPosition; + } drawPos.Y = -drawPos.Y; float recoilOffset = 0.0f; - if (RecoilDistance > 0.0f && recoilTimer > 0.0f) + if (Math.Abs(RecoilDistance) > 0.0f && recoilTimer > 0.0f) { //move the barrel backwards 0.1 seconds after launching if (recoilTimer >= Math.Max(Reload, 0.1f) - 0.1f) @@ -369,13 +372,26 @@ namespace Barotrauma.Items.Components tooltipOffset = new Vector2(size / 2 + 5, -10), inputAreaMargin = 20, RequireMouseOn = false - }; + }; widgets.Add(id, widget); initMethod?.Invoke(widget); } return widget; } + private void GetAvailablePower(out float availableCharge, out float availableCapacity) + { + var batteries = item.GetConnectedComponents(); + + availableCharge = 0.0f; + availableCapacity = 0.0f; + foreach (PowerContainer battery in batteries) + { + availableCharge += battery.Charge; + availableCapacity += battery.Capacity; + } + } + /// /// Returns correct angle between -2PI and +2PI /// @@ -488,11 +504,17 @@ namespace Barotrauma.Items.Components public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { UInt16 projectileID = msg.ReadUInt16(); - //projectile removed, do nothing - if (projectileID == 0) return; + float newTargetRotation = msg.ReadRangedSingle(minRotation, maxRotation, 8); - Item projectile = Entity.FindEntityByID(projectileID) as Item; - if (projectile == null) + if (Character.Controlled == null || user != Character.Controlled) + { + targetRotation = newTargetRotation; + } + + //projectile removed, do nothing + if (projectileID == 0) { return; } + + if (!(Entity.FindEntityByID(projectileID) is Item projectile)) { DebugConsole.ThrowError("Failed to launch a projectile - item with the ID \"" + projectileID + " not found"); return; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs index 93249475c..a49647775 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs @@ -150,9 +150,9 @@ namespace Barotrauma.Items.Components if (joint == null) { string errorMsg = "Error while reading a docking port network event (Dock method did not create a joint between the ports)." + - " Submarine: " + (item.Submarine?.Name ?? "null") + - ", target submarine: " + (DockingTarget.item.Submarine?.Name ?? "null"); - if (item.Submarine?.DockedTo.Contains(DockingTarget.item.Submarine) ?? false) + " Submarine: " + (item.Submarine?.Info.Name ?? "null") + + ", target submarine: " + (DockingTarget.item.Submarine?.Info.Name ?? "null"); + if (item.Submarine?.ConnectedDockingPorts.ContainsKey(DockingTarget.item.Submarine) ?? false) { errorMsg += "\nAlready docked."; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index b7b4eb4d5..370dde2e6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -45,12 +45,17 @@ namespace Barotrauma public float QuickUseTimer; public string QuickUseButtonToolTip; + public bool IsMoving = false; + private static Rectangle offScreenRect = new Rectangle(new Point(-1000, 0), Point.Zero); public GUIComponent.ComponentState EquipButtonState; public Rectangle EquipButtonRect { get { + // Returns a point off-screen, Rectangle.Empty places buttons in the top left of the screen + if (IsMoving) return offScreenRect; + int buttonDir = Math.Sign(SubInventoryDir); float sizeY = Inventory.UnequippedIndicator.size.Y * Inventory.UIScale * Inventory.IndicatorScaleAdjustment; @@ -284,6 +289,7 @@ namespace Barotrauma } protected static HashSet highlightedSubInventorySlots = new HashSet(); + private static List subInventorySlotsToDraw = new List(); protected static SlotReference selectedSlot; @@ -1048,11 +1054,14 @@ namespace Barotrauma return hoverArea; } + public static void DrawFront(SpriteBatch spriteBatch) { - if (GUI.PauseMenuOpen || GUI.SettingsMenuOpen) return; + if (GUI.PauseMenuOpen || GUI.SettingsMenuOpen) { return; } - foreach (var slot in highlightedSubInventorySlots) + subInventorySlotsToDraw.Clear(); + subInventorySlotsToDraw.AddRange(highlightedSubInventorySlots); + foreach (var slot in subInventorySlotsToDraw) { int slotIndex = Array.IndexOf(slot.ParentInventory.slots, slot.Slot); if (slotIndex > -1 && slotIndex < slot.ParentInventory.slots.Length) @@ -1136,11 +1145,11 @@ namespace Barotrauma /*if (inventory != null && (CharacterInventory.PersonalSlots.HasFlag(type) || (inventory.isSubInventory && (inventory.Owner as Item) != null && (inventory.Owner as Item).AllowedSlots.Any(a => CharacterInventory.PersonalSlots.HasFlag(a))))) { - slotColor = slot.IsHighlighted ? GUIColorSettings.EquipmentSlotColor : GUIColorSettings.EquipmentSlotColor * 0.8f; + slotColor = slot.IsHighlighted ? GUI.Style.EquipmentSlotColor : GUI.Style.EquipmentSlotColor * 0.8f; } else { - slotColor = slot.IsHighlighted ? GUIColorSettings.InventorySlotColor : GUIColorSettings.InventorySlotColor * 0.8f; + slotColor = slot.IsHighlighted ? GUI.Style.InventorySlotColor : GUI.Style.InventorySlotColor * 0.8f; }*/ if (inventory != null && inventory.Locked) { slotColor = Color.Gray * 0.5f; } @@ -1199,13 +1208,15 @@ namespace Barotrauma dir < 0 ? rect.Bottom + HUDLayoutSettings.Padding / 2 : rect.Y - HUDLayoutSettings.Padding / 2 - ContainedIndicatorHeight, rect.Width, ContainedIndicatorHeight); containedIndicatorArea.Inflate(-4, 0); + Color backgroundColor = GUI.Style.ColorInventoryBackground; + if (itemContainer.ContainedStateIndicator?.Texture == null) { containedIndicatorArea.Inflate(0, -2); - GUI.DrawRectangle(spriteBatch, containedIndicatorArea, Color.Gray * 0.9f, true); + GUI.DrawRectangle(spriteBatch, containedIndicatorArea, backgroundColor, true); GUI.DrawRectangle(spriteBatch, new Rectangle(containedIndicatorArea.X, containedIndicatorArea.Y, (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Height), - ToolBox.GradientLerp(containedState, Color.Red, Color.Orange, Color.LightGreen) * 0.8f, true); + ToolBox.GradientLerp(containedState, GUI.Style.ColorInventoryEmpty, GUI.Style.ColorInventoryHalf, GUI.Style.ColorInventoryFull) * 0.8f, true); GUI.DrawLine(spriteBatch, new Vector2(containedIndicatorArea.X + (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Y), new Vector2(containedIndicatorArea.X + (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Bottom), @@ -1224,12 +1235,12 @@ namespace Barotrauma } indicatorSprite.Draw(spriteBatch, containedIndicatorArea.Center.ToVector2(), - (inventory != null && inventory.Locked) ? Color.Gray * 0.5f : Color.Gray * 0.9f, + (inventory != null && inventory.Locked) ? backgroundColor * 0.5f : backgroundColor, origin: indicatorSprite.size / 2, rotate: 0.0f, scale: indicatorScale); - Color indicatorColor = ToolBox.GradientLerp(containedState, Color.Red, Color.Orange, Color.LightGreen); + Color indicatorColor = ToolBox.GradientLerp(containedState, GUI.Style.ColorInventoryEmpty, GUI.Style.ColorInventoryHalf, GUI.Style.ColorInventoryFull); if (inventory != null && inventory.Locked) { indicatorColor *= 0.5f; } spriteBatch.Draw(indicatorSprite.Texture, containedIndicatorArea.Center.ToVector2(), diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index d4ded9f01..4f5d60369 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -29,14 +29,8 @@ namespace Barotrauma private bool editingHUDRefreshPending; private float editingHUDRefreshTimer; - class SpriteState - { - public float RotationState; - public float OffsetState; - public bool IsActive = true; - } - private Dictionary spriteAnimState = new Dictionary(); + private readonly Dictionary spriteAnimState = new Dictionary(); private Sprite activeSprite; public override Sprite Sprite @@ -160,7 +154,7 @@ namespace Barotrauma foreach (var decorativeSprite in ((ItemPrefab)prefab).DecorativeSprites) { decorativeSprite.Sprite.EnsureLazyLoaded(); - spriteAnimState.Add(decorativeSprite, new SpriteState()); + spriteAnimState.Add(decorativeSprite, new DecorativeSprite.State()); } } @@ -197,8 +191,8 @@ namespace Barotrauma public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { - if (!Visible || (!editing && HiddenInGame)) return; - if (editing && !ShowItems) return; + if (!Visible || (!editing && HiddenInGame)) { return; } + if (editing && !ShowItems) { return; } Color color = IsHighlighted && !GUI.DisableItemHighlights && Screen.Selected != GameMain.GameScreen ? GUI.Style.Orange : GetSpriteColor(); //if (IsSelected && editing) color = Color.Lerp(color, Color.Gold, 0.5f); @@ -333,15 +327,6 @@ namespace Barotrauma if (GameMain.DebugDraw) { - aiTarget?.Draw(spriteBatch); - var containedItems = ContainedItems; - if (containedItems != null) - { - foreach (Item item in containedItems) - { - item.AiTarget?.Draw(spriteBatch); - } - } if (body != null) { body.DebugDraw(spriteBatch, Color.White); @@ -407,46 +392,7 @@ namespace Barotrauma public void UpdateSpriteStates(float deltaTime) { - foreach (int spriteGroup in Prefab.DecorativeSpriteGroups.Keys) - { - for (int i = 0; i < Prefab.DecorativeSpriteGroups[spriteGroup].Count; i++) - { - var decorativeSprite = Prefab.DecorativeSpriteGroups[spriteGroup][i]; - if (decorativeSprite == null) { continue; } - if (spriteGroup > 0) - { - int activeSpriteIndex = ID % Prefab.DecorativeSpriteGroups[spriteGroup].Count; - if (i != activeSpriteIndex) - { - spriteAnimState[decorativeSprite].IsActive = false; - continue; - } - } - - //check if the sprite is active (whether it should be drawn or not) - var spriteState = spriteAnimState[decorativeSprite]; - spriteState.IsActive = true; - foreach (PropertyConditional conditional in decorativeSprite.IsActiveConditionals) - { - if (!ConditionalMatches(conditional)) - { - spriteState.IsActive = false; - break; - } - } - if (!spriteState.IsActive) { continue; } - - //check if the sprite should be animated - bool animate = true; - foreach (PropertyConditional conditional in decorativeSprite.AnimationConditionals) - { - if (!ConditionalMatches(conditional)) { animate = false; break; } - } - if (!animate) { continue; } - spriteState.OffsetState += deltaTime; - spriteState.RotationState += deltaTime; - } - } + DecorativeSprite.UpdateSpriteStates(Prefab.DecorativeSpriteGroups, spriteAnimState, ID, deltaTime, ConditionalMatches); } public override void UpdateEditing(Camera cam) @@ -1227,6 +1173,8 @@ namespace Barotrauma } } + byte bodyType = msg.ReadByte(); + byte teamID = msg.ReadByte(); bool tagsChanged = msg.ReadBoolean(); string tags = ""; @@ -1284,6 +1232,11 @@ namespace Barotrauma ID = itemId }; + if (item.body != null) + { + item.body.BodyType = (BodyType)bodyType; + } + foreach (WifiComponent wifiComponent in item.GetComponents()) { wifiComponent.TeamID = (Character.TeamType)teamID; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index b2fc35ae7..ac8c01edf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -24,7 +24,7 @@ namespace Barotrauma public override void Draw(SpriteBatch sb, bool editing, bool back = true) { - if (GameMain.DebugDraw) + if (!GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f) { Vector2 center = new Vector2(WorldRect.X + rect.Width / 2.0f, -(WorldRect.Y - rect.Height / 2.0f)); GUI.DrawLine(sb, center, center + new Vector2(flowForce.X, -flowForce.Y) / 10.0f, GUI.Style.Red); @@ -41,7 +41,7 @@ namespace Barotrauma } } - if (!editing || !ShowGaps) return; + if (!editing || !ShowGaps) { return; } Color clr = (open == 0.0f) ? GUI.Style.Red : Color.Cyan; if (IsHighlighted) clr = Color.Gold; @@ -76,32 +76,35 @@ namespace Barotrauma clr * 0.6f, width: lineWidth); } - for (int i = 0; i < linkedTo.Count; i++) + if (linkedTo.Count != 2 || linkedTo[0] != linkedTo[1]) { - Vector2 dir = IsHorizontal ? - new Vector2(Math.Sign(linkedTo[i].Rect.Center.X - rect.Center.X), 0.0f) - : new Vector2(0.0f, Math.Sign((linkedTo[i].Rect.Y - linkedTo[i].Rect.Height / 2.0f) - (rect.Y - rect.Height / 2.0f))); - - Vector2 arrowPos = new Vector2(WorldRect.Center.X, -(WorldRect.Y - WorldRect.Height / 2)); - arrowPos += new Vector2(dir.X * (WorldRect.Width / 2), dir.Y * (WorldRect.Height / 2)); - - float arrowWidth = 32.0f; - float arrowSize = 15.0f; - - bool invalidDir = false; - if (dir == Vector2.Zero) + for (int i = 0; i < linkedTo.Count; i++) { - invalidDir = true; - dir = IsHorizontal ? Vector2.UnitX : Vector2.UnitY; - } + Vector2 dir = IsHorizontal ? + new Vector2(Math.Sign(linkedTo[i].Rect.Center.X - rect.Center.X), 0.0f) + : new Vector2(0.0f, Math.Sign((linkedTo[i].Rect.Y - linkedTo[i].Rect.Height / 2.0f) - (rect.Y - rect.Height / 2.0f))); - GUI.Arrow.Draw(sb, - arrowPos, invalidDir ? Color.Red : clr * 0.8f, - GUI.Arrow.Origin, MathUtils.VectorToAngle(dir) + MathHelper.PiOver2, - IsHorizontal ? - new Vector2(Math.Min(rect.Height, arrowWidth) / GUI.Arrow.size.X, arrowSize / GUI.Arrow.size.Y) : - new Vector2(Math.Min(rect.Width, arrowWidth) / GUI.Arrow.size.X, arrowSize / GUI.Arrow.size.Y), - SpriteEffects.None, depth); + Vector2 arrowPos = new Vector2(WorldRect.Center.X, -(WorldRect.Y - WorldRect.Height / 2)); + arrowPos += new Vector2(dir.X * (WorldRect.Width / 2), dir.Y * (WorldRect.Height / 2)); + + float arrowWidth = 32.0f; + float arrowSize = 15.0f; + + bool invalidDir = false; + if (dir == Vector2.Zero) + { + invalidDir = true; + dir = IsHorizontal ? Vector2.UnitX : Vector2.UnitY; + } + + GUI.Arrow.Draw(sb, + arrowPos, invalidDir ? Color.Red : clr * 0.8f, + GUI.Arrow.Origin, MathUtils.VectorToAngle(dir) + MathHelper.PiOver2, + IsHorizontal ? + new Vector2(Math.Min(rect.Height, arrowWidth) / GUI.Arrow.size.X, arrowSize / GUI.Arrow.size.Y) : + new Vector2(Math.Min(rect.Width, arrowWidth) / GUI.Arrow.size.X, arrowSize / GUI.Arrow.size.Y), + SpriteEffects.None, depth); + } } if (IsSelected) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index baf8124c6..219996222 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -246,9 +246,7 @@ namespace Barotrauma if (!ShowHulls && !GameMain.DebugDraw) return; - if (!editing && !GameMain.DebugDraw) return; - - if (aiTarget != null) aiTarget.Draw(spriteBatch); + if (!editing && (!GameMain.DebugDraw || Screen.Selected.Cam.Zoom < 0.1f)) return; Rectangle drawRect = Submarine == null ? rect : new Rectangle((int)(Submarine.DrawPosition.X + rect.X), (int)(Submarine.DrawPosition.Y + rect.Y), rect.Width, rect.Height); @@ -271,7 +269,8 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, (int)(100 * Math.Min(waterVolume / Volume, 1.0f))), Color.Cyan, true); if (WaterVolume > Volume) { - GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, (int)(100 * (waterVolume - Volume) / MaxCompress)), GUI.Style.Red, true); + float maxExcessWater = Volume * MaxCompress; + GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, (int)(100 * (waterVolume - Volume) / maxExcessWater)), GUI.Style.Red, true); } GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, 100), Color.Black); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs index 72d5db102..94501138a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs @@ -39,7 +39,7 @@ namespace Barotrauma foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "sprite") continue; + if (!subElement.Name.ToString().Equals("sprite", System.StringComparison.OrdinalIgnoreCase)) { continue; } Sprite = new Sprite(subElement, lazyLoad: true); break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs index 41ff02ce6..a59df6509 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs @@ -1,7 +1,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using FarseerPhysics.Dynamics; +using System; using System.Linq; using System.Collections.Generic; using FarseerPhysics; @@ -54,7 +54,7 @@ namespace Barotrauma if (renderer == null) return; renderer.Draw(spriteBatch, cam); - if (GameMain.DebugDraw) + if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f) { foreach (InterestingPosition pos in positionsOfInterest) { @@ -78,6 +78,35 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, ruinArea, Color.DarkSlateBlue, false, 0, 5); } + + foreach (var positions in wreckPositions.Values) + { + for (int i = 0; i < positions.Count; i++) + { + float t = (i + 1) / (float)positions.Count; + float multiplier = MathHelper.Lerp(0, 1, t); + Color color = Color.Red * multiplier; + var pos = positions[i]; + pos.Y = -pos.Y; + var size = new Vector2(100); + GUI.DrawRectangle(spriteBatch, pos - size / 2, size, color, thickness: 10); + if (i < positions.Count - 1) + { + var nextPos = positions[i + 1]; + nextPos.Y = -nextPos.Y; + GUI.DrawLine(spriteBatch, pos, nextPos, color, width: 10); + } + } + } + foreach (var rects in blockedRects.Values) + { + foreach (var rect in rects) + { + Rectangle newRect = rect; + newRect.Y = -newRect.Y; + GUI.DrawRectangle(spriteBatch, newRect, Color.Red, thickness: 5); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs index e8ca4f341..fdc5a0afe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs @@ -126,7 +126,7 @@ namespace Barotrauma int j = 0; foreach (XElement subElement in Prefab.Config.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "deformablesprite") continue; + if (!subElement.Name.ToString().Equals("deformablesprite", StringComparison.OrdinalIgnoreCase)) { continue; } foreach (XElement animationElement in subElement.Elements()) { var newDeformation = SpriteDeformation.Load(animationElement, Prefab.Name); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index b961d85be..dd66c27c5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -4,6 +4,7 @@ using Barotrauma.SpriteDeformations; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Xml.Linq; namespace Barotrauma @@ -120,7 +121,7 @@ namespace Barotrauma SerializableProperty.SerializeProperties(this, element); - foreach (XElement subElement in element.Elements()) + foreach (XElement subElement in element.Elements().ToList()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -139,7 +140,7 @@ namespace Barotrauma break; } } - + foreach (LightSourceParams lightSourceParams in LightSourceParams) { var lightElement = new XElement("LightSource"); @@ -160,7 +161,7 @@ namespace Barotrauma bool elementFound = false; foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "overridecommonness" + if (subElement.Name.ToString().Equals("overridecommonness", System.StringComparison.OrdinalIgnoreCase) && subElement.GetAttributeString("leveltype", "") == overrideCommonness.Key) { subElement.Attribute("commonness").Value = overrideCommonness.Value.ToString("G", CultureInfo.InvariantCulture); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs index 07ac14679..55a24802c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs @@ -228,7 +228,7 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Camera cam) { - if (GameMain.DebugDraw && cam.Zoom > 0.05f) + if (GameMain.DebugDraw && cam.Zoom > 0.1f) { var cells = level.GetCells(cam.WorldViewCenter, 2); foreach (VoronoiCell cell in cells) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index fe2abefaf..8a9114105 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -977,7 +977,7 @@ namespace Barotrauma.Lights origin, -Rotation, SpriteScale, LightSpriteEffect); } - if (GameMain.DebugDraw) + if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f) { Vector2 drawPos = position; if (ParentSub != null) { drawPos += ParentSub.DrawPosition; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs index 215fd0d52..22c2b7627 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs @@ -12,10 +12,10 @@ namespace Barotrauma { public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { - if (!editing || wallVertices == null) return; + if (!editing || wallVertices == null) { return; } - Color color = (IsHighlighted) ? GUI.Style.Orange : GUI.Style.Green; - if (IsSelected) color = GUI.Style.Red; + Color color = IsHighlighted ? GUI.Style.Orange : GUI.Style.Green; + if (IsSelected) { color = GUI.Style.Red; } Vector2 pos = Position; @@ -37,11 +37,7 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, pos + Vector2.UnitY * 50.0f, pos - Vector2.UnitY * 50.0f, color, 0.0f, 5); GUI.DrawLine(spriteBatch, pos + Vector2.UnitX * 50.0f, pos - Vector2.UnitX * 50.0f, color, 0.0f, 5); - Rectangle drawRect = rect; - drawRect.Y = -rect.Y; - GUI.DrawRectangle(spriteBatch, drawRect, GUI.Style.Red, true); - - if (!Item.ShowLinks) return; + if (!Item.ShowLinks) { return; } foreach (MapEntity e in linkedTo) { @@ -135,7 +131,7 @@ namespace Barotrauma return false; } - XDocument doc = Submarine.OpenFile(pathBox.Text); + XDocument doc = SubmarineInfo.OpenFile(pathBox.Text); if (doc == null || doc.Root == null) return false; pathBox.Flash(GUI.Style.Green); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index f485e4d9c..a71ea1459 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -18,6 +18,11 @@ namespace Barotrauma public static Vector2 StartMovingPos => startMovingPos; + // Quick undo/redo for size and movement only. TODO: Remove if we do a more general implementation. + private Memento rectMemento; + + public event Action Resized; + private static bool resizing; private int resizeDirX, resizeDirY; @@ -589,6 +594,14 @@ namespace Barotrauma if (structure.FlippedX && structure.Prefab.CanSpriteFlipX) spriteEffects ^= SpriteEffects.FlipHorizontally; if (structure.flippedY && structure.Prefab.CanSpriteFlipY) spriteEffects ^= SpriteEffects.FlipVertically; } + else if (e is WayPoint wayPoint) + { + Vector2 drawPos = e.WorldPosition; + drawPos.Y = -drawPos.Y; + drawPos += moveAmount; + wayPoint.Draw(spriteBatch, drawPos); + continue; + } e.prefab?.DrawPlacing(spriteBatch, new Rectangle(e.WorldRect.Location + new Point((int)moveAmount.X, (int)-moveAmount.Y), e.WorldRect.Size), e.Scale, spriteEffects); GUI.DrawRectangle(spriteBatch, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 121bad258..e60fb34c9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -18,11 +18,13 @@ namespace Barotrauma private List convexHulls; + private readonly Dictionary spriteAnimState = new Dictionary(); + public override bool SelectableInEditor { get { - return HasBody ? ShowWalls : ShowStructures;; + return HasBody ? ShowWalls : ShowStructures; } } @@ -38,6 +40,12 @@ namespace Barotrauma { Prefab.sprite?.EnsureLazyLoaded(); Prefab.BackgroundSprite?.EnsureLazyLoaded(); + + foreach (var decorativeSprite in Prefab.DecorativeSprites) + { + decorativeSprite.Sprite.EnsureLazyLoaded(); + spriteAnimState.Add(decorativeSprite, new DecorativeSprite.State()); + } } partial void CreateConvexHull(Vector2 position, Vector2 size, float rotation) @@ -156,19 +164,19 @@ namespace Barotrauma { Rectangle worldRect = WorldRect; - if (worldRect.X > worldView.Right || worldRect.Right < worldView.X) return false; - if (worldRect.Y < worldView.Y - worldView.Height || worldRect.Y - worldRect.Height > worldView.Y) return false; + if (worldRect.X > worldView.Right || worldRect.Right < worldView.X) { return false; } + if (worldRect.Y < worldView.Y - worldView.Height || worldRect.Y - worldRect.Height > worldView.Y) { return false; } return true; } public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { - if (prefab.sprite == null) return; + if (prefab.sprite == null) { return; } if (editing) { - if (!HasBody && !ShowStructures) return; - if (HasBody && !ShowWalls) return; + if (!HasBody && !ShowStructures) { return; } + if (HasBody && !ShowWalls) { return; } } Draw(spriteBatch, editing, back, null); @@ -188,12 +196,13 @@ namespace Barotrauma private void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Effect damageEffect = null) { - if (prefab.sprite == null) return; + if (prefab.sprite == null) { return; } if (editing) { - if (!HasBody && !ShowStructures) return; - if (HasBody && !ShowWalls) return; + if (!HasBody && !ShowStructures) { return; } + if (HasBody && !ShowWalls) { return; } } + else if (HiddenInGame) { return; } Color color = IsHighlighted ? GUI.Style.Orange : spriteColor; if (IsSelected && editing) @@ -254,7 +263,7 @@ namespace Barotrauma spriteBatch, new Vector2(rect.X + drawOffset.X, -(rect.Y + drawOffset.Y)), new Vector2(rect.Width, rect.Height), - color: color, + color: Prefab.BackgroundSpriteColor, textureScale: TextureScale * Scale, startOffset: backGroundOffset, depth: Math.Max(Prefab.BackgroundSprite.Depth + (ID % 255) * 0.000001f, depth + 0.000001f)); @@ -318,10 +327,20 @@ namespace Barotrauma depth: depth, textureScale: TextureScale * Scale); } + + foreach (var decorativeSprite in Prefab.DecorativeSprites) + { + if (!spriteAnimState[decorativeSprite].IsActive) { continue; } + float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState) * Scale; + decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, + rotation, Scale, prefab.sprite.effects, + depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.sprite.Depth), 0.999f)); + } prefab.sprite.effects = oldEffects; } - if (GameMain.DebugDraw) + if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.5f) { if (Bodies != null) { @@ -350,11 +369,67 @@ namespace Barotrauma } } } - - AiTarget?.Draw(spriteBatch); } } + public void UpdateSpriteStates(float deltaTime) + { + DecorativeSprite.UpdateSpriteStates(Prefab.DecorativeSpriteGroups, spriteAnimState, ID, deltaTime, ConditionalMatches); + foreach (int spriteGroup in Prefab.DecorativeSpriteGroups.Keys) + { + for (int i = 0; i < Prefab.DecorativeSpriteGroups[spriteGroup].Count; i++) + { + var decorativeSprite = Prefab.DecorativeSpriteGroups[spriteGroup][i]; + if (decorativeSprite == null) { continue; } + if (spriteGroup > 0) + { + int activeSpriteIndex = ID % Prefab.DecorativeSpriteGroups[spriteGroup].Count; + if (i != activeSpriteIndex) + { + spriteAnimState[decorativeSprite].IsActive = false; + continue; + } + } + + //check if the sprite is active (whether it should be drawn or not) + var spriteState = spriteAnimState[decorativeSprite]; + spriteState.IsActive = true; + foreach (PropertyConditional conditional in decorativeSprite.IsActiveConditionals) + { + if (!ConditionalMatches(conditional)) + { + spriteState.IsActive = false; + break; + } + } + if (!spriteState.IsActive) { continue; } + + //check if the sprite should be animated + bool animate = true; + foreach (PropertyConditional conditional in decorativeSprite.AnimationConditionals) + { + if (!ConditionalMatches(conditional)) { animate = false; break; } + } + if (!animate) { continue; } + spriteState.OffsetState += deltaTime; + spriteState.RotationState += deltaTime; + } + } + } + + private bool ConditionalMatches(PropertyConditional conditional) + { + if (!string.IsNullOrEmpty(conditional.TargetItemComponentName)) + { + return false; + } + else + { + if (!conditional.Matches(this)) { return false; } + } + return true; + } + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { byte sectionCount = msg.ReadByte(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs index 34b5a4d12..dec803786 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs @@ -1,11 +1,21 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Collections.Generic; namespace Barotrauma { partial class StructurePrefab : MapEntityPrefab { + public Color BackgroundSpriteColor + { + get; + private set; + } + + public List DecorativeSprites = new List(); + public Dictionary> DecorativeSpriteGroups = new Dictionary>(); + public override void UpdatePlacing(Camera cam) { Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); @@ -40,7 +50,7 @@ namespace Barotrauma if (PlayerInput.PrimaryMouseButtonReleased()) { newRect.Location -= MathUtils.ToPoint(Submarine.MainSub.Position); - var structure = new Structure(newRect, this, Submarine.MainSub) + new Structure(newRect, this, Submarine.MainSub) { Submarine = Submarine.MainSub }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index c29f8c331..aa88465d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -35,7 +35,6 @@ namespace Barotrauma partial class Submarine : Entity, IServerSerializable { - public Sprite PreviewImage; public static Vector2 MouseToWorldGrid(Camera cam, Submarine sub) { Vector2 position = PlayerInput.MousePosition; @@ -56,6 +55,8 @@ namespace Barotrauma private static List roundSounds = null; public static RoundSound LoadRoundSound(XElement element, bool stream = false) { + if (GameMain.SoundManager?.Disabled ?? true) { return null; } + string filename = element.GetAttributeString("file", ""); if (string.IsNullOrEmpty(filename)) filename = element.GetAttributeString("sound", ""); @@ -222,7 +223,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, worldBorders, Color.White, false, 0, 5); - if (sub.subBody.PositionBuffer.Count < 2) continue; + if (sub.subBody == null || sub.subBody.PositionBuffer.Count < 2) continue; Vector2 prevPos = ConvertUnits.ToDisplayUnits(sub.subBody.PositionBuffer[0].Position); prevPos.Y = -prevPos.Y; @@ -329,125 +330,6 @@ namespace Barotrauma } } - public static bool SaveCurrent(string filePath, MemoryStream previewImage = null) - { - if (MainSub == null) - { - MainSub = new Submarine(filePath); - } - - MainSub.filePath = filePath; - return MainSub.SaveAs(filePath, previewImage); - } - - public void CreatePreviewWindow(GUIComponent parent) - { - var content = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null); - - if (PreviewImage == null) - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), TextManager.Get(SavedSubmarines.Contains(this) ? "SubPreviewImageNotFound" : "SubNotDownloaded")); - } - else - { - var submarinePreviewBackground = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), style: null) { Color = Color.Black }; - new GUIImage(new RectTransform(new Vector2(0.98f), submarinePreviewBackground.RectTransform, Anchor.Center), PreviewImage, scaleToFit: true); - new GUIFrame(new RectTransform(Vector2.One, submarinePreviewBackground.RectTransform), "InnerGlow", color: Color.Black); - } - var descriptionBox = new GUIListBox(new RectTransform(new Vector2(1, 0.5f), content.RectTransform, Anchor.BottomCenter)) - { - UserData = "descriptionbox", - ScrollBarVisible = true, - Spacing = 5 - }; - - //space - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.03f), descriptionBox.Content.RectTransform), style: null); - - new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), TextManager.Get("submarine.name." + Name, true) ?? Name, font: GUI.LargeFont, wrap: true) { ForceUpperCase = true, CanBeFocused = false }; - - float leftPanelWidth = 0.6f; - float rightPanelWidth = 0.4f / leftPanelWidth; - - ScalableFont font = descriptionBox.Rect.Width < 350 ? GUI.SmallFont : GUI.Font; - - Vector2 realWorldDimensions = Dimensions * Physics.DisplayToRealWorldRatio; - if (realWorldDimensions != Vector2.Zero) - { - string dimensionsStr = TextManager.GetWithVariables("DimensionsFormat", new string[2] { "[width]", "[height]" }, new string[2] { ((int)realWorldDimensions.X).ToString(), ((int)realWorldDimensions.Y).ToString() }); - - var dimensionsText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), - TextManager.Get("Dimensions"), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), dimensionsText.RectTransform, Anchor.TopRight, Pivot.TopLeft), - dimensionsStr, textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - dimensionsText.RectTransform.MinSize = new Point(0, dimensionsText.Children.First().Rect.Height); - } - - if (RecommendedCrewSizeMax > 0) - { - var crewSizeText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), - TextManager.Get("RecommendedCrewSize"), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crewSizeText.RectTransform, Anchor.TopRight, Pivot.TopLeft), - RecommendedCrewSizeMin + " - " + RecommendedCrewSizeMax, textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - crewSizeText.RectTransform.MinSize = new Point(0, crewSizeText.Children.First().Rect.Height); - } - - if (!string.IsNullOrEmpty(RecommendedCrewExperience)) - { - var crewExperienceText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), - TextManager.Get("RecommendedCrewExperience"), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crewExperienceText.RectTransform, Anchor.TopRight, Pivot.TopLeft), - TextManager.Get(RecommendedCrewExperience), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - crewExperienceText.RectTransform.MinSize = new Point(0, crewExperienceText.Children.First().Rect.Height); - } - - if (RequiredContentPackages.Any()) - { - var contentPackagesText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), - TextManager.Get("RequiredContentPackages"), textAlignment: Alignment.TopLeft, font: font) - { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), contentPackagesText.RectTransform, Anchor.TopRight, Pivot.TopLeft), - string.Join(", ", RequiredContentPackages), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - contentPackagesText.RectTransform.MinSize = new Point(0, contentPackagesText.Children.First().Rect.Height); - } - - // show what game version the submarine was created on - if (!IsVanillaSubmarine() && GameVersion != null) - { - var versionText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), - TextManager.Get("serverlistversion"), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), versionText.RectTransform, Anchor.TopRight, Pivot.TopLeft), - GameVersion.ToString(), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - - versionText.RectTransform.MinSize = new Point(0, versionText.Children.First().Rect.Height); - } - - GUITextBlock.AutoScaleAndNormalize(descriptionBox.Content.Children.Where(c => c is GUITextBlock).Cast()); - - //space - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), descriptionBox.Content.RectTransform), style: null); - - if (!string.IsNullOrEmpty(Description)) - { - new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), - TextManager.Get("SaveSubDialogDescription", fallBackTag: "WorkshopItemDescription"), font: GUI.Font, wrap: true) { CanBeFocused = false, ForceUpperCase = true }; - } - - new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), Description, font: font, wrap: true) - { - CanBeFocused = false - }; - } - public void CreateMiniMap(GUIComponent parent, IEnumerable pointsOfInterest = null) { Rectangle worldBorders = GetDockedBorders(); @@ -496,21 +378,6 @@ namespace Barotrauma } } - public bool IsVanillaSubmarine() - { - var vanilla = GameMain.VanillaContent; - if (vanilla != null) - { - var vanillaSubs = vanilla.GetFilesOfType(ContentType.Submarine); - string pathToCompare = filePath.Replace(@"\", @"/").ToLowerInvariant(); - if (vanillaSubs.Any(sub => sub.Replace(@"\", @"/").ToLowerInvariant() == pathToCompare)) - { - return true; - } - } - return false; - } - public void CheckForErrors() { List errorMsgs = new List(); @@ -531,6 +398,11 @@ namespace Barotrauma } } + if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Human)) + { + errorMsgs.Add(TextManager.Get("NoHumanSpawnpointWarning")); + } + if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Path)) { errorMsgs.Add(TextManager.Get("NoWaypointsWarning")); @@ -605,7 +477,7 @@ namespace Barotrauma public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { - var posInfo = PhysicsBody.ClientRead(type, msg, sendingTime, parentDebugName: Name); + var posInfo = PhysicsBody.ClientRead(type, msg, sendingTime, parentDebugName: Info.Name); msg.ReadPadBits(); if (posInfo != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs new file mode 100644 index 000000000..b638ec40f --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -0,0 +1,149 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml.Linq; + +namespace Barotrauma +{ + partial class SubmarineInfo : IDisposable + { + public Sprite PreviewImage; + + partial void InitProjectSpecific() + { + string previewImageData = SubmarineElement.GetAttributeString("previewimage", ""); + if (!string.IsNullOrEmpty(previewImageData)) + { + try + { + using (MemoryStream mem = new MemoryStream(Convert.FromBase64String(previewImageData))) + { + var texture = TextureLoader.FromStream(mem, path: FilePath); + if (texture == null) { throw new Exception("PreviewImage texture returned null"); } + PreviewImage = new Sprite(texture, null, null); + } + } + catch (Exception e) + { + DebugConsole.ThrowError("Loading the preview image of the submarine \"" + Name + "\" failed. The file may be corrupted.", e); + GameAnalyticsManager.AddErrorEventOnce("Submarine..ctor:PreviewImageLoadingFailed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + "Loading the preview image of the submarine \"" + Name + "\" failed. The file may be corrupted."); + PreviewImage = null; + } + } + } + + + public void CreatePreviewWindow(GUIComponent parent) + { + var content = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null); + + if (PreviewImage == null) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), TextManager.Get(SavedSubmarines.Contains(this) ? "SubPreviewImageNotFound" : "SubNotDownloaded")); + } + else + { + var submarinePreviewBackground = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), style: null) { Color = Color.Black }; + new GUIImage(new RectTransform(new Vector2(0.98f), submarinePreviewBackground.RectTransform, Anchor.Center), PreviewImage, scaleToFit: true); + new GUIFrame(new RectTransform(Vector2.One, submarinePreviewBackground.RectTransform), "InnerGlow", color: Color.Black); + } + var descriptionBox = new GUIListBox(new RectTransform(new Vector2(1, 0.5f), content.RectTransform, Anchor.BottomCenter)) + { + UserData = "descriptionbox", + ScrollBarVisible = true, + Spacing = 5 + }; + + //space + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.03f), descriptionBox.Content.RectTransform), style: null); + + new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), TextManager.Get("submarine.name." + Name, true) ?? Name, font: GUI.LargeFont, wrap: true) { ForceUpperCase = true, CanBeFocused = false }; + + float leftPanelWidth = 0.6f; + float rightPanelWidth = 0.4f / leftPanelWidth; + + ScalableFont font = descriptionBox.Rect.Width < 350 ? GUI.SmallFont : GUI.Font; + + Vector2 realWorldDimensions = Dimensions * Physics.DisplayToRealWorldRatio; + if (realWorldDimensions != Vector2.Zero) + { + string dimensionsStr = TextManager.GetWithVariables("DimensionsFormat", new string[2] { "[width]", "[height]" }, new string[2] { ((int)realWorldDimensions.X).ToString(), ((int)realWorldDimensions.Y).ToString() }); + + var dimensionsText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), + TextManager.Get("Dimensions"), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), dimensionsText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + dimensionsStr, textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + dimensionsText.RectTransform.MinSize = new Point(0, dimensionsText.Children.First().Rect.Height); + } + + if (RecommendedCrewSizeMax > 0) + { + var crewSizeText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), + TextManager.Get("RecommendedCrewSize"), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crewSizeText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + RecommendedCrewSizeMin + " - " + RecommendedCrewSizeMax, textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + crewSizeText.RectTransform.MinSize = new Point(0, crewSizeText.Children.First().Rect.Height); + } + + if (!string.IsNullOrEmpty(RecommendedCrewExperience)) + { + var crewExperienceText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), + TextManager.Get("RecommendedCrewExperience"), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crewExperienceText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + TextManager.Get(RecommendedCrewExperience), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + crewExperienceText.RectTransform.MinSize = new Point(0, crewExperienceText.Children.First().Rect.Height); + } + + if (RequiredContentPackages.Any()) + { + var contentPackagesText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), + TextManager.Get("RequiredContentPackages"), textAlignment: Alignment.TopLeft, font: font) + { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), contentPackagesText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + string.Join(", ", RequiredContentPackages), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + contentPackagesText.RectTransform.MinSize = new Point(0, contentPackagesText.Children.First().Rect.Height); + } + + // show what game version the submarine was created on + if (!IsVanillaSubmarine() && GameVersion != null) + { + var versionText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), + TextManager.Get("serverlistversion"), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), versionText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + GameVersion.ToString(), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + + versionText.RectTransform.MinSize = new Point(0, versionText.Children.First().Rect.Height); + } + + GUITextBlock.AutoScaleAndNormalize(descriptionBox.Content.Children.Where(c => c is GUITextBlock).Cast()); + + //space + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), descriptionBox.Content.RectTransform), style: null); + + if (!string.IsNullOrEmpty(Description)) + { + new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), + TextManager.Get("SaveSubDialogDescription", fallBackTag: "WorkshopItemDescription"), font: GUI.Font, wrap: true) + { CanBeFocused = false, ForceUpperCase = true }; + } + + new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), Description, font: font, wrap: true) + { + CanBeFocused = false + }; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 195cb57a4..b4bf3c3e8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -1,17 +1,15 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using Barotrauma.Items.Components; -using System.Linq; namespace Barotrauma { partial class WayPoint : MapEntity { - private static Texture2D iconTexture; - private const int IconSize = 32; - private static int[] iconIndices = { 3, 0, 1, 2 }; + private static Dictionary iconSprites; + private const int WaypointSize = 12, SpawnPointSize = 32; public override bool IsVisible(Rectangle worldView) { @@ -23,58 +21,58 @@ namespace Barotrauma get { return !IsHidden(); } } + public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { - if (!editing && !GameMain.DebugDraw) { return; } - + if (!editing && (!GameMain.DebugDraw || Screen.Selected.Cam.Zoom < 0.1f)) { return; } if (IsHidden()) { return; } - //Rectangle drawRect = - // Submarine == null ? rect : new Rectangle((int)(Submarine.DrawPosition.X + rect.X), (int)(Submarine.DrawPosition.Y + rect.Y), rect.Width, rect.Height); - Vector2 drawPos = Position; - if (Submarine != null) drawPos += Submarine.DrawPosition; + if (Submarine != null) { drawPos += Submarine.DrawPosition; } drawPos.Y = -drawPos.Y; - Color clr = currentHull == null ? Color.Blue : Color.White; + Draw(spriteBatch, drawPos); + } + + public void Draw(SpriteBatch spriteBatch, Vector2 drawPos) + { + Color clr = currentHull == null ? Color.CadetBlue : GUI.Style.Green; + if (spawnType != SpawnType.Path) { clr = Color.Gray; } if (isObstructed) { clr = Color.Black; } - if (IsSelected) clr = GUI.Style.Red; - if (IsHighlighted) clr = Color.DarkRed; + if (IsHighlighted || IsHighlighted) { clr = Color.Lerp(clr, Color.White, 0.8f); } - int iconX = iconIndices[(int)spawnType] * IconSize % iconTexture.Width; - int iconY = (int)(Math.Floor(iconIndices[(int)spawnType] * IconSize / (float)iconTexture.Width)) * IconSize; + int iconSize = spawnType == SpawnType.Path ? WaypointSize : SpawnPointSize; + if (ConnectedGap != null || Ladders != null || Stairs != null || SpawnType != SpawnType.Path) { iconSize = (int)(iconSize * 1.5f); } - int iconSize = IconSize; - if (ConnectedGap != null) + if (IsSelected || IsHighlighted) { - iconSize = (int)(iconSize * 1.5f); - } - if (Ladders != null) - { - iconSize = (int)(iconSize * 1.5f); - } - if (Stairs != null) - { - iconSize = (int)(iconSize * 1.5f); + int glowSize = (int)(iconSize * 1.5f); + GUI.Style.UIGlowCircular.Draw(spriteBatch, + new Rectangle((int)(drawPos.X - glowSize / 2), (int)(drawPos.Y - glowSize / 2), glowSize, glowSize), + Color.White); } - spriteBatch.Draw(iconTexture, - new Rectangle((int)(drawPos.X - iconSize / 2), (int)(drawPos.Y - iconSize / 2), iconSize, iconSize), - new Rectangle(iconX, iconY, IconSize, IconSize), clr); - - //GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.X, -drawRect.Y, rect.Width, rect.Height), clr, true); - - //GUI.SmallFont.DrawString(spriteBatch, Position.ToString(), new Vector2(Position.X, -Position.Y), Color.White); + Sprite sprite = iconSprites[SpawnType]; + if (spawnType == SpawnType.Human && AssignedJob?.Icon != null) + { + sprite = iconSprites[SpawnType.Path]; + } + sprite.Draw(spriteBatch, drawPos, clr, scale: iconSize / (float)sprite.SourceRect.Width, depth: 0.001f); + sprite.RelativeOrigin = Vector2.One * 0.5f; + if (spawnType == SpawnType.Human && AssignedJob?.Icon != null) + { + AssignedJob.Icon.Draw(spriteBatch, drawPos, AssignedJob.UIColor, scale: iconSize / (float)AssignedJob.Icon.SourceRect.Width * 0.8f, depth: 0.0f); + } foreach (MapEntity e in linkedTo) { GUI.DrawLine(spriteBatch, drawPos, new Vector2(e.DrawPosition.X, -e.DrawPosition.Y), - isObstructed ? Color.Gray : GUI.Style.Green, width: 5); + (isObstructed ? Color.Gray : GUI.Style.Green) * 0.7f, width: 5, depth: 0.002f); } GUI.SmallFont.DrawString(spriteBatch, @@ -83,6 +81,14 @@ namespace Barotrauma Color.WhiteSmoke); } + public override bool IsMouseOn(Vector2 position) + { + if (IsHidden()) { return false; } + float dist = Vector2.DistanceSquared(position, WorldPosition); + float radius = (SpawnType == SpawnType.Path ? WaypointSize : SpawnPointSize) * 0.6f; + return dist < radius * radius; + } + private bool IsHidden() { if (spawnType == SpawnType.Path) @@ -178,14 +184,19 @@ namespace Barotrauma private bool ChangeSpawnType(GUIButton button, object obj) { GUITextBlock spawnTypeText = button.Parent.GetChildByUserData("spawntypetext") as GUITextBlock; - spawnType += (int)button.UserData; - - if (spawnType > SpawnType.Cargo) spawnType = SpawnType.Human; - if (spawnType < SpawnType.Human) spawnType = SpawnType.Cargo; - + var values = Enum.GetValues(typeof(SpawnType)); + int firstIndex = 1; + int lastIndex = values.Length - 1; + if ((int)spawnType > lastIndex) + { + spawnType = (SpawnType)firstIndex; + } + if ((int)spawnType < firstIndex) + { + spawnType = (SpawnType)values.GetValue(lastIndex); + } spawnTypeText.Text = spawnType.ToString(); - return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index ae9a597bc..51fc9d4fa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -15,6 +15,7 @@ namespace Barotrauma.Networking public byte ID; public UInt16 CharacterID; public bool Muted; + public bool InGame; public bool AllowKicking; } @@ -71,7 +72,8 @@ namespace Barotrauma.Networking else { VoipSound.SetPosition(null); - } + VoipSound.Gain = 1.0f; + } } partial void InitProjSpecific() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 47ff63bfb..821661faa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -8,6 +8,7 @@ using System.IO.Compression; using System.Linq; using System.Text; using System.Threading; +using System.Threading.Tasks; namespace Barotrauma.Networking { @@ -60,11 +61,24 @@ namespace Barotrauma.Networking private bool connected; + private enum RoundInitStatus + { + NotStarted, + Starting, + WaitingForStartGameFinalize, + Started, + TimedOut, + Error, + Interrupted + } + + private RoundInitStatus roundInitStatus = RoundInitStatus.NotStarted; + private byte myID; private List otherClients; - private readonly List serverSubmarines = new List(); + private readonly List serverSubmarines = new List(); private string serverIP, serverName; @@ -148,6 +162,8 @@ namespace Barotrauma.Networking this.ownerKey = ownerKey; this.steamP2POwner = steamP2POwner; + roundInitStatus = RoundInitStatus.NotStarted; + allowReconnect = true; netStats = new NetStats(); @@ -472,13 +488,17 @@ namespace Barotrauma.Networking var msgBox = new GUIMessageBox(pwMsg, "", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, relativeSize: new Vector2(0.25f, 0.1f), minSize: new Point(400, 170)); - var passwordHolder = new GUILayoutGroup(new RectTransform(Vector2.One, msgBox.Content.RectTransform), childAnchor: Anchor.TopCenter); - var passwordBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1f) , passwordHolder.RectTransform) { MinSize = new Point(0, 20) }) + var passwordHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), msgBox.Content.RectTransform), childAnchor: Anchor.TopCenter); + var passwordBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1f), passwordHolder.RectTransform) { MinSize = new Point(0, 20) }) { UserData = "password", Censor = true }; + msgBox.Content.Recalculate(); + msgBox.Content.RectTransform.MinSize = new Point(0, msgBox.Content.RectTransform.Children.Sum(c => c.Rect.Height)); + msgBox.Content.Parent.RectTransform.MinSize = new Point(0, (int)(msgBox.Content.RectTransform.MinSize.Y / msgBox.Content.RectTransform.RelativeSize.Y)); + var okButton = msgBox.Buttons[0]; var cancelButton = msgBox.Buttons[1]; @@ -524,6 +544,7 @@ namespace Barotrauma.Networking foreach (Client c in ConnectedClients) { + if (c.Character != null && c.Character.Removed) { c.Character = null; } c.UpdateSoundPosition(); } @@ -558,6 +579,13 @@ namespace Barotrauma.Networking try { + incomingMessagesToProcess.Clear(); + incomingMessagesToProcess.AddRange(pendingIncomingMessages); + foreach (var inc in incomingMessagesToProcess) + { + ReadDataMessage(inc); + } + pendingIncomingMessages.Clear(); clientPeer?.Update(deltaTime); } catch (Exception e) @@ -632,11 +660,23 @@ namespace Barotrauma.Networking } } - private CoroutineHandle startGameCoroutine; + private readonly List pendingIncomingMessages = new List(); + private readonly List incomingMessagesToProcess = new List(); private void ReadDataMessage(IReadMessage inc) { ServerPacketHeader header = (ServerPacketHeader)inc.ReadByte(); + + if (header != ServerPacketHeader.STARTGAMEFINALIZE && + header != ServerPacketHeader.ENDGAME && + roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize) + { + //rewind the header byte we just read + inc.BitPosition -= 8; + pendingIncomingMessages.Add(inc); + return; + } + switch (header) { case ServerPacketHeader.UPDATE_LOBBY: @@ -714,7 +754,13 @@ namespace Barotrauma.Networking } break; case ServerPacketHeader.STARTGAME: - startGameCoroutine = GameMain.Instance.ShowLoading(StartGame(inc), false); + GameMain.Instance.ShowLoading(StartGame(inc), false); + break; + case ServerPacketHeader.STARTGAMEFINALIZE: + if (roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize) + { + ReadStartGameFinalize(inc); + } break; case ServerPacketHeader.ENDGAME: string endMessage = inc.ReadString(); @@ -725,6 +771,8 @@ namespace Barotrauma.Networking GameMain.GameSession.WinningTeam = winningTeam; GameMain.GameSession.Mission.Completed = true; } + + roundInitStatus = RoundInitStatus.Interrupted; CoroutineManager.StartCoroutine(EndGame(endMessage), "EndGame"); break; case ServerPacketHeader.CAMPAIGN_SETUP_INFO: @@ -775,7 +823,37 @@ namespace Barotrauma.Networking break; } } - + + private void ReadStartGameFinalize(IReadMessage inc) + { + ushort contentToPreloadCount = inc.ReadUInt16(); + List contentToPreload = new List(); + for (int i = 0; i < contentToPreloadCount; i++) + { + ContentType contentType = (ContentType)inc.ReadByte(); + string filePath = inc.ReadString(); + contentToPreload.Add(new ContentFile(filePath, contentType)); + } + + GameMain.GameSession.EventManager.PreloadContent(contentToPreload); + + int levelEqualityCheckVal = inc.ReadInt32(); + + if (Level.Loaded.EqualityCheckVal != levelEqualityCheckVal) + { + string errorMsg = "Level equality check failed. The level generated at your end doesn't match the level generated by the server (seed: " + Level.Loaded.Seed + + ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortHash + ")" + + ", mirrored: " + Level.Loaded.Mirrored + ")."; + GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + throw new Exception(errorMsg); + } + + GameMain.GameSession.Mission?.ClientReadInitial(inc); + + roundInitStatus = RoundInitStatus.Started; + } + + private void OnDisconnect() { if (SteamManager.IsInitialized) @@ -884,6 +962,7 @@ namespace Barotrauma.Networking } else { + connected = false; connectCancelled = true; string msg = ""; @@ -1108,6 +1187,7 @@ namespace Barotrauma.Networking if (Character != null) Character.Remove(); HasSpawned = false; eventErrorWritten = false; + GameMain.NetLobbyScreen.StopWaitingForStartRound(); while (CoroutineManager.IsCoroutineRunning("EndGame")) { @@ -1126,9 +1206,11 @@ namespace Barotrauma.Networking EndVoteTickBox.Selected = false; + roundInitStatus = RoundInitStatus.Starting; + int seed = inc.ReadInt32(); string levelSeed = inc.ReadString(); - int levelEqualityCheckVal = inc.ReadInt32(); + //int levelEqualityCheckVal = inc.ReadInt32(); float levelDifficulty = inc.ReadSingle(); byte losMode = inc.ReadByte(); @@ -1146,24 +1228,16 @@ namespace Barotrauma.Networking int missionIndex = inc.ReadInt16(); bool respawnAllowed = inc.ReadBoolean(); - bool loadSecondSub = inc.ReadBoolean(); bool disguisesAllowed = inc.ReadBoolean(); bool rewiringAllowed = inc.ReadBoolean(); bool allowRagdollButton = inc.ReadBoolean(); - ushort contentToPreloadCount = inc.ReadUInt16(); - List contentToPreload = new List(); - for (int i = 0; i < contentToPreloadCount; i++) - { - ContentType contentType = (ContentType)inc.ReadByte(); - string filePath = inc.ReadString(); - contentToPreload.Add(new ContentFile(filePath, contentType)); - } - serverSettings.ReadMonsterEnabled(inc); + bool includesFinalize = inc.ReadBoolean(); inc.ReadPadBits(); + GameModePreset gameMode = GameModePreset.List.Find(gm => gm.Identifier == modeIdentifier); MultiPlayerCampaign campaign = GameMain.NetLobbyScreen.SelectedMode == GameMain.GameSession?.GameMode.Preset && gameMode == GameMain.NetLobbyScreen.SelectedMode ? @@ -1237,31 +1311,114 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Failure; } + MissionPrefab missionPrefab = missionIndex < 0 ? null : MissionPrefab.List[missionIndex]; + GameMain.GameSession = missionIndex < 0 ? new GameSession(GameMain.NetLobbyScreen.SelectedSub, "", gameMode, MissionType.None) : - new GameSession(GameMain.NetLobbyScreen.SelectedSub, "", gameMode, MissionPrefab.List[missionIndex]); - GameMain.GameSession.StartRound(levelSeed, levelDifficulty, loadSecondSub); + new GameSession(GameMain.NetLobbyScreen.SelectedSub, "", gameMode, missionPrefab); + + //startRoundTask = Task.Run(async () => { await Task.Yield(); GameMain.GameSession.StartRound(levelSeed, levelDifficulty); }); + GameMain.GameSession.StartRound(levelSeed, levelDifficulty); } else { if (GameMain.GameSession?.CrewManager != null) GameMain.GameSession.CrewManager.Reset(); + /*startRoundTask = Task.Run(async () => + { + await Task.Yield(); + GameMain.GameSession.StartRound(campaign.Map.SelectedConnection.Level, + reloadSub: true, + mirrorLevel: campaign.Map.CurrentLocation != campaign.Map.SelectedConnection.Locations[0]); + });*/ GameMain.GameSession.StartRound(campaign.Map.SelectedConnection.Level, - reloadSub: true, - loadSecondSub: false, - mirrorLevel: campaign.Map.CurrentLocation != campaign.Map.SelectedConnection.Locations[0]); + mirrorLevel: campaign.Map.CurrentLocation != campaign.Map.SelectedConnection.Locations[0]); } - GameMain.GameSession.Mission?.ClientReadInitial(inc); + roundInitStatus = RoundInitStatus.WaitingForStartGameFinalize; - if (GameMain.GameSession.Submarine.IsFileCorrupted) + DateTime? timeOut = null; + DateTime requestFinalizeTime = DateTime.Now; + TimeSpan requestFinalizeInterval = new TimeSpan(0, 0, 2); + + while (true) { - DebugConsole.ThrowError($"Failed to start a round. Could not load the submarine \"{GameMain.GameSession.Submarine.Name}\"."); + try + { + if (timeOut.HasValue) + { + if (DateTime.Now > requestFinalizeTime) + { + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte)ClientPacketHeader.REQUEST_STARTGAMEFINALIZE); + clientPeer.Send(msg, DeliveryMethod.Unreliable); + requestFinalizeTime = DateTime.Now + requestFinalizeInterval; + } + if (DateTime.Now > timeOut) + { + DebugConsole.ThrowError("Error while starting the round (did not receive STARTGAMEFINALIZE message from the server). Stopping the round..."); + roundInitStatus = RoundInitStatus.TimedOut; + break; + } + } + else + { + if (includesFinalize) + { + ReadStartGameFinalize(inc); + break; + } + + //wait for up to 30 seconds for the server to send the STARTGAMEFINALIZE message + timeOut = DateTime.Now + new TimeSpan(0, 0, seconds: 30); + } + + if (!connected) + { + roundInitStatus = RoundInitStatus.Interrupted; + break; + } + + if (roundInitStatus != RoundInitStatus.WaitingForStartGameFinalize) + { + break; + } + + clientPeer.Update((float)Timing.Step); + } + catch (Exception e) + { + DebugConsole.ThrowError("There was an error initializing the round.", e, true); + roundInitStatus = RoundInitStatus.Error; + break; + } + + //waiting for a STARTGAMEFINALIZE message + yield return CoroutineStatus.Running; + } + + if (roundInitStatus != RoundInitStatus.Started) + { + if (roundInitStatus != RoundInitStatus.Interrupted) + { + DebugConsole.ThrowError(roundInitStatus.ToString()); + CoroutineManager.StartCoroutine(EndGame("")); + yield return CoroutineStatus.Failure; + } + else + { + yield return CoroutineStatus.Success; + } + } + + if (GameMain.GameSession.Submarine.Info.IsFileCorrupted) + { + DebugConsole.ThrowError($"Failed to start a round. Could not load the submarine \"{GameMain.GameSession.Submarine.Info.Name}\"."); yield return CoroutineStatus.Failure; } for (int i = 0; i < Submarine.MainSubs.Length; i++) { - if (!loadSecondSub && i > 0) { break; } + if (Submarine.MainSubs[i] == null) { break; } var teamID = i == 0 ? Character.TeamType.Team1 : Character.TeamType.Team2; Submarine.MainSubs[i].TeamID = teamID; @@ -1271,23 +1428,10 @@ namespace Barotrauma.Networking } } - if (Level.Loaded.EqualityCheckVal != levelEqualityCheckVal) - { - string errorMsg = "Level equality check failed. The level generated at your end doesn't match the level generated by the server (seed: " + Level.Loaded.Seed + - ", sub: " + Submarine.MainSub.Name + " (" + Submarine.MainSub.MD5Hash.ShortHash + ")" + - ", mirrored: " + Level.Loaded.Mirrored + ")."; - DebugConsole.ThrowError(errorMsg, createMessageBox: true); - GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + levelSeed, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); - CoroutineManager.StartCoroutine(EndGame("")); - yield return CoroutineStatus.Failure; - } - if (respawnAllowed) { respawnManager = new RespawnManager(this, GameMain.NetLobbyScreen.UsingShuttle ? GameMain.NetLobbyScreen.SelectedShuttle : null); } - GameMain.GameSession.EventManager.PreloadContent(contentToPreload); - - ServerSettings.ServerDetailsChanged = true; gameStarted = true; + ServerSettings.ServerDetailsChanged = true; GameMain.GameScreen.Select(); @@ -1358,8 +1502,8 @@ namespace Barotrauma.Networking bool requiredContentPackagesInstalled = inc.ReadBoolean(); var matchingSub = - Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash) ?? - new Submarine(Path.Combine(Submarine.SavePath, subName) + ".sub", subHash, false); + SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash) ?? + new SubmarineInfo(Path.Combine(SubmarineInfo.SavePath, subName) + ".sub", subHash); matchingSub.RequiredContentPackagesInstalled = requiredContentPackagesInstalled; serverSubmarines.Add(matchingSub); @@ -1394,6 +1538,7 @@ namespace Barotrauma.Networking string preferredJob = inc.ReadString(); UInt16 characterID = inc.ReadUInt16(); bool muted = inc.ReadBoolean(); + bool inGame = inc.ReadBoolean(); bool allowKicking = inc.ReadBoolean(); inc.ReadPadBits(); @@ -1406,6 +1551,7 @@ namespace Barotrauma.Networking PreferredJob = preferredJob, CharacterID = characterID, Muted = muted, + InGame = inGame, AllowKicking = allowKicking }); } @@ -1424,6 +1570,7 @@ namespace Barotrauma.Networking { SteamID = tc.SteamID, Muted = tc.Muted, + InGame = tc.InGame, AllowKicking = tc.AllowKicking }; ConnectedClients.Add(existingClient); @@ -1433,15 +1580,12 @@ namespace Barotrauma.Networking existingClient.PreferredJob = tc.PreferredJob; existingClient.Character = null; existingClient.Muted = tc.Muted; + existingClient.InGame = tc.InGame; existingClient.AllowKicking = tc.AllowKicking; GameMain.NetLobbyScreen.SetPlayerNameAndJobPreference(existingClient); - if (tc.CharacterID > 0) + if (Screen.Selected != GameMain.NetLobbyScreen && tc.CharacterID > 0) { - existingClient.Character = Entity.FindEntityByID(tc.CharacterID) as Character; - if (existingClient.Character == null) - { - updateClientListId = false; - } + existingClient.CharacterID = tc.CharacterID; } if (existingClient.ID == myID) { @@ -1888,15 +2032,15 @@ namespace Barotrauma.Networking { case FileTransferType.Submarine: new GUIMessageBox(TextManager.Get("ServerDownloadFinished"), TextManager.GetWithVariable("FileDownloadedNotification", "[filename]", transfer.FileName)); - var newSub = new Submarine(transfer.FilePath); + var newSub = new SubmarineInfo(transfer.FilePath); if (newSub.IsFileCorrupted) { return; } - var existingSubs = Submarine.SavedSubmarines.Where(s => s.Name == newSub.Name && s.MD5Hash.Hash == newSub.MD5Hash.Hash).ToList(); - foreach (Submarine existingSub in existingSubs) + var existingSubs = SubmarineInfo.SavedSubmarines.Where(s => s.Name == newSub.Name && s.MD5Hash.Hash == newSub.MD5Hash.Hash).ToList(); + foreach (SubmarineInfo existingSub in existingSubs) { existingSub.Dispose(); } - Submarine.AddToSavedSubs(newSub); + SubmarineInfo.AddToSavedSubs(newSub); for (int i = 0; i < 2; i++) { @@ -1905,8 +2049,8 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SubList.Content.Children; var subElement = subListChildren.FirstOrDefault(c => - ((Submarine)c.UserData).Name == newSub.Name && - ((Submarine)c.UserData).MD5Hash.Hash == newSub.MD5Hash.Hash); + ((SubmarineInfo)c.UserData).Name == newSub.Name && + ((SubmarineInfo)c.UserData).MD5Hash.Hash == newSub.MD5Hash.Hash); if (subElement == null) continue; subElement.GetChild().TextColor = new Color(subElement.GetChild().TextColor, 1.0f); @@ -1934,17 +2078,17 @@ namespace Barotrauma.Networking if (campaign == null) { return; } GameMain.GameSession.SavePath = transfer.FilePath; - if (GameMain.GameSession.Submarine == null) + if (GameMain.GameSession.SubmarineInfo == null) { var gameSessionDoc = SaveUtil.LoadGameSessionDoc(GameMain.GameSession.SavePath); string subPath = Path.Combine(SaveUtil.TempPath, gameSessionDoc.Root.GetAttributeString("submarine", "")) + ".sub"; - GameMain.GameSession.Submarine = new Submarine(subPath, ""); + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(subPath, ""); } SaveUtil.LoadGame(GameMain.GameSession.SavePath, GameMain.GameSession); GameMain.GameSession?.Submarine?.CheckSubsLeftBehind(); - if (GameMain.GameSession?.Submarine?.Name != null) + if (GameMain.GameSession?.SubmarineInfo?.Name != null) { - GameMain.NetLobbyScreen.TryDisplayCampaignSubmarine(GameMain.GameSession.Submarine); + GameMain.NetLobbyScreen.TryDisplayCampaignSubmarine(GameMain.GameSession.SubmarineInfo); } campaign.LastSaveID = campaign.PendingSaveID; @@ -1979,8 +2123,7 @@ namespace Barotrauma.Networking { if (!permissions.HasFlag(ClientPermissions.ConsoleCommands)) { return false; } - commandName = commandName.ToLowerInvariant(); - if (permittedConsoleCommands.Any(c => c.ToLowerInvariant() == commandName)) { return true; } + if (permittedConsoleCommands.Any(c => c.Equals(commandName, StringComparison.OrdinalIgnoreCase))) { return true; } //check aliases foreach (DebugConsole.Command command in DebugConsole.Commands) @@ -2231,7 +2374,7 @@ namespace Barotrauma.Networking clientPeer.Send(msg, DeliveryMethod.Reliable); } - public void SetupNewCampaign(Submarine sub, string saveName, string mapSeed) + public void SetupNewCampaign(SubmarineInfo sub, string saveName, string mapSeed) { GameMain.NetLobbyScreen.CampaignSetupFrame.Visible = false; @@ -2440,7 +2583,7 @@ namespace Barotrauma.Networking if (GUI.KeyboardDispatcher.Subscriber == null) { bool chatKeyHit = PlayerInput.KeyHit(InputType.Chat); - bool radioKeyHit = PlayerInput.KeyHit(InputType.RadioChat); + bool radioKeyHit = PlayerInput.KeyHit(InputType.RadioChat) && (Character.Controlled == null || Character.Controlled.SpeechImpediment < 0); if (chatKeyHit || radioKeyHit) { @@ -2498,6 +2641,7 @@ namespace Barotrauma.Networking { var transfer = fileReceiver.ActiveTransfers.First(); GameMain.NetLobbyScreen.FileTransferFrame.Visible = true; + GameMain.NetLobbyScreen.FileTransferFrame.UserData = transfer; GameMain.NetLobbyScreen.FileTransferTitle.Text = ToolBox.LimitString( TextManager.GetWithVariable("DownloadingFile", "[filename]", transfer.FileName), @@ -2789,7 +2933,7 @@ namespace Barotrauma.Networking } if (GameMain.GameSession?.Submarine != null) { - errorLines.Add("Submarine: " + GameMain.GameSession.Submarine.Name); + errorLines.Add("Submarine: " + GameMain.GameSession.Submarine.Info.Name); } if (Level.Loaded != null) { @@ -2811,8 +2955,8 @@ namespace Barotrauma.Networking errorLines.Add(" " + DebugConsole.Messages[i].Time + " - " + DebugConsole.Messages[i].Text); } - string filePath = "event_error_log_client_" + Name + "_" + ToolBox.RemoveInvalidFileNameChars(DateTime.UtcNow.ToShortTimeString() + ".log"); - filePath = Path.Combine(ServerLog.SavePath, filePath); + string filePath = "event_error_log_client_" + Name + "_" + DateTime.UtcNow.ToShortTimeString() + ".log"; + filePath = Path.Combine(ServerLog.SavePath, ToolBox.RemoveInvalidFileNameChars(filePath)); if (!Directory.Exists(ServerLog.SavePath)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index d315062c3..41f8f65af 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -91,6 +91,7 @@ namespace Barotrauma.Networking return; } + incomingLidgrenMessages.Clear(); netClient.ReadMessages(incomingLidgrenMessages); foreach (NetIncomingMessage inc in incomingLidgrenMessages) @@ -107,8 +108,6 @@ namespace Barotrauma.Networking break; } } - - incomingLidgrenMessages.Clear(); } private void HandleDataMessage(NetIncomingMessage inc) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index c334482f2..cf5534b9e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -204,8 +204,9 @@ namespace Barotrauma.Networking } } - while (Steamworks.SteamNetworking.IsP2PPacketAvailable()) + for (int i=0;i<100;i++) { + if (!Steamworks.SteamNetworking.IsP2PPacketAvailable()) { break; } var packet = Steamworks.SteamNetworking.ReadP2PPacket(); if (packet.HasValue) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index a279517b2..c299c8380 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -571,6 +571,9 @@ namespace Barotrauma.Networking var ragdollButtonBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsAllowRagdollButton")); GetPropertyData("AllowRagdollButton").AssignGUIComponent(ragdollButtonBox); + var disableBotConversationsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsDisableBotConversations")); + GetPropertyData("DisableBotConversations").AssignGUIComponent(disableBotConversationsBox); + /*var traitorRatioBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsUseTraitorRatio")); CreateLabeledSlider(roundsTab, "", out slider, out sliderLabel); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs index 6b2f569db..886e62140 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs @@ -15,6 +15,8 @@ namespace Barotrauma.Steam { static partial class SteamManager { + private static Dictionary modCopiesInProgress = new Dictionary(); + private static void InitializeProjectSpecific() { if (isInitialized) { return; } @@ -63,20 +65,6 @@ namespace Barotrauma.Steam private static void UpdateProjectSpecific(float deltaTime) { - for (int i=0;i<(ugcResultPageTasks?.Count ?? 0);i++) - { - var task = ugcResultPageTasks[i]; - if (task.IsCompleted) - { - if (!task.IsCompletedSuccessfully) - { - DebugConsole.ThrowError("Failed to retrieve Steam Workshop page info: TaskStatus = "+task.Status.ToString()); - } - ugcResultPageTasks.RemoveAt(i); - i--; - } - } - if (ugcSubscriptionTasks != null) { var ugcSubscriptionKeys = ugcSubscriptionTasks.Keys.ToList(); @@ -525,7 +513,37 @@ namespace Barotrauma.Steam } } - private static List ugcResultPageTasks; + private static async Task> GetWorkshopItemsAsync(Steamworks.Ugc.Query query, int clampResults = 0, Predicate itemPredicate=null) + { + await Task.Yield(); + + int pageIndex = 1; + Steamworks.Ugc.ResultPage? resultPage = await query.GetPageAsync(pageIndex); + + List retVal = new List(); + while (resultPage.HasValue && resultPage?.ResultCount > 0) + { + if (itemPredicate != null) + { + retVal.AddRange(resultPage.Value.Entries.Where(it => itemPredicate(it))); + } + else + { + retVal.AddRange(resultPage.Value.Entries); + } + + if (clampResults > 0 && retVal.Count >= clampResults) + { + retVal = retVal.Take(clampResults).ToList(); + break; + } + + pageIndex++; + resultPage = await query.GetPageAsync(pageIndex); + } + + return retVal; + } public static void GetSubscribedWorkshopItems(Action> onItemsFound, List requireTags = null) { @@ -535,30 +553,9 @@ namespace Barotrauma.Steam .RankedByTotalUniqueSubscriptions() .WhereUserSubscribed() .WithLongDescription(); - if (requireTags != null) query.WithTags(requireTags); + if (requireTags != null) { query = query.WithTags(requireTags); } - ugcResultPageTasks ??= new List(); - ugcResultPageTasks.Add(Task.Run(async () => - { - int processedResults = 0; int pageIndex = 1; - Steamworks.Ugc.ResultPage? resultPage = await query.GetPageAsync(pageIndex); - - while (resultPage.HasValue && resultPage?.ResultCount > 0) - { - onItemsFound?.Invoke(resultPage.Value.Entries.ToList()); - - processedResults += resultPage.Value.ResultCount; - pageIndex++; - if (processedResults < resultPage?.TotalCount) - { - resultPage = await query.GetPageAsync(pageIndex); - } - else - { - resultPage = null; - } - } - })); + TaskPool.Add(GetWorkshopItemsAsync(query), (task) => { onItemsFound?.Invoke(task.Result); }); } public static void GetPopularWorkshopItems(Action> onItemsFound, int amount, List requireTags = null) @@ -570,65 +567,40 @@ namespace Barotrauma.Steam .WithLongDescription(); if (requireTags != null) query.WithTags(requireTags); - ugcResultPageTasks ??= new List(); - ugcResultPageTasks.Add(Task.Run(async () => - { - int processedResults = 0; int pageIndex = 1; - Steamworks.Ugc.ResultPage? resultPage = await query.GetPageAsync(pageIndex); + TaskPool.Add(GetWorkshopItemsAsync(query, amount, (item) => !item.IsSubscribed), (task) => { + var entries = task.Result; - while (resultPage.HasValue && resultPage?.ResultCount > 0) + //count the number of each unique tag + foreach (var item in entries) { - var entries = resultPage.Value.Entries.ToList(); - //count the number of each unique tag - foreach (var item in entries) + foreach (string tag in item.Tags) { - foreach (string tag in item.Tags) + if (string.IsNullOrEmpty(tag)) { continue; } + string caseInvariantTag = tag.ToLowerInvariant(); + if (!tagCommonness.ContainsKey(caseInvariantTag)) { - if (string.IsNullOrEmpty(tag)) { continue; } - string caseInvariantTag = tag.ToLowerInvariant(); - if (!tagCommonness.ContainsKey(caseInvariantTag)) - { - tagCommonness[caseInvariantTag] = 1; - } - else - { - tagCommonness[caseInvariantTag]++; - } + tagCommonness[caseInvariantTag] = 1; } - } - //populate the popularTags list with tags sorted by commonness - popularTags.Clear(); - foreach (KeyValuePair tagCommonnessKVP in tagCommonness) - { - int i = 0; - while (i < popularTags.Count && - tagCommonness[popularTags[i]] > tagCommonnessKVP.Value) + else { - i++; + tagCommonness[caseInvariantTag]++; } - popularTags.Insert(i, tagCommonnessKVP.Key); - } - - var nonSubscribedItems = entries.Where(it => !it.IsSubscribed); - if (nonSubscribedItems.Count() > (amount-processedResults)) - { - nonSubscribedItems = nonSubscribedItems.Take(amount - processedResults); - } - - onItemsFound?.Invoke(nonSubscribedItems.ToList()); - - processedResults += resultPage.Value.ResultCount; - pageIndex++; - if (processedResults < resultPage?.TotalCount && processedResults < amount) - { - resultPage = await query.GetPageAsync(pageIndex); - } - else - { - resultPage = null; } } - })); + //populate the popularTags list with tags sorted by commonness + popularTags.Clear(); + foreach (KeyValuePair tagCommonnessKVP in tagCommonness) + { + int i = 0; + while (i < popularTags.Count && + tagCommonness[popularTags[i]] > tagCommonnessKVP.Value) + { + i++; + } + popularTags.Insert(i, tagCommonnessKVP.Key); + } + onItemsFound?.Invoke(task.Result); + }); } public static void GetPublishedWorkshopItems(Action> onItemsFound, List requireTags = null) @@ -641,28 +613,7 @@ namespace Barotrauma.Steam .WithLongDescription(); if (requireTags != null) query.WithTags(requireTags); - ugcResultPageTasks ??= new List(); - ugcResultPageTasks.Add(Task.Run(async () => - { - int processedResults = 0; int pageIndex = 1; - Steamworks.Ugc.ResultPage? resultPage = await query.GetPageAsync(pageIndex); - - while (resultPage.HasValue && resultPage?.ResultCount > 0) - { - onItemsFound?.Invoke(resultPage.Value.Entries.ToList()); - - processedResults += resultPage.Value.ResultCount; - pageIndex++; - if (processedResults < resultPage?.TotalCount) - { - resultPage = await query.GetPageAsync(pageIndex); - } - else - { - resultPage = null; - } - } - })); + TaskPool.Add(GetWorkshopItemsAsync(query), (task) => { onItemsFound?.Invoke(task.Result); }); } private static Dictionary ugcSubscriptionTasks; @@ -926,7 +877,7 @@ namespace Barotrauma.Steam /// /// Enables a workshop item by moving it to the game folder. /// - public static bool EnableWorkShopItem(Steamworks.Ugc.Item? item, bool allowFileOverwrite, out string errorMsg) + public static bool EnableWorkShopItem(Steamworks.Ugc.Item? item, bool allowFileOverwrite, out string errorMsg, bool selectContentPackage = false, bool suppressInstallNotif = false) { if (!(item?.IsInstalled ?? false)) { @@ -972,57 +923,116 @@ namespace Barotrauma.Steam return false; } - GameMain.Config.SuppressModFolderWatcher = true; + Task newTask = null; - CopyWorkShopItem(item, contentPackage, newContentPackagePath, metaDataFilePath, allowFileOverwrite, out errorMsg); - - var newPackage = new ContentPackage(contentPackage.Path, newContentPackagePath) + lock (modCopiesInProgress) { - SteamWorkshopUrl = item?.Url, - InstallTime = item?.Updated > item?.Created ? item?.Updated : item?.Created - }; - - foreach (ContentFile contentFile in newPackage.Files) - { - contentFile.Path = CorrectContentFilePath(contentFile.Path, contentPackage, true); + if (modCopiesInProgress.ContainsKey(item.Value.Id)) + { + if (!modCopiesInProgress[item.Value.Id].IsCompleted && + !modCopiesInProgress[item.Value.Id].IsFaulted && + !modCopiesInProgress[item.Value.Id].IsCanceled) + { + errorMsg = ""; return true; + } + modCopiesInProgress.Remove(item.Value.Id); + } + newTask = CopyWorkShopItemAsync(item, contentPackage, newContentPackagePath, metaDataFilePath, allowFileOverwrite); + modCopiesInProgress.Add(item.Value.Id, newTask); } + + TaskPool.Add(newTask, + contentPackage, + (task, cp) => + { + if (task.IsFaulted || task.IsCanceled) + { + DebugConsole.ThrowError($"Failed to copy \"{item?.Title}\"", task.Exception); + GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); + return; + } + if (!string.IsNullOrWhiteSpace(task.Result)) + { + DebugConsole.ThrowError($"Failed to copy \"{item?.Title}\": {task.Result}"); + GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); + return; + } - if (!Directory.Exists(Path.GetDirectoryName(newContentPackagePath))) - { - Directory.CreateDirectory(Path.GetDirectoryName(newContentPackagePath)); - } - newPackage.Save(newContentPackagePath); - ContentPackage.List.Add(newPackage); - if (newPackage.CorePackage) - { - GameMain.Config.SelectCorePackage(newPackage); - } - else - { - GameMain.Config.SelectContentPackage(newPackage); - } - GameMain.Config.SaveNewPlayerConfig(); + GameMain.Config.SuppressModFolderWatcher = true; - GameMain.Config.WarnIfContentPackageSelectionDirty(); + var newPackage = new ContentPackage(cp.Path, newContentPackagePath) + { + SteamWorkshopUrl = item?.Url, + InstallTime = item?.Updated > item?.Created ? item?.Updated : item?.Created + }; - GameMain.Config.SuppressModFolderWatcher = false; + foreach (ContentFile contentFile in newPackage.Files) + { + contentFile.Path = CorrectContentFilePath(contentFile.Path, cp, true); + } - if (newPackage.Files.Any(f => f.Type == ContentType.Submarine)) - { - Submarine.RefreshSavedSubs(); - } + if (!Directory.Exists(Path.GetDirectoryName(newContentPackagePath))) + { + Directory.CreateDirectory(Path.GetDirectoryName(newContentPackagePath)); + } + newPackage.Save(newContentPackagePath); + ContentPackage.List.Add(newPackage); + if (selectContentPackage) + { + if (newPackage.CorePackage) + { + GameMain.Config.SelectCorePackage(newPackage); + } + else + { + GameMain.Config.SelectContentPackage(newPackage); + } + GameMain.Config.SaveNewPlayerConfig(); + + GameMain.Config.WarnIfContentPackageSelectionDirty(); + + if (newPackage.Files.Any(f => f.Type == ContentType.Submarine)) + { + SubmarineInfo.RefreshSavedSubs(); + } + } + else if (!suppressInstallNotif) + { + GameMain.MainMenuScreen?.SetEnableModsNotification(true); + } + + GameMain.Config.SuppressModFolderWatcher = false; + + GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Green); + + }); + errorMsg = ""; return true; } - private static bool CopyWorkShopItem(Steamworks.Ugc.Item? item, ContentPackage contentPackage, string newContentPackagePath, string metaDataFilePath, bool allowFileOverwrite, out string errorMsg) + /// + /// Asynchronously copies a Workshop item into the Mods folder. + /// + /// Returns an empty string on success, otherwise returns an error message. + private async static Task CopyWorkShopItemAsync(Steamworks.Ugc.Item? item, ContentPackage contentPackage, string newContentPackagePath, string metaDataFilePath, bool allowFileOverwrite) { - errorMsg = ""; + await Task.Yield(); + + string targetPath = Path.GetDirectoryName(GetWorkshopItemContentPackagePath(contentPackage)); + string copyingPath = Path.Combine(targetPath, CopyIndicatorFileName); + + string errorMsg = ""; if (contentPackage.GameVersion > new Version(0, 9, 1, 0)) { - SaveUtil.CopyFolder(item?.Directory, Path.GetDirectoryName(GetWorkshopItemContentPackagePath(contentPackage)), copySubDirs: true, overwriteExisting: true); - return true; + Directory.CreateDirectory(targetPath); + File.WriteAllText(copyingPath, "TEMPORARY FILE"); + + SaveUtil.CopyFolder(item?.Directory, targetPath, copySubDirs: true, overwriteExisting: true); + + File.Delete(copyingPath); + return ""; } var allPackageFiles = Directory.GetFiles(item?.Directory, "*", SearchOption.AllDirectories); @@ -1042,7 +1052,7 @@ namespace Barotrauma.Steam { errorMsg = TextManager.GetWithVariables("WorkshopErrorOverwriteOnEnable", new string[2] { "[itemname]", "[filename]" }, new string[2] { item?.Title, newContentPackagePath }); DebugConsole.NewMessage(errorMsg, Color.Red); - return false; + return errorMsg; } foreach (ContentFile contentFile in contentPackage.Files) @@ -1053,84 +1063,79 @@ namespace Barotrauma.Steam { errorMsg = TextManager.GetWithVariables("WorkshopErrorOverwriteOnEnable", new string[2] { "[itemname]", "[filename]" }, new string[2] { item?.Title, contentFile.Path }); DebugConsole.NewMessage(errorMsg, Color.Red); - return false; + return errorMsg; } } } - try + Directory.CreateDirectory(targetPath); + File.WriteAllText(copyingPath, "TEMPORARY FILE"); + + foreach (ContentFile contentFile in contentPackage.Files) { - foreach (ContentFile contentFile in contentPackage.Files) + contentFile.Path = contentFile.Path.CleanUpPath(); + string sourceFile = Path.Combine(item?.Directory, contentFile.Path); + if (!File.Exists(sourceFile)) { - contentFile.Path = contentFile.Path.CleanUpPath(); - string sourceFile = Path.Combine(item?.Directory, contentFile.Path); - if (!File.Exists(sourceFile)) + string[] splitPath = contentFile.Path.Split('/'); + if (splitPath.Length >= 2 && splitPath[0] == "Mods") { - string[] splitPath = contentFile.Path.Split('/'); - if (splitPath.Length >= 2 && splitPath[0] == "Mods") - { - sourceFile = Path.Combine(item?.Directory, string.Join("/", splitPath.Skip(2))); - } + sourceFile = Path.Combine(item?.Directory, string.Join("/", splitPath.Skip(2))); } + } - contentFile.Path = CorrectContentFilePath(contentFile.Path, contentPackage, false); + contentFile.Path = CorrectContentFilePath(contentFile.Path, contentPackage, + contentFile.Type != ContentType.Submarine); - - //path not allowed -> the content file must be a reference to an external file (such as some vanilla file outside the Mods folder) - if (!ContentPackage.IsModFilePathAllowed(contentFile)) + //path not allowed -> the content file must be a reference to an external file (such as some vanilla file outside the Mods folder) + if (!ContentPackage.IsModFilePathAllowed(contentFile)) + { + //the content package is trying to copy a file to a prohibited path, which is not allowed + if (File.Exists(sourceFile)) { - //the content package is trying to copy a file to a prohibited path, which is not allowed - if (File.Exists(sourceFile)) - { - errorMsg = TextManager.GetWithVariable("WorkshopErrorIllegalPathOnEnable", "[filename]", contentFile.Path); - return false; - } - //not trying to copy anything, so this is a reference to an external file - //if the external file doesn't exist, we cannot enable the package - else if (!File.Exists(contentFile.Path)) - { - errorMsg = TextManager.GetWithVariable("WorkshopErrorEnableFailed", "[itemname]", item?.Title) + " " + TextManager.GetWithVariable("WorkshopFileNotFound", "[path]", "\"" + contentFile.Path + "\""); - return false; - } + errorMsg = TextManager.GetWithVariable("WorkshopErrorIllegalPathOnEnable", "[filename]", contentFile.Path); + return errorMsg; + } + //not trying to copy anything, so this is a reference to an external file + //if the external file doesn't exist, we cannot enable the package + else if (!File.Exists(contentFile.Path)) + { + errorMsg = TextManager.GetWithVariable("WorkshopErrorEnableFailed", "[itemname]", item?.Title) + " " + TextManager.GetWithVariable("WorkshopFileNotFound", "[path]", "\"" + contentFile.Path + "\""); + return errorMsg; + } + continue; + } + else if (!File.Exists(sourceFile)) + { + if (File.Exists(contentFile.Path)) + { + //the file is already present in the game folder, all good continue; } - else if (!File.Exists(sourceFile)) + else { - if (File.Exists(contentFile.Path)) - { - //the file is already present in the game folder, all good - continue; - } - else - { - //file not present in either the mod or the game folder -> cannot enable the package - errorMsg = TextManager.GetWithVariable("WorkshopErrorEnableFailed", "[itemname]", item?.Title) + " " + TextManager.GetWithVariable("WorkshopFileNotFound", "[path]", "\"" + contentFile.Path + "\""); - return false; - } + //file not present in either the mod or the game folder -> cannot enable the package + errorMsg = TextManager.GetWithVariable("WorkshopErrorEnableFailed", "[itemname]", item?.Title) + " " + TextManager.GetWithVariable("WorkshopFileNotFound", "[path]", "\"" + contentFile.Path + "\""); + return errorMsg; } - - //make sure the destination directory exists - Directory.CreateDirectory(Path.GetDirectoryName(contentFile.Path)); - CorrectContentFileCopy(contentPackage, sourceFile, contentFile.Path, overwrite: true); } - foreach (string nonContentFile in nonContentFiles) - { - string sourceFile = Path.Combine(item?.Directory, nonContentFile); - if (!File.Exists(sourceFile)) { continue; } - string destinationPath = CorrectContentFilePath(nonContentFile, contentPackage, false); - Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); - CorrectContentFileCopy(contentPackage, sourceFile, destinationPath, overwrite: true); - } + //make sure the destination directory exists + Directory.CreateDirectory(Path.GetDirectoryName(contentFile.Path)); + CorrectContentFileCopy(contentPackage, sourceFile, contentFile.Path, overwrite: true); } - catch (Exception e) + + foreach (string nonContentFile in nonContentFiles) { - errorMsg = TextManager.GetWithVariable("WorkshopErrorEnableFailed", "[itemname]", item?.Title) + " {" + e.Message + "}"; - DebugConsole.NewMessage(errorMsg, Color.Red); - return false; + string sourceFile = Path.Combine(item?.Directory, nonContentFile); + if (!File.Exists(sourceFile)) { continue; } + string destinationPath = CorrectContentFilePath(nonContentFile, contentPackage, false); + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); + CorrectContentFileCopy(contentPackage, sourceFile, destinationPath, overwrite: true); } - return true; + File.Delete(copyingPath); + return ""; } private static bool CheckFileEquality(string filePath1, string filePath2) @@ -1149,6 +1154,44 @@ namespace Barotrauma.Steam } } + private static void RemoveMods(Func predicate) + { + var toRemove = ContentPackage.List.Where(predicate).ToList(); + var packagesToDeselect = GameMain.Config.SelectedContentPackages.Where(p => toRemove.Contains(p)).ToList(); + foreach (var cp in packagesToDeselect) + { + if (cp.CorePackage) + { + GameMain.Config.SelectCorePackage(ContentPackage.List.Find(cpp => cpp.CorePackage && !toRemove.Contains(cpp))); + } + else + { + GameMain.Config.DeselectContentPackage(cp); + } + } + + foreach (var cp in toRemove) + { + try + { + string path = Path.GetDirectoryName(cp.Path); + if (Directory.Exists(path)) { Directory.Delete(path, true); } + } + catch (Exception e) + { + DebugConsole.ThrowError($"An error occurred while attempting to delete {Path.GetDirectoryName(cp.Path)}", e); + } + } + + ContentPackage.List.RemoveAll(cp => toRemove.Contains(cp)); + GameMain.Config.SelectedContentPackages.RemoveAll(cp => !ContentPackage.List.Contains(cp)); + + ContentPackage.SortContentPackages(); + GameMain.Config.SaveNewPlayerConfig(); + + GameMain.Config.WarnIfContentPackageSelectionDirty(); + } + /// /// Disables a workshop item by removing the files from the game folder. /// @@ -1173,44 +1216,11 @@ namespace Barotrauma.Steam GameMain.Config.SuppressModFolderWatcher = true; try { - - var toRemove = ContentPackage.List.Where(cp => !string.IsNullOrWhiteSpace(cp.SteamWorkshopUrl) && cp.SteamWorkshopUrl == contentPackage.SteamWorkshopUrl).ToList(); - var packagesToDeselect = GameMain.Config.SelectedContentPackages.Where(p => toRemove.Contains(p)).ToList(); - foreach (var cp in packagesToDeselect) - { - if (cp.CorePackage) - { - GameMain.Config.SelectCorePackage(ContentPackage.List.Find(cpp => cpp.CorePackage && !toRemove.Contains(cpp))); - } - else - { - GameMain.Config.DeselectContentPackage(cp); - } - } - - foreach (var cp in toRemove) - { - try - { - Directory.Delete(Path.GetDirectoryName(cp.Path), true); - } - catch (Exception e) - { - DebugConsole.ThrowError($"An error occurred while attempting to delete {Path.GetDirectoryName(cp.Path)}", e); - } - } - - ContentPackage.List.RemoveAll(cp => toRemove.Contains(cp)); - GameMain.Config.SelectedContentPackages.RemoveAll(cp => !ContentPackage.List.Contains(cp)); - - ContentPackage.SortContentPackages(); - GameMain.Config.SaveNewPlayerConfig(); - - GameMain.Config.WarnIfContentPackageSelectionDirty(); + RemoveMods(cp => !string.IsNullOrWhiteSpace(cp.SteamWorkshopUrl) && cp.SteamWorkshopUrl == contentPackage.SteamWorkshopUrl); } catch (Exception e) { - errorMsg = "Disabling the workshop item \"" + item?.Title + "\" failed. " + e.Message; + errorMsg = "Disabling the workshop item \"" + item?.Title + "\" failed. " + e.Message + "\n" + e.StackTrace; if (!noLog) { DebugConsole.NewMessage(errorMsg, Microsoft.Xna.Framework.Color.Red); @@ -1219,6 +1229,8 @@ namespace Barotrauma.Steam } GameMain.Config.SuppressModFolderWatcher = false; + GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, false, null); + errorMsg = ""; return true; } @@ -1314,79 +1326,77 @@ namespace Barotrauma.Steam return upToDate; } - public static bool AutoUpdateWorkshopItems() + public static async Task AutoUpdateWorkshopItemsAsync() { if (!isInitialized) { return false; } var query = new Steamworks.Ugc.Query(Steamworks.UgcType.All) .WhereUserSubscribed() .WithLongDescription(); - //ugcResultPageTasks ??= new List(); - //ugcResultPageTasks.Add(); - CancellationTokenSource cancelTokenSource = new CancellationTokenSource(); - CancellationToken cancelToken = cancelTokenSource.Token; - Task task = Task.Factory.StartNew(async () => - { - int processedResults = 0; int pageIndex = 1; - Steamworks.Ugc.ResultPage? resultPage = await query.GetPageAsync(pageIndex); - while (resultPage.HasValue && resultPage?.ResultCount > 0) + List items = await GetWorkshopItemsAsync(query); + + GameMain.Config.SuppressModFolderWatcher = true; + + //remove mods that the player is no longer subscribed to + RemoveMods(cp => !string.IsNullOrWhiteSpace(cp.SteamWorkshopUrl) && !items.Any(it => it.Id == GetWorkshopItemIDFromUrl(cp.SteamWorkshopUrl))); + + GameMain.Config.SuppressModFolderWatcher = false; + + foreach (var item in items) + { + try { - foreach (var item in resultPage.Value.Entries) + if (!item.IsInstalled) { continue; } + + bool installedSuccessfully = false; + string errorMsg; + if (!CheckWorkshopItemEnabled(item)) { - if (cancelToken.IsCancellationRequested) - { - cancelToken.ThrowIfCancellationRequested(); - } - try - { - if (!item.IsInstalled || !CheckWorkshopItemEnabled(item) || CheckWorkshopItemUpToDate(item)) { continue; } - if (!UpdateWorkshopItem(item, out string errorMsg)) - { - DebugConsole.ThrowError(errorMsg); - new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item.Title, errorMsg })); - } - else - { - //TODO: potential race condition - new GUIMessageBox("", TextManager.GetWithVariable("WorkshopItemUpdated", "[itemname]", item.Title)); - } - } - catch (Exception e) - { - new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item.Title, e.Message + ", " + e.TargetSite })); - GameAnalyticsManager.AddErrorEventOnce( - "SteamManager.AutoUpdateWorkshopItems:" + e.Message, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Failed to autoupdate workshop item \"" + item.Title + "\". " + e.Message + "\n" + e.StackTrace); - } + installedSuccessfully = EnableWorkShopItem(item, true, out errorMsg); } - - processedResults += resultPage.Value.ResultCount; - pageIndex++; - if (processedResults < resultPage?.TotalCount) + else if (!CheckWorkshopItemUpToDate(item)) { - resultPage = await query.GetPageAsync(pageIndex); + installedSuccessfully = UpdateWorkshopItem(item, out errorMsg); } else { - resultPage = null; + continue; + } + + if (!installedSuccessfully) + { + DebugConsole.ThrowError(errorMsg); + new GUIMessageBox( + TextManager.Get("Error"), + TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item.Title, errorMsg })); + } + else + { + //TODO: potential race condition + new GUIMessageBox("", TextManager.GetWithVariable("WorkshopItemUpdated", "[itemname]", item.Title)); } } - }, cancelToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); - - task.Wait(10000); - if (!task.IsCompleted) - { - cancelTokenSource.Cancel(); - task.Wait(); + catch (Exception e) + { + new GUIMessageBox( + TextManager.Get("Error"), + TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item.Title, e.Message + ", " + e.TargetSite })); + GameAnalyticsManager.AddErrorEventOnce( + "SteamManager.AutoUpdateWorkshopItems:" + e.Message, + GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + "Failed to autoupdate workshop item \"" + item.Title + "\". " + e.Message + "\n" + e.StackTrace); + } } - - return task.Status == TaskStatus.RanToCompletion; + + List tasks; + lock (modCopiesInProgress) + { + tasks = modCopiesInProgress.Values.ToList(); + } + await Task.WhenAll(tasks); + + return true; } public static bool UpdateWorkshopItem(Steamworks.Ugc.Item? item, out string errorMsg) @@ -1431,7 +1441,7 @@ namespace Barotrauma.Steam private static void CorrectContentFileCopy(ContentPackage package, string src, string dest, bool overwrite) { - if (Path.GetExtension(src).ToLowerInvariant() == ".xml") + if (Path.GetExtension(src).Equals(".xml", StringComparison.OrdinalIgnoreCase)) { XDocument doc = XMLExtensions.TryLoadXml(src); if (doc != null) @@ -1481,7 +1491,7 @@ namespace Barotrauma.Steam { if (checkIfFileExists) { - ContentPackage otherContentPackage = ContentPackage.List.Find(cp => cp.Name.ToLowerInvariant() == splitPath[1].ToLowerInvariant()); + ContentPackage otherContentPackage = ContentPackage.List.Find(cp => cp.Name.Equals(splitPath[1], StringComparison.OrdinalIgnoreCase)); if (otherContentPackage != null) { string otherPackageName = Path.GetDirectoryName(otherContentPackage.Path); @@ -1493,7 +1503,8 @@ namespace Barotrauma.Steam } } } - newPath = Path.Combine(packageName, string.Join("/", splitPath.Skip(2))); + splitPath = splitPath.Skip(Math.Clamp(splitPath.Length-1, 0, 2)).ToArray(); + newPath = Path.Combine(packageName, string.Join("/", splitPath)); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index 59db1c580..574dffcb2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -93,7 +93,7 @@ namespace Barotrauma.Networking if (client.VoipSound == null) { DebugConsole.Log("Recreating voipsound " + queueId); - client.VoipSound = new VoipSound(GameMain.SoundManager, client.VoipQueue); + client.VoipSound = new VoipSound(client.Name, GameMain.SoundManager, client.VoipQueue); } if (client.Character != null && !client.Character.IsDead && !client.Character.Removed && client.Character.SpeechImpediment <= 100.0f) @@ -119,7 +119,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen?.SetPlayerSpeaking(client); GameMain.GameSession?.CrewManager?.SetClientSpeaking(client); - if (client.VoipSound.CurrentAmplitude > 0.1f) //TODO: might need to tweak + if ((client.VoipSound.CurrentAmplitude * client.VoipSound.Gain * GameMain.SoundManager.GetCategoryGainMultiplier("voip")) > 0.1f) //TODO: might need to tweak { if (client.Character != null && !client.Character.Removed) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index 556265ebf..49650e599 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -116,7 +116,7 @@ namespace Barotrauma Submarine sub = data as Submarine; if (sub == null) return; - msg.Write(sub.Name); + msg.Write(sub.Info.Name); break; case VoteType.Mode: GameModePreset gameMode = data as GameModePreset; @@ -158,7 +158,7 @@ namespace Barotrauma { if (item.UserData != null && item.UserData is Submarine) serversubs.Add(item.UserData as Submarine); } - Submarine sub = serversubs.FirstOrDefault(sm => sm.Name == subName); + Submarine sub = serversubs.FirstOrDefault(sm => sm.Info.Name == subName); SetVoteText(GameMain.NetLobbyScreen.SubList, sub, votes); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/DecalPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/DecalPrefab.cs index d4c179ae1..da75fd497 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/DecalPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/DecalPrefab.cs @@ -11,7 +11,18 @@ namespace Barotrauma.Particles public string OriginalName { get { return Name; } } - public string Identifier { get { return Name.ToLowerInvariant(); } } + private string _identifier; + public string Identifier + { + get + { + if (_identifier == null) + { + _identifier = Name.ToLowerInvariant(); + } + return _identifier; + } + } public string FilePath { get; private set; } @@ -46,7 +57,7 @@ namespace Barotrauma.Particles foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "sprite") + if (subElement.Name.ToString().Equals("sprite", StringComparison.OrdinalIgnoreCase)) { Sprites.Add(new Sprite(subElement)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index f7471bda7..6a140118c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -108,7 +108,8 @@ namespace Barotrauma.Particles public readonly bool CopyEntityAngle; - public readonly bool DrawOnTop; + public bool DrawOnTop => forceDrawOnTop || ParticlePrefab.DrawOnTop; + private readonly bool forceDrawOnTop; public ParticleEmitterPrefab(XElement element) { @@ -150,6 +151,12 @@ namespace Barotrauma.Particles { DistanceMin = DistanceMax = element.GetAttributeFloat("distance", 0.0f); } + if (DistanceMax < DistanceMin) + { + var temp = DistanceMin; + DistanceMin = DistanceMax; + DistanceMax = temp; + } if (element.Attribute("velocity") == null) { @@ -160,13 +167,19 @@ namespace Barotrauma.Particles { VelocityMin = VelocityMax = element.GetAttributeFloat("velocity", 0.0f); } + if (VelocityMax < VelocityMin) + { + var temp = VelocityMin; + VelocityMin = VelocityMax; + VelocityMax = temp; + } EmitInterval = element.GetAttributeFloat("emitinterval", 0.0f); ParticlesPerSecond = element.GetAttributeInt("particlespersecond", 0); ParticleAmount = element.GetAttributeInt("particleamount", 0); HighQualityCollisionDetection = element.GetAttributeBool("highqualitycollisiondetection", false); CopyEntityAngle = element.GetAttributeBool("copyentityangle", false); - DrawOnTop = element.GetAttributeBool("drawontop", false); + forceDrawOnTop = element.GetAttributeBool("drawontop", false); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index c8c31dd77..4666696e9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -228,14 +228,7 @@ namespace Barotrauma.Particles if (inSub.HasValue) { bool isOutside = particle.CurrentHull == null; - if (particle.DrawOnTop) - { - if (isOutside != inSub.Value) - { - continue; - } - } - else if (isOutside == inSub.Value) + if (!particle.DrawOnTop && isOutside == inSub.Value) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index 067269913..9a0cb6a6c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -199,6 +199,9 @@ namespace Barotrauma.Particles [Editable, Serialize(DrawTargetType.Air, false, description: "Should the particle be rendered in air, water or both.")] public DrawTargetType DrawTarget { get; private set; } + [Editable, Serialize(false, false, description: "Should the particle be always rendered on top of entities?")] + public bool DrawOnTop { get; private set; } + [Editable, Serialize(ParticleBlendState.AlphaBlend, false, description: "The type of blending to use when rendering the particle.")] public ParticleBlendState BlendState { get; private set; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index ed8e7afa5..6ecf9226b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -7,6 +7,8 @@ using System.Text; using GameAnalyticsSDK.Net; using Barotrauma.Steam; using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Xml.Linq; #if WINDOWS using SharpDX; @@ -22,42 +24,56 @@ namespace Barotrauma /// public static class Program { + +#if LINUX + /// + /// Sets the required environment variables for the game to initialize Steamworks correctly. + /// + [DllImport("linux_steam_env", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + private static extern void setLinuxEnv(); +#endif + /// /// The main entry point for the application. /// [STAThread] static void Main(string[] args) { - GameMain game = null; string executableDir = ""; + #if !DEBUG + AppDomain currentDomain = AppDomain.CurrentDomain; + currentDomain.UnhandledException += new UnhandledExceptionEventHandler(CrashHandler); +#endif + +#if LINUX + setLinuxEnv(); +#endif + + Game = null; + executableDir = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location); + Directory.SetCurrentDirectory(executableDir); + SteamManager.Initialize(); + Game = new GameMain(args); + Game.Run(); + Game.Dispose(); + } + + private static GameMain Game; + + private static void CrashHandler(object sender, UnhandledExceptionEventArgs args) + { try { -#endif - executableDir = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location); - Directory.SetCurrentDirectory(executableDir); - SteamManager.Initialize(); - game = new GameMain(args); - game.Run(); - game.Dispose(); -#if !DEBUG + Game?.Exit(); + CrashDump(Game, "crashreport.log", (Exception)args.ExceptionObject); + Game?.Dispose(); } - catch (Exception e) + catch { - try - { - CrashDump(game, Path.Combine(executableDir,"crashreport.log"), e); - } - catch (Exception e2) - { - CrashMessageBox("Barotrauma seems to have crashed, and failed to generate a crash report: " - + e2.Message + "\n" + e2.StackTrace.ToString(), - null); - } - game?.Dispose(); + //exception handler is broken, we have a serious problem here!! return; } -#endif } public static void CrashMessageBox(string message, string filePath) @@ -83,16 +99,9 @@ namespace Barotrauma string exePath = System.Reflection.Assembly.GetEntryAssembly().Location; var md5 = System.Security.Cryptography.MD5.Create(); Md5Hash exeHash = null; - try + using (var stream = File.OpenRead(exePath)) { - using (var stream = File.OpenRead(exePath)) - { - exeHash = new Md5Hash(stream); - } - } - catch - { - //gotta catch them all, we don't want to throw an exception while writing a crash report + exeHash = new Md5Hash(stream); } StreamWriter sw = new StreamWriter(filePath); @@ -102,6 +111,33 @@ namespace Barotrauma sb.AppendLine("\n"); sb.AppendLine("Barotrauma seems to have crashed. Sorry for the inconvenience! "); sb.AppendLine("\n"); + + try + { + if (exception is GameMain.LoadingException) + { + //exception occurred in loading screen: + //assume content packages are the culprit and reset them + XDocument doc = XMLExtensions.TryLoadXml(GameSettings.PlayerSavePath); + XDocument baseDoc = XMLExtensions.TryLoadXml(GameSettings.SavePath); + if (doc != null && baseDoc != null) + { + XElement newElement = new XElement(doc.Root.Name); + newElement.Add(doc.Root.Attributes()); + newElement.Add(doc.Root.Elements().Where(e => !e.Name.LocalName.Equals("contentpackage", StringComparison.InvariantCultureIgnoreCase))); + newElement.Add(baseDoc.Root.Elements().Where(e => e.Name.LocalName.Equals("contentpackage", StringComparison.InvariantCultureIgnoreCase))); + XDocument newDoc = new XDocument(newElement); + newDoc.Save(GameSettings.PlayerSavePath); + sb.AppendLine("To prevent further startup errors, installed mods will be disabled the next time you launch the game."); + sb.AppendLine("\n"); + } + } + } + catch + { + //welp i guess we couldn't reset the config! + } + if (exeHash?.Hash != null) { sb.AppendLine(exeHash.Hash); @@ -119,7 +155,7 @@ namespace Barotrauma sb.AppendLine("Selected content packages: " + (!GameMain.SelectedPackages.Any() ? "None" : string.Join(", ", GameMain.SelectedPackages.Select(c => c.Name)))); } sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed)); - sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Name + " (" + Submarine.MainSub.MD5Hash + ")")); + sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")")); sb.AppendLine("Selected screen: " + (Screen.Selected == null ? "None" : Screen.Selected.ToString())); if (SteamManager.IsInitialized) { @@ -217,4 +253,4 @@ namespace Barotrauma } } #endif -} + } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs index d140b5d4e..bd4ae4c0c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs @@ -21,7 +21,7 @@ namespace Barotrauma private GUIButton loadGameButton, deleteMpSaveButton; - public Action StartNewGame; + public Action StartNewGame; public Action LoadGame; public GUIButton StartButton @@ -32,7 +32,7 @@ namespace Barotrauma private readonly bool isMultiplayer; - public CampaignSetupUI(bool isMultiplayer, GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable submarines, IEnumerable saveFiles = null) + public CampaignSetupUI(bool isMultiplayer, GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable submarines, IEnumerable saveFiles = null) { this.isMultiplayer = isMultiplayer; this.newGameContainer = newGameContainer; @@ -115,12 +115,12 @@ namespace Barotrauma return false; } - Submarine selectedSub = null; + SubmarineInfo selectedSub = null; if (!isMultiplayer) { - if (!(subList.SelectedData is Submarine)) { return false; } - selectedSub = subList.SelectedData as Submarine; + if (!(subList.SelectedData is SubmarineInfo)) { return false; } + selectedSub = subList.SelectedData as SubmarineInfo; } else { @@ -226,7 +226,7 @@ namespace Barotrauma { foreach (GUIComponent child in subList.Content.Children) { - var sub = child.UserData as Submarine; + var sub = child.UserData as SubmarineInfo; if (sub == null) { return; } child.Visible = string.IsNullOrEmpty(filter) ? true : sub.DisplayName.ToLower().Contains(filter.ToLower()); } @@ -238,7 +238,7 @@ namespace Barotrauma (subPreviewContainer.Parent as GUILayoutGroup)?.Recalculate(); subPreviewContainer.ClearChildren(); - Submarine sub = obj as Submarine; + SubmarineInfo sub = obj as SubmarineInfo; if (sub == null) { return true; } sub.CreatePreviewWindow(subPreviewContainer); @@ -278,7 +278,7 @@ namespace Barotrauma saveNameBox.Text = Path.GetFileNameWithoutExtension(savePath); } - public void UpdateSubList(IEnumerable submarines) + public void UpdateSubList(IEnumerable submarines) { #if !DEBUG var subsToShow = submarines.Where(s => !s.HasTag(SubmarineTag.HideInMenus)); @@ -288,7 +288,7 @@ namespace Barotrauma subList.ClearChildren(); - foreach (Submarine sub in subsToShow) + foreach (SubmarineInfo sub in subsToShow) { var textBlock = new GUITextBlock( new RectTransform(new Vector2(1, 0.1f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, @@ -319,7 +319,7 @@ namespace Barotrauma }; } } - if (Submarine.SavedSubmarines.Any()) + if (SubmarineInfo.SavedSubmarines.Any()) { var nonShuttles = subsToShow.Where(s => !s.HasTag(SubmarineTag.Shuttle)).ToList(); if (nonShuttles.Count > 0) @@ -392,18 +392,19 @@ namespace Barotrauma { nameText.Text = Path.GetFileNameWithoutExtension(saveFile); XDocument doc = SaveUtil.LoadGameSessionDoc(saveFile); - if (doc.Root.GetChildElement("multiplayercampaign") != null) - { - //multiplayer campaign save in the wrong folder -> don't show the save - saveList.Content.RemoveChild(saveFrame); - continue; - } + if (doc?.Root == null) { DebugConsole.ThrowError("Error loading save file \"" + saveFile + "\". The file may be corrupted."); nameText.TextColor = GUI.Style.Red; continue; } + if (doc.Root.GetChildElement("multiplayercampaign") != null) + { + //multiplayer campaign save in the wrong folder -> don't show the save + saveList.Content.RemoveChild(saveFrame); + continue; + } subName = doc.Root.GetAttributeString("submarine", ""); saveTime = doc.Root.GetAttributeString("savetime", ""); contentPackageStr = doc.Root.GetAttributeString("selectedcontentpackages", ""); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 1ff4d6a40..e7ef26c18 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -119,8 +119,8 @@ namespace Barotrauma.CharacterEditor if (Submarine.MainSub == null) { ResetVariables(); - Submarine.MainSub = new Submarine("Content/AnimEditor.sub"); - Submarine.MainSub.Load(unloadPrevious: false, showWarningMessages: false); + var subInfo = new SubmarineInfo("Content/AnimEditor.sub"); + Submarine.MainSub = new Submarine(subInfo); Submarine.MainSub.PhysicsBody.Enabled = false; originalWall = new WallGroup(new List(Structure.WallList)); CloneWalls(); @@ -3347,6 +3347,7 @@ namespace Barotrauma.CharacterEditor void CreateCloseButton(SerializableEntityEditor editor, Action onButtonClicked, float size = 1) { + if (editor == null) { return; } int height = 30; var parent = new GUIFrame(new RectTransform(new Point(editor.Rect.Width, (int)(height * size * GUI.yScale)), editor.RectTransform, isFixedSize: true), style: null) { @@ -3366,6 +3367,7 @@ namespace Barotrauma.CharacterEditor void CreateAddButtonAtLast(ParamsEditor editor, Action onButtonClicked, string text) { + if (editor == null) { return; } var parentFrame = new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, (int)(50 * GUI.yScale)), editor.EditorBox.Content.RectTransform), style: null, color: ParamsEditor.Color) { CanBeFocused = false @@ -3383,6 +3385,7 @@ namespace Barotrauma.CharacterEditor void CreateAddButton(SerializableEntityEditor editor, Action onButtonClicked, string text) { + if (editor == null) { return; } var parent = new GUIFrame(new RectTransform(new Point(editor.Rect.Width, (int)(60 * GUI.yScale)), editor.RectTransform), style: null) { CanBeFocused = false @@ -4386,7 +4389,7 @@ namespace Barotrauma.CharacterEditor ResetParamsEditor(); } limb.PullJointWorldAnchorA = ScreenToSim(PlayerInput.MousePosition); - TryUpdateLimbParam(limb, "pullpos", ConvertUnits.ToDisplayUnits(limb.PullJointLocalAnchorA / limb.Params.Ragdoll.LimbScale)); + TryUpdateLimbParam(limb, "pullpos", ConvertUnits.ToDisplayUnits(limb.PullJointLocalAnchorA / limb.Params.Scale / limb.Params.Ragdoll.LimbScale)); GUI.DrawLine(spriteBatch, SimToScreen(limb.SimPosition), tformedPullPos, Color.MediumPurple); }); } @@ -4469,7 +4472,7 @@ namespace Barotrauma.CharacterEditor if (joint.BodyA == limb.body.FarseerBody) { joint.LocalAnchorA += input; - Vector2 transformedValue = ConvertUnits.ToDisplayUnits(joint.LocalAnchorA / RagdollParams.JointScale); + Vector2 transformedValue = ConvertUnits.ToDisplayUnits(joint.LocalAnchorA / joint.Scale); TryUpdateJointParam(joint, "limb1anchor", transformedValue); // Snap all selected joints to the first selected if (copyJointSettings) @@ -4484,7 +4487,7 @@ namespace Barotrauma.CharacterEditor else if (joint.BodyB == limb.body.FarseerBody) { joint.LocalAnchorB += input; - Vector2 transformedValue = ConvertUnits.ToDisplayUnits(joint.LocalAnchorB / RagdollParams.JointScale); + Vector2 transformedValue = ConvertUnits.ToDisplayUnits(joint.LocalAnchorB / joint.Scale); TryUpdateJointParam(joint, "limb2anchor", transformedValue); // Snap all selected joints to the first selected if (copyJointSettings) @@ -4504,12 +4507,12 @@ namespace Barotrauma.CharacterEditor if (joint.BodyA == limb.body.FarseerBody && otherJoint.BodyA == otherLimb.body.FarseerBody) { otherJoint.LocalAnchorA = joint.LocalAnchorA; - TryUpdateJointParam(otherJoint, "limb1anchor", ConvertUnits.ToDisplayUnits(joint.LocalAnchorA / RagdollParams.JointScale)); + TryUpdateJointParam(otherJoint, "limb1anchor", ConvertUnits.ToDisplayUnits(joint.LocalAnchorA / joint.Scale)); } else if (joint.BodyB == limb.body.FarseerBody && otherJoint.BodyB == otherLimb.body.FarseerBody) { otherJoint.LocalAnchorB = joint.LocalAnchorB; - TryUpdateJointParam(otherJoint, "limb2anchor", ConvertUnits.ToDisplayUnits(joint.LocalAnchorB / RagdollParams.JointScale)); + TryUpdateJointParam(otherJoint, "limb2anchor", ConvertUnits.ToDisplayUnits(joint.LocalAnchorB / joint.Scale)); } }); } @@ -4873,10 +4876,10 @@ namespace Barotrauma.CharacterEditor { // 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; - l.body.SetSize(new Vector2(ConvertUnits.ToSimUnits(width), ConvertUnits.ToSimUnits(height)) * RagdollParams.LimbScale * RagdollParams.TextureScale * multiplier); - TryUpdateLimbParam(l, "radius", ConvertUnits.ToDisplayUnits(l.body.radius / RagdollParams.LimbScale / RagdollParams.TextureScale)); - TryUpdateLimbParam(l, "width", ConvertUnits.ToDisplayUnits(l.body.width / RagdollParams.LimbScale / RagdollParams.TextureScale)); - TryUpdateLimbParam(l, "height", ConvertUnits.ToDisplayUnits(l.body.height / RagdollParams.LimbScale / RagdollParams.TextureScale)); + 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)); + TryUpdateLimbParam(l, "height", ConvertUnits.ToDisplayUnits(l.body.height / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); } void RecalculateOrigin(Limb l) { @@ -4963,7 +4966,7 @@ namespace Barotrauma.CharacterEditor { continue; } - Vector2 tformedJointPos = jointPos = jointPos / RagdollParams.JointScale / limb.TextureScale * spriteSheetZoom; + Vector2 tformedJointPos = jointPos = jointPos / joint.Scale / limb.TextureScale * spriteSheetZoom; tformedJointPos.Y = -tformedJointPos.Y; tformedJointPos.X *= character.AnimController.Dir; tformedJointPos += limbScreenPos; @@ -4991,11 +4994,11 @@ namespace Barotrauma.CharacterEditor Vector2 input = ConvertUnits.ToSimUnits(scaledMouseSpeed); input.Y = -input.Y; input.X *= character.AnimController.Dir; - input *= RagdollParams.JointScale * limb.TextureScale / spriteSheetZoom; + input *= joint.Scale * limb.TextureScale / spriteSheetZoom; if (joint.BodyA == limb.body.FarseerBody) { joint.LocalAnchorA += input; - Vector2 transformedValue = ConvertUnits.ToDisplayUnits(joint.LocalAnchorA / RagdollParams.JointScale); + Vector2 transformedValue = ConvertUnits.ToDisplayUnits(joint.LocalAnchorA / joint.Scale); TryUpdateJointParam(joint, "limb1anchor", transformedValue); // Snap all selected joints to the first selected if (copyJointSettings) @@ -5010,7 +5013,7 @@ namespace Barotrauma.CharacterEditor else if (joint.BodyB == limb.body.FarseerBody) { joint.LocalAnchorB += input; - Vector2 transformedValue = ConvertUnits.ToDisplayUnits(joint.LocalAnchorB / RagdollParams.JointScale); + Vector2 transformedValue = ConvertUnits.ToDisplayUnits(joint.LocalAnchorB / joint.Scale); TryUpdateJointParam(joint, "limb2anchor", transformedValue); // Snap all selected joints to the first selected if (copyJointSettings) @@ -5029,12 +5032,12 @@ namespace Barotrauma.CharacterEditor if (joint.BodyA == limb.body.FarseerBody && otherJoint.BodyA == otherLimb.body.FarseerBody) { otherJoint.LocalAnchorA = joint.LocalAnchorA; - TryUpdateJointParam(otherJoint, "limb1anchor", ConvertUnits.ToDisplayUnits(joint.LocalAnchorA / RagdollParams.JointScale)); + TryUpdateJointParam(otherJoint, "limb1anchor", ConvertUnits.ToDisplayUnits(joint.LocalAnchorA / joint.Scale)); } else if (joint.BodyB == limb.body.FarseerBody && otherJoint.BodyB == otherLimb.body.FarseerBody) { otherJoint.LocalAnchorB = joint.LocalAnchorB; - TryUpdateJointParam(otherJoint, "limb2anchor", ConvertUnits.ToDisplayUnits(joint.LocalAnchorB / RagdollParams.JointScale)); + TryUpdateJointParam(otherJoint, "limb2anchor", ConvertUnits.ToDisplayUnits(joint.LocalAnchorB / joint.Scale)); } }); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index ba788163d..8f6eb15ce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -394,7 +394,7 @@ namespace Barotrauma.CharacterEditor return false; } var path = Path.GetFileName(TexturePath); - if (!path.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) + if (!path.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) { GUI.AddMessage(TextManager.Get("WrongFileType"), GUI.Style.Red); texturePathElement.Flash(GUI.Style.Red); @@ -724,8 +724,8 @@ namespace Barotrauma.CharacterEditor { ParseLimbsFromGUIElements(); ParseJointsFromGUIElements(); - var main = LimbXElements.Values.Select(xe => xe.Attribute("type")).Where(a => a.Value.ToLowerInvariant() == "torso").FirstOrDefault() ?? - LimbXElements.Values.Select(xe => xe.Attribute("type")).Where(a => a.Value.ToLowerInvariant() == "head").FirstOrDefault(); + var main = LimbXElements.Values.Select(xe => xe.Attribute("type")).Where(a => a.Value.Equals("torso", StringComparison.OrdinalIgnoreCase)).FirstOrDefault() ?? + LimbXElements.Values.Select(xe => xe.Attribute("type")).Where(a => a.Value.Equals("head", StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); if (main == null) { GUI.AddMessage(GetCharacterEditorTranslation("MissingTorsoOrHead"), GUI.Style.Red); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 3eeb3be8a..8278e964c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -91,7 +91,13 @@ namespace Barotrauma c.DoVisibilityCheck(cam); if (c.IsVisible != wasVisible) { - c.AnimController.Limbs.ForEach(l => { if (l.LightSource != null) l.LightSource.Enabled = c.IsVisible; }); + c.AnimController.Limbs.ForEach(l => + { + if (l.LightSource != null) + { + l.LightSource.Enabled = c.IsVisible; + } + }); } } @@ -116,10 +122,12 @@ namespace Barotrauma { if (Submarine.MainSubs[i] == null) continue; if (Level.Loaded != null && Submarine.MainSubs[i].WorldPosition.Y < Level.MaxEntityDepth) continue; - + + Vector2 position = Submarine.MainSubs[i].SubBody != null ? Submarine.MainSubs[i].WorldPosition : Submarine.MainSubs[i].HiddenSubPosition; + Color indicatorColor = i == 0 ? Color.LightBlue * 0.5f : GUI.Style.Red * 0.5f; GUI.DrawIndicator( - spriteBatch, Submarine.MainSubs[i].WorldPosition, cam, + spriteBatch, position, cam, Math.Max(Submarine.MainSub.Borders.Width, Submarine.MainSub.Borders.Height), GUI.SubmarineIcon, indicatorColor); } @@ -282,14 +290,23 @@ namespace Barotrauma } spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.None, null, null, cam.Transform); - foreach (Character c in Character.CharacterList) c.DrawFront(spriteBatch, cam); - - if (Level.Loaded != null) Level.Loaded.DrawFront(spriteBatch, cam); - if (GameMain.DebugDraw && GameMain.GameSession?.EventManager != null) + foreach (Character c in Character.CharacterList) { - GameMain.GameSession.EventManager.DebugDraw(spriteBatch); + c.DrawFront(spriteBatch, cam); + } + if (Level.Loaded != null) + { + Level.Loaded.DrawFront(spriteBatch, cam); + } + if (GameMain.DebugDraw) + { + MapEntity.mapEntityList.ForEach(me => me.AiTarget?.Draw(spriteBatch)); + Character.CharacterList.ForEach(c => c.AiTarget?.Draw(spriteBatch)); + if (GameMain.GameSession?.EventManager != null) + { + GameMain.GameSession.EventManager.DebugDraw(spriteBatch); + } } - spriteBatch.End(); if (GameMain.LightManager.LosEnabled && GameMain.LightManager.LosMode != LosMode.None && Character.Controlled != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 7ef7322d3..842c28657 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -466,6 +466,7 @@ namespace Barotrauma Submarine.Draw(spriteBatch, false); Submarine.DrawFront(spriteBatch); Submarine.DrawDamageable(spriteBatch, null); + GUI.DrawRectangle(spriteBatch, new Rectangle(new Point(0, -Level.Loaded.Size.Y), Level.Loaded.Size), Color.White, thickness: (int)(1.0f / cam.Zoom)); spriteBatch.End(); if (lightingEnabled.Selected) @@ -517,8 +518,21 @@ namespace Barotrauma { foreach (XElement element in doc.Root.Elements()) { - if (element.Name.ToString().ToLowerInvariant() != genParams.Name.ToLowerInvariant()) continue; - SerializableProperty.SerializeProperties(genParams, element, true); + XElement levelParamElement = element; + if (element.IsOverride()) + { + foreach (XElement subElement in element.Elements()) + { + if (subElement.Name.ToString().Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) + { + SerializableProperty.SerializeProperties(genParams, subElement, true); + } + } + } + else if (element.Name.ToString().Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) + { + SerializableProperty.SerializeProperties(genParams, element, true); + } break; } } @@ -539,7 +553,7 @@ namespace Barotrauma { foreach (XElement element in doc.Root.Elements()) { - if (element.Name.ToString().ToLowerInvariant() != levelObjPrefab.Name.ToLowerInvariant()) continue; + if (!element.Name.ToString().Equals(levelObjPrefab.Name, StringComparison.OrdinalIgnoreCase)) { continue; } levelObjPrefab.Save(element); break; } @@ -564,7 +578,7 @@ namespace Barotrauma bool elementFound = false; foreach (XElement element in doc.Root.Elements()) { - if (element.Name.ToString().ToLowerInvariant() != genParams.Name.ToLowerInvariant()) continue; + if (!element.Name.ToString().Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { continue; } SerializableProperty.SerializeProperties(genParams, element, true); elementFound = true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LobbyScreen.cs index 7a3818d74..52b9b2f26 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LobbyScreen.cs @@ -78,9 +78,7 @@ namespace Barotrauma private IEnumerable LoadRound() { - GameMain.GameSession.StartRound(campaignUI.SelectedLevel, - reloadSub: true, - loadSecondSub: false, + GameMain.GameSession.StartRound(campaignUI.SelectedLevel, mirrorLevel: GameMain.GameSession.Map.CurrentLocation != GameMain.GameSession.Map.SelectedConnection.Locations[0]); GameMain.GameScreen.Select(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index d8144f7e3..fdea888d7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -31,6 +31,7 @@ namespace Barotrauma private GUITextBox serverNameBox, /*portBox, queryPortBox,*/ passwordBox, maxPlayersBox; private GUITickBox isPublicBox, wrongPasswordBanBox, karmaEnabledBox; private GUIDropDown karmaPresetDD; + private readonly GUIFrame downloadingModsContainer, enableModsContainer; private readonly GUIButton joinServerButton, hostServerButton, steamWorkshopButton; private readonly GameMain game; @@ -230,13 +231,31 @@ namespace Barotrauma }; #if USE_STEAM - steamWorkshopButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("SteamWorkshopButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") + var steamWorkshopButtonContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), style: null); + + steamWorkshopButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), steamWorkshopButtonContainer.RectTransform), TextManager.Get("SteamWorkshopButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { ForceUpperCase = true, Enabled = false, UserData = Tab.SteamWorkshop, OnClicked = SelectTab }; + + downloadingModsContainer = new GUIFrame(new RectTransform(new Vector2(1.4f, 0.9f), steamWorkshopButtonContainer.RectTransform, + Anchor.CenterRight, Pivot.CenterLeft) + { RelativeOffset = new Vector2(0.3f, 0.0f) }, + "MainMenuNotifBackground", Color.Yellow) + { + CanBeFocused = false, + UserData = "workshopnotif", + Visible = false + }; + new GUITextBlock(new RectTransform(Vector2.One * 0.9f, downloadingModsContainer.RectTransform, Anchor.CenterLeft, Pivot.CenterLeft) { RelativeOffset = new Vector2(0.05f, 0.0f) }, + TextManager.Get("ModsDownloadingNotif"), Color.Black) + { + CanBeFocused = false, + }; + #endif new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("SubEditorButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") @@ -280,12 +299,28 @@ namespace Barotrauma RelativeSpacing = 0.035f }; - new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), optionList.RectTransform), TextManager.Get("SettingsButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") + var settingsButtonContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), optionList.RectTransform), style: null); + + new GUIButton(new RectTransform(Vector2.One, settingsButtonContainer.RectTransform), TextManager.Get("SettingsButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { ForceUpperCase = true, UserData = Tab.Settings, OnClicked = SelectTab }; + + enableModsContainer = new GUIFrame(new RectTransform(new Vector2(1.4f, 0.9f), settingsButtonContainer.RectTransform, + Anchor.CenterRight, Pivot.CenterLeft) { RelativeOffset = new Vector2(0.5f, 0.0f) }, + "MainMenuNotifBackground", Color.Yellow) + { + CanBeFocused = false, + UserData = "settingsnotif", + Visible = false + }; + new GUITextBlock(new RectTransform(Vector2.One * 0.9f, enableModsContainer.RectTransform, Anchor.CenterLeft, Pivot.CenterLeft) { RelativeOffset = new Vector2(0.05f, 0.0f) }, + TextManager.Get("ModsInstalledNotif"), Color.Black) + { + CanBeFocused = false + }; new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), optionList.RectTransform), TextManager.Get("CreditsButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { @@ -401,12 +436,18 @@ namespace Barotrauma GameMain.Client = null; } + GameMain.SubEditorScreen?.ClearBackedUpSubInfo(); Submarine.Unload(); ResetButtonStates(null); GameAnalyticsManager.SetCustomDimension01(""); + if (GameMain.SteamWorkshopScreen != null) + { + CoroutineManager.StartCoroutine(GameMain.SteamWorkshopScreen.RefreshDownloadState()); + } + #if OSX // Hack for adjusting the viewport properly after splash screens on older Macs if (firstLoadOnMac) @@ -495,12 +536,13 @@ namespace Barotrauma } campaignSetupUI.CreateDefaultSaveName(); campaignSetupUI.RandomizeSeed(); - campaignSetupUI.UpdateSubList(Submarine.SavedSubmarines); + campaignSetupUI.UpdateSubList(SubmarineInfo.SavedSubmarines); break; case Tab.LoadGame: campaignSetupUI.UpdateLoadMenu(); break; case Tab.Settings: + GameMain.MainMenuScreen?.SetEnableModsNotification(false); menuTabs[(int)Tab.Settings].RectTransform.ClearChildren(); GameMain.Config.SettingsFrame.RectTransform.Parent = menuTabs[(int)Tab.Settings].RectTransform; GameMain.Config.SettingsFrame.RectTransform.RelativeSize = Vector2.One; @@ -631,12 +673,12 @@ namespace Barotrauma Rand.SetLocalRandom(1); } - Submarine selectedSub = null; + SubmarineInfo selectedSub = null; string subName = GameMain.Config.QuickStartSubmarineName; if (!string.IsNullOrEmpty(subName)) { DebugConsole.NewMessage($"Loading the predefined quick start sub \"{subName}\"", Color.White); - selectedSub = Submarine.SavedSubmarines.FirstOrDefault(s => + selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.ToLower() == subName.ToLower()); if (selectedSub == null) @@ -647,7 +689,7 @@ namespace Barotrauma if (selectedSub == null) { DebugConsole.NewMessage("Loading a random sub.", Color.White); - var subs = Submarine.SavedSubmarines.Where(s => !s.HasTag(SubmarineTag.Shuttle) && !s.HasTag(SubmarineTag.HideInMenus)); + var subs = SubmarineInfo.SavedSubmarines.Where(s => !s.HasTag(SubmarineTag.Shuttle) && !s.HasTag(SubmarineTag.HideInMenus)); selectedSub = subs.ElementAt(Rand.Int(subs.Count())); } var gamesession = new GameSession( @@ -684,6 +726,16 @@ namespace Barotrauma } } + public void SetEnableModsNotification(bool visible) + { + if (enableModsContainer != null) { enableModsContainer.Visible = visible; } + } + + public void SetDownloadingModsNotification(bool visible) + { + if (downloadingModsContainer != null) { downloadingModsContainer.Visible = visible; } + } + private void ShowTutorialSkipWarning(Tab tabToContinueTo) { var tutorialSkipWarning = new GUIMessageBox("", TextManager.Get("tutorialskipwarning"), new string[] { TextManager.Get("tutorialwarningskiptutorials"), TextManager.Get("tutorialwarningplaytutorials") }); @@ -990,7 +1042,7 @@ namespace Barotrauma spriteBatch.End(); } - private void StartGame(Submarine selectedSub, string saveName, string mapSeed) + private void StartGame(SubmarineInfo selectedSub, string saveName, string mapSeed) { if (string.IsNullOrEmpty(saveName)) return; @@ -1027,7 +1079,7 @@ namespace Barotrauma return; } - selectedSub = new Submarine(Path.Combine(SaveUtil.TempPath, selectedSub.Name + ".sub"), ""); + selectedSub = new SubmarineInfo(Path.Combine(SaveUtil.TempPath, selectedSub.Name + ".sub")); GameMain.GameSession = new GameSession(selectedSub, saveName, GameModePreset.List.Find(g => g.Identifier == "singleplayercampaign")); @@ -1072,7 +1124,7 @@ namespace Barotrauma var paddedLoadGame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[(int)Tab.LoadGame].RectTransform, Anchor.Center) { AbsoluteOffset = new Point(0, 10) }, style: null); - campaignSetupUI = new CampaignSetupUI(false, paddedNewGame, paddedLoadGame, Submarine.SavedSubmarines) + campaignSetupUI = new CampaignSetupUI(false, paddedNewGame, paddedLoadGame, SubmarineInfo.SavedSubmarines) { LoadGame = LoadGame, StartNewGame = StartGame diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index d49a95eda..3c29fae89 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -208,15 +208,15 @@ namespace Barotrauma private set; } - public Submarine SelectedSub + public SubmarineInfo SelectedSub { - get { return subList.SelectedData as Submarine; } + get { return subList.SelectedData as SubmarineInfo; } set { subList.Select(value); } } - public Submarine SelectedShuttle + public SubmarineInfo SelectedShuttle { - get { return shuttleList.SelectedData as Submarine; } + get { return shuttleList.SelectedData as SubmarineInfo; } } public bool UsingShuttle @@ -443,7 +443,7 @@ namespace Barotrauma { OnClicked = (btn, userdata) => { - if (!(userdata is FileReceiver.FileTransferIn transfer)) { return false; } + if (!(FileTransferFrame.UserData is FileReceiver.FileTransferIn transfer)) { return false; } GameMain.Client?.CancelFileTransfer(transfer); GameMain.Client.FileReceiver.StopTransfer(transfer); return true; @@ -658,7 +658,7 @@ namespace Barotrauma OnClicked = (btn, obj) => { GameMain.Client.RequestStartRound(); - CoroutineManager.StartCoroutine(WaitForStartRound(StartButton, allowCancel: true), "WaitForStartRound"); + CoroutineManager.StartCoroutine(WaitForStartRound(StartButton, allowCancel: false), "WaitForStartRound"); return true; } }; @@ -1147,6 +1147,22 @@ namespace Barotrauma clientDisabledElements.AddRange(botSpawnModeButtons); } + public void StopWaitingForStartRound() + { + CoroutineManager.StopCoroutines("WaitForStartRound"); + + GUIMessageBox.CloseAll(); + if (StartButton != null) + { + StartButton.Enabled = true; + } + if (campaignUI?.StartButton != null) + { + campaignUI.StartButton.Enabled = true; + } + GUI.ClearCursorWait(); + } + public IEnumerable WaitForStartRound(GUIButton startButton, bool allowCancel) { GUI.SetCursorWaiting(); @@ -1173,7 +1189,8 @@ namespace Barotrauma } DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 10); - while (Selected == GameMain.NetLobbyScreen && DateTime.Now < timeOut) + while (Selected == GameMain.NetLobbyScreen && + DateTime.Now < timeOut) { msgBox.Header.Text = headerText + new string('.', ((int)Timing.TotalTime % 3 + 1)); yield return CoroutineStatus.Running; @@ -1322,6 +1339,8 @@ namespace Barotrauma if (GameMain.Client == null) return; spectateButton.Visible = true; spectateButton.Enabled = true; + + StartButton.Visible = false; } public void SetCampaignCharacterInfo(CharacterInfo newCampaignCharacterInfo) @@ -1609,19 +1628,19 @@ namespace Barotrauma MissionType = missionType; } - public void UpdateSubList(GUIComponent subList, List submarines) + public void UpdateSubList(GUIComponent subList, List submarines) { if (subList == null) { return; } subList.ClearChildren(); - foreach (Submarine sub in submarines) + foreach (SubmarineInfo sub in submarines) { AddSubmarine(subList, sub); } } - private void AddSubmarine(GUIComponent subList, Submarine sub) + private void AddSubmarine(GUIComponent subList, SubmarineInfo sub) { if (subList is GUIListBox) { @@ -1646,8 +1665,8 @@ namespace Barotrauma CanBeFocused = false }; - var matchingSub = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name && s.MD5Hash?.Hash == sub.MD5Hash?.Hash); - if (matchingSub == null) matchingSub = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name); + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name && s.MD5Hash?.Hash == sub.MD5Hash?.Hash); + if (matchingSub == null) matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name); if (matchingSub == null) { @@ -1704,7 +1723,7 @@ namespace Barotrauma { if (!GameMain.Client.ServerSettings.Voting.AllowSubVoting) { - var selectedSub = component.UserData as Submarine; + var selectedSub = component.UserData as SubmarineInfo; if (!selectedSub.RequiredContentPackagesInstalled) { var msgBox = new GUIMessageBox(TextManager.Get("ContentPackageMismatch"), @@ -1729,7 +1748,7 @@ namespace Barotrauma } return false; } - if (component.UserData is Submarine sub) + if (component.UserData is SubmarineInfo sub) { CreateSubPreview(sub); } @@ -1761,7 +1780,7 @@ namespace Barotrauma } GameMain.Client.RequestSelectMode(component.Parent.GetChildIndex(component)); HighlightMode(SelectedModeIndex); - return (presetName.ToLowerInvariant() != "multiplayercampaign"); + return !presetName.Equals("multiplayercampaign", StringComparison.OrdinalIgnoreCase); } return false; } @@ -1793,6 +1812,7 @@ namespace Barotrauma SelectedColor = Color.White * 0.85f, OutlineColor = Color.White * 0.5f, TextColor = Color.White, + SelectedTextColor = Color.Black, UserData = client }; var soundIcon = new GUIImage(new RectTransform(new Point((int)(textBlock.Rect.Height * 0.8f)), textBlock.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(5, 0) }, @@ -1884,7 +1904,7 @@ namespace Barotrauma OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) ClosePlayerFrame(btn, userdata); return true; } }; - Vector2 frameSize = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions) ? new Vector2(.24f, .5f) : new Vector2(.24f, .24f); + Vector2 frameSize = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions) ? new Vector2(.28f, .5f) : new Vector2(.28f, .24f); var playerFrameInner = new GUIFrame(new RectTransform(frameSize, playerFrame.RectTransform, Anchor.Center) { MinSize = new Point(550, 0) }); var paddedPlayerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.88f), playerFrameInner.RectTransform, Anchor.Center)) @@ -2111,7 +2131,7 @@ namespace Barotrauma { if (GameMain.Client.HasPermission(ClientPermissions.Ban)) { - var banButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonAreaTop.RectTransform), + var banButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaTop.RectTransform), TextManager.Get("Ban")) { UserData = selectedClient @@ -2119,7 +2139,7 @@ namespace Barotrauma banButton.OnClicked = (bt, userdata) => { BanPlayer(selectedClient); return true; }; banButton.OnClicked += ClosePlayerFrame; - var rangebanButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonAreaTop.RectTransform), + var rangebanButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaTop.RectTransform), TextManager.Get("BanRange")) { UserData = selectedClient @@ -2132,7 +2152,7 @@ namespace Barotrauma if (GameMain.Client != null && GameMain.Client.ServerSettings.Voting.AllowVoteKick && selectedClient != null && selectedClient.AllowKicking) { - var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonAreaLower.RectTransform), + var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaLower.RectTransform), TextManager.Get("VoteToKick")) { Enabled = !selectedClient.HasKickVoteFromID(GameMain.Client.ID), @@ -2144,7 +2164,7 @@ namespace Barotrauma if (GameMain.Client.HasPermission(ClientPermissions.Kick) && selectedClient != null && selectedClient.AllowKicking) { - var kickButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonAreaLower.RectTransform), + var kickButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaLower.RectTransform), TextManager.Get("Kick")) { UserData = selectedClient @@ -2153,6 +2173,9 @@ namespace Barotrauma kickButton.OnClicked += ClosePlayerFrame; } + GUITextBlock.AutoScaleAndNormalize( + buttonAreaTop.Children.Select(c => ((GUIButton)c).TextBlock).Concat(buttonAreaLower.Children.Select(c => ((GUIButton)c).TextBlock))); + new GUITickBox(new RectTransform(new Vector2(0.25f, 1.0f), buttonAreaTop.RectTransform, Anchor.TopRight), TextManager.Get("Mute")) { @@ -2162,7 +2185,7 @@ namespace Barotrauma }; } - var closeButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonAreaLower.RectTransform, Anchor.BottomRight), + var closeButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonAreaLower.RectTransform, Anchor.TopRight), TextManager.Get("Close")) { IgnoreLayoutGroups = true, @@ -2241,7 +2264,7 @@ namespace Barotrauma targetMicStyle = "GUIMicrophoneDisabled"; } - if (targetMicStyle.ToLowerInvariant() != currMicStyle.ToLowerInvariant()) + if (!targetMicStyle.Equals(currMicStyle, StringComparison.OrdinalIgnoreCase)) { GUI.Style.Apply(micIcon, targetMicStyle); } @@ -2597,7 +2620,7 @@ namespace Barotrauma GUILayoutGroup row = null; int itemsInRow = 0; - XElement headElement = info.Ragdoll.MainElement.Elements().FirstOrDefault(e => e.GetAttributeString("type", "").ToLowerInvariant() == "head"); + XElement headElement = info.Ragdoll.MainElement.Elements().FirstOrDefault(e => e.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)); XElement headSpriteElement = headElement.Element("sprite"); string spritePathWithTags = headSpriteElement.Attribute("texture").Value; @@ -2655,8 +2678,11 @@ namespace Barotrauma private bool SwitchJob(GUIButton button, object obj) { + if (JobList == null) { return false; } + int childIndex = JobList.SelectedIndex; var child = JobList.SelectedComponent; + if (child == null) { return false; } bool moveToNext = obj != null; @@ -2745,11 +2771,11 @@ namespace Barotrauma availableJobs = availableJobs.ToList(); - int itemsInRow = 1; + int itemsInRow = 0; foreach (var jobPrefab in availableJobs) { - if (itemsInRow >= 4) + if (itemsInRow >= 3) { row = new GUILayoutGroup(new RectTransform(Vector2.One, rows.RectTransform), true); itemsInRow = 0; @@ -3025,7 +3051,7 @@ namespace Barotrauma }*/ } - public void TryDisplayCampaignSubmarine(Submarine submarine) + public void TryDisplayCampaignSubmarine(SubmarineInfo submarine) { string name = submarine?.Name; bool displayed = false; @@ -3034,13 +3060,13 @@ namespace Barotrauma subPreviewContainer.ClearChildren(); foreach (GUIComponent child in subList.Content.Children) { - if (!(child.UserData is Submarine sub)) { continue; } + if (!(child.UserData is SubmarineInfo sub)) { continue; } //just check the name, even though the campaign sub may not be the exact same version //we're selecting the sub just for show, the selection is not actually used for anything if (sub.Name == name) { subList.Select(sub); - if (Submarine.SavedSubmarines.Contains(sub)) + if (SubmarineInfo.SavedSubmarines.Contains(sub)) { CreateSubPreview(sub); displayed = true; @@ -3200,10 +3226,10 @@ namespace Barotrauma { return false; } - - Submarine sub = subList.Content.Children - .FirstOrDefault(c => c.UserData is Submarine s && s.Name == subName && s.MD5Hash?.Hash == md5Hash)? - .UserData as Submarine; + + SubmarineInfo sub = subList.Content.Children + .FirstOrDefault(c => c.UserData is SubmarineInfo s && s.Name == subName && s.MD5Hash?.Hash == md5Hash)? + .UserData as SubmarineInfo; //matching sub found and already selected, all good if (sub != null) @@ -3212,7 +3238,7 @@ namespace Barotrauma { CreateSubPreview(sub); } - if (subList.SelectedData is Submarine selectedSub && selectedSub.MD5Hash?.Hash == md5Hash && System.IO.File.Exists(sub.FilePath)) + if (subList.SelectedData is SubmarineInfo selectedSub && selectedSub.MD5Hash?.Hash == md5Hash && System.IO.File.Exists(sub.FilePath)) { return true; } @@ -3222,8 +3248,8 @@ namespace Barotrauma if (sub == null) { sub = subList.Content.Children - .FirstOrDefault(c => c.UserData is Submarine s && s.Name == subName)? - .UserData as Submarine; + .FirstOrDefault(c => c.UserData is SubmarineInfo s && s.Name == subName)? + .UserData as SubmarineInfo; } //found a sub that at least has the same name, select it @@ -3246,7 +3272,7 @@ namespace Barotrauma FailedSelectedShuttle = null; //hashes match, all good - if (sub.MD5Hash?.Hash == md5Hash && Submarine.SavedSubmarines.Contains(sub)) + if (sub.MD5Hash?.Hash == md5Hash && SubmarineInfo.SavedSubmarines.Contains(sub)) { return true; } @@ -3261,7 +3287,7 @@ namespace Barotrauma FailedSelectedShuttle = new Pair(subName, md5Hash); string errorMsg = ""; - if (sub == null || !Submarine.SavedSubmarines.Contains(sub)) + if (sub == null || !SubmarineInfo.SavedSubmarines.Contains(sub)) { errorMsg = TextManager.GetWithVariable("SubNotFoundError", "[subname]", subName) + " "; } @@ -3303,7 +3329,7 @@ namespace Barotrauma return false; } - private void CreateSubPreview(Submarine sub) + private void CreateSubPreview(SubmarineInfo sub) { subPreviewContainer?.ClearChildren(); sub.CreatePreviewWindow(subPreviewContainer); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs index 922e24e57..d37df90f9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs @@ -237,7 +237,7 @@ namespace Barotrauma { foreach (XElement element in doc.Root.Elements()) { - if (element.Name.ToString().ToLowerInvariant() != prefab.Name.ToLowerInvariant()) continue; + if (!element.Name.ToString().Equals(prefab.Name, StringComparison.OrdinalIgnoreCase)) { continue; } SerializableProperty.SerializeProperties(prefab, element, true); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index 7bd320d68..d3df520ff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -965,7 +965,7 @@ namespace Barotrauma child.Visible = serverInfo.OwnerVerified && - serverInfo.ServerName.ToLowerInvariant().Contains(searchBox.Text.ToLowerInvariant()) && + serverInfo.ServerName.Contains(searchBox.Text, StringComparison.OrdinalIgnoreCase) && (!filterSameVersion.Selected || (remoteVersion != null && NetworkMember.IsCompatible(remoteVersion, GameMain.Version))) && (!filterPassword.Selected || !serverInfo.HasPassword) && (!filterIncompatible.Selected || !incompatible) && @@ -996,7 +996,7 @@ namespace Barotrauma foreach (GUITickBox tickBox in gameModeTickBoxes) { var gameMode = (string)tickBox.UserData; - if (!tickBox.Selected && (serverInfo.GameMode == gameMode.ToLowerInvariant() || serverInfo.GameMode == gameMode)) + if (!tickBox.Selected && serverInfo.GameMode.Equals(gameMode, StringComparison.OrdinalIgnoreCase)) { child.Visible = false; break; @@ -1304,6 +1304,8 @@ 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); #endif info.ConnectName = null; info.ConnectEndpoint = null; @@ -1512,7 +1514,7 @@ namespace Barotrauma { serverList.ClearChildren(); - if (masterServerData.Substring(0, 5).ToLowerInvariant() == "error") + if (masterServerData.Substring(0, 5).Equals("error", StringComparison.OrdinalIgnoreCase)) { DebugConsole.ThrowError("Error while connecting to master server (" + masterServerData + ")!"); return; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs index 59ef3ab77..5d66494e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs @@ -26,10 +26,24 @@ namespace Barotrauma //listbox that shows the files included in the item being created private GUIListBox createItemFileList; + private FileSystemWatcher createItemWatcher; + private readonly List tabButtons = new List(); - private readonly HashSet pendingPreviewImageDownloads = new HashSet(); - private readonly Dictionary itemPreviewSprites = new Dictionary(); + private class PendingPreviewImageDownload + { + /// + /// Was the image downloaded + /// + public bool Downloaded = false; + + /// + /// How many tasks are looking to create a preview image based on this download + /// + public int PendingLoads = 1; + } + private readonly Dictionary pendingPreviewImageDownloads = new Dictionary(); + private Dictionary itemPreviewSprites = new Dictionary(); private enum Tab { @@ -54,6 +68,8 @@ namespace Barotrauma { GameMain.Instance.OnResolutionChanged += CreateUI; CreateUI(); + + Steamworks.SteamUGC.GlobalOnItemInstalled += OnItemInstalled; } private void CreateUI() @@ -199,7 +215,7 @@ namespace Barotrauma { if (GUI.MouseOn is GUIButton || GUI.MouseOn?.Parent is GUIButton) { return false; } publishedItemList.Deselect(); - if (userdata is Submarine sub) + if (userdata is SubmarineInfo sub) { CreateWorkshopItem(sub); } @@ -215,6 +231,8 @@ namespace Barotrauma createItemFrame = new GUIFrame(new RectTransform(new Vector2(0.58f, 1.0f), tabs[(int)Tab.Publish].RectTransform, Anchor.TopRight), style: null); SelectTab(Tab.Mods); + + subscribedCoroutine = CoroutineManager.StartCoroutine(PollSubscribedItems()); } public override void Select() @@ -230,11 +248,37 @@ namespace Barotrauma SelectTab(Tab.Mods); } + private void OnItemInstalled(ulong itemId) + { + RefreshSubscribedItems(); + } + + CoroutineHandle subscribedCoroutine; + + private IEnumerable PollSubscribedItems() + { + if (!SteamManager.IsInitialized) { yield return CoroutineStatus.Success; } + + uint numSubscribed = 0; + while (true) + { + while (CoroutineManager.IsCoroutineRunning("Load")) { yield return new WaitForSeconds(1.0f); } + uint newNumSubscribed = Steamworks.SteamUGC.NumSubscribedItems; + if (newNumSubscribed != numSubscribed) + { + RefreshSubscribedItems(); + numSubscribed = newNumSubscribed; + } + + yield return new WaitForSeconds(1.0f); + } + } + private void SelectTab(Tab tab) { for (int i = 0; i < tabs.Length; i++) { - tabButtons[i].Selected = tabs[i].Visible = i == (int)tab; + tabButtons[i].Selected = tabs[i].Visible = i == (int)tab; } if (createItemFrame.CountChildren == 0) @@ -246,6 +290,7 @@ namespace Barotrauma }; } + createItemWatcher?.Dispose(); createItemWatcher = null; if (Screen.Selected == this) { switch (tab) @@ -272,6 +317,25 @@ namespace Barotrauma GameMain.SteamWorkshopScreen.Select(); } + public IEnumerable RefreshDownloadState() + { + bool isDownloading = true; + while (true) + { + SteamManager.GetSubscribedWorkshopItems((items) => + { + isDownloading = items.Any(it => it.IsDownloading || it.IsDownloadPending); + + GameMain.MainMenuScreen.SetDownloadingModsNotification(isDownloading); + }); + + if (!isDownloading) { break; } + + yield return new WaitForSeconds(0.5f); + } + yield return CoroutineStatus.Success; + } + private void RefreshSubscribedItems() { SteamManager.GetSubscribedWorkshopItems((items) => @@ -279,6 +343,8 @@ namespace Barotrauma //filter out the items published by the player (they're shown in the publish tab) var mySteamID = SteamManager.GetSteamID(); OnItemsReceived(GetVisibleItems(items.Where(it => it.Owner.Id != mySteamID)), subscribedItemList); + + GameMain.MainMenuScreen.SetDownloadingModsNotification(items.Any(it => it.IsDownloading || it.IsDownloadPending)); }); } @@ -312,7 +378,7 @@ namespace Barotrauma { CanBeFocused = false }; - foreach (Submarine sub in Submarine.SavedSubmarines) + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { if (sub.HasTag(SubmarineTag.HideInMenus)) { continue; } string subPath = Path.GetFullPath(sub.FilePath); @@ -414,7 +480,7 @@ namespace Barotrauma CanBeFocused = false }; } - else + else if (Screen.Selected == this) { new GUIImage(new RectTransform(new Point(iconSize), innerFrame.RectTransform), SteamManager.DefaultPreviewImage, scaleToFit: true) { @@ -430,16 +496,20 @@ namespace Barotrauma bool isNewImage; lock (pendingPreviewImageDownloads) { - isNewImage = !pendingPreviewImageDownloads.Contains(item?.PreviewImageUrl); - if (isNewImage) { pendingPreviewImageDownloads.Add(item?.PreviewImageUrl); } + isNewImage = !pendingPreviewImageDownloads.ContainsKey(item.Value.Id); + if (isNewImage) + { + if (File.Exists(imagePreviewPath)) + { + File.Delete(imagePreviewPath); + } + + pendingPreviewImageDownloads.Add(item.Value.Id, new PendingPreviewImageDownload()); + } } if (isNewImage) { - if (File.Exists(imagePreviewPath)) - { - File.Delete(imagePreviewPath); - } Directory.CreateDirectory(SteamManager.WorkshopItemPreviewImageFolder); Uri baseAddress = new Uri(item?.PreviewImageUrl); @@ -450,16 +520,23 @@ namespace Barotrauma var request = new RestRequest(fileName, Method.GET); client.ExecuteAsync(request, response => { - lock (pendingPreviewImageDownloads) - { - pendingPreviewImageDownloads.Remove(item?.PreviewImageUrl); - } - OnPreviewImageDownloaded(response, imagePreviewPath); - CoroutineManager.StartCoroutine(WaitForItemPreviewDownloaded(item, listBox, imagePreviewPath)); + OnPreviewImageDownloaded(response, imagePreviewPath, + () => + { + lock (pendingPreviewImageDownloads) + { + pendingPreviewImageDownloads[item.Value.Id].Downloaded = true; + } + CoroutineManager.StartCoroutine(WaitForItemPreviewDownloaded(item, listBox, imagePreviewPath)); + }); }); } else { + lock (pendingPreviewImageDownloads) + { + pendingPreviewImageDownloads[item.Value.Id].PendingLoads++; + } CoroutineManager.StartCoroutine(WaitForItemPreviewDownloaded(item, listBox, imagePreviewPath)); } } @@ -468,7 +545,7 @@ namespace Barotrauma { lock (pendingPreviewImageDownloads) { - pendingPreviewImageDownloads.Remove(item?.PreviewImageUrl); + pendingPreviewImageDownloads.Remove(item.Value.Id); } DebugConsole.ThrowError("Downloading the preview image of the Workshop item \"" + item?.Title + "\" failed.", e); } @@ -488,10 +565,11 @@ namespace Barotrauma CanBeFocused = false }; - if ((item?.IsSubscribed ?? false) && (item?.IsInstalled ?? false)) + if ((item?.IsSubscribed ?? false) && (item?.IsInstalled ?? false) && Directory.Exists(item?.Directory)) { - GUITickBox enabledTickBox = null; - try + bool installed = SteamManager.CheckWorkshopItemEnabled(item); + + if (!installed) { bool? compatible = SteamManager.CheckWorkshopItemCompatibility(item); if (compatible.HasValue && !compatible.Value) @@ -504,63 +582,32 @@ namespace Barotrauma } else { - enabledTickBox = new GUITickBox(new RectTransform(new Point(32, 32), rightColumn.RectTransform), null) - { - ToolTip = TextManager.Get("WorkshopItemEnabled"), - UserData = item, - }; - enabledTickBox.Selected = SteamManager.CheckWorkshopItemEnabled(item); - enabledTickBox.OnSelected = ToggleItemEnabled; - } - } - catch (Exception e) - { - if (enabledTickBox != null) { enabledTickBox.Enabled = false; } - itemFrame.ToolTip = e.Message; - itemFrame.Color = GUI.Style.Red; - itemFrame.HoverColor = GUI.Style.Red; - itemFrame.SelectedColor = GUI.Style.Red; - titleText.TextColor = GUI.Style.Red; + installed = SteamManager.EnableWorkShopItem(item, true, out string errorMsg, Screen.Selected == this); - if (item?.IsSubscribed ?? false) - { - new GUIButton(new RectTransform(new Vector2(0.5f, 0.5f), rightColumn.RectTransform), TextManager.Get("WorkshopItemUnsubscribe")) + if (!installed) { - UserData = item, - OnClicked = (btn, userdata) => - { - item?.Unsubscribe(); - subscribedItemList.RemoveChild(subscribedItemList.Content.GetChildByUserData(item)); - return true; - } - }; - } - } - - if (listBox != publishedItemList && SteamManager.CheckWorkshopItemEnabled(item) && !SteamManager.CheckWorkshopItemUpToDate(item)) - { - new GUIButton(new RectTransform(new Vector2(0.4f, 0.5f), rightColumn.RectTransform, Anchor.BottomLeft), text: TextManager.Get("WorkshopItemUpdate")) - { - UserData = "updatebutton", - Font = GUI.SmallFont, - OnClicked = (btn, userdata) => - { - if (SteamManager.UpdateWorkshopItem(item, out string errorMsg)) - { - new GUIMessageBox("", TextManager.GetWithVariable("WorkshopItemUpdated", "[itemname]", item?.Title)); - } - else - { - DebugConsole.ThrowError(errorMsg); - new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item?.Title, errorMsg })); - } - btn.Enabled = false; - btn.Visible = false; - return true; + DebugConsole.ThrowError(errorMsg); + new GUIMessageBox( + TextManager.Get("Error"), + TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { TextManager.EnsureUTF8(item?.Title), errorMsg })); } - }; + } + } + + if (installed) + { + bool upToDate = SteamManager.CheckWorkshopItemUpToDate(item); + + if (!upToDate) + { + if (!SteamManager.UpdateWorkshopItem(item, out string errorMsg)) + { + DebugConsole.ThrowError(errorMsg); + new GUIMessageBox( + TextManager.Get("Error"), + TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { TextManager.EnsureUTF8(item?.Title), errorMsg })); + } + } } } @@ -568,7 +615,11 @@ namespace Barotrauma { new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), rightColumn.RectTransform), TextManager.Get("WorkshopItemDownloading")); } - else + else if (item?.IsDownloadPending ?? false) + { + new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), rightColumn.RectTransform), TextManager.Get("WorkshopItemDownloadPending")); + } + else if (!(item?.IsSubscribed ?? false)) { var downloadBtn = new GUIButton(new RectTransform(new Point((int)(32 * GUI.Scale)), rightColumn.RectTransform), "", style: "GUIPlusButton") { @@ -579,10 +630,66 @@ namespace Barotrauma downloadBtn.OnClicked = (btn, userdata) => { DownloadItem(itemFrame, downloadBtn, item); return true; }; } + if ((item?.IsSubscribed ?? false) && listBox == subscribedItemList) + { + var reinstallBtn = new GUIButton(new RectTransform(new Point((int)(32 * GUI.Scale)), rightColumn.RectTransform), "", style: "GUIReloadButton") + { + ToolTip = TextManager.Get("WorkshopItemReinstall"), + ForceUpperCase = true, + UserData = "reinstall" + }; + reinstallBtn.OnClicked = (btn, userdata) => + { + var elem = subscribedItemList.Content.GetChildByUserData(item); + try + { + bool reselect = GameMain.Config.SelectedContentPackages.Any(cp => !string.IsNullOrWhiteSpace(cp.SteamWorkshopUrl) && cp.SteamWorkshopUrl == item?.Url); + if (!SteamManager.DisableWorkShopItem(item, false, out string errorMsg) || + !SteamManager.EnableWorkShopItem(item, true, out errorMsg, reselect, true)) + { + DebugConsole.ThrowError($"Failed to reinstall \"{item?.Title}\": {errorMsg}", null, true); + elem.Flash(GUI.Style.Red); + } + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to reinstall \"{item?.Title}\"", e, true); + elem.Flash(GUI.Style.Red); + } + return true; + }; + var unsubBtn = new GUIButton(new RectTransform(new Point((int)(32 * GUI.Scale)), rightColumn.RectTransform), "", style: "GUIMinusButton") + { + ToolTip = TextManager.Get("WorkshopItemUnsubscribe"), + ForceUpperCase = true, + UserData = "unsubscribe" + }; + unsubBtn.OnClicked = (btn, userdata) => + { + SteamManager.DisableWorkShopItem(item, true, out _); + item?.Unsubscribe(); + subscribedItemList.RemoveChild(subscribedItemList.Content.GetChildByUserData(item)); + return true; + }; + } + innerFrame.Recalculate(); listBox.RecalculateChildren(); } + public void SetReinstallButtonStatus(Steamworks.Ugc.Item? item, bool enabled, Color? flashColor) + { + var child = subscribedItemList.Content.FindChild((component) => { return (component.UserData is Steamworks.Ugc.Item?) && (component.UserData as Steamworks.Ugc.Item?)?.Id == item?.Id; }); + if (child != null) + { + var reinstallBtn = child.FindChild("reinstall", true); + if (reinstallBtn != null) { reinstallBtn.Enabled = enabled; } + var unsubBtn = child.FindChild("unsubscribe", true); + if (unsubBtn != null) { unsubBtn.Enabled = enabled; } + if (flashColor.HasValue) { child.Flash(flashColor); } + } + } + private void RemoveItemFromLists(ulong itemID) { RemoveItemFromList(publishedItemList); @@ -596,7 +703,7 @@ namespace Barotrauma } } - private void CreateMyItemFrame(Submarine submarine, GUIListBox listBox) + private void CreateMyItemFrame(SubmarineInfo submarine, GUIListBox listBox) { var itemFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform, minSize: new Point(0, 80)), style: "ListBoxElement") @@ -629,21 +736,27 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), innerFrame.RectTransform), contentPackage.Name, textAlignment: Alignment.CenterLeft); } - private void OnPreviewImageDownloaded(IRestResponse response, string previewImagePath) + private void OnPreviewImageDownloaded(IRestResponse response, string previewImagePath, Action action) { if (response.ResponseStatus == ResponseStatus.Completed) { - try - { - File.WriteAllBytes(previewImagePath, response.RawBytes); - } - catch (Exception e) - { - string errorMsg = "Failed to save workshop item preview image to \"" + previewImagePath + "\"."; - GameAnalyticsManager.AddErrorEventOnce("SteamWorkshopScreen.OnItemPreviewDownloaded:WriteAllBytesFailed" + previewImagePath, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + "\n" + e.Message); - return; - } + TaskPool.Add(WritePreviewImageAsync(response, previewImagePath), (task) => { action?.Invoke(); }); + } + } + + private async Task WritePreviewImageAsync(IRestResponse response, string previewImagePath) + { + await Task.Yield(); + try + { + File.WriteAllBytes(previewImagePath, response.RawBytes); + } + catch (Exception e) + { + string errorMsg = "Failed to save workshop item preview image to \"" + previewImagePath + "\"."; + GameAnalyticsManager.AddErrorEventOnce("SteamWorkshopScreen.OnItemPreviewDownloaded:WriteAllBytesFailed" + previewImagePath, + GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + "\n" + e.Message); + return; } } @@ -653,47 +766,67 @@ namespace Barotrauma { lock (pendingPreviewImageDownloads) { - if (!pendingPreviewImageDownloads.Contains(item?.PreviewImageUrl)) { break; } + if (pendingPreviewImageDownloads[item.Value.Id].Downloaded){ break; } } - yield return CoroutineStatus.Running; + yield return new WaitForSeconds(0.2f); } if (File.Exists(previewImagePath)) { - Sprite newSprite; - if (itemPreviewSprites.ContainsKey(item?.PreviewImageUrl)) + TaskPool.Add(LoadPreviewImageAsync(item?.PreviewImageUrl, previewImagePath), + new Tuple(item, listBox), + (task, tuple) => { - newSprite = itemPreviewSprites[item?.PreviewImageUrl]; - } - else - { - newSprite = new Sprite(previewImagePath, sourceRectangle: null); - itemPreviewSprites.Add(item?.PreviewImageUrl, newSprite); - } + (var it, var lb) = tuple; + var previewImage = lb.Content.FindChild(item)?.GetChildByUserData("previewimage") as GUIImage; + if (previewImage != null) + { + previewImage.Sprite = task.Result; + } + else + { + CreateWorkshopItemFrame(it, lb); + } - if (listBox.Content.FindChild(item)?.GetChildByUserData("previewimage") is GUIImage previewImage) - { - previewImage.Sprite = newSprite; - } - else - { - CreateWorkshopItemFrame(item, listBox); - } + if (modsPreviewFrame.FindChild(it) != null) + { + ShowItemPreview(it, modsPreviewFrame); + } + if (browsePreviewFrame.FindChild(item) != null) + { + ShowItemPreview(it, browsePreviewFrame); + } - if (modsPreviewFrame.FindChild(item) != null) - { - ShowItemPreview(item, modsPreviewFrame); - } - if (browsePreviewFrame.FindChild(item) != null) - { - ShowItemPreview(item, browsePreviewFrame); - } + lock (pendingPreviewImageDownloads) + { + pendingPreviewImageDownloads[it.Value.Id].PendingLoads--; + if (pendingPreviewImageDownloads[it.Value.Id].PendingLoads <= 0) { pendingPreviewImageDownloads.Remove(it.Value.Id); } + } + }); } yield return CoroutineStatus.Success; } + private async Task LoadPreviewImageAsync(string previewImageUrl, string previewImagePath) + { + await Task.Yield(); + lock (itemPreviewSprites) + { + if (itemPreviewSprites.ContainsKey(previewImageUrl)) + { + return itemPreviewSprites[previewImageUrl]; + } + else + { + Sprite newSprite = new Sprite(previewImagePath, sourceRectangle: null); + itemPreviewSprites.Add(previewImageUrl, newSprite); + return newSprite; + } + } + } + private bool DownloadItem(GUIComponent frame, GUIButton downloadButton, Steamworks.Ugc.Item? item) { if (item == null) { return false; } @@ -721,49 +854,6 @@ namespace Barotrauma return true; } - private bool ToggleItemEnabled(GUITickBox tickBox) - { - if (!(tickBox.UserData is Steamworks.Ugc.Item?)) { return false; } - - var item = tickBox.UserData as Steamworks.Ugc.Item?; - if (item == null) { return false; } - - //currently editing the item, don't allow enabling/disabling it - if (itemEditor?.FileId == item?.Id) { tickBox.Selected = true; return false; } - - var updateButton = tickBox.Parent.FindChild("updatebutton"); - - string errorMsg; - if (tickBox.Selected) - { - if (!SteamManager.EnableWorkShopItem(item, false, out errorMsg)) - { - tickBox.Visible = false; - tickBox.Selected = false; - if (tickBox.Parent.GetChildByUserData("titletext") is GUITextBlock titleText) { titleText.TextColor = GUI.Style.Red; } - } - } - else - { - if (!SteamManager.DisableWorkShopItem(item, false, out errorMsg)) - { - tickBox.Enabled = false; - } - GameMain.Config.EnsureCoreContentPackageSelected(); - } - if (updateButton != null) - { - //cannot update if enabling/disabling the item failed or if the item is not enabled - updateButton.Enabled = tickBox.Enabled && tickBox.Selected; - } - if (!string.IsNullOrEmpty(errorMsg)) - { - new GUIMessageBox(TextManager.Get("Error"), errorMsg); - } - - return true; - } - private void ShowItemPreview(Steamworks.Ugc.Item? item, GUIFrame itemPreviewFrame) { itemPreviewFrame.ClearChildren(); @@ -920,7 +1010,7 @@ namespace Barotrauma }; } - private void CreateWorkshopItem(Submarine sub) + private void CreateWorkshopItem(SubmarineInfo sub) { string destinationFolder = Path.Combine("Mods", sub.Name); itemContentPackage = ContentPackage.CreatePackage(sub.Name, Path.Combine(destinationFolder, SteamManager.MetadataFileName), corePackage: false); @@ -940,8 +1030,8 @@ namespace Barotrauma itemContentPackage.AddFile(sub.FilePath, ContentType.Submarine); itemContentPackage.Name = sub.Name; itemContentPackage.Save(itemContentPackage.Path); - ContentPackage.List.Add(itemContentPackage); - GameMain.Config.SelectContentPackage(itemContentPackage); + //ContentPackage.List.Add(itemContentPackage); + //GameMain.Config.SelectContentPackage(itemContentPackage); itemEditor = itemEditor?.WithTitle(sub.Name).WithTag("Submarine").WithDescription(sub.Description); @@ -1097,7 +1187,7 @@ namespace Barotrauma var tagBtn = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), tagHolder.Content.RectTransform, anchor: Anchor.CenterLeft), tag.CapitaliseFirstInvariant(), style: "GUIButtonRound"); tagBtn.TextBlock.AutoScaleHorizontal = true; - tagBtn.Selected = itemEditor?.Tags?.Any(t => t.ToLowerInvariant() == tag) ?? false; + tagBtn.Selected = itemEditor?.Tags?.Any(t => t.Equals(tag, StringComparison.OrdinalIgnoreCase)) ?? false; tagBtn.OnClicked = (btn, userdata) => { @@ -1108,7 +1198,7 @@ namespace Barotrauma } else { - itemEditor?.Tags?.RemoveAll(t => t.ToLowerInvariant() == tagBtn.Text.ToLowerInvariant()); + itemEditor?.Tags?.RemoveAll(t => t.Equals(tagBtn.Text, StringComparison.OrdinalIgnoreCase)); tagBtn.Selected = false; } return true; @@ -1201,6 +1291,16 @@ namespace Barotrauma OnClicked = (btn, userdata) => { ToolBox.OpenFileWithShell(Path.GetFullPath(Path.GetDirectoryName(itemContentPackage.Path))); return true; } }; createItemFileList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.35f), createItemContent.RectTransform)); + createItemWatcher?.Dispose(); + createItemWatcher = new FileSystemWatcher(Path.GetDirectoryName(itemContentPackage.Path)) + { + Filter = "*", + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName + }; + createItemWatcher.Created += OnFileSystemChanges; + createItemWatcher.Deleted += OnFileSystemChanges; + createItemWatcher.Renamed += OnFileSystemChanges; + createItemWatcher.EnableRaisingEvents = true; RefreshCreateItemFileList(); var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), createItemContent.RectTransform), isHorizontal: true) @@ -1447,23 +1547,54 @@ namespace Barotrauma { destinationPath = Path.Combine(modFolder, filePathRelativeToModFolder); } - itemContentPackage.AddFile(destinationPath, ContentType.None); } - itemContentPackage.Save(itemContentPackage.Path); RefreshCreateItemFileList(); } - + + volatile bool refreshFileList = false; + + private void OnFileSystemChanges(object sender, FileSystemEventArgs e) + { + refreshFileList = true; + } + private void RefreshCreateItemFileList() { createItemFileList.ClearChildren(); if (itemContentPackage == null) return; var contentTypes = Enum.GetValues(typeof(ContentType)); - - foreach (ContentFile contentFile in itemContentPackage.Files) + + List files = itemContentPackage.Files.ToList(); + + foreach (ContentFile contentFile in files) + { + bool fileExists = File.Exists(contentFile.Path); + + if (!fileExists) { itemContentPackage.Files.Remove(contentFile); continue; } + } + + List allFiles = Directory.GetFiles(Path.GetDirectoryName(itemContentPackage.Path), "*", SearchOption.AllDirectories) + .Select(f => new ContentFile(f, ContentType.None)) + .Where(file => Path.GetFileName(file.Path) != SteamManager.MetadataFileName && + Path.GetFileName(file.Path) != SteamManager.PreviewImageName) + .ToList(); + for (int i=0;i string.Equals(Path.GetFullPath(f.Path).CleanUpPath(), + Path.GetFullPath(file.Path).CleanUpPath(), + StringComparison.InvariantCultureIgnoreCase)); + if (otherFile != null) + { + //replace the generated ContentFile object with the one that's present in the + //content package to determine which tickboxes should already be checked + allFiles[i] = otherFile; + } + } + + foreach (ContentFile contentFile in allFiles) { bool illegalPath = !ContentPackage.IsModFilePathAllowed(contentFile); - //string pathInStagingFolder = Path.Combine(SteamManager.WorkshopItemStagingFolder, contentFile.Path); - //bool fileInStagingFolder = File.Exists(pathInStagingFolder); bool fileExists = File.Exists(contentFile.Path); var fileFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.12f), createItemFileList.Content.RectTransform) { MinSize = new Point(0, 20) }, @@ -1479,11 +1610,25 @@ namespace Barotrauma RelativeSpacing = 0.05f }; - var tickBox = new GUITickBox(new RectTransform(new Vector2(0.1f, 0.8f), content.RectTransform), "") + var tickBox = new GUITickBox(new RectTransform(Vector2.One, content.RectTransform, scaleBasis: ScaleBasis.BothHeight), "") { - Selected = fileExists && !illegalPath, - Enabled = false, - ToolTip = TextManager.Get(illegalPath ? "WorkshopItemFileNotIncluded" : "WorkshopItemFileIncluded") + Selected = itemContentPackage.Files.Contains(contentFile), + UserData = contentFile + }; + + tickBox.OnSelected = (tb) => + { + ContentFile f = tb.UserData as ContentFile; + if (tb.Selected) + { + if (!itemContentPackage.Files.Contains(f)) { itemContentPackage.Files.Add(f); } + } + else + { + if (itemContentPackage.Files.Contains(f)) { itemContentPackage.Files.Remove(f); } + } + + return true; }; var nameText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), content.RectTransform, Anchor.CenterLeft), contentFile.Path, font: GUI.SmallFont) @@ -1523,10 +1668,29 @@ namespace Barotrauma { OnClicked = (btn, userdata) => { - itemContentPackage.RemoveFile(contentFile); - itemContentPackage.Save(itemContentPackage.Path); - RefreshCreateItemFileList(); - RefreshMyItemList(); + var msgBox = new GUIMessageBox(TextManager.Get("ConfirmFileDeletionHeader"), + TextManager.GetWithVariable("ConfirmFileDeletion", "[file]", contentFile.Path), + new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) + { + UserData = "verificationprompt" + }; + msgBox.Buttons[0].OnClicked = (applyButton, obj) => + { + try + { + File.Delete(contentFile.Path); + if (contentFile.Type == ContentType.Submarine) { SubmarineInfo.RefreshSavedSub(contentFile.Path); } + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to delete \"${contentFile.Path}\".", e); + } + //RefreshCreateItemFileList(); + RefreshMyItemList(); + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked = msgBox.Close; return true; } }; @@ -1536,6 +1700,8 @@ namespace Barotrauma new Point(0, (int)(content.RectTransform.Children.Max(c => c.MinSize.Y) / content.RectTransform.RelativeSize.Y)); nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, maxWidth: nameText.Rect.Width); } + + itemContentPackage.Save(itemContentPackage.Path); } private void PublishWorkshopItem() @@ -1640,6 +1806,11 @@ namespace Barotrauma public override void Update(double deltaTime) { + if (refreshFileList) + { + RefreshCreateItemFileList(); + refreshFileList = false; + } } #endregion diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index f8e71d755..7de1726b2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -7,6 +7,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; +using EventInput; +using Microsoft.Xna.Framework.Input; namespace Barotrauma { @@ -30,6 +32,8 @@ namespace Barotrauma private readonly Camera cam; + private SubmarineInfo backedUpSubInfo; + private Point screenResolution; private bool lightingEnabled; @@ -88,6 +92,9 @@ namespace Barotrauma private GUITextBlock submarineDescriptionCharacterCount; private Mode mode; + + // Prevent the mode from changing + private bool lockMode; public override Camera Cam { @@ -96,9 +103,9 @@ namespace Barotrauma public string GetSubDescription() { - string localizedDescription = TextManager.Get("submarine.description." + (Submarine.MainSub?.Name ?? ""), true); + string localizedDescription = TextManager.Get("submarine.description." + (Submarine.MainSub?.Info.Name ?? ""), true); if (localizedDescription != null) { return localizedDescription; } - return (Submarine.MainSub == null) ? "" : Submarine.MainSub.Description; + return (Submarine.MainSub == null) ? "" : Submarine.MainSub.Info.Description; } private string GetTotalHullVolume() @@ -214,63 +221,10 @@ namespace Barotrauma 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), - TextManager.Get("unspecifiedsubfilename"), font: GUI.LargeFont, textAlignment: Alignment.CenterLeft); - - linkedSubBox = new GUIDropDown(new RectTransform(new Vector2(0.15f, 0.9f), paddedTopPanel.RectTransform), - TextManager.Get("AddSubButton"), elementCount: 20) + new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "TestButton") { - ToolTip = TextManager.Get("AddSubToolTip") - }; - foreach (Submarine sub in Submarine.SavedSubmarines) - { - linkedSubBox.AddItem(sub.Name, sub); - } - linkedSubBox.OnSelected += SelectLinkedSub; - linkedSubBox.OnDropped += (component, obj) => - { - MapEntity.SelectedList.Clear(); - return true; - }; - - new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), paddedTopPanel.RectTransform), style: "VerticalLine"); - - defaultModeTickBox = new GUITickBox(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "EditSubButton") - { - ToolTip = TextManager.Get("SubEditorEditingMode"), - OnSelected = (GUITickBox tBox) => - { - if (tBox.Selected) { SetMode(Mode.Default); } - return true; - } - }; - - characterModeTickBox = new GUITickBox(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "CharacterModeButton") - { - ToolTip = TextManager.Get("CharacterModeButton") + '\n' + TextManager.Get("CharacterModeToolTip"), - OnSelected = (GUITickBox tBox) => - { - SetMode(tBox.Selected ? Mode.Character : Mode.Default); - return true; - } - }; - - wiringModeTickBox = new GUITickBox(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "WiringModeButton") - { - ToolTip = TextManager.Get("WiringModeButton") + '\n' + TextManager.Get("WiringModeToolTip"), - OnSelected = (GUITickBox tBox) => - { - SetMode(tBox.Selected ? Mode.Wiring : Mode.Default); - return true; - } - }; - - new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), paddedTopPanel.RectTransform), style: "VerticalLine"); - - new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "GenerateWaypointsButton") - { - ToolTip = TextManager.Get("GenerateWaypointsButton") + '\n' + TextManager.Get("GenerateWaypointsToolTip"), - OnClicked = GenerateWaypoints + ToolTip = TextManager.Get("TestSubButton"), + OnClicked = TestSubmarine }; new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), paddedTopPanel.RectTransform), style: "VerticalLine"); @@ -282,7 +236,7 @@ namespace Barotrauma { previouslyUsedPanel.Visible = false; showEntitiesPanel.Visible = !showEntitiesPanel.Visible; - showEntitiesPanel.RectTransform.AbsoluteOffset = new Point(btn.Rect.X, TopPanel.Rect.Height); + showEntitiesPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(btn.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); return true; } }; @@ -294,7 +248,110 @@ namespace Barotrauma { showEntitiesPanel.Visible = false; previouslyUsedPanel.Visible = !previouslyUsedPanel.Visible; - previouslyUsedPanel.RectTransform.AbsoluteOffset = new Point(btn.Rect.X, TopPanel.Rect.Height); + previouslyUsedPanel.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), + TextManager.Get("unspecifiedsubfilename"), font: GUI.LargeFont, textAlignment: Alignment.CenterLeft); + + linkedSubBox = new GUIDropDown(new RectTransform(new Vector2(0.15f, 0.9f), paddedTopPanel.RectTransform), + TextManager.Get("AddSubButton"), elementCount: 20) + { + ToolTip = TextManager.Get("AddSubToolTip") + }; + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) + { + linkedSubBox.AddItem(sub.Name, sub); + } + linkedSubBox.OnSelected += SelectLinkedSub; + linkedSubBox.OnDropped += (component, obj) => + { + MapEntity.SelectedList.Clear(); + return true; + }; + + var spacing = new GUIFrame(new RectTransform(new Vector2(0.02f, 1.0f), paddedTopPanel.RectTransform), style: null); + new GUIFrame(new RectTransform(new Vector2(0.1f, 0.9f), spacing.RectTransform, Anchor.Center), style: "VerticalLine"); + + defaultModeTickBox = new GUITickBox(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "EditSubButton") + { + ToolTip = TextManager.Get("SubEditorEditingMode"), + OnSelected = (GUITickBox tBox) => + { + if (!lockMode) + { + if (tBox.Selected) { SetMode(Mode.Default); } + + return true; + } + else { return false; } + } + }; + + characterModeTickBox = new GUITickBox(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "CharacterModeButton") + { + ToolTip = TextManager.Get("CharacterModeButton") + '\n' + TextManager.Get("CharacterModeToolTip"), + OnSelected = (GUITickBox tBox) => + { + if (!lockMode) + { + SetMode(tBox.Selected ? Mode.Character : Mode.Default); + return true; + } + else { return false; } + } + }; + + wiringModeTickBox = new GUITickBox(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "WiringModeButton") + { + ToolTip = TextManager.Get("WiringModeButton") + '\n' + TextManager.Get("WiringModeToolTip"), + OnSelected = (GUITickBox tBox) => + { + if (!lockMode) + { + SetMode(tBox.Selected ? Mode.Wiring : Mode.Default); + return true; + } + else { return false; } + } + }; + + spacing = new GUIFrame(new RectTransform(new Vector2(0.02f, 1.0f), paddedTopPanel.RectTransform), style: null); + new GUIFrame(new RectTransform(new Vector2(0.1f, 0.9f), spacing.RectTransform, Anchor.Center), style: "VerticalLine"); + + new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "GenerateWaypointsButton") + { + ToolTip = TextManager.Get("GenerateWaypointsButton") + '\n' + TextManager.Get("GenerateWaypointsToolTip"), + OnClicked = (btn, userdata) => + { + if (WayPoint.WayPointList.Any()) + { + var generateWaypointsVerification = new GUIMessageBox("", TextManager.Get("generatewaypointsverification"), new string[] { TextManager.Get("ok"), TextManager.Get("cancel") }); + generateWaypointsVerification.Buttons[0].OnClicked = (btn, userdata) => + { + if (GenerateWaypoints()) + { + GUI.AddMessage(TextManager.Get("waypointsgeneratedsuccesfully"), GUI.Style.Green); + } + WayPoint.ShowWayPoints = true; + generateWaypointsVerification.Close(); + return true; + }; + generateWaypointsVerification.Buttons[1].OnClicked = generateWaypointsVerification.Close; + } + else + { + if (GenerateWaypoints()) + { + GUI.AddMessage(TextManager.Get("waypointsgeneratedsuccesfully"), GUI.Style.Green); + } + WayPoint.ShowWayPoints = true; + + } return true; } }; @@ -325,8 +382,7 @@ namespace Barotrauma showEntitiesPanel = new GUIFrame(new RectTransform(new Vector2(0.08f, 0.5f), GUI.Canvas) { - MinSize = new Point(170, 0), - AbsoluteOffset = new Point(visibilityButton.Rect.X, TopPanel.Rect.Height) + MinSize = new Point(170, 0) }) { Visible = false @@ -613,6 +669,41 @@ namespace Barotrauma screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } + private bool TestSubmarine(GUIButton button, object obj) + { + List errorMsgs = new List(); + + if (!Hull.hullList.Any()) + { + errorMsgs.Add(TextManager.Get("NoHullsWarning")); + } + + if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Human)) + { + errorMsgs.Add(TextManager.Get("NoHumanSpawnpointWarning")); + } + + if (errorMsgs.Any()) + { + new GUIMessageBox(TextManager.Get("Error"), string.Join("\n\n", errorMsgs), new Vector2(0.25f, 0.0f), new Point(400, 200)); + return true; + } + + backedUpSubInfo = new SubmarineInfo(Submarine.MainSub); + + GameMain.GameScreen.Select(); + + GameSession gameSession = new GameSession(backedUpSubInfo, "", GameModePreset.List.Find(gm => gm.Identifier == "subtest"), null); + gameSession.StartRound(null, false); + + return true; + } + + public void ClearBackedUpSubInfo() + { + backedUpSubInfo = null; + } + private void UpdateEntityList() { entityList.Content.ClearChildren(); @@ -741,9 +832,20 @@ namespace Barotrauma { base.Select(); + GameMain.LightManager.AmbientLight = + Level.Loaded?.GenerationParams?.AmbientLightColor ?? + LevelGenerationParams.LevelParams?.FirstOrDefault()?.AmbientLightColor ?? + new Color(20, 20, 20, 255); + UpdateEntityList(); - string name = (Submarine.MainSub == null) ? TextManager.Get("unspecifiedsubfilename") : Submarine.MainSub.Name; + if (backedUpSubInfo != null) + { + Submarine.Unload(); + } + + string name = (Submarine.MainSub == null) ? TextManager.Get("unspecifiedsubfilename") : Submarine.MainSub.Info.Name; + if (backedUpSubInfo != null) { name = backedUpSubInfo.Name; } subNameLabel.Text = ToolBox.LimitString(name, subNameLabel.Font, subNameLabel.Rect.Width); foreach (MapEntityPrefab prefab in MapEntityPrefab.List) @@ -760,23 +862,26 @@ namespace Barotrauma GUI.ForceMouseOn(null); SetMode(Mode.Default); - if (Submarine.MainSub != null) + if (backedUpSubInfo != null) { - Submarine.MainSub.SetPrevTransform(Submarine.MainSub.Position); - Submarine.MainSub.UpdateTransform(); - cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; + Submarine.MainSub = new Submarine(backedUpSubInfo); + backedUpSubInfo = null; } - else + else if (Submarine.MainSub == null) { - Submarine.MainSub = new Submarine(Path.Combine(Submarine.SavePath, TextManager.Get("UnspecifiedSubFileName") + ".sub"), "", false); - cam.Position = Submarine.MainSub.Position; + var subInfo = new SubmarineInfo(); + Submarine.MainSub = new Submarine(subInfo); } + Submarine.MainSub.SetPrevTransform(Submarine.MainSub.Position); + Submarine.MainSub.UpdateTransform(interpolate: false); + cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; + GameMain.SoundManager.SetCategoryGainMultiplier("default", 0.0f, 0); GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", 0.0f, 0); linkedSubBox.ClearChildren(); - foreach (Submarine sub in Submarine.SavedSubmarines) + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { linkedSubBox.AddItem(sub.Name, sub); } @@ -987,27 +1092,37 @@ namespace Barotrauma nameBox.Flash(); return false; } - - foreach (char illegalChar in Path.GetInvalidFileNameChars()) + var result = SaveSubToFile(nameBox.Text); + saveFrame = null; + return result; + } + + private bool SaveSubToFile(string name) + { + if (string.IsNullOrWhiteSpace(name)) { - if (nameBox.Text.Contains(illegalChar)) - { - GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUI.Style.Red); - nameBox.Flash(); - return false; - } + GUI.AddMessage(TextManager.Get("SubNameMissingWarning"), GUI.Style.Red); + return false; } - string savePath = nameBox.Text + ".sub"; - string prevSavePath = null; - if (Submarine.MainSub != null) + foreach (var illegalChar in Path.GetInvalidFileNameChars()) { - prevSavePath = Submarine.MainSub.FilePath; - savePath = Path.Combine(Path.GetDirectoryName(Submarine.MainSub.FilePath), savePath); + if (!name.Contains(illegalChar)) continue; + GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUI.Style.Red); + return false; + } + + string savePath = name + ".sub"; + string prevSavePath = null; + if (!string.IsNullOrEmpty(Submarine.MainSub?.Info.FilePath) && + Submarine.MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) + { + prevSavePath = Submarine.MainSub.Info.FilePath.CleanUpPath(); + savePath = Path.Combine(Path.GetDirectoryName(Submarine.MainSub.Info.FilePath), savePath).CleanUpPath(); } else { - savePath = Path.Combine(Submarine.SavePath, savePath); + savePath = Path.Combine(SubmarineInfo.SavePath, savePath); } #if !DEBUG @@ -1015,8 +1130,8 @@ namespace Barotrauma if (vanilla != null) { var vanillaSubs = vanilla.GetFilesOfType(ContentType.Submarine); - string pathToCompare = savePath.Replace(@"\", @"/").ToLowerInvariant(); - if (vanillaSubs.Any(sub => sub.Replace(@"\", @"/").ToLowerInvariant() == pathToCompare)) + string pathToCompare = savePath.Replace(@"\", @"/"); + if (vanillaSubs.Any(sub => sub.Replace(@"\", @"/").Equals(pathToCompare, StringComparison.OrdinalIgnoreCase))) { GUI.AddMessage(TextManager.Get("CannotEditVanillaSubs"), GUI.Style.Red, font: GUI.LargeFont); return false; @@ -1024,38 +1139,35 @@ namespace Barotrauma } #endif - if (previewImage.Sprite?.Texture != null) + if (previewImage?.Sprite?.Texture != null) { using (MemoryStream imgStream = new MemoryStream()) { previewImage.Sprite.Texture.SaveAsPng(imgStream, previewImage.Sprite.Texture.Width, previewImage.Sprite.Texture.Height); - Submarine.SaveCurrent(savePath, imgStream); + Submarine.MainSub.SaveAs(savePath, imgStream); } } else { - Submarine.SaveCurrent(savePath); + Submarine.MainSub.SaveAs(savePath); } - Submarine.MainSub.CheckForErrors(); + Submarine.MainSub?.CheckForErrors(); - GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", Submarine.MainSub.FilePath), GUI.Style.Green); - - Submarine.RefreshSavedSub(savePath); + GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUI.Style.Green); + SubmarineInfo.RefreshSavedSub(savePath); if (prevSavePath != null && prevSavePath != savePath) { - Submarine.RefreshSavedSub(prevSavePath); + SubmarineInfo.RefreshSavedSub(prevSavePath); } linkedSubBox.ClearChildren(); - foreach (Submarine sub in Submarine.SavedSubmarines) + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { linkedSubBox.AddItem(sub.Name, sub); } - subNameLabel.Text = ToolBox.LimitString(Submarine.MainSub.Name, subNameLabel.Font, subNameLabel.Rect.Width); + subNameLabel.Text = ToolBox.LimitString(Submarine.MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); - saveFrame = null; - return false; } @@ -1160,15 +1272,15 @@ namespace Barotrauma crewSizeMin.OnValueChanged += (numberInput) => { crewSizeMax.IntValue = Math.Max(crewSizeMax.IntValue, numberInput.IntValue); - Submarine.MainSub.RecommendedCrewSizeMin = crewSizeMin.IntValue; - Submarine.MainSub.RecommendedCrewSizeMax = crewSizeMax.IntValue; + Submarine.MainSub.Info.RecommendedCrewSizeMin = crewSizeMin.IntValue; + Submarine.MainSub.Info.RecommendedCrewSizeMax = crewSizeMax.IntValue; }; crewSizeMax.OnValueChanged += (numberInput) => { crewSizeMin.IntValue = Math.Min(crewSizeMin.IntValue, numberInput.IntValue); - Submarine.MainSub.RecommendedCrewSizeMin = crewSizeMin.IntValue; - Submarine.MainSub.RecommendedCrewSizeMax = crewSizeMax.IntValue; + Submarine.MainSub.Info.RecommendedCrewSizeMin = crewSizeMin.IntValue; + Submarine.MainSub.Info.RecommendedCrewSizeMax = crewSizeMax.IntValue; }; var crewExpArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), leftColumn.RectTransform), isHorizontal: true) @@ -1191,7 +1303,7 @@ namespace Barotrauma if (currentIndex < 0) currentIndex = crewExperienceLevels.Length - 1; experienceText.UserData = crewExperienceLevels[currentIndex]; experienceText.Text = TextManager.Get(crewExperienceLevels[currentIndex]); - Submarine.MainSub.RecommendedCrewExperience = (string)experienceText.UserData; + Submarine.MainSub.Info.RecommendedCrewExperience = (string)experienceText.UserData; return true; }; @@ -1202,18 +1314,18 @@ namespace Barotrauma if (currentIndex >= crewExperienceLevels.Length) currentIndex = 0; experienceText.UserData = crewExperienceLevels[currentIndex]; experienceText.Text = TextManager.Get(crewExperienceLevels[currentIndex]); - Submarine.MainSub.RecommendedCrewExperience = (string)experienceText.UserData; + Submarine.MainSub.Info.RecommendedCrewExperience = (string)experienceText.UserData; return true; }; if (Submarine.MainSub != null) { - int min = Submarine.MainSub.RecommendedCrewSizeMin; - int max = Submarine.MainSub.RecommendedCrewSizeMax; + int min = Submarine.MainSub.Info.RecommendedCrewSizeMin; + int max = Submarine.MainSub.Info.RecommendedCrewSizeMax; crewSizeMin.IntValue = min; crewSizeMax.IntValue = max; - experienceText.UserData = string.IsNullOrEmpty(Submarine.MainSub.RecommendedCrewExperience) ? - crewExperienceLevels[0] : Submarine.MainSub.RecommendedCrewExperience; + experienceText.UserData = string.IsNullOrEmpty(Submarine.MainSub.Info.RecommendedCrewExperience) ? + crewExperienceLevels[0] : Submarine.MainSub.Info.RecommendedCrewExperience; experienceText.Text = TextManager.Get((string)experienceText.UserData); } @@ -1222,7 +1334,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), TextManager.Get("SubPreviewImage"), font: GUI.SubHeadingFont); var previewImageHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), rightColumn.RectTransform), style: null) { Color = Color.Black, CanBeFocused = false }; - previewImage = new GUIImage(new RectTransform(Vector2.One, previewImageHolder.RectTransform), Submarine.MainSub?.PreviewImage, scaleToFit: true); + previewImage = new GUIImage(new RectTransform(Vector2.One, previewImageHolder.RectTransform), Submarine.MainSub?.Info.PreviewImage, scaleToFit: true); var previewImageButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; @@ -1236,7 +1348,7 @@ namespace Barotrauma previewImage.Sprite = new Sprite(TextureLoader.FromStream(imgStream), null, null); if (Submarine.MainSub != null) { - Submarine.MainSub.PreviewImage = previewImage.Sprite; + Submarine.MainSub.Info.PreviewImage = previewImage.Sprite; } } return true; @@ -1258,7 +1370,7 @@ namespace Barotrauma previewImage.Sprite = new Sprite(file, sourceRectangle: null); if (Submarine.MainSub != null) { - Submarine.MainSub.PreviewImage = previewImage.Sprite; + Submarine.MainSub.Info.PreviewImage = previewImage.Sprite; } }; FileSelection.ClearFileTypeFilters(); @@ -1288,7 +1400,7 @@ namespace Barotrauma var tagTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), tagContainer.Content.RectTransform), tagStr, font: GUI.SmallFont) { - Selected = Submarine.MainSub == null ? false : Submarine.MainSub.HasTag(tag), + Selected = Submarine.MainSub == null ? false : Submarine.MainSub.Info.HasTag(tag), UserData = tag, OnSelected = (GUITickBox tickBox) => @@ -1296,11 +1408,11 @@ namespace Barotrauma if (Submarine.MainSub == null) return false; if (tickBox.Selected) { - Submarine.MainSub.AddTag((SubmarineTag)tickBox.UserData); + Submarine.MainSub.Info.AddTag((SubmarineTag)tickBox.UserData); } else { - Submarine.MainSub.RemoveTag((SubmarineTag)tickBox.UserData); + Submarine.MainSub.Info.RemoveTag((SubmarineTag)tickBox.UserData); } return true; } @@ -1313,7 +1425,7 @@ namespace Barotrauma var contentPackList = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f - contentPackagesLabel.RectTransform.RelativeSize.Y), horizontalArea.RectTransform, Anchor.BottomRight)); - List contentPacks = Submarine.MainSub.RequiredContentPackages.ToList(); + List contentPacks = Submarine.MainSub.Info.RequiredContentPackages.ToList(); foreach (ContentPackage contentPack in ContentPackage.List) { //don't show content packages that only define submarine files @@ -1326,18 +1438,18 @@ namespace Barotrauma { var cpTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), contentPackList.Content.RectTransform), contentPackageName, font: GUI.SmallFont) { - Selected = Submarine.MainSub.RequiredContentPackages.Contains(contentPackageName), + Selected = Submarine.MainSub.Info.RequiredContentPackages.Contains(contentPackageName), UserData = contentPackageName }; cpTickBox.OnSelected += (GUITickBox tickBox) => { if (tickBox.Selected) { - Submarine.MainSub.RequiredContentPackages.Add((string)tickBox.UserData); + Submarine.MainSub.Info.RequiredContentPackages.Add((string)tickBox.UserData); } else { - Submarine.MainSub.RequiredContentPackages.Remove((string)tickBox.UserData); + Submarine.MainSub.Info.RequiredContentPackages.Remove((string)tickBox.UserData); } return true; }; @@ -1363,7 +1475,7 @@ namespace Barotrauma }; paddedSaveFrame.Recalculate(); leftColumn.Recalculate(); - descriptionBox.Text = Submarine.MainSub == null ? "" : Submarine.MainSub.Description; + descriptionBox.Text = Submarine.MainSub == null ? "" : Submarine.MainSub.Info.Description; submarineDescriptionCharacterCount.Text = descriptionBox.Text.Length + " / " + submarineDescriptionLimit; } @@ -1513,7 +1625,7 @@ namespace Barotrauma #if DEBUG deleteBtn.Enabled = true; #else - deleteBtn.Enabled = userData is Submarine sub && !sub.IsVanillaSubmarine(); + deleteBtn.Enabled = userData is Submarine sub && !sub.Info.IsVanillaSubmarine(); #endif } return true; @@ -1524,7 +1636,7 @@ namespace Barotrauma searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; }; - foreach (Submarine sub in Submarine.SavedSubmarines) + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, ToolBox.LimitString(sub.Name, GUI.Font, subList.Rect.Width - 80)) @@ -1554,7 +1666,7 @@ namespace Barotrauma { if (subList.SelectedComponent != null) { - TryDeleteSub(subList.SelectedComponent.UserData as Submarine); + TryDeleteSub(subList.SelectedComponent.UserData as SubmarineInfo); } deleteButton.Enabled = false; return true; @@ -1586,7 +1698,7 @@ namespace Barotrauma foreach (GUIComponent child in subList.Content.Children) { if (!(child.UserData is Submarine sub)) { return; } - child.Visible = string.IsNullOrEmpty(filter) ? true : sub.Name.ToLower().Contains(filter.ToLower()); + child.Visible = string.IsNullOrEmpty(filter) ? true : sub.Info.Name.ToLower().Contains(filter.ToLower()); } } @@ -1606,14 +1718,14 @@ namespace Barotrauma } if (subList.SelectedComponent == null) { return false; } - if (!(subList.SelectedComponent.UserData is Submarine selectedSub)) { return false; } + if (!(subList.SelectedComponent.UserData is SubmarineInfo selectedSubInfo)) { return false; } - selectedSub.Load(true); + Submarine.Unload(); + var selectedSub = new Submarine(selectedSubInfo); Submarine.MainSub = selectedSub; - Submarine.MainSub.SetPrevTransform(Submarine.MainSub.Position); - Submarine.MainSub.UpdateTransform(); + Submarine.MainSub.UpdateTransform(interpolate: false); - string name = Submarine.MainSub.Name; + string name = Submarine.MainSub.Info.Name; subNameLabel.Text = ToolBox.LimitString(name, subNameLabel.Font, subNameLabel.Rect.Width); cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; @@ -1624,10 +1736,13 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { var lightComponent = item.GetComponent(); - if (lightComponent != null) lightComponent.Light.Enabled = item.ParentInventory == null; + if (lightComponent != null) + { + lightComponent.Light.Enabled = item.ParentInventory == null; + } } - if (selectedSub.GameVersion < new Version("0.8.9.0")) + if (selectedSub.Info.GameVersion < new Version("0.8.9.0")) { var adjustLightsPrompt = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("AdjustLightsPrompt"), new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); @@ -1649,7 +1764,7 @@ namespace Barotrauma return true; } - private void TryDeleteSub(Submarine sub) + private void TryDeleteSub(SubmarineInfo sub) { if (sub == null) { return; } @@ -1674,9 +1789,9 @@ namespace Barotrauma { try { - sub.Remove(); + sub.Dispose(); File.Delete(sub.FilePath); - Submarine.RefreshSavedSubs(); + SubmarineInfo.RefreshSavedSubs(); CreateLoadScreen(); } catch (Exception e) @@ -1754,18 +1869,20 @@ namespace Barotrauma return true; } - public void SetMode(Mode mode) + public void SetMode(Mode newMode) { - if (mode == this.mode) { return; } - this.mode = mode; + if (newMode == mode) { return; } + mode = newMode; - defaultModeTickBox.Selected = mode == Mode.Default; + lockMode = true; + defaultModeTickBox.Selected = newMode == Mode.Default; defaultModeTickBox.CanBeFocused = !defaultModeTickBox.Selected; - characterModeTickBox.Selected = mode == Mode.Character; - wiringModeTickBox.Selected = mode == Mode.Wiring; - - switch (mode) + characterModeTickBox.Selected = newMode == Mode.Character; + wiringModeTickBox.Selected = newMode == Mode.Wiring; + lockMode = false; + + switch (newMode) { case Mode.Character: CreateDummyCharacter(); @@ -1790,6 +1907,7 @@ namespace Barotrauma } MapEntity.DeselectAll(); + MapEntity.FilteredSelectedList.Clear(); } private void RemoveDummyCharacter() @@ -1954,7 +2072,7 @@ namespace Barotrauma return false; } - if (Submarine.MainSub != null) Submarine.MainSub.Name = text; + if (Submarine.MainSub != null) Submarine.MainSub.Info.Name = text; textBox.Deselect(); textBox.Text = text; @@ -1968,7 +2086,7 @@ namespace Barotrauma { if (Submarine.MainSub != null) { - Submarine.MainSub.Description = text; + Submarine.MainSub.Info.Description = text; } else { @@ -1994,6 +2112,7 @@ namespace Barotrauma { previouslyUsedPanel.Visible = false; showEntitiesPanel.Visible = true; + showEntitiesPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(entityCountPanel.Rect.Right, saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); matchingTickBox.Selected = true; matchingTickBox.Flash(GUI.Style.Green); } @@ -2004,12 +2123,10 @@ namespace Barotrauma return false; } - private bool GenerateWaypoints(GUIButton button, object obj) + private bool GenerateWaypoints() { - if (Submarine.MainSub == null) return false; - - WayPoint.GenerateSubWaypoints(Submarine.MainSub); - return true; + if (Submarine.MainSub == null) { return false; } + return WayPoint.GenerateSubWaypoints(Submarine.MainSub); } private void AddPreviouslyUsed(MapEntityPrefab mapEntityPrefab) @@ -2406,12 +2523,53 @@ namespace Barotrauma hullVolumeFrame.Visible = MapEntity.SelectedList.Any(s => s is Hull); saveAssemblyFrame.Visible = MapEntity.SelectedList.Count > 0; - if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Tab)) + if (GUI.KeyboardDispatcher.Subscriber == null) { - entityFilterBox.Select(); + // TODO adjust when the new inventory stuff rolls in + if (PlayerInput.KeyHit(Keys.Q) && mode == Mode.Default) + { + toggleEntityMenuButton.OnClicked?.Invoke(toggleEntityMenuButton, toggleEntityMenuButton.UserData); + } + + if (PlayerInput.KeyHit(Keys.Tab)) + { + entityFilterBox.Select(); + } + + if (PlayerInput.KeyDown(Keys.LeftControl)) + { + // Save menu + if (PlayerInput.KeyHit(Keys.S)) + { + if (PlayerInput.KeyDown(Keys.LeftShift)) + { + // Save the sub without a menu + if (subNameLabel != null) + { + SaveSubToFile(subNameLabel.Text); + } + } + else + { + // Save menu + if (saveFrame == null) + { + CreateSaveScreen(); + } + } + } + + // 1-3 keys on the keyboard for switching modes + if (PlayerInput.KeyHit(Keys.D1)) { SetMode(Mode.Default); } + if (PlayerInput.KeyHit(Keys.D2)) { SetMode(Mode.Character); } + if (PlayerInput.KeyHit(Keys.D3)) { SetMode(Mode.Wiring); } + } + else + { + cam.MoveCamera((float) deltaTime, true); + } } - - cam.MoveCamera((float)deltaTime, true); + if (PlayerInput.MidButtonHeld()) { Vector2 moveSpeed = PlayerInput.MouseSpeed * (float)deltaTime * 100.0f / cam.Zoom; @@ -2647,11 +2805,30 @@ namespace Barotrauma if (Submarine.MainSub != null) { + Vector2 position = Submarine.MainSub.SubBody != null ? Submarine.MainSub.WorldPosition : Submarine.MainSub.HiddenSubPosition; + GUI.DrawIndicator( - spriteBatch, Submarine.MainSub.WorldPosition, cam, + spriteBatch, position, cam, cam.WorldView.Width, GUI.SubmarineIcon, Color.LightBlue * 0.5f); } + + var notificationIcon = GUI.Style.GetComponentStyle("GUINotificationButton"); + var tooltipStyle = GUI.Style.GetComponentStyle("GUIToolTip"); + foreach (Gap gap in Gap.GapList) + { + if (gap.linkedTo.Count == 2 && gap.linkedTo[0] == gap.linkedTo[1]) + { + Vector2 screenPos = Cam.WorldToScreen(gap.WorldPosition); + Rectangle rect = new Rectangle(screenPos.ToPoint() - new Point(20), new Point(40)); + tooltipStyle.Sprites[GUIComponent.ComponentState.None][0].Draw(spriteBatch, rect, Color.White); + notificationIcon.Sprites[GUIComponent.ComponentState.None][0].Draw(spriteBatch, rect, GUI.Style.Orange); + if (Vector2.Distance(PlayerInput.MousePosition, screenPos) < 30 * Cam.Zoom) + { + GUIComponent.DrawToolTip(spriteBatch, TextManager.Get("gapinsidehullwarning"), new Rectangle(screenPos.ToPoint(), new Point(10))); + } + } + } if ((CharacterMode || WiringMode) && dummyCharacter != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 1976a0158..5cf2e0892 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -569,15 +569,13 @@ namespace Barotrauma Font = GUI.SmallFont, Text = value, OverflowClip = true, - OnEnterPressed = (textBox, text) => + }; + propertyBox.OnDeselected += (textBox, keys) => + { + if (property.TrySetValue(entity, textBox.Text)) { - if (property.TrySetValue(entity, text)) - { - TrySendNetworkUpdate(entity, property); - textBox.Text = (string)property.GetValue(entity); - textBox.Deselect(); - } - return true; + TrySendNetworkUpdate(entity, property); + textBox.Text = (string)property.GetValue(entity); } }; if (translationTextTag != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index cb28e241c..f1b73d220 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -97,6 +97,32 @@ namespace Barotrauma.Sounds if (position != null) { + if (float.IsNaN(position.Value.X)) + { + throw new Exception("Failed to set source's position: " + debugName + ", position.X is NaN"); + } + if (float.IsNaN(position.Value.Y)) + { + throw new Exception("Failed to set source's position: " + debugName + ", position.Y is NaN"); + } + if (float.IsNaN(position.Value.Z)) + { + throw new Exception("Failed to set source's position: " + debugName + ", position.Z is NaN"); + } + + if (float.IsInfinity(position.Value.X)) + { + throw new Exception("Failed to set source's position: " + debugName + ", position.X is Infinity"); + } + if (float.IsInfinity(position.Value.Y)) + { + throw new Exception("Failed to set source's position: " + debugName + ", position.Y is Infinity"); + } + if (float.IsInfinity(position.Value.Z)) + { + throw new Exception("Failed to set source's position: " + debugName + ", position.Z is Infinity"); + } + uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); Al.Sourcei(alSource, Al.SourceRelative, Al.False); int alError = Al.GetError(); @@ -378,12 +404,12 @@ namespace Barotrauma.Sounds uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); if (!Al.IsSource(alSource)) return false; Al.GetSourcei(alSource, Al.SourceState, out state); - bool playing = state == Al.Playing; int alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to determine playing state from source: " + debugName + ", " + Al.GetErrorString(alError)); } + bool playing = state == Al.Playing; return playing; } } @@ -615,7 +641,7 @@ namespace Barotrauma.Sounds uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); int state; - Al.GetSourcei(Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.SourceState, out state); + Al.GetSourcei(alSource, Al.SourceState, out state); bool playing = state == Al.Playing; int alError = Al.GetError(); if (alError != Al.NoError) @@ -630,7 +656,7 @@ namespace Barotrauma.Sounds { throw new Exception("Failed to determine processed buffers from streamed source: " + debugName + ", " + Al.GetErrorString(alError)); } - + Al.SourceUnqueueBuffers(alSource, unqueuedBufferCount, unqueuedBuffers); alError = Al.GetError(); if (alError != Al.NoError) @@ -727,9 +753,20 @@ namespace Barotrauma.Sounds streamAmplitude = streamBufferAmplitudes[queueStartIndex]; Al.GetSourcei(alSource, Al.SourceState, out state); + alError = Al.GetError(); + if (alError != Al.NoError) + { + throw new Exception("Failed to retrieve stream source state: " + debugName + ", " + Al.GetErrorString(alError)); + } + if (state != Al.Playing) { Al.SourcePlay(alSource); + alError = Al.GetError(); + if (alError != Al.NoError) + { + throw new Exception("Failed to start stream playback: " + debugName + ", " + Al.GetErrorString(alError)); + } } } @@ -738,6 +775,10 @@ namespace Barotrauma.Sounds streamAmplitude = 0.0f; } } + catch (Exception e) + { + DebugConsole.ThrowError($"An exception was thrown when updating a sound stream ({debugName})", e); + } finally { Monitor.Exit(mutex); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 51ccff9aa..9a8404fbf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -123,7 +123,7 @@ namespace Barotrauma string filePathB = b.GetAttributeString("file", "").CleanUpPath(); float baseGainB = b.GetAttributeFloat("volume", 1.0f); float rangeB = b.GetAttributeFloat("range", 1000.0f); - return a.Name.ToString().ToLowerInvariant() == b.Name.ToString().ToLowerInvariant() && + return a.Name.ToString().Equals(b.Name.ToString(), StringComparison.OrdinalIgnoreCase) && filePathA == filePathB && MathUtils.NearlyEqual(baseGainA, baseGainB) && MathUtils.NearlyEqual(rangeA, rangeB); } @@ -151,7 +151,7 @@ namespace Barotrauma SoundCount = 1 + soundElements.Count(); - var startUpSoundElement = soundElements.Find(e => e.Name.ToString().ToLowerInvariant() == "startupsound"); + var startUpSoundElement = soundElements.Find(e => e.Name.ToString().Equals("startupsound", StringComparison.OrdinalIgnoreCase)); if (startUpSoundElement != null) { startUpSound = GameMain.SoundManager.LoadSound(startUpSoundElement, false); @@ -182,7 +182,7 @@ namespace Barotrauma musicClips.AddIfNotNull(newMusicClip); if (loadedSoundElements != null) { - if (newMusicClip.Type.ToLowerInvariant() == "menu") + if (newMusicClip.Type.Equals("menu", StringComparison.OrdinalIgnoreCase)) { targetMusic[0] = newMusicClip; } @@ -742,22 +742,30 @@ namespace Barotrauma Screen.Selected == GameMain.LevelEditorScreen || Screen.Selected == GameMain.ParticleEditorScreen || Screen.Selected == GameMain.SpriteEditorScreen || - Screen.Selected == GameMain.SubEditorScreen) + Screen.Selected == GameMain.SubEditorScreen || + (Screen.Selected == GameMain.GameScreen && GameMain.GameSession?.GameMode is SubTestMode)) { return "editor"; } if (Screen.Selected != GameMain.GameScreen) { return "menu"; } - if (Character.Controlled != null && - Level.Loaded != null && Level.Loaded.Ruins != null && - Level.Loaded.Ruins.Any(r => r.Area.Contains(Character.Controlled.WorldPosition))) + + if (Character.Controlled != null) { - return "ruins"; + if (Level.Loaded != null && Level.Loaded.Ruins != null && + Level.Loaded.Ruins.Any(r => r.Area.Contains(Character.Controlled.WorldPosition))) + { + return "ruins"; + } + + if (Character.Controlled.Submarine?.Info?.IsWreck ?? false) + { + return "wreck"; + } } Submarine targetSubmarine = Character.Controlled?.Submarine; - if ((targetSubmarine != null && targetSubmarine.AtDamageDepth) || (GameMain.GameScreen != null && Screen.Selected == GameMain.GameScreen && GameMain.GameScreen.Cam.Position.Y < SubmarineBody.DamageDepth)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs index bd137b800..974a67cdd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs @@ -59,8 +59,10 @@ namespace Barotrauma.Sounds get { return soundChannel?.CurrentAmplitude ?? 0.0f; } } - public VoipSound(SoundManager owner, VoipQueue q) : base(owner, "voip", true, true) + public VoipSound(string name, SoundManager owner, VoipQueue q) : base(owner, "voip", true, true) { + Filename = $"VoIP ({name})"; + VoipConfig.SetupEncoding(); ALFormat = Al.FormatMono16; @@ -93,9 +95,28 @@ namespace Barotrauma.Sounds public void ApplyFilters(short[] buffer, int readSamples) { + for (int i = 0; i < readSamples; i++) + { + float fVal = ShortToFloat(buffer[i]); + if (UseMuffleFilter) + { + foreach (var filter in muffleFilters) + { + fVal = filter.Process(fVal); + } + } + if (UseRadioFilter) + { + foreach (var filter in radioFilters) + { + fVal = filter.Process(fVal); + } + } + buffer[i] = FloatToShort(fVal); + } if (UseMuffleFilter) { - ApplyFilters(radioFilters, buffer, readSamples); + ApplyFilters(muffleFilters, buffer, readSamples); } if (UseRadioFilter) @@ -106,15 +127,6 @@ namespace Barotrauma.Sounds private void ApplyFilters(IEnumerable filters, short[] buffer, int readSamples) { - for (int i = 0; i < readSamples; i++) - { - float fVal = ShortToFloat(buffer[i]); - foreach (var filter in filters) - { - fVal = filter.Process(fVal); - } - buffer[i] = FloatToShort(fVal); - } } public override SoundChannel Play(float gain, float range, Vector2 position, bool muffle = false) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs index 10b117dd4..8c9c45a86 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs @@ -8,6 +8,13 @@ namespace Barotrauma { class DecorativeSprite : ISerializableEntity { + public class State + { + public float RotationState; + public float OffsetState; + public bool IsActive = true; + } + public string Name => $"Decorative Sprite"; public Dictionary SerializableProperties { get; set; } @@ -113,10 +120,10 @@ namespace Barotrauma switch (OffsetAnim) { case AnimationType.Sine: - offsetState = offsetState % (MathHelper.TwoPi / OffsetAnimSpeed); + offsetState %= (MathHelper.TwoPi / OffsetAnimSpeed); return Offset * (float)Math.Sin(offsetState * OffsetAnimSpeed); case AnimationType.Noise: - offsetState = offsetState % (1.0f / (OffsetAnimSpeed * 0.1f)); + offsetState %= (1.0f / (OffsetAnimSpeed * 0.1f)); float t = offsetState * 0.1f * OffsetAnimSpeed; return new Vector2( @@ -146,6 +153,51 @@ namespace Barotrauma } } + public static void UpdateSpriteStates(Dictionary> spriteGroups, Dictionary animStates, + int entityID, float deltaTime, Func checkConditional) + { + foreach (int spriteGroup in spriteGroups.Keys) + { + for (int i = 0; i < spriteGroups.Count; i++) + { + var decorativeSprite = spriteGroups[spriteGroup][i]; + if (decorativeSprite == null) { continue; } + if (spriteGroup > 0) + { + int activeSpriteIndex = entityID % spriteGroups[spriteGroup].Count; + if (i != activeSpriteIndex) + { + animStates[decorativeSprite].IsActive = false; + continue; + } + } + + //check if the sprite is active (whether it should be drawn or not) + var spriteState = animStates[decorativeSprite]; + spriteState.IsActive = true; + foreach (PropertyConditional conditional in decorativeSprite.IsActiveConditionals) + { + if (!checkConditional(conditional)) + { + spriteState.IsActive = false; + break; + } + } + if (!spriteState.IsActive) { continue; } + + //check if the sprite should be animated + bool animate = true; + foreach (PropertyConditional conditional in decorativeSprite.AnimationConditionals) + { + if (!checkConditional(conditional)) { animate = false; break; } + } + if (!animate) { continue; } + spriteState.OffsetState += deltaTime; + spriteState.RotationState += deltaTime; + } + } + } + public void Remove() { Sprite?.Remove(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs index 4df76b354..d88669f5a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs @@ -49,6 +49,9 @@ namespace Barotrauma.SpriteDeformations [Serialize(false, true), Editable] public bool StopWhenHostIsDead { get; set; } + [Serialize(false, true), Editable] + public bool OnlyInWater { get; set; } + /// /// Only used if UseMovementSine is enabled. Multiplier for Pi. /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index c1fab4f3a..9ccf7dc16 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -52,7 +52,7 @@ namespace Barotrauma } } - partial void ApplyProjSpecific(float deltaTime, Entity entity, List targets, Hull hull, Vector2 worldPosition) + partial void ApplyProjSpecific(float deltaTime, Entity entity, IEnumerable targets, Hull hull, Vector2 worldPosition) { if (entity == null) { return; } @@ -112,17 +112,19 @@ namespace Barotrauma foreach (ParticleEmitter emitter in particleEmitters) { float angle = 0.0f; + float particleRotation = 0.0f; if (emitter.Prefab.CopyEntityAngle) { if (entity is Item item && item.body != null) { angle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); + particleRotation = -item.body.Rotation; + if (item.body.Dir < 0.0f) { particleRotation += MathHelper.Pi; } } } - emitter.Emit(deltaTime, worldPosition, hull, angle); - } - + emitter.Emit(deltaTime, worldPosition, hull, angle: angle, particleRotation: particleRotation); + } } static partial void UpdateAllProjSpecific(float deltaTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs index 9a900cf4c..42d8d7f84 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs @@ -35,7 +35,7 @@ namespace Barotrauma Identifier = element.GetAttributeString("identifier", ""); foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "icon") + if (subElement.Name.ToString().Equals("icon", StringComparison.OrdinalIgnoreCase)) { Icon = new Sprite(subElement); IconColor = subElement.GetAttributeColor("color", Color.White); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs index ad2518771..c04d65ed9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -240,13 +240,13 @@ namespace Barotrauma name = null; endpoint = null; lobbyId = 0; if (args == null || args.Length < 2) { return; } - if (args[0].Equals("-connect", StringComparison.InvariantCultureIgnoreCase)) + if (args[0].Equals("-connect", StringComparison.OrdinalIgnoreCase)) { if (args.Length < 3) { return; } name = args[1]; endpoint = args[2]; } - else if (args[0].Equals("+connect_lobby", StringComparison.InvariantCultureIgnoreCase)) + else if (args[0].Equals("+connect_lobby", StringComparison.OrdinalIgnoreCase)) { UInt64.TryParse(args[1], out lobbyId); } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index b5fd4f6d5..83a5aee27 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -2,11 +2,11 @@ WinExe - netcoreapp3.0 + netcoreapp3.1 Barotrauma FakeFish, Undertow Games Barotrauma - 0.9.7.1 + 0.9.9.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index de068fa67..7e1e8ffa1 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -2,11 +2,11 @@ WinExe - netcoreapp3.0 + netcoreapp3.1 Barotrauma FakeFish, Undertow Games Barotrauma - 0.9.7.1 + 0.9.9.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 6db530ef8..4642e0645 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -2,16 +2,17 @@ WinExe - netcoreapp3.0 + netcoreapp3.1 Barotrauma FakeFish, Undertow Games Barotrauma - 0.9.7.1 + 0.9.9.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + app.manifest diff --git a/Barotrauma/BarotraumaClient/libwebm_mem_playback_x64.dylib b/Barotrauma/BarotraumaClient/libwebm_mem_playback_x64.dylib index 12bfaed0b..413455a2b 100644 Binary files a/Barotrauma/BarotraumaClient/libwebm_mem_playback_x64.dylib and b/Barotrauma/BarotraumaClient/libwebm_mem_playback_x64.dylib differ diff --git a/Barotrauma/BarotraumaServer/DedicatedServer.exe b/Barotrauma/BarotraumaServer/DedicatedServer.exe new file mode 100644 index 000000000..8808709d4 --- /dev/null +++ b/Barotrauma/BarotraumaServer/DedicatedServer.exe @@ -0,0 +1 @@ +./DedicatedServer diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 55665f547..e6e084590 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -2,11 +2,11 @@ Exe - netcoreapp3.0 + netcoreapp3.1 Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.9.7.1 + 0.9.9.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer @@ -54,6 +54,7 @@ + diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 94e78064c..eda82c727 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -2,11 +2,11 @@ Exe - netcoreapp3.0 + netcoreapp3.1 Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.9.7.1 + 0.9.9.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer @@ -61,6 +61,7 @@ + diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index fc7254410..618535b40 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -14,15 +14,18 @@ namespace Barotrauma GameMain.Server.KarmaManager.OnCharacterHealthChanged(this, attacker, attackResult.Damage, attackResult.Afflictions); } - partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction) + partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log) { - if (causeOfDeath == CauseOfDeathType.Affliction) + if (log) { - GameServer.Log(LogName + " has died (Cause of death: " + causeOfDeathAffliction.Prefab.Name + ")", ServerLog.MessageType.Attack); - } - else - { - GameServer.Log(LogName + " has died (Cause of death: " + causeOfDeath + ")", ServerLog.MessageType.Attack); + if (causeOfDeath == CauseOfDeathType.Affliction) + { + GameServer.Log(LogName + " has died (Cause of death: " + causeOfDeathAffliction.Prefab.Name + ")", ServerLog.MessageType.Attack); + } + else + { + GameServer.Log(LogName + " has died (Cause of death: " + causeOfDeath + ")", ServerLog.MessageType.Attack); + } } healthUpdateTimer = 0.0f; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index e537f50db..7e0bdcd8b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -52,7 +52,7 @@ namespace Barotrauma if (memInput.Count > 0) { prevDequeuedInput = dequeuedInput; - dequeuedInput = memInput[memInput.Count - 1].states; + dequeuedInput = memInput[memInput.Count - 1].states & InputNetFlags.Ragdoll; memInput.RemoveAt(memInput.Count - 1); } } @@ -243,7 +243,7 @@ namespace Barotrauma return; } - if (IsUnconscious) + if (IsIncapacitated) { var causeOfDeath = CharacterHealth.GetCauseOfDeath(); Kill(causeOfDeath.First, causeOfDeath.Second); @@ -333,6 +333,14 @@ namespace Barotrauma attack = dequeuedInput.HasFlag(InputNetFlags.Attack); shoot = dequeuedInput.HasFlag(InputNetFlags.Shoot); } + else if (keys != null) + { + aiming = keys[(int)InputType.Aim].GetHeldQueue; + use = keys[(int)InputType.Use].GetHeldQueue; + attack = keys[(int)InputType.Attack].GetHeldQueue; + shoot = keys[(int)InputType.Shoot].GetHeldQueue; + networkUpdateSent = true; + } tempBuffer.Write(aiming); tempBuffer.Write(shoot); @@ -346,7 +354,7 @@ namespace Barotrauma Vector2 relativeCursorPos = cursorPosition - AimRefPosition; tempBuffer.Write((UInt16)(65535.0 * Math.Atan2(relativeCursorPos.Y, relativeCursorPos.X) / (2.0 * Math.PI))); - tempBuffer.Write(IsRagdolled || IsUnconscious || Stun > 0.0f || IsDead); + tempBuffer.Write(IsRagdolled || Stun > 0.0f || IsDead || IsIncapacitated); tempBuffer.Write(AnimController.Dir > 0.0f); } @@ -489,6 +497,31 @@ namespace Barotrauma msg.Write(this is AICharacter); msg.Write(info.SpeciesName); info.ServerWrite(msg); + + // Current order + if (info.CurrentOrder != null) + { + 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(true); + msg.Write(info.CurrentOrder.OrderGiver.ID); + } + else + { + msg.Write(false); + } + msg.Write((byte)(string.IsNullOrWhiteSpace(info.CurrentOrderOption) ? 0 : + Array.IndexOf(info.CurrentOrder.Prefab.Options, info.CurrentOrderOption))); + } + else + { + msg.Write(false); + } + TryWriteStatus(msg); void TryWriteStatus(IWriteMessage msg) diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index b4ed048c7..e08792a13 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -558,7 +558,7 @@ namespace Barotrauma ShowQuestionPrompt("Rank to grant to \"" + client.Name + "\"?", (rank) => { - PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.ToLowerInvariant() == rank.ToLowerInvariant()); + PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.Equals(rank, StringComparison.OrdinalIgnoreCase)); if (preset == null) { ThrowError("Rank \"" + rank + "\" not found."); @@ -1165,7 +1165,7 @@ namespace Barotrauma else { string modeName = string.Join(" ", args); - if (modeName.ToLowerInvariant() == "campaign") + if (modeName.Equals("campaign", StringComparison.OrdinalIgnoreCase)) { MultiPlayerCampaign.StartCampaignSetup(); } @@ -1210,7 +1210,7 @@ namespace Barotrauma commands.Add(new Command("sub|submarine", "submarine [name]: Select the submarine for the next round.", (string[] args) => { - Submarine sub = GameMain.NetLobbyScreen.GetSubList().Find(s => s.Name.ToLower() == string.Join(" ", args).ToLower()); + SubmarineInfo sub = GameMain.NetLobbyScreen.GetSubList().Find(s => s.Name.ToLower() == string.Join(" ", args).ToLower()); if (sub != null) { @@ -1223,13 +1223,13 @@ namespace Barotrauma { return new string[][] { - Submarine.SavedSubmarines.Select(s => s.Name).ToArray() + SubmarineInfo.SavedSubmarines.Select(s => s.Name).ToArray() }; })); commands.Add(new Command("shuttle", "shuttle [name]: Select the specified submarine as the respawn shuttle for the next round.", (string[] args) => { - Submarine shuttle = GameMain.NetLobbyScreen.GetSubList().Find(s => s.Name.ToLower() == string.Join(" ", args).ToLower()); + SubmarineInfo shuttle = GameMain.NetLobbyScreen.GetSubList().Find(s => s.Name.ToLower() == string.Join(" ", args).ToLower()); if (shuttle != null) { @@ -1242,7 +1242,7 @@ namespace Barotrauma { return new string[][] { - Submarine.SavedSubmarines.Select(s => s.Name).ToArray() + SubmarineInfo.SavedSubmarines.Select(s => s.Name).ToArray() }; })); @@ -1475,6 +1475,27 @@ namespace Barotrauma } ); + AssignOnClientRequestExecute( + "teleportsub", + (Client client, Vector2 cursorWorldPos, string[] args) => + { + if (Submarine.MainSub == null || Level.Loaded == null) return; + + if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase)) + { + Submarine.MainSub.SetPosition(cursorWorldPos); + } + else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase)) + { + Submarine.MainSub.SetPosition(Level.Loaded.StartPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); + } + else + { + Submarine.MainSub.SetPosition(Level.Loaded.EndPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); + } + } + ); + AssignOnClientRequestExecute( "godmode", (Client client, Vector2 cursorWorldPos, string[] args) => @@ -1493,7 +1514,7 @@ namespace Barotrauma { if (args.Length < 2) return; - AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Name.ToLowerInvariant() == args[0].ToLowerInvariant()); + AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab == null) { GameMain.Server.SendConsoleMessage("Affliction \"" + args[0] + "\" not found.", client); @@ -1718,7 +1739,7 @@ namespace Barotrauma } string rank = string.Join("", args.Skip(1)); - PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.ToLowerInvariant() == rank.ToLowerInvariant()); + PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.Equals(rank, StringComparison.OrdinalIgnoreCase)); if (preset == null) { GameMain.Server.SendConsoleMessage("Rank \"" + rank + "\" not found.", senderClient); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs index 92697eac9..170b5b538 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs @@ -21,7 +21,7 @@ namespace Barotrauma } } - public override bool AssignTeamIDs(List clients) + public override void AssignTeamIDs(List clients) { List randList = new List(clients); for (int i = 0; i < randList.Count; i++) @@ -44,7 +44,6 @@ namespace Barotrauma randList[i].TeamID = Character.TeamType.Team2; } } - return true; } public override void Update(float deltaTime) @@ -76,8 +75,8 @@ namespace Barotrauma } else { - teamDead[0] = crews[0].All(c => c.IsDead || c.IsUnconscious); - teamDead[1] = crews[1].All(c => c.IsDead || c.IsUnconscious); + teamDead[0] = crews[0].All(c => c.IsDead || c.IsIncapacitated); + teamDead[1] = crews[1].All(c => c.IsDead || c.IsIncapacitated); } if (state == 0) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs index 09a323ee8..3ebe4b95b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs @@ -6,7 +6,15 @@ namespace Barotrauma { public override void ServerWriteInitial(IWriteMessage msg, Client c) { - item.WriteSpawnData(msg, item.ID); + msg.Write(usedExistingItem); + if (usedExistingItem) + { + msg.Write(item.ID); + } + else + { + item.WriteSpawnData(msg, item.ID); + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index fe6bc2504..3ce575eb9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -61,7 +61,7 @@ namespace Barotrauma if (vanillaContent == null) { // TODO: Dynamic method for defining and finding the vanilla content package. - vanillaContent = ContentPackage.List.SingleOrDefault(cp => Path.GetFileName(cp.Path).ToLowerInvariant() == "vanilla 0.9.xml"); + vanillaContent = ContentPackage.List.SingleOrDefault(cp => Path.GetFileName(cp.Path).Equals("vanilla 0.9.xml", StringComparison.OrdinalIgnoreCase)); } return vanillaContent; } @@ -111,6 +111,7 @@ namespace Barotrauma StructurePrefab.LoadAll(GetFilesOfType(ContentType.Structure)); ItemPrefab.LoadAll(GetFilesOfType(ContentType.Item)); JobPrefab.LoadAll(GetFilesOfType(ContentType.Jobs)); + CorpsePrefab.LoadAll(GetFilesOfType(ContentType.Corpses)); NPCConversation.LoadAll(GetFilesOfType(ContentType.NPCConversations)); ItemAssemblyPrefab.LoadAll(); LevelObjectPrefab.LoadAll(); @@ -118,7 +119,7 @@ namespace Barotrauma GameModePreset.Init(); LocationType.Init(); - Submarine.RefreshSavedSubs(); + SubmarineInfo.RefreshSavedSubs(); Screen.SelectNull(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index caff80bd3..2fc35dd0d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -15,7 +15,7 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(savePath)) return; - GameMain.GameSession = new GameSession(new Submarine(subPath, ""), savePath, + GameMain.GameSession = new GameSession(new SubmarineInfo(subPath, ""), savePath, GameModePreset.List.Find(g => g.Identifier == "multiplayercampaign")); var campaign = ((MultiPlayerCampaign)GameMain.GameSession.GameMode); campaign.GenerateMap(seed); @@ -46,7 +46,7 @@ namespace Barotrauma DebugConsole.NewMessage("********* CAMPAIGN SETUP *********", Color.White); DebugConsole.ShowQuestionPrompt("Do you want to start a new campaign? Y/N", (string arg) => { - if (arg.ToLowerInvariant() == "y" || arg.ToLowerInvariant() == "yes") + if (arg.Equals("y", StringComparison.OrdinalIgnoreCase) || arg.Equals("yes", StringComparison.OrdinalIgnoreCase)) { DebugConsole.ShowQuestionPrompt("Enter a save name for the campaign:", (string saveName) => { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs new file mode 100644 index 000000000..d7331f33b --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs @@ -0,0 +1,45 @@ +using Barotrauma.Networking; +using System.Collections.Generic; + +namespace Barotrauma.Items.Components +{ + partial class LightComponent : Powered, IServerSerializable + { + private CoroutineHandle sendStateCoroutine; + private bool lastSentState; + private float sendStateTimer; + + partial void OnStateChanged() + { + sendStateTimer = 0.5f; + if (sendStateCoroutine == null) + { + sendStateCoroutine = CoroutineManager.StartCoroutine(SendStateAfterDelay()); + } + } + + private IEnumerable SendStateAfterDelay() + { + while (sendStateTimer > 0.0f) + { + sendStateTimer -= CoroutineManager.DeltaTime; + yield return CoroutineStatus.Running; + } + + if (item.Removed || GameMain.NetworkMember == null) + { + yield return CoroutineStatus.Success; + } + + sendStateCoroutine = null; + if (lastSentState != IsActive) { item.CreateServerEvent(this); } + yield return CoroutineStatus.Success; + } + + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + msg.Write(IsActive); + lastSentState = IsActive; + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs index 5edc2c619..5c915dec8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs @@ -33,6 +33,8 @@ namespace Barotrauma.Items.Components public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { + msg.Write((byte)State); + msg.Write(timeUntilReady); int itemIndex = fabricatedItem == null ? -1 : fabricationRecipes.IndexOf(fabricatedItem); msg.WriteRangedInteger(itemIndex, -1, fabricationRecipes.Count - 1); UInt16 userID = fabricatedItem == null || user == null ? (UInt16)0 : user.ID; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs new file mode 100644 index 000000000..6c06d44d9 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs @@ -0,0 +1,38 @@ +using Barotrauma.Networking; +using System; + +namespace Barotrauma.Items.Components +{ + partial class Projectile : ItemComponent + { + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + msg.Write(StickTarget != null); + if (StickTarget != null) + { + msg.Write(item.body.SimPosition.X); + msg.Write(item.body.SimPosition.Y); + msg.Write(stickJoint.Axis.X); + msg.Write(stickJoint.Axis.Y); + if (StickTarget.UserData is Structure structure) + { + msg.Write(structure.ID); + msg.Write((byte)structure.Bodies.IndexOf(StickTarget)); + } + else if (StickTarget.UserData is Entity entity) + { + msg.Write(entity.ID); + } + else if (StickTarget.UserData is Limb limb) + { + msg.Write(limb.character.ID); + msg.Write((byte)Array.IndexOf(limb.character.AnimController.Limbs, limb)); + } + else + { + throw new NotImplementedException(StickTarget.UserData?.ToString() ?? "null" + " is not a valid projectile stick target."); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs index 43b056872..3cf56ec69 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs @@ -12,7 +12,7 @@ namespace Barotrauma.Items.Components public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { - if (c.Character == null) return; + if (c.Character == null) { return; } var requestedFixAction = (FixActions)msg.ReadRangedInteger(0, 2); if (requestedFixAction != FixActions.None) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs new file mode 100644 index 000000000..16e3abf9a --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs @@ -0,0 +1,12 @@ +using Barotrauma.Networking; + +namespace Barotrauma.Items.Components +{ + partial class Rope : ItemComponent + { + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + msg.Write(Snapped); + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs index e11921678..d4187a717 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs @@ -66,6 +66,9 @@ namespace Barotrauma.Items.Components if (!CheckCharacterSuccess(c.Character)) { + item.CreateServerEvent(this); + c.Character.SelectedItems[0]?.GetComponent()?.CreateNetworkEvent(); + c.Character.SelectedItems[1]?.GetComponent()?.CreateNetworkEvent(); GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, c.Character.ID }); return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs index 7786d2621..58c896d2e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs @@ -10,9 +10,17 @@ namespace Barotrauma.Items.Components public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { bool[] elementStates = new bool[customInterfaceElementList.Count]; + string[] elementValues = new string[customInterfaceElementList.Count]; for (int i = 0; i < customInterfaceElementList.Count; i++) { - elementStates[i] = msg.ReadBoolean(); + if (!string.IsNullOrEmpty(customInterfaceElementList[i].PropertyName)) + { + elementValues[i] = msg.ReadString(); + } + else + { + elementStates[i] = msg.ReadBoolean(); + } } CustomInterfaceElement clickedButton = null; @@ -20,7 +28,11 @@ namespace Barotrauma.Items.Components { for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (customInterfaceElementList[i].ContinuousSignal) + if (!string.IsNullOrEmpty(customInterfaceElementList[i].PropertyName)) + { + TextChanged(customInterfaceElementList[i], elementValues[i]); + } + else if (customInterfaceElementList[i].ContinuousSignal) { TickBoxToggled(customInterfaceElementList[i], elementStates[i]); } @@ -48,7 +60,11 @@ namespace Barotrauma.Items.Components //extradata contains an array of buttons clicked by a client (or nothing if nothing was clicked) for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (customInterfaceElementList[i].ContinuousSignal) + if (!string.IsNullOrEmpty(customInterfaceElementList[i].PropertyName)) + { + msg.Write(customInterfaceElementList[i].Signal); + } + else if(customInterfaceElementList[i].ContinuousSignal) { msg.Write(customInterfaceElementList[i].State); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs index c7653ef60..7bd88be3d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs @@ -17,9 +17,10 @@ namespace Barotrauma.Items.Components GameServer.Log(c.Character.LogName + " entered \"" + newOutputValue + "\" on " + item.Name, ServerLog.MessageType.ItemInteraction); OutputValue = newOutputValue; + item.SendSignal(0, newOutputValue, "signal_out", null); + item.CreateServerEvent(this); } - item.CreateServerEvent(this); } public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs index aa741edfe..7586f86c0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components { partial class Wire : ItemComponent, IDrawableComponent, IServerSerializable { - private void CreateNetworkEvent() + public void CreateNetworkEvent() { if (GameMain.Server == null) return; //split into multiple events because one might not be enough to fit all the nodes diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index 907307bef..c1fe0a535 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -88,6 +88,10 @@ namespace Barotrauma if (!prevItems.Contains(item) && !item.CanClientAccess(c)) { + if (item.body != null && !c.PendingPositionUpdates.Contains(item)) + { + c.PendingPositionUpdates.Enqueue(item); + } item.PositionUpdateInterval = 0.0f; continue; } @@ -106,7 +110,7 @@ namespace Barotrauma CreateNetworkEvent(); foreach (Inventory prevInventory in prevItemInventories.Distinct()) { - if (prevInventory != this) prevInventory?.CreateNetworkEvent(); + if (prevInventory != this) { prevInventory?.CreateNetworkEvent(); } } foreach (Item item in Items.Distinct()) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 8f1a873a2..826ff1260 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -8,6 +8,11 @@ namespace Barotrauma { partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable { + public override Sprite Sprite + { + get { return prefab?.sprite; } + } + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { string errorMsg = ""; @@ -255,6 +260,8 @@ namespace Barotrauma msg.Write(slotIndex < 0 ? (byte)255 : (byte)slotIndex); } + msg.Write(body == null ? (byte)0 : (byte)body.BodyType); + byte teamID = 0; foreach (WifiComponent wifiComponent in GetComponents()) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs index d205980e9..74704c129 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs @@ -332,7 +332,7 @@ namespace Barotrauma.Networking case (byte)FileTransferType.Submarine: string fileName = inc.ReadString(); string fileHash = inc.ReadString(); - var requestedSubmarine = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == fileName && s.MD5Hash.Hash == fileHash); + var requestedSubmarine = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == fileName && s.MD5Hash.Hash == fileHash); if (requestedSubmarine != null) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index d3df8d576..05e174ce6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -205,12 +205,12 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.RandomizeSettings(); if (!string.IsNullOrEmpty(serverSettings.SelectedSubmarine)) { - Submarine sub = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == serverSettings.SelectedSubmarine); + SubmarineInfo sub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSettings.SelectedSubmarine); if (sub != null) { GameMain.NetLobbyScreen.SelectedSub = sub; } } if (!string.IsNullOrEmpty(serverSettings.SelectedShuttle)) { - Submarine shuttle = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == serverSettings.SelectedShuttle); + SubmarineInfo shuttle = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSettings.SelectedShuttle); if (shuttle != null) { GameMain.NetLobbyScreen.SelectedShuttle = shuttle; } } started = true; @@ -382,7 +382,7 @@ namespace Barotrauma.Networking } bool isCrewDead = - connectedClients.All(c => c.Character == null || c.Character.IsDead || c.Character.IsUnconscious); + connectedClients.All(c => c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated); bool subAtLevelEnd = false; if (Submarine.MainSub != null && Submarine.MainSubs[1] == null) @@ -529,7 +529,7 @@ namespace Barotrauma.Networking c.ChatSpamSpeed = Math.Max(0.0f, c.ChatSpamSpeed - deltaTime); //constantly increase AFK timer if the client is controlling a character (gets reset to zero every time an input is received) - if (gameStarted && c.Character != null && !c.Character.IsDead && !c.Character.IsUnconscious) + if (gameStarted && c.Character != null && !c.Character.IsDead && !c.Character.IsIncapacitated) { if (c.Connection != OwnerConnection) c.KickAFKTimer += deltaTime; } @@ -627,16 +627,21 @@ namespace Barotrauma.Networking //game already started -> send start message immediately if (gameStarted) { - SendStartMessage(roundStartSeed, Submarine.MainSub, GameMain.GameSession.GameMode.Preset, connectedClient); + SendStartMessage(roundStartSeed, GameMain.GameSession.Level.Seed, GameMain.GameSession, connectedClient, true); } } break; + case ClientPacketHeader.REQUEST_STARTGAMEFINALIZE: + if (gameStarted && connectedClient != null) + { + SendRoundStartFinalize(connectedClient); + } + break; case ClientPacketHeader.UPDATE_LOBBY: ClientReadLobby(inc); break; case ClientPacketHeader.UPDATE_INGAME: - if (!gameStarted) return; - + if (!gameStarted) { return; } ClientReadIngame(inc); break; case ClientPacketHeader.CAMPAIGN_SETUP_INFO: @@ -648,7 +653,7 @@ namespace Barotrauma.Networking string subName = inc.ReadString(); string subHash = inc.ReadString(); - var matchingSub = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash); + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash); if (matchingSub == null) { @@ -743,7 +748,7 @@ namespace Barotrauma.Networking if (Level.Loaded != null && levelEqualityCheckVal != Level.Loaded.EqualityCheckVal) { errorStr += " Level equality check failed. The level generated at your end doesn't match the level generated by the server(seed: " + Level.Loaded.Seed + - ", sub: " + Submarine.MainSub.Name + " (" + Submarine.MainSub.MD5Hash.ShortHash + ")" + + ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortHash + ")" + ", mirrored: " + Level.Loaded.Mirrored + ")."; } @@ -778,8 +783,8 @@ namespace Barotrauma.Networking Directory.CreateDirectory(ServerLog.SavePath); } - string filePath = "event_error_log_server_" + client.Name + "_" + ToolBox.RemoveInvalidFileNameChars(DateTime.UtcNow.ToShortTimeString() + ".log"); - filePath = Path.Combine(ServerLog.SavePath, filePath); + string filePath = "event_error_log_server_" + client.Name + "_" + DateTime.UtcNow.ToShortTimeString() + ".log"; + filePath = Path.Combine(ServerLog.SavePath, ToolBox.RemoveInvalidFileNameChars(filePath)); if (File.Exists(filePath)) { return; } List errorLines = new List @@ -793,7 +798,7 @@ namespace Barotrauma.Networking } if (GameMain.GameSession?.Submarine != null) { - errorLines.Add("Submarine: " + GameMain.GameSession.Submarine.Name); + errorLines.Add("Submarine: " + GameMain.GameSession.Submarine.Info.Name); } if (Level.Loaded != null) { @@ -1072,7 +1077,7 @@ namespace Barotrauma.Networking case ClientPermissions.Kick: string kickedName = inc.ReadString().ToLowerInvariant(); string kickReason = inc.ReadString(); - var kickedClient = connectedClients.Find(cl => cl != sender && cl.Name.ToLowerInvariant() == kickedName && cl.Connection != OwnerConnection); + var kickedClient = connectedClients.Find(cl => cl != sender && cl.Name.Equals(kickedName, StringComparison.OrdinalIgnoreCase) && cl.Connection != OwnerConnection); if (kickedClient != null) { Log("Client \"" + sender.Name + "\" kicked \"" + kickedClient.Name + "\".", ServerLog.MessageType.ServerMessage); @@ -1089,7 +1094,7 @@ namespace Barotrauma.Networking bool range = inc.ReadBoolean(); double durationSeconds = inc.ReadDouble(); - var bannedClient = connectedClients.Find(cl => cl != sender && cl.Name.ToLowerInvariant() == bannedName && cl.Connection != OwnerConnection); + var bannedClient = connectedClients.Find(cl => cl != sender && cl.Name.Equals(bannedName, StringComparison.OrdinalIgnoreCase) && cl.Connection != OwnerConnection); if (bannedClient != null) { Log("Client \"" + sender.Name + "\" banned \"" + bannedClient.Name + "\".", ServerLog.MessageType.ServerMessage); @@ -1148,7 +1153,7 @@ namespace Barotrauma.Networking break; case ClientPermissions.SelectMode: UInt16 modeIndex = inc.ReadUInt16(); - if (GameMain.NetLobbyScreen.GameModes[modeIndex].Identifier.ToLowerInvariant() == "multiplayercampaign") + if (GameMain.NetLobbyScreen.GameModes[modeIndex].Identifier.Equals("multiplayercampaign", StringComparison.OrdinalIgnoreCase)) { string[] saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer).ToArray(); for (int i = 0; i < saveFiles.Length; i++) @@ -1337,7 +1342,7 @@ 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.IsOutpost || sub.DockedTo.Any(s => s.ID < sub.ID)) continue; + if (sub.Info.IsOutpost || sub.DockedTo.Any(s => s.ID < sub.ID)) continue; if (!c.PendingPositionUpdates.Contains(sub)) c.PendingPositionUpdates.Enqueue(sub); } @@ -1480,6 +1485,7 @@ namespace Barotrauma.Networking outmsg.Write(client.Character == null || !gameStarted ? (client.PreferredJob ?? "") : ""); outmsg.Write(client.Character == null || !gameStarted ? (ushort)0 : client.Character.ID); outmsg.Write(client.Muted); + outmsg.Write(client.InGame); outmsg.Write(client.Connection != OwnerConnection); //is kicking the player allowed outmsg.WritePadBits(); } @@ -1649,12 +1655,12 @@ namespace Barotrauma.Networking Log("Starting a new round...", ServerLog.MessageType.ServerMessage); - Submarine selectedSub = null; - Submarine selectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle; + SubmarineInfo selectedSub = null; + SubmarineInfo selectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle; if (serverSettings.Voting.AllowSubVoting) { - selectedSub = serverSettings.Voting.HighestVoted(VoteType.Sub, connectedClients); + selectedSub = serverSettings.Voting.HighestVoted(VoteType.Sub, connectedClients); if (selectedSub == null) selectedSub = GameMain.NetLobbyScreen.SelectedSub; } else @@ -1686,7 +1692,7 @@ namespace Barotrauma.Networking return true; } - private IEnumerable InitiateStartGame(Submarine selectedSub, Submarine selectedShuttle, bool usingShuttle, GameModePreset selectedMode) + private IEnumerable InitiateStartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, bool usingShuttle, GameModePreset selectedMode) { initiatedStartGame = true; @@ -1734,7 +1740,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Success; } - private IEnumerable StartGame(Submarine selectedSub, Submarine selectedShuttle, bool usingShuttle, GameModePreset selectedMode) + private IEnumerable StartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, bool usingShuttle, GameModePreset selectedMode) { entityEventManager.Clear(); @@ -1773,10 +1779,10 @@ namespace Barotrauma.Networking //always allow the server owner to spectate even if it's disallowed in server settings playingClients.RemoveAll(c => c.Connection == OwnerConnection && c.SpectateOnly); - if (GameMain.GameSession.GameMode.Mission != null && - GameMain.GameSession.GameMode.Mission.AssignTeamIDs(playingClients)) + if (GameMain.GameSession.GameMode.Mission != null) { - teamCount = 2; + GameMain.GameSession.GameMode.Mission.AssignTeamIDs(playingClients); + teamCount = GameMain.GameSession.GameMode.Mission.TeamCount; } else { @@ -1796,29 +1802,31 @@ namespace Barotrauma.Networking campaign.Map.SelectRandomLocation(preferUndiscovered: true); } + SendStartMessage(roundStartSeed, campaign.Map.SelectedConnection.Level.Seed, GameMain.GameSession, connectedClients, false); + GameMain.GameSession.StartRound(campaign.Map.SelectedConnection.Level, - reloadSub: true, - loadSecondSub: teamCount > 1, mirrorLevel: campaign.Map.CurrentLocation != campaign.Map.SelectedConnection.Locations[0]); campaign.AssignClientCharacterInfos(connectedClients); Log("Game mode: " + selectedMode.Name, ServerLog.MessageType.ServerMessage); - Log("Submarine: " + GameMain.GameSession.Submarine.Name, ServerLog.MessageType.ServerMessage); + Log("Submarine: " + GameMain.GameSession.SubmarineInfo.Name, ServerLog.MessageType.ServerMessage); Log("Level seed: " + campaign.Map.SelectedConnection.Level.Seed, ServerLog.MessageType.ServerMessage); } else { - GameMain.GameSession.StartRound(GameMain.NetLobbyScreen.LevelSeed, serverSettings.SelectedLevelDifficulty, teamCount > 1); + SendStartMessage(roundStartSeed, GameMain.NetLobbyScreen.LevelSeed, GameMain.GameSession, connectedClients, false); + + GameMain.GameSession.StartRound(GameMain.NetLobbyScreen.LevelSeed, serverSettings.SelectedLevelDifficulty); Log("Game mode: " + selectedMode.Name, ServerLog.MessageType.ServerMessage); Log("Submarine: " + selectedSub.Name, ServerLog.MessageType.ServerMessage); Log("Level seed: " + GameMain.NetLobbyScreen.LevelSeed, ServerLog.MessageType.ServerMessage); } - if (GameMain.GameSession.Submarine.IsFileCorrupted) + if (GameMain.GameSession.SubmarineInfo.IsFileCorrupted) { CoroutineManager.StopCoroutines(startGameCoroutine); initiatedStartGame = false; - SendChatMessage(TextManager.FormatServerMessage($"SubLoadError~[subname]={GameMain.GameSession.Submarine.Name}"), ChatMessageType.Error); + SendChatMessage(TextManager.FormatServerMessage($"SubLoadError~[subname]={GameMain.GameSession.SubmarineInfo.Name}"), ChatMessageType.Error); yield return CoroutineStatus.Failure; } @@ -1827,6 +1835,7 @@ namespace Barotrauma.Networking if (serverSettings.AllowRespawn && missionAllowRespawn) { respawnManager = new RespawnManager(this, usingShuttle ? selectedShuttle : null); } + Level.Loaded?.SpawnCorpses(); AutoItemPlacer.PlaceIfNeeded(GameMain.GameSession.GameMode); entityEventManager.RefreshEntityIDs(); @@ -1946,8 +1955,6 @@ namespace Barotrauma.Networking GameAnalyticsManager.AddDesignEvent("Traitors:" + (TraitorManager == null ? "Disabled" : "Enabled")); - SendStartMessage(roundStartSeed, Submarine.MainSub, GameMain.GameSession.GameMode.Preset, connectedClients); - yield return CoroutineStatus.Running; GameMain.GameScreen.Select(); @@ -1965,35 +1972,34 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Success; } - private void SendStartMessage(int seed, Submarine selectedSub, GameModePreset selectedMode, List clients) + private void SendStartMessage(int seed, string levelSeed, GameSession gameSession, List clients, bool includesFinalize) { foreach (Client client in clients) { - SendStartMessage(seed, selectedSub, selectedMode, client); + SendStartMessage(seed, levelSeed, gameSession, client, includesFinalize); } } - private void SendStartMessage(int seed, Submarine selectedSub, GameModePreset selectedMode, Client client) + private void SendStartMessage(int seed, string levelSeed, GameSession gameSession, Client client, bool includesFinalize) { IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.STARTGAME); msg.Write(seed); - msg.Write(GameMain.GameSession.Level.Seed); - msg.Write(GameMain.GameSession.Level.EqualityCheckVal); + msg.Write(levelSeed); msg.Write(serverSettings.SelectedLevelDifficulty); msg.Write((byte)GameMain.Config.LosMode); msg.Write((byte)GameMain.NetLobbyScreen.MissionType); - msg.Write(selectedSub.Name); - msg.Write(selectedSub.MD5Hash.Hash); + msg.Write(gameSession.SubmarineInfo.Name); + msg.Write(gameSession.SubmarineInfo.MD5Hash.Hash); msg.Write(serverSettings.UseRespawnShuttle); msg.Write(GameMain.NetLobbyScreen.SelectedShuttle.Name); msg.Write(GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash.Hash); - msg.Write(selectedMode.Identifier); + msg.Write(gameSession.GameMode.Preset.Identifier); msg.Write((short)(GameMain.GameSession.GameMode?.Mission == null ? -1 : MissionPrefab.List.IndexOf(GameMain.GameSession.GameMode.Mission.Prefab))); @@ -2002,13 +2008,33 @@ namespace Barotrauma.Networking MissionMode missionMode = GameMain.GameSession.GameMode as MissionMode; bool missionAllowRespawn = campaign == null && (missionMode?.Mission == null || missionMode.Mission.AllowRespawn); msg.Write(serverSettings.AllowRespawn && missionAllowRespawn); - msg.Write(Submarine.MainSubs[1] != null); //loadSecondSub msg.Write(serverSettings.AllowDisguises); msg.Write(serverSettings.AllowRewiring); msg.Write(serverSettings.AllowRagdollButton); + serverSettings.WriteMonsterEnabled(msg); + + msg.Write(includesFinalize); msg.WritePadBits(); + if (includesFinalize) + { + WriteRoundStartFinalize(msg, client); + } + + serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); + } + + private void SendRoundStartFinalize(Client client) + { + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte)ServerPacketHeader.STARTGAMEFINALIZE); + WriteRoundStartFinalize(msg, client); + serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); + } + + private void WriteRoundStartFinalize(IWriteMessage msg, Client client) + { //tell the client what content files they should preload var contentToPreload = GameMain.GameSession.EventManager.GetFilesToPreload(); msg.Write((ushort)contentToPreload.Count()); @@ -2017,12 +2043,8 @@ namespace Barotrauma.Networking msg.Write((byte)contentFile.Type); msg.Write(contentFile.Path); } - - serverSettings.WriteMonsterEnabled(msg); - + msg.Write(GameMain.GameSession.Level.EqualityCheckVal); GameMain.GameSession.Mission?.ServerWriteInitial(msg, client); - - serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } public void EndGame() @@ -2170,11 +2192,9 @@ namespace Barotrauma.Networking public override void KickPlayer(string playerName, string reason) { - playerName = playerName.ToLowerInvariant(); - Client client = connectedClients.Find(c => - c.Name.ToLowerInvariant() == playerName || - (c.Character != null && c.Character.Name.ToLowerInvariant() == playerName)); + c.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase) || + (c.Character != null && c.Character.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase))); KickClient(client, reason); } @@ -2208,11 +2228,9 @@ namespace Barotrauma.Networking public override void BanPlayer(string playerName, string reason, bool range = false, TimeSpan? duration = null) { - playerName = playerName.ToLowerInvariant(); - Client client = connectedClients.Find(c => - c.Name.ToLowerInvariant() == playerName || - (c.Character != null && c.Character.Name.ToLowerInvariant() == playerName)); + c.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase) || + (c.Character != null && c.Character.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase))); if (client == null) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index 12cd2c5e9..81c37c14c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -175,7 +175,7 @@ namespace Barotrauma.Networking //UNLESS the character is unconscious, in which case we'll read the messages immediately (because further inputs will be ignored) //atm the "give in" command is the only thing unconscious characters can do, other types of events are ignored - if (!bufferedEvent.Character.IsUnconscious && + if (!bufferedEvent.Character.IsIncapacitated && NetIdUtils.IdMoreRecent(bufferedEvent.CharacterStateID, bufferedEvent.Character.LastProcessedID)) { continue; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index d1a158d2e..8a12853f7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -258,6 +258,9 @@ namespace Barotrauma.Networking { bool bot = i >= clients.Count; + characterInfos[i].CurrentOrder = null; + characterInfos[i].CurrentOrderOption = null; + var character = Character.Create(characterInfos[i], shuttleSpawnPoints[i].WorldPosition, characterInfos[i].Name, !bot, bot); character.TeamID = Character.TeamType.Team1; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 77b9c4c1e..b92cc8ea1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -367,7 +367,7 @@ namespace Barotrauma.Networking if (clientElement.Attribute("preset") == null) { string permissionsStr = clientElement.GetAttributeString("permissions", ""); - if (permissionsStr.ToLowerInvariant() == "all") + if (permissionsStr.Equals("all", StringComparison.OrdinalIgnoreCase)) { foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions))) { @@ -384,7 +384,7 @@ namespace Barotrauma.Networking { foreach (XElement commandElement in clientElement.Elements()) { - if (commandElement.Name.ToString().ToLowerInvariant() != "command") continue; + if (!commandElement.Name.ToString().Equals("command", StringComparison.OrdinalIgnoreCase)) { continue; } string commandName = commandElement.GetAttributeString("name", ""); DebugConsole.Command command = DebugConsole.FindCommand(commandName); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 5a5962955..9052887a6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -38,7 +38,7 @@ namespace Barotrauma { case VoteType.Sub: string subName = inc.ReadString(); - Submarine sub = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == subName); + SubmarineInfo sub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName); sender.SetVote(voteType, sub); break; @@ -97,7 +97,7 @@ namespace Barotrauma foreach (Pair vote in voteList) { msg.Write((byte)vote.Second); - msg.Write(((Submarine)vote.First).Name); + msg.Write(((SubmarineInfo)vote.First).Name); } } msg.Write(AllowModeVoting); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index 2506ea91f..e355d1d3d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Text; using System.Threading; +using System.Runtime.InteropServices; #endregion @@ -17,53 +18,63 @@ namespace Barotrauma /// public static class Program { +#if LINUX + /// + /// Sets the required environment variables for the game to initialize Steamworks correctly. + /// + [DllImport("linux_steam_env", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + private static extern void setLinuxEnv(); +#endif + /// /// The main entry point for the application. /// [STAThread] static void Main(string[] args) { - GameMain game = null; - #if !DEBUG - try - { + AppDomain currentDomain = AppDomain.CurrentDomain; + currentDomain.UnhandledException += new UnhandledExceptionEventHandler(CrashHandler); #endif - Console.WriteLine("Barotrauma Dedicated Server " + GameMain.Version + - " (" + AssemblyInfo.GetBuildString() + ", branch " + AssemblyInfo.GetGitBranch() + ", revision " + AssemblyInfo.GetGitRevision() + ")"); - game = new GameMain(args); - - game.Run(); - if (GameSettings.SendUserStatistics) { GameAnalytics.OnQuit(); } - SteamManager.ShutDown(); -#if !DEBUG - } - catch (Exception e) - { - CrashDump(game, "servercrashreport.log", e); - GameMain.Server?.NotifyCrash(); - } +#if LINUX + setLinuxEnv(); #endif + Console.WriteLine("Barotrauma Dedicated Server " + GameMain.Version + + " (" + AssemblyInfo.GetBuildString() + ", branch " + AssemblyInfo.GetGitBranch() + ", revision " + AssemblyInfo.GetGitRevision() + ")"); + + Game = new GameMain(args); + + Game.Run(); + if (GameSettings.SendUserStatistics) { GameAnalytics.OnQuit(); } + SteamManager.ShutDown(); } - - static void CrashDump(GameMain game, string filePath, Exception exception) + + static GameMain Game; + + private static void CrashHandler(object sender, UnhandledExceptionEventArgs args) { try { - GameMain.Server?.ServerSettings?.SaveSettings(); - GameMain.Server?.ServerSettings?.BanList.Save(); - if (GameMain.Server?.ServerSettings?.KarmaPreset == "custom") - { - GameMain.Server?.KarmaManager?.SaveCustomPreset(); - GameMain.Server?.KarmaManager?.Save(); - } + Game?.Exit(); + CrashDump("servercrashreport.log", (Exception)args.ExceptionObject); + GameMain.Server?.NotifyCrash(); } - //gotta catch them all, we don't want to crash while writing a crash report - catch (Exception e) + catch { - string errorMsg = "Exception thrown while writing a crash report: " + e.Message + "\n" + e.StackTrace; - GameAnalyticsManager.AddErrorEventOnce("CrashDump:FailedToSaveSettings", EGAErrorSeverity.Error, errorMsg); + //exception handler is broken, we have a serious problem here!! + return; + } + } + + static void CrashDump(string filePath, Exception exception) + { + GameMain.Server?.ServerSettings?.SaveSettings(); + GameMain.Server?.ServerSettings?.BanList.Save(); + if (GameMain.Server?.ServerSettings?.KarmaPreset == "custom") + { + GameMain.Server?.KarmaManager?.SaveCustomPreset(); + GameMain.Server?.KarmaManager?.Save(); } int existingFiles = 0; @@ -81,11 +92,13 @@ 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("Selected content packages: " + (!GameMain.SelectedPackages.Any() ? "None" : string.Join(", ", GameMain.SelectedPackages.Select(c => c.Name)))); + sb.AppendLine("Game version " + GameMain.Version + " (" + AssemblyInfo.GetBuildString() + ", branch " + AssemblyInfo.GetGitBranch() + ", revision " + AssemblyInfo.GetGitRevision() + ")"); + if (GameMain.SelectedPackages != null) + { + sb.AppendLine("Selected content packages: " + (!GameMain.SelectedPackages.Any() ? "None" : string.Join(", ", GameMain.SelectedPackages.Select(c => c.Name)))); + } sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed)); - sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Name + " (" + Submarine.MainSub.MD5Hash + ")")); + sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")")); sb.AppendLine("Selected screen: " + (Screen.Selected == null ? "None" : Screen.Selected.ToString())); if (GameMain.Server != null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index 5eb445456..be3523b4d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -8,10 +8,10 @@ namespace Barotrauma { partial class NetLobbyScreen : Screen { - private Submarine selectedSub; - private Submarine selectedShuttle; + private SubmarineInfo selectedSub; + private SubmarineInfo selectedShuttle; - public Submarine SelectedSub + public SubmarineInfo SelectedSub { get { return selectedSub; } set @@ -24,7 +24,7 @@ namespace Barotrauma } } } - public Submarine SelectedShuttle + public SubmarineInfo SelectedShuttle { get { return selectedShuttle; } set { selectedShuttle = value; lastUpdateID++; } @@ -121,7 +121,7 @@ namespace Barotrauma { LevelSeed = ToolBox.RandomSeed(8); - subs = Submarine.SavedSubmarines.Where(s => !s.HasTag(SubmarineTag.HideInMenus)).ToList(); + subs = SubmarineInfo.SavedSubmarines.Where(s => !s.HasTag(SubmarineTag.HideInMenus)).ToList(); if (subs == null || subs.Count() == 0) { @@ -150,8 +150,8 @@ namespace Barotrauma GameModes = GameModePreset.List.ToArray(); } - private List subs; - public List GetSubList() + private List subs; + public List GetSubList() { return subs; } @@ -198,7 +198,7 @@ namespace Barotrauma if (GameMain.Server.ServerSettings.SubSelectionMode == SelectionMode.Random) { - var nonShuttles = Submarine.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)).ToList(); SelectedSub = nonShuttles[Rand.Range(0, nonShuttles.Count)]; } if (GameMain.Server.ServerSettings.ModeSelectionMode == SelectionMode.Random) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs index 2472d685f..5bfff56a5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs @@ -67,7 +67,7 @@ namespace Barotrauma { continue; } - if (character.SpeciesName.ToLowerInvariant() == entities[activeEntityIndex] && Vector2.Distance(activeEntitySavedPosition, character.WorldPosition) < graceDistance) + if (character.SpeciesName.Equals(entities[activeEntityIndex], StringComparison.OrdinalIgnoreCase) && Vector2.Distance(activeEntitySavedPosition, character.WorldPosition) < graceDistance) { activeEntity = character; transformationTime = 0.0; @@ -117,7 +117,7 @@ namespace Barotrauma { continue; } - if (character.SpeciesName.ToLowerInvariant() == entities[activeEntityIndex].ToLowerInvariant()) + if (character.SpeciesName.Equals(entities[activeEntityIndex], StringComparison.OrdinalIgnoreCase)) { activeEntity = character; break; @@ -131,7 +131,7 @@ namespace Barotrauma { continue; } - if (item.prefab.Identifier.ToLowerInvariant() == entities[0].ToLowerInvariant()) + if (item.prefab.Identifier.Equals(entities[0], StringComparison.OrdinalIgnoreCase)) { activeEntity = item; break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs index 414bc00ce..cceda84c4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs @@ -23,7 +23,7 @@ namespace Barotrauma var floodingAmount = 0.0f; foreach (Hull hull in Hull.hullList) { - if (hull.Submarine == null || hull.Submarine.IsOutpost || Traitors.All(traitor => hull.Submarine.TeamID != traitor.Character.TeamID)) { continue; } + if (hull.Submarine == null || hull.Submarine.Info.IsOutpost || Traitors.All(traitor => hull.Submarine.TeamID != traitor.Character.TeamID)) { continue; } if (hull.Submarine == GameMain.Server?.RespawnManager?.RespawnShuttle) { continue; } ++validHullsCount; floodingAmount += hull.WaterVolume / hull.Volume; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs index 6e40f2681..8d4500fa8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs @@ -52,7 +52,7 @@ namespace Barotrauma { continue; } - if (character.SpeciesName.ToLowerInvariant() == speciesId) + if (character.SpeciesName.Equals(speciesId, StringComparison.OrdinalIgnoreCase)) { targetCharacter = character; break; diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 53ebc9350..978f3cde6 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -2,11 +2,11 @@ Exe - netcoreapp3.0 + netcoreapp3.1 Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.9.7.1 + 0.9.9.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer @@ -54,6 +54,7 @@ + diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index 7a70b5557..62415487a 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -1,5 +1,5 @@ - - + + @@ -51,10 +51,12 @@ - + + + @@ -73,13 +75,19 @@ - + + + + + + + @@ -93,6 +101,7 @@ + @@ -106,39 +115,43 @@ - - + + + + - + + - - + + - + - - - - - - - - - - - - - + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs index 4f25f046e..403f31dc0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -25,7 +25,7 @@ namespace Barotrauma /// /// How long does it take for the ai target to fade out if not kept alive. /// - public float FadeOutTime { get; private set; } = 1; + public float FadeOutTime { get; private set; } = 2; public bool Static { get; private set; } public bool StaticSound { get; private set; } @@ -92,7 +92,7 @@ namespace Barotrauma public string SonarLabel; public string SonarIconIdentifier; - public bool Enabled = true; + public bool Enabled => SoundRange > 0 || SightRange > 0; public float MinSoundRange, MinSightRange; public float MaxSoundRange = 100000, MaxSightRange = 100000; @@ -177,7 +177,8 @@ namespace Barotrauma StaticSight = true; } SonarDisruption = element.GetAttributeFloat("sonardisruption", 0.0f); - SonarLabel = element.GetAttributeString("sonarlabel", ""); + string label = element.GetAttributeString("sonarlabel", ""); + SonarLabel = TextManager.Get(label, returnNull: true) ?? label; SonarIconIdentifier = element.GetAttributeString("sonaricon", ""); string typeString = element.GetAttributeString("type", "Any"); if (Enum.TryParse(typeString, out TargetType t)) @@ -195,7 +196,7 @@ namespace Barotrauma public void Update(float deltaTime) { - if (!Static && FadeOutTime > 0) + if (Enabled && !Static && FadeOutTime > 0) { // The aitarget goes silent/invisible if the components don't keep it active if (!StaticSight) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 44bfc5ad5..11e3fdebe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -89,9 +89,6 @@ namespace Barotrauma private readonly float memoryFadeTime = 0.5f; private readonly float avoidTime = 3; - //Has the character been attacked since the last Update. - private bool wasAttacked; - private float avoidTimer; public LatchOntoAI LatchOntoAI { get; private set; } @@ -234,13 +231,6 @@ namespace Barotrauma public override void Update(float deltaTime) { - if (wasAttacked) - { - LatchOntoAI?.DeattachFromBody(); - Character.AnimController.ReleaseStuckLimbs(); - wasAttacked = false; - } - if (DisableEnemyAI) { return; } base.Update(deltaTime); @@ -305,7 +295,8 @@ namespace Barotrauma FadeMemories(updateMemoriesInverval); updateMemoriesTimer = updateMemoriesInverval; } - if (Character.HealthPercentage <= FleeHealthThreshold) + if (Character.HealthPercentage <= FleeHealthThreshold && SelectedAiTarget != null && + SelectedAiTarget.Entity is Character target && (target.IsPlayer || IsBeingChasedBy(target))) { State = AIState.Flee; wallTarget = null; @@ -384,9 +375,9 @@ namespace Barotrauma State = AIState.Idle; return; } - float distance = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition); + float squaredDistance = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition); var attackLimb = GetAttackLimb(SelectedAiTarget.WorldPosition); - if (attackLimb != null && distance <= Math.Pow(attackLimb.attack.Range, 2)) + if (attackLimb != null && squaredDistance <= Math.Pow(attackLimb.attack.Range, 2)) { run = true; if (State == AIState.Avoid) @@ -402,17 +393,18 @@ namespace Barotrauma { bool isBeingChased = IsBeingChased; float reactDistance = !isBeingChased && selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget); - if (distance <= Math.Pow(reactDistance + escapeMargin, 2)) + if (squaredDistance <= Math.Pow(reactDistance + escapeMargin, 2)) { float halfReactDistance = reactDistance / 2; - if (State == AIState.Aggressive || State == AIState.PassiveAggressive && distance < Math.Pow(halfReactDistance, 2)) + float attackDistance = selectedTargetingParams != null && selectedTargetingParams.AttackDistance > 0 ? selectedTargetingParams.AttackDistance : halfReactDistance; + if (State == AIState.Aggressive || State == AIState.PassiveAggressive && squaredDistance < Math.Pow(attackDistance, 2)) { run = true; UpdateAttack(deltaTime); } else { - run = isBeingChased ? true : distance < Math.Pow(halfReactDistance, 2); + run = isBeingChased ? true : squaredDistance < Math.Pow(halfReactDistance, 2); if (escapeMargin <= 0) { escapeMargin = halfReactDistance; @@ -440,13 +432,14 @@ namespace Barotrauma SwarmBehavior.Refresh(); SwarmBehavior.UpdateSteering(deltaTime); } - float speed = Character.AnimController.GetCurrentSpeed(run); + 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); if (Character.CurrentHull != null && Character.AnimController.InWater) { // Halve the swimming speed inside the sub - speed /= 2; + Character.AnimController.TargetMovement *= 0.5f; } - steeringManager.Update(speed); } #region Idle @@ -577,7 +570,7 @@ namespace Barotrauma else if (SelectedAiTarget?.Entity is Character targetCharacter && targetCharacter.CurrentHull == Character.CurrentHull) { // Steer away from the target if in the same room - Vector2 escapeDir = Vector2.Normalize(SelectedAiTarget != null ? WorldPosition - SelectedAiTarget.WorldPosition : Character.GetTargetMovement()); + Vector2 escapeDir = Vector2.Normalize(SelectedAiTarget != null ? WorldPosition - SelectedAiTarget.WorldPosition : Character.AnimController.TargetMovement); if (!MathUtils.IsValid(escapeDir)) escapeDir = Vector2.UnitY; SteeringManager.SteeringManual(deltaTime, escapeDir); } @@ -615,7 +608,7 @@ namespace Barotrauma { escapeTarget = null; allGapsSearched = false; - Vector2 escapeDir = Vector2.Normalize(SelectedAiTarget != null ? WorldPosition - SelectedAiTarget.WorldPosition : Character.GetTargetMovement()); + Vector2 escapeDir = Vector2.Normalize(SelectedAiTarget != null ? WorldPosition - SelectedAiTarget.WorldPosition : Character.AnimController.TargetMovement); if (!MathUtils.IsValid(escapeDir)) escapeDir = Vector2.UnitY; SteeringManager.SteeringManual(deltaTime, escapeDir); if (Character.CurrentHull == null) @@ -758,6 +751,10 @@ namespace Barotrauma bool pursue = false; if (IsCoolDownRunning) { + if (AttackingLimb.attack.CoolDownTimer >= AttackingLimb.attack.CoolDown + AttackingLimb.attack.CurrentRandomCoolDown - AttackingLimb.attack.AfterAttackDelay) + { + return; + } switch (AttackingLimb.attack.AfterAttack) { case AIBehaviorAfterAttack.Pursue: @@ -1017,58 +1014,63 @@ namespace Barotrauma return; } - Vector2 offset = Character.SimPosition - steeringLimb.SimPosition; - // Offset so that we don't overshoot the movement - Vector2 steerPos = attackSimPos + offset; - - if (SteeringManager is IndoorsSteeringManager pathSteering) + if (AttackingLimb != null && AttackingLimb.attack.Retreat) { - if (pathSteering.CurrentPath != null) + UpdateFallBack(attackWorldPos, deltaTime, false); + } + else + { + Vector2 offset = Character.SimPosition - steeringLimb.SimPosition; + // Offset so that we don't overshoot the movement + Vector2 steerPos = attackSimPos + offset; + if (SteeringManager is IndoorsSteeringManager pathSteering) { - // Attack doors - if (canAttackSub) + if (pathSteering.CurrentPath != null) { - // If the target is in the same hull, there shouldn't be any doors blocking the path - if (targetCharacter == null || targetCharacter.CurrentHull != Character.CurrentHull) + // Attack doors + if (canAttackSub) { - var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor; - if (door != null && !door.IsOpen) + // If the target is in the same hull, there shouldn't be any doors blocking the path + if (targetCharacter == null || targetCharacter.CurrentHull != Character.CurrentHull) { - if (door.Item.AiTarget != null && SelectedAiTarget != door.Item.AiTarget) + var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor; + if (door != null && !door.IsOpen) { - SelectTarget(door.Item.AiTarget, selectedTargetMemory.Priority); - return; + if (door.Item.AiTarget != null && SelectedAiTarget != door.Item.AiTarget) + { + SelectTarget(door.Item.AiTarget, selectedTargetMemory.Priority); + return; + } } } } - } - // Steer towards the target if in the same room and swimming - if ((Character.AnimController.InWater || pursue) && targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull)) - { - SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - steeringLimb.SimPosition)); + // Steer towards the target if in the same room and swimming + if ((Character.AnimController.InWater || pursue) && targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull)) + { + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - steeringLimb.SimPosition)); + } + else + { + SteeringManager.SteeringSeek(steerPos, 2); + // Switch to Idle when cannot reach the target and if cannot damage the walls + if ((!canAttackSub || wallTarget == null) && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable) + { + State = AIState.Idle; + return; + } + } } else { - SteeringManager.SteeringSeek(steerPos, 2); - // Switch to Idle when cannot reach the target and if cannot damage the walls - if ((!canAttackSub || wallTarget == null) && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable) - { - State = AIState.Idle; - return; - } + SteeringManager.SteeringSeek(steerPos, 5); } } else { - SteeringManager.SteeringSeek(steerPos, 5); + SteeringManager.SteeringSeek(steerPos, 10); + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); } } - else - { - SteeringManager.SteeringSeek(steerPos, 10); - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); - } - if (canAttack) { if (!UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance, attackTargetLimb)) @@ -1110,31 +1112,11 @@ namespace Barotrauma return false; } - private bool CanAttack(Entity target) - { - if (target == null) { return false; } - if (target is Character c) - { - if (Character.CurrentHull == null && c.CurrentHull != null || Character.CurrentHull != null && c.CurrentHull == null) - { - return false; - } - } - else if (target is Item i && i.GetComponent() == null) - { - if (Character.CurrentHull == null && i.CurrentHull != null || Character.CurrentHull != null && i.CurrentHull == null) - { - return false; - } - } - return true; - } - private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb = null) { var currentContexts = Character.GetAttackContexts(); Entity target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity; - if (!CanAttack(target)) { return null; } + if (target == null) { return null; } Limb selectedLimb = null; float currentPriority = -1; foreach (Limb limb in Character.AnimController.Limbs) @@ -1174,14 +1156,18 @@ namespace Barotrauma { wallTarget = null; if (SelectedAiTarget == null) { return; } + if (SelectedAiTarget.Entity == null) { return; } //check if there's a wall between the target and the Character Vector2 rayStart = SimPosition; Vector2 rayEnd = SelectedAiTarget.SimPosition; - bool offset = SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null; - if (offset) + if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) { rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition; } + else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null) + { + rayEnd -= Character.Submarine.SimPosition; + } Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine); if (Submarine.LastPickedFraction != 1.0f && closestBody != null) { @@ -1259,9 +1245,16 @@ namespace Barotrauma float reactionTime = Rand.Range(0.1f, 0.3f); updateTargetsTimer = Math.Min(updateTargetsTimer, reactionTime); - wasAttacked = true; - + bool wasLatched = IsLatchedOnSub; + Character.AnimController.ReleaseStuckLimbs(); + LatchOntoAI?.DeattachFromBody(); if (attacker == null || attacker.AiTarget == null) { return; } + if (wasLatched) + { + avoidTimer = avoidTime * Rand.Range(0.75f, 1.25f); + SelectTarget(attacker.AiTarget); + return; + } if (State == AIState.Flee) { @@ -1272,22 +1265,30 @@ namespace Barotrauma if (attackResult.Damage > 0.0f) { bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackSub; - if (Character.Params.AI.AttackWhenProvoked) + if (Character.Params.AI.AttackWhenProvoked && canAttack) { - if (canAttack) + if (attacker.IsHusk) + { + ChangeTargetState("husk", AIState.Attack, 100); + } + else { ChangeTargetState(attacker, AIState.Attack, 100); } } else if (!AIParams.HasTag(attacker.SpeciesName)) { - if (attacker.AIController is EnemyAIController enemyAI) + if (attacker.IsHusk) + { + ChangeTargetState("husk", canAttack ? AIState.Attack : AIState.Escape, 100); + } + else if (attacker.AIController is EnemyAIController enemyAI) { if (enemyAI.CombatStrength > CombatStrength) { if (!AIParams.HasTag("stronger")) { - ChangeTargetState(attacker, AIState.Escape, 100); + ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100); } } else if (enemyAI.CombatStrength < CombatStrength) @@ -1305,7 +1306,14 @@ namespace Barotrauma } else { - ChangeTargetState(attacker, AIState.Escape, 100); + ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100); + } + } + else if (canAttack && attacker.IsHuman && AIParams.TryGetTarget(attacker.SpeciesName, out CharacterParams.TargetParams targetingParams)) + { + if (targetingParams.State == AIState.Aggressive) + { + ChangeTargetState(attacker, AIState.Attack, 100); } } } @@ -1502,7 +1510,7 @@ namespace Barotrauma if (aiTarget.Type == AITarget.TargetType.HumanOnly) { continue; } if (!TargetOutposts) { - if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.IsOutpost) { continue; } + if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.Info.IsOutpost) { continue; } } Character targetCharacter = aiTarget.Entity as Character; //ignore the aitarget if it is the Character itself @@ -1512,29 +1520,6 @@ namespace Barotrauma string targetingTag = null; if (targetCharacter != null) { - if (targetCharacter.Submarine != Character.Submarine) - { - // In a different sub or the target is outside when we are inside or vice versa. - if (State == AIState.Avoid && State == AIState.Escape & State == AIState.Flee) - { - // If we are escaping, let's not ignore the target entirely, because there can be a gaps where we or they can go freely - if (targetCharacter.Submarine != null) - { - // Target is inside -> reduce the priority - valueModifier *= 0.5f; - if (Character.Submarine != null) - { - // Both inside different submarines -> can ignore safely - continue; - } - } - } - else - { - // Don't attack targets that are not in the same submarine - continue; - } - } if (targetCharacter.IsDead) { targetingTag = "dead"; @@ -1550,38 +1535,47 @@ namespace Barotrauma // Ignore targets that are in the same group (treat them like they were of the same species) continue; } - if (enemy.CombatStrength > CombatStrength) + if (targetCharacter.IsHusk && AIParams.HasTag("husk")) { - targetingTag = "stronger"; + targetingTag = "husk"; } - else if (enemy.CombatStrength < CombatStrength) + else { - targetingTag = "weaker"; - } - if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee)) - { - if (SelectedAiTarget == aiTarget) + if (enemy.CombatStrength > CombatStrength) { - // Freightened -> hold on to the target - valueModifier *= 2; + targetingTag = "stronger"; } - if (IsBeingChasedBy(targetCharacter)) + else if (enemy.CombatStrength < CombatStrength) { - valueModifier *= 2; + targetingTag = "weaker"; } - if (Character.CurrentHull != null && !VisibleHulls.Contains(targetCharacter.CurrentHull)) + if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee)) { - // Inside but in a different room - valueModifier /= 2; + if (SelectedAiTarget == aiTarget) + { + // Freightened -> hold on to the target + valueModifier *= 2; + } + if (IsBeingChasedBy(targetCharacter)) + { + valueModifier *= 2; + } + if (Character.CurrentHull != null && !VisibleHulls.Contains(targetCharacter.CurrentHull)) + { + // Inside but in a different room + valueModifier /= 2; + } } } } } else if (aiTarget.Entity != null) { + // Ignore all structures and items inside wrecks + if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.Info.IsWreck) { continue; } // Ignore the target if it's a room and the character is already inside a sub if (character.CurrentHull != null && aiTarget.Entity is Hull) { continue; } - + Door door = null; if (aiTarget.Entity is Item item) { @@ -1619,15 +1613,13 @@ namespace Barotrauma { continue; } - if (character.CurrentHull != null) + bool isCharacterOutside = s.Submarine == null || character.CurrentHull == null; + bool targetInnerWalls = AIParams.TargetInnerWalls; + if (!isCharacterOutside && !targetInnerWalls) { // Ignore walls when inside (walltargets still work) continue; } - if (s.Submarine == null) - { - continue; - } valueModifier = 1; if (!Character.AnimController.CanEnterSubmarine && IsWallDisabled(s)) { @@ -1640,30 +1632,37 @@ namespace Barotrauma bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null; if (Character.AnimController.CanEnterSubmarine) { - if (CanPassThroughHole(s, i)) + if (isCharacterOutside) { - valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : 0; + if (CanPassThroughHole(s, i)) + { + valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : (targetInnerWalls ? 1 : 0); + } + else + { + // Ignore holes that cannot be passed through if cannot attack items/structures. Holes that are big enough should be targeted, so that we can get in + if (!canAttackSub) + { + continue; + } + if (AggressiveBoarding && leadsInside) + { + // Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside + valueModifier *= 1 + section.gap.Open; + } + } } - else + else if (!canAttackSub || CanPassThroughHole(s, i)) { - // Ignore holes that cannot be passed through if cannot attack items/structures. Holes that are big enough should be targeted, so that we can get in - if (!canAttackSub) - { - valueModifier = 0; - break; - } - if (AggressiveBoarding) - { - // Up to 100% priority increase for every gap in the wall - valueModifier *= 1 + section.gap.Open; - } + // Already inside -> ignore holes in the walls and ignore walls if cannot attack the sub. + continue; } } - else if (!leadsInside) + else if (!leadsInside || !canAttackSub) { - // Ignore inner walls - valueModifier = 0; - break; + // Can't get in, ignore inner walls + // Also ignore all walls if cannot attack the sub + continue; } } } @@ -1746,6 +1745,53 @@ namespace Barotrauma if (valueModifier > targetValue) { + // Don't target items that we own. + // This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive) + if (aiTarget.Entity is Item i && i.IsOwnedBy(character)) { continue; } + if (targetCharacter != null) + { + if (targetCharacter.Submarine != Character.Submarine) + { + if (targetCharacter.Submarine != null) + { + // Target is inside -> reduce the priority + valueModifier *= 0.5f; + if (Character.Submarine != null) + { + // Both inside different submarines -> can ignore safely + continue; + } + } + else if (Character.CurrentHull != null) + { + // Target outside, but we are inside -> Check if we can get to the target. + // Only check if we are not already targeting the character. + // If we are, keep the target (unless we choose another). + if (SelectedAiTarget?.Entity != targetCharacter) + { + foreach (var gap in Character.CurrentHull.ConnectedGaps) + { + var door = gap.ConnectedDoor; + if (door == null || !door.IsOpen) + { + var wall = gap.ConnectedWall; + if (wall != null) + { + for (int j = 0; j < wall.Sections.Length; j++) + { + WallSection section = wall.Sections[j]; + if (!CanPassThroughHole(wall, j) && section?.gap != null) + { + continue; + } + } + } + } + } + } + } + } + } newTarget = aiTarget; selectedTargetMemory = targetMemory; targetValue = valueModifier; @@ -1867,6 +1913,39 @@ namespace Barotrauma private readonly Dictionary modifiedParams = new Dictionary(); private readonly Dictionary tempParams = new Dictionary(); + private void ChangeParams(string tag, AIState state, float? priority = null, bool onlyExisting = false) + { + if (!AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams)) + { + if (!onlyExisting && !tempParams.ContainsKey(tag)) + { + if (AIParams.TryAddNewTarget(tag, state, priority ?? 100, out targetParams)) + { + tempParams.Add(tag, targetParams); + } + } + } + if (targetParams != null) + { + if (priority.HasValue) + { + targetParams.Priority = priority.Value; + } + targetParams.State = state; + if (!modifiedParams.ContainsKey(tag)) + { + modifiedParams.Add(tag, targetParams); + } + } + } + + private void ChangeTargetState(string tag, AIState state, float? priority = null) + { + isStateChanged = true; + SetStateResetTimer(); + ChangeParams(tag, state, priority); + } + /// /// Temporarily changes the predefined state for a target. Eg. Idle -> Attack. /// @@ -1874,50 +1953,27 @@ namespace Barotrauma { isStateChanged = true; SetStateResetTimer(); - ChangeParams(target.SpeciesName); - // Target also items, because if we are blind and the target doesn't move, we can only perceive the target when it uses items - if (state == AIState.Attack || state == AIState.Escape) + ChangeParams(target.SpeciesName, state, priority); + if (target.IsHuman) { - ChangeParams("weapon"); - ChangeParams("tool"); - } - if (state == AIState.Attack) - { - // If the target is shooting from the submarine, we might not perceive it because it doesn't move. - // --> Target the submarine too. - if (target.Submarine != null && canAttackSub) + // Target also items, because if we are blind and the target doesn't move, we can only perceive the target when it uses items + if (state == AIState.Attack || state == AIState.Escape) { - ChangeParams("room"); - ChangeParams("wall"); - ChangeParams("door"); + ChangeParams("weapon", state, priority); + ChangeParams("tool", state, priority); } - ChangeParams("provocative", onlyExisting: true); - ChangeParams("light", onlyExisting: true); - } - - void ChangeParams(string tag, bool onlyExisting = false) - { - if (!AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams)) + if (state == AIState.Attack) { - if (!onlyExisting && !tempParams.ContainsKey(tag)) + // If the target is shooting from the submarine, we might not perceive it because it doesn't move. + // --> Target the submarine too. + if (target.Submarine != null && canAttackSub) { - if (AIParams.TryAddNewTarget(tag, state, priority ?? 100, out targetParams)) - { - tempParams.Add(tag, targetParams); - } - } - } - if (targetParams != null) - { - if (priority.HasValue) - { - targetParams.Priority = priority.Value; - } - targetParams.State = state; - if (!modifiedParams.ContainsKey(tag)) - { - modifiedParams.Add(tag, targetParams); + ChangeParams("room", state, priority); + ChangeParams("wall", state, priority); + ChangeParams("door", state, priority); } + ChangeParams("provocative", state, priority, onlyExisting: true); + ChangeParams("light", state, priority, onlyExisting: true); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 0d685c7cc..6e6a5fbc9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -74,7 +74,7 @@ namespace Barotrauma public override void Update(float deltaTime) { - if (DisableCrewAI || Character.IsUnconscious || Character.Removed) { return; } + if (DisableCrewAI || Character.IsIncapacitated || Character.Removed) { return; } base.Update(deltaTime); if (unreachableClearTimer > 0) @@ -139,7 +139,10 @@ namespace Barotrauma } if (Character.SpeechImpediment < 100.0f) { - ReportProblems(); + if (Character.Submarine != null && Character.Submarine.TeamID == Character.TeamID && !Character.Submarine.Info.IsWreck) + { + ReportProblems(); + } UpdateSpeaking(); } UnequipUnnecessaryItems(); @@ -170,12 +173,7 @@ namespace Barotrauma } } } - if (run) - { - run = !AnimController.Crouching && !AnimController.IsMovingBackwards; - } - float currentSpeed = Character.AnimController.GetCurrentSpeed(run); - steeringManager.Update(currentSpeed); + steeringManager.Update(Character.AnimController.GetCurrentSpeed(run && Character.CanRun)); bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); @@ -209,17 +207,6 @@ namespace Barotrauma targetMovement = new Vector2(Character.AnimController.TargetMovement.X, MathHelper.Clamp(Character.AnimController.TargetMovement.Y, -1.0f, 1.0f)); } - float maxSpeed = Character.ApplyTemporarySpeedLimits(currentSpeed); - targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed); - targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed); - - //apply speed multiplier if - // a. it's boosting the movement speed and the character is trying to move fast (= running) - // b. it's a debuff that decreases movement speed - float speedMultiplier = Character.SpeedMultiplier; - if (run || speedMultiplier <= 0.0f) targetMovement *= speedMultiplier; - Character.ResetSpeedMultiplier(); // Reset, items will set the value before the next update - if (Character.AnimController.InWater && targetMovement.LengthSquared() < 0.000001f) { bool isAiming = false; @@ -243,7 +230,7 @@ namespace Barotrauma } } - Character.AnimController.TargetMovement = targetMovement; + Character.AnimController.TargetMovement = Character.ApplyMovementLimits(targetMovement, AnimController.GetCurrentSpeed(run)); flipTimer -= deltaTime; if (flipTimer <= 0.0f) @@ -323,7 +310,7 @@ namespace Barotrauma || ObjectiveManager.IsCurrentObjective() || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn); bool removeDivingSuit = !Character.AnimController.HeadInWater && oxygenLow; - AIObjectiveGoTo gotoObjective = ObjectiveManager.CurrentOrder as AIObjectiveGoTo; + AIObjectiveGoTo gotoObjective = ObjectiveManager.GetActiveObjective(); if (!removeDivingSuit) { bool targetHasNoSuit = gotoObjective != null && gotoObjective.mimic && !HasDivingSuit(gotoObjective.Target as Character); @@ -536,14 +523,16 @@ namespace Barotrauma Hull targetHull = null; if (Character.CurrentHull != null) { + bool isFighting = ObjectiveManager.HasActiveObjective(); + bool isFleeing = ObjectiveManager.HasActiveObjective(); foreach (var hull in VisibleHulls) { - foreach (Character c in Character.CharacterList) + foreach (Character target in Character.CharacterList) { - if (c.CurrentHull != hull || !c.Enabled) { continue; } - if (AIObjectiveFightIntruders.IsValidTarget(c, Character)) + if (target.CurrentHull != hull || !target.Enabled) { continue; } + if (AIObjectiveFightIntruders.IsValidTarget(target, Character)) { - if (AddTargets(Character, c) && newOrder == null) + if (AddTargets(Character, target) && newOrder == null) { var orderPrefab = Order.GetPrefab("reportintruders"); newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); @@ -560,42 +549,48 @@ namespace Barotrauma targetHull = hull; } } - foreach (Character c in Character.CharacterList) + if (!isFighting) { - if (c.CurrentHull != hull) { continue; } - if (AIObjectiveRescueAll.IsValidTarget(c, Character)) + foreach (var gap in hull.ConnectedGaps) { - if (AddTargets(c, Character) && newOrder == null && !ObjectiveManager.HasActiveObjective()) + if (AIObjectiveFixLeaks.IsValidTarget(gap, Character)) { - var orderPrefab = Order.GetPrefab("requestfirstaid"); - newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); - targetHull = hull; + if (AddTargets(Character, gap) && newOrder == null && !gap.IsRoomToRoom) + { + var orderPrefab = Order.GetPrefab("reportbreach"); + newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); + targetHull = hull; + } } } - } - foreach (var gap in hull.ConnectedGaps) - { - if (AIObjectiveFixLeaks.IsValidTarget(gap, Character)) + if (!isFleeing) { - if (AddTargets(Character, gap) && newOrder == null && !gap.IsRoomToRoom) + foreach (Character target in Character.CharacterList) { - var orderPrefab = Order.GetPrefab("reportbreach"); - newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); - targetHull = hull; + if (target.CurrentHull != hull) { continue; } + if (AIObjectiveRescueAll.IsValidTarget(target, Character)) + { + if (AddTargets(Character, target) && newOrder == null && !ObjectiveManager.HasActiveObjective()) + { + var orderPrefab = Order.GetPrefab("requestfirstaid"); + newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); + targetHull = hull; + } + } } - } - } - foreach (Item item in Item.ItemList) - { - if (item.CurrentHull != hull) { continue; } - if (AIObjectiveRepairItems.IsValidTarget(item, Character)) - { - if (item.Repairables.All(r => item.ConditionPercentage > r.AIRepairThreshold)) { continue; } - if (AddTargets(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective()) + foreach (Item item in Item.ItemList) { - var orderPrefab = Order.GetPrefab("reportbrokendevices"); - newOrder = new Order(orderPrefab, hull, item.Repairables?.FirstOrDefault(), orderGiver: Character); - targetHull = hull; + if (item.CurrentHull != hull) { continue; } + if (AIObjectiveRepairItems.IsValidTarget(item, Character)) + { + if (item.Repairables.All(r => item.ConditionPercentage > r.AIRepairThreshold)) { continue; } + if (AddTargets(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective()) + { + var orderPrefab = Order.GetPrefab("reportbrokendevices"); + newOrder = new Order(orderPrefab, hull, item.Repairables?.FirstOrDefault(), orderGiver: Character); + targetHull = hull; + } + } } } } @@ -650,7 +645,7 @@ namespace Barotrauma // Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate). return; } - if (!attacker.IsRemotePlayer && Character.Controlled != attacker && attacker.AIController != null && attacker.AIController.Enabled) + if (!attacker.IsPlayer && attacker.AIController != null && attacker.AIController.Enabled) { // Don't retaliate on damage done by friendly ai, because we know that it's accidental AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced)); @@ -664,9 +659,8 @@ namespace Barotrauma } else { - float currentVitality = Character.CharacterHealth.Vitality; - float dmgPercentage = damage / currentVitality * 100; - if (dmgPercentage < currentVitality / 10) + float dmgPercentage = MathUtils.Percentage(damage, Character.CharacterHealth.Vitality); + if (dmgPercentage < 10) { // Don't retaliate on minor (accidental) dmg done by characters that are in the same team AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced)); @@ -711,7 +705,6 @@ namespace Barotrauma public void SetOrder(Order order, string option, Character orderGiver, bool speak = true) { - SetOrderProjSpecific(order, option); CurrentOrderOption = option; CurrentOrder = order; objectiveManager.SetOrder(order, option, orderGiver); @@ -752,8 +745,6 @@ namespace Barotrauma } } - partial void SetOrderProjSpecific(Order order, string option); - public override void SelectTarget(AITarget target) { SelectedAiTarget = target; @@ -806,11 +797,11 @@ namespace Barotrauma /// public static bool HasDivingMask(Character character, float conditionPercentage = 0) => HasItem(character, "divingmask", "oxygensource", conditionPercentage); - public static bool HasItem(Character character, string identifier, string containedTag, float conditionPercentage = 0) + public static bool HasItem(Character character, string tagOrIdentifier, string containedTag = null, float conditionPercentage = 0) { if (character == null) { return false; } if (character.Inventory == null) { return false; } - var item = character.Inventory.FindItemByIdentifier(identifier) ?? character.Inventory.FindItemByTag(identifier); + var item = character.Inventory.FindItemByIdentifier(tagOrIdentifier) ?? character.Inventory.FindItemByTag(tagOrIdentifier); return item != null && item.ConditionPercentage > conditionPercentage && character.HasEquippedItem(item) && @@ -934,7 +925,7 @@ namespace Barotrauma visibleHulls = VisibleHulls; } // TODO: should we calculate the visible hulls for each hull? -> could be a bit heavy. - bool ignoreFire = ObjectiveManager.IsCurrentObjective() || objectiveManager.HasActiveObjective(); + bool ignoreFire = objectiveManager.HasActiveObjective(); bool ignoreWater = HasDivingSuit(character); bool ignoreOxygen = ignoreWater || HasDivingMask(character); bool ignoreEnemies = ObjectiveManager.IsCurrentObjective(); @@ -1022,15 +1013,23 @@ namespace Barotrauma return false; } - public static int CountCrew(Character character, Func predicate = null) + public static int CountCrew(Character character, Func predicate = null, bool onlyActive = true, bool onlyBots = false) { if (character == null) { return 0; } int count = 0; - foreach (var c in Character.CharacterList) + foreach (var other in Character.CharacterList) { - if (FilterCrewMember(character, c)) + if (onlyActive && !IsActive(other)) { - if (predicate == null || predicate(c.AIController as HumanAIController)) + continue; + } + if (onlyBots && other.IsPlayer) + { + continue; + } + if (FilterCrewMember(character, other)) + { + if (predicate == null || predicate(other.AIController as HumanAIController)) { count++; } @@ -1053,12 +1052,61 @@ namespace Barotrauma private static bool FilterCrewMember(Character self, Character other) => other != null && !other.IsDead && !other.Removed && other.AIController is HumanAIController humanAi && humanAi.IsFriendly(self); + public static bool IsItemOperatedByAnother(Character character, ItemComponent target, out Character operatingCharacter) + { + operatingCharacter = null; + foreach (var c in Character.CharacterList) + { + if (character != null) + { + if (c == character) { continue; } + if (!IsFriendly(character, c)) { continue; } + } + if (c.SelectedConstruction != target.Item) { continue; } + operatingCharacter = c; + // If the other character is player, don't try to operate + if (c.IsRemotePlayer || Character.Controlled == c) { return true; } + if (c.AIController is HumanAIController controllingHumanAi) + { + // If the other character is ordered to operate the item, let him do it + if (controllingHumanAi.ObjectiveManager.IsCurrentOrder()) + { + return true; + } + else + { + if (character == null) + { + return true; + } + else if (target is Steering) + { + // Steering is hard-coded -> cannot use the required skills collection defined in the xml + return character.GetSkillLevel("helm") <= c.GetSkillLevel("helm"); + } + else + { + return target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c); + } + } + } + else + { + // Shouldn't go here, unless we allow non-humans to operate items + return false; + } + + } + return false; + } + #region Wrappers public bool IsFriendly(Character other) => IsFriendly(Character, other); public void DoForEachCrewMember(Action action) => DoForEachCrewMember(Character, action); public bool IsTrueForAnyCrewMember(Func predicate) => IsTrueForAnyCrewMember(Character, predicate); public bool IsTrueForAllCrewMembers(Func predicate) => IsTrueForAllCrewMembers(Character, predicate); - public int CountCrew(Func predicate = null) => CountCrew(Character, predicate); + public int CountCrew(Func predicate = null, bool onlyActive = true, bool onlyBots = false) => CountCrew(Character, predicate, onlyActive, onlyBots); + public bool IsItemOperatedByAnother(ItemComponent target, out Character operatingCharacter) => IsItemOperatedByAnother(Character, target, out operatingCharacter); #endregion } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index e0cd0198f..6628d279f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -134,7 +134,7 @@ namespace Barotrauma private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null) { - bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || currentPath.NextNode == null || Vector2.DistanceSquared(target, currentTarget) > 1); + bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || currentPath.Finished || Vector2.DistanceSquared(target, currentTarget) > 1); //find a new path if one hasn't been found yet or the target is different from the current target if (needsNewPath || findPathTimer < -1.0f) { @@ -308,7 +308,7 @@ namespace Barotrauma currentPath.SkipToNextNode(); } } - else + else if (!IsNextLadderSameAsCurrent) { Vector2 colliderBottom = character.AnimController.GetColliderBottom(); Vector2 colliderSize = collider.GetSize(); @@ -530,7 +530,6 @@ namespace Barotrauma if (node.Waypoint != null && node.Waypoint.CurrentHull != null) { var hull = node.Waypoint.CurrentHull; - if (hull.FireSources.Count > 0) { foreach (FireSource fs in hull.FireSources) @@ -538,9 +537,14 @@ namespace Barotrauma penalty += fs.Size.X * 10.0f; } } - - if (character.NeedsAir && hull.WaterVolume / hull.Rect.Width > 100.0f) penalty += 500.0f; - if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume) penalty += 1000.0f; + if (character.NeedsAir && hull.WaterVolume / hull.Rect.Width > 100.0f) + { + penalty += 500.0f; + } + if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume) + { + penalty += 1000.0f; + } } return penalty; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index dc4c1b926..68746f340 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -253,7 +253,7 @@ namespace Barotrauma jointDir = attachLimb.Dir; - Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.character.AnimController.RagdollParams.LimbScale; + Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.Scale * attachLimb.Params.Ragdoll.LimbScale; if (jointDir < 0.0f) transformedLocalAttachPos.X = -transformedLocalAttachPos.X; //transformedLocalAttachPos = Vector2.Transform(transformedLocalAttachPos, Matrix.CreateRotationZ(attachLimb.Rotation)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index ece8f6cf3..c5ab0ea33 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -136,9 +136,10 @@ namespace Barotrauma string allowedJobsStr = element.GetAttributeString("allowedjobs", ""); foreach (string allowedJobIdentifier in allowedJobsStr.Split(',')) { - if (JobPrefab.Prefabs.ContainsKey(allowedJobIdentifier.ToLowerInvariant())) + string key = allowedJobIdentifier.ToLowerInvariant(); + if (JobPrefab.Prefabs.ContainsKey(key)) { - AllowedJobs.Add(JobPrefab.Prefabs[allowedJobIdentifier.ToLowerInvariant()]); + AllowedJobs.Add(JobPrefab.Prefabs[key]); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index c71d9043d..581ef4f49 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -32,6 +32,18 @@ namespace Barotrauma public virtual bool UnequipItems => false; protected readonly List subObjectives = new List(); + private float _cumulatedDevotion; + protected float CumulatedDevotion + { + get { return _cumulatedDevotion; } + set { _cumulatedDevotion = MathHelper.Clamp(value, 0, MaxDevotion); } + } + + protected virtual float MaxDevotion => 10; + + /// + /// Final priority value after all calculations. + /// public float Priority { get; set; } public float PriorityModifier { get; private set; } = 1; public readonly Character character; @@ -59,6 +71,7 @@ namespace Barotrauma /// public virtual bool IsLoop { get; set; } public IEnumerable SubObjectives => subObjectives; + public AIObjective CurrentSubObjective => subObjectives.FirstOrDefault(); private readonly List all = new List(); public IEnumerable GetSubObjectivesRecursive(bool includingSelf = false) @@ -86,7 +99,7 @@ namespace Barotrauma public AIObjective GetActiveObjective() { - var subObjective = SubObjectives.FirstOrDefault(); + var subObjective = CurrentSubObjective; return subObjective == null ? this : subObjective.GetActiveObjective(); } @@ -157,7 +170,8 @@ namespace Barotrauma { if (!AllowSubObjectiveSorting) { return; } if (subObjectives.None()) { return; } - subObjectives.Sort((x, y) => y.GetPriority().CompareTo(x.GetPriority())); + subObjectives.ForEach(so => so.GetPriority()); + subObjectives.Sort((x, y) => y.Priority.CompareTo(x.Priority)); if (ConcurrentObjectives) { subObjectives.ForEach(so => so.SortSubObjectives()); @@ -168,7 +182,23 @@ namespace Barotrauma } } - public virtual float GetPriority() => Priority * PriorityModifier; + /// + /// Call this only when the priority needs to be recalculated. Use the cached Priority property when you don't need to recalculate. + /// + public virtual float GetPriority() + { + Priority = CumulatedDevotion * PriorityModifier; + return Priority; + } + + private void UpdateDevotion(float deltaTime) + { + var currentObjective = objectiveManager.CurrentObjective; + if (currentObjective != null && (currentObjective == this || currentObjective.subObjectives.Any(so => so == this))) + { + CumulatedDevotion += Devotion * PriorityModifier * deltaTime; + } + } public virtual bool IsDuplicate(T otherObjective) where T : AIObjective => otherObjective.Option == Option; @@ -180,14 +210,7 @@ namespace Barotrauma } else if (objectiveManager.WaitTimer <= 0) { - if (objectiveManager.CurrentObjective != null) - { - if (objectiveManager.CurrentObjective == this || objectiveManager.CurrentObjective.subObjectives.Any(so => so == this)) - { - Priority += Devotion * PriorityModifier * deltaTime; - } - } - Priority = MathHelper.Clamp(Priority, 0, 100); + UpdateDevotion(deltaTime); } subObjectives.ForEach(so => so.Update(deltaTime)); } @@ -264,6 +287,7 @@ namespace Barotrauma public virtual void OnDeselected() { + CumulatedDevotion = 0; Deselected?.Invoke(); } @@ -282,6 +306,7 @@ namespace Barotrauma isCompleted = false; hasBeenChecked = false; _abandon = false; + CumulatedDevotion = 0; } protected abstract void Act(float deltaTime); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index aff1bf4b7..123530a7e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -100,7 +100,11 @@ namespace Barotrauma } } - public override float GetPriority() => (Enemy != null && (Enemy.Removed || Enemy.IsDead)) ? 0 : Math.Min(100 * PriorityModifier, 100); + public override float GetPriority() + { + Priority = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) ? 0 : Math.Min(100 * PriorityModifier, 100); + return Priority; + } public override void Update(float deltaTime) { @@ -139,18 +143,18 @@ namespace Barotrauma } if (seekAmmunition == null) { - if (TryArm() && Enemy != null && !Enemy.Removed) + if (Mode != CombatMode.Retreat && TryArm() && Enemy != null && !Enemy.Removed) { OperateWeapon(deltaTime); } if (!HoldPosition && seekAmmunition == null) { - Move(); + Move(deltaTime); } } } - private void Move() + private void Move(float deltaTime) { switch (Mode) { @@ -159,7 +163,7 @@ namespace Barotrauma break; case CombatMode.Defensive: case CombatMode.Retreat: - Retreat(); + Retreat(deltaTime); break; default: throw new NotImplementedException(); @@ -407,7 +411,10 @@ namespace Barotrauma return true; } - private void Retreat() + private float findHullTimer; + private readonly float findHullInterval = 1.0f; + + private void Retreat(float deltaTime) { RemoveSubObjective(ref followTargetObjective); RemoveSubObjective(ref seekAmmunition); @@ -417,7 +424,15 @@ namespace Barotrauma } if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted)) { - retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls); + if (findHullTimer > 0) + { + findHullTimer -= deltaTime; + } + else + { + retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls); + findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f); + } } if (retreatTarget != null && character.CurrentHull != retreatTarget) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 15959d86f..a77455b67 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -74,15 +74,6 @@ namespace Barotrauma } } - public override float GetPriority() - { - if (objectiveManager.CurrentOrder == this) - { - return AIObjectiveManager.OrderPriority; - } - return 1.0f; - } - private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage > ConditionLevel; protected override void Act(float deltaTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index 63a86fea7..f13e6e548 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -54,15 +54,6 @@ namespace Barotrauma protected override bool Check() => IsCompleted; - public override float GetPriority() - { - if (objectiveManager.CurrentOrder == this) - { - return AIObjectiveManager.OrderPriority; - } - return 1.0f; - } - protected override void Act(float deltaTime) { Item itemToDecontain = targetItem ?? sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)), recursive: false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 9dcb5c3a6..8ccdf33c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -1,5 +1,4 @@ using Barotrauma.Items.Components; -using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Linq; @@ -29,32 +28,44 @@ namespace Barotrauma public override float GetPriority() { if (!objectiveManager.IsCurrentOrder() - && Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return 0; } - float yDist = Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y); - 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) + && Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { - distanceFactor = 1; + Priority = 0; } - float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull); - float severityFactor = MathHelper.Lerp(0, 1, severity / 100); - float devotion = Math.Min(Priority, 10) / 100; - return MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + severityFactor * distanceFactor, 0, 1)); + else + { + float yDist = Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y); + 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) + { + 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)); + } + return Priority; } protected override bool Check() => targetHull.FireSources.None(); + private float sinTime; protected override void Act(float deltaTime) { - var extinguisherItem = character.Inventory.FindItemByIdentifier("extinguisher") ?? character.Inventory.FindItemByTag("extinguisher"); + var extinguisherItem = character.Inventory.FindItemByIdentifier("fireextinguisher") ?? character.Inventory.FindItemByTag("fireextinguisher"); if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem)) { TryAddSubObjective(ref getExtinguisherObjective, () => { character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f); - return new AIObjectiveGetItem(character, "extinguisher", objectiveManager, equip: true); + return new AIObjectiveGetItem(character, "fireextinguisher", objectiveManager, equip: true) + { + // If the item is inside an unsafe hull, decrease the priority + GetItemPriority = i => HumanAIController.UnsafeHulls.Contains(i.CurrentHull) ? 0.1f : 1 + }; }); } else @@ -79,8 +90,12 @@ namespace Barotrauma { useExtinquisherTimer = 0.0f; } + // Aim character.CursorPosition = fs.Position; - if (extinguisher.Item.RequireAimToUse) + Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition; + float dist = fromCharacterToFireSource.Length(); + character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2); + if (extinguisherItem.RequireAimToUse) { bool isOperatingButtons = false; if (SteeringManager == PathSteering) @@ -95,8 +110,9 @@ namespace Barotrauma { character.SetInput(InputType.Aim, false, true); } + sinTime += deltaTime * 10; } - character.SetInput(extinguisher.Item.IsShootable ? InputType.Shoot : InputType.Use, false, true); + character.SetInput(extinguisherItem.IsShootable ? InputType.Shoot : InputType.Use, false, true); extinguisher.Use(deltaTime, character); if (!targetHull.FireSources.Contains(fs)) { @@ -110,7 +126,7 @@ namespace Barotrauma if (move) { //go to the first firesource - TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager) + TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range / 2) { DialogueIdentifier = "dialogcannotreachfire", TargetName = fs.Hull.DisplayName diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs index f5142278c..fd6bc49ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs @@ -9,7 +9,6 @@ namespace Barotrauma { public override string DebugTag => "extinguish fires"; public override bool ForceRun => true; - public override bool IgnoreUnsafeHulls => true; public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index c41580047..9b851aefb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -9,7 +9,6 @@ namespace Barotrauma public override string DebugTag => $"find diving gear ({gearTag})"; public override bool ForceRun => true; public override bool KeepDivingGearOn => true; - public override bool IgnoreUnsafeHulls => true; private readonly string gearTag; private readonly string fallbackTag; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 1676bffc4..63eb1372b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -33,6 +33,8 @@ namespace Barotrauma private bool resetPriority; + public override float GetPriority() => Priority; + public override void Update(float deltaTime) { if (resetPriority) @@ -252,7 +254,7 @@ namespace Barotrauma else { // Outside - if (hull.RoomName != null && hull.RoomName.ToLowerInvariant().Contains("airlock")) + if (hull.RoomName != null && hull.RoomName.Contains("airlock", StringComparison.OrdinalIgnoreCase)) { hullSafety = 100; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 9467b940c..4121ccdce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -29,16 +29,23 @@ namespace Barotrauma public override float GetPriority() { - if (Leak.Removed || Leak.Open <= 0) { return 0; } - float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X); - float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y); - // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally). - // If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby. - float distanceFactor = xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, xDist + yDist * 3.0f)); - float severity = AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; - float max = Math.Min((AIObjectiveManager.OrderPriority - 1), 90); - float devotion = Math.Min(Priority, 10) / 100; - return MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + severity * distanceFactor * PriorityModifier, 0, 1)); + if (Leak.Removed || Leak.Open <= 0) + { + Priority = 0; + } + else + { + float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X); + float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y); + // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally). + // If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby. + float distanceFactor = xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, xDist + yDist * 3.0f)); + float severity = AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; + float max = Math.Min((AIObjectiveManager.OrderPriority - 1), 90); + float devotion = CumulatedDevotion / 100; + Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); + } + return Priority; } protected override void Act(float deltaTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index 5e35d9135..659958632 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -11,7 +11,6 @@ namespace Barotrauma public override string DebugTag => "fix leaks"; public override bool ForceRun => true; public override bool KeepDivingGearOn => true; - public override bool IgnoreUnsafeHulls => true; public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } @@ -36,7 +35,7 @@ namespace Barotrauma protected override float TargetEvaluation() { - int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective()); + int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective(), onlyBots: true); int totalLeaks = Targets.Count(); if (totalLeaks == 0) { return 0; } int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom); @@ -44,13 +43,13 @@ namespace Barotrauma bool anyFixers = otherFixers > 0; if (objectiveManager.CurrentOrder == this) { - float ratio = anyFixers ? totalLeaks / otherFixers : 1; + float ratio = anyFixers ? totalLeaks / (float)otherFixers : 1; return Targets.Sum(t => GetLeakSeverity(t)) * ratio; } else { float ratio = leaks == 0 ? 1 : anyFixers ? leaks / otherFixers : 1; - if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / HumanAIController.CountCrew() > 0.75f)) + if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f)) { // Enough fixers return 0; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 27950b88d..626c49064 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -31,15 +31,6 @@ namespace Barotrauma public bool AllowToFindDivingGear { get; set; } = true; - public override float GetPriority() - { - if (objectiveManager.CurrentOrder == this) - { - return AIObjectiveManager.OrderPriority; - } - return 1.0f; - } - public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index f76dbdd71..844343053 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -51,14 +51,19 @@ namespace Barotrauma public override float GetPriority() { - if (followControlledCharacter && Character.Controlled == null) { return 0.0f; } - if (Target is Entity e && e.Removed) { return 0.0f; } - if (IgnoreIfTargetDead && Target is Character character && character.IsDead) { return 0.0f; } - if (objectiveManager.CurrentOrder == this) + if (followControlledCharacter && Character.Controlled == null) { - return AIObjectiveManager.OrderPriority; + Priority = 0; } - return 1.0f; + if (Target is Entity e && e.Removed) + { + Priority = 0; + } + if (IgnoreIfTargetDead && Target is Character character && character.IsDead) + { + Priority = 0; + } + return objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : Priority; } public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 998691f95..0d9a61f81 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -45,20 +45,17 @@ namespace Barotrauma private float randomUpdateInterval = 5; public float Random { get; private set; } - public void SetRandom() + public void CalculatePriority() { Random = Rand.Range(0.5f, 1.5f); randomTimer = randomUpdateInterval; - } - - public override float GetPriority() - { float max = Math.Min(Math.Min(AIObjectiveManager.RunPriority, AIObjectiveManager.OrderPriority) - 1, 100); float initiative = character.GetSkillLevel("initiative"); Priority = MathHelper.Lerp(1, max, MathUtils.InverseLerp(100, 0, initiative * Random)); - return Priority; } + public override float GetPriority() => Priority; + public override void Update(float deltaTime) { if (objectiveManager.CurrentObjective == this) @@ -69,7 +66,7 @@ namespace Barotrauma } else { - SetRandom(); + CalculatePriority(); } } } @@ -182,7 +179,7 @@ namespace Barotrauma if (!character.IsClimbing) { if (SteeringManager != PathSteering || (PathSteering.CurrentPath != null && - (PathSteering.CurrentPath.NextNode == null || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes))) + (PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes))) { Wander(deltaTime); return; @@ -264,9 +261,9 @@ namespace Barotrauma public static bool IsForbidden(Hull hull) { if (hull == null) { return true; } - string hullName = hull.RoomName?.ToLowerInvariant(); + string hullName = hull.RoomName; if (hullName == null) { return false; } - return hullName.Contains("ballast") || hullName.Contains("airlock"); + return hullName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || hullName.Contains("airlock", StringComparison.OrdinalIgnoreCase); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index 61ce5ef6a..8c2a4b340 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -45,6 +45,7 @@ namespace Barotrauma public override bool CanBeCompleted => true; public override bool AbandonWhenCannotCompleteSubjectives => false; public override bool AllowSubObjectiveSorting => true; + public virtual bool InverseTargetEvaluation => false; public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); } @@ -107,21 +108,46 @@ namespace Barotrauma public override float GetPriority() { - if (character.LockHands) { return 0; } - if (character.Submarine == null) { return 0; } - if (Targets.None()) { return 0; } - // Allow the target value to be more than 100. - float targetValue = TargetEvaluation(); - // If the target value is less than 1% of the max value, let's just treat it as zero. - if (targetValue < 1) { return 0; } - if (objectiveManager.CurrentOrder == this) + if (character.LockHands || character.Submarine == null || Targets.None()) { - return AIObjectiveManager.OrderPriority; + Priority = 0; } - float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90); - float devotion = MathHelper.Min(10, Priority); - float value = MathHelper.Clamp((devotion + targetValue * PriorityModifier) / 100, 0, 1); - return MathHelper.Lerp(0, max, value); + else + { + // Allow the target value to be more than 100. + float targetValue = TargetEvaluation(); + if (InverseTargetEvaluation) + { + targetValue = 100 - targetValue; + } + var currentSubObjective = CurrentSubObjective; + if (currentSubObjective != null && currentSubObjective.Priority > targetValue) + { + // If the priority is higher than the target value, let's just use it. + // The priority calculation is more precise, but it takes into account things like distances, + // so it's better not to use it if it's lower than the rougher targetValue. + targetValue = Priority; + } + // If the target value is less than 1% of the max value, let's just treat it as zero. + if (targetValue < 1) + { + Priority = 0; + } + else + { + if (objectiveManager.CurrentOrder == this) + { + Priority = AIObjectiveManager.OrderPriority; + } + else + { + float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90); + float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1); + Priority = MathHelper.Lerp(0, max, value); + } + } + } + return Priority; } protected void UpdateTargets() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index cdf43f230..abe0f4052 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -41,6 +41,15 @@ namespace Barotrauma public bool IsActiveObjective() where T : AIObjective => GetActiveObjective() is T; public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective(); + /// + /// Returns the last active objective of the specific type. + /// + public T GetActiveObjective() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T; + + /// + /// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently. + /// + public IEnumerable GetActiveObjectives() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T); public bool HasActiveObjective() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T); @@ -146,7 +155,7 @@ namespace Barotrauma { var previousObjective = CurrentObjective; var firstObjective = Objectives.FirstOrDefault(); - if (CurrentOrder != null && firstObjective != null && CurrentOrder.GetPriority() > firstObjective.GetPriority()) + if (CurrentOrder != null && firstObjective != null && CurrentOrder.Priority > firstObjective.Priority) { CurrentObjective = CurrentOrder; } @@ -158,14 +167,14 @@ namespace Barotrauma { previousObjective?.OnDeselected(); CurrentObjective?.OnSelected(); - GetObjective().SetRandom(); + GetObjective().CalculatePriority(); } return CurrentObjective; } public float GetCurrentPriority() { - return CurrentObjective == null ? 0.0f : CurrentObjective.GetPriority(); + return CurrentObjective == null ? 0.0f : CurrentObjective.Priority; } public void UpdateObjectives(float deltaTime) @@ -205,7 +214,8 @@ namespace Barotrauma { if (Objectives.Any()) { - Objectives.Sort((x, y) => y.GetPriority().CompareTo(x.GetPriority())); + Objectives.ForEach(o => o.GetPriority()); + Objectives.Sort((x, y) => y.Priority.CompareTo(x.Priority)); } GetCurrentObjective()?.SortSubObjectives(); } @@ -297,7 +307,7 @@ namespace Barotrauma { IsLoop = true, // Don't override unless it's an order by a player - Override = orderGiver != null && (orderGiver == Character.Controlled || orderGiver.IsRemotePlayer) + Override = orderGiver != null && orderGiver.IsPlayer }; break; default: @@ -306,7 +316,7 @@ namespace Barotrauma { IsLoop = true, // Don't override unless it's an order by a player - Override = orderGiver != null && (orderGiver == Character.Controlled || orderGiver.IsRemotePlayer) + Override = orderGiver != null && orderGiver.IsPlayer }; break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index a726858d3..62dae935b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -3,6 +3,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -31,19 +32,33 @@ namespace Barotrauma public override float GetPriority() { - if (component.Item.ConditionPercentage <= 0) { return 0; } - if (objectiveManager.CurrentOrder == this) + if (component.Item.ConditionPercentage <= 0) { - return AIObjectiveManager.OrderPriority; + Priority = 0; } - if (component.Item.CurrentHull == null) { return 0; } - if (component.Item.CurrentHull.FireSources.Count > 0) { return 0; } - if (IsOperatedByAnother(GetTarget())) { return 0; } - if (Character.CharacterList.Any(c => c.CurrentHull == component.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return 0; } - float devotion = MathHelper.Min(10, Priority); - float value = devotion + AIObjectiveManager.OrderPriority * PriorityModifier; - float max = MathHelper.Min((AIObjectiveManager.OrderPriority - 1), 90); - return MathHelper.Clamp(value, 0, max); + else + { + if (objectiveManager.CurrentOrder == this) + { + Priority = AIObjectiveManager.OrderPriority; + } + + if (component.Item.CurrentHull == null || component.Item.CurrentHull.FireSources.Any() || HumanAIController.IsItemOperatedByAnother(GetTarget(), out _)) + { + Priority = 0; + } + else if (Character.CharacterList.Any(c => c.CurrentHull == component.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) + { + Priority = 0; + } + else + { + float value = CumulatedDevotion + (AIObjectiveManager.OrderPriority * PriorityModifier); + float max = MathHelper.Min((AIObjectiveManager.OrderPriority - 1), 90); + Priority = MathHelper.Clamp(value, 0, max); + } + } + return Priority; } public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip, Entity operateTarget = null, bool useController = false, float priorityModifier = 1) @@ -62,45 +77,6 @@ namespace Barotrauma } } - private bool IsOperatedByAnother(ItemComponent target) - { - foreach (var c in Character.CharacterList) - { - if (c == character) { continue; } - if (!HumanAIController.IsFriendly(c)) { continue; } - if (c.SelectedConstruction != target.Item) { continue; } - // If the other character is player, don't try to operate - if (c.IsRemotePlayer || Character.Controlled == c) { return true; } - if (c.AIController is HumanAIController humanAi) - { - // If the other character is ordered to operate the item, let him do it - if (humanAi.ObjectiveManager.IsCurrentOrder()) - { - return true; - } - else - { - if (target is Steering) - { - // Steering is hard-coded -> cannot use the required skills collection defined in the xml - return character.GetSkillLevel("helm") <= c.GetSkillLevel("helm"); - } - else - { - return target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c); - } - } - } - else - { - // Shouldn't go here, unless we allow non-humans to operate items - return false; - } - - } - return false; - } - protected override void Act(float deltaTime) { if (character.LockHands) @@ -116,7 +92,7 @@ namespace Barotrauma return; } // Don't allow to operate an item that someone with a better skills already operates, unless this is an order - if (objectiveManager.CurrentOrder != this && IsOperatedByAnother(target)) + if (objectiveManager.CurrentOrder != this && HumanAIController.IsItemOperatedByAnother(target, out _)) { // Don't abandon return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index 9d5b1058c..0921dc1ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -12,7 +12,6 @@ namespace Barotrauma public override string DebugTag => "pump water"; public override bool KeepDivingGearOn => true; public override bool UnequipItems => true; - public override bool IgnoreUnsafeHulls => true; private IEnumerable pumpList; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 20a892037..1e5971576 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -30,21 +30,28 @@ namespace Barotrauma { // TODO: priority list? // Ignore items that are being repaired by someone else. - if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character)) { return 0; } - 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; - float distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist)); - if (Item.CurrentHull == character.CurrentHull) + if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character)) { - distanceFactor = 1; + Priority = 0; } - float damagePriority = MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition); - float successFactor = MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character))); - float isSelected = IsRepairing ? 50 : 0; - float devotion = (Math.Min(Priority, 10) + isSelected) / 100; - float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90); - return MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + damagePriority * distanceFactor * successFactor * PriorityModifier, 0, 1)); + else + { + 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; + float distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist)); + if (Item.CurrentHull == character.CurrentHull) + { + distanceFactor = 1; + } + float damagePriority = MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition); + float successFactor = MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character))); + float isSelected = IsRepairing ? 50 : 0; + float devotion = (CumulatedDevotion + isSelected) / 100; + float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90); + Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (damagePriority * distanceFactor * successFactor * PriorityModifier), 0, 1)); + } + return Priority; } protected override bool Check() @@ -158,7 +165,7 @@ namespace Barotrauma } repairable.StopRepairing(character); } - else + else if (repairable.CurrentFixer != character) { repairable.StartRepairing(character, Repairable.FixActions.Repair); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index 28183a43c..f6df24810 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -81,18 +81,17 @@ namespace Barotrauma // Don't stop fixing until done return 100; } - int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective()); + int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective(), onlyBots: true); int items = Targets.Count; bool anyFixers = otherFixers > 0; - float ratio = anyFixers ? items / otherFixers : 1; - var result = ratio; + float ratio = anyFixers ? items / (float)otherFixers : 1; if (objectiveManager.CurrentOrder == this) { return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio; } else { - if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / HumanAIController.CountCrew() > 0.75f)) + if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f)) { // Enough fixers return 0; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 2a8b6c756..a44ce0ed8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -14,7 +14,7 @@ namespace Barotrauma const float TreatmentDelay = 0.5f; - const float CloseEnoughToTreat = 150.0f; + const float CloseEnoughToTreat = 100.0f; private readonly Character targetCharacter; @@ -22,6 +22,8 @@ namespace Barotrauma private AIObjectiveGetItem getItemObjective; private float treatmentTimer; private Hull safeHull; + private float findHullTimer; + private readonly float findHullInterval = 1.0f; public AIObjectiveRescue(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) @@ -44,11 +46,20 @@ namespace Barotrauma Abandon = true; return; } + if (targetCharacter.SelectedBy != null && targetCharacter.SelectedBy != character) + { + var otherCharacter = character.SelectedBy; + if (otherCharacter != null) + { + // Someone else is rescuing/holding the target. + Abandon = otherCharacter.IsPlayer || character.GetSkillLevel("medical") < otherCharacter.GetSkillLevel("medical"); + } + } if (targetCharacter != character) { - // Unconcious target is not in a safe place -> Move to a safe place first - if (targetCharacter.IsUnconscious && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD) + // Incapacitated target is not in a safe place -> Move to a safe place first + if (targetCharacter.IsIncapacitated && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD) { if (character.SelectedCharacter != targetCharacter) { @@ -67,7 +78,11 @@ namespace Barotrauma TargetName = targetCharacter.DisplayName }, onCompleted: () => RemoveSubObjective(ref goToObjective), - onAbandon: () => RemoveSubObjective(ref goToObjective)); + onAbandon: () => + { + RemoveSubObjective(ref goToObjective); + Abandon = true; + }); } else { @@ -79,14 +94,26 @@ namespace Barotrauma // Drag the character into safety if (safeHull == null) { - safeHull = objectiveManager.GetObjective().FindBestHull(HumanAIController.VisibleHulls); + if (findHullTimer > 0) + { + findHullTimer -= deltaTime; + } + else + { + safeHull = objectiveManager.GetObjective().FindBestHull(HumanAIController.VisibleHulls); + findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f); + } } - if (character.CurrentHull != safeHull) + if (safeHull != null && character.CurrentHull != safeHull) { RemoveSubObjective(ref goToObjective); TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager), onCompleted: () => RemoveSubObjective(ref goToObjective), - onAbandon: () => RemoveSubObjective(ref goToObjective)); + onAbandon: () => + { + RemoveSubObjective(ref goToObjective); + safeHull = character.CurrentHull; + }); } } } @@ -105,7 +132,11 @@ namespace Barotrauma TargetName = targetCharacter.DisplayName }, onCompleted: () => RemoveSubObjective(ref goToObjective), - onAbandon: () => RemoveSubObjective(ref goToObjective)); + onAbandon: () => + { + RemoveSubObjective(ref goToObjective); + Abandon = true; + }); } else { @@ -127,6 +158,11 @@ namespace Barotrauma private Dictionary currentTreatmentSuitabilities = new Dictionary(); private void GiveTreatment(float deltaTime) { + if (!targetCharacter.IsPlayer) + { + // If the target is a bot, don't let it move + targetCharacter.AIController?.SteeringManager.Reset(); + } if (treatmentTimer > 0.0f) { treatmentTimer -= deltaTime; @@ -137,9 +173,8 @@ namespace Barotrauma //find which treatments are the most suitable to treat the character's current condition targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, normalize: false); - var allAfflictions = GetVitalityReducingAfflictions(targetCharacter).OrderByDescending(a => a.GetVitalityDecrease(targetCharacter.CharacterHealth)); //check if we already have a suitable treatment for any of the afflictions - foreach (Affliction affliction in allAfflictions) + foreach (Affliction affliction in GetSortedAfflictions(targetCharacter)) { foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitability) { @@ -200,10 +235,12 @@ namespace Barotrauma onAbandon: () => RemoveSubObjective(ref getItemObjective)); } } - character.AnimController.Anim = AnimController.Animation.CPR; + if (character != targetCharacter) + { + character.AnimController.Anim = AnimController.Animation.CPR; + } } - private void ApplyTreatment(Affliction affliction, Item item) { var targetLimb = targetCharacter.CharacterHealth.GetAfflictionLimb(affliction); @@ -240,7 +277,7 @@ namespace Barotrauma Abandon = true; return false; } - bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) > AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager); + bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter); if (isCompleted && targetCharacter != character) { character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name), @@ -253,20 +290,24 @@ namespace Barotrauma { if (targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead) { - return 0; + Priority = 0; } - // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally) - float dist = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y) * 2.0f; - float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist)); - if (targetCharacter.CurrentHull == character.CurrentHull) + else { - distanceFactor = 1; + // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally) + float dist = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y) * 2.0f; + float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist)); + if (targetCharacter.CurrentHull == character.CurrentHull) + { + distanceFactor = 1; + } + float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100; + float devotion = CumulatedDevotion / 100; + Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1)); } - float vitalityFactor = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter); - float devotion = Math.Min(Priority, 10) / 100; - return MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + vitalityFactor * distanceFactor, 0, 1)); + return Priority; } - public static IEnumerable GetVitalityReducingAfflictions(Character character) => character.CharacterHealth.GetAllAfflictions(a => a.GetVitalityDecrease(character.CharacterHealth) > 0); + public static IEnumerable GetSortedAfflictions(Character character) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions()); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 6cc31471c..d3d9adfed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -8,11 +8,11 @@ namespace Barotrauma { public override string DebugTag => "rescue all"; public override bool ForceRun => true; - public override bool IgnoreUnsafeHulls => true; + public override bool InverseTargetEvaluation => true; private const float vitalityThreshold = 80; - private const float vitalityThresholdForOrders = 95; - public static float GetVitalityThreshold(AIObjectiveManager manager) + private const float vitalityThresholdForOrders = 100; + public static float GetVitalityThreshold(AIObjectiveManager manager, Character character, Character target) { if (manager == null) { @@ -20,7 +20,7 @@ namespace Barotrauma } else { - return manager.CurrentOrder is AIObjectiveRescueAll ? vitalityThresholdForOrders : vitalityThreshold; + return character == target || manager.CurrentOrder is AIObjectiveRescueAll ? vitalityThresholdForOrders : vitalityThreshold; } } @@ -31,9 +31,50 @@ namespace Barotrauma protected override IEnumerable GetList() => Character.CharacterList; - protected override float TargetEvaluation() => Targets.Max(t => GetVitalityFactor(t)); + protected override float TargetEvaluation() + { + int otherRescuers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective(), onlyBots: true); + int targetCount = Targets.Count; + bool anyRescuers = otherRescuers > 0; + float ratio = anyRescuers ? targetCount / (float)otherRescuers : 1; + if (objectiveManager.CurrentOrder == this) + { + return Targets.Min(t => GetVitalityFactor(t)) / ratio; + } + else + { + float multiplier = 1; + if (anyRescuers) + { + float mySkill = character.GetSkillLevel("medical"); + int betterRescuers = HumanAIController.CountCrew(c => c != HumanAIController && c.Character.Info.Job.GetSkillLevel("medical") >= mySkill, onlyBots: true); + if (targetCount / (float)betterRescuers <= 1) + { + // Enough rescuers + return 100; + } + else + { + bool foundOtherMedics = HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.Info.Job.Prefab.Identifier == "medicaldoctor"); + if (foundOtherMedics) + { + if (character.Info.Job.Prefab.Identifier != "medicaldoctor") + { + // Double the vitality factor -> less likely to take action + multiplier = 2; + } + } + } + } + return Targets.Min(t => GetVitalityFactor(t)) / ratio * multiplier; + } + } - public static float GetVitalityFactor(Character character) => Math.Min(character.HealthPercentage - character.Bleeding - character.Bloodloss - Math.Min(character.Oxygen, 0), 100); + public static float GetVitalityFactor(Character character) + { + float vitality = character.HealthPercentage - character.Bleeding - character.Bloodloss + Math.Min(character.Oxygen, 0); + return Math.Clamp(vitality, 0, 100); + } protected override AIObjective ObjectiveConstructor(Character target) => new AIObjectiveRescue(character, target, objectiveManager, PriorityModifier); @@ -47,16 +88,34 @@ namespace Barotrauma if (!HumanAIController.IsFriendly(character, target)) { return false; } if (character.AIController is HumanAIController humanAI) { - if (GetVitalityFactor(target) > GetVitalityThreshold(humanAI.ObjectiveManager)) { return false; } + if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) { return false; } + if (!humanAI.ObjectiveManager.IsCurrentOrder()) + { + // Ignore unsafe hulls, unless ordered + if (humanAI.UnsafeHulls.Contains(target.CurrentHull)) + { + return false; + } + } } else { - if (GetVitalityFactor(target) > vitalityThreshold) { return false; } + if (GetVitalityFactor(target) >= vitalityThreshold) { return false; } } if (target.Submarine == null || character.Submarine == null) { return false; } if (target.Submarine.TeamID != character.Submarine.TeamID) { return false; } if (target.CurrentHull == null) { return false; } if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; } + if (!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI) + { + // Ignore all concious targets that are currently fighting, fleeing or treating characters + if (targetAI.ObjectiveManager.HasActiveObjective() || + targetAI.ObjectiveManager.HasActiveObjective() || + targetAI.ObjectiveManager.HasActiveObjective()) + { + return false; + } + } // Don't go into rooms that have enemies if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) { return false; } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 16ba0c9ea..53e1b4ac6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -291,7 +291,7 @@ namespace Barotrauma } for (int i = 0; i < AppropriateJobs.Length; i++) { - if (character.Info.Job.Prefab.Identifier.ToLowerInvariant() == AppropriateJobs[i].ToLowerInvariant()) { return true; } + if (character.Info.Job.Prefab.Identifier.Equals(AppropriateJobs[i], StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs new file mode 100644 index 000000000..3bd42daaf --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -0,0 +1,272 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using Barotrauma.Networking; +using System.Linq; +using System; + +namespace Barotrauma +{ + class WreckAI : IServerSerializable + { + public Submarine Wreck { get; private set; } + + public bool IsAlive { get; private set; } + + private readonly List allItems; + private readonly List thalamusItems; + private readonly List turrets = new List(); + private readonly List wayPoints = new List(); + private readonly List hulls = new List(); + private readonly List spawnOrgans = new List(); + private readonly Item brain; + + private bool initialCellsSpawned; + + public readonly WreckAIConfig Config; + + private bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; + + public WreckAI(Submarine wreck, Item brain, List items = null) + { + Config = WreckAIConfig.GetRandom(); + if (Config == null) + { + DebugConsole.ThrowError("WreckAI: No wreck AI config found!"); + Kill(); + return; + } + allItems = items ?? wreck.GetItems(false); + thalamusItems = allItems.FindAll(i => i.Prefab.Category == MapEntityCategory.Thalamus || i.HasTag("thalamus")); + foreach (Item item in allItems) + { + if (thalamusItems.Contains(item)) + { + // Ensure that thalamus items are visible + item.HiddenInGame = false; + } + else + { + // Load regular turrets + var turret = item.GetComponent(); + if (turret != null) + { + foreach (var linkedItem in item.GetLinkedEntities()) + { + var container = linkedItem.GetComponent(); + if (container == null) { continue; } + for (int i = 0; i < container.Inventory.Capacity; i++) + { + if (container.Inventory.Items[i] != null) { continue; } + if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab i && container.CanBeContained(i) && + Config.ForbiddenAmmunition.None(id => id.Equals(i.Identifier, StringComparison.OrdinalIgnoreCase)), Rand.RandSync.Server) is ItemPrefab ammoPrefab) + { + Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, wreck); + if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false)) + { + item.Remove(); + } + } + } + } + } + } + } + this.brain = brain; + Wreck = wreck; + foreach (var item in Wreck.GetItems(false)) + { + var turret = item.GetComponent(); + if (turret != null) + { + turrets.Add(turret); + } + if (item.HasTag("cellspawnorgan")) + { + if (!spawnOrgans.Contains(item)) + { + spawnOrgans.Add(item); + } + } + } + wayPoints.AddRange(Wreck.GetWaypoints(false)); + hulls.AddRange(Wreck.GetHulls(false)); + IsAlive = true; + } + + private readonly List destroyedOrgans = new List(); + public void Update(float deltaTime) + { + if (!IsAlive || Wreck == null || Wreck.Removed) + { + cells.ForEach(c => c.OnDeath -= OnCellDeath); + return; + } + if (brain == null || brain.Removed || brain.Condition <= 0) + { + Kill(); + } + destroyedOrgans.Clear(); + foreach (var organ in spawnOrgans) + { + if (organ.Condition <= 0) + { + destroyedOrgans.Add(organ); + } + } + destroyedOrgans.ForEach(o => spawnOrgans.Remove(o)); + + bool someoneNearby = false; + float minDist = Sonar.DefaultSonarRange * 2.0f; + foreach (Submarine submarine in Submarine.Loaded) + { + if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } + if (Vector2.DistanceSquared(submarine.WorldPosition, Wreck.WorldPosition) < minDist * minDist) + { + someoneNearby = true; + break; + } + } + foreach (Character c in Character.CharacterList) + { + if (c != Character.Controlled && !c.IsRemotePlayer) { continue; } + if (Vector2.DistanceSquared(c.WorldPosition, Wreck.WorldPosition) < minDist * minDist) + { + someoneNearby = true; + break; + } + } + if (!someoneNearby) { return; } + + OperateTurrets(deltaTime); + if (!IsClient) + { + if (!initialCellsSpawned) { SpawnInitialCells(); } + UpdateReinforcements(deltaTime); + } + } + + private void SpawnInitialCells() + { + int brainRoomCells = Rand.Range(MinCellsPerBrainRoom, MaxCellsPerRoom); + if (brain.CurrentHull?.WaterPercentage >= MinWaterLevel) + { + for (int i = 0; i < brainRoomCells; i++) + { + if (!TrySpawnCell(out _, brain.CurrentHull)) { break; } + } + } + int cellsInside = Rand.Range(MinCellsInside, MaxCellsInside); + for (int i = 0; i < cellsInside; i++) + { + if (!TrySpawnCell(out _)) { break; } + } + int cellsOutside = Rand.Range(MinCellsOutside, MaxCellsOutside); + // If we failed to spawn some of the cells in the brainroom/inside, spawn some extra cells outside. + cellsOutside = Math.Clamp(cellsOutside + brainRoomCells + cellsInside - cells.Count, cellsOutside, MaxCellsOutside); + for (int i = 0; i < cellsOutside; i++) + { + ISpatialEntity targetEntity = wayPoints.GetRandom(wp => wp.CurrentHull == null); + if (targetEntity == null) { break; } + if (!TrySpawnCell(out _, targetEntity)) { break; } + } + initialCellsSpawned = true; + } + + public void Kill() + { + if (!IsClient) + { + brain.Condition = 0; + } + IsAlive = false; + } + + // The client doesn't use these, so we don't have to sync them. + private readonly List cells = new List(); + // Intentionally contains duplicates. + private readonly List populatedHulls = new List(); + private float cellSpawnTimer; + + private float CellSpawnTime => Config.CellSpawnTime; + private float CellSpawnRandomFactor => Config.CellSpawnRandomFactor; + private int MinCellsPerBrainRoom => Config.MinCellsPerBrainRoom; + private int MaxCellsPerRoom => Config.MaxCellsPerRoom; + private int MinCellsOutside => Config.MinCellsOutside; + private int MaxCellsOutside => Config.MaxCellsOutside; + private int MinCellsInside => Config.MinCellsInside; + private int MaxCellsInside => Config.MaxCellsInside; + private int MaxCellCount => Config.MaxCellCount; + private float MinWaterLevel => Config.MinWaterLevel; + + void UpdateReinforcements(float deltaTime) + { + if (cells.Count >= MaxCellCount) { return; } + cellSpawnTimer -= deltaTime; + if (cellSpawnTimer < 0) + { + TrySpawnCell(out _, spawnOrgans.GetRandom()); + cellSpawnTimer = CellSpawnTime * Rand.Range(CellSpawnRandomFactor, 1 + CellSpawnRandomFactor); + } + } + + bool TrySpawnCell(out Character cell, ISpatialEntity targetEntity = null) + { + cell = null; + if (cells.Count >= MaxCellCount) { return false; } + if (targetEntity == null) + { + targetEntity = + wayPoints.GetRandom(wp => wp.CurrentHull != null && populatedHulls.Count(h => h == wp.CurrentHull) < MaxCellsPerRoom && wp.CurrentHull.WaterPercentage >= MinWaterLevel) ?? + hulls.GetRandom(h => populatedHulls.Count(h2 => h2 == h) < MaxCellsPerRoom && h.WaterPercentage >= MinWaterLevel) as ISpatialEntity; + } + if (targetEntity == null) { return false; } + if (targetEntity is Hull h) + { + populatedHulls.Add(h); + } + else if (targetEntity is WayPoint wp && wp.CurrentHull != null) + { + populatedHulls.Add(wp.CurrentHull); + } + // Don't add items in the list, because we want to be able to ignore the restrictions for spawner organs. + cell = Character.Create("Leucocyte", targetEntity.WorldPosition, ToolBox.RandomSeed(8), hasAi: true, createNetworkEvent: true); + cells.Add(cell); + cell.OnDeath += OnCellDeath; + cellSpawnTimer = CellSpawnTime * Rand.Range(CellSpawnRandomFactor, 1 + CellSpawnRandomFactor); + return true; + } + + void OperateTurrets(float deltaTime) + { + foreach (var turret in turrets) + { + // Never target other creatures than humans with the turrets. + turret.ThalamusOperate(deltaTime, + !turret.Item.HasTag("ignorecharacters"), + targetOtherCreatures: false, + !turret.Item.HasTag("ignoresubmarines"), + turret.Item.HasTag("ignoreaimdelay")); + } + } + + void OnCellDeath(Character character, CauseOfDeath causeOfDeath) + { + cells.Remove(character); + } + +#if SERVER + public void ServerWrite(IWriteMessage msg, Client client, object[] extraData = null) + { + msg.Write(IsAlive); + } +#endif +#if CLIENT + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + IsAlive = msg.ReadBoolean(); + } +#endif + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs new file mode 100644 index 000000000..76a6f2758 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs @@ -0,0 +1,97 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + class WreckAIConfig : ISerializableEntity + { + public string Name => "Wreck AI Config"; + + public Dictionary SerializableProperties { get; private set; } + + [Serialize(60f, false)] + public float CellSpawnTime { get; set; } + + [Serialize(0.5f, false)] + public float CellSpawnRandomFactor { get; set; } + + [Serialize(0, false)] + public int MinCellsPerBrainRoom { get; set; } + + [Serialize(3, false)] + public int MaxCellsPerRoom { get; set; } + + [Serialize(2, false)] + public int MinCellsOutside { get; set; } + + [Serialize(5, false)] + public int MaxCellsOutside { get; set; } + + [Serialize(3, false)] + public int MinCellsInside { get; set; } + + [Serialize(10, false)] + public int MaxCellsInside { get; set; } + + [Serialize(15, false)] + public int MaxCellCount { get; set; } + + [Serialize(100f, false)] + public float MinWaterLevel { get; set; } + + public readonly string[] ForbiddenAmmunition; + + public static List List + { + get + { + if (paramsList == null) + { + LoadAll(); + } + return paramsList; + } + } + + private static List paramsList; + + public static WreckAIConfig GetRandom() => List.GetRandom(Rand.RandSync.Server); + + public WreckAIConfig(XElement element) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + ForbiddenAmmunition = XMLExtensions.GetAttributeStringArray(element, "ForbiddenAmmunition", new string[0], convertToLowerInvariant: true); + } + + public static void LoadAll() + { + paramsList = new List(); + var files = GameMain.Instance.GetFilesOfType(ContentType.WreckAIConfig); + if (files.None()) + { + DebugConsole.ThrowError("Cannot find any Wreck AI config!"); + return; + } + foreach (ContentFile file in files) + { + XDocument doc = XMLExtensions.TryLoadXml(file.Path); + if (doc == null) { continue; } + var mainElement = doc.Root; + if (mainElement.IsOverride()) + { + mainElement = doc.Root.FirstElement(); + paramsList.Clear(); + DebugConsole.NewMessage($"Overriding the wreck ai config with '{file.Path}'", Color.Yellow); + } + else if (paramsList.Any()) + { + DebugConsole.NewMessage($"Adding additional wreck ai config from file '{file.Path}'"); + } + paramsList.Add(new WreckAIConfig(mainElement)); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs index 41812f4e9..8ffcb47bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs @@ -70,7 +70,7 @@ namespace Barotrauma } } - if (IsDead || Vitality <= 0.0f || IsUnconscious || Stun > 0.0f) return; + if (IsDead || Vitality <= 0.0f|| Stun > 0.0f || IsIncapacitated) return; if (!aiController.Enabled) return; if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) return; if (Controlled == this) return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index ad6603ad1..bcb29ad85 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -231,25 +231,12 @@ namespace Barotrauma } else { - Limb refLimb = GetLimb(LimbType.Head); - float refAngle; - if (refLimb == null) + float rotation = MathHelper.WrapAngle(Collider.Rotation); + rotation = MathHelper.ToDegrees(rotation); + if (rotation < 0.0f) { - refAngle = CurrentAnimationParams.TorsoAngleInRadians; - refLimb = GetLimb(LimbType.Torso); + rotation += 360; } - else - { - refAngle = CurrentAnimationParams.HeadAngleInRadians; - } - - float rotation = refLimb.Rotation; - if (!float.IsNaN(refAngle)) { rotation -= refAngle * Dir; } - - rotation = MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(rotation)); - - if (rotation < 0.0f) rotation += 360; - if (rotation > 20 && rotation < 160) { TargetDir = Direction.Left; @@ -347,9 +334,23 @@ namespace Barotrauma target.AnimController.Collider.MoveToPos(mouthPos, (float)(Math.Sin(eatTimer) + dragForce)); } - //pull the character's mouth to the target character (again with a fluctuating force) - float pullStrength = (float)(Math.Sin(eatTimer) * Math.Max(Math.Sin(eatTimer * 0.5f), 0.0f)); - mouthLimb.body.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f * pullStrength, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + if (InWater) + { + //pull the character's mouth to the target character (again with a fluctuating force) + float pullStrength = (float)(Math.Sin(eatTimer) * Math.Max(Math.Sin(eatTimer * 0.5f), 0.0f)); + mouthLimb.body.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f * pullStrength, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + } + else + { + float force = (float)Math.Sin(eatTimer * 100) * mouthLimb.Mass; + mouthLimb.body.ApplyLinearImpulse(Vector2.UnitY * force * 2, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + mouthLimb.body.ApplyTorque(-force * 50); + } + var jaw = GetLimb(LimbType.Jaw); + if (jaw != null) + { + jaw.body.ApplyTorque(-(float)Math.Sin(eatTimer * 150) * jaw.Mass * 25); + } character.ApplyStatusEffects(ActionType.OnEating, deltaTime); @@ -439,7 +440,7 @@ namespace Barotrauma if (CurrentSwimParams.RotateTowardsMovement) { - Collider.SmoothRotate(movementAngle, CurrentSwimParams.SteerTorque); + Collider.SmoothRotate(movementAngle, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); if (TorsoAngle.HasValue) { Limb torso = GetLimb(LimbType.Torso); @@ -491,11 +492,11 @@ namespace Barotrauma } if (mainLimb.type == LimbType.Head && HeadAngle.HasValue) { - Collider.SmoothRotate(HeadAngle.Value * Dir, CurrentSwimParams.SteerTorque); + Collider.SmoothRotate(HeadAngle.Value * Dir, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); } else if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue) { - Collider.SmoothRotate(TorsoAngle.Value * Dir, CurrentSwimParams.SteerTorque); + Collider.SmoothRotate(TorsoAngle.Value * Dir, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); } if (TorsoAngle.HasValue) { @@ -515,7 +516,7 @@ namespace Barotrauma } var waveLength = Math.Abs(CurrentSwimParams.WaveLength * RagdollParams.JointScale); - var waveAmplitude = Math.Abs(CurrentSwimParams.WaveAmplitude); + var waveAmplitude = Math.Abs(CurrentSwimParams.WaveAmplitude * character.SpeedMultiplier); if (waveLength > 0 && waveAmplitude > 0) { WalkPos -= transformedMovement.Length() / Math.Abs(waveLength); @@ -524,6 +525,10 @@ namespace Barotrauma foreach (var limb in Limbs) { + if (Math.Abs(limb.Params.ConstantTorque) > 0) + { + limb.body.SmoothRotate(movementAngle + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Params.ConstantTorque, wrapAngle: true); + } switch (limb.type) { case LimbType.LeftFoot: @@ -548,7 +553,7 @@ namespace Barotrauma if (Limbs[i].SteerForce <= 0.0f) { continue; } if (!Collider.PhysEnabled) { continue; } Vector2 pullPos = Limbs[i].PullJointWorldAnchorA; - Limbs[i].body.ApplyForce(movement * Limbs[i].SteerForce * Limbs[i].Mass, pullPos); + Limbs[i].body.ApplyForce(movement * Limbs[i].SteerForce * Limbs[i].Mass * Math.Max(character.SpeedMultiplier, 1), pullPos); } Vector2 mainLimbDiff = mainLimb.PullJointWorldAnchorB - mainLimb.SimPosition; @@ -665,6 +670,10 @@ namespace Barotrauma foreach (Limb limb in Limbs) { + if (Math.Abs(limb.Params.ConstantTorque) > 0) + { + limb.body.SmoothRotate(movementAngle + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Params.ConstantTorque, wrapAngle: true); + } switch (limb.type) { case LimbType.LeftFoot: @@ -729,7 +738,10 @@ namespace Barotrauma break; case LimbType.LeftLeg: case LimbType.RightLeg: - if (Math.Abs(CurrentGroundedParams.LegTorque) > 0.001f) limb.body.ApplyTorque(limb.Mass * CurrentGroundedParams.LegTorque * Dir); + if (Math.Abs(CurrentGroundedParams.LegTorque) > 0) + { + limb.body.ApplyTorque(limb.Mass * CurrentGroundedParams.LegTorque * Dir); + } break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index cf3bc553d..059d0fb65 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -900,7 +900,7 @@ namespace Barotrauma surfaceLimiter = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 0.4f) - surfacePos; surfaceLimiter = Math.Max(1.0f, surfaceLimiter); - if (surfaceLimiter > 50.0f) return; + if (surfaceLimiter > 50.0f) { return; } } Limb leftHand = GetLimb(LimbType.LeftHand); @@ -928,8 +928,7 @@ namespace Barotrauma if (!aiming) { float newRotation = MathUtils.VectorToAngle(TargetMovement) - MathHelper.PiOver2; - Collider.SmoothRotate(newRotation, 5.0f); - //torso.body.SmoothRotate(newRotation); + Collider.SmoothRotate(newRotation, 5.0f * character.SpeedMultiplier); } } else @@ -942,13 +941,13 @@ namespace Barotrauma TargetMovement = new Vector2(0.0f, -0.1f); float newRotation = MathUtils.VectorToAngle(diff); - Collider.SmoothRotate(newRotation, 5.0f); + Collider.SmoothRotate(newRotation, 5.0f * character.SpeedMultiplier); } } torso.body.MoveToPos(Collider.SimPosition + new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * 0.4f, 5.0f); - if (TargetMovement == Vector2.Zero) return; + if (TargetMovement == Vector2.Zero) { return; } movement = MathUtils.SmoothStep(movement, TargetMovement, 0.3f); @@ -1007,7 +1006,7 @@ namespace Barotrauma var waist = GetLimb(LimbType.Waist); footPos = waist == null ? Vector2.Zero : waist.SimPosition - new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * (upperLegLength + lowerLegLength); - Vector2 transformedFootPos = new Vector2((float)Math.Sin(legCyclePos / CurrentSwimParams.LegCycleLength) * CurrentSwimParams.LegMoveAmount * CurrentAnimationParams.CycleSpeed, 0.0f); + Vector2 transformedFootPos = new Vector2((float)Math.Sin(legCyclePos / CurrentSwimParams.LegCycleLength / character.SpeedMultiplier) * CurrentSwimParams.LegMoveAmount, 0.0f); transformedFootPos = Vector2.Transform(transformedFootPos, Matrix.CreateRotationZ(Collider.Rotation)); if (rightFoot != null && !rightFoot.Disabled) @@ -1061,7 +1060,7 @@ namespace Barotrauma rightHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, rightHandPos.X) : Math.Min(-0.3f, rightHandPos.X); rightHandPos = Vector2.Transform(rightHandPos, rotationMatrix); - HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.HandMoveStrength); + HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier); } if (leftHand != null && !leftHand.Disabled) @@ -1070,7 +1069,7 @@ namespace Barotrauma leftHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, leftHandPos.X) : Math.Min(-0.3f, leftHandPos.X); leftHandPos = Vector2.Transform(leftHandPos, rotationMatrix); - HandIK(leftHand, handPos + leftHandPos, CurrentSwimParams.HandMoveStrength); + HandIK(leftHand, handPos + leftHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier); } } @@ -1653,7 +1652,10 @@ namespace Barotrauma //TODO: refactor this method, it's way too convoluted public override void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f) { - if (character.IsUnconscious || character.Stun > 0.0f) aim = false; + if (character.Stun > 0.0f || character.IsIncapacitated) + { + aim = false; + } //calculate the handle positions Matrix itemTransfrom = Matrix.CreateRotationZ(item.body.Rotation); @@ -1677,7 +1679,7 @@ namespace Barotrauma Holdable holdable = item.GetComponent(); - if (!isClimbing && !usingController && character.Stun <= 0.0f && aim && itemPos != Vector2.Zero) + if (!isClimbing && !usingController && character.Stun <= 0.0f && aim && itemPos != Vector2.Zero && !character.IsIncapacitated) { Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition); @@ -1764,7 +1766,7 @@ namespace Barotrauma if (holdable.Pusher != null) { - if (character.IsUnconscious || character.Stun > 0.0f) + if (character.Stun > 0.0f || character.IsIncapacitated) { holdable.Pusher.Enabled = false; } @@ -1779,7 +1781,7 @@ namespace Barotrauma else { holdable.Pusher.TargetPosition = currItemPos; - holdable.Pusher.TargetRotation = character.IsUnconscious || character.Stun > 0.0f ? itemAngle : holdAngle * Dir; + holdable.Pusher.TargetRotation = holdAngle * Dir; holdable.Pusher.MoveToTargetPosition(true); @@ -1929,7 +1931,7 @@ namespace Barotrauma float sqrDist = Vector2.DistanceSquared(character.WorldPosition, handWorldPos); if (sqrDist > MathUtils.Pow(ConvertUnits.ToDisplayUnits(upperArmLength + forearmLength), 2)) { - TargetMovement = Vector2.Normalize(handWorldPos - character.WorldPosition) * GetCurrentSpeed(false); + TargetMovement = Vector2.Normalize(handWorldPos - character.WorldPosition) * GetCurrentSpeed(false) * Math.Max(character.SpeedMultiplier, 1); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 6490858b9..20f981e2e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -97,8 +97,6 @@ namespace Barotrauma protected float strongestImpact; - protected double onFloorTimer; - private float splashSoundTimer; //the movement speed of the ragdoll @@ -383,7 +381,7 @@ namespace Barotrauma } } - if (character.IsHusk) + if (character.IsHusk && character.Params.UseHuskAppendage) { var characterPrefab = CharacterPrefab.FindByFilePath(character.ConfigPath); if (characterPrefab?.XDocument != null) @@ -732,14 +730,20 @@ namespace Barotrauma limbJoint.IsSevered = true; limbJoint.Enabled = false; + Vector2 limbDiff = limbJoint.LimbA.SimPosition - limbJoint.LimbB.SimPosition; + if (limbDiff.LengthSquared() < 0.0001f) { limbDiff = Rand.Vector(1.0f); } + limbDiff = Vector2.Normalize(limbDiff); + float mass = limbJoint.BodyA.Mass + limbJoint.BodyB.Mass; + limbJoint.LimbA.body.ApplyLinearImpulse(limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f); + limbJoint.LimbB.body.ApplyLinearImpulse(-limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f); + List connectedLimbs = new List(); List checkedJoints = new List(); GetConnectedLimbs(connectedLimbs, checkedJoints, MainLimb); foreach (Limb limb in Limbs) { - if (connectedLimbs.Contains(limb)) continue; - + if (connectedLimbs.Contains(limb)) { continue; } limb.IsSevered = true; } @@ -1626,14 +1630,14 @@ namespace Barotrauma float sin = (float)Math.Sin(mouthLimb.Rotation); Vector2 bodySize = mouthLimb.body.GetSize(); Vector2 offset = new Vector2(mouthLimb.MouthPos.X * bodySize.X / 2, mouthLimb.MouthPos.Y * bodySize.Y / 2); - return mouthLimb.SimPosition + new Vector2(offset.X * cos - offset.Y * sin, offset.X * sin + offset.Y * cos) * RagdollParams.LimbScale; + return mouthLimb.SimPosition + new Vector2(offset.X * cos - offset.Y * sin, offset.X * sin + offset.Y * cos) * mouthLimb.Scale * RagdollParams.LimbScale; } public Vector2 GetColliderBottom() { float offset = 0.0f; - if (!character.IsUnconscious && !character.IsDead && character.Stun <= 0.0f) + if (!character.IsDead && character.Stun <= 0.0f && !character.IsIncapacitated) { offset = -ColliderHeightFromFloor; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index a3b3a9ea2..c722ecb4b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -13,11 +13,12 @@ namespace Barotrauma public enum AttackContext { - NotDefined, + Any, Water, Ground, Inside, - Outside + Outside, + NotDefined } public enum AttackTarget @@ -72,13 +73,13 @@ namespace Barotrauma partial class Attack : ISerializableEntity { - [Serialize(AttackContext.NotDefined, true, description: "The attack will be used only in this context."), Editable] + [Serialize(AttackContext.Any, true, description: "The attack will be used only in this context."), Editable] public AttackContext Context { get; private set; } [Serialize(AttackTarget.Any, true, description: "Does the attack target only specific targets?"), Editable] public AttackTarget TargetType { get; private set; } - [Serialize(LimbType.None, true, description: "If not defined or set to none, the closest limb is used (default)."), Editable] + [Serialize(LimbType.None, true, description: "To which limb is the attack aimed at? If not defined or set to none, the closest limb is used (default)."), Editable] public LimbType TargetLimbType { get; private set; } [Serialize(HitDetection.Distance, true, description: "Collision detection is more accurate, but it only affects targets that are in contact with the limb."), Editable] @@ -87,9 +88,15 @@ namespace Barotrauma [Serialize(AIBehaviorAfterAttack.FallBack, true, description: "The preferred AI behavior after the attack."), Editable] public AIBehaviorAfterAttack AfterAttack { get; set; } - [Serialize(false, true, description: "Should the AI try to reverse when aiming with this attack?"), Editable] + [Serialize(0f, true, description: "A delay before reacting after performing an attack."), Editable] + public float AfterAttackDelay { get; set; } + + [Serialize(false, true, description: "Should the AI try to turn around when aiming with this attack?"), Editable] public bool Reverse { get; private set; } + [Serialize(false, true, description: "Should the AI try to steer away from the target when aiming with this attack? Best combined with PassiveAggressive behavior."), Editable] + public bool Retreat { get; private set; } + [Serialize(0.0f, true, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] public float Range { get; set; } @@ -147,7 +154,19 @@ namespace Barotrauma [Serialize(0.0f, true, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs). The direction of the force is towards the target that's being attacked."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] public float Force { get; private set; } - [Serialize(0.0f, true, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs)"), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] + [Serialize("0.0, 0.0", true, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] + public Vector2 RootForceWorldStart { get; private set; } + + [Serialize("0.0, 0.0", true, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] + public Vector2 RootForceWorldMiddle { get; private set; } + + [Serialize("0.0, 0.0", true, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] + public Vector2 RootForceWorldEnd { get; private set; } + + [Serialize(TransitionMode.Linear, true, description:""), Editable] + public TransitionMode RootTransitionEasing { get; private set; } + + [Serialize(0.0f, true, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs)"), Editable(MinValueFloat = -10000.0f, MaxValueFloat = 10000.0f)] public float Torque { get; private set; } [Serialize(false, true), Editable] @@ -156,13 +175,13 @@ namespace Barotrauma [Serialize(0.0f, true, description: "Applied to the target the attack hits. The direction of the impulse is from this limb towards the target (use negative values to pull the target closer)."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] public float TargetImpulse { get; private set; } - [Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards)."), Editable] + [Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable] public Vector2 TargetImpulseWorld { get; private set; } [Serialize(0.0f, true, description: "Applied to the target the attack hits. The direction of the force is from this limb towards the target (use negative values to pull the target closer)."), Editable(-1000.0f, 1000.0f)] public float TargetForce { get; private set; } - [Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards)."), Editable] + [Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable] public Vector2 TargetForceWorld { get; private set; } [Serialize(0.0f, true, description: "How likely the attack causes target limbs to be severed when the target is dead."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] @@ -279,7 +298,7 @@ namespace Barotrauma { DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names."); string afflictionName = subElement.GetAttributeString("name", "").ToLowerInvariant(); - afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.ToLowerInvariant() == afflictionName); + afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.Equals(afflictionName, System.StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab == null) { DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found."); @@ -289,7 +308,7 @@ namespace Barotrauma else { string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant(); - afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier); + afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab == null) { DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found."); @@ -324,7 +343,7 @@ namespace Barotrauma AfflictionPrefab afflictionPrefab; Affliction affliction; string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant(); - afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier); + afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab != null) { float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength"); @@ -419,7 +438,10 @@ namespace Barotrauma public AttackResult DoDamageToLimb(Character attacker, Limb targetLimb, Vector2 worldPosition, float deltaTime, bool playSound = true) { - if (targetLimb == null) return new AttackResult(); + if (targetLimb == null) + { + return new AttackResult(); + } if (OnlyHumans) { @@ -461,6 +483,7 @@ namespace Barotrauma public float AttackTimer { get; private set; } public float CoolDownTimer { get; set; } + public float CurrentRandomCoolDown { get; private set; } public float SecondaryCoolDownTimer { get; set; } public bool IsRunning { get; private set; } @@ -492,7 +515,8 @@ namespace Barotrauma public void SetCoolDown() { float randomFraction = CoolDown * CoolDownRandomFactor; - CoolDownTimer = CoolDown + MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value(Rand.RandSync.Server)); + CurrentRandomCoolDown = MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value(Rand.RandSync.Server)); + CoolDownTimer = CoolDown + CurrentRandomCoolDown; randomFraction = SecondaryCoolDown * CoolDownRandomFactor; SecondaryCoolDownTimer = SecondaryCoolDown + MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value(Rand.RandSync.Server)); } @@ -501,11 +525,12 @@ namespace Barotrauma { CoolDownTimer = 0; SecondaryCoolDownTimer = 0; + CurrentRandomCoolDown = 0; } partial void DamageParticles(float deltaTime, Vector2 worldPosition); - public bool IsValidContext(AttackContext context) => Context == context || Context == AttackContext.NotDefined; + public bool IsValidContext(AttackContext context) => Context == context || Context == AttackContext.Any || Context == AttackContext.NotDefined; public bool IsValidContext(IEnumerable contexts) { @@ -559,5 +584,11 @@ namespace Barotrauma return true; } } + + public Vector2 CalculateAttackPhase(TransitionMode easing = TransitionMode.Linear) + { + float t = AttackTimer / Duration; + return MathUtils.Bezier(RootForceWorldStart, RootForceWorldMiddle, RootForceWorldEnd, ToolBox.GetEasing(easing, t)); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index c88604bcc..fc0f34d0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -57,6 +57,9 @@ namespace Barotrauma public Hull CurrentHull = null; public bool IsRemotePlayer; + + public bool IsPlayer => Controlled == this || IsRemotePlayer; + public readonly Dictionary Properties; public Dictionary SerializableProperties { @@ -122,19 +125,54 @@ namespace Barotrauma set => Params.NeedsAir = value; } + public bool NeedsWater + { + get => Params.NeedsWater; + set => Params.NeedsWater = value; + } + + public bool NeedsOxygen => NeedsAir || NeedsWater && !AnimController.InWater; + public float Noise { get => Params.Noise; set => Params.Noise = value; } + public float Visibility + { + get => Params.Visibility; + set => Params.Visibility = value; + } + public bool IsTraitor; public string TraitorCurrentObjective = ""; public bool IsHuman => SpeciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase); private float attackCoolDown; - public Order CurrentOrder { get; private set; } + public Order CurrentOrder + { + get + { + return Info?.CurrentOrder; + } + private set + { + if (Info != null) { Info.CurrentOrder = value; } + } + } + public string CurrentOrderOption + { + get + { + return Info?.CurrentOrderOption; + } + private set + { + if (Info != null) { Info.CurrentOrderOption = value; } + } + } private readonly List statusEffects = new List(); private readonly List speedMultipliers = new List(); @@ -160,7 +198,7 @@ namespace Barotrauma if (turret != null) { viewTargetWorldPos = new Vector2( - targetItem.WorldRect.X + turret.TransformedBarrelPos.X, + targetItem.WorldRect.X + turret.TransformedBarrelPos.X, targetItem.WorldRect.Y - turret.TransformedBarrelPos.Y); } } @@ -201,7 +239,7 @@ namespace Barotrauma { displayName = TextManager.Get($"Character.{SpeciesName}", returnNull: true); } - return displayName ?? Name; + return string.IsNullOrWhiteSpace(displayName) ? Name : displayName; } } @@ -245,7 +283,7 @@ namespace Barotrauma //text displayed when the character is highlighted if custom interact is set public string customInteractHUDText; private Action onCustomInteract; - + private float lockHandsTimer; public bool LockHands { @@ -261,22 +299,22 @@ namespace Barotrauma public bool AllowInput { - get { return !IsUnconscious && Stun <= 0.0f && !IsDead; } + get { return Stun <= 0.0f && !IsDead && !IsIncapacitated; } } public bool CanMove { get { - if (!AllowInput) { return false; } if (!AnimController.InWater && !AnimController.CanWalk) { return false; } + if (!AllowInput) { return false; } return true; } } public bool CanInteract { - get { return AllowInput && IsHumanoid && !LockHands && !Removed; } + get { return AllowInput && IsHumanoid && !LockHands && !Removed && !IsIncapacitated; } } public Vector2 CursorPosition @@ -362,12 +400,21 @@ namespace Barotrauma pressureProtection = MathHelper.Clamp(value, 0.0f, 100.0f); } } - + private float ragdollingLockTimer; public bool IsRagdolled; public bool IsForceRagdolled; public bool dontFollowCursor; + public bool IsIncapacitated + { + get + { + if (IsUnconscious) { return true; } + return CharacterHealth.Afflictions.Any(a => a.Prefab.AfflictionType == "paralysis" && a.Strength >= a.Prefab.MaxStrength); + } + } + public bool IsUnconscious { get { return CharacterHealth.IsUnconscious; } @@ -491,6 +538,8 @@ namespace Barotrauma public bool IsDead { get; private set; } + public bool EnableDespawn { get; set; } = true; + public CauseOfDeath CauseOfDeath { get; @@ -513,7 +562,7 @@ namespace Barotrauma { if (!canBeDragged) { return false; } if (Removed || !AnimController.Draggable) { return false; } - return IsDead || Stun > 0.0f || LockHands || IsUnconscious; + return IsDead || Stun > 0.0f || LockHands || IsIncapacitated; } set { canBeDragged = value; } } @@ -531,7 +580,7 @@ namespace Barotrauma } else { - return (IsDead || Stun > 0.0f || LockHands || IsUnconscious); + return (IsDead || Stun > 0.0f || LockHands || IsIncapacitated); } } set { canInventoryBeAccessed = value; } @@ -758,7 +807,7 @@ namespace Barotrauma var matchingAffliction = AfflictionPrefab.List .Where(p => p.AfflictionType == "huskinfection") .Select(p => p as AfflictionPrefabHusk) - .FirstOrDefault(p => p.TargetSpecies.Any(t => t.Equals(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p), StringComparison.InvariantCultureIgnoreCase))); + .FirstOrDefault(p => p.TargetSpecies.Any(t => t.Equals(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p), StringComparison.OrdinalIgnoreCase))); string nonHuskedSpeciesName = string.Empty; if (matchingAffliction == null) { @@ -770,10 +819,14 @@ namespace Barotrauma { nonHuskedSpeciesName = AfflictionHusk.GetNonHuskedSpeciesName(speciesName, matchingAffliction); } - ragdollParams = IsHumanoid ? RagdollParams.GetDefaultRagdollParams(nonHuskedSpeciesName) : RagdollParams.GetDefaultRagdollParams(nonHuskedSpeciesName) as RagdollParams; - if (info == null) + if (ragdollParams == null) { - info = new CharacterInfo(nonHuskedSpeciesName, ragdollParams.FileName); + string name = Params.UseHuskAppendage ? nonHuskedSpeciesName : speciesName; + ragdollParams = IsHumanoid ? RagdollParams.GetDefaultRagdollParams(name) : RagdollParams.GetDefaultRagdollParams(name) as RagdollParams; + } + if (Params.HasInfo && info == null) + { + info = new CharacterInfo(nonHuskedSpeciesName); } } @@ -844,7 +897,7 @@ namespace Barotrauma Info.HairElement?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Hair))); #if CLIENT - head.LoadHuskSprite(); + head.EnableHuskSprite = Params.Husk; head.LoadHerpesSprite(); head.UpdateWearableTypesToHide(); #endif @@ -1010,59 +1063,55 @@ namespace Barotrauma } else { - if (IsKeyDown(InputType.Left)) targetMovement.X -= 1.0f; - if (IsKeyDown(InputType.Right)) targetMovement.X += 1.0f; - if (IsKeyDown(InputType.Up)) targetMovement.Y += 1.0f; - if (IsKeyDown(InputType.Down)) targetMovement.Y -= 1.0f; + if (IsKeyDown(InputType.Left)) { targetMovement.X -= 1.0f; } + if (IsKeyDown(InputType.Right)) { targetMovement.X += 1.0f; } + if (IsKeyDown(InputType.Up)) { targetMovement.Y += 1.0f; } + if (IsKeyDown(InputType.Down)) { targetMovement.Y -= 1.0f; } } + bool run = false; + if ((IsKeyDown(InputType.Run) && AnimController.ForceSelectAnimationType == AnimationType.NotDefined) || ForceRun) + { + run = CanRun; + } + return ApplyMovementLimits(targetMovement, AnimController.GetCurrentSpeed(run)); + } + + //can't run if + // - dragging someone + // - crouching + // - moving backwards + public bool CanRun => (SelectedCharacter == null || !SelectedCharacter.CanBeDragged) && + (!(AnimController is HumanoidAnimController) || !((HumanoidAnimController)AnimController).Crouching) && + !AnimController.IsMovingBackwards; + + public Vector2 ApplyMovementLimits(Vector2 targetMovement, float currentSpeed) + { //the vertical component is only used for falling through platforms and climbing ladders when not in water, //so the movement can't be normalized or the Character would walk slower when pressing down/up if (AnimController.InWater) { float length = targetMovement.Length(); - if (length > 0.0f) targetMovement /= length; + if (length > 0.0f) + { + targetMovement /= length; + } } - - bool run = false; - if ((IsKeyDown(InputType.Run) && AnimController.ForceSelectAnimationType == AnimationType.NotDefined) || ForceRun) - { - //can't run if - // - dragging someone - // - crouching - // - moving backwards - run = (SelectedCharacter == null || !SelectedCharacter.CanBeDragged) && - (!(AnimController is HumanoidAnimController) || !((HumanoidAnimController)AnimController).Crouching) && - !AnimController.IsMovingBackwards; - } - - float currentSpeed = AnimController.GetCurrentSpeed(run); targetMovement *= currentSpeed; float maxSpeed = ApplyTemporarySpeedLimits(currentSpeed); targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed); targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed); - - //apply speed multiplier if - // a. it's boosting the movement speed and the character is trying to move fast (= running) - // b. it's a debuff that decreases movement speed - float speedMultiplier = SpeedMultiplier; - if (run || speedMultiplier <= 0.0f) targetMovement *= speedMultiplier; - - ResetSpeedMultiplier(); // Reset, items will set the value before the next update - + SpeedMultiplier = greatestPositiveSpeedMultiplier - (1f - greatestNegativeSpeedMultiplier); + targetMovement *= SpeedMultiplier; + // Reset, status effects will set the value before the next update + ResetSpeedMultiplier(); return targetMovement; } /// /// Can be used to modify the character's speed via StatusEffects /// - public float SpeedMultiplier - { - get - { - return greatestPositiveSpeedMultiplier - (1f - greatestNegativeSpeedMultiplier); - } - } + public float SpeedMultiplier { get; private set; } public void StackSpeedMultiplier(float val) { @@ -2012,8 +2061,8 @@ namespace Barotrauma HideFace = false; - UpdateSightRange(); - UpdateSoundRange(); + UpdateSightRange(deltaTime); + UpdateSoundRange(deltaTime); if (IsDead) { return; } @@ -2084,12 +2133,17 @@ namespace Barotrauma UpdateControlled(deltaTime, cam); //Health effects - if (NeedsAir) { UpdateOxygen(deltaTime); } + if (NeedsOxygen) + { + UpdateOxygen(deltaTime); + } CharacterHealth.Update(deltaTime); - if (IsUnconscious) + if (IsIncapacitated) { - UpdateUnconscious(); + Stun = Math.Max(5.0f, Stun); + AnimController.ResetPullJoints(); + SelectedConstruction = null; return; } @@ -2162,33 +2216,44 @@ namespace Barotrauma partial void UpdateProjSpecific(float deltaTime, Camera cam); + partial void SetOrderProjSpecific(Order order, string orderOption); + private void UpdateOxygen(float deltaTime) { - PressureProtection -= deltaTime * 100.0f; - float hullAvailableOxygen = 0.0f; - if (!AnimController.HeadInWater && AnimController.CurrentHull != null) + if (NeedsAir) { - //don't decrease the amount of oxygen in the hull if the character has more oxygen available than the hull - //(i.e. if the character has some external source of oxygen) - if (OxygenAvailable * 0.98f < AnimController.CurrentHull.OxygenPercentage) + PressureProtection -= deltaTime * 100.0f; + } + if (NeedsWater) + { + float waterAvailable = 100; + if (!AnimController.InWater && CurrentHull != null) { - AnimController.CurrentHull.Oxygen -= Hull.OxygenConsumptionSpeed * deltaTime; + waterAvailable = CurrentHull.WaterPercentage; } - hullAvailableOxygen = AnimController.CurrentHull.OxygenPercentage; + OxygenAvailable += MathHelper.Clamp(waterAvailable - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f); + } + else + { + float hullAvailableOxygen = 0.0f; + if (!AnimController.HeadInWater && AnimController.CurrentHull != null) + { + //don't decrease the amount of oxygen in the hull if the character has more oxygen available than the hull + //(i.e. if the character has some external source of oxygen) + if (OxygenAvailable * 0.98f < AnimController.CurrentHull.OxygenPercentage) + { + AnimController.CurrentHull.Oxygen -= Hull.OxygenConsumptionSpeed * deltaTime; + } + hullAvailableOxygen = AnimController.CurrentHull.OxygenPercentage; + + } + OxygenAvailable += MathHelper.Clamp(hullAvailableOxygen - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f); } - OxygenAvailable += MathHelper.Clamp(hullAvailableOxygen - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f); } + partial void UpdateOxygenProjSpecific(float prevOxygen); - private void UpdateUnconscious() - { - Stun = Math.Max(5.0f, Stun); - - AnimController.ResetPullJoints(); - SelectedConstruction = null; - } - /// /// How far the character is from the closest human player (including spectators) /// @@ -2221,23 +2286,29 @@ namespace Barotrauma } private float despawnTimer; - private const float DespawnDelay = 5.0f * 60.0f; //5 minutes private void UpdateDespawn(float deltaTime) { + if (!EnableDespawn) { return; } + //clients don't despawn characters unless the server says so if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; } if (!IsDead) { return; } + if (Submarine != null && CharacterList.Count(c => c.IsDead && c.Submarine == Submarine) < GameMain.Config.CorpsesPerSubDespawnThreshold) + { + return; + } + float distToClosestPlayer = GetDistanceToClosestPlayer(); if (distToClosestPlayer > NetConfig.DisableCharacterDist) { - //despawn in 1 second if very far from all human players - despawnTimer = Math.Max(despawnTimer, DespawnDelay - 1.0f); + //despawn in 1 minute if very far from all human players + despawnTimer = Math.Max(despawnTimer, GameMain.Config.CorpseDespawnDelay - 60.0f); } - + despawnTimer += deltaTime; - if (despawnTimer < DespawnDelay) { return; } + if (despawnTimer < GameMain.Config.CorpseDespawnDelay) { return; } if (IsHuman) { @@ -2264,7 +2335,12 @@ namespace Barotrauma if (itemContainer == null) { return; } foreach (Item inventoryItem in Inventory.Items) { - itemContainer.Inventory.TryPutItem(inventoryItem, user: null); + if (inventoryItem == null) { continue; } + if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null)) + { + //if the item couldn't be put inside the despawn container, just drop it + inventoryItem.Drop(dropper: this); + } } } } @@ -2274,7 +2350,7 @@ namespace Barotrauma public void DespawnNow() { - despawnTimer = DespawnDelay; + despawnTimer = GameMain.Config.CorpseDespawnDelay; } public static void RemoveByPrefab(CharacterPrefab prefab) @@ -2290,18 +2366,39 @@ namespace Barotrauma } } - private void UpdateSightRange() + private readonly float maxAIRange = 10000; + private readonly float aiTargetChangeSpeed = 5; + + private void UpdateSightRange(float deltaTime) { if (aiTarget == null) { return; } - float range = (float)Math.Sqrt(Mass) * 250 + AnimController.Collider.LinearVelocity.Length() * 500; - aiTarget.SightRange = MathHelper.Clamp(range, 0, 10000); + float minRange = Math.Clamp((float)Math.Sqrt(Mass) * Visibility, 250, 1000); + float massFactor = (float)Math.Sqrt(Mass / 20); + float targetRange = Math.Min(minRange + massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Visibility, maxAIRange); + float newRange = MathHelper.SmoothStep(aiTarget.SightRange, targetRange, deltaTime * aiTargetChangeSpeed); + if (!float.IsNaN(newRange)) + { + aiTarget.SightRange = newRange; + } } - private void UpdateSoundRange() + private void UpdateSoundRange(float deltaTime) { if (aiTarget == null) { return; } - float range = ((float)Math.Sqrt(Mass) / 3) * (AnimController.TargetMovement.Length() * 2) * Noise; - aiTarget.SoundRange = MathHelper.Clamp(range, 0, 10000); + if (IsDead) + { + aiTarget.SoundRange = 0; + } + else + { + float massFactor = (float)Math.Sqrt(Mass / 10); + float targetRange = Math.Min(massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Noise, maxAIRange); + float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed); + if (!float.IsNaN(newRange)) + { + aiTarget.SoundRange = newRange; + } + } } public bool CanHearCharacter(Character speaker) @@ -2316,24 +2413,17 @@ namespace Barotrauma public void SetOrder(Order order, string orderOption, Character orderGiver, bool speak = true) { - if (orderGiver != null) - { - //set the character order only if the character is close enough to hear the message - if (!CanHearCharacter(orderGiver)) { return; } - } + //set the character order only if the character is close enough to hear the message + if (orderGiver != null && !CanHearCharacter(orderGiver)) { return; } if (AIController is HumanAIController humanAI) { humanAI.SetOrder(order, orderOption, orderGiver, speak); } -#if CLIENT - else - { - GameMain.GameSession?.CrewManager?.DisplayCharacterOrder(this, order, orderOption); - } -#endif + SetOrderProjSpecific(order, orderOption); CurrentOrder = order; + CurrentOrderOption = orderOption; } private readonly List aiChatMessageQueue = new List(); @@ -2469,13 +2559,17 @@ namespace Barotrauma DamageLimb(worldPosition, targetLimb, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, attacker); if (limbHit == null) { return new AttackResult(); } - - limbHit.body?.ApplyLinearImpulse(attack.TargetImpulseWorld + attack.TargetForceWorld * deltaTime, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld; + if (attacker != null) + { + forceWorld.X *= attacker.AnimController.Dir; + } + limbHit.body?.ApplyLinearImpulse(forceWorld * deltaTime, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); var mainLimb = limbHit.character.AnimController.MainLimb; if (limbHit != mainLimb) { // Always add force to mainlimb - mainLimb.body?.ApplyLinearImpulse(attack.TargetImpulseWorld + attack.TargetForceWorld * deltaTime, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + mainLimb.body?.ApplyLinearImpulse(forceWorld * deltaTime, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } #if SERVER if (attacker is Character attackingCharacter && attackingCharacter.AIController == null) @@ -2602,6 +2696,7 @@ namespace Barotrauma mainLimb.body.ApplyLinearImpulse(impulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } } + bool wasDead = IsDead; Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir); AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound); CharacterHealth.ApplyDamage(hitLimb, attackResult); @@ -2609,6 +2704,10 @@ namespace Barotrauma { OnAttacked?.Invoke(attacker, attackResult); OnAttackedProjSpecific(attacker, attackResult); + if (!wasDead) + { + TryAdjustAttackerSkill(attacker, -attackResult.Damage); + } }; if (attacker != null && attackResult.Damage > 0.0f) @@ -2621,6 +2720,30 @@ namespace Barotrauma partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult); + public void TryAdjustAttackerSkill(Character attacker, float healthChange) + { + if (attacker == null) { return; } + + bool isEnemy = AIController is EnemyAIController || TeamID != attacker.TeamID; + if (isEnemy) + { + if (healthChange < 0.0f) + { + float attackerSkillLevel = attacker.GetSkillLevel("weapons"); + attacker.Info?.IncreaseSkillLevel("weapons", + -healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f), + attacker.WorldPosition + Vector2.UnitY * 100.0f); + } + } + else if (healthChange > 0.0f) + { + float attackerSkillLevel = attacker.GetSkillLevel("medical"); + attacker.Info?.IncreaseSkillLevel("medical", + healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f), + attacker.WorldPosition + Vector2.UnitY * 100.0f); + } + } + public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; } @@ -2702,7 +2825,7 @@ namespace Barotrauma partial void ImplodeFX(); - public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false) + public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false, bool log = true) { if (IsDead || CharacterHealth.Unkillable) { return; } @@ -2745,9 +2868,9 @@ namespace Barotrauma SteamAchievementManager.OnCharacterKilled(this, CauseOfDeath); - KillProjSpecific(causeOfDeath, causeOfDeathAffliction); + KillProjSpecific(causeOfDeath, causeOfDeathAffliction, log); - if (info != null) info.CauseOfDeath = CauseOfDeath; + if (info != null) { info.CauseOfDeath = CauseOfDeath; } AnimController.movement = Vector2.Zero; AnimController.TargetMovement = Vector2.Zero; @@ -2770,7 +2893,7 @@ namespace Barotrauma GameMain.GameSession.KillCharacter(this); } } - partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction); + partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log); public void Revive() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 37d41d47e..e1b4fe019 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -293,6 +293,9 @@ namespace Barotrauma private NPCPersonalityTrait personalityTrait; + public Order CurrentOrder { get; set;} + public string CurrentOrderOption { get; set; } + //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 public ushort ID; @@ -524,9 +527,11 @@ namespace Barotrauma } foreach (XElement subElement in infoElement.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "job") continue; - Job = new Job(subElement); - break; + if (subElement.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase)) + { + Job = new Job(subElement); + break; + } } LoadHeadAttachments(); } @@ -661,7 +666,7 @@ namespace Barotrauma { foreach (XElement limbElement in Ragdoll.MainElement.Elements()) { - if (limbElement.GetAttributeString("type", "").ToLowerInvariant() != "head") { continue; } + if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } XElement spriteElement = limbElement.Element("sprite"); if (spriteElement == null) { continue; } @@ -677,7 +682,7 @@ namespace Barotrauma //go through the files in the directory to find a matching sprite foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath))) { - if (!file.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) + if (!file.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) { continue; } @@ -828,6 +833,11 @@ namespace Barotrauma { if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; } + if (Job.Prefab.Identifier == "assistant") + { + increase *= SkillSettings.Current.AssistantSkillIncreaseMultiplier; + } + float prevLevel = Job.GetSkillLevel(skillIdentifier); Job.IncreaseSkillLevel(skillIdentifier, increase); @@ -955,7 +965,7 @@ namespace Barotrauma foreach (XElement childInvElement in itemElement.Elements()) { if (itemContainerIndex >= itemContainers.Count) break; - if (childInvElement.Name.ToString().ToLowerInvariant() != "inventory") continue; + if (!childInvElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; } SpawnInventoryItemsRecursive(itemContainers[itemContainerIndex].Inventory, childInvElement); itemContainerIndex++; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 23dc3f22d..d8f268ea5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -14,8 +14,13 @@ namespace Barotrauma public Dictionary SerializableProperties { get; set; } + protected float _strength; [Serialize(0f, true), Editable] - public float Strength { get; set; } + public virtual float Strength + { + get { return _strength; } + set { _strength = value; } + } [Serialize("", true), Editable] public string Identifier { get; private set; } @@ -38,7 +43,7 @@ namespace Barotrauma public Affliction(AfflictionPrefab prefab, float strength) { Prefab = prefab; - Strength = strength; + _strength = strength; Identifier = prefab?.Identifier; } @@ -173,11 +178,11 @@ namespace Barotrauma if (currentEffect.StrengthChange < 0) // Reduce diminishing of buffs if boosted { - Strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier; + _strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier; } else // Reduce strengthening of afflictions if resistant { - Strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab.Identifier)); + _strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab.Identifier)); } foreach (StatusEffect statusEffect in currentEffect.StatusEffects) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 445b65755..cc0509492 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using System.IO; using System.Linq; using System.Xml.Linq; +using System; namespace Barotrauma { @@ -9,7 +9,7 @@ namespace Barotrauma { public enum InfectionState { - Dormant, Transition, Active + Initial, Dormant, Transition, Active, Final } private bool subscribedToDeathEvent; @@ -17,154 +17,157 @@ namespace Barotrauma private InfectionState state; private List huskAppendage; - + + private Character character; + + private readonly List huskInfection = new List(); + + [Serialize(0f, true), Editable] + public override float Strength + { + get { return _strength; } + set + { + // Don't allow to set the strength too high (from outside) to avoid rapid transformation into husk when taking lots of damage from husks. + // If the strength is more than the value, this will effectively reset the current strength to the max. That's why we use two steps. + float max = _strength > ActiveThreshold ? ActiveThreshold + 1 : DormantThreshold - 1; + _strength = Math.Clamp(value, 0, max); + } + } + public InfectionState State { get { return state; } + private set + { + if (state == value) { return; } + state = value; + if (character != null && character == Character.Controlled) + { + UpdateMessages(); + } + } } - public AfflictionHusk(AfflictionPrefab prefab, float strength) : - base(prefab, strength) - { - } + private float DormantThreshold => Prefab.MaxStrength * 0.5f; + private float ActiveThreshold => Prefab.MaxStrength * 0.75f; + + public AfflictionHusk(AfflictionPrefab prefab, float strength) : base(prefab, strength) { } public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime) { - float prevStrength = Strength; base.Update(characterHealth, targetLimb, deltaTime); - + character = characterHealth.Character; + if (character == null) { return; } if (!subscribedToDeathEvent) { - characterHealth.Character.OnDeath += CharacterDead; + character.OnDeath += CharacterDead; subscribedToDeathEvent = true; } - - if (characterHealth.Character == Character.Controlled) UpdateMessages(prevStrength, characterHealth.Character); - if (Strength < Prefab.MaxStrength * 0.5f) + if (Strength < DormantThreshold) { - UpdateDormantState(deltaTime, characterHealth.Character); + DeactivateHusk(); + State = InfectionState.Dormant; + } + else if (Strength < ActiveThreshold) + { + DeactivateHusk(); + character.SpeechImpediment = 100; + State = InfectionState.Transition; } else if (Strength < Prefab.MaxStrength) { - characterHealth.Character.SpeechImpediment = 100.0f; - UpdateTransitionState(deltaTime, characterHealth.Character); + if (State != InfectionState.Active) + { + character.SetStun(Rand.Range(2, 4, Rand.RandSync.Server)); + } + State = InfectionState.Active; + ActivateHusk(); } else { - characterHealth.Character.SpeechImpediment = 100.0f; - UpdateActiveState(deltaTime, characterHealth.Character); + State = InfectionState.Final; + ActivateHusk(); + ApplyDamage(deltaTime, applyForce: true); + character.SetStun(1); } } - partial void UpdateMessages(float prevStrength, Character character); + partial void UpdateMessages(); - private void UpdateDormantState(float deltaTime, Character character) + private void ApplyDamage(float deltaTime, bool applyForce) { - if (state != InfectionState.Dormant) - { - DeactivateHusk(character); - } - - state = InfectionState.Dormant; - } - - private void UpdateTransitionState(float deltaTime, Character character) - { - if (state != InfectionState.Transition) - { - DeactivateHusk(character); - } - - state = InfectionState.Transition; - } - - private void UpdateActiveState(float deltaTime, Character character) - { - if (state != InfectionState.Active) - { - ActivateHusk(character); - state = InfectionState.Active; - } - foreach (Limb limb in character.AnimController.Limbs) { + float random = Rand.Value(Rand.RandSync.Server); + huskInfection.Clear(); + huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * deltaTime / character.AnimController.Limbs.Length)); character.LastDamageSource = null; - character.DamageLimb( - limb.WorldPosition, limb, - new List() { AfflictionPrefab.InternalDamage.Instantiate(0.5f * deltaTime / character.AnimController.Limbs.Length) }, - 0.0f, false, 0.0f); + float force = applyForce ? random * 0.1f * limb.Mass : 0; + character.DamageLimb(limb.WorldPosition, limb, huskInfection, 0, false, force); } } - public void ActivateHusk(Character character) + public void ActivateHusk() { - if (huskAppendage == null) + if (huskAppendage == null && character.Params.UseHuskAppendage) { huskAppendage = AttachHuskAppendage(character, Prefab.Identifier); - if (huskAppendage != null) - { - character.NeedsAir = false; - character.SetStun(0.5f); - } -#if CLIENT - character.AnimController.GetLimb(LimbType.Head).EnableHuskSprite = true; -#endif } + character.NeedsAir = false; + character.SpeechImpediment = 100; } - private void DeactivateHusk(Character character) + private void DeactivateHusk() { character.NeedsAir = character.Params.MainElement.GetAttributeBool("needsair", false); if (huskAppendage != null) { huskAppendage.ForEach(l => character.AnimController.RemoveLimb(l)); huskAppendage = null; -#if CLIENT - character.AnimController.GetLimb(LimbType.Head).EnableHuskSprite = false; -#endif } } - public void Remove(Character character) + public void Remove() { - DeactivateHusk(character); - if (character != null) character.OnDeath -= CharacterDead; + if (character == null) { return; } + DeactivateHusk(); + character.OnDeath -= CharacterDead; subscribedToDeathEvent = false; } private void CharacterDead(Character character, CauseOfDeath causeOfDeath) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (Strength < Prefab.MaxStrength * 0.5f || character.Removed) { return; } + if (Strength < ActiveThreshold || character.Removed) { return; } //don't turn the character into a husk if any of its limbs are severed if (character.AnimController?.LimbJoints != null) { foreach (var limbJoint in character.AnimController.LimbJoints) { - if (limbJoint.IsSevered) return; + if (limbJoint.IsSevered) { return; } } } //create the AI husk in a coroutine to ensure that we don't modify the character list while enumerating it - CoroutineManager.StartCoroutine(CreateAIHusk(character)); + CoroutineManager.StartCoroutine(CreateAIHusk()); } - private IEnumerable CreateAIHusk(Character character) + private IEnumerable CreateAIHusk() { character.Enabled = false; Entity.Spawner.AddToRemoveQueue(character); - string speciesName = GetHuskedSpeciesName(character.SpeciesName, Prefab as AfflictionPrefabHusk); - CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(speciesName); + string huskedSpeciesName = GetHuskedSpeciesName(character.SpeciesName, Prefab as AfflictionPrefabHusk); + CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName); if (prefab == null) { DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - husk config file not found."); yield return CoroutineStatus.Success; } - - var husk = Character.Create(speciesName, character.WorldPosition, character.Info.Name, character.Info, isRemotePlayer: false, hasAi: true, ragdoll: character.AnimController.RagdollParams); + var husk = Character.Create(huskedSpeciesName, character.WorldPosition, ToolBox.RandomSeed(8), character.Info, isRemotePlayer: false, hasAi: true); foreach (Limb limb in husk.AnimController.Limbs) { @@ -197,6 +200,11 @@ namespace Barotrauma husk.Inventory.TryPutItem(character.Inventory.Items[i], i, true, false, null); } + husk.SetStun(5); + yield return new WaitForSeconds(5, false); +#if CLIENT + husk.PlaySound(CharacterSound.SoundType.Idle); +#endif yield return CoroutineStatus.Success; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 1b0dd640d..c411e2abb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -114,6 +114,10 @@ namespace Barotrauma private List limbHealths = new List(); //non-limb-specific afflictions private List afflictions = new List(); + /// + /// Note: returns only the non-limb-secific afflictions. Use GetAllAfflictions or some other method for getting also the limb-specific afflictions. + /// + public IEnumerable Afflictions => afflictions; private HashSet irremovableAfflictions = new HashSet(); private Affliction bloodlossAffliction; @@ -160,12 +164,12 @@ namespace Barotrauma { get { - if (!Character.NeedsAir || Unkillable) return 100.0f; + if (!Character.NeedsOxygen || Unkillable) { return 100.0f; } return -oxygenLowAffliction.Strength + 100; } set { - if (!Character.NeedsAir || Unkillable) return; + if (!Character.NeedsOxygen || Unkillable) { return; } oxygenLowAffliction.Strength = MathHelper.Clamp(-value + 100, 0.0f, 200.0f); } } @@ -216,7 +220,7 @@ namespace Barotrauma limbHealths.Clear(); foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "limb") continue; + if (!subElement.Name.ToString().Equals("limb", StringComparison.OrdinalIgnoreCase)) { continue; } limbHealths.Add(new LimbHealth(subElement, this)); } if (limbHealths.Count == 0) @@ -269,30 +273,6 @@ namespace Barotrauma } } - public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true) - { - foreach (Affliction affliction in afflictions) - { - if (affliction.Prefab.Identifier == identifier) return affliction; - } - if (!allowLimbAfflictions) return null; - - foreach (LimbHealth limbHealth in limbHealths) - { - foreach (Affliction affliction in limbHealth.Afflictions) - { - if (affliction.Prefab.Identifier == identifier) return affliction; - } - } - - return null; - } - - public T GetAffliction(string identifier, bool allowLimbAfflictions = true) where T : Affliction - { - return GetAffliction(identifier, allowLimbAfflictions) as T; - } - public IEnumerable GetAfflictionsByType(string afflictionType, Limb limb) { if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) @@ -304,6 +284,37 @@ namespace Barotrauma return limbHealths[limb.HealthIndex].Afflictions.Where(a => a.Prefab.AfflictionType == afflictionType); } + public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true) + => GetAffliction(a => a.Prefab.Identifier == identifier, allowLimbAfflictions); + + public Affliction GetAfflictionOfType(string afflictionType, bool allowLimbAfflictions = true) + => GetAffliction(a => a.Prefab.AfflictionType == afflictionType, allowLimbAfflictions); + + private Affliction GetAffliction(Func predicate, bool allowLimbAfflictions = true) + { + foreach (Affliction affliction in afflictions) + { + if (predicate(affliction)) { return affliction; } + } + if (!allowLimbAfflictions) + { + return null; + } + foreach (LimbHealth limbHealth in limbHealths) + { + foreach (Affliction affliction in limbHealth.Afflictions) + { + if (predicate(affliction)) { return affliction; } + } + } + return null; + } + + public T GetAffliction(string identifier, bool allowLimbAfflictions = true) where T : Affliction + { + return GetAffliction(identifier, allowLimbAfflictions) as T; + } + public Affliction GetAffliction(string identifier, Limb limb) { if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) @@ -408,11 +419,10 @@ namespace Barotrauma return resistance; } + private List matchingAfflictions = new List(); public void ReduceAffliction(Limb targetLimb, string affliction, float amount) { - affliction = affliction.ToLowerInvariant(); - - List matchingAfflictions = new List(afflictions); + matchingAfflictions.Clear(); if (targetLimb != null) { @@ -426,8 +436,8 @@ namespace Barotrauma } } matchingAfflictions.RemoveAll(a => - a.Prefab.Identifier.ToLowerInvariant() != affliction && - a.Prefab.AfflictionType.ToLowerInvariant() != affliction); + !a.Prefab.Identifier.Equals(affliction, StringComparison.OrdinalIgnoreCase) && + !a.Prefab.AfflictionType.Equals(affliction, StringComparison.OrdinalIgnoreCase)); if (matchingAfflictions.Count == 0) return; @@ -526,7 +536,7 @@ namespace Barotrauma private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction) { if (!DoesBleed && newAffliction is AfflictionBleeding) return; - if (!Character.NeedsAir && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return; + if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return; foreach (Affliction affliction in limbHealth.Afflictions) { @@ -559,7 +569,7 @@ namespace Barotrauma private void AddAffliction(Affliction newAffliction) { if (!DoesBleed && newAffliction is AfflictionBleeding) return; - if (!Character.NeedsAir && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return; + if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return; if (newAffliction.Prefab.AfflictionType == "huskinfection") { var huskPrefab = newAffliction.Prefab as AfflictionPrefabHusk; @@ -653,7 +663,7 @@ namespace Barotrauma private void UpdateOxygen(float deltaTime) { - if (!Character.NeedsAir) return; + if (!Character.NeedsOxygen) { return; } float prevOxygen = OxygenAmount; if (IsUnconscious) @@ -691,13 +701,15 @@ namespace Barotrauma foreach (Affliction affliction in limbHealth.Afflictions) { float vitalityDecrease = affliction.GetVitalityDecrease(this); - if (limbHealth.VitalityMultipliers.ContainsKey(affliction.Prefab.Identifier.ToLowerInvariant())) + string identifier = affliction.Prefab.Identifier.ToLowerInvariant(); + string type = affliction.Prefab.AfflictionType.ToLowerInvariant(); + if (limbHealth.VitalityMultipliers.ContainsKey(identifier)) { - vitalityDecrease *= limbHealth.VitalityMultipliers[affliction.Prefab.Identifier.ToLowerInvariant()]; + vitalityDecrease *= limbHealth.VitalityMultipliers[identifier]; } - if (limbHealth.VitalityTypeMultipliers.ContainsKey(affliction.Prefab.AfflictionType.ToLowerInvariant())) + if (limbHealth.VitalityTypeMultipliers.ContainsKey(type)) { - vitalityDecrease *= limbHealth.VitalityTypeMultipliers[affliction.Prefab.AfflictionType.ToLowerInvariant()]; + vitalityDecrease *= limbHealth.VitalityTypeMultipliers[type]; } vitalityDecrease *= damageResistanceMultiplier; Vitality -= vitalityDecrease; @@ -750,6 +762,7 @@ namespace Barotrauma return new Pair(causeOfDeath, strongestAffliction); } + // TODO: this method is called a lot (every half second) -> optimize, don't create new class instances and lists every time! private List GetAllAfflictions(bool mergeSameAfflictions) { List allAfflictions = new List(afflictions); @@ -791,8 +804,7 @@ namespace Barotrauma /// /// A dictionary where the key is the identifier of the item and the value the suitability /// If true, the suitability values are normalized between 0 and 1. If not, they're arbitrary values defined in the medical item XML, where negative values are unsuitable, and positive ones suitable. - /// Amount of randomization to apply to the values (0 = the values are accurate, 1 = the values are completely random) - + /// Amount of randomization to apply to the values (0 = the values are accurate, 1 = the values are completely random) public void GetSuitableTreatments(Dictionary treatmentSuitability, bool normalize, float randomization = 0.0f) { //key = item identifier @@ -833,10 +845,18 @@ namespace Barotrauma } } + private readonly List activeAfflictions = new List(); + private readonly List> limbAfflictions = new List>(); public void ServerWrite(IWriteMessage msg) { - List activeAfflictions = afflictions.FindAll(a => a.Strength > 0.0f && a.Strength >= a.Prefab.ActivationThreshold); - + activeAfflictions.Clear(); + foreach (var affliction in afflictions) + { + if (affliction.Strength > 0.0f && affliction.Strength >= affliction.Prefab.ActivationThreshold) + { + activeAfflictions.Add(affliction); + } + } msg.Write((byte)activeAfflictions.Count); foreach (Affliction affliction in activeAfflictions) { @@ -846,7 +866,7 @@ namespace Barotrauma 0.0f, affliction.Prefab.MaxStrength, 8); } - List> limbAfflictions = new List>(); + limbAfflictions.Clear(); foreach (LimbHealth limbHealth in limbHealths) { foreach (Affliction limbAffliction in limbHealth.Afflictions) @@ -873,5 +893,11 @@ namespace Barotrauma } partial void RemoveProjSpecific(); + + /// + /// Automatically filters out buffs. + /// + public static IEnumerable SortAfflictionsBySeverity(IEnumerable afflictions) => + afflictions.Where(a => !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index 85bb5925c..68a0cf326 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -62,7 +62,7 @@ namespace Barotrauma skills = new Dictionary(); foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "skill") { continue; } + if (!subElement.Name.ToString().Equals("skill", System.StringComparison.OrdinalIgnoreCase)) { continue; } string skillIdentifier = subElement.GetAttributeString("identifier", ""); if (string.IsNullOrEmpty(skillIdentifier)) { continue; } skills.Add( diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index b6ffe5ed0..3b738e1b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -146,6 +146,7 @@ namespace Barotrauma private set; } + // TODO: not used [Serialize(10.0f, false)] public float Commonness { @@ -241,7 +242,7 @@ namespace Barotrauma } - public static JobPrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(sync); + public static JobPrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(p => p.Identifier != "watchman", sync); public static void LoadAll(IEnumerable files) { @@ -262,7 +263,7 @@ namespace Barotrauma } foreach (XElement element in mainElement.Elements()) { - if (element.Name.ToString().ToLowerInvariant() == "nojob") { continue; } + if (element.Name.ToString().Equals("nojob", StringComparison.OrdinalIgnoreCase)) { continue; } if (element.IsOverride()) { var job = new JobPrefab(element.FirstElement(), file.Path) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 1feb334d1..b990d25d7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -17,7 +17,7 @@ namespace Barotrauma public enum LimbType { None, LeftHand, RightHand, LeftArm, RightArm, LeftForearm, RightForearm, - LeftLeg, RightLeg, LeftFoot, RightFoot, Head, Torso, Tail, Legs, RightThigh, LeftThigh, Waist + LeftLeg, RightLeg, LeftFoot, RightFoot, Head, Torso, Tail, Legs, RightThigh, LeftThigh, Waist, Jaw }; partial class LimbJoint : RevoluteJoint @@ -28,6 +28,8 @@ namespace Barotrauma public readonly Ragdoll ragdoll; public readonly Limb LimbA, LimbB; + public float Scale => Params.Scale * ragdoll.RagdollParams.JointScale; + public LimbJoint(Limb limbA, Limb limbB, JointParams jointParams, Ragdoll ragdoll) : this(limbA, limbB, Vector2.One, Vector2.One) { Params = jointParams; @@ -59,15 +61,15 @@ namespace Barotrauma } if (ragdoll.IsFlipped) { - LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb1Anchor.X, Params.Limb1Anchor.Y) * Params.Ragdoll.JointScale); - LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb2Anchor.X, Params.Limb2Anchor.Y) * Params.Ragdoll.JointScale); + LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb1Anchor.X, Params.Limb1Anchor.Y) * Scale); + LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb2Anchor.X, Params.Limb2Anchor.Y) * Scale); UpperLimit = MathHelper.ToRadians(-Params.LowerLimit); LowerLimit = MathHelper.ToRadians(-Params.UpperLimit); } else { - LocalAnchorA = ConvertUnits.ToSimUnits(Params.Limb1Anchor * Params.Ragdoll.JointScale); - LocalAnchorB = ConvertUnits.ToSimUnits(Params.Limb2Anchor * Params.Ragdoll.JointScale); + LocalAnchorA = ConvertUnits.ToSimUnits(Params.Limb1Anchor * Scale); + LocalAnchorB = ConvertUnits.ToSimUnits(Params.Limb2Anchor * Scale); UpperLimit = MathHelper.ToRadians(Params.UpperLimit); LowerLimit = MathHelper.ToRadians(Params.LowerLimit); } @@ -125,9 +127,29 @@ namespace Barotrauma private Direction dir; public int HealthIndex => Params.HealthIndex; - public float Scale => Params.Ragdoll.LimbScale; + public float Scale => Params.Scale * Params.Ragdoll.LimbScale; public float AttackPriority => Params.AttackPriority; - public bool DoesFlip => Params.Flip; + public bool DoesFlip + { + get + { + if (character.AnimController.CurrentAnimationParams is GroundedMovementParams) + { + switch (type) + { + case LimbType.LeftFoot: + case LimbType.LeftLeg: + case LimbType.LeftThigh: + case LimbType.RightFoot: + case LimbType.RightLeg: + case LimbType.RightThigh: + // Legs always has to flip + return true; + } + } + return Params.Flip; + } + } public float SteerForce => Params.SteerForce; @@ -654,33 +676,37 @@ namespace Barotrauma } Vector2 diff = attackSimPos - SimPosition; - bool applyForces = (!attack.ApplyForcesOnlyOnce || !wasRunning) && diff.LengthSquared() > 0.00001f; + bool applyForces = !attack.ApplyForcesOnlyOnce || !wasRunning; if (applyForces) { - if (attack.ForceOnLimbIndices != null && attack.ForceOnLimbIndices.Count > 0) { foreach (int limbIndex in attack.ForceOnLimbIndices) { - if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) continue; - + if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) { continue; } Limb limb = character.AnimController.Limbs[limbIndex]; - limb.body.ApplyTorque(limb.Mass * character.AnimController.Dir * attack.Torque); + diff = attackSimPos - limb.SimPosition; + if (diff == Vector2.Zero) { continue; } + limb.body.ApplyTorque(limb.Mass * character.AnimController.Dir * attack.Torque * limb.Params.AttackForceMultiplier); Vector2 forcePos = limb.pullJoint == null ? limb.body.SimPosition : limb.pullJoint.WorldAnchorA; - limb.body.ApplyLinearImpulse(limb.Mass * attack.Force * Vector2.Normalize(attackSimPos - SimPosition), forcePos, - maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + limb.body.ApplyLinearImpulse(limb.Mass * attack.Force * limb.Params.AttackForceMultiplier * Vector2.Normalize(diff), forcePos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } } - else + else if (diff != Vector2.Zero) { - body.ApplyTorque(Mass * character.AnimController.Dir * attack.Torque); + body.ApplyTorque(Mass * character.AnimController.Dir * attack.Torque * Params.AttackForceMultiplier); Vector2 forcePos = pullJoint == null ? body.SimPosition : pullJoint.WorldAnchorA; - body.ApplyLinearImpulse( - Mass * attack.Force * Vector2.Normalize(attackSimPos - SimPosition), - forcePos, - maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + body.ApplyLinearImpulse(Mass * attack.Force * Params.AttackForceMultiplier * Vector2.Normalize(diff), forcePos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } } + Vector2 forceWorld = attack.CalculateAttackPhase(attack.RootTransitionEasing); + forceWorld.X *= character.AnimController.Dir; + character.AnimController.MainLimb.body.ApplyLinearImpulse(character.Mass * forceWorld, character.SimPosition, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + if (!attack.IsRunning) + { + // Set the main collider where the body lands after the attack + character.AnimController.Collider.SetTransform(character.AnimController.MainLimb.body.SimPosition, rotation: 0); + } return wasHit; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index 91c4e209d..9f97ec0be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -125,7 +125,7 @@ namespace Barotrauma public static string GetFolder(XDocument doc, string filePath) { var folder = doc.Root?.Element("animations")?.GetAttributeString("folder", string.Empty); - if (string.IsNullOrEmpty(folder) || folder.ToLowerInvariant() == "default") + if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase)) { folder = Path.Combine(Path.GetDirectoryName(filePath), "Animations"); } @@ -198,7 +198,7 @@ namespace Barotrauma } else { - selectedFile = filteredFiles.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).ToLowerInvariant() == fileName.ToLowerInvariant()); + selectedFile = filteredFiles.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); if (selectedFile == null) { DebugConsole.ThrowError($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 8e3333721..52d131b72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -28,18 +28,30 @@ namespace Barotrauma [Serialize(false, true), Editable] public bool Humanoid { get; private set; } + [Serialize(false, true), Editable] + public bool HasInfo { get; private set; } + [Serialize(false, true), Editable] public bool Husk { get; private set; } + [Serialize(false, true), Editable] + public bool UseHuskAppendage { get; private set; } + [Serialize(false, true), Editable] public bool NeedsAir { get; set; } + [Serialize(false, true, description: "Can the creature live without water or does it die on dry land?"), Editable] + public bool NeedsWater { get; set; } + [Serialize(false, true), Editable] public bool CanSpeak { get; set; } [Serialize(100f, true, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 1000f)] public float Noise { get; set; } + [Serialize(100f, true, description: "How visible the character is?"), Editable(minValue: 0f, maxValue: 1000f)] + public float Visibility { get; set; } + [Serialize("blood", true), Editable] public string BloodDecal { get; private set; } @@ -450,6 +462,12 @@ namespace Barotrauma [Serialize(false, true, description: "Does the character try to break inside the sub?"), Editable()] public bool AggressiveBoarding { get; private set; } + [Serialize(true, true, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable()] + public bool EnforceAggressiveBehaviorForMissions { get; private set; } + + [Serialize(false, true, description: "Should the character target or ignore walls when it's inside the submarine. Doesn't have any effect if no target priority for walls is defined."), Editable()] + public bool TargetInnerWalls { get; private set; } + // TODO: latchonto, swarming public IEnumerable Targets => targets; @@ -538,6 +556,9 @@ namespace Barotrauma [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)] 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; } + 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 f9e20691e..0101cff43 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -94,8 +94,7 @@ namespace Barotrauma public static string GetFolder(string speciesName, ContentPackage contentPackage = null) { - CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier.ToLowerInvariant()==speciesName.ToLowerInvariant() && - (contentPackage==null || p.ContentPackage == contentPackage)); + CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier.Equals(speciesName, StringComparison.OrdinalIgnoreCase) && (contentPackage == null || p.ContentPackage == contentPackage)); if (prefab?.XDocument == null) { DebugConsole.ThrowError($"Failed to find config file for '{speciesName}' (content package {contentPackage?.Name ?? "null"})"); @@ -107,7 +106,7 @@ namespace Barotrauma public static string GetFolder(XDocument doc, string filePath) { var folder = doc.Root?.Element("ragdolls")?.GetAttributeString("folder", string.Empty); - if (string.IsNullOrEmpty(folder) || folder.ToLowerInvariant() == "default") + if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase)) { folder = Path.Combine(Path.GetDirectoryName(filePath), "Ragdolls") + Path.DirectorySeparatorChar; } @@ -150,7 +149,7 @@ namespace Barotrauma } else { - selectedFile = files.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).ToLowerInvariant() == fileName.ToLowerInvariant()); + selectedFile = files.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); if (selectedFile == null) { DebugConsole.ThrowError($"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll."); @@ -489,6 +488,9 @@ namespace Barotrauma [Serialize(0.25f, true), Editable] public float Stiffness { get; set; } + [Serialize(1f, true, description: "CAUTION: Not fully implemented. Only use for limb joints that connect non-animated limbs!"), Editable] + public float Scale { get; set; } + public JointParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { } } @@ -591,6 +593,18 @@ namespace Barotrauma [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; } + // Non-editable -> [Serialize(0, true)] public int HealthIndex { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs index f0814daed..abe6aab58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs @@ -65,6 +65,37 @@ namespace Barotrauma set { skillIncreasePerFabricatorRequiredSkill = value; } } + private float skillIncreasePerHostileDamage; + [Serialize(0.01f, true)] + public float SkillIncreasePerHostileDamage + { + get { return skillIncreasePerHostileDamage * GetCurrentSkillGainMultiplier(); } + set { skillIncreasePerHostileDamage = value; } + } + + private float skillIncreasePerSecondWhenOperatingTurret; + [Serialize(0.001f, true)] + public float SkillIncreasePerSecondWhenOperatingTurret + { + get { return skillIncreasePerSecondWhenOperatingTurret * GetCurrentSkillGainMultiplier(); } + set { skillIncreasePerSecondWhenOperatingTurret = value; } + } + + private float skillIncreasePerFriendlyHealed; + [Serialize(0.001f, true)] + public float SkillIncreasePerFriendlyHealed + { + get { return skillIncreasePerFriendlyHealed * GetCurrentSkillGainMultiplier(); } + set { skillIncreasePerFriendlyHealed = value; } + } + + [Serialize(1.1f, true)] + public float AssistantSkillIncreaseMultiplier + { + get; + set; + } + private SkillSettings(XElement element) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs index 7087bfb5b..a9fe770d2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs @@ -39,7 +39,10 @@ namespace Barotrauma TraitorMissions, EventManagerSettings, Orders, - SkillSettings + SkillSettings, + Wreck, + Corpses, + WreckAIConfig } public class ContentPackage @@ -63,8 +66,10 @@ namespace Barotrauma ContentType.LevelObjectPrefabs, ContentType.RuinConfig, ContentType.Outpost, + ContentType.Wreck, ContentType.Afflictions, - ContentType.Orders + ContentType.Orders, + ContentType.Corpses }; //at least one file of each these types is required in core content packages @@ -75,6 +80,7 @@ namespace Barotrauma ContentType.Character, ContentType.Structure, ContentType.Outpost, + ContentType.Wreck, ContentType.Text, ContentType.Executable, ContentType.ServerExecutable, @@ -87,7 +93,8 @@ namespace Barotrauma ContentType.Afflictions, ContentType.UIStyle, ContentType.EventManagerSettings, - ContentType.Orders + ContentType.Orders, + ContentType.Corpses }; public static IEnumerable CorePackageRequiredFiles @@ -284,6 +291,7 @@ namespace Barotrauma case ContentType.None: case ContentType.Outpost: case ContentType.Submarine: + case ContentType.Wreck: break; default: try @@ -364,7 +372,10 @@ namespace Barotrauma { if (Files.Find(file => file.Path == path && file.Type == type) != null) return null; - ContentFile cf = new ContentFile(path, type); + ContentFile cf = new ContentFile(path, type) + { + ContentPackage = this + }; Files.Add(cf); return cf; @@ -460,12 +471,12 @@ namespace Barotrauma XDocument doc = XMLExtensions.TryLoadXml(file.Path); var rootElement = doc.Root; var element = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement; - var ragdollFolder = RagdollParams.GetFolder(doc, file.Path); + var ragdollFolder = RagdollParams.GetFolder(doc, file.Path).CleanUpPathCrossPlatform(true); if (Directory.Exists(ragdollFolder)) { Directory.GetFiles(ragdollFolder, "*.xml").ForEach(f => filePaths.Add(f)); } - var animationFolder = AnimationParams.GetFolder(doc, file.Path); + var animationFolder = AnimationParams.GetFolder(doc, file.Path).CleanUpPathCrossPlatform(true); if (Directory.Exists(animationFolder)) { Directory.GetFiles(animationFolder, "*.xml").ForEach(f => filePaths.Add(f)); @@ -508,14 +519,22 @@ namespace Barotrauma return IsModFilePathAllowed(path); } /// - /// Are mods allowed to install a file into the specified path. If a content package XML includes files - /// with a prohibited path, they are treated as references to external files. For example, a mod could include - /// some vanilla files in the XML, in which case the game will simply use the vanilla files present in the game folder. + /// Returns whether mods are allowed to install a file into the specified path. + /// Currently mods are only allowed to install files into the Mods folder. + /// The only exception to this rule is the Vanilla content package. /// /// /// public static bool IsModFilePathAllowed(string path) { + if (GameMain.VanillaContent.Files.Any(f => string.Equals(System.IO.Path.GetFullPath(f.Path).CleanUpPath(), + System.IO.Path.GetFullPath(path).CleanUpPath(), + StringComparison.InvariantCultureIgnoreCase))) + { + //file is in vanilla package, this is allowed + return true; + } + while (true) { string temp = System.IO.Path.GetDirectoryName(path); @@ -573,7 +592,13 @@ namespace Barotrauma { if (System.IO.Path.GetFileName(modDirectory.TrimEnd(System.IO.Path.DirectorySeparatorChar)) == "ExampleMod") { continue; } string modFilePath = System.IO.Path.Combine(modDirectory, Steam.SteamManager.MetadataFileName); - if (File.Exists(modFilePath)) + string copyingFilePath = System.IO.Path.Combine(modDirectory, Steam.SteamManager.CopyIndicatorFileName); + if (File.Exists(copyingFilePath)) + { + //this mod didn't clean up its copying file; assume it's corrupted and delete it + Directory.Delete(modDirectory, true); + } + else if (File.Exists(modFilePath)) { List.Add(new ContentPackage(modFilePath)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs b/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs index 47b732af0..6ef497afd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs @@ -186,6 +186,7 @@ namespace Barotrauma #endif if (handle.Thread == null) { + if (handle.AbortRequested) { return true; } if (handle.Coroutine.Current != null) { WaitForSeconds wfs = handle.Coroutine.Current as WaitForSeconds; diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 455ebca33..88728b6fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -475,10 +475,7 @@ namespace Barotrauma commands.Add(new Command("teleportcharacter|teleport", "teleport [character name]: Teleport the specified character to the position of the cursor. If the name parameter is omitted, the controlled character will be teleported.", null, () => { - return new string[][] - { - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() - }; + return new string[][] { ListCharacterNames() }; }, isCheat: true)); commands.Add(new Command("godmode", "godmode: Toggle submarine godmode. Makes the main submarine invulnerable to damage.", (string[] args) => @@ -531,18 +528,17 @@ namespace Barotrauma commands.Add(new Command("findentityids", "findentityids [entityname]", (string[] args) => { - if (args.Length == 0) return; - args[0] = args[0].ToLowerInvariant(); + if (args.Length == 0) { return; } foreach (MapEntity mapEntity in MapEntity.mapEntityList) { - if (mapEntity.Name.ToLowerInvariant() == args[0]) + if (mapEntity.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)) { ThrowError(mapEntity.ID + ": " + mapEntity.Name.ToString()); } } foreach (Character character in Character.CharacterList) { - if (character.Name.ToLowerInvariant() == args[0] || character.SpeciesName.ToLowerInvariant() == args[0]) + if (character.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || character.SpeciesName.Equals(args[0], StringComparison.OrdinalIgnoreCase)) { ThrowError(character.ID + ": " + character.Name.ToString()); } @@ -554,8 +550,8 @@ namespace Barotrauma if (args.Length < 2) return; AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => - a.Name.ToLowerInvariant() == args[0].ToLowerInvariant() || - a.Identifier.ToLowerInvariant() == args[0].ToLowerInvariant()); + a.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || + a.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab == null) { ThrowError("Affliction \"" + args[0] + "\" not found."); @@ -695,18 +691,20 @@ namespace Barotrauma NewMessage("Level seed: " + Level.Loaded.Seed); } },null)); - -#if DEBUG - commands.Add(new Command("crash", "crash: Crashes the game.", (string[] args) => - { - throw new Exception("crash command issued"); - })); - + commands.Add(new Command("teleportsub", "teleportsub [start/end]: Teleport the submarine to the start or end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) => { if (Submarine.MainSub == null || Level.Loaded == null) return; - if (args.Length > 0 && args[0].ToLowerInvariant() == "start") + if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase)) + { +#if SERVER + ThrowError("Cannot teleport the sub to the position of the cursor. Use \"start\" or \"end\", or execute the command as a client."); +#else + Submarine.MainSub.SetPosition(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition)); +#endif + } + else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase)) { Submarine.MainSub.SetPosition(Level.Loaded.StartPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); } @@ -714,8 +712,21 @@ namespace Barotrauma { Submarine.MainSub.SetPosition(Level.Loaded.EndPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); } + }, + () => + { + return new string[][] + { + new string[] { "start", "end", "cursor" } + }; }, isCheat: true)); +#if DEBUG + commands.Add(new Command("crash", "crash: Crashes the game.", (string[] args) => + { + throw new Exception("crash command issued"); + })); + commands.Add(new Command("removecharacter", "removecharacter [character name]: Immediately deletes the specified character.", (string[] args) => { if (args.Length == 0) { return; } @@ -751,18 +762,18 @@ namespace Barotrauma IEnumerable TestLevels() { - Submarine selectedSub = null; + SubmarineInfo selectedSub = null; string subName = GameMain.Config.QuickStartSubmarineName; if (!string.IsNullOrEmpty(subName)) { - selectedSub = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name.ToLower() == subName.ToLower()); + selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.ToLower() == subName.ToLower()); } int count = 0; while (true) { var gamesession = new GameSession( - Submarine.SavedSubmarines.GetRandom(s => !s.HasTag(SubmarineTag.HideInMenus)), + SubmarineInfo.SavedSubmarines.GetRandom(s => !s.HasTag(SubmarineTag.HideInMenus)), "Data/Saves/test.xml", GameModePreset.List.Find(gm => gm.Identifier == "devsandbox"), missionPrefab: null); @@ -776,7 +787,7 @@ namespace Barotrauma { if (ruin.Area.Intersects(subWorldRect)) { - ThrowError("Ruins intersect with the sub. Seed: " + seed + ", Submarine: " + Submarine.MainSub.Name); + ThrowError("Ruins intersect with the sub. Seed: " + seed + ", Submarine: " + Submarine.MainSub.Info.Name); yield return CoroutineStatus.Success; } } @@ -797,7 +808,7 @@ namespace Barotrauma (int)(maxExtents.X - minExtents.X), (int)(maxExtents.Y - minExtents.Y)); if (cellRect.Intersects(subWorldRect)) { - ThrowError("Level cells intersect with the sub. Seed: " + seed + ", Submarine: " + Submarine.MainSub.Name); + ThrowError("Level cells intersect with the sub. Seed: " + seed + ", Submarine: " + Submarine.MainSub.Info.Name); yield return CoroutineStatus.Success; } } @@ -816,7 +827,7 @@ namespace Barotrauma } #endif - commands.Add(new Command("fixitems", "fixitems: Repairs all items and restores them to full condition.", (string[] args) => + commands.Add(new Command("fixitems", "fixitems: Repairs all items and restores them to full condition.", (string[] args) => { foreach (Item it in Item.ItemList) { @@ -1097,10 +1108,7 @@ namespace Barotrauma //TODO: alphabetical order? commands.Add(new Command("control", "control [character name]: Start controlling the specified character (client-only).", null, () => { - return new string[][] - { - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() - }; + return new string[][] { ListCharacterNames() }; })); commands.Add(new Command("los", "Toggle the line of sight effect on/off (client-only).", null, isCheat: true)); commands.Add(new Command("lighting|lights", "Toggle lighting on/off (client-only).", null, isCheat: true)); @@ -1220,15 +1228,17 @@ namespace Barotrauma return; } - if (!splitCommand[0].ToLowerInvariant().Equals("admin")) + string firstCommand = splitCommand[0].ToLowerInvariant(); + + if (!firstCommand.Equals("admin", StringComparison.OrdinalIgnoreCase)) { NewMessage(command, Color.White, true); } - + #if CLIENT if (GameMain.Client != null) { - Command matchingCommand = commands.Find(c => c.names.Contains(splitCommand[0].ToLowerInvariant())); + Command matchingCommand = commands.Find(c => c.names.Contains(firstCommand)); if (matchingCommand == null) { //if the command is not defined client-side, we'll relay it anyway because it may be a custom command at the server's side @@ -1236,7 +1246,7 @@ namespace Barotrauma NewMessage("Server command: " + command, Color.Cyan); return; } - else if (GameMain.Client.HasConsoleCommandPermission(splitCommand[0].ToLowerInvariant())) + else if (GameMain.Client.HasConsoleCommandPermission(firstCommand)) { if (matchingCommand.RelayToServer) { @@ -1249,13 +1259,20 @@ namespace Barotrauma } return; } +#if !DEBUG + if (!IsCommandPermitted(splitCommand[0].ToLowerInvariant(), GameMain.Client)) + { + ThrowError("You're not permitted to use the command \"" + splitCommand[0].ToLowerInvariant() + "\"!"); + return; + } +#endif } #endif bool commandFound = false; foreach (Command c in commands) { - if (!c.names.Contains(splitCommand[0].ToLowerInvariant())) continue; + if (!c.names.Contains(firstCommand)) { continue; } c.Execute(splitCommand.Skip(1).ToArray()); commandFound = true; break; @@ -1266,7 +1283,9 @@ namespace Barotrauma ThrowError("Command \"" + splitCommand[0] + "\" not found."); } } - + + private static string[] ListCharacterNames() => Character.CharacterList.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).Select(c => c.Name).Distinct().ToArray(); + private static Character FindMatchingCharacter(string[] args, bool ignoreRemotePlayers = false, Client allowedRemotePlayer = null) { if (args.Length == 0) return null; @@ -1283,7 +1302,7 @@ namespace Barotrauma } var matchingCharacters = Character.CharacterList.FindAll(c => - c.Name.ToLowerInvariant() == characterName && + c.Name.Equals(characterName, StringComparison.OrdinalIgnoreCase) && (!c.IsRemotePlayer || !ignoreRemotePlayers || allowedRemotePlayer?.Character == c)); if (!matchingCharacters.Any()) @@ -1329,7 +1348,7 @@ namespace Barotrauma JobPrefab job = null; if (!JobPrefab.Prefabs.ContainsKey(characterLowerCase)) { - job = JobPrefab.Prefabs.Find(jp => jp.Name?.ToLowerInvariant() == characterLowerCase); + job = JobPrefab.Prefabs.Find(jp => jp.Name != null && jp.Name.Equals(characterLowerCase, StringComparison.OrdinalIgnoreCase)); } else { @@ -1587,12 +1606,7 @@ namespace Barotrauma return true; } - public static Command FindCommand(string commandName) - { - commandName = commandName.ToLowerInvariant(); - return commands.Find(c => c.names.Any(n => n.ToLowerInvariant() == commandName)); - } - + public static Command FindCommand(string commandName) => commands.Find(c => c.names.Any(n => n.Equals(commandName, StringComparison.OrdinalIgnoreCase))); public static void Log(string message) { @@ -1610,8 +1624,25 @@ namespace Barotrauma } } System.Diagnostics.Debug.WriteLine(error); - NewMessage(error, Color.Red); #if CLIENT + if (listBox == null) { NewMessage(error, Color.Red); return; } + + var textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), style: "InnerFrame", color: Color.White) + { + CanBeFocused = false + }; + var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width - 5, 0), textContainer.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(2, 2) }, + error, textAlignment: Alignment.TopLeft, font: GUI.SmallFont, wrap: true) + { + CanBeFocused = false, + TextColor = Color.Red + }; + textContainer.RectTransform.NonScaledSize = new Point(textContainer.RectTransform.NonScaledSize.X, textBlock.RectTransform.NonScaledSize.Y + 5); + textBlock.SetTextPos(); + + listBox.UpdateScrollBarSize(); + listBox.BarScroll = 1.0f; + if (createMessageBox) { CoroutineManager.StartCoroutine(CreateMessageBox(error)); @@ -1620,6 +1651,8 @@ namespace Barotrauma { isOpen = true; } +#else + NewMessage(error, Color.Red); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs new file mode 100644 index 000000000..cdcb77796 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Barotrauma +{ + public enum TransitionMode + { + Linear, + Smooth, + Smoother, + EaseIn, + EaseOut, + Exponential + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index 2dd33710c..34ec9fe34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -1,6 +1,4 @@ using Microsoft.Xna.Framework; -using System; -using System.Xml.Linq; namespace Barotrauma { @@ -53,7 +51,7 @@ namespace Barotrauma public override void Init(bool affectSubImmediately) { spawnPos = Level.Loaded.GetRandomItemPos( - (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < 0.5f) ? Level.PositionType.MainPath : Level.PositionType.Cave | Level.PositionType.Ruin, + (Rand.Value(Rand.RandSync.Server) < 0.5f) ? Level.PositionType.MainPath : Level.PositionType.Cave | Level.PositionType.Ruin, 500.0f, 10000.0f, 30.0f); spawnPending = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 01dd3c557..f8e552ad9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -219,13 +218,44 @@ namespace Barotrauma private void CreateEvents(ScriptedEventSet eventSet) { - if (eventSet.ChooseRandom) + int applyCount = 1; + if (eventSet.PerRuin) { - if (eventSet.EventPrefabs.Count > 0) + applyCount = Level.Loaded.Ruins.Count(); + } + else if (eventSet.PerWreck) + { + applyCount = Submarine.Loaded.Count(s => s.Info.IsWreck && (s.ThalamusAI == null || !s.ThalamusAI.IsAlive)); + } + for (int i = 0; i < applyCount; i++) + { + if (eventSet.ChooseRandom) { - MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed)); - var eventPrefab = ToolBox.SelectWeightedRandom(eventSet.EventPrefabs, eventSet.EventPrefabs.Select(e => e.Commonness).ToList(), rand); - if (eventPrefab != null) + if (eventSet.EventPrefabs.Count > 0) + { + MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed)); + var eventPrefab = ToolBox.SelectWeightedRandom(eventSet.EventPrefabs, eventSet.EventPrefabs.Select(e => e.Commonness).ToList(), rand); + if (eventPrefab != null) + { + var newEvent = eventPrefab.CreateInstance(); + newEvent.Init(true); + DebugConsole.Log("Initialized event " + newEvent.ToString()); + if (!selectedEvents.ContainsKey(eventSet)) + { + selectedEvents.Add(eventSet, new List()); + } + selectedEvents[eventSet].Add(newEvent); + } + } + if (eventSet.ChildSets.Count > 0) + { + var newEventSet = SelectRandomEvents(eventSet.ChildSets); + if (newEventSet != null) { CreateEvents(newEventSet); } + } + } + else + { + foreach (ScriptedEventPrefab eventPrefab in eventSet.EventPrefabs) { var newEvent = eventPrefab.CreateInstance(); newEvent.Init(true); @@ -236,30 +266,11 @@ namespace Barotrauma } selectedEvents[eventSet].Add(newEvent); } - } - if (eventSet.ChildSets.Count > 0) - { - var newEventSet = SelectRandomEvents(eventSet.ChildSets); - if (newEventSet != null) { CreateEvents(newEventSet); } - } - } - else - { - foreach (ScriptedEventPrefab eventPrefab in eventSet.EventPrefabs) - { - var newEvent = eventPrefab.CreateInstance(); - newEvent.Init(true); - DebugConsole.Log("Initialized event " + newEvent.ToString()); - if (!selectedEvents.ContainsKey(eventSet)) - { - selectedEvents.Add(eventSet, new List()); - } - selectedEvents[eventSet].Add(newEvent); - } - foreach (ScriptedEventSet childEventSet in eventSet.ChildSets) - { - CreateEvents(childEventSet); + foreach (ScriptedEventSet childEventSet in eventSet.ChildSets) + { + CreateEvents(childEventSet); + } } } } @@ -296,11 +307,14 @@ namespace Barotrauma 0.0f, 1.0f); //don't create new events if within 50 meters of the start/end of the level - if (distanceTraveled <= 0.0f || - distFromStart * Physics.DisplayToRealWorldRatio < 50.0f || - distFromEnd * Physics.DisplayToRealWorldRatio < 50.0f) + if (!eventSet.AllowAtStart) { - return false; + if (distanceTraveled <= 0.0f || + distFromStart * Physics.DisplayToRealWorldRatio < 50.0f || + distFromEnd * Physics.DisplayToRealWorldRatio < 50.0f) + { + return false; + } } if ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) && @@ -368,17 +382,15 @@ namespace Barotrauma pendingEventSets.RemoveAt(i); - if (!selectedEvents.ContainsKey(eventSet)) + if (selectedEvents.ContainsKey(eventSet)) { - //no events selected from this event set - continue; + //start events in this set + foreach (ScriptedEvent scriptedEvent in selectedEvents[eventSet]) + { + activeEvents.Add(scriptedEvent); + } } - //start events in this set - foreach (ScriptedEvent scriptedEvent in selectedEvents[eventSet]) - { - activeEvents.Add(scriptedEvent); - } //add child event sets to pending foreach (ScriptedEventSet childEventSet in eventSet.ChildSets) { @@ -431,7 +443,7 @@ namespace Barotrauma enemyDanger = 0.0f; foreach (Character character in Character.CharacterList) { - if (character.IsDead || character.IsUnconscious || !character.Enabled) continue; + if (character.IsDead || character.IsIncapacitated || !character.Enabled) continue; EnemyAIController enemyAI = character.AIController as EnemyAIController; if (enemyAI == null) continue; @@ -458,7 +470,7 @@ namespace Barotrauma int hullCount = 0; foreach (Hull hull in Hull.hullList) { - if (hull.Submarine == null || hull.Submarine.IsOutpost) { continue; } + if (hull.Submarine == null || hull.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } hullCount++; foreach (Gap gap in hull.ConnectedGaps) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index cb4973991..c5960b7e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -69,7 +69,7 @@ namespace Barotrauma return; } - WayPoint cargoSpawnPos = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub, true); + WayPoint cargoSpawnPos = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub, useSyncedRand: true); if (cargoSpawnPos == null) { DebugConsole.ThrowError("Couldn't spawn items for cargo mission, cargo spawnpoint not found"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs index 6e5da3bf8..20c2dafd9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs @@ -44,6 +44,11 @@ namespace Barotrauma } } + public override int TeamCount + { + get { return 2; } + } + public CombatMission(MissionPrefab prefab, Location[] locations) : base(prefab, locations) { @@ -113,7 +118,7 @@ namespace Barotrauma { for (int i = 0; i < 2; i++) { - if (wifiComponent.Item.Submarine == subs[i] || subs[i].DockedTo.Contains(wifiComponent.Item.Submarine)) + if (wifiComponent.Item.Submarine == subs[i] || subs[i].ConnectedDockingPorts.ContainsKey(wifiComponent.Item.Submarine)) { wifiComponent.TeamID = subs[i].TeamID; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 3386a00ec..7b9ddacd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -74,6 +74,11 @@ namespace Barotrauma get { return true; } } + public virtual int TeamCount + { + get { return 1; } + } + public virtual IEnumerable SonarPositions { get { return Enumerable.Empty(); } @@ -136,7 +141,7 @@ namespace Barotrauma } else { - allowedMissions.AddRange(MissionPrefab.List.Where(m => ((int)(missionType & m.type)) != 0)); + allowedMissions.AddRange(MissionPrefab.List.Where(m => ((int)(missionType & m.Type)) != 0)); } allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly); @@ -168,10 +173,9 @@ namespace Barotrauma public virtual void Update(float deltaTime) { } - public virtual bool AssignTeamIDs(List clients) + public virtual void AssignTeamIDs(List clients) { clients.ForEach(c => c.TeamID = Character.TeamType.Team1); - return false; } protected void ShowMessage(int missionState) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index aa0092485..a354e7972 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -31,7 +31,7 @@ namespace Barotrauma private readonly ConstructorInfo constructor; - public readonly MissionType type; + public readonly MissionType Type; public readonly bool MultiplayerOnly, SingleplayerOnly; @@ -154,18 +154,18 @@ namespace Barotrauma } string missionTypeName = element.GetAttributeString("type", ""); - if (!Enum.TryParse(missionTypeName, out type)) + if (!Enum.TryParse(missionTypeName, out Type)) { DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type."); return; } - if (type == MissionType.None) + if (Type == MissionType.None) { DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - mission type cannot be none."); return; } - constructor = missionClasses[type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) }); + constructor = missionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) }); InitProjSpecific(element); } @@ -176,11 +176,11 @@ namespace Barotrauma { foreach (Pair allowedLocationType in AllowedLocationTypes) { - if (allowedLocationType.First.ToLowerInvariant() == "any" || - allowedLocationType.First.ToLowerInvariant() == from.Type.Identifier.ToLowerInvariant()) + if (allowedLocationType.First.Equals("any", StringComparison.OrdinalIgnoreCase) || + allowedLocationType.First.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase)) { - if (allowedLocationType.Second.ToLowerInvariant() == "any" || - allowedLocationType.Second.ToLowerInvariant() == to.Type.Identifier.ToLowerInvariant()) + if (allowedLocationType.Second.Equals("any", StringComparison.OrdinalIgnoreCase) || + allowedLocationType.Second.Equals(to.Type.Identifier, StringComparison.OrdinalIgnoreCase)) { return true; } @@ -189,7 +189,7 @@ namespace Barotrauma return false; } - + public Mission Instantiate(Location[] locations) { return constructor?.Invoke(new object[] { this, locations }) as Mission; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index 06a19a7c8..88c43fabb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -2,9 +2,6 @@ using System.Collections.Generic; using System.Linq; using System; -using System.Xml.Linq; -using Barotrauma.Extensions; -using Barotrauma.Networking; namespace Barotrauma { @@ -113,7 +110,25 @@ namespace Barotrauma private void InitializeMonsters(IEnumerable monsters) { - monsters.ForEach(m => m.Enabled = false); + foreach (var monster in monsters) + { + monster.Enabled = false; + if (monster.Params.AI.EnforceAggressiveBehaviorForMissions) + { + foreach (var targetParam in monster.Params.AI.Targets) + { + switch (targetParam.State) + { + case AIState.Avoid: + case AIState.Escape: + case AIState.Flee: + case AIState.PassiveAggressive: + targetParam.State = AIState.Attack; + break; + } + } + } + } SwarmBehavior.CreateSwarm(monsters.Cast()); foreach (Character monster in monsters) { @@ -191,7 +206,10 @@ namespace Barotrauma completed = true; } - public bool IsEliminated(Character enemy) => enemy.Removed || enemy.IsDead || enemy.AIController is EnemyAIController ai && ai.State == AIState.Flee; - + public bool IsEliminated(Character enemy) => + enemy == null || + enemy.Removed || + enemy.IsDead || + enemy.AIController is EnemyAIController ai && ai.State == AIState.Flee; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index e3691f34e..70c347779 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -13,6 +13,14 @@ namespace Barotrauma private Item item; private readonly Level.PositionType spawnPositionType; + + private readonly string containerTag; + + private readonly string existingItemTag; + + private bool usedExistingItem; + + private readonly bool showMessageWhenPickedUp; public override IEnumerable SonarPositions { @@ -24,7 +32,7 @@ namespace Barotrauma } else { - yield return ConvertUnits.ToDisplayUnits(item.SimPosition); + yield return item.WorldPosition; } } } @@ -32,6 +40,8 @@ namespace Barotrauma public SalvageMission(MissionPrefab prefab, Location[] locations) : base(prefab, locations) { + containerTag = prefab.ConfigElement.GetAttributeString("containertag", ""); + if (prefab.ConfigElement.Attribute("itemname") != null) { DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item."); @@ -52,6 +62,9 @@ namespace Barotrauma } } + existingItemTag = prefab.ConfigElement.GetAttributeString("existingitemtag", ""); + showMessageWhenPickedUp = prefab.ConfigElement.GetAttributeBool("showmessagewhenpickedup", false); + string spawnPositionTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", ""); if (string.IsNullOrWhiteSpace(spawnPositionTypeStr) || !Enum.TryParse(spawnPositionTypeStr, true, out spawnPositionType)) @@ -64,20 +77,66 @@ namespace Barotrauma { if (!IsClient) { - //ruin items are allowed to spawn close to the sub - float minDistance = spawnPositionType == Level.PositionType.Ruin ? 0.0f : Level.Loaded.Size.X * 0.3f; + //ruin/wreck items are allowed to spawn close to the sub + float minDistance = spawnPositionType == Level.PositionType.Ruin || spawnPositionType == Level.PositionType.Wreck ? + 0.0f : Level.Loaded.Size.X * 0.3f; Vector2 position = Level.Loaded.GetRandomItemPos(spawnPositionType, 100.0f, minDistance, 30.0f); - item = new Item(itemPrefab, position, null); - item.body.FarseerBody.BodyType = BodyType.Kinematic; - - if (item.HasTag("alien")) + if (!string.IsNullOrEmpty(existingItemTag)) + { + var suitableItems = Item.ItemList.Where(it => it.HasTag(existingItemTag)); + switch (spawnPositionType) + { + case Level.PositionType.Cave: + case Level.PositionType.MainPath: + item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f); + break; + case Level.PositionType.Ruin: + item = suitableItems.FirstOrDefault(it => it.ParentRuin != null && it.ParentRuin.Area.Contains(position)); + break; + case Level.PositionType.Wreck: + foreach (Item it in suitableItems) + { + if (it.Submarine == null || it.Submarine.Info.Type != SubmarineInfo.SubmarineType.Wreck) { continue; } + Rectangle worldBorders = it.Submarine.Borders; + worldBorders.Location += it.Submarine.WorldPosition.ToPoint(); + if (Submarine.RectContains(worldBorders, it.WorldPosition)) + { + item = it; + usedExistingItem = true; + break; + } + } + break; + } + } + + if (item == null) + { + item = new Item(itemPrefab, position, null); + item.body.FarseerBody.BodyType = BodyType.Kinematic; + item.FindHull(); + } + + //try to find a container and place the item inside it + if (!string.IsNullOrEmpty(containerTag) && item.ParentInventory == null) { - //try to find an artifact holder and place the artifact inside it foreach (Item it in Item.ItemList) { - if (it.Submarine != null || !it.HasTag("artifactholder")) continue; - + if (!it.HasTag(containerTag)) { continue; } + switch (spawnPositionType) + { + case Level.PositionType.Cave: + case Level.PositionType.MainPath: + if (it.Submarine != null || it.ParentRuin != null) { continue; } + break; + case Level.PositionType.Ruin: + if (it.ParentRuin == null) { continue; } + break; + case Level.PositionType.Wreck: + if (it.Submarine == null || it.Submarine.Info.Type != SubmarineInfo.SubmarineType.Wreck) { continue; } + break; + } var itemContainer = it.GetComponent(); if (itemContainer == null) { continue; } if (itemContainer.Combine(item, user: null)) { break; } // Placement successful @@ -97,14 +156,21 @@ namespace Barotrauma { case 0: if (item.ParentInventory != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } - if (item.CurrentHull?.Submarine == null) { return; } + if (showMessageWhenPickedUp) + { + if (!(item.ParentInventory?.Owner is Character)) { return; } + } + else + { + if (item.CurrentHull?.Submarine == null || item.CurrentHull.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { return; } + } State = 1; break; case 1: if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) { return; } State = 2; break; - } + } } public override void End() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index df5c497b1..8be908783 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -1,50 +1,46 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Xml.Linq; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; namespace Barotrauma { class MonsterEvent : ScriptedEvent { - private string speciesName; - - private int minAmount, maxAmount; - + private readonly string speciesName; + private readonly int minAmount, maxAmount; private List monsters; - private bool spawnDeep; + private readonly bool spawnDeep; private Vector2? spawnPos; - private bool disallowed; - - private Level.PositionType spawnPosType; + private readonly bool disallowed; + + private readonly Level.PositionType spawnPosType; private bool spawnPending; - private string characterFileName; - public override Vector2 DebugDrawPos { - get { return spawnPos.HasValue ? spawnPos.Value : Vector2.Zero; } + get { return spawnPos ?? Vector2.Zero; } } - + public override string ToString() { if (maxAmount <= 1) { - return "MonsterEvent (" + characterFileName + ")"; + return "MonsterEvent (" + speciesName + ")"; } else if (minAmount < maxAmount) { - return "MonsterEvent (" + characterFileName + " x" + minAmount + "-" + maxAmount + ")"; + return "MonsterEvent (" + speciesName + " x" + minAmount + "-" + maxAmount + ")"; } else { - return "MonsterEvent (" + characterFileName + " x" + maxAmount + ")"; + return "MonsterEvent (" + speciesName + " x" + maxAmount + ")"; } } @@ -76,7 +72,6 @@ namespace Barotrauma } spawnDeep = prefab.ConfigElement.GetAttributeBool("spawndeep", false); - characterFileName = Path.GetFileName(Path.GetDirectoryName(speciesName)).ToLower(); if (GameMain.NetworkMember != null) { @@ -85,7 +80,10 @@ namespace Barotrauma if (!string.IsNullOrWhiteSpace(tryKey)) { - if (!GameMain.NetworkMember.ServerSettings.MonsterEnabled[tryKey]) disallowed = true; //spawn was disallowed by host + if (!GameMain.NetworkMember.ServerSettings.MonsterEnabled[tryKey]) + { + disallowed = true; //spawn was disallowed by host + } } } } @@ -106,18 +104,8 @@ namespace Barotrauma public override bool CanAffectSubImmediately(Level level) { - float maxRange = Items.Components.Sonar.DefaultSonarRange * 0.8f; - - List positions = GetAvailableSpawnPositions(); - foreach (Vector2 position in positions) - { - if (Vector2.DistanceSquared(position, Submarine.MainSub.WorldPosition) < maxRange * maxRange) - { - return true; - } - } - - return false; + float maxRange = Sonar.DefaultSonarRange * 0.8f; + return GetAvailableSpawnPositions().Any(p => Vector2.DistanceSquared(p.Position.ToVector2(), Submarine.MainSub.WorldPosition) < maxRange * maxRange); } public override void Init(bool affectSubImmediately) @@ -128,28 +116,44 @@ namespace Barotrauma } } - private List GetAvailableSpawnPositions() + private List GetAvailableSpawnPositions() { - var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => spawnPosType.HasFlag(p.PositionType)); - - List positions = new List(); - foreach (var allowedPosition in availablePositions) + var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => spawnPosType.HasFlag(p.PositionType) && !Level.Loaded.UsedPositions.Contains(p)); + var removals = new List(); + foreach (var position in availablePositions) { - if (Level.Loaded.ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(allowedPosition.Position.ToVector2())))) { continue; } - positions.Add(allowedPosition.Position.ToVector2()); - } - - if (spawnDeep) - { - for (int i = 0; i < positions.Count; i++) + if (position.Submarine != null) { - positions[i] = new Vector2(positions[i].X, positions[i].Y - Level.Loaded.Size.Y); + if (position.Submarine.ThalamusAI != null && position.Submarine.ThalamusAI.IsAlive) + { + removals.Add(position); + } + else + { + continue; + } + } + if (position.PositionType != Level.PositionType.MainPath) { continue; } + if (Level.Loaded.ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(position.Position.ToVector2())))) + { + removals.Add(position); + } + if (spawnDeep) + { + for (int i = 0; i < availablePositions.Count; i++) + { + var pos = availablePositions[i].Position; + pos = new Point(pos.X, pos.Y - Level.Loaded.Size.Y); + availablePositions[i] = new Level.InterestingPosition(pos, availablePositions[i].PositionType); + } + } + if (position.Position.Y < Level.Loaded.GetBottomPosition(position.Position.X).Y) + { + removals.Add(position); } } - - positions.RemoveAll(pos => pos.Y < Level.Loaded.GetBottomPosition(pos.X).Y); - - return positions; + removals.ForEach(r => availablePositions.Remove(r)); + return availablePositions; } private void FindSpawnPosition(bool affectSubImmediately) @@ -158,67 +162,98 @@ namespace Barotrauma spawnPos = Vector2.Zero; var availablePositions = GetAvailableSpawnPositions(); - if (affectSubImmediately && spawnPosType != Level.PositionType.Ruin) + var chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false); + var removedPositions = new List(); + foreach (var position in availablePositions) { - if (availablePositions.Count == 0) + if (Rand.Value(Rand.RandSync.Server) > prefab.SpawnProbability) + { + removedPositions.Add(position); + if (prefab.AllowOnlyOnce) + { + Level.Loaded.UsedPositions.Add(position); + } + } + } + removedPositions.ForEach(p => availablePositions.Remove(p)); + bool isSubOrWreck = spawnPosType == Level.PositionType.Ruin || spawnPosType == Level.PositionType.Wreck; + if (affectSubImmediately && !isSubOrWreck) + { + if (availablePositions.None()) { //no suitable position found, disable the event Finished(); return; } - float closestDist = float.PositiveInfinity; //find the closest spawnposition that isn't too close to any of the subs - foreach (Vector2 position in availablePositions) + foreach (var position in availablePositions) { - float dist = Vector2.DistanceSquared(position, Submarine.MainSub.WorldPosition); + Vector2 pos = position.Position.ToVector2(); + float dist = Vector2.DistanceSquared(pos, Submarine.MainSub.WorldPosition); foreach (Submarine sub in Submarine.Loaded) { - if (sub.IsOutpost) { continue; } + if (sub.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } float minDistToSub = GetMinDistanceToSub(sub); if (dist > minDistToSub * minDistToSub && dist < closestDist) { closestDist = dist; - spawnPos = position; + chosenPosition = position; } } } - //only found a spawnpos that's very far from the sub, pick one that's closer //and wait for the sub to move further before spawning if (closestDist > 15000.0f * 15000.0f) { - foreach (Vector2 position in availablePositions) + foreach (var position in availablePositions) { - float dist = Vector2.DistanceSquared(position, Submarine.MainSub.WorldPosition); + float dist = Vector2.DistanceSquared(position.Position.ToVector2(), Submarine.MainSub.WorldPosition); if (dist < closestDist) { closestDist = dist; - spawnPos = position; + chosenPosition = position; } } } } else { - float minDist = spawnPosType == Level.PositionType.Ruin ? 0.0f : 20000.0f; - availablePositions.RemoveAll(p => Vector2.Distance(Submarine.MainSub.WorldPosition, p) < minDist); - if (availablePositions.Count == 0) + if (!isSubOrWreck) + { + float minDistance = 20000; + availablePositions.RemoveAll(p => Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, p.Position.ToVector2()) < minDistance * minDistance); + } + if (availablePositions.None()) { //no suitable position found, disable the event Finished(); return; } - - spawnPos = availablePositions[Rand.Int(availablePositions.Count, Rand.RandSync.Server)]; + chosenPosition = availablePositions.GetRandom(); + } + if (chosenPosition.IsValid) + { + spawnPos = chosenPosition.Position.ToVector2(); + if (chosenPosition.Submarine != null || chosenPosition.Ruin != null) + { + var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine, useSyncedRand: false); + if (spawnPoint != null) + { + spawnPos = spawnPoint.WorldPosition; + } + } + spawnPending = true; + if (prefab.AllowOnlyOnce) + { + Level.Loaded.UsedPositions.Add(chosenPosition); + } } - spawnPending = true; } private float GetMinDistanceToSub(Submarine submarine) { - //9000 units is slightly less than the default range of the sonar - return Math.Max(Math.Max(submarine.Borders.Width, submarine.Borders.Height), 9000.0f); + return Math.Max(Math.Max(submarine.Borders.Width, submarine.Borders.Height), Sonar.DefaultSonarRange * 0.9f); } public override void Update(float deltaTime) @@ -243,9 +278,38 @@ namespace Barotrauma //wait until there are no submarines at the spawnpos foreach (Submarine submarine in Submarine.Loaded) { - if (submarine.IsOutpost) { continue; } + if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } float minDist = GetMinDistanceToSub(submarine); - if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) return; + if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { return; } + } + + //if spawning in a ruin/cave, wait for someone to be close to it to spawning + //unnecessary monsters in places the players might never visit during the round + if (spawnPosType == Level.PositionType.Ruin || spawnPosType == Level.PositionType.Cave || spawnPosType == Level.PositionType.Wreck) + { + bool someoneNearby = false; + float minDist = Sonar.DefaultSonarRange * 0.8f; + foreach (Submarine submarine in Submarine.Loaded) + { + if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } + if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) + { + someoneNearby = true; + break; + } + } + foreach (Character c in Character.CharacterList) + { + if (c == Character.Controlled || c.IsRemotePlayer) + { + if (Vector2.DistanceSquared(c.WorldPosition, spawnPos.Value) < minDist * minDist) + { + someoneNearby = true; + break; + } + } + } + if (!someoneNearby) { return; } } spawnPending = false; @@ -280,7 +344,7 @@ namespace Barotrauma Entity targetEntity = Submarine.FindClosest(GameMain.GameScreen.Cam.WorldViewCenter); #if CLIENT - if (Character.Controlled != null) targetEntity = (Entity)Character.Controlled; + if (Character.Controlled != null) { targetEntity = Character.Controlled; } #endif bool monstersDead = true; @@ -297,7 +361,7 @@ namespace Barotrauma } } - if (monstersDead) Finished(); + if (monstersDead) { Finished(); } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index 58c5889f8..886750df4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -1,6 +1,5 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; -using System.Linq; namespace Barotrauma { @@ -8,7 +7,7 @@ namespace Barotrauma { protected bool isFinished; - private readonly ScriptedEventPrefab prefab; + protected readonly ScriptedEventPrefab prefab; public bool IsFinished { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventPrefab.cs index 60e022532..a6dc76b1e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventPrefab.cs @@ -7,12 +7,11 @@ namespace Barotrauma { class ScriptedEventPrefab { - public readonly XElement ConfigElement; - - public readonly Type EventType; - + public readonly XElement ConfigElement; + public readonly Type EventType; public readonly string MusicType; - + public readonly float SpawnProbability; + public readonly bool AllowOnlyOnce; public float Commonness; public ScriptedEventPrefab(XElement element) @@ -34,6 +33,8 @@ namespace Barotrauma DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\"."); } Commonness = element.GetAttributeFloat("commonness", 1.0f); + SpawnProbability = Math.Clamp(element.GetAttributeFloat("spawnprobability", 1.0f), 0, 1); + AllowOnlyOnce = element.GetAttributeBool("allowonlyonce", false); } public ScriptedEvent CreateInstance() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventSet.cs index 333974395..33a357cc0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventSet.cs @@ -25,6 +25,11 @@ namespace Barotrauma //the events in this set are delayed if the current EventManager intensity is not between these values public readonly float MinIntensity, MaxIntensity; + public readonly bool AllowAtStart; + + public readonly bool PerRuin; + public readonly bool PerWreck; + public readonly Dictionary Commonness; public readonly List EventPrefabs; @@ -54,6 +59,10 @@ namespace Barotrauma MinDistanceTraveled = element.GetAttributeFloat("mindistancetraveled", 0.0f); MinMissionTime = element.GetAttributeFloat("minmissiontime", 0.0f); + AllowAtStart = element.GetAttributeBool("allowatstart", false); + PerRuin = element.GetAttributeBool("perruin", false); + PerWreck = element.GetAttributeBool("perwreck", false); + Commonness[""] = 1.0f; foreach (XElement subElement in element.Elements()) { @@ -63,7 +72,7 @@ namespace Barotrauma Commonness[""] = subElement.GetAttributeFloat("commonness", 0.0f); foreach (XElement overrideElement in subElement.Elements()) { - if (overrideElement.Name.ToString().ToLowerInvariant() == "override") + if (overrideElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { string levelType = overrideElement.GetAttributeString("leveltype", ""); if (!Commonness.ContainsKey(levelType)) @@ -116,7 +125,7 @@ namespace Barotrauma int i = 0; foreach (XElement element in doc.Root.Elements()) { - if (element.Name.ToString().ToLowerInvariant() != "eventset") { continue; } + if (!element.Name.ToString().Equals("eventset", StringComparison.OrdinalIgnoreCase)) { continue; } List.Add(new ScriptedEventSet(element, i.ToString())); i++; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index a769afa54..b2dfb78c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -7,24 +7,23 @@ namespace Barotrauma.Extensions public static class IEnumerableExtensions { /// - /// Randomizes the collection and returns it. + /// Randomizes the collection (using OrderBy) and returns it. /// - public static IOrderedEnumerable Randomize(this IEnumerable source) + public static IOrderedEnumerable Randomize(this IEnumerable source, Rand.RandSync randSync = Rand.RandSync.Unsynced) { - return source.OrderBy(i => Rand.Value()); + return source.OrderBy(i => Rand.Value(randSync)); } /// - /// Randomizes the list in place. + /// Randomizes the list in place without creating a new collection, using a Fisher-Yates-based algorithm. /// - public static void RandomizeList(this List list) + public static void Shuffle(this IList list, Rand.RandSync randSync = Rand.RandSync.Unsynced) { - //Fisher-Yates shuffle int n = list.Count; while (n > 1) { n--; - int k = Rand.Int(n + 1); + int k = Rand.Int(n + 1, randSync); T value = list[k]; list[k] = list[n]; list[n] = value; @@ -90,6 +89,11 @@ namespace Barotrauma.Extensions return source.Count(predicate) > 1; } } + + public static IEnumerable ToEnumerable(this T item) + { + yield return item; + } // source: https://stackoverflow.com/questions/19237868/get-all-children-to-one-list-recursive-c-sharp public static IEnumerable SelectManyRecursive(this IEnumerable source, Func> selector) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/RectangleExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/RectangleExtensions.cs index b76860792..3c221e32e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/RectangleExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/RectangleExtensions.cs @@ -57,5 +57,43 @@ namespace Barotrauma.Extensions var size = rect.MultiplySize(scale); return new Rectangle(rect.X, rect.Y, size.X, size.Y); } + + public static bool IntersectsWorld(this Rectangle rect, Rectangle value) + { + int bottom = rect.Y - rect.Height; + int otherBottom = value.Y - value.Height; + return value.Left < rect.Right && rect.Left < value.Right && + value.Top > bottom && rect.Top > otherBottom; + } + + /// + /// Like the XNA method, but treats the y-coordinate so that up is greater and down is lower. + /// + public static bool ContainsWorld(this Rectangle rect, Rectangle other) + { + return + (rect.X <= other.X) && ((other.X + other.Width) <= (rect.X + rect.Width)) && + (rect.Y >= other.Y) && ((other.Y - other.Height) >= (rect.Y - rect.Height)); + } + + /// + /// Like the XNA method, but treats the y-coordinate so that up is greater and down is lower. + /// + public static bool ContainsWorld(this Rectangle rect, Vector2 point) + { + return + (rect.X <= point.X) && (point.X < (rect.X + rect.Width)) && + (rect.Y >= point.Y) && (point.Y > (rect.Y - rect.Height)); + } + + /// + /// Like the XNA method, but treats the y-coordinate so that up is greater and down is lower. + /// + public static bool ContainsWorld(this Rectangle rect, Point point) + { + return + (rect.X <= point.X) && (point.X < (rect.X + rect.Width)) && + (rect.Y >= point.Y) && (point.Y > (rect.Y - rect.Height)); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index b77825408..d60200a7a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -23,11 +23,18 @@ namespace Barotrauma { if (Submarine.MainSubs[i] == null) { continue; } List subs = new List() { Submarine.MainSubs[i] }; - subs.AddRange(Submarine.MainSubs[i].DockedTo.Where(d => !d.IsOutpost)); + subs.AddRange(Submarine.MainSubs[i].DockedTo.Where(d => !d.Info.IsOutpost)); Place(subs); } if (campaign != null) { campaign.InitialSuppliesSpawned = true; } - } + } + foreach (var wreck in Submarine.Loaded) + { + if (wreck.Info.IsWreck) + { + Place(wreck.ToEnumerable()); + } + } } private static void Place(IEnumerable subs) @@ -38,10 +45,10 @@ namespace Barotrauma return; } - int sizeApprox = MapEntityPrefab.List.Count() / 3; - var containers = new List(100); - var prefabsWithContainer = new List(sizeApprox / 3); - var prefabsWithoutContainer = new List(sizeApprox); + int itemCountApprox = MapEntityPrefab.List.Count() / 3; + var containers = new List(70 + 30 * subs.Count()); + var prefabsWithContainer = new List(itemCountApprox / 3); + var prefabsWithoutContainer = new List(itemCountApprox); var removals = new List(); foreach (Item item in Item.ItemList) @@ -49,6 +56,7 @@ namespace Barotrauma if (!subs.Contains(item.Submarine)) { continue; } containers.AddRange(item.GetComponents()); } + containers.Shuffle(); foreach (MapEntityPrefab prefab in MapEntityPrefab.List) { @@ -66,7 +74,7 @@ namespace Barotrauma spawnedItems.Clear(); var validContainers = new Dictionary(); - prefabsWithContainer.RandomizeList(); + prefabsWithContainer.Shuffle(); // Spawn items that have an ItemContainer component first so we can fill them up with items if needed (oxygen tanks inside the spawned diving masks, etc) for (int i = 0; i < prefabsWithContainer.Count; i++) { @@ -82,12 +90,13 @@ namespace Barotrauma // Another pass for items with containers because also they can spawn inside other items (like smg magazine) prefabsWithContainer.ForEach(i => SpawnItems(i)); // Spawn items that don't have containers last - prefabsWithoutContainer.RandomizeList(); + prefabsWithoutContainer.Shuffle(); prefabsWithoutContainer.ForEach(i => SpawnItems(i)); if (OutputDebugInfo) { - DebugConsole.NewMessage("Automatically placed items: "); + var subNames = subs.Select(s => s.Info.Name).ToList(); + DebugConsole.NewMessage($"Automatically placed items in { string.Join(", ", subNames) }:"); foreach (string itemName in spawnedItems.Select(it => it.Name).Distinct()) { DebugConsole.NewMessage(" - " + itemName + " x" + spawnedItems.Count(it => it.Name == itemName)); @@ -149,7 +158,12 @@ namespace Barotrauma private static bool SpawnItem(ItemPrefab itemPrefab, List containers, KeyValuePair validContainer) { bool success = false; - if (Rand.Value() > validContainer.Value.SpawnProbability) { return success; } + if (Rand.Value() > validContainer.Value.SpawnProbability) { return false; } + // Don't add dangerously reactive materials in thalamus wrecks + if (validContainer.Key.Item.Submarine.ThalamusAI != null && itemPrefab.Tags.Contains("explodesinwater")) + { + return false; + } int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1); for (int i = 0; i < amount; i++) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index ca5d46580..98b2bf6ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -8,6 +8,7 @@ namespace Barotrauma { const float ConversationIntervalMin = 100.0f; const float ConversationIntervalMax = 180.0f; + const float ConversationIntervalMultiplierMultiplayer = 5.0f; private float conversationTimer, conversationLineTimer; private List> pendingConversationLines = new List>(); @@ -74,11 +75,17 @@ namespace Barotrauma private void UpdateConversations(float deltaTime) { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.ServerSettings.DisableBotConversations) { return; } + conversationTimer -= deltaTime; if (conversationTimer <= 0.0f) { CreateRandomConversation(); conversationTimer = Rand.Range(ConversationIntervalMin, ConversationIntervalMax); + if (GameMain.NetworkMember != null) + { + conversationTimer *= ConversationIntervalMultiplierMultiplayer; + } } if (pendingConversationLines.Count > 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index a5ced4b18..f11ef8cb3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -64,7 +64,7 @@ namespace Barotrauma return Submarine.Loaded.FindAll(s => s != leavingSub && !leavingSub.DockedTo.Contains(s) && - s != Level.Loaded.StartOutpost && s != Level.Loaded.EndOutpost && + s.Info.Type == SubmarineInfo.SubmarineType.Player && (s.AtEndPosition != leavingSub.AtEndPosition || s.AtStartPosition != leavingSub.AtStartPosition)); } @@ -80,7 +80,7 @@ namespace Barotrauma { foreach (Structure wall in Structure.WallList) { - if (wall.Submarine == null || wall.Submarine.IsOutpost) { continue; } + if (wall.Submarine == null || wall.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } if (wall.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(wall.Submarine)) { for (int i = 0; i < wall.SectionCount; i++) @@ -95,7 +95,7 @@ namespace Barotrauma { foreach (Item item in Item.ItemList) { - if (item.Submarine == null || item.Submarine.IsOutpost) { continue; } + if (item.Submarine == null || item.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } if (item.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(item.Submarine)) { if (item.GetComponent() != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs index bbe5dd8e1..b652d1e8f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs @@ -44,6 +44,7 @@ namespace Barotrauma { #if CLIENT new GameModePreset("singleplayercampaign", typeof(SinglePlayerCampaign), true); + new GameModePreset("subtest", typeof(SubTestMode), true); new GameModePreset("tutorial", typeof(TutorialMode), true); new GameModePreset("devsandbox", typeof(GameMode), true); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 0bf30613e..39fd4452c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -69,9 +69,18 @@ namespace Barotrauma #if CLIENT if (GameMain.Client != null) { + bool success = + GameMain.Client.ConnectedClients.Any(c => c.Character != null && !c.Character.IsDead); + GameMain.GameSession.EndRound(""); GameMain.GameSession.CrewManager.EndRound(); - return; + + if (success) + { + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + } + + return; } #endif @@ -109,12 +118,6 @@ namespace Barotrauma } } - //remove all items that are in someone's inventory - foreach (Character c in Character.CharacterList) - { - c.Inventory?.DeleteAllItems(); - } - if (success) { bool atEndPosition = Submarine.MainSub.AtEndPosition; @@ -142,6 +145,8 @@ namespace Barotrauma } map.ProgressWorld(); + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + SaveUtil.SaveGame(GameMain.GameSession.SavePath); } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 3964464a1..47490a8d7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -65,44 +65,51 @@ namespace Barotrauma } } + public SubmarineInfo SubmarineInfo { get; set; } + public Submarine Submarine { get; set; } public string SavePath { get; set; } partial void InitProjSpecific(); - public GameSession(Submarine submarine, string savePath, GameModePreset gameModePreset, MissionType missionType = MissionType.None) - : this(submarine, savePath) + public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, MissionType missionType = MissionType.None) + : this(submarineInfo, savePath) { CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer); GameMode = gameModePreset.Instantiate(missionType); } - public GameSession(Submarine submarine, string savePath, GameModePreset gameModePreset, MissionPrefab missionPrefab) - : this(submarine, savePath) + public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, MissionPrefab missionPrefab) + : this(submarineInfo, savePath) { CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer); GameMode = gameModePreset.Instantiate(missionPrefab); + +#if CLIENT + if (GameMode is SubTestMode) { EventManager = null; } +#endif } - private GameSession(Submarine submarine, string savePath) + private GameSession(SubmarineInfo submarineInfo, string savePath) { InitProjSpecific(); - Submarine.MainSub = submarine; - this.Submarine = submarine; + SubmarineInfo = submarineInfo; + /*Submarine = new Submarine(submarineInfo); + Submarine.MainSub = Submarine;*/ GameMain.GameSession = this; EventManager = new EventManager(); this.SavePath = savePath; } - public GameSession(Submarine selectedSub, string saveFile, XDocument doc) - : this(selectedSub, saveFile) + public GameSession(SubmarineInfo selectedSubInfo, string saveFile, XDocument doc) + : this(selectedSubInfo, saveFile) { Submarine.MainSub = Submarine; GameMain.GameSession = this; - selectedSub.Name = doc.Root.GetAttributeString("submarine", selectedSub.Name); + //selectedSub.Name = doc.Root.GetAttributeString("submarine", selectedSub.Name); foreach (XElement subElement in doc.Root.Elements()) { @@ -150,14 +157,14 @@ namespace Barotrauma SaveUtil.LoadGame(SavePath); } - public void StartRound(string levelSeed, float? difficulty = null, bool loadSecondSub = false) + public void StartRound(string levelSeed, float? difficulty = null) { Level randomLevel = Level.CreateRandom(levelSeed, difficulty); - StartRound(randomLevel, true, loadSecondSub); + StartRound(randomLevel); } - public void StartRound(Level level, bool reloadSub = true, bool loadSecondSub = false, bool mirrorLevel = false) + public void StartRound(Level level, bool mirrorLevel = false) { //make sure no status effects have been carried on from the next round //(they should be stopped in EndRound, this is a safeguard against cases where the round is ended ungracefully) @@ -169,33 +176,26 @@ namespace Barotrauma #endif this.Level = level; - if (Submarine == null) + if (SubmarineInfo == null) { DebugConsole.ThrowError("Couldn't start game session, submarine not selected."); return; } - if (reloadSub || Submarine.MainSub != Submarine) { Submarine.Load(true); } - Submarine.MainSub = Submarine; - if (loadSecondSub) - { - if (Submarine.MainSubs[1] == null) - { - Submarine.MainSubs[1] = new Submarine(Submarine.MainSub.FilePath, Submarine.MainSub.MD5Hash.Hash, true); - Submarine.MainSubs[1].Load(false); - } - else if (reloadSub) - { - Submarine.MainSubs[1].Load(false); - } - } - - if (Submarine.IsFileCorrupted) + if (SubmarineInfo.IsFileCorrupted) { DebugConsole.ThrowError("Couldn't start game session, submarine file corrupted."); return; } + Submarine.Unload(); + Submarine = Submarine.MainSub = new Submarine(SubmarineInfo); + Submarine.MainSub = Submarine; + if (GameMode.Mission != null && GameMode.Mission.TeamCount > 1 && Submarine.MainSubs[1] == null) + { + Submarine.MainSubs[1] = new Submarine(SubmarineInfo, true); + } + if (level != null) { level.Generate(mirrorLevel); @@ -231,7 +231,7 @@ namespace Barotrauma if (port.Item.WorldPosition.Y < Submarine.WorldPosition.Y) { continue; } float dist = Vector2.DistanceSquared(port.Item.WorldPosition, level.StartOutpost.WorldPosition); - if (myPort == null || dist < closestDistance) + if (myPort == null || dist < closestDistance || (port.MainDockingPort && !myPort.MainDockingPort)) { myPort = port; closestDistance = dist; @@ -254,14 +254,14 @@ namespace Barotrauma foreach (var sub in Submarine.Loaded) { - if (sub.IsOutpost) + if (sub.Info.IsOutpost) { sub.DisableObstructedWayPoints(); } } Entity.Spawner = new EntitySpawner(); - + if (GameMode.Mission != null) { Mission = GameMode.Mission; } if (GameMode != null) { GameMode.Start(); } if (GameMode.Mission != null) @@ -277,7 +277,7 @@ namespace Barotrauma } } - EventManager.StartRound(level); + EventManager?.StartRound(level); SteamAchievementManager.OnStartRound(); if (GameMode != null) @@ -286,8 +286,9 @@ namespace Barotrauma if (GameMain.NetworkMember == null) { - //only autoplace items here in single player + //only place items and corpses here in single player //the server does this after loading the respawn shuttle + Level?.SpawnCorpses(); AutoItemPlacer.PlaceIfNeeded(GameMode); } if (GameMode is MultiPlayerCampaign mpCampaign && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) @@ -296,18 +297,18 @@ namespace Barotrauma } } - GameAnalyticsManager.AddDesignEvent("Submarine:" + Submarine.Name); - GameAnalyticsManager.AddDesignEvent("Level", ToolBox.StringToInt(level.Seed)); + GameAnalyticsManager.AddDesignEvent("Submarine:" + Submarine.Info.Name); + GameAnalyticsManager.AddDesignEvent("Level", ToolBox.StringToInt(level?.Seed ?? "[NO_LEVEL]")); GameAnalyticsManager.AddProgressionEvent(GameAnalyticsSDK.Net.EGAProgressionStatus.Start, GameMode.Preset.Identifier, (Mission == null ? "None" : Mission.GetType().ToString())); #if CLIENT if (GameMode is SinglePlayerCampaign) { SteamAchievementManager.OnBiomeDiscovered(level.Biome); } - RoundSummary = new RoundSummary(this); + if (!(GameMode is SubTestMode)) { RoundSummary = new RoundSummary(this); } GameMain.GameScreen.ColorFade(Color.Black, Color.TransparentBlack, 5.0f); - if (!(GameMode is TutorialMode)) + if (!(GameMode is TutorialMode) && !(GameMode is SubTestMode)) { GUI.AddMessage("", Color.Transparent, 3.0f, playSound: false); GUI.AddMessage(level.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, level.Difficulty / 100.0f), 5.0f, playSound: false); @@ -322,7 +323,7 @@ namespace Barotrauma public void Update(float deltaTime) { - EventManager.Update(deltaTime); + EventManager?.Update(deltaTime); GameMode?.Update(deltaTime); Mission?.Update(deltaTime); @@ -352,7 +353,7 @@ namespace Barotrauma } #endif - EventManager.EndRound(); + EventManager?.EndRound(); SteamAchievementManager.OnRoundEnded(this); Mission = null; @@ -451,7 +452,7 @@ namespace Barotrauma XDocument doc = new XDocument(new XElement("Gamesession")); doc.Root.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal)); - doc.Root.Add(new XAttribute("submarine", Submarine == null ? "" : Submarine.Name)); + doc.Root.Add(new XAttribute("submarine", SubmarineInfo == null ? "" : SubmarineInfo.Name)); doc.Root.Add(new XAttribute("mapseed", Map.Seed)); doc.Root.Add(new XAttribute("selectedcontentpackages", string.Join("|", GameMain.Config.SelectedContentPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).Select(cp => cp.Path)))); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs index 11424d818..cf1ddf4a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs @@ -27,10 +27,10 @@ namespace Barotrauma } public partial class GameSettings - { - const string savePath = "config.xml"; - const string playerSavePath = "config_player.xml"; - const string vanillaContentPackagePath = "Data/ContentPackages/Vanilla"; + { + public const string SavePath = "config.xml"; + public const string PlayerSavePath = "config_player.xml"; + public const string VanillaContentPackagePath = "Data/ContentPackages/Vanilla"; public int GraphicsWidth { get; set; } public int GraphicsHeight { get; set; } @@ -138,6 +138,13 @@ namespace Barotrauma public bool CrewMenuOpen { get; set; } = true; public bool ChatOpen { get; set; } = true; + public float CorpseDespawnDelay { get; set; } = 10.0f * 60.0f; + + /// + /// How many corpses there can be in a sub before they start to get despawned + /// + public int CorpsesPerSubDespawnThreshold { get; set; } = 5; + private string overrideSaveFolder, overrideMultiplayerSaveFolder; private bool unsavedSettings; @@ -198,7 +205,7 @@ namespace Barotrauma { voiceChatVolume = MathHelper.Clamp(value, 0.0f, 1.0f); #if CLIENT - GameMain.SoundManager?.SetCategoryGainMultiplier("voip", voiceChatVolume * 30.0f, 0); + GameMain.SoundManager?.SetCategoryGainMultiplier("voip", voiceChatVolume, 0); #endif } } @@ -236,6 +243,7 @@ namespace Barotrauma #if DEBUG public bool AutomaticQuickStartEnabled { get; set; } + public bool TextManagerDebugModeEnabled { get; set; } #endif private FileSystemWatcher modsFolderWatcher; @@ -311,7 +319,7 @@ namespace Barotrauma ref shouldRefreshAfflictions); if (shouldRefreshAfflictions) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); } - if (shouldRefreshSubs) { Submarine.RefreshSavedSubs(); } + if (shouldRefreshSubs) { SubmarineInfo.RefreshSavedSubs(); } if (shouldRefreshFabricationRecipes) { ItemPrefab.InitFabricationRecipes(); } if (shouldRefreshRuinGenerationParams) { RuinGeneration.RuinGenerationParams.ClearAll(); } if (shouldRefreshScriptedEventSets) { ScriptedEventSet.LoadPrefabs(); } @@ -359,7 +367,7 @@ namespace Barotrauma ref shouldRefreshAfflictions); if (shouldRefreshAfflictions) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); } - if (shouldRefreshSubs) { Submarine.RefreshSavedSubs(); } + if (shouldRefreshSubs) { SubmarineInfo.RefreshSavedSubs(); } if (shouldRefreshFabricationRecipes) { ItemPrefab.InitFabricationRecipes(); } if (shouldRefreshRuinGenerationParams) { RuinGeneration.RuinGenerationParams.ClearAll(); } if (shouldRefreshScriptedEventSets) { ScriptedEventSet.LoadPrefabs(); } @@ -408,7 +416,7 @@ namespace Barotrauma ref shouldRefreshAfflictions); if (shouldRefreshAfflictions) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); } - if (shouldRefreshSubs) { Submarine.RefreshSavedSubs(); } + if (shouldRefreshSubs) { SubmarineInfo.RefreshSavedSubs(); } if (shouldRefreshFabricationRecipes) { ItemPrefab.InitFabricationRecipes(); } if (shouldRefreshRuinGenerationParams) { RuinGeneration.RuinGenerationParams.ClearAll(); } if (shouldRefreshScriptedEventSets) { ScriptedEventSet.LoadPrefabs(); } @@ -496,10 +504,10 @@ namespace Barotrauma shouldRefreshSoundPlayer = true; break; case ContentType.Particles: - GameMain.ParticleManager.LoadPrefabsFromFile(file); + GameMain.ParticleManager?.LoadPrefabsFromFile(file); break; case ContentType.Decals: - GameMain.DecalManager.LoadFromFile(file); + GameMain.DecalManager?.LoadFromFile(file); break; #endif } @@ -579,10 +587,10 @@ namespace Barotrauma shouldRefreshSoundPlayer = true; break; case ContentType.Particles: - GameMain.ParticleManager.RemovePrefabsByFile(file.Path); + GameMain.ParticleManager?.RemovePrefabsByFile(file.Path); break; case ContentType.Decals: - GameMain.DecalManager.RemoveByFile(file.Path); + GameMain.DecalManager?.RemoveByFile(file.Path); break; #endif } @@ -615,6 +623,7 @@ namespace Barotrauma case ContentType.Particles: case ContentType.Decals: case ContentType.Outpost: + case ContentType.Wreck: case ContentType.BackgroundCreaturePrefabs: case ContentType.ServerExecutable: case ContentType.None: @@ -643,7 +652,7 @@ namespace Barotrauma ItemAssemblyPrefab.Prefabs.SortAll(); StructurePrefab.Prefabs.SortAll(); - Submarine.RefreshSavedSubs(); + SubmarineInfo.RefreshSavedSubs(); ItemPrefab.InitFabricationRecipes(); RuinGeneration.RuinGenerationParams.ClearAll(); ScriptedEventSet.LoadPrefabs(); @@ -757,7 +766,7 @@ namespace Barotrauma private void OnModFolderUpdate(object sender, FileSystemEventArgs e) { - if (SuppressModFolderWatcher || !(GameMain.NetworkMember?.IsClient ?? false)) { return; } + if (SuppressModFolderWatcher || (GameMain.NetworkMember?.IsClient ?? false)) { return; } switch (e.ChangeType) { case WatcherChangeTypes.Created: @@ -818,7 +827,7 @@ namespace Barotrauma private void LoadDefaultConfig(bool setLanguage = true) { - XDocument doc = XMLExtensions.TryLoadXml(savePath); + XDocument doc = XMLExtensions.TryLoadXml(SavePath); if (doc == null) { GraphicsWidth = 1024; @@ -931,7 +940,7 @@ namespace Barotrauma foreach (ContentPackage contentPackage in SelectedContentPackages) { - if (contentPackage.Path.Contains(vanillaContentPackagePath)) + if (contentPackage.Path.Contains(VanillaContentPackagePath)) { doc.Root.Add(new XElement("contentpackage", new XAttribute("path", contentPackage.Path))); break; @@ -986,7 +995,7 @@ namespace Barotrauma try { - using (var writer = XmlWriter.Create(savePath, settings)) + using (var writer = XmlWriter.Create(SavePath, settings)) { doc.WriteTo(writer); writer.Flush(); @@ -1021,7 +1030,7 @@ namespace Barotrauma /// private bool LoadPlayerConfigInternal() { - XDocument doc = XMLExtensions.LoadXml(playerSavePath); + XDocument doc = XMLExtensions.LoadXml(PlayerSavePath); if (doc == null || doc.Root == null) { ShowUserStatisticsPrompt = true; @@ -1199,9 +1208,12 @@ namespace Barotrauma new XAttribute("crewmenuopen", CrewMenuOpen), new XAttribute("campaigndisclaimershown", CampaignDisclaimerShown), new XAttribute("editordisclaimershown", EditorDisclaimerShown), - new XAttribute("tutorialskipwarning", ShowTutorialSkipWarning) + new XAttribute("tutorialskipwarning", ShowTutorialSkipWarning), + new XAttribute("corpsedespawndelay", CorpseDespawnDelay), + new XAttribute("corpsespersubdespawnthreshold", CorpsesPerSubDespawnThreshold) #if DEBUG , new XAttribute("automaticquickstartenabled", AutomaticQuickStartEnabled) + , new XAttribute("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled) #endif ); @@ -1349,7 +1361,7 @@ namespace Barotrauma try { - using (var writer = XmlWriter.Create(playerSavePath, settings)) + using (var writer = XmlWriter.Create(PlayerSavePath, settings)) { doc.WriteTo(writer); writer.Flush(); @@ -1382,11 +1394,14 @@ namespace Barotrauma EnableMouseLook = doc.Root.GetAttributeBool("enablemouselook", EnableMouseLook); CrewMenuOpen = doc.Root.GetAttributeBool("crewmenuopen", CrewMenuOpen); ChatOpen = doc.Root.GetAttributeBool("chatopen", ChatOpen); + CorpseDespawnDelay = doc.Root.GetAttributeInt("corpsedespawndelay", 10 * 60); + CorpsesPerSubDespawnThreshold = doc.Root.GetAttributeInt("corpsespersubdespawnthreshold", 5); CampaignDisclaimerShown = doc.Root.GetAttributeBool("campaigndisclaimershown", CampaignDisclaimerShown); EditorDisclaimerShown = doc.Root.GetAttributeBool("editordisclaimershown", EditorDisclaimerShown); ShowTutorialSkipWarning = doc.Root.GetAttributeBool("tutorialskipwarning", true); #if DEBUG AutomaticQuickStartEnabled = doc.Root.GetAttributeBool("automaticquickstartenabled", AutomaticQuickStartEnabled); + TextManagerDebugModeEnabled = doc.Root.GetAttributeBool("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled); #endif XElement gameplayElement = doc.Root.Element("gameplay"); jobPreferences = new List>(); @@ -1560,6 +1575,8 @@ namespace Barotrauma InventoryScale = 1; AutoUpdateWorkshopItems = true; CampaignDisclaimerShown = false; + CorpseDespawnDelay = 10 * 60; + CorpsesPerSubDespawnThreshold = 5; if (resetLanguage) { Language = "English"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index c6cc4d6d8..e962781f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -73,7 +73,7 @@ namespace Barotrauma foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "item") continue; + if (!subElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; } string itemIdentifier = subElement.GetAttributeString("identifier", ""); ItemPrefab itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index d958b9eb3..b320d792d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -58,6 +58,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false, description: "If set to true, this docking port is used when spawning the submarine docked to an outpost (if possible).")] + public bool MainDockingPort + { + get; + set; + } + public DockingPort DockingTarget { get; private set; } public bool Docked @@ -173,8 +180,8 @@ namespace Barotrauma.Items.Components if (!item.linkedTo.Contains(target.item)) item.linkedTo.Add(target.item); if (!target.item.linkedTo.Contains(item)) target.item.linkedTo.Add(item); - if (!target.item.Submarine.DockedTo.Contains(item.Submarine)) target.item.Submarine.DockedTo.Add(item.Submarine); - if (!item.Submarine.DockedTo.Contains(target.item.Submarine)) item.Submarine.DockedTo.Add(target.item.Submarine); + if (!target.item.Submarine.DockedTo.Contains(item.Submarine)) target.item.Submarine.ConnectedDockingPorts.Add(item.Submarine, target); + if (!item.Submarine.DockedTo.Contains(target.item.Submarine)) item.Submarine.ConnectedDockingPorts.Add(target.item.Submarine, this); DockingTarget = target; DockingTarget.DockingTarget = this; @@ -234,12 +241,12 @@ namespace Barotrauma.Items.Components Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA; if (item.Submarine.PhysicsBody.Mass < DockingTarget.item.Submarine.PhysicsBody.Mass || - DockingTarget.item.Submarine.IsOutpost) + DockingTarget.item.Submarine.Info.IsOutpost) { item.Submarine.SubBody.SetPosition(item.Submarine.SubBody.Position + ConvertUnits.ToDisplayUnits(jointDiff)); } else if (DockingTarget.item.Submarine.PhysicsBody.Mass < item.Submarine.PhysicsBody.Mass || - item.Submarine.IsOutpost) + item.Submarine.Info.IsOutpost) { DockingTarget.item.Submarine.SubBody.SetPosition(DockingTarget.item.Submarine.SubBody.Position - ConvertUnits.ToDisplayUnits(jointDiff)); } @@ -703,8 +710,8 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnSecondaryUse, 1.0f); - DockingTarget.item.Submarine.DockedTo.Remove(item.Submarine); - item.Submarine.DockedTo.Remove(DockingTarget.item.Submarine); + DockingTarget.item.Submarine.ConnectedDockingPorts.Remove(item.Submarine); + item.Submarine.ConnectedDockingPorts.Remove(DockingTarget.item.Submarine); if (door != null && DockingTarget.door != null) { @@ -951,12 +958,12 @@ namespace Barotrauma.Items.Components if (docked) { if (item.Submarine != null && DockingTarget?.item?.Submarine != null) - GameServer.Log(sender.LogName + " docked " + item.Submarine.Name + " to " + DockingTarget.item.Submarine.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(sender.LogName + " docked " + item.Submarine.Info.Name + " to " + DockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction); } else { if (item.Submarine != null && prevDockingTarget?.item?.Submarine != null) - GameServer.Log(sender.LogName + " undocked " + item.Submarine.Name + " from " + prevDockingTarget.item.Submarine.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(sender.LogName + " undocked " + item.Submarine.Info.Name + " from " + prevDockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction); } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 7a9b760b7..45d565f4c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -287,24 +287,21 @@ namespace Barotrauma.Items.Components public override bool Select(Character character) { - if (!isBroken) + if (isBroken) { return true; } + bool hasRequiredItems = HasRequiredItems(character, false); + if (HasAccess(character)) { - bool hasRequiredItems = HasRequiredItems(character, false); - if (HasAccess(character)) - { - float originalPickingTime = PickingTime; - PickingTime = 0; - ToggleState(ActionType.OnUse, character); - PickingTime = originalPickingTime; - } -#if CLIENT - else if (hasRequiredItems && character != null && character == Character.Controlled) - { - GUI.AddMessage(accessDeniedTxt, GUI.Style.Red); - - } -#endif + float originalPickingTime = PickingTime; + PickingTime = 0; + ToggleState(ActionType.OnUse, character); + PickingTime = originalPickingTime; } +#if CLIENT + else if (hasRequiredItems && character != null && character == Character.Controlled) + { + GUI.AddMessage(accessDeniedTxt, GUI.Style.Red); + } +#endif return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index f2a6bf1f6..0f575f5d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -54,7 +54,7 @@ namespace Barotrauma.Items.Components { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "attack") { continue; } + if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; } attack = new Attack(subElement, item.Name + ", MeleeWeapon"); } item.IsShootable = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 94f4d3e38..aa299a0a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -29,6 +29,13 @@ namespace Barotrauma.Items.Components set { reload = Math.Max(value, 0.0f); } } + [Serialize(1, false, description: "How projectiles the weapon launches when fired once.")] + public int ProjectileCount + { + get; + set; + } + [Serialize(0.0f, false, description: "Random spread applied to the firing angle of the projectiles when used by a character with sufficient skills to use the weapon (in degrees).")] public float Spread { @@ -110,55 +117,62 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnFailure, 1.0f, character); } - Projectile projectile = FindProjectile(triggerOnUseOnContainers: true); - if (projectile == null) { return true; } - - float spread = GetSpread(character); - float rotation = (item.body.Dir == 1.0f) ? item.body.Rotation : item.body.Rotation - MathHelper.Pi; - rotation += spread * Rand.Range(-0.5f, 0.5f); - - projectile.User = character; - //add the limbs of the shooter to the list of bodies to be ignored - //so that the player can't shoot himself - projectile.IgnoredBodies = new List(limbBodies); - - Vector2 projectilePos = item.SimPosition; - Vector2 sourcePos = character?.AnimController == null ? item.SimPosition : character.AnimController.AimSourceSimPos; - Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition; - //make sure there's no obstacles between the base of the weapon (or the shoulder of the character) and the end of the barrel - if (Submarine.PickBody(sourcePos, barrelPos, projectile.IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking) == null) + for (int i = 0; i < ProjectileCount; i++) { - //no obstacles -> we can spawn the projectile at the barrel - projectilePos = barrelPos; + Projectile projectile = FindProjectile(triggerOnUseOnContainers: true); + if (projectile == null) { return true; } + + float spread = GetSpread(character); + float rotation = (item.body.Dir == 1.0f) ? item.body.Rotation : item.body.Rotation - MathHelper.Pi; + rotation += spread * Rand.Range(-0.5f, 0.5f); + + projectile.User = character; + //add the limbs of the shooter to the list of bodies to be ignored + //so that the player can't shoot himself + projectile.IgnoredBodies = new List(limbBodies); + + Vector2 projectilePos = item.SimPosition; + Vector2 sourcePos = character?.AnimController == null ? item.SimPosition : character.AnimController.AimSourceSimPos; + Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition; + //make sure there's no obstacles between the base of the weapon (or the shoulder of the character) and the end of the barrel + if (Submarine.PickBody(sourcePos, barrelPos, projectile.IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking) == null) + { + //no obstacles -> we can spawn the projectile at the barrel + projectilePos = barrelPos; + } + else if ((sourcePos - barrelPos).LengthSquared() > 0.0001f) + { + //spawn the projectile body.GetMaxExtent() away from the position where the raycast hit the obstacle + projectilePos = sourcePos - Vector2.Normalize(barrelPos - projectilePos) * Math.Max(projectile.Item.body.GetMaxExtent(), 0.1f); + } + + projectile.Item.body.ResetDynamics(); + projectile.Item.SetTransform(projectilePos, rotation); + + projectile.Use(deltaTime); + projectile.Item.GetComponent()?.Attach(item, projectile.Item); + if (projectile.Item.Removed) { continue; } + projectile.User = character; + + projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f)); + + //set the rotation of the projectile again because dropping the projectile resets the rotation + projectile.Item.SetTransform(projectilePos, + rotation + (projectile.Item.body.Dir * projectile.LaunchRotationRadians)); + + item.RemoveContained(projectile.Item); + + if (i == 0) + { + //recoil + item.body.ApplyLinearImpulse( + new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * item.body.Mass * -50.0f, + maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + } } - else if ((sourcePos - barrelPos).LengthSquared() > 0.0001f) - { - //spawn the projectile body.GetMaxExtent() away from the position where the raycast hit the obstacle - projectilePos = sourcePos - Vector2.Normalize(barrelPos - projectilePos) * Math.Max(projectile.Item.body.GetMaxExtent(), 0.1f); - } - - projectile.Item.body.ResetDynamics(); - projectile.Item.SetTransform(projectilePos, rotation); - - projectile.Use(deltaTime); - if (projectile.Item.Removed) { return true; } - projectile.User = character; - - projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f)); - - //set the rotation of the projectile again because dropping the projectile resets the rotation - projectile.Item.SetTransform(projectilePos, - rotation + (projectile.Item.body.Dir * projectile.LaunchRotationRadians)); - - //recoil - item.body.ApplyLinearImpulse( - new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * item.body.Mass * -50.0f, - maxVelocity: NetConfig.MaxPhysicsBodyVelocity); LaunchProjSpecific(); - item.RemoveContained(projectile.Item); - return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 31b47c057..7d7638c33 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -578,13 +578,7 @@ namespace Barotrauma.Items.Components { character.SetInput(InputType.Aim, false, true); } - bool isAiming = false; - var holdable = item.GetComponent(); - if (holdable != null) - { - isAiming = holdable.ControlPose; - } - sinTime = isAiming ? sinTime + deltaTime * 5 : 0; + sinTime += deltaTime * 5; } // Press the trigger only when the tool is approximately facing the target. Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 336303f42..317b5c9d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components if (!picker.IsKeyDown(InputType.Aim) && !throwing) { throwPos = 0.0f; } bool aim = picker.IsKeyDown(InputType.Aim) && (picker.SelectedConstruction == null || picker.SelectedConstruction.GetComponent() != null); - if (picker.IsUnconscious || picker.IsDead || !picker.AllowInput) + if (picker.IsDead || !picker.AllowInput) { throwing = false; aim = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 73e3baad9..8faa8e1b4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -68,7 +68,6 @@ namespace Barotrauma.Items.Components protected const float CorrectionDelay = 1.0f; protected CoroutineHandle delayedCorrectionCoroutine; - protected float correctionTimer; [Editable, Serialize(0.0f, false, description: "How long it takes to pick up the item (in seconds).")] public float PickingTime @@ -81,7 +80,6 @@ namespace Barotrauma.Items.Components public Action OnActiveStateChanged; - public float IsActiveTimer; public virtual bool IsActive { get { return isActive; } @@ -222,7 +220,6 @@ namespace Barotrauma.Items.Components set; } - /// /// How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced). /// @@ -632,7 +629,7 @@ namespace Barotrauma.Items.Components } /// - /// Only checks the id card(s). Much simpler and a bit different than HasRequiredItems. + /// Only checks if any of the Picked requirements are matched (used for checking id card(s)). Much simpler and a bit different than HasRequiredItems. /// public bool HasAccess(Character character) { @@ -641,7 +638,7 @@ namespace Barotrauma.Items.Components foreach (Item item in character.Inventory.Items) { - if (item?.Prefab.Identifier == "idcard" && requiredItems.Any(ri => ri.Value.Any(r => r.MatchesItem(item)))) + if (requiredItems.Any(ri => ri.Value.Any(r => r.Type == RelatedItem.RelationType.Picked && r.MatchesItem(item)))) { return true; } @@ -741,14 +738,14 @@ namespace Barotrauma.Items.Components { foreach (XAttribute attribute in componentElement.Attributes()) { - if (!SerializableProperties.TryGetValue(attribute.Name.ToString().ToLowerInvariant(), out SerializableProperty property)) continue; + if (!SerializableProperties.TryGetValue(attribute.Name.ToString().ToLowerInvariant(), out SerializableProperty property)) { continue; } property.TrySetValue(this, attribute.Value); } ParseMsg(); OverrideRequiredItems(componentElement); } - if (item.Submarine != null) { SerializableProperty.UpgradeGameVersion(this, originalElement, item.Submarine.GameVersion); } + if (item.Submarine != null) { SerializableProperty.UpgradeGameVersion(this, originalElement, item.Submarine.Info.GameVersion); } } /// @@ -870,6 +867,7 @@ namespace Barotrauma.Items.Components switch (subElement.Name.ToString().ToLowerInvariant()) { case "requireditem": + case "requireditems": RelatedItem newRequiredItem = RelatedItem.Load(subElement, returnEmptyRequirements, item.Name); if (newRequiredItem == null) continue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index b9b51b551..4bea6fd32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -52,6 +52,9 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false)] + public bool AccessOnlyWhenBroken { get; set; } + [Serialize(5, false, description: "How many inventory slots the inventory has per row.")] public int SlotsPerRow { get; set; } @@ -197,10 +200,21 @@ namespace Barotrauma.Items.Components } } + public override bool HasRequiredItems(Character character, bool addMessage, string msg = null) + { + return (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); + } + public override bool Select(Character character) { if (item.Container != null) { return false; } - + if (AccessOnlyWhenBroken) + { + if (item.Condition > 0) + { + return false; + } + } if (AutoInteractWithContained && character.SelectedConstruction == null) { foreach (Item contained in Inventory.Items) @@ -218,6 +232,13 @@ namespace Barotrauma.Items.Components public override bool Pick(Character picker) { + if (AccessOnlyWhenBroken) + { + if (item.Condition > 0) + { + return false; + } + } if (AutoInteractWithContained) { foreach (Item contained in Inventory.Items) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 23931bb20..ab5aa5c90 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma.Items.Components { @@ -209,7 +210,7 @@ namespace Barotrauma.Items.Components return true; } - + public override bool SecondaryUse(float deltaTime, Character character = null) { if (this.user != character) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index 3f98ac709..df57597b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -55,6 +55,15 @@ namespace Barotrauma.Items.Components get { return Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage / MinVoltage, 1.0f))); } } + public float CurrentBrokenVolume + { + get + { + if (item.ConditionPercentage > 10.0f) { return 0.0f; } + return Math.Abs(targetForce / 100.0f) * (1.0f - item.ConditionPercentage / 10.0f); + } + } + public Engine(Item item, XElement element) : base(item, element) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 7bdd7bfdc..89cb750fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -22,6 +22,30 @@ namespace Barotrauma.Items.Components private ItemContainer inputContainer, outputContainer; + private enum FabricatorState + { + Active = 1, + Paused = 2, + Stopped = 0 + } + + private FabricatorState state; + private FabricatorState State + { + get + { + return state; + } + set + { + if (state == value) { return; } + state = value; +#if SERVER + item.CreateServerEvent(this); +#endif + } + } + public ItemContainer InputContainer { get { return inputContainer; } @@ -39,7 +63,7 @@ namespace Barotrauma.Items.Components { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "fabricableitem") + if (subElement.Name.ToString().Equals("fabricableitem", StringComparison.OrdinalIgnoreCase)) { DebugConsole.ThrowError("Error in item " + item.Name + "! Fabrication recipes should be defined in the craftable item's xml, not in the fabricator."); break; @@ -61,6 +85,8 @@ namespace Barotrauma.Items.Components } } + state = FabricatorState.Stopped; + InitProjSpecific(); } @@ -147,12 +173,15 @@ namespace Barotrauma.Items.Components currPowerConsumption = powerConsumption; currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition); + if (GameMain.NetworkMember?.IsServer ?? true) + { + State = FabricatorState.Active; + } #if SERVER if (user != null) { GameServer.Log(user.LogName + " started fabricating " + selectedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); } - item.CreateServerEvent(this); #endif } @@ -180,12 +209,15 @@ namespace Barotrauma.Items.Components inputContainer.Inventory.Locked = false; outputContainer.Inventory.Locked = false; + if (GameMain.NetworkMember?.IsServer ?? true) + { + State = FabricatorState.Stopped; + } #if SERVER if (user != null) { GameServer.Log(user.LogName + " cancelled the fabrication of " + fabricatedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); } - item.CreateServerEvent(this); #endif } @@ -199,8 +231,25 @@ namespace Barotrauma.Items.Components progressState = fabricatedItem == null ? 0.0f : (requiredTime - timeUntilReady) / requiredTime; - hasPower = Voltage >= MinVoltage; - if (!hasPower) { return; } + if (GameMain.NetworkMember?.IsClient ?? false) + { + hasPower = State != FabricatorState.Paused; + if (!hasPower) + { + return; + } + } + else + { + hasPower = Voltage >= MinVoltage; + + if (!hasPower) + { + State = FabricatorState.Paused; + return; + } + State = FabricatorState.Active; + } var repairable = item.GetComponent(); if (repairable != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index 520b0b0a3..fa2c78107 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -37,23 +37,10 @@ namespace Barotrauma.Items.Components private float currFlow; public float CurrFlow { - get + get { if (!IsActive) { return 0.0f; } - return Math.Abs(currFlow); - } - } - - public override bool IsActive - { - get => base.IsActive; - set - { - base.IsActive = value; - if (!IsActive) - { - powerConsumption = 0; - } + return Math.Abs(currFlow); } } @@ -77,11 +64,6 @@ namespace Barotrauma.Items.Components float hullPercentage = 0.0f; if (item.CurrentHull != null) { hullPercentage = (item.CurrentHull.WaterVolume / item.CurrentHull.Volume) * 100.0f; } FlowPercentage = ((float)targetLevel - hullPercentage) * 10.0f; - - if (pumpSpeedLockTimer <= 0.0f) - { - targetLevel = null; - } } currPowerConsumption = powerConsumption * Math.Abs(flowPercentage / 100.0f); @@ -125,6 +107,7 @@ namespace Barotrauma.Items.Components if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed)) { flowPercentage = MathHelper.Clamp(tempSpeed, -100.0f, 100.0f); + targetLevel = null; pumpSpeedLockTimer = 0.1f; } } @@ -144,7 +127,7 @@ namespace Barotrauma.Items.Components if (GameMain.Client != null) { return false; } #endif - if (objective.Option.ToLowerInvariant() == "stoppumping") + if (objective.Option.Equals("stoppumping", StringComparison.OrdinalIgnoreCase)) { #if SERVER if (FlowPercentage > 0.0f) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 68d569275..82f3f24f6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; +using System.Globalization; namespace Barotrauma.Items.Components { @@ -651,6 +652,20 @@ namespace Barotrauma.Items.Components unsentChanges = true; } break; + case "set_fissionrate": + if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate)) + { + FissionRate = newFissionRate; + unsentChanges = true; + } + break; + case "set_turbineoutput": + if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput)) + { + TurbineOutput = newTurbineOutput; + unsentChanges = true; + } + break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index e514c6a91..2fb664778 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -165,7 +165,6 @@ namespace Barotrauma.Items.Components #region Docking public List DockingSources = new List(); - public DockingPort ActiveDockingSource, DockingTarget; private bool searchedConnectedDockingPort; private bool dockingModeEnabled; @@ -200,7 +199,7 @@ namespace Barotrauma.Items.Components if (dockingConnection != null) { var connectedPorts = item.GetConnectedComponentsRecursive(dockingConnection); - DockingSources.AddRange(connectedPorts.Where(p => p.Item.Submarine != null && !p.Item.Submarine.IsOutpost)); + DockingSources.AddRange(connectedPorts.Where(p => p.Item.Submarine != null && !p.Item.Submarine.Info.IsOutpost)); } } #endregion @@ -344,6 +343,7 @@ namespace Barotrauma.Items.Components autopilotRecalculatePathTimer = RecalculatePathInterval; } + if (steeringPath == null) { return; } steeringPath.CheckProgress(ConvertUnits.ToSimUnits(controlledSub.WorldPosition), 10.0f); if (autopilotRayCastTimer <= 0.0f && steeringPath.NextNode != null) @@ -475,6 +475,8 @@ namespace Barotrauma.Items.Components private void UpdatePath() { + if (Level.Loaded == null) { return; } + if (pathFinder == null) pathFinder = new PathFinder(WayPoint.WayPointList, false); Vector2 target; @@ -536,7 +538,6 @@ namespace Barotrauma.Items.Components } } - private bool aiDockingToggled; public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (objective.Override) @@ -572,7 +573,7 @@ namespace Barotrauma.Items.Components } break; case "navigateback": - if (!aiDockingToggled && DockingSources.Any(d => d.Docked)) + if (DockingSources.Any(d => d.Docked)) { item.SendSignal(0, "1", "toggle_docking", sender: null); } @@ -586,7 +587,7 @@ namespace Barotrauma.Items.Components } break; case "navigatetodestination": - if (!aiDockingToggled && DockingSources.Any(d => d.Docked)) + if (DockingSources.Any(d => d.Docked)) { item.SendSignal(0, "1", "toggle_docking", sender: null); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index f2d9706b1..755391dc9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -213,7 +213,7 @@ namespace Barotrauma.Items.Components } if (HasBeenTuned) { return true; } - if (string.IsNullOrEmpty(objective.Option) || objective.Option.ToLowerInvariant() == "charge") + if (string.IsNullOrEmpty(objective.Option) || objective.Option.Equals("charge", StringComparison.OrdinalIgnoreCase)) { if (Math.Abs(rechargeSpeed - maxRechargeSpeed * aiRechargeTargetRatio) > 0.05f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 27e46c46d..cdfde4231 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -11,7 +11,7 @@ using System.Xml.Linq; namespace Barotrauma.Items.Components { - class Projectile : ItemComponent + partial class Projectile : ItemComponent, IServerSerializable { struct HitscanResult { @@ -32,11 +32,13 @@ namespace Barotrauma.Items.Components { public Fixture Fixture; public Vector2 Normal; + public Vector2 LinearVelocity; - public Impact(Fixture fixture, Vector2 normal) + public Impact(Fixture fixture, Vector2 normal, Vector2 velocity) { Fixture = fixture; Normal = normal; + LinearVelocity = velocity; } } @@ -47,13 +49,13 @@ namespace Barotrauma.Items.Components //a duration during which the projectile won't drop from the body it's stuck to private const float PersistentStickJointDuration = 1.0f; - - private float launchImpulse; - private PrismaticJoint stickJoint; - private Body stickTarget; - private Attack attack; + private readonly Attack attack; + + private Vector2 launchPos; + + private readonly HashSet hits = new HashSet(); public List IgnoredBodies; @@ -68,14 +70,15 @@ namespace Barotrauma.Items.Components } } + public IEnumerable Hits + { + get { return hits; } + } + private float persistentStickJointTimer; [Serialize(10.0f, false, description: "The impulse applied to the physics body of the item when it's launched. Higher values make the projectile faster.")] - public float LaunchImpulse - { - get { return launchImpulse; } - set { launchImpulse = value; } - } + public float LaunchImpulse { get; set; } [Serialize(0.0f, false, description: "The rotation of the item relative to the rotation of the weapon when launched (in degrees).")] public float LaunchRotation @@ -98,6 +101,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false, description: "When set to true, the item won't fall of a target it's stuck to unless removed.")] + public bool StickPermanently + { + get; + set; + } + [Serialize(false, false, description: "Can the item stick to the character it hits.")] public bool StickToCharacters { @@ -136,6 +146,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(1, false, description: "How many targets the projectile can hit before it stops.")] + public int MaxTargetsToHit + { + get; + set; + } + [Serialize(false, false, description: "Should the item be deleted when it hits something.")] public bool RemoveOnHit { @@ -150,6 +167,17 @@ namespace Barotrauma.Items.Components set; } + public Body StickTarget + { + get; + private set; + } + + public bool IsStuckToTarget + { + get { return StickTarget != null; } + } + public Projectile(Item item, XElement element) : base (item, element) { @@ -157,7 +185,7 @@ namespace Barotrauma.Items.Components foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "attack") continue; + if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; } attack = new Attack(subElement, item.Name + ", Projectile"); } } @@ -201,7 +229,7 @@ namespace Barotrauma.Items.Components } else { - Launch(launchDir * launchImpulse * item.body.Mass); + Launch(launchDir * LaunchImpulse * item.body.Mass); } } @@ -212,6 +240,9 @@ namespace Barotrauma.Items.Components private void Launch(Vector2 impulse) { + hits.Clear(); + MaxTargetsToHit = 2; + if (item.AiTarget != null) { item.AiTarget.SightRange = item.AiTarget.MaxSightRange; @@ -220,6 +251,8 @@ namespace Barotrauma.Items.Components item.Drop(null); + launchPos = item.SimPosition; + item.body.Enabled = true; item.body.ApplyLinearImpulse(impulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); @@ -233,7 +266,7 @@ namespace Barotrauma.Items.Components if (stickJoint == null) { return; } - stickTarget = null; + StickTarget = null; GameMain.World.Remove(stickJoint); stickJoint = null; } @@ -287,7 +320,7 @@ namespace Barotrauma.Items.Components foreach (HitscanResult h in hits) { item.body.SetTransform(h.Point, rotation); - if (HandleProjectileCollision(h.Fixture, h.Normal)) + if (HandleProjectileCollision(h.Fixture, h.Normal, Vector2.Zero)) { hitSomething = true; break; @@ -321,7 +354,7 @@ namespace Barotrauma.Items.Components { //ignore sensors and items if (fixture?.Body == null || fixture.IsSensor) { return true; } - if (fixture.Body.UserData is Item) { return true; } + if (fixture.Body.UserData is Item item && item.GetComponent() == null && !item.Prefab.DamagedByProjectiles) { return true; } if (fixture.Body?.UserData as string == "ruinroom") { return true; } //ignore everything else than characters, sub walls and level walls @@ -341,7 +374,7 @@ namespace Barotrauma.Items.Components //ignore sensors and items if (fixture?.Body == null || fixture.IsSensor) { return -1; } - if (fixture.Body.UserData is Item item && item.GetComponent() == null) { return -1; } + if (fixture.Body.UserData is Item item && item.GetComponent() == null && !item.Prefab.DamagedByProjectiles) { return -1; } if (fixture.Body?.UserData as string == "ruinroom") { return -1; } //ignore everything else than characters, sub walls and level walls @@ -364,7 +397,7 @@ namespace Barotrauma.Items.Components while (impactQueue.Count > 0) { var impact = impactQueue.Dequeue(); - HandleProjectileCollision(impact.Fixture, impact.Normal); + HandleProjectileCollision(impact.Fixture, impact.Normal, impact.LinearVelocity); } if (item.body != null && item.body.FarseerBody.IsBullet) @@ -377,7 +410,7 @@ namespace Barotrauma.Items.Components } } - if (stickJoint == null) { return; } + if (stickJoint == null || StickPermanently) { return; } if (persistentStickJointTimer > 0.0f) { @@ -385,19 +418,20 @@ namespace Barotrauma.Items.Components return; } - if (stickJoint.JointTranslation < stickJoint.LowerLimit * 0.9f || stickJoint.JointTranslation > stickJoint.UpperLimit * 0.9f) + if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - stickTarget = null; - if (stickJoint != null) + if (stickJoint.JointTranslation < stickJoint.LowerLimit * 0.9f || + stickJoint.JointTranslation > stickJoint.UpperLimit * 0.9f) { - if (GameMain.World.JointList.Contains(stickJoint)) - { - GameMain.World.Remove(stickJoint); - } - stickJoint = null; + Unstick(); } - if (!item.body.FarseerBody.IsBullet) { IsActive = false; } - } +#if SERVER + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + { + item.CreateServerEvent(this); + } +#endif + } } @@ -405,6 +439,12 @@ namespace Barotrauma.Items.Components { if (User != null && User.Removed) { User = null; return false; } if (IgnoredBodies.Contains(target.Body)) { return false; } + //ignore character colliders (the projectile only hits limbs) + if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character) + { + return false; + } + if (hits.Contains(target.Body)) { return false; } if (target.Body.UserData is Submarine sub) { Vector2 dir = item.body.LinearVelocity.LengthSquared() < 0.001f ? @@ -415,9 +455,12 @@ namespace Barotrauma.Items.Components item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) - dir, item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) + dir, collisionCategory: Physics.CollisionWall); - if (wallBody?.FixtureList?.First() != null && wallBody.UserData is Structure structure) + if (wallBody?.FixtureList?.First() != null && wallBody.UserData is Structure structure && + //ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction + Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0) { target = wallBody.FixtureList.First(); + if (hits.Contains(target.Body)) { return false; } } else { @@ -440,18 +483,23 @@ namespace Barotrauma.Items.Components return false; } - impactQueue.Enqueue(new Impact(target, contact.Manifold.LocalNormal)); - item.body.FarseerBody.OnCollision -= OnProjectileCollision; - - return true; + hits.Add(target.Body); + impactQueue.Enqueue(new Impact(target, contact.Manifold.LocalNormal, item.body.LinearVelocity)); + if (hits.Count() >= MaxTargetsToHit) + { + item.body.FarseerBody.OnCollision -= OnProjectileCollision; + return true; + } + else + { + return false; + } } - private bool HandleProjectileCollision(Fixture target, Vector2 collisionNormal) + private bool HandleProjectileCollision(Fixture target, Vector2 collisionNormal, Vector2 velocity) { if (User != null && User.Removed) { User = null; } - if (IgnoredBodies.Contains(target.Body)) { return false; } - //ignore character colliders (the projectile only hits limbs) if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character) { @@ -472,7 +520,6 @@ namespace Barotrauma.Items.Components //severed limbs don't deactivate the projectile (but may still slow it down enough to make it inactive) if (limb.IsSevered) { - target.Body.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass); return true; } @@ -552,21 +599,30 @@ namespace Barotrauma.Items.Components } } - item.body.FarseerBody.OnCollision -= OnProjectileCollision; + target.Body.ApplyLinearImpulse(velocity * item.body.Mass); - item.body.CollisionCategories = Physics.CollisionItem; - item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; - - IgnoredBodies.Clear(); - - target.Body.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass); + if (hits.Count() >= MaxTargetsToHit) + { + item.body.FarseerBody.OnCollision -= OnProjectileCollision; + if (item.Prefab.DamagedByProjectiles || item.Prefab.DamagedByMeleeWeapons) + { + item.body.CollisionCategories = Physics.CollisionCharacter; + item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile; + } + else + { + item.body.CollisionCategories = Physics.CollisionItem; + item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; + } + IgnoredBodies.Clear(); + } if (attackResult.AppliedDamageModifiers != null && attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles)) { item.body.LinearVelocity *= 0.1f; } - else if (Vector2.Dot(item.body.LinearVelocity, collisionNormal) < 0.0f && + else if (Vector2.Dot(velocity, collisionNormal) < 0.0f && hits.Count() >= MaxTargetsToHit && (DoesStick || (StickToCharacters && target.Body.UserData is Limb) || (StickToStructures && target.Body.UserData is Structure) || @@ -575,8 +631,24 @@ namespace Barotrauma.Items.Components Vector2 dir = new Vector2( (float)Math.Cos(item.body.Rotation), (float)Math.Sin(item.body.Rotation)); - - StickToTarget(target.Body, dir); + + if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) + { + if (target.Body.UserData is Structure structure && structure.Submarine != item.Submarine && structure.Submarine != null) + { + StickToTarget(structure.Submarine.PhysicsBody.FarseerBody, dir); + } + else + { + StickToTarget(target.Body, dir); + } + } +#if SERVER + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + { + item.CreateServerEvent(this); + } +#endif item.body.LinearVelocity *= 0.5f; return Hitscan; @@ -608,7 +680,7 @@ namespace Barotrauma.Items.Components private void StickToTarget(Body targetBody, Vector2 axis) { - if (stickJoint != null) return; + if (stickJoint != null) { return; } stickJoint = new PrismaticJoint(targetBody, item.body.FarseerBody, item.body.SimPosition, axis, true) { @@ -616,19 +688,38 @@ namespace Barotrauma.Items.Components MaxMotorForce = 30.0f, LimitEnabled = true }; - if (item.Sprite != null) + + if (StickPermanently) { - stickJoint.LowerLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * -0.3f); - stickJoint.UpperLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * 0.3f); + stickJoint.LowerLimit = stickJoint.UpperLimit = 0.0f; + } + else if (item.Sprite != null) + { + stickJoint.LowerLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * -0.3f * item.Scale); + stickJoint.UpperLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * 0.3f * item.Scale); } persistentStickJointTimer = PersistentStickJointDuration; - stickTarget = targetBody; + StickTarget = targetBody; GameMain.World.Add(stickJoint); IsActive = true; } + private void Unstick() + { + StickTarget = null; + if (stickJoint != null) + { + if (GameMain.World.JointList.Contains(stickJoint)) + { + GameMain.World.Remove(stickJoint); + } + stickJoint = null; + } + if (!item.body.FarseerBody.IsBullet) { IsActive = false; } + } + protected override void RemoveComponentSpecific() { if (stickJoint != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 4544bd612..1b8e07500 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Globalization; @@ -111,7 +112,7 @@ namespace Barotrauma.Items.Components element.GetAttributeString("name", ""); //backwards compatibility - var showRepairUIAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().ToLowerInvariant() == "showrepairuithreshold"); + var showRepairUIAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals("showrepairuithreshold", StringComparison.OrdinalIgnoreCase)); if (showRepairUIAttribute != null) { float repairThreshold; @@ -130,7 +131,26 @@ namespace Barotrauma.Items.Components } partial void InitProjSpecific(XElement element); - + + /// + /// Check if the character manages to succesfully repair the item + /// + public bool CheckCharacterSuccess(Character character) + { + if (character == null) { return false; } + + if (statusEffectLists == null || statusEffectLists.None(s => s.Key == ActionType.OnFailure)) { return true; } + + // unpowered (electrical) items can be repaired without a risk of electrical shock + if (requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical", StringComparison.OrdinalIgnoreCase)) && + item.GetComponent() is Powered powered && powered.Voltage < 0.1f) { return true; } + + if (Rand.Range(0.0f, 0.5f) < DegreeOfSuccess(character)) { return true; } + + ApplyStatusEffects(ActionType.OnFailure, 1.0f, character); + return false; + } + public bool StartRepairing(Character character, FixActions action) { if (character == null || character.IsDead || action == FixActions.None) @@ -143,8 +163,15 @@ namespace Barotrauma.Items.Components #if SERVER if (CurrentFixer != character || currentFixerAction != action) { + if (!CheckCharacterSuccess(character)) + { + GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, character.ID }); + return false; + } item.CreateServerEvent(this); } +#else + if (GameMain.Client == null && (CurrentFixer != character || currentFixerAction != action) && !CheckCharacterSuccess(character)) { return false; } #endif CurrentFixer = character; CurrentFixerAction = action; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs new file mode 100644 index 000000000..72b995824 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -0,0 +1,252 @@ +using Barotrauma.Networking; +using FarseerPhysics; +using FarseerPhysics.Dynamics; +using Microsoft.Xna.Framework; +using System; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + partial class Rope : ItemComponent, IServerSerializable + { + private Item source, target; + + private float snapTimer; + private const float SnapAnimDuration = 1.0f; + + private float raycastTimer; + private const float RayCastInterval = 0.2f; + + [Serialize(0.0f, false, description: "How much force is applied to pull the projectile the rope is attached to.")] + public float ProjectilePullForce + { + get; + set; + } + + [Serialize(0.0f, false, description: "How much force is applied to pull the target the rope is attached to.")] + public float TargetPullForce + { + get; + set; + } + + [Serialize(0.0f, false, description: "How much force is applied to pull the source the rope is attached to.")] + public float SourcePullForce + { + get; + set; + } + + [Serialize(1000.0f, false, description: "How far the source item can be from the projectile until the rope breaks.")] + public float MaxLength + { + get; + set; + } + + [Serialize(true, false, description: "Should the rope snap when it collides with a structure/submarine (if not, it will just go through it).")] + public bool SnapOnCollision + { + get; + set; + } + + private bool snapped; + public bool Snapped + { + get { return snapped; } + set + { + if (snapped == value) { return; } + if (GameMain.NetworkMember != null) + { + if (GameMain.NetworkMember.IsClient) + { + return; + } + else + { +#if SERVER + item.CreateServerEvent(this); +#endif + } + } + snapped = value; + } + } + + public Rope(Item item, XElement element) : base(item, element) + { + InitProjSpecific(element); + } + + partial void InitProjSpecific(XElement element); + + + public void Attach(Item source, Item target) + { + System.Diagnostics.Debug.Assert(source != null); + System.Diagnostics.Debug.Assert(target != null); + this.source = source; + this.target = target; + ApplyStatusEffects(ActionType.OnUse, 1.0f, worldPosition: item.WorldPosition); + IsActive = true; + } + + public override void Update(float deltaTime, Camera cam) + { + if (source == null || source.Removed || target == null || target.Removed) + { + IsActive = false; + return; + } + + if (Snapped) + { + snapTimer += deltaTime; + if (snapTimer >= SnapAnimDuration) + { + IsActive = false; + } + return; + } + + Vector2 diff = target.WorldPosition - source.WorldPosition; + if (diff.LengthSquared() > MaxLength * MaxLength) + { + Snapped = true; + return; + } + + if (SnapOnCollision) + { + raycastTimer += deltaTime; + if (raycastTimer > RayCastInterval) + { + if (Submarine.PickBody(ConvertUnits.ToSimUnits(source.WorldPosition), ConvertUnits.ToSimUnits(target.WorldPosition), + collisionCategory: Physics.CollisionLevel | Physics.CollisionWall, + customPredicate: (Fixture f) => + { + var projectile = target?.GetComponent(); + if (projectile != null) + { + foreach (Body body in projectile.Hits) + { + Submarine alreadyHitSub = null; + if (body.UserData is Structure hitStructure) + { + alreadyHitSub = hitStructure.Submarine; + } + else if (body.UserData is Submarine hitSub) + { + alreadyHitSub = hitSub; + } + if (alreadyHitSub != null) + { + if (f.Body?.UserData is MapEntity me && me.Submarine == alreadyHitSub) { return false; } + if (f.Body?.UserData as Submarine == alreadyHitSub) { return false; } + } + } + } + Submarine targetSub = target?.GetComponent()?.StickTarget?.UserData as Submarine ?? target.Submarine; + + if (f.Body?.UserData is MapEntity mapEntity && mapEntity.Submarine != null) + { + if (mapEntity.Submarine == targetSub || mapEntity.Submarine == source.Submarine) + { + return false; + } + } + else if (f.Body?.UserData is Submarine sub) + { + if (sub == targetSub || sub == source.Submarine) + { + return false; + } + } + return true; + }) != null) + { + Snapped = true; + return; + } + raycastTimer = 0.0f; + } + } + + Vector2 forceDir = diff; + if (forceDir.LengthSquared() > 0.01f) + { + forceDir = Vector2.Normalize(forceDir); + } + + if (Math.Abs(ProjectilePullForce) > 0.001f) + { + var projectile = target.GetComponent(); + projectile?.Item?.body?.ApplyForce(-forceDir * ProjectilePullForce); + } + + if (Math.Abs(SourcePullForce) > 0.001f) + { + var sourceBody = GetBodyToPull(source); + if (sourceBody != null) + { + sourceBody.ApplyForce(forceDir * SourcePullForce); + } + } + + if (Math.Abs(TargetPullForce) > 0.001f) + { + var targetBody = GetBodyToPull(target); + if (targetBody != null) + { + targetBody.ApplyForce(-forceDir * TargetPullForce); + } + } + } + + public override void UpdateBroken(float deltaTime, Camera cam) + { + base.UpdateBroken(deltaTime, cam); + if (Snapped) + { + snapTimer += deltaTime; + if (snapTimer >= SnapAnimDuration) + { + IsActive = false; + } + } + } + + private PhysicsBody GetBodyToPull(Item target) + { + if (target.ParentInventory is CharacterInventory characterInventory && + characterInventory.Owner is Character ownerCharacter) + { + if (ownerCharacter.Removed) { return null; } + return ownerCharacter.AnimController.Collider; + } + var projectile = target.GetComponent(); + if (projectile != null) + { + if (projectile.StickTarget?.UserData is Structure structure) + { + return structure.Submarine?.PhysicsBody; + } + else if (projectile.StickTarget?.UserData is Submarine sub) + { + return sub?.PhysicsBody; + } + else if (projectile.StickTarget?.UserData is Character character) + { + return character.AnimController.Collider; + } + return null; + } + if (target.body != null) { return target.body; } + return null; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index ada7e145b..edbfddcdd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -88,7 +88,7 @@ namespace Barotrauma.Items.Components { foreach (XElement subElement in item.Prefab.ConfigElement.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "connectionpanel") { continue; } + if (!subElement.Name.ToString().Equals("connectionpanel", StringComparison.OrdinalIgnoreCase)) { continue; } foreach (XElement connectionElement in subElement.Elements()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 46812f554..e73698405 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -79,6 +79,12 @@ namespace Barotrauma.Items.Components Wire wire = wireItem.GetComponent(); if (wire != null) { + if (Item.ItemList.Any(it => it != item && (it.GetComponent()?.DisconnectedWires.Contains(wire) ?? false))) + { + if (wire.Item.body != null) { wire.Item.body.Enabled = false; } + wire.IsActive = false; + wire.UpdateSections(); + } DisconnectedWires.Add(wire); base.IsActive = true; } @@ -197,7 +203,7 @@ namespace Barotrauma.Items.Components float degreeOfSuccess = DegreeOfSuccess(character); if (Rand.Range(0.0f, 0.5f) < degreeOfSuccess) { return true; } - item.ApplyStatusEffects(ActionType.OnFailure, 1.0f, character); + ApplyStatusEffects(ActionType.OnFailure, 1.0f, character); return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index 6f0ca45b8..66ce55965 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -12,6 +12,7 @@ namespace Barotrauma.Items.Components public bool ContinuousSignal; public bool State; public string ConnectionName; + public string PropertyName; public Connection Connection; [Serialize("", false, translationTextTag: "Label.", description: "The text displayed on this button/tickbox."), Editable] public string Label { get; set; } @@ -28,11 +29,12 @@ namespace Barotrauma.Items.Components { Label = element.GetAttributeString("text", ""); ConnectionName = element.GetAttributeString("connection", ""); + PropertyName = element.GetAttributeString("propertyname", "").ToLowerInvariant(); Signal = element.GetAttributeString("signal", "1"); foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "statuseffect") + if (subElement.Name.ToString().Equals("statuseffect", System.StringComparison.OrdinalIgnoreCase)) { StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName: "custom interface element (label " + Label + ")")); } @@ -89,6 +91,7 @@ namespace Barotrauma.Items.Components switch (subElement.Name.ToString().ToLowerInvariant()) { case "button": + case "textbox": var button = new CustomInterfaceElement(subElement) { ContinuousSignal = false @@ -106,7 +109,7 @@ namespace Barotrauma.Items.Components }; if (string.IsNullOrEmpty(tickBox.Label)) { - tickBox.Label = "Signal out " + customInterfaceElementList.Count(e => !e.ContinuousSignal); + tickBox.Label = "Signal out " + customInterfaceElementList.Count(e => e.ContinuousSignal); } customInterfaceElementList.Add(tickBox); break; @@ -168,6 +171,18 @@ namespace Barotrauma.Items.Components tickBoxElement.State = state; } + private void TextChanged(CustomInterfaceElement textElement, string text) + { + textElement.Signal = text; + foreach (ISerializableEntity e in item.AllPropertyObjects) + { + if (e.SerializableProperties.ContainsKey(textElement.PropertyName)) + { + e.SerializableProperties[textElement.PropertyName].TrySetValue(e, text); + } + } + } + public override void Update(float deltaTime, Camera cam) { UpdateProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index 7a150d53f..593b7113f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -21,7 +21,7 @@ namespace Barotrauma.Items.Components private float blinkTimer; - private bool itemLoaded; + private double lastToggleSignalTime; public PhysicsBody ParentBody; @@ -78,9 +78,7 @@ namespace Barotrauma.Items.Components if (IsActive == value) { return; } IsActive = value; -#if SERVER - if (GameMain.Server != null && itemLoaded) { item.CreateServerEvent(this); } -#endif + OnStateChanged(); } } @@ -117,6 +115,13 @@ namespace Barotrauma.Items.Components } } + [Serialize(false, false, description: "If enabled, the component will ignore continuous signals received in the toggle input (i.e. a continuous signal will only toggle it once).")] + public bool IgnoreContinuousToggle + { + get; + set; + } + public override void Move(Vector2 amount) { #if CLIENT @@ -158,14 +163,7 @@ namespace Barotrauma.Items.Components IsActive = IsOn; item.AddTag("light"); } - - public override void OnItemLoaded() - { - base.OnItemLoaded(); - itemLoaded = true; - SetLightSourceState(IsActive, lightBrightness); - } - + public override void Update(float deltaTime, Camera cam) { if (item.AiTarget != null) @@ -249,15 +247,21 @@ namespace Barotrauma.Items.Components return true; } + partial void OnStateChanged(); + public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) { switch (connection.Name) { case "toggle": - IsActive = !IsActive; + if (IgnoreContinuousToggle && lastToggleSignalTime < Timing.TotalTime - 0.1) + { + IsOn = !IsOn; + } + lastToggleSignalTime = Timing.TotalTime; break; case "set_state": - IsActive = (signal != "0"); + IsOn = signal != "0"; break; case "set_color": LightColor = XMLExtensions.ParseColor(signal, false); @@ -265,20 +269,14 @@ namespace Barotrauma.Items.Components } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) - { - msg.Write(IsOn); - } - private void UpdateAITarget(AITarget target) { - target.Enabled = IsActive; if (!IsActive) { return; } if (target.MaxSightRange <= 0) { target.MaxSightRange = Range * 5; } - target.SightRange = target.MaxSightRange * lightBrightness; + target.SightRange = Math.Max(target.SightRange, target.MaxSightRange * lightBrightness); } partial void SetLightSourceState(bool enabled, float brightness); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index a1da8e1d1..5513e315e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -39,6 +39,14 @@ namespace Barotrauma.Items.Components } } + + [Serialize(false, false, description: "Can the component communicate with wifi components in another team's submarine (e.g. enemy sub in Combat missions, respawn shuttle). Needs to be enabled on both the component transmitting the signal and the component receiving it.")] + public bool AllowCrossTeamCommunication + { + get; + set; + } + [Editable, Serialize(false, false, description: "If enabled, any signals received from another chat-linked wifi component are displayed " + "as chat messages in the chatbox of the player holding the item.")] public bool LinkToChat @@ -84,7 +92,7 @@ namespace Barotrauma.Items.Components { if (sender == null || sender.channel != channel) { return false; } - if (sender.TeamID != TeamID) + if (sender.TeamID != TeamID && !AllowCrossTeamCommunication) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 5879e6be8..2262d3de7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -34,6 +34,8 @@ namespace Barotrauma.Items.Components private int failedLaunchAttempts; + private readonly List activeProjectiles = new List(); + private Character user; [Serialize("0,0", false, description: "The position of the barrel relative to the upper left corner of the base sprite (in pixels).")] @@ -72,6 +74,20 @@ namespace Barotrauma.Items.Components set { reloadTime = value; } } + [Serialize(1, false, description: "How projectiles the weapon launches when fired once.")] + public int ProjectileCount + { + get; + set; + } + + [Serialize(false, false, description: "Can the turret be fired without projectiles (causing it just to execute the OnUse effects and the firing animation without actually firing anything).")] + public bool LaunchWithoutProjectile + { + get; + set; + } + [Editable, Serialize("0.0,0.0", true, description: "The range at which the barrel can rotate. TODO")] public Vector2 RotationLimits { @@ -95,6 +111,13 @@ namespace Barotrauma.Items.Components } } + [Serialize(0.0f, false, description: "Random spread applied to the firing angle of the projectiles (in degrees).")] + public float Spread + { + get; + set; + } + [Editable(0.0f, 1000.0f, DecimalCount = 2), Serialize(5.0f, false, description: "How much torque is applied to rotate the barrel when the item is used by a character" + " with insufficient skills to operate it. Higher values make the barrel rotate faster.")] @@ -155,7 +178,21 @@ namespace Barotrauma.Items.Components UpdateTransformedBarrelPos(); } } - + + [Serialize(3000.0f, true, description: "How close to a target the turret has to be for an AI character to fire it.")] + public float AIRange + { + get; + set; + } + + [Serialize(-1, true, description: "The turret won't fire additional projectiles if the number of previously fired, still active projectiles reaches this limit. If set to -1, there is no limit to the number of projectiles.")] + public int MaxActiveProjectiles + { + get; + set; + } + public Turret(Item item, XElement element) : base(item, element) { @@ -213,13 +250,13 @@ namespace Barotrauma.Items.Components { this.cam = cam; - if (reload > 0.0f) reload -= deltaTime; + if (reload > 0.0f) { reload -= deltaTime; } ApplyStatusEffects(ActionType.OnActive, deltaTime, null); UpdateProjSpecific(deltaTime); - if (minRotation == maxRotation) return; + if (minRotation == maxRotation) { return; } float targetMidDiff = MathHelper.WrapAngle(targetRotation - (minRotation + maxRotation) / 2.0f); @@ -230,12 +267,19 @@ namespace Barotrauma.Items.Components targetRotation = (targetMidDiff < 0.0f) ? minRotation : maxRotation; } - float degreeOfSuccess = user == null ? 0.5f : DegreeOfSuccess(user); - if (degreeOfSuccess < 0.5f) degreeOfSuccess *= degreeOfSuccess; //the ease of aiming drops quickly with insufficient skill levels + float degreeOfSuccess = user == null ? 0.5f : DegreeOfSuccess(user); + if (degreeOfSuccess < 0.5f) { degreeOfSuccess *= degreeOfSuccess; } //the ease of aiming drops quickly with insufficient skill levels float springStiffness = MathHelper.Lerp(SpringStiffnessLowSkill, SpringStiffnessHighSkill, degreeOfSuccess); float springDamping = MathHelper.Lerp(SpringDampingLowSkill, SpringDampingHighSkill, degreeOfSuccess); float rotationSpeed = MathHelper.Lerp(RotationSpeedLowSkill, RotationSpeedHighSkill, degreeOfSuccess); + if (user?.Info != null) + { + user.Info.IncreaseSkillLevel("weapons", + SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f), + user.WorldPosition + Vector2.UnitY * 150.0f); + } + angularVelocity += (MathHelper.WrapAngle(targetRotation - rotation) * springStiffness - angularVelocity * springDamping) * deltaTime; angularVelocity = MathHelper.Clamp(angularVelocity, -rotationSpeed, rotationSpeed); @@ -265,95 +309,110 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character character = null) { - if (!characterUsable && character != null) return false; + if (!characterUsable && character != null) { return false; } return TryLaunch(deltaTime, character); } - private bool TryLaunch(float deltaTime, Character character = null) + private bool TryLaunch(float deltaTime, Character character = null, bool ignorePower = false) { -#if CLIENT - if (GameMain.Client != null) return false; -#endif + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } - if (reload > 0.0f) return false; + if (reload > 0.0f) { return false; } - if (GetAvailableBatteryPower() < powerConsumption) + if (MaxActiveProjectiles >= 0) { -#if CLIENT - if (!flashLowPower && character != null && character == Character.Controlled) + activeProjectiles.RemoveAll(it => it.Removed); + if (activeProjectiles.Count >= MaxActiveProjectiles) { - flashLowPower = true; - GUI.PlayUISound(GUISoundType.PickItemFail); + return false; } -#endif - return false; } - foreach (MapEntity e in item.linkedTo) + if (!ignorePower) { - //use linked projectile containers in case they have to react to the turret being launched somehow - //(play a sound, spawn more projectiles) - if (!(e is Item linkedItem)) continue; - ItemContainer projectileContainer = linkedItem.GetComponent(); - if (projectileContainer != null) + if (GetAvailableBatteryPower() < powerConsumption) { - linkedItem.Use(deltaTime, null); - var repairable = linkedItem.GetComponent(); - if (repairable != null) - { - repairable.LastActiveTime = (float)Timing.TotalTime + 1.0f; - } - } - } - - var projectiles = GetLoadedProjectiles(true); - if (projectiles.Count == 0) - { - //coilguns spawns ammo in the ammo boxes with the OnUse statuseffect when the turret is launched, - //causing a one frame delay before the gun can be launched (or more in multiplayer where there may be a longer delay) - // -> attempt to launch the gun multiple times before showing the "no ammo" flash - failedLaunchAttempts++; #if CLIENT - if (!flashNoAmmo && character != null && character == Character.Controlled && failedLaunchAttempts > 20) - { - flashNoAmmo = true; - failedLaunchAttempts = 0; - GUI.PlayUISound(GUISoundType.PickItemFail); - } -#endif - return false; - } - - failedLaunchAttempts = 0; - - var batteries = item.GetConnectedComponents(); - float neededPower = powerConsumption; - - while (neededPower > 0.0001f && batteries.Count > 0) - { - batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f); - float takePower = neededPower / batteries.Count; - takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); - foreach (PowerContainer battery in batteries) - { - neededPower -= takePower; - battery.Charge -= takePower / 3600.0f; -#if SERVER - if (GameMain.Server != null) + if (!flashLowPower && character != null && character == Character.Controlled) { - battery.Item.CreateServerEvent(battery); + flashLowPower = true; + GUI.PlayUISound(GUISoundType.PickItemFail); } #endif + return false; } } - Launch(projectiles[0].Item, character); + Projectile launchedProjectile = null; + for (int i = 0; i < ProjectileCount; i++) + { + foreach (MapEntity e in item.linkedTo) + { + //use linked projectile containers in case they have to react to the turret being launched somehow + //(play a sound, spawn more projectiles) + if (!(e is Item linkedItem)) { continue; } + ItemContainer projectileContainer = linkedItem.GetComponent(); + if (projectileContainer != null) + { + linkedItem.Use(deltaTime, null); + var repairable = linkedItem.GetComponent(); + if (repairable != null) + { + repairable.LastActiveTime = (float)Timing.TotalTime + 1.0f; + } + } + } + var projectiles = GetLoadedProjectiles(true); + if (projectiles.Count == 0 && !LaunchWithoutProjectile) + { + //coilguns spawns ammo in the ammo boxes with the OnUse statuseffect when the turret is launched, + //causing a one frame delay before the gun can be launched (or more in multiplayer where there may be a longer delay) + // -> attempt to launch the gun multiple times before showing the "no ammo" flash + failedLaunchAttempts++; +#if CLIENT + if (!flashNoAmmo && character != null && character == Character.Controlled && failedLaunchAttempts > 20) + { + flashNoAmmo = true; + failedLaunchAttempts = 0; + GUI.PlayUISound(GUISoundType.PickItemFail); + } +#endif + return false; + } + failedLaunchAttempts = 0; + launchedProjectile = projectiles.FirstOrDefault(); + + if (!ignorePower) + { + var batteries = item.GetConnectedComponents(); + float neededPower = powerConsumption; + while (neededPower > 0.0001f && batteries.Count > 0) + { + batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f); + float takePower = neededPower / batteries.Count; + takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); + foreach (PowerContainer battery in batteries) + { + neededPower -= takePower; + battery.Charge -= takePower / 3600.0f; +#if SERVER + battery.Item.CreateServerEvent(battery); +#endif + } + } + } + + if (launchedProjectile != null || LaunchWithoutProjectile) + { + Launch(launchedProjectile?.Item, character); + } + } #if SERVER - if (character != null) + if (character != null && launchedProjectile != null) { - string msg = character.LogName + " launched " + item.Name + " (projectile: " + projectiles[0].Item.Name; - var containedItems = projectiles[0].Item.ContainedItems; + string msg = character.LogName + " launched " + item.Name + " (projectile: " + launchedProjectile.Item.Name; + var containedItems = launchedProjectile.Item.ContainedItems; if (containedItems == null || !containedItems.Any()) { msg += ")"; @@ -373,27 +432,36 @@ namespace Barotrauma.Items.Components { reload = reloadTime; - projectile.Drop(null); - projectile.body.Dir = 1.0f; - - projectile.body.ResetDynamics(); - projectile.body.Enabled = true; - projectile.SetTransform(ConvertUnits.ToSimUnits(new Vector2(item.WorldRect.X + transformedBarrelPos.X, item.WorldRect.Y - transformedBarrelPos.Y)), -rotation); - projectile.UpdateTransform(); - projectile.Submarine = projectile.body.Submarine; - - Projectile projectileComponent = projectile.GetComponent(); - if (projectileComponent != null) + if (projectile != null) { - projectileComponent.Use((float)Timing.Step); - projectileComponent.User = user; - } + activeProjectiles.Add(projectile); + projectile.Drop(null); + if (projectile.body != null) + { + projectile.body.Dir = 1.0f; + projectile.body.ResetDynamics(); + projectile.body.Enabled = true; + } - if (projectile.Container != null) projectile.Container.RemoveContained(projectile); + float spread = MathHelper.ToRadians(Spread) * Rand.Range(-0.5f, 0.5f); + projectile.SetTransform(ConvertUnits.ToSimUnits(new Vector2(item.WorldRect.X + transformedBarrelPos.X, item.WorldRect.Y - transformedBarrelPos.Y)), -rotation + spread); + projectile.UpdateTransform(); + projectile.Submarine = projectile.body?.Submarine; + + Projectile projectileComponent = projectile.GetComponent(); + if (projectileComponent != null) + { + projectileComponent.Use((float)Timing.Step); + projectile.GetComponent()?.Attach(item, projectile); + projectileComponent.User = user; + } + + if (projectile.Container != null) { projectile.Container.RemoveContained(projectile); } - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) - { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), projectile }); + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + { + GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), projectile }); + } } ApplyStatusEffects(ActionType.OnUse, 1.0f, user: user); @@ -402,6 +470,182 @@ namespace Barotrauma.Items.Components partial void LaunchProjSpecific(); + private float waitTimer; + private float disorderTimer; + + private float prevTargetRotation; + private float updateTimer; + private bool updatePending; + public void ThalamusOperate(float deltaTime, bool targetHumans, bool targetOtherCreatures, bool targetSubmarines, bool ignoreDelay) + { + IsActive = true; + + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) + { + return; + } + + if (updatePending) + { + if (updateTimer < 0.0f) + { +#if SERVER + item.CreateServerEvent(this); +#endif + prevTargetRotation = targetRotation; + updateTimer = 0.25f; + } + updateTimer -= deltaTime; + } + + if (!ignoreDelay && waitTimer > 0) + { + waitTimer -= deltaTime; + return; + } + Submarine closestSub = null; + float maxDistance = 10000.0f; + float shootDistance = AIRange; + ISpatialEntity target = null; + float closestDist = shootDistance * shootDistance; + if (targetHumans || targetOtherCreatures) + { + foreach (var character in Character.CharacterList) + { + if (character == null || character.Removed || character.IsDead) { continue; } + if (character.Params.Group.Equals("thalamus", StringComparison.OrdinalIgnoreCase)) { continue; } + bool isHuman = character.IsHuman || character.Params.Group.Equals("human", StringComparison.OrdinalIgnoreCase); + if (isHuman) + { + if (!targetHumans) + { + // Don't target humans if not defined to. + continue; + } + } + else if (!targetOtherCreatures) + { + // Don't target other creatures if not defined to. + continue; + } + float dist = Vector2.DistanceSquared(character.WorldPosition, item.WorldPosition); + if (dist > closestDist) { continue; } + target = character; + closestDist = dist; + } + } + if (targetSubmarines) + { + if (target == null || target.Submarine != null) + { + closestDist = maxDistance * maxDistance; + foreach (Submarine sub in Submarine.Loaded) + { + if (sub.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } + float dist = Vector2.DistanceSquared(sub.WorldPosition, item.WorldPosition); + if (dist > closestDist) { continue; } + closestSub = sub; + closestDist = dist; + } + closestDist = shootDistance * shootDistance; + if (closestSub != null) + { + foreach (var hull in Hull.hullList) + { + if (!closestSub.IsEntityFoundOnThisSub(hull, true)) { continue; } + float dist = Vector2.DistanceSquared(hull.WorldPosition, item.WorldPosition); + if (dist > closestDist) { continue; } + target = hull; + closestDist = dist; + } + } + } + } + if (!ignoreDelay) + { + if (target == null) + { + // Random movement + waitTimer = Rand.Value(Rand.RandSync.Unsynced) < 0.98f ? 0f : Rand.Range(5f, 20f); + targetRotation = Rand.Range(minRotation, maxRotation); + updatePending = true; + return; + } + if (disorderTimer < 0) + { + // Random disorder + disorderTimer = Rand.Range(0f, 3f); + waitTimer = Rand.Range(0.25f, 1f); + targetRotation = MathUtils.WrapAngleTwoPi(targetRotation += Rand.Range(-1f, 1f)); + updatePending = true; + return; + } + else + { + disorderTimer -= deltaTime; + } + } + if (target == null) { return; } + + float angle = -MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition); + targetRotation = MathUtils.WrapAngleTwoPi(angle); + + if (Math.Abs(targetRotation - prevTargetRotation) > 0.1f) { updatePending = true; } + + if (target is Hull targetHull) + { + Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); + if (!MathUtils.GetLineRectangleIntersection(item.WorldPosition, item.WorldPosition + barrelDir * AIRange, targetHull.WorldRect, out _)) + { + return; + } + } + else + { + float midRotation = (minRotation + maxRotation) / 2.0f; + while (midRotation - angle < -MathHelper.Pi) { angle -= MathHelper.TwoPi; } + while (midRotation - angle > MathHelper.Pi) { angle += MathHelper.TwoPi; } + if (angle < minRotation || angle > maxRotation) { return; } + float enemyAngle = MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition); + float turretAngle = -rotation; + if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return; } + } + + Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition); + Vector2 end = ConvertUnits.ToSimUnits(target.WorldPosition); + if (target.Submarine != null) + { + start -= target.Submarine.SimPosition; + end -= target.Submarine.SimPosition; + } + var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel; + var pickedBody = Submarine.PickBody(start, end, null, collisionCategories); + if (pickedBody == null) { return; } + Character targetCharacter = null; + if (pickedBody.UserData is Character c) + { + targetCharacter = c; + } + else if (pickedBody.UserData is Limb limb) + { + targetCharacter = limb.character; + } + if (targetCharacter != null) + { + if (targetCharacter.Params.Group.Equals("thalamus", StringComparison.OrdinalIgnoreCase)) + { + // Don't shoot friendly characters + return; + } + } + else if (!(pickedBody.UserData is Structure) && !(pickedBody.UserData is Item)) + { + // Hit something else than a wall or an item (probably a level wall) + return; + } + TryLaunch(deltaTime, ignorePower: true); + } + public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && @@ -483,7 +727,7 @@ namespace Barotrauma.Items.Components //enough shells and power Character closestEnemy = null; - float closestDist = 3000 * 3000; + float closestDist = AIRange * AIRange; foreach (Character enemy in Character.CharacterList) { // Ignore dead, friendly, and those that are inside the same sub @@ -547,7 +791,7 @@ namespace Barotrauma.Items.Components return false; } - if (objective.Option.ToLowerInvariant() == "fireatwill") + if (objective.Option.Equals("fireatwill", StringComparison.OrdinalIgnoreCase)) { character?.Speak(TextManager.GetWithVariable("DialogFireTurret", "[itemname]", item.Name, true), null, 0.0f, "fireturret", 5.0f); character.SetInput(InputType.Shoot, true, true); @@ -556,19 +800,6 @@ namespace Barotrauma.Items.Components return false; } - private void GetAvailablePower(out float availableCharge, out float availableCapacity) - { - var batteries = item.GetConnectedComponents(); - - availableCharge = 0.0f; - availableCapacity = 0.0f; - foreach (PowerContainer battery in batteries) - { - availableCharge += battery.Charge; - availableCapacity += battery.Capacity; - } - } - protected override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); @@ -591,7 +822,7 @@ namespace Barotrauma.Items.Components foreach (MapEntity e in item.linkedTo) { if (e is Item projectileContainer) { CheckProjectileContainer(projectileContainer, projectiles, returnFirst); } - if (returnFirst && projectiles.Any()) return projectiles; + if (returnFirst && projectiles.Any()) { return projectiles; } } return projectiles; @@ -600,27 +831,27 @@ namespace Barotrauma.Items.Components private void CheckProjectileContainer(Item projectileContainer, List projectiles, bool returnFirst) { var containedItems = projectileContainer.ContainedItems; - if (containedItems == null) return; + if (containedItems == null) { return; } foreach (Item containedItem in containedItems) { var projectileComponent = containedItem.GetComponent(); - if (projectileComponent != null) + if (projectileComponent != null && projectileComponent.Item.body != null) { projectiles.Add(projectileComponent); - if (returnFirst) return; + if (returnFirst) { return; } } else { //check if the contained item is another itemcontainer with projectiles inside it - if (containedItem.ContainedItems == null) continue; + if (containedItem.ContainedItems == null) { continue; } foreach (Item subContainedItem in containedItem.ContainedItems) { projectileComponent = subContainedItem.GetComponent(); - if (projectileComponent != null) + if (projectileComponent != null && projectileComponent.Item.body != null) { projectiles.Add(projectileComponent); - if (returnFirst) return; + if (returnFirst) { return; } } } } @@ -694,7 +925,6 @@ namespace Barotrauma.Items.Components TryLaunch((float)Timing.Step, sender); } break; - case "toggle": case "toggle_light": if (lightComponent != null) { @@ -706,8 +936,9 @@ namespace Barotrauma.Items.Components public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { - Item item = (Item)extraData[2]; - msg.Write(item.Removed ? (ushort)0 : item.ID); + Item item = extraData.Length > 2 ? (Item)extraData[2] : null; + msg.Write(item == null || item.Removed ? (ushort)0 : item.ID); + msg.WriteRangedSingle(MathHelper.Clamp(targetRotation, minRotation, maxRotation), minRotation, maxRotation, 8); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index df34a4c9a..c95adedb1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -211,6 +211,7 @@ namespace Barotrauma.Items.Components } public bool AutoEquipWhenFull { get; private set; } + public bool DisplayContainedStatus { get; private set; } public readonly int Variants; @@ -264,6 +265,7 @@ namespace Barotrauma.Items.Components limbType = new LimbType[spriteCount]; limb = new Limb[spriteCount]; AutoEquipWhenFull = element.GetAttributeBool("autoequipwhenfull", true); + DisplayContainedStatus = element.GetAttributeBool("displaycontainedstatus", false); int i = 0; foreach (XElement subElement in element.Elements()) { @@ -284,7 +286,7 @@ namespace Barotrauma.Items.Components foreach (XElement lightElement in subElement.Elements()) { - if (lightElement.Name.ToString().ToLowerInvariant() != "lightcomponent") continue; + if (!lightElement.Name.ToString().Equals("lightcomponent", StringComparison.OrdinalIgnoreCase)) { continue; } wearableSprites[i].LightComponent = new LightComponent(item, lightElement) { Parent = this diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 2a46248ef..98eb99010 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -197,6 +197,7 @@ namespace Barotrauma if (item.body != null) { item.body.Enabled = false; + item.body.BodyType = FarseerPhysics.BodyType.Dynamic; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 2310af049..81e482f8f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -25,7 +25,8 @@ namespace Barotrauma OnFire, InWater, NotInWater, OnImpact, OnEating, - OnDeath = OnBroken + OnDeath = OnBroken, + OnDamaged } partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable @@ -164,13 +165,6 @@ namespace Barotrauma get { return description ?? prefab.Description; } set { description = value; } } - - [Editable, Serialize(false, true)] - public bool HiddenInGame - { - get; - set; - } [Editable, Serialize(false, true)] public bool NonInteractable @@ -290,6 +284,24 @@ namespace Barotrauma set { /*do nothing*/ } } + + [Serialize("", true)] + + /// + /// Can be used to modify the AITarget's label using status effects + /// + public string SonarLabel + { + get { return AiTarget?.SonarLabel ?? ""; } + set + { + if (AiTarget != null) + { + AiTarget.SonarLabel = value; + } + } + } + [Serialize(false, false)] /// /// Can be used by status effects or conditionals to check if the physics body of the item is active @@ -1237,6 +1249,8 @@ namespace Barotrauma float damageAmount = attack.GetItemDamage(deltaTime); Condition -= damageAmount; + ApplyStatusEffects(ActionType.OnDamaged, 1.0f); + return new AttackResult(damageAmount, null); } @@ -1296,6 +1310,7 @@ namespace Barotrauma #if CLIENT if (ic.HasSounds) { + ic.PlaySound(ActionType.Always); ic.UpdateSounds(); if (!ic.WasUsed) { @@ -1343,7 +1358,7 @@ namespace Barotrauma UpdateTransform(); if (CurrentHull == null && body.SimPosition.Y < ConvertUnits.ToSimUnits(Level.MaxEntityDepth)) { - Spawner.AddToRemoveQueue(this); + Spawner?.AddToRemoveQueue(this); return; } } @@ -1726,16 +1741,15 @@ namespace Barotrauma public bool TryInteract(Character picker, bool ignoreRequiredItems = false, bool forceSelectKey = false, bool forceActionKey = false) { - bool hasRequiredSkills = true; - bool picked = false, selected = false; - +#if CLIENT + bool hasRequiredSkills = true; Skill requiredSkill = null; - +#endif foreach (ItemComponent ic in components) { bool pickHit = false, selectHit = false; - + if (picker.IsKeyDown(InputType.Aim)) { pickHit = false; @@ -1779,13 +1793,11 @@ namespace Barotrauma picker.IsKeyHit(InputType.Select); } #endif - - if (!pickHit && !selectHit) continue; - - if (!ic.HasRequiredSkills(picker, out Skill tempRequiredSkill)) hasRequiredSkills = false; + if (!pickHit && !selectHit) { continue; } bool showUiMsg = false; #if CLIENT + if (!ic.HasRequiredSkills(picker, out Skill tempRequiredSkill)) { hasRequiredSkills = false; } showUiMsg = picker == Character.Controlled && Screen.Selected != GameMain.SubEditorScreen; #endif if (!ignoreRequiredItems && !ic.HasRequiredItems(picker, showUiMsg)) continue; @@ -1795,10 +1807,9 @@ namespace Barotrauma picked = true; ic.ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker); #if CLIENT - if (picker == Character.Controlled) GUI.ForceMouseOn(null); + if (picker == Character.Controlled) { GUI.ForceMouseOn(null); } + if (tempRequiredSkill != null) { requiredSkill = tempRequiredSkill; } #endif - if (tempRequiredSkill != null) requiredSkill = tempRequiredSkill; - if (ic.CanBeSelected) selected = true; } } @@ -1839,6 +1850,30 @@ namespace Barotrauma return true; } + public float GetContainedItemConditionPercentage() + { + var containedItems = ContainedItems; + + if (containedItems != null) + { + float condition = 0f; + float maxCondition = 0f; + + foreach (Item item in containedItems) + { + condition += item.condition; + maxCondition += item.MaxCondition; + } + + if (maxCondition > 0.0f) + { + return condition / maxCondition; + } + } + + return -1; + } + public void Use(float deltaTime, Character character = null, Limb targetLimb = null) { if (RequireAimToUse && (character == null || !character.IsKeyDown(InputType.Aim))) @@ -2333,9 +2368,9 @@ namespace Barotrauma item.SetActiveSprite(); - if (submarine?.GameVersion != null) + if (submarine?.Info.GameVersion != null) { - SerializableProperty.UpgradeGameVersion(item, item.Prefab.ConfigElement, submarine.GameVersion); + SerializableProperty.UpgradeGameVersion(item, item.Prefab.ConfigElement, submarine.Info.GameVersion); } foreach (ItemComponent component in item.components) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 041b021ad..b29a31ad5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -255,6 +255,13 @@ namespace Barotrauma private set; } + //if true and the item has trigger areas defined, players can only highlight the item when the cursor is on the trigger + [Serialize(false, false)] + public bool RequireCursorInsideTrigger + { + get; + private set; + } //should the camera focus on the item when selected [Serialize(false, false)] @@ -535,6 +542,8 @@ namespace Barotrauma } Category = category; + var parentType = element.Parent?.GetAttributeString("itemtype", "") ?? string.Empty; + //nameidentifier can be used to make multiple items use the same names and descriptions string nameIdentifier = element.GetAttributeString("nameidentifier", ""); @@ -566,7 +575,15 @@ namespace Barotrauma identifier = GenerateLegacyIdentifier(originalName); } } - + + if (string.Equals(parentType, "wrecked", StringComparison.OrdinalIgnoreCase)) + { + if (!string.IsNullOrEmpty(name)) + { + name = TextManager.GetWithVariable("wreckeditemformat", "[name]", name); + } + } + if (string.IsNullOrEmpty(name)) { DebugConsole.ThrowError($"Unnamed item ({identifier})in {filePath}!"); @@ -810,7 +827,7 @@ namespace Barotrauma public PriceInfo GetPrice(Location location) { - if (prices == null || !prices.ContainsKey(location.Type.Identifier.ToLowerInvariant())) return null; + if (prices == null || !prices.ContainsKey(location.Type.Identifier.ToLowerInvariant())) { return null; } return prices[location.Type.Identifier.ToLowerInvariant()]; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index d21bb00a2..4cc0bc78c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -152,7 +152,7 @@ namespace Barotrauma public void Save(XElement element) { element.Add( - new XAttribute("identifiers", JoinedIdentifiers), + new XAttribute("items", JoinedIdentifiers), new XAttribute("type", type.ToString()), new XAttribute("optional", IsOptional), new XAttribute("ignoreineditor", IgnoreInEditor)); @@ -219,7 +219,10 @@ namespace Barotrauma string typeStr = element.GetAttributeString("type", ""); if (string.IsNullOrEmpty(typeStr)) { - if (element.Name.ToString().ToLowerInvariant() == "containable") typeStr = "Contained"; + if (element.Name.ToString().Equals("containable", StringComparison.OrdinalIgnoreCase)) + { + typeStr = "Contained"; + } } if (!Enum.TryParse(typeStr, true, out ri.type)) { @@ -246,7 +249,7 @@ namespace Barotrauma foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "statuseffect") continue; + if (!subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { continue; } ri.statusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/CorpsePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/CorpsePrefab.cs new file mode 100644 index 000000000..07011e517 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/CorpsePrefab.cs @@ -0,0 +1,227 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + class CorpsePrefab : IPrefab, IDisposable + { + public static readonly PrefabCollection Prefabs = new PrefabCollection(); + + private bool disposed = false; + public void Dispose() + { + if (disposed) { return; } + disposed = true; + Prefabs.Remove(this); + } + + public static CorpsePrefab Get(string identifier) + { + if (Prefabs == null) + { + DebugConsole.ThrowError("Issue in the code execution order: job prefabs not loaded."); + return null; + } + if (Prefabs.ContainsKey(identifier)) + { + return Prefabs[identifier]; + } + else + { + DebugConsole.ThrowError("Couldn't find a job prefab with the given identifier: " + identifier); + return null; + } + } + + [Serialize("notfound", false)] + public string Identifier { get; private set; } + + [Serialize("any", false)] + public string Job { get; private set; } + + [Serialize(1f, false)] + public float Commonness { get; private set; } + + [Serialize(Level.PositionType.Wreck, false)] + public Level.PositionType SpawnPosition { get; private set; } + + public string OriginalName { get { return Identifier; } } + + public ContentPackage ContentPackage { get; private set; } + + public string FilePath { get; private set; } + + public XElement Element { get; private set; } + + public readonly Dictionary ItemSets = new Dictionary(); + + public CorpsePrefab(XElement element, string filePath, bool allowOverriding) + { + FilePath = filePath; + SerializableProperty.DeserializeProperties(this, element); + Identifier = Identifier.ToLowerInvariant(); + Job = Job.ToLowerInvariant(); + Element = element; + element.GetChildElements("itemset").ForEach(e => ItemSets.Add(e, e.GetAttributeFloat("commonness", 1))); + Prefabs.Add(this, allowOverriding); + } + + public static CorpsePrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(sync); + + public static void LoadAll(IEnumerable files) + { + foreach (ContentFile file in files) + { + LoadFromFile(file); + } + } + + public static void LoadFromFile(ContentFile file) + { + DebugConsole.Log("*** " + file.Path + " ***"); + RemoveByFile(file.Path); + + XDocument doc = XMLExtensions.TryLoadXml(file.Path); + if (doc == null) { return; } + + var rootElement = doc.Root; + switch (rootElement.Name.ToString().ToLowerInvariant()) + { + case "corpse": + new CorpsePrefab(rootElement, file.Path, false) + { + ContentPackage = file.ContentPackage + }; + break; + case "corpses": + foreach (var element in rootElement.Elements()) + { + if (element.IsOverride()) + { + var itemElement = element.GetChildElement("item"); + if (itemElement != null) + { + new CorpsePrefab(itemElement, file.Path, true) + { + ContentPackage = file.ContentPackage + }; + } + else + { + DebugConsole.ThrowError($"Cannot find an item element from the children of the override element defined in {file.Path}"); + } + } + else + { + new CorpsePrefab(element, file.Path, false) + { + ContentPackage = file.ContentPackage + }; + } + } + break; + case "override": + var corpses = rootElement.GetChildElement("corpses"); + if (corpses != null) + { + foreach (var element in corpses.Elements()) + { + new CorpsePrefab(element, file.Path, true) + { + ContentPackage = file.ContentPackage, + }; + } + } + foreach (var element in rootElement.GetChildElements("corpse")) + { + new CorpsePrefab(element, file.Path, true) + { + ContentPackage = file.ContentPackage + }; + } + break; + default: + DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name.ToString()}' in {file.Path}"); + break; + } + } + + public static void RemoveByFile(string filePath) + { + Prefabs.RemoveByFile(filePath); + } + + public void GiveItems(Character character) + { + var spawnItems = ToolBox.SelectWeightedRandom(ItemSets.Keys.ToList(), ItemSets.Values.ToList(), Rand.RandSync.Unsynced); + foreach (XElement itemElement in spawnItems.GetChildElements("item")) + { + InitializeItems(character, itemElement); + } + } + + private void InitializeItems(Character character, XElement itemElement, Item parentItem = null) + { + ItemPrefab itemPrefab; + string itemIdentifier = itemElement.GetAttributeString("identifier", ""); + itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; + if (itemPrefab == null) + { + DebugConsole.ThrowError("Tried to spawn \"" + Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found."); + return; + } + Item item = new Item(itemPrefab, character.Position, null); +#if SERVER + if (GameMain.Server != null && Entity.Spawner != null) + { + if (GameMain.Server.EntityEventManager.UniqueEvents.Any(ev => ev.Entity == item)) + { + string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item); + GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item); + } + + Entity.Spawner.CreateNetworkEvent(item, false); + } +#endif + if (itemElement.GetAttributeBool("equip", false)) + { + List allowedSlots = new List(item.AllowedSlots); + allowedSlots.Remove(InvSlotType.Any); + + character.Inventory.TryPutItem(item, null, allowedSlots); + } + else + { + character.Inventory.TryPutItem(item, null, item.AllowedSlots); + } + if (item.Prefab.Identifier == "idcard") + { + item.AddTag("name:" + character.Name); + var job = character.Info?.Job; + if (job != null) + { + item.AddTag("job:" + job.Name); + } + } + foreach (WifiComponent wifiComponent in item.GetComponents()) + { + wifiComponent.TeamID = character.TeamID; + } + if (parentItem != null) + { + parentItem.Combine(item, user: null); + } + foreach (XElement childItemElement in itemElement.Elements()) + { + InitializeItems(character, childItemElement, item); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs index 50f3b9554..08feb0f1c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs @@ -14,7 +14,7 @@ namespace Barotrauma partial class FireSource : ISpatialEntity { const float OxygenConsumption = 50.0f; - const float GrowSpeed = 5.0f; + const float GrowSpeed = 20.0f; protected Hull hull; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 454068b5e..73f7a993a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -128,8 +128,9 @@ namespace Barotrauma outsideCollisionBlocker.CollisionCategories = Physics.CollisionWall; outsideCollisionBlocker.CollidesWith = Physics.CollisionCharacter; outsideCollisionBlocker.Enabled = false; +#if CLIENT Resized += newRect => IsHorizontal = newRect.Width < newRect.Height; - +#endif DebugConsole.Log("Created gap (" + ID + ")"); } @@ -226,8 +227,6 @@ namespace Barotrauma if (hulls[i] == null) hulls[i] = Hull.FindHullOld(searchPos[i], null, false, true); } - if (hulls[1] == hulls[0]) { hulls[1] = null; } - if (hulls[0] == null && hulls[1] == null) { return; } if (hulls[0] == null && hulls[1] != null) @@ -241,7 +240,7 @@ namespace Barotrauma for (int i = 0; i < 2; i++) { - if (hulls[i] == null) continue; + if (hulls[i] == null) { continue; } linkedTo.Add(hulls[i]); if (!hulls[i].ConnectedGaps.Contains(this)) hulls[i].ConnectedGaps.Add(this); } @@ -259,17 +258,21 @@ namespace Barotrauma return; } - UpdateOxygen(); + Hull hull1 = (Hull)linkedTo[0]; + Hull hull2 = linkedTo.Count < 2 ? null : (Hull)linkedTo[1]; + if (hull1 == hull2) { return; } + + UpdateOxygen(hull1, hull2); if (linkedTo.Count == 1) { //gap leading from a room to outside - UpdateRoomToOut(deltaTime); + UpdateRoomToOut(deltaTime, hull1); } - else + else if (linkedTo.Count == 2) { //gap leading from a room to another - UpdateRoomToRoom(deltaTime); + UpdateRoomToRoom(deltaTime, hull1, hull2); } flowForce.X = MathHelper.Clamp(flowForce.X, -MaxFlowForce, MaxFlowForce); @@ -329,12 +332,8 @@ namespace Barotrauma partial void EmitParticles(float deltaTime); - void UpdateRoomToRoom(float deltaTime) + void UpdateRoomToRoom(float deltaTime, Hull hull1, Hull hull2) { - if (linkedTo.Count < 2) return; - Hull hull1 = (Hull)linkedTo[0]; - Hull hull2 = (Hull)linkedTo[1]; - Vector2 subOffset = Vector2.Zero; if (hull1.Submarine != Submarine) { @@ -378,7 +377,7 @@ namespace Barotrauma delta = Math.Min(((hull2.Pressure + subOffset.Y) - hull1.Pressure) * 5.0f * sizeModifier, Math.Min(hull2.WaterVolume, hull2.Volume)); //make sure not to place more water to the target room than it can hold - delta = Math.Min(delta, hull1.Volume + Hull.MaxCompress - (hull1.WaterVolume)); + delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - (hull1.WaterVolume)); hull1.WaterVolume += delta; hull2.WaterVolume -= delta; if (hull1.WaterVolume > hull1.Volume) @@ -399,7 +398,7 @@ namespace Barotrauma delta = Math.Min((hull1.Pressure - (hull2.Pressure + subOffset.Y)) * 5.0f * sizeModifier, Math.Min(hull1.WaterVolume, hull1.Volume)); //make sure not to place more water to the target room than it can hold - delta = Math.Min(delta, hull2.Volume + Hull.MaxCompress - (hull2.WaterVolume)); + delta = Math.Min(delta, hull2.Volume * Hull.MaxCompress - (hull2.WaterVolume)); hull1.WaterVolume -= delta; hull2.WaterVolume += delta; if (hull2.WaterVolume > hull2.Volume) @@ -414,14 +413,14 @@ namespace Barotrauma { float avg = (hull1.Surface + hull2.Surface) / 2.0f; - if (hull1.WaterVolume < hull1.Volume - Hull.MaxCompress && + if (hull1.WaterVolume < hull1.Volume / Hull.MaxCompress && hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1] < rect.Y) { hull1.WaveVel[hull1.WaveY.Length - 1] = (avg - (hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1])) * 0.1f; hull1.WaveVel[hull1.WaveY.Length - 2] = hull1.WaveVel[hull1.WaveY.Length - 1]; } - if (hull2.WaterVolume < hull2.Volume - Hull.MaxCompress && + if (hull2.WaterVolume < hull2.Volume / Hull.MaxCompress && hull2.Surface + hull2.WaveY[0] < rect.Y) { hull2.WaveVel[0] = (avg - (hull2.Surface + hull2.WaveY[0])) * 0.1f; @@ -436,12 +435,12 @@ namespace Barotrauma //lower room is full of water if (hull2.Pressure + subOffset.Y > hull1.Pressure && hull2.WaterVolume > 0.0f) { - float delta = Math.Min(hull2.WaterVolume - hull2.Volume + Hull.MaxCompress, deltaTime * 8000.0f * sizeModifier); + float delta = Math.Min(hull2.WaterVolume - hull2.Volume + (hull2.Volume * Hull.MaxCompress), deltaTime * 8000.0f * sizeModifier); //make sure not to place more water to the target room than it can hold - if (hull1.WaterVolume + delta > hull1.Volume + Hull.MaxCompress) + if (hull1.WaterVolume + delta > hull1.Volume * Hull.MaxCompress) { - delta -= (hull1.WaterVolume + delta) - (hull1.Volume + Hull.MaxCompress); + delta -= (hull1.WaterVolume + delta) - (hull1.Volume * Hull.MaxCompress); } delta = Math.Max(delta, 0.0f); @@ -469,9 +468,9 @@ namespace Barotrauma float delta = Math.Min(hull1.WaterVolume, deltaTime * 25000f * sizeModifier); //make sure not to place more water to the target room than it can hold - if (hull2.WaterVolume + delta > hull2.Volume + Hull.MaxCompress) + if (hull2.WaterVolume + delta > hull2.Volume * Hull.MaxCompress) { - delta -= (hull2.WaterVolume + delta) - (hull2.Volume + Hull.MaxCompress); + delta -= (hull2.WaterVolume + delta) - (hull2.Volume * Hull.MaxCompress); } hull1.WaterVolume -= delta; hull2.WaterVolume += delta; @@ -489,7 +488,7 @@ namespace Barotrauma if (open > 0.0f) { - if (hull1.WaterVolume > hull1.Volume - Hull.MaxCompress && hull2.WaterVolume > hull2.Volume - Hull.MaxCompress) + if (hull1.WaterVolume > hull1.Volume / Hull.MaxCompress && hull2.WaterVolume > hull2.Volume / Hull.MaxCompress) { float avgLethality = (hull1.LethalPressure + hull2.LethalPressure) / 2.0f; hull1.LethalPressure = avgLethality; @@ -503,22 +502,18 @@ namespace Barotrauma } } - void UpdateRoomToOut(float deltaTime) + void UpdateRoomToOut(float deltaTime, Hull hull1) { - if (linkedTo.Count != 1) return; - - float size = (IsHorizontal) ? rect.Height : rect.Width; - - Hull hull1 = (Hull)linkedTo[0]; + float size = IsHorizontal ? rect.Height : rect.Width; //a variable affecting the water flow through the gap //the larger the gap is, the faster the water flows float sizeModifier = size * open * open; - float delta = Hull.MaxCompress * sizeModifier * deltaTime; + float delta = hull1.Volume * Hull.MaxCompress * sizeModifier * deltaTime; //make sure not to place more water to the target room than it can hold - delta = Math.Min(delta, hull1.Volume + Hull.MaxCompress - hull1.WaterVolume); + delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - hull1.WaterVolume); hull1.WaterVolume += delta; if (hull1.WaterVolume > hull1.Volume) hull1.Pressure += 0.5f; @@ -541,7 +536,7 @@ namespace Barotrauma higherSurface = hull1.Surface; lowerSurface = rect.Y; - if (hull1.WaterVolume < hull1.Volume - Hull.MaxCompress && + if (hull1.WaterVolume < hull1.Volume / Hull.MaxCompress && hull1.Surface < rect.Y) { if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f) @@ -576,7 +571,7 @@ namespace Barotrauma { flowForce = new Vector2(0.0f, delta); } - if (hull1.WaterVolume >= hull1.Volume - Hull.MaxCompress) + if (hull1.WaterVolume >= hull1.Volume / Hull.MaxCompress) { hull1.LethalPressure += (Submarine != null && Submarine.AtDamageDepth) ? 100.0f * deltaTime : 10.0f * deltaTime; } @@ -615,6 +610,19 @@ namespace Barotrauma Vector2 rayStart = ConvertUnits.ToSimUnits(WorldPosition); Vector2 rayEnd = rayStart + rayDir * 500.0f; + var levelCells = Level.Loaded.GetCells(WorldPosition, searchDepth: 1); + foreach (var cell in levelCells) + { + if (cell.IsPointInside(WorldPosition)) + { + outsideCollisionBlocker.Enabled = true; + Vector2 colliderPos = rayStart - Submarine.SimPosition; + float colliderRotation = MathUtils.VectorToAngle(rayDir) - MathHelper.PiOver2; + outsideCollisionBlocker.SetTransformIgnoreContacts(ref colliderPos, colliderRotation); + return; + } + } + var blockingBody = Submarine.CheckVisibility(rayStart, rayEnd); if (blockingBody != null) { @@ -631,11 +639,9 @@ namespace Barotrauma } } - private void UpdateOxygen() + private void UpdateOxygen(Hull hull1, Hull hull2) { - if (linkedTo.Count < 2) { return; } - Hull hull1 = (Hull)linkedTo[0]; - Hull hull2 = (Hull)linkedTo[1]; + if (hull1 == null || hull2 == null) { return; } if (IsHorizontal) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 06ff02e92..3de38ad57 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -26,8 +26,9 @@ namespace Barotrauma public static float WaveSpread = 0.05f; public static float WaveDampening = 0.05f; - //how much excess water the room can contain (= more than the volume of the room) - public const float MaxCompress = 10000f; + //how much excess water the room can contain, relative to the volume of the room. + //needed to make it possible for pressure to "push" water up through U-shaped hull configurations + public const float MaxCompress = 1.05f; public readonly Dictionary properties; public Dictionary SerializableProperties @@ -154,7 +155,7 @@ namespace Barotrauma set { if (!MathUtils.IsValid(value)) return; - waterVolume = MathHelper.Clamp(value, 0.0f, Volume + MaxCompress); + waterVolume = MathHelper.Clamp(value, 0.0f, Volume * MaxCompress); if (waterVolume < Volume) Pressure = rect.Y - rect.Height + waterVolume / rect.Width; if (waterVolume > 0.0f) update = true; } @@ -228,7 +229,7 @@ namespace Barotrauma surface = rect.Y - rect.Height; - if (submarine != null) + if (submarine?.Info != null && !submarine.Info.IsWreck) { aiTarget = new AITarget(this) { @@ -321,6 +322,7 @@ namespace Barotrauma CeilingHeight = ConvertUnits.ToDisplayUnits(upperPickedPos.Y - lowerPickedPos.Y); } } + Pressure = rect.Y - rect.Height + waterVolume / rect.Width; } public void AddToGrid(Submarine submarine) @@ -444,15 +446,23 @@ namespace Barotrauma lethalPressure = 0.0f; return; } + + float waterDepth = WaterVolume / rect.Width; + if (waterDepth < 1.0f) + { + //if there's only a minuscule amount of water, consider the surface to be at the bottom of the hull + //otherwise unnoticeable amounts of water can for example cause magnesium to explode + waterDepth = 0.0f; + } surface = Math.Max(MathHelper.Lerp( surface, - rect.Y - rect.Height + WaterVolume / rect.Width, + rect.Y - rect.Height + waterDepth, deltaTime * 10.0f), rect.Y - rect.Height); //interpolate the position of the rendered surface towards the "target surface" drawSurface = Math.Max(MathHelper.Lerp( drawSurface, - rect.Y - rect.Height + WaterVolume / rect.Width, + rect.Y - rect.Height + waterDepth, deltaTime * 10.0f), rect.Y - rect.Height); for (int i = 0; i < waveY.Length; i++) @@ -873,7 +883,7 @@ namespace Barotrauma var hull = new Hull(MapEntityPrefab.Find(null, "hull"), rect, submarine) { - waterVolume = element.GetAttributeFloat("pressure", 0.0f), + WaterVolume = element.GetAttributeFloat("pressure", 0.0f), ID = (ushort)int.Parse(element.Attribute("ID").Value) }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 412298f03..6b556debe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -27,18 +27,24 @@ namespace Barotrauma [Flags] public enum PositionType { - MainPath = 1, Cave = 2, Ruin = 4 + MainPath = 1, Cave = 2, Ruin = 4, Wreck = 8 } public struct InterestingPosition { public Point Position; public readonly PositionType PositionType; + public bool IsValid; + public Submarine Submarine; + public Ruin Ruin; - public InterestingPosition(Point position, PositionType positionType) + public InterestingPosition(Point position, PositionType positionType, bool isValid = true, Submarine submarine = null, Ruin ruin = null) { Position = position; PositionType = positionType; + IsValid = isValid; + Submarine = submarine; + Ruin = ruin; } } @@ -68,6 +74,8 @@ namespace Barotrauma private List ruins; + private List wrecks; + private LevelGenerationParams generationParams; private List> smallTunnels = new List>(); @@ -133,11 +141,13 @@ namespace Barotrauma get { return positionsOfInterest; } } + public readonly List UsedPositions = new List(); + public Submarine StartOutpost { get; private set; } public Submarine EndOutpost { get; private set; } - private Submarine preSelectedStartOutpost; - private Submarine preSelectedEndOutpost; + private SubmarineInfo preSelectedStartOutpost; + private SubmarineInfo preSelectedEndOutpost; public string Seed { @@ -210,7 +220,7 @@ namespace Barotrauma /// /// A scalar between 0-100 /// A scalar between 0-1 (0 = the minimum width defined in the generation params is used, 1 = the max width is used) - public Level(string seed, float difficulty, float sizeFactor, LevelGenerationParams generationParams, Biome biome, Submarine startOutpost = null, Submarine endOutPost = null) + public Level(string seed, float difficulty, float sizeFactor, LevelGenerationParams generationParams, Biome biome, SubmarineInfo startOutpost = null, SubmarineInfo endOutPost = null) : base(null) { @@ -310,6 +320,7 @@ namespace Barotrauma if (Submarine.MainSub != null) { Rectangle dockedSubBorders = Submarine.MainSub.GetDockedBorders(); + dockedSubBorders.Inflate(dockedSubBorders.Size.ToVector2() * 0.05f); minWidth = Math.Max(minWidth, Math.Max(dockedSubBorders.Width, dockedSubBorders.Height)); minWidth = Math.Min(minWidth, maxWidth); } @@ -668,7 +679,6 @@ namespace Barotrauma renderer.SetWallVertices(CaveGenerator.GenerateWallShapes(cellsWithBody, this), generationParams.WallColor); #endif - //---------------------------------------------------------------------------------- // create (placeholder) outposts at the start and end of the level //---------------------------------------------------------------------------------- @@ -691,6 +701,12 @@ namespace Barotrauma GenerateSeaFloor(mirror); + //---------------------------------------------------------------------------------- + // create wrecks + //---------------------------------------------------------------------------------- + + CreateWrecks(); + levelObjectManager.PlaceObjects(this, generationParams.LevelObjectAmount); GenerateItems(); @@ -1163,7 +1179,7 @@ namespace Barotrauma return; } string errorMsg = "Failed to find a suitable position for ruins. Level seed: " + seed + - ", ruin size: " + ruinSize + ", selected sub " + (Submarine.MainSub == null ? "none" : Submarine.MainSub.Name); + ", ruin size: " + ruinSize + ", selected sub " + (Submarine.MainSub == null ? "none" : Submarine.MainSub.Info.Name); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("Level.GenerateRuins:PosNotFound", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); break; @@ -1200,13 +1216,15 @@ namespace Barotrauma ruins.Add(ruin); ruin.RuinShapes.Sort((shape1, shape2) => shape2.DistanceFromEntrance.CompareTo(shape1.DistanceFromEntrance)); + // TODO: autogenerate waypoints inside the ruins and connect them to the main path in multiple places. + // We need the waypoints for the AI navigation and we could use them for spawning the creatures too. int waypointCount = 0; foreach (WayPoint wp in WayPoint.WayPointList) { if (wp.SpawnType != SpawnType.Enemy || wp.Submarine != null) { continue; } if (ruin.RuinShapes.Any(rs => rs.Rect.Contains(wp.WorldPosition))) { - positionsOfInterest.Add(new InterestingPosition(new Point((int)wp.WorldPosition.X, (int)wp.WorldPosition.Y), PositionType.Ruin)); + positionsOfInterest.Add(new InterestingPosition(new Point((int)wp.WorldPosition.X, (int)wp.WorldPosition.Y), PositionType.Ruin, ruin: ruin)); waypointCount++; } } @@ -1214,7 +1232,7 @@ namespace Barotrauma //not enough waypoints inside ruins -> create some spawn positions manually for (int i = 0; i < 4 - waypointCount && i < ruin.RuinShapes.Count; i++) { - positionsOfInterest.Add(new InterestingPosition(ruin.RuinShapes[i].Rect.Center, PositionType.Ruin)); + positionsOfInterest.Add(new InterestingPosition(ruin.RuinShapes[i].Rect.Center, PositionType.Ruin, ruin: ruin)); } foreach (RuinShape ruinShape in ruin.RuinShapes) @@ -1327,8 +1345,6 @@ namespace Barotrauma Vector2 position = Vector2.Zero; - offsetFromWall = ConvertUnits.ToSimUnits(offsetFromWall); - int tries = 0; do { @@ -1397,7 +1413,7 @@ namespace Barotrauma { foreach (Submarine sub in Submarine.Loaded) { - if (sub.IsOutpost) { continue; } + if (sub.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } farEnoughPositions.RemoveAll(p => Vector2.DistanceSquared(p.Position.ToVector2(), sub.WorldPosition) < minDistFromSubs * minDistFromSubs); } } @@ -1483,8 +1499,10 @@ namespace Barotrauma return cells; } + private readonly List tempCells = new List(); public List GetCells(Vector2 worldPos, int searchDepth = 2) { + tempCells.Clear(); int gridPosX = (int)Math.Floor(worldPos.X / GridCellSize); int gridPosY = (int)Math.Floor(worldPos.Y / GridCellSize); @@ -1494,12 +1512,11 @@ namespace Barotrauma int startY = Math.Max(gridPosY - searchDepth, 0); int endY = Math.Min(gridPosY + searchDepth, cellGrid.GetLength(1) - 1); - List cells = new List(); for (int y = startY; y <= endY; y++) { for (int x = startX; x <= endX; x++) { - cells.AddRange(cellGrid[x, y]); + tempCells.AddRange(cellGrid[x, y]); } } @@ -1507,11 +1524,310 @@ namespace Barotrauma { foreach (VoronoiCell cell in wall.Cells) { - cells.Add(cell); + tempCells.Add(cell); } } - return cells; + return tempCells; + } + + // For debugging + private readonly Dictionary> wreckPositions = new Dictionary>(); + private readonly Dictionary> blockedRects = new Dictionary>(); + private void CreateWrecks() + { + var totalSW = new Stopwatch(); + var tempSW = new Stopwatch(); + totalSW.Start(); + var wreckFiles = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Wreck).ToList(); + if (wreckFiles.None()) + { + DebugConsole.ThrowError("No wreck files found in the selected content packages!"); + return; + } + wreckFiles.Shuffle(Rand.RandSync.Server); + int wreckCount = Math.Min(Loaded.GenerationParams.WreckCount, wreckFiles.Count); + // Min distance between a wreck and the start/end/other wreck. + float minDistance = Sonar.DefaultSonarRange; + float squaredMinDistance = minDistance * minDistance; + Vector2 start = startPosition.ToVector2(); + Vector2 end = endPosition.ToVector2(); + var waypoints = WayPoint.WayPointList.Where(wp => + wp.Submarine == null && + wp.SpawnType == SpawnType.Path && + Vector2.DistanceSquared(wp.WorldPosition, start) > squaredMinDistance && + Vector2.DistanceSquared(wp.WorldPosition, end) > squaredMinDistance).ToList(); + wrecks = new List(wreckCount); + for (int i = 0; i < wreckCount; i++) + { + ContentFile contentFile = wreckFiles[i]; + if (contentFile == null) { continue; } + var subDoc = SubmarineInfo.OpenFile(contentFile.Path); + Rectangle borders = Submarine.GetBorders(subDoc.Root); + string wreckName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path); + // Add some vertical margin so that the wreck doesn't block the path entirely. It's still possible that some larger subs can't pass by. + Point paddedDimensions = new Point(borders.Width, borders.Height + 3000); + tempSW.Restart(); + // For storing the translations. Used only for debugging. + var positions = new List(); + var rects = new List(); + int maxAttempts = 50; + int attemptsLeft = maxAttempts; + bool success = false; + Vector2 spawnPoint = Vector2.Zero; + while (attemptsLeft > 0) + { + if (attemptsLeft < maxAttempts) + { + Debug.WriteLine($"Failed to position the wreck {wreckName}. Trying again."); + } + attemptsLeft--; + if (TryGetSpawnPoint(out spawnPoint)) + { + success = TryPositionWreck(borders, wreckName, ref spawnPoint); + if (success) + { + break; + } + else + { + positions.Clear(); + } + } + else + { + DebugConsole.NewMessage($"Failed to find any spawn point for the wreck: {wreckName} (No valid waypoints left).", Color.Red); + break; + } + } + tempSW.Stop(); + if (success) + { + Debug.WriteLine($"Wreck {wreckName} successfully positioned to {spawnPoint} in {tempSW.ElapsedMilliseconds.ToString()} (ms)"); + tempSW.Restart(); + SubmarineInfo info = new SubmarineInfo(contentFile.Path) + { + Type = SubmarineInfo.SubmarineType.Wreck + }; + Submarine wreck = new Submarine(info); + //wreck.Load(unloadPrevious: false); + wreck.MakeWreck(); + tempSW.Stop(); + Debug.WriteLine($"Wreck {wreck.Info.Name} loaded in { tempSW.ElapsedMilliseconds.ToString()} (ms)"); + wrecks.Add(wreck); + wreck.SetPosition(spawnPoint); + wreckPositions.Add(wreck, positions); + blockedRects.Add(wreck, rects); + positionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.Wreck, submarine: wreck)); + foreach (Hull hull in wreck.GetHulls(false)) + { + if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.WreckHullFloodingChance) + { + hull.WaterVolume = hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.Server); + } + } + if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.ThalamusProbability) + { + if (!wreck.CreateThalamus()) + { + DebugConsole.NewMessage($"Failed to create thalamus inside {wreckName}.", Color.Red); + wreck.DisableThalamus(); + } + } + else + { + wreck.DisableThalamus(); + } + } + else + { + DebugConsole.NewMessage($"Failed to position wreck {wreckName}. Used {tempSW.ElapsedMilliseconds.ToString()} (ms).", Color.Red); + } + + bool TryPositionWreck(Rectangle borders, string wreckName, ref Vector2 spawnPoint) + { + positions.Add(spawnPoint); + bool bottomFound = TryRaycastToBottom(borders, ref spawnPoint); + positions.Add(spawnPoint); + + bool leftSideBlocked = IsSideBlocked(borders, false); + bool rightSideBlocked = IsSideBlocked(borders, true); + int step = 5; + if (rightSideBlocked && !leftSideBlocked) + { + bottomFound = TryMove(borders, ref spawnPoint, -step); + } + else if (leftSideBlocked && !rightSideBlocked) + { + bottomFound = TryMove(borders, ref spawnPoint, step); + } + else if (!bottomFound) + { + if (!leftSideBlocked) + { + bottomFound = TryMove(borders, ref spawnPoint, -step); + } + else if (!rightSideBlocked) + { + bottomFound = TryMove(borders, ref spawnPoint, step); + } + else + { + Debug.WriteLine($"Invalid position {spawnPoint}. Does not touch the ground."); + return false; + } + } + positions.Add(spawnPoint); + bool isBlocked = IsBlocked(spawnPoint, borders.Size - new Point(step + 50)); + if (isBlocked) + { + rects.Add(ToolBox.GetWorldBounds(spawnPoint.ToPoint(), borders.Size)); + Debug.WriteLine($"Invalid position {spawnPoint}. Blocked by level walls."); + } + else if (!bottomFound) + { + Debug.WriteLine($"Invalid position {spawnPoint}. Does not touch the ground."); + } + else + { + var sp = spawnPoint; + if (wrecks.Any(w => Vector2.DistanceSquared(w.WorldPosition, sp) < squaredMinDistance)) + { + Debug.WriteLine($"Invalid position {spawnPoint}. Too close to other wreck(s)."); + return false; + } + } + return !isBlocked && bottomFound; + + bool TryMove(Rectangle borders, ref Vector2 spawnPoint, float amount) + { + float maxMovement = 5000; + float totalAmount = 0; + bool foundBottom = TryRaycastToBottom(borders, ref spawnPoint); + while (!IsSideBlocked(borders, amount > 0)) + { + foundBottom = TryRaycastToBottom(borders, ref spawnPoint); + totalAmount += amount; + spawnPoint = new Vector2(spawnPoint.X + amount, spawnPoint.Y); + if (Math.Abs(totalAmount) > maxMovement) + { + Debug.WriteLine($"Moving the wreck {wreckName} failed."); + break; + } + } + return foundBottom; + } + } + + bool TryGetSpawnPoint(out Vector2 spawnPoint) + { + spawnPoint = Vector2.Zero; + while (waypoints.Any()) + { + var wp = waypoints.GetRandom(Rand.RandSync.Server); + waypoints.Remove(wp); + if (!IsBlocked(wp.WorldPosition, paddedDimensions)) + { + spawnPoint = wp.WorldPosition; + return true; + } + } + return false; + } + + static bool TryRaycastToBottom(Rectangle borders, ref Vector2 spawnPoint) + { + // Shoot five rays and pick the highest hit point. + int rayCount = 5; + var positions = new Vector2[rayCount]; + bool hit = false; + for (int i = 0; i < rayCount; i++) + { + float quarterWidth = borders.Width * 0.25f; + Vector2 rayStart = spawnPoint; + switch (i) + { + case 1: + rayStart = new Vector2(spawnPoint.X - quarterWidth, spawnPoint.Y); + break; + case 2: + rayStart = new Vector2(spawnPoint.X + quarterWidth, spawnPoint.Y); + break; + case 3: + rayStart = new Vector2(spawnPoint.X - quarterWidth / 2, spawnPoint.Y); + break; + case 4: + rayStart = new Vector2(spawnPoint.X + quarterWidth / 2, spawnPoint.Y); + break; + } + var simPos = ConvertUnits.ToSimUnits(rayStart); + var body = Submarine.PickBody(simPos, new Vector2(simPos.X, -1), + customPredicate: f => f.Body?.UserData is VoronoiCell cell && cell.Body.BodyType == BodyType.Static, + collisionCategory: Physics.CollisionLevel | Physics.CollisionWall); + if (body != null) + { + positions[i] = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + new Vector2(0, borders.Height / 2); + hit = true; + } + } + float highestPoint = positions.Max(p => p.Y); + spawnPoint = new Vector2(spawnPoint.X, highestPoint); + return hit; + } + + bool IsSideBlocked(Rectangle borders, bool front) + { + // Shoot three rays and check whether any of them hits. + int rayCount = 3; + Vector2 halfSize = borders.Size.ToVector2() / 2; + Vector2 quarterSize = halfSize / 2; + var positions = new Vector2[rayCount]; + for (int i = 0; i < rayCount; i++) + { + float dir = front ? 1 : -1; + Vector2 rayStart; + Vector2 to; + switch (i) + { + case 1: + rayStart = new Vector2(spawnPoint.X + halfSize.X * dir, spawnPoint.Y + quarterSize.Y); + to = new Vector2(spawnPoint.X + (halfSize.X - quarterSize.X) * dir, rayStart.Y); + break; + case 2: + rayStart = new Vector2(spawnPoint.X + halfSize.X * dir, spawnPoint.Y - quarterSize.Y); + to = new Vector2(spawnPoint.X + (halfSize.X - quarterSize.X) * dir, rayStart.Y); + break; + case 0: + default: + rayStart = spawnPoint; + to = new Vector2(spawnPoint.X + halfSize.X * dir, rayStart.Y); + break; + } + Vector2 simPos = ConvertUnits.ToSimUnits(rayStart); + if (Submarine.PickBody(simPos, ConvertUnits.ToSimUnits(to), + customPredicate: f => f.Body?.UserData is VoronoiCell cell, + collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null) + { + return true; + } + } + return false; + } + + bool IsBlocked(Vector2 pos, Point size, float maxDistanceMultiplier = 1) + { + float maxDistance = size.Multiply(maxDistanceMultiplier).ToVector2().LengthSquared(); + Rectangle bounds = ToolBox.GetWorldBounds(pos.ToPoint(), size); + if (ruins.Any(r => ToolBox.GetWorldBounds(r.Area.Center, r.Area.Size).IntersectsWorld(bounds))) + { + return true; + } + var cells = Loaded.GetAllCells().Where(c => c.Body != null && Vector2.DistanceSquared(pos, c.Center) <= maxDistance); + return cells.Any(c => c.BodyVertices.Any(v => bounds.ContainsWorld(v))); + } + } + totalSW.Stop(); + Debug.WriteLine($"{wrecks.Count} wrecks created in { totalSW.ElapsedMilliseconds.ToString()} (ms)"); } private void CreateOutposts() @@ -1536,20 +1852,20 @@ namespace Barotrauma continue; } - Submarine outpost = null; - + SubmarineInfo outpostInfo = null; if (i == 0 && preSelectedStartOutpost == null || i == 1 && preSelectedEndOutpost == null) { string outpostFile = outpostFiles.GetRandom(Rand.RandSync.Server).Path; - outpost = new Submarine(outpostFile, tryLoad: false); + outpostInfo = new SubmarineInfo(outpostFile); } else { - outpost = (i == 0) ? preSelectedStartOutpost : preSelectedEndOutpost; + outpostInfo = (i == 0) ? preSelectedStartOutpost : preSelectedEndOutpost; } - outpost.Load(unloadPrevious: false); - outpost.MakeOutpost(); + outpostInfo.Type = SubmarineInfo.SubmarineType.Outpost; + + var outpost = new Submarine(outpostInfo); Point? minSize = null; DockingPort subPort = null; @@ -1596,9 +1912,9 @@ namespace Barotrauma if (Math.Abs(subDockingPortOffset) > 5000.0f) { subDockingPortOffset = MathHelper.Clamp(subDockingPortOffset, -5000.0f, 5000.0f); - string warningMsg = "Docking port very far from the sub's center of mass (submarine: " + Submarine.MainSub.Name + ", dist: " + subDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; + string warningMsg = "Docking port very far from the sub's center of mass (submarine: " + Submarine.MainSub.Info.Name + ", dist: " + subDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; DebugConsole.NewMessage(warningMsg, Color.Orange); - GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:DockingPortVeryFar" + Submarine.MainSub.Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, warningMsg); + GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:DockingPortVeryFar" + Submarine.MainSub.Info.Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, warningMsg); } float outpostDockingPortOffset = subPort == null ? 0.0f : outpostPort.Item.WorldPosition.X - outpost.WorldPosition.X; @@ -1606,23 +1922,23 @@ namespace Barotrauma if (Math.Abs(outpostDockingPortOffset) > 5000.0f) { outpostDockingPortOffset = MathHelper.Clamp(outpostDockingPortOffset, -5000.0f, 5000.0f); - string warningMsg = "Docking port very far from the outpost's center of mass (outpost: " + outpost.Name + ", dist: " + outpostDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; + string warningMsg = "Docking port very far from the outpost's center of mass (outpost: " + outpost.Info.Name + ", dist: " + outpostDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; DebugConsole.NewMessage(warningMsg, Color.Orange); - GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:OutpostDockingPortVeryFar" + outpost.Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, warningMsg); + GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:OutpostDockingPortVeryFar" + outpost.Info.Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, warningMsg); } outpost.SetPosition(outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize, subDockingPortOffset - outpostDockingPortOffset)); if ((i == 0) == !Mirrored) { StartOutpost = outpost; - if (GameMain.GameSession?.StartLocation != null) { outpost.Name = GameMain.GameSession.StartLocation.Name; } + if (GameMain.GameSession?.StartLocation != null) { outpost.Info.Name = GameMain.GameSession.StartLocation.Name; } } else { EndOutpost = outpost; - if (GameMain.GameSession?.EndLocation != null) { outpost.Name = GameMain.GameSession.EndLocation.Name; } + if (GameMain.GameSession?.EndLocation != null) { outpost.Info.Name = GameMain.GameSession.EndLocation.Name; } } - } + } } private bool IsModeStartOutpostCompatible() @@ -1634,6 +1950,90 @@ namespace Barotrauma #endif } + public void SpawnCorpses() + { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + + foreach (Submarine wreck in wrecks) + { + int corpseCount = Rand.Range(Loaded.GenerationParams.MinCorpseCount, Loaded.GenerationParams.MaxCorpseCount); + var allSpawnPoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == wreck && wp.CurrentHull != null); + var pathPoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Path); + var corpsePoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Corpse); + var spawnPoints = corpsePoints.Union(pathPoints).ToList(); + spawnPoints.Shuffle(Rand.RandSync.Unsynced); + int spawnCounter = 0; + for (int j = 0; j < corpseCount; j++) + { + WayPoint sp = spawnPoints.FirstOrDefault(); + JobPrefab job = sp?.AssignedJob; + CorpsePrefab selectedPrefab; + if (job == null) + { + // Deduce the job from the selected prefab + selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck); + job = GetJobPrefab(); + } + else + { + selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck && (p.Job == "any" || p.Job == job.Identifier)); + if (selectedPrefab == null) + { + spawnPoints.Remove(sp); + sp = spawnPoints.FirstOrDefault(sp => sp.AssignedJob == null); + // Deduce the job from the selected prefab + selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck); + job = GetJobPrefab(); + } + } + if (selectedPrefab == null) { continue; } + Vector2 pos; + if (sp == null) + { + if (!TryGetExtraSpawnPoint(out pos)) + { + break; + } + job = GetJobPrefab(); + } + else + { + pos = sp.WorldPosition; + spawnPoints.Remove(sp); + } + if (job == null) { continue; } + var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job); + var corpse = Character.Create(CharacterPrefab.HumanConfigFile, pos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); + corpse.TeamID = Character.TeamType.None; + corpse.EnableDespawn = false; + selectedPrefab.GiveItems(corpse); + corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false); + spawnCounter++; + + static CorpsePrefab GetCorpsePrefab(Func predicate) + { + IEnumerable filteredPrefabs = CorpsePrefab.Prefabs.Where(predicate); + return ToolBox.SelectWeightedRandom(filteredPrefabs.ToList(), filteredPrefabs.Select(p => p.Commonness).ToList(), Rand.RandSync.Unsynced); + } + + JobPrefab GetJobPrefab() => selectedPrefab.Job != null && selectedPrefab.Job != "any" ? JobPrefab.Get(selectedPrefab.Job) : JobPrefab.Random(); + } + + DebugConsole.NewMessage($"{spawnCounter}/{corpseCount} corpses spawned in {wreck.Info.Name}.", spawnCounter == corpseCount ? Color.Green : Color.Yellow); + + bool TryGetExtraSpawnPoint(out Vector2 point) + { + point = Vector2.Zero; + var hull = Hull.hullList.FindAll(h => h.Submarine == wreck).GetRandom(); + if (hull != null) + { + point = hull.WorldPosition; + } + return hull != null; + } + } + } + public override void Remove() { base.Remove(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 463db19dc..64fd8ffc9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -9,7 +9,6 @@ namespace Barotrauma { class Biome { - public readonly string Identifier; public readonly string DisplayName; public readonly string Description; @@ -105,8 +104,6 @@ namespace Barotrauma private int mountainHeightMin, mountainHeightMax; - private int ruinCount; - private float waterParticleScale; //which biomes can this type of level appear in @@ -338,12 +335,33 @@ namespace Barotrauma } } - [Serialize(1, true, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 50)] - public int RuinCount - { - get { return ruinCount; } - set { ruinCount = MathHelper.Clamp(value, 0, 10); } - } + [Serialize(1, true, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)] + public int RuinCount { get; set; } + + [Serialize(1, true, description: "The maximum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)] + public int WreckCount { get; set; } + + // TODO: Move the wreck parameters under a separate class? +#region Wreck parameters + [Serialize(1, true, description: "The minimum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)] + public int MinCorpseCount { get; set; } + + [Serialize(5, true, description: "The maximum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)] + public int MaxCorpseCount { get; set; } + + // TODO: default to 0 + [Serialize(1f, true, description: "How likely is it that a Thalamus inhabits a wreck. Percentage from 0 to 1 per wreck."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + public float ThalamusProbability { get; set; } + + [Serialize(0.5f, true, description: "How likely the water level of a hull inside a wreck is randomly set."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + public float WreckHullFloodingChance { get; set; } + + [Serialize(0.1f, true, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + public float WreckFloodingHullMinWaterPercentage { get; set; } + + [Serialize(1.0f, true, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + public float WreckFloodingHullMaxWaterPercentage { get; set; } +#endregion [Serialize(0.4f, true, description: "The probability for wall cells to be removed from the bottom of the map. A value of 0 will produce a completely enclosed tunnel and 1 will make the entire bottom of the level completely open."), Editable()] public float BottomHoleProbability @@ -415,10 +433,10 @@ namespace Barotrauma string biomeName = biomeNames[i].Trim().ToLowerInvariant(); if (biomeName == "none") { continue; } - Biome matchingBiome = biomes.Find(b => b.Identifier.ToLowerInvariant() == biomeName); + Biome matchingBiome = biomes.Find(b => b.Identifier.Equals(biomeName, StringComparison.OrdinalIgnoreCase)); if (matchingBiome == null) { - matchingBiome = biomes.Find(b => b.DisplayName.ToLowerInvariant() == biomeName); + matchingBiome = biomes.Find(b => b.DisplayName.Equals(biomeName, StringComparison.OrdinalIgnoreCase)); if (matchingBiome == null) { DebugConsole.ThrowError("Error in level generation parameters: biome \"" + biomeName + "\" not found."); @@ -487,7 +505,7 @@ namespace Barotrauma mainElement = doc.Root.FirstElement(); biomeElements.Clear(); levelParamElements.Clear(); - DebugConsole.NewMessage($"Overriding the level generation parameters with '{file.Path}'", Color.Yellow); + DebugConsole.NewMessage($"Overriding the level generation parameters and biomes with '{file.Path}'", Color.Yellow); } else if (biomeElements.Any() || levelParamElements.Any()) { @@ -497,7 +515,22 @@ namespace Barotrauma foreach (XElement element in mainElement.Elements()) { - if (element.Name.ToString().ToLowerInvariant() == "biomes") + if (element.IsOverride()) + { + if (element.FirstElement().Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase)) + { + biomeElements.Clear(); + biomeElements.AddRange(element.FirstElement().Elements()); + DebugConsole.NewMessage($"Overriding biomes with '{file.Path}'", Color.Yellow); + } + else + { + levelParamElements.Clear(); + DebugConsole.NewMessage($"Overriding the level generation parameters with '{file.Path}'", Color.Yellow); + levelParamElements.AddRange(element.Elements()); + } + } + else if (element.Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase)) { biomeElements.AddRange(element.Elements()); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs index 623fed74a..5fdb2ebec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs @@ -79,11 +79,13 @@ namespace Barotrauma Vector2[] vertices = new Vector2[4]; vertices[0] = edgePositions[i]; vertices[1] = edgePositions[i + 1]; - vertices[2] = vertices[0] + extendAmount; - vertices[3] = vertices[1] + extendAmount; + vertices[2] = vertices[1] + extendAmount; + vertices[3] = vertices[0] + extendAmount; - VoronoiCell wallCell = new VoronoiCell(vertices); - wallCell.CellType = CellType.Edge; + VoronoiCell wallCell = new VoronoiCell(vertices) + { + CellType = CellType.Edge + }; wallCell.Edges[0].Cell1 = wallCell; wallCell.Edges[1].Cell1 = wallCell; wallCell.Edges[2].Cell1 = wallCell; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs index 14c5f9d6a..4eb5b637f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs @@ -277,7 +277,7 @@ namespace Barotrauma.RuinGeneration { foreach (XElement subElement in element2.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "chooseone") + if (subElement.Name.ToString().Equals("chooseone", StringComparison.OrdinalIgnoreCase)) { groupIndex++; LoadEntities(subElement, ref groupIndex); @@ -390,7 +390,7 @@ namespace Barotrauma.RuinGeneration SourceEntityIdentifier = element.GetAttributeString("sourceentity", ""); foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "wire") + if (subElement.Name.ToString().Equals("wire", StringComparison.OrdinalIgnoreCase)) { WireConnection = new Pair( subElement.GetAttributeString("from", ""), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs index 1d29696e7..2a96202f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs @@ -721,8 +721,10 @@ namespace Barotrauma.RuinGeneration //doors create their own gaps, don't create an additional one if there's a door at this bool doorFound = false; - foreach (Door door in doors) + foreach (Item item in Item.ItemList) { + var door = item.GetComponent(); + if (door == null) { continue; } if (Math.Abs(door.Item.WorldPosition.X - gapRect.Value.Center.X) < 5 && Math.Abs(door.Item.WorldPosition.Y - gapRect.Value.Center.Y) < 5) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 29606d4ae..ac570b18d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -30,7 +30,7 @@ namespace Barotrauma { System.Diagnostics.Debug.Assert(Submarine.MainSub != null); - LinkedSubmarine.CreateDummy(Submarine.MainSub, mainSub.FilePath, rect.Location.ToVector2()); + LinkedSubmarine.CreateDummy(Submarine.MainSub, mainSub.Info.FilePath, rect.Location.ToVector2()); } } @@ -92,7 +92,7 @@ namespace Barotrauma public static LinkedSubmarine CreateDummy(Submarine mainSub, string filePath, Vector2 position) { - XDocument doc = Submarine.OpenFile(filePath); + XDocument doc = SubmarineInfo.OpenFile(filePath); if (doc == null || doc.Root == null) return null; LinkedSubmarine sl = CreateDummy(mainSub, doc.Root, position); @@ -105,15 +105,20 @@ namespace Barotrauma { LinkedSubmarine sl = new LinkedSubmarine(mainSub); sl.GenerateWallVertices(element); + if (sl.wallVertices.Any()) + { + sl.Rect = new Rectangle( + (int)sl.wallVertices.Min(v => v.X + position.X), + (int)sl.wallVertices.Max(v => v.Y + position.Y), + (int)sl.wallVertices.Max(v => v.X + position.X), + (int)sl.wallVertices.Min(v => v.Y + position.Y)); - sl.Rect = new Rectangle( - (int)sl.wallVertices.Min(v => v.X + position.X), - (int)sl.wallVertices.Max(v => v.Y + position.Y), - (int)sl.wallVertices.Max(v => v.X + position.X), - (int)sl.wallVertices.Min(v => v.Y + position.Y)); - - sl.rect = new Rectangle((int)position.X, (int)position.Y, 1, 1); - + sl.Rect = new Rectangle(sl.rect.X, sl.rect.Y, sl.rect.Width - sl.rect.X, sl.rect.Y - sl.rect.Height); + } + else + { + sl.Rect = new Rectangle((int)position.X, (int)position.Y, 10, 10); + } return sl; } @@ -212,7 +217,8 @@ namespace Barotrauma { if (!loadSub) { return; } - sub = Submarine.Load(saveElement, false); + SubmarineInfo info = new SubmarineInfo(Submarine.Info.FilePath, "", saveElement); + sub = Submarine.Load(info, false); Vector2 worldPos = saveElement.GetAttributeVector2("worldpos", Vector2.Zero); if (worldPos != Vector2.Zero) @@ -325,7 +331,7 @@ namespace Barotrauma { if (this.saveElement == null) { - var doc = Submarine.OpenFile(filePath); + var doc = SubmarineInfo.OpenFile(filePath); saveElement = doc.Root; saveElement.Name = "LinkedSubmarine"; saveElement.Add(new XAttribute("filepath", filePath)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 1bca7d130..d1b5bff03 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -284,7 +284,7 @@ namespace Barotrauma foreach (LocationConnection connection in connections) { float centerDist = Vector2.Distance(connection.CenterPos, mapCenter); - connection.Difficulty = MathHelper.Clamp(((1.0f - centerDist / mapRadius) * 100) + Rand.Range(-10.0f, 10.0f, Rand.RandSync.Server), 0, 100); + connection.Difficulty = MathHelper.Clamp(((1.0f - centerDist / mapRadius) * 100) + Rand.Range(-10.0f, 0.0f, Rand.RandSync.Server), 0, 100); } AssignBiomes(); @@ -463,7 +463,7 @@ namespace Barotrauma bool disallowedFound = false; foreach (string disallowedLocationName in typeChange.DisallowedAdjacentLocations) { - if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.ToLowerInvariant() == disallowedLocationName.ToLowerInvariant())) + if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.Equals(disallowedLocationName, StringComparison.OrdinalIgnoreCase))) { disallowedFound = true; break; @@ -475,7 +475,7 @@ namespace Barotrauma bool requiredFound = false; foreach (string requiredLocationName in typeChange.RequiredAdjacentLocations) { - if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.ToLowerInvariant() == requiredLocationName.ToLowerInvariant())) + if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.Equals(requiredLocationName, StringComparison.OrdinalIgnoreCase))) { requiredFound = true; break; @@ -499,7 +499,7 @@ namespace Barotrauma if (selectedTypeChange != null) { string prevName = location.Name; - location.ChangeType(LocationType.List.Find(lt => lt.Identifier.ToLowerInvariant() == selectedTypeChange.ChangeToType.ToLowerInvariant())); + location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(selectedTypeChange.ChangeToType, StringComparison.OrdinalIgnoreCase))); ChangeLocationType(location, prevName, selectedTypeChange); location.TypeChangeTimer = -1; break; @@ -553,13 +553,12 @@ namespace Barotrauma string prevLocationName = location.Name; LocationType prevLocationType = location.Type; location.Discovered = true; - location.ChangeType(LocationType.List.Find(lt => lt.Identifier.ToLowerInvariant() == locationType.ToLowerInvariant())); + location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase))); location.TypeChangeTimer = typeChangeTimer; location.MissionsCompleted = missionsCompleted; if (showNotifications && prevLocationType != location.Type) { - var change = prevLocationType.CanChangeTo.Find(c => - c.ChangeToType.ToLowerInvariant() == location.Type.Identifier.ToLowerInvariant()); + var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType.Equals(location.Type.Identifier, StringComparison.OrdinalIgnoreCase)); if (change != null) { ChangeLocationType(location, prevLocationName, change); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 1f03417ba..ff075b815 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -36,8 +36,6 @@ namespace Barotrauma //is the mouse inside the rect private bool isHighlighted; - public event Action Resized; - public bool IsHighlighted { get { return isHighlighted || ExternalHighlight; } @@ -115,6 +113,40 @@ namespace Barotrauma } } + // We could use NaN or nullables, but in this case the first is not preferable, because it needs to be checked every time the value is used. + // Nullable on the other requires boxing that we don't want to do too often, since it generates garbage. + public bool SpriteDepthOverrideIsSet { get; private set; } + public float SpriteOverrideDepth => SpriteDepth; + private float _spriteOverrideDepth = float.NaN; + [Editable(0.001f, 0.999f, decimals: 3), Serialize(float.NaN, true)] + public float SpriteDepth + { + get + { + if (SpriteDepthOverrideIsSet) { return _spriteOverrideDepth; } + return Sprite != null ? Sprite.Depth : 0; + } + set + { + if (!float.IsNaN(value)) + { + _spriteOverrideDepth = MathHelper.Clamp(value, 0.001f, 0.999f); + if (this is Item) { _spriteOverrideDepth = Math.Min(_spriteOverrideDepth, 0.9f); } + SpriteDepthOverrideIsSet = true; + } + } + } + + [Serialize(1f, true), Editable(0.01f, 10f, DecimalCount = 3, ValueStep = 0.1f)] + public virtual float Scale { get; set; } = 1; + + [Editable, Serialize(false, true)] + public bool HiddenInGame + { + get; + set; + } + public override Vector2 Position { get @@ -175,9 +207,6 @@ namespace Barotrauma get { return ""; } } - // Quick undo/redo for size and movement only. TODO: Remove if we do a more general implementation. - private Memento rectMemento; - public MapEntity(MapEntityPrefab prefab, Submarine submarine) : base(submarine) { this.prefab = prefab; @@ -560,34 +589,5 @@ namespace Barotrauma } } } - - #region Serialized properties - // We could use NaN or nullables, but in this case the first is not preferable, because it needs to be checked every time the value is used. - // Nullable on the other requires boxing that we don't want to do too often, since it generates garbage. - public bool SpriteDepthOverrideIsSet { get; private set; } - public float SpriteOverrideDepth => SpriteDepth; - private float _spriteOverrideDepth = float.NaN; - [Editable(0.001f, 0.999f, decimals: 3), Serialize(float.NaN, true)] - public float SpriteDepth - { - get - { - if (SpriteDepthOverrideIsSet) { return _spriteOverrideDepth; } - return Sprite != null ? Sprite.Depth : 0; - } - set - { - if (!float.IsNaN(value)) - { - _spriteOverrideDepth = MathHelper.Clamp(value, 0.001f, 0.999f); - if (this is Item) { _spriteOverrideDepth = Math.Min(_spriteOverrideDepth, 0.9f); } - SpriteDepthOverrideIsSet = true; - } - } - } - - [Serialize(1f, true), Editable(0.01f, 10f, DecimalCount = 3, ValueStep = 0.1f)] - public virtual float Scale { get; set; } = 1; - #endregion } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index 3d5aade29..31b6bc9cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -10,7 +10,7 @@ namespace Barotrauma [Flags] enum MapEntityCategory { - Structure = 1, Decorative = 2, Machine = 4, Equipment = 8, Electrical = 16, Material = 32, Misc = 64, Alien = 128, ItemAssembly = 256, Legacy = 512 + Structure = 1, Decorative = 2, Machine = 4, Equipment = 8, Electrical = 16, Material = 32, Misc = 64, Alien = 128, Wrecked = 256, Thalamus = 512, ItemAssembly = 1024, Legacy = 2048 } abstract partial class MapEntityPrefab : IPrefab, IDisposable @@ -43,7 +43,6 @@ namespace Barotrauma protected string originalName; protected string identifier; - protected ContentPackage contentPackage; public Sprite sprite; @@ -243,23 +242,35 @@ namespace Barotrauma /// The identifier of the item (if null, the identifier is ignored and the search is done only based on the name) public static MapEntityPrefab Find(string name, string identifier = null, bool showErrorMessages = true) { - if (name != null) name = name.ToLowerInvariant(); + if (name != null) + { + name = name.ToLowerInvariant(); + } foreach (MapEntityPrefab prefab in List) { if (identifier != null) { if (prefab.identifier != identifier) { + if (prefab.Aliases != null && prefab.Aliases.Any(a => a.Equals(identifier, StringComparison.OrdinalIgnoreCase))) + { + return prefab; + } continue; } else { - if (string.IsNullOrEmpty(name)) return prefab; + if (string.IsNullOrEmpty(name)) { return prefab; } } } if (!string.IsNullOrEmpty(name)) { - if (prefab.Name.ToLowerInvariant() == name || prefab.originalName.ToLowerInvariant() == name || (prefab.Aliases != null && prefab.Aliases.Any(a => a.ToLowerInvariant() == name))) return prefab; + if (prefab.Name.Equals(name, StringComparison.OrdinalIgnoreCase) || + prefab.originalName.Equals(name, StringComparison.OrdinalIgnoreCase) || + (prefab.Aliases != null && prefab.Aliases.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase)))) + { + return prefab; + } } } @@ -282,27 +293,9 @@ namespace Barotrauma /// /// Check if the name or any of the aliases of this prefab match the given name. /// - public bool NameMatches(string name, bool caseSensitive = false) - { - if (caseSensitive) - { - return this.originalName == name || (Aliases != null && Aliases.Any(a => a == name)); - } - else - { - name = name.ToLowerInvariant(); - return this.originalName.ToLowerInvariant() == name || (Aliases != null && Aliases.Any(a => a.ToLowerInvariant() == name)); - } - } + public bool NameMatches(string name, StringComparison comparisonType) => originalName.Equals(name, comparisonType) || (Aliases != null && Aliases.Any(a => a.Equals(name, comparisonType))); - public bool NameMatches(IEnumerable allowedNames, bool caseSensitive = false) - { - foreach (string name in allowedNames) - { - if (NameMatches(name, caseSensitive)) return true; - } - return false; - } + public bool NameMatches(IEnumerable allowedNames, StringComparison comparisonType) => allowedNames.Any(n => NameMatches(n, comparisonType)); public bool IsLinkAllowed(MapEntityPrefab target) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index a3d64c723..aa972af0e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -322,7 +322,7 @@ namespace Barotrauma : base(sp, submarine) { System.Diagnostics.Debug.Assert(rectangle.Width > 0 && rectangle.Height > 0); - if (rectangle.Width == 0 || rectangle.Height == 0) return; + if (rectangle.Width == 0 || rectangle.Height == 0) { return; } defaultRect = rectangle; rect = rectangle; @@ -358,27 +358,30 @@ namespace Barotrauma InitProjSpecific(); - if (Prefab.Body) + if (!HiddenInGame) { - Bodies = new List(); - WallList.Add(this); - - CreateSections(); - UpdateSections(); - } - else - { - Sections = new WallSection[1]; - Sections[0] = new WallSection(rect); - - if (StairDirection != Direction.None) + if (Prefab.Body) { - CreateStairBodies(); + Bodies = new List(); + WallList.Add(this); + + CreateSections(); + UpdateSections(); + } + else + { + Sections = new WallSection[1]; + Sections[0] = new WallSection(rect); + + if (StairDirection != Direction.None) + { + CreateStairBodies(); + } } } // Only add ai targets automatically to submarine/outpost walls - if (aiTarget == null && HasBody && Tags.Contains("wall") && submarine != null && !Prefab.NoAITarget) + if (aiTarget == null && HasBody && Tags.Contains("wall") && submarine != null && !submarine.Info.IsWreck && !Prefab.NoAITarget) { aiTarget = new AITarget(this) { @@ -1216,9 +1219,9 @@ namespace Barotrauma SerializableProperty.DeserializeProperties(s, element); - if (submarine?.GameVersion != null) + if (submarine?.Info.GameVersion != null) { - SerializableProperty.UpgradeGameVersion(s, s.Prefab.ConfigElement, submarine.GameVersion); + SerializableProperty.UpgradeGameVersion(s, s.Prefab.ConfigElement, submarine.Info.GameVersion); } foreach (XElement subElement in element.Elements()) @@ -1322,6 +1325,8 @@ namespace Barotrauma public virtual void Reset() { SerializableProperties = SerializableProperty.DeserializeProperties(this, Prefab.ConfigElement); + Sprite.ReloadXML(); + SpriteDepth = Sprite.Depth; } public override void Update(float deltaTime, Camera cam) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index ef8076b81..51df613ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Collections.Generic; using System.Xml.Linq; +using System.IO; #if CLIENT using Microsoft.Xna.Framework.Graphics; #endif @@ -231,6 +232,23 @@ namespace Barotrauma sp.name = sp.originalName; sp.ConfigElement = element; sp.identifier = element.GetAttributeString("identifier", ""); + + var parentType = element.Parent?.GetAttributeString("prefabtype", "") ?? string.Empty; + + string nameIdentifier = element.GetAttributeString("nameidentifier", ""); + + if (string.IsNullOrEmpty(sp.originalName)) + { + if (string.IsNullOrEmpty(nameIdentifier)) + { + sp.name = TextManager.Get("EntityName." + sp.identifier, true) ?? string.Empty; + } + else + { + sp.name = TextManager.Get("EntityName." + nameIdentifier, true) ?? string.Empty; + } + } + if (string.IsNullOrEmpty(sp.name)) { sp.name = TextManager.Get("EntityName." + sp.identifier, returnNull: true) ?? $"Not defined ({sp.identifier})"; @@ -258,17 +276,15 @@ namespace Barotrauma { DebugConsole.ThrowError("Warning - sprite sourcerect not configured for structure \"" + sp.name + "\"!"); } - #if CLIENT - if (subElement.GetAttributeBool("fliphorizontal", false)) + if (subElement.GetAttributeBool("fliphorizontal", false)) sp.sprite.effects = SpriteEffects.FlipHorizontally; - if (subElement.GetAttributeBool("flipvertical", false)) + if (subElement.GetAttributeBool("flipvertical", false)) sp.sprite.effects = SpriteEffects.FlipVertically; #endif - sp.canSpriteFlipX = subElement.GetAttributeBool("canflipx", true); sp.canSpriteFlipY = subElement.GetAttributeBool("canflipy", true); - + if (subElement.Attribute("name") == null && !string.IsNullOrWhiteSpace(sp.Name)) { sp.sprite.Name = sp.Name; @@ -286,19 +302,52 @@ namespace Barotrauma sp.BackgroundSprite.RelativeOrigin = subElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f)); } #if CLIENT - if (subElement.GetAttributeBool("fliphorizontal", false)) - sp.BackgroundSprite.effects = SpriteEffects.FlipHorizontally; - if (subElement.GetAttributeBool("flipvertical", false)) - sp.BackgroundSprite.effects = SpriteEffects.FlipVertically; + if (subElement.GetAttributeBool("fliphorizontal", false)) { sp.BackgroundSprite.effects = SpriteEffects.FlipHorizontally; } + if (subElement.GetAttributeBool("flipvertical", false)) { sp.BackgroundSprite.effects = SpriteEffects.FlipVertically; } + sp.BackgroundSpriteColor = subElement.GetAttributeColor("color", Color.White); #endif - break; + case "decorativesprite": +#if CLIENT + string decorativeSpriteFolder = ""; + if (!subElement.GetAttributeString("texture", "").Contains("/")) + { + decorativeSpriteFolder = Path.GetDirectoryName(file.Path); + } + + int groupID = 0; + DecorativeSprite decorativeSprite = null; + if (subElement.Attribute("texture") == null) + { + groupID = subElement.GetAttributeInt("randomgroupid", 0); + } + else + { + decorativeSprite = new DecorativeSprite(subElement, decorativeSpriteFolder, lazyLoad: true); + sp.DecorativeSprites.Add(decorativeSprite); + groupID = decorativeSprite.RandomGroupID; + } + if (!sp.DecorativeSpriteGroups.ContainsKey(groupID)) + { + sp.DecorativeSpriteGroups.Add(groupID, new List()); + } + sp.DecorativeSpriteGroups[groupID].Add(decorativeSprite); +#endif + break; + } + } + + if (string.Equals(parentType, "wrecked", StringComparison.OrdinalIgnoreCase)) + { + if (!string.IsNullOrEmpty(sp.Name)) + { + sp.name = TextManager.GetWithVariable("wreckeditemformat", "[name]", sp.name); } } if (!Enum.TryParse(element.GetAttributeString("category", "Structure"), true, out MapEntityCategory category)) { - category = MapEntityCategory.Structure; + category = MapEntityCategory.Structure; } sp.Category = category; @@ -325,7 +374,14 @@ namespace Barotrauma if (string.IsNullOrEmpty(sp.Description)) { - sp.Description = TextManager.Get("EntityDescription." + sp.identifier, returnNull: true) ?? string.Empty; + if (string.IsNullOrEmpty(nameIdentifier)) + { + sp.Description = TextManager.Get("EntityDescription." + sp.identifier, returnNull: true) ?? string.Empty; + } + else + { + sp.Description = TextManager.Get("EntityDescription." + nameIdentifier, true) ?? string.Empty; + } } //backwards compatibility diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index d30787186..f05ef3413 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1,6 +1,6 @@ using Barotrauma.Items.Components; using Barotrauma.Networking; -using Barotrauma.RuinGeneration; +using Barotrauma.Extensions; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; @@ -21,20 +21,11 @@ namespace Barotrauma None = 0, Left = 1, Right = 2 } - [Flags] - public enum SubmarineTag - { - [Description("Shuttle")] - Shuttle = 1, - [Description("Hide in menus")] - HideInMenus = 2 - } - partial class Submarine : Entity, IServerSerializable { - public Character.TeamType TeamID = Character.TeamType.None; + public SubmarineInfo Info { get; private set; } - public const string SavePath = "Submarines"; + public Character.TeamType TeamID = Character.TeamType.None; public static readonly Vector2 HiddenSubStartPosition = new Vector2(-50000.0f, 10000.0f); //position of the "actual submarine" which is rendered wherever the SubmarineBody is @@ -53,12 +44,6 @@ namespace Barotrauma public static bool LockX, LockY; - private static List savedSubmarines = new List(); - public static IEnumerable SavedSubmarines - { - get { return savedSubmarines; } - } - public static readonly Vector2 GridSize = new Vector2(16.0f, 16.0f); public static readonly Submarine[] MainSubs = new Submarine[2]; @@ -77,66 +62,33 @@ namespace Barotrauma private SubmarineBody subBody; - public readonly List DockedTo; + public readonly Dictionary ConnectedDockingPorts; + public IEnumerable DockedTo + { + get + { + if (ConnectedDockingPorts == null) { yield break; } + foreach (Submarine sub in ConnectedDockingPorts.Keys) + { + yield return sub; + } + } + } private static Vector2 lastPickedPosition; private static float lastPickedFraction; private static Vector2 lastPickedNormal; - private Task hashTask; - private Md5Hash hash; - - private string filePath; - private string name; - public readonly DateTime LastModifiedTime; - - private SubmarineTag tags; - private Vector2 prevPosition; private float networkUpdateTimer; private EntityGrid entityGrid = null; - public int RecommendedCrewSizeMin = 1, RecommendedCrewSizeMax = 2; - public string RecommendedCrewExperience; - - public HashSet RequiredContentPackages = new HashSet(); - //properties ---------------------------------------------------- - public string Name - { - get { return name; } - set { name = value; } - } - - private string displayName; - public string DisplayName - { - get { return displayName; } - } - public bool ShowSonarMarker = true; - public string Description - { - get; - set; - } - - public Version GameVersion - { - get; - private set; - } - - public bool IsOutpost - { - get; - private set; - } - public static Vector2 LastPickedPosition { get { return lastPickedPosition; } @@ -164,22 +116,6 @@ namespace Barotrauma set; } - public Md5Hash MD5Hash - { - get - { - if (hash == null) - { - XDocument doc = OpenFile(filePath); - StartHashDocTask(doc); - hashTask.Wait(); - hashTask = null; - } - - return hash; - } - } - public static List Loaded { get { return loaded; } @@ -203,12 +139,6 @@ namespace Barotrauma } } - public Vector2 Dimensions - { - get; - private set; - } - public override Vector2 Position { get { return subBody == null ? Vector2.Zero : subBody.Position - HiddenSubPosition; } @@ -254,8 +184,9 @@ namespace Barotrauma get { if (subsLeftBehind.HasValue) { return subsLeftBehind.Value; } - - CheckSubsLeftBehind(); + + CheckSubsLeftBehind(Info.SubmarineElement); + return subsLeftBehind.Value; } //set { subsLeftBehind = value; } @@ -294,13 +225,6 @@ namespace Barotrauma get { return subBody.HullVertices; } } - - public string FilePath - { - get { return filePath; } - set { filePath = value; } - } - public bool AtDamageDepth { get { return subBody != null && subBody.AtDamageDepth; } @@ -308,7 +232,7 @@ namespace Barotrauma public override string ToString() { - return "Barotrauma.Submarine (" + name + ")"; + return "Barotrauma.Submarine (" + Info?.Name ?? "[NULL INFO]" + ")"; } public override bool Removed @@ -319,269 +243,124 @@ namespace Barotrauma } } - public bool IsFileCorrupted + public void MakeWreck() { - get; - private set; - } - - private bool? requiredContentPackagesInstalled; - public bool RequiredContentPackagesInstalled - { - get - { - if (requiredContentPackagesInstalled.HasValue) { return requiredContentPackagesInstalled.Value; } - return RequiredContentPackages.All(cp => GameMain.SelectedPackages.Any(cp2 => cp2.Name == cp)); - } - set - { - requiredContentPackagesInstalled = value; - } - } - - //constructors & generation ---------------------------------------------------- - - public Submarine(string filePath, string hash = "", bool tryLoad = true) : base(null) - { - this.filePath = filePath; - if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath)) - { - LastModifiedTime = File.GetLastWriteTime(filePath); - } - try - { - name = displayName = Path.GetFileNameWithoutExtension(filePath); - } - catch (Exception e) - { - DebugConsole.ThrowError("Error loading submarine " + filePath + "!", e); - } - - if (!string.IsNullOrWhiteSpace(hash)) - { - this.hash = new Md5Hash(hash); - } - - IsFileCorrupted = false; - - if (tryLoad) - { - XDocument doc = null; - int maxLoadRetries = 4; - for (int i = 0; i <= maxLoadRetries; i++) - { - doc = OpenFile(filePath, out Exception e); - if (e != null && !(e is IOException)) { break; } - if (doc != null || i == maxLoadRetries || !File.Exists(filePath)) { break; } - DebugConsole.NewMessage("Opening submarine file \"" + filePath + "\" failed, retrying in 250 ms..."); - Thread.Sleep(250); - } - if (doc == null || doc.Root == null) - { - IsFileCorrupted = true; - return; - } - - if (doc != null && doc.Root != null) - { - if (string.IsNullOrWhiteSpace(hash)) - { - StartHashDocTask(doc); - } - - displayName = TextManager.Get("Submarine.Name." + name, true); - if (displayName == null || displayName.Length == 0) displayName = name; - - Description = TextManager.Get("Submarine.Description." + name, true); - if (Description == null || Description.Length == 0) Description = doc.Root.GetAttributeString("description", ""); - - GameVersion = new Version(doc.Root.GetAttributeString("gameversion", "0.0.0.0")); - Enum.TryParse(doc.Root.GetAttributeString("tags", ""), out tags); - Dimensions = doc.Root.GetAttributeVector2("dimensions", Vector2.Zero); - RecommendedCrewSizeMin = doc.Root.GetAttributeInt("recommendedcrewsizemin", 0); - RecommendedCrewSizeMax = doc.Root.GetAttributeInt("recommendedcrewsizemax", 0); - RecommendedCrewExperience = doc.Root.GetAttributeString("recommendedcrewexperience", "Unknown"); - - //backwards compatibility (use text tags instead of the actual text) - if (RecommendedCrewExperience == "Beginner") - RecommendedCrewExperience = "CrewExperienceLow"; - else if (RecommendedCrewExperience == "Intermediate") - RecommendedCrewExperience = "CrewExperienceMid"; - else if (RecommendedCrewExperience == "Experienced") - RecommendedCrewExperience = "CrewExperienceHigh"; - - string[] contentPackageNames = doc.Root.GetAttributeStringArray("requiredcontentpackages", new string[0]); - foreach (string contentPackageName in contentPackageNames) - { - RequiredContentPackages.Add(contentPackageName); - } - - CheckSubsLeftBehind(doc.Root); -#if CLIENT - string previewImageData = doc.Root.GetAttributeString("previewimage", ""); - if (!string.IsNullOrEmpty(previewImageData)) - { - try - { - using (MemoryStream mem = new MemoryStream(Convert.FromBase64String(previewImageData))) - { - var texture = TextureLoader.FromStream(mem, path: filePath); - if (texture == null) { throw new Exception("PreviewImage texture returned null"); } - PreviewImage = new Sprite(texture, null, null); - } - } - catch (Exception e) - { - DebugConsole.ThrowError("Loading the preview image of the submarine \"" + Name + "\" failed. The file may be corrupted.", e); - GameAnalyticsManager.AddErrorEventOnce("Submarine..ctor:PreviewImageLoadingFailed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Loading the preview image of the submarine \"" + Name + "\" failed. The file may be corrupted."); - PreviewImage = null; - } - } -#endif - } - } - - DockedTo = new List(); - - FreeID(); - } - - public void StartHashDocTask(XDocument doc) - { - if (hash != null) { return; } - if (hashTask != null) { return; } - - hashTask = new Task(() => - { - hash = new Md5Hash(doc, filePath); - }); - hashTask.Start(); - } - - public bool HasTag(SubmarineTag tag) - { - return tags.HasFlag(tag); - } - - public void AddTag(SubmarineTag tag) - { - if (tags.HasFlag(tag)) return; - - tags |= tag; - } - - public void RemoveTag(SubmarineTag tag) - { - if (!tags.HasFlag(tag)) return; - - tags &= ~tag; - } - - public void CheckSubsLeftBehind(XElement element = null) - { - if (element == null) - { - XDocument doc = null; - int maxLoadRetries = 4; - for (int i = 0; i <= maxLoadRetries; i++) - { - doc = OpenFile(filePath, out Exception e); - if (e != null && !(e is IOException)) { break; } - if (doc != null || i == maxLoadRetries || !File.Exists(filePath)) { break; } - DebugConsole.NewMessage("Opening submarine file \"" + filePath + "\" failed, retrying in 250 ms..."); - Thread.Sleep(250); - } - if (doc?.Root == null) { return; } - element = doc.Root; - } - - subsLeftBehind = false; - LeftBehindSubDockingPortOccupied = false; - foreach (XElement subElement in element.Elements()) - { - if (subElement.Name.ToString().ToLowerInvariant() != "linkedsubmarine") { continue; } - if (subElement.Attribute("location") == null) { continue; } - - subsLeftBehind = true; - ushort targetDockingPortID = (ushort)subElement.GetAttributeInt("originallinkedto", 0); - XElement targetPortElement = targetDockingPortID == 0 ? null : - element.Elements().FirstOrDefault(e => e.GetAttributeInt("ID", 0) == targetDockingPortID); - if (targetPortElement != null && targetPortElement.GetAttributeIntArray("linked", new int[0]).Length > 0) - { - LeftBehindSubDockingPortOccupied = true; - } - } - } - - public void MakeOutpost() - { - IsOutpost = true; + Info.Type = SubmarineInfo.SubmarineType.Wreck; ShowSonarMarker = false; PhysicsBody.FarseerBody.BodyType = BodyType.Static; - TeamID = Character.TeamType.FriendlyNPC; + TeamID = Character.TeamType.None; + } - foreach (MapEntity me in MapEntity.mapEntityList) + public WreckAI ThalamusAI { get; private set; } + public bool CreateThalamus() + { + MakeWreck(); + var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => p.Category == MapEntityCategory.Thalamus || p.Tags.Contains("thalamus")); + var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains("thalamusbrain"), Rand.RandSync.Server); + if (brainPrefab == null) { return false; } + var allItems = GetItems(false); + var thalamusItems = allItems.FindAll(i => i.Prefab.Category == MapEntityCategory.Thalamus || i.HasTag("thalamus")); + var hulls = GetHulls(false); + Item brain = new Item(brainPrefab, Vector2.Zero, this); + Vector2 negativeMargin = new Vector2(40, 20); + Vector2 minSize = brain.Rect.Size.ToVector2() - negativeMargin; + Vector2 maxSize = new Vector2(brain.Rect.Width * 3, brain.Rect.Height * 3); + // First try to get a room that is not too big and not in the edges of the sub. + // Also try not to create the brain in a room that already have carrier items inside. + // Ignore hulls that have any linked hulls to keep the calculations simple. + // Shrink the horizontal axis so that the brain is not placed in the left or right side, where we often have curved walls. + // Also ignore hulls that have open gaps, because we'll want the room to be full of water. The room will be filled with water when the brain is inserted in the room. + Rectangle shrinkedBounds = ToolBox.GetWorldBounds(WorldPosition.ToPoint(), new Point(Borders.Width - 500, Borders.Height)); + bool BaseCondition(Hull h) => h.RectWidth > minSize.X && h.RectHeight > minSize.Y && h.GetLinkedEntities().None() && h.ConnectedGaps.None(g => g.Open > 0); + bool IsNotTooBig(Hull h) => h.RectWidth < maxSize.X && h.RectHeight < maxSize.Y; + bool IsNotInFringes(Hull h) => shrinkedBounds.ContainsWorld(h.WorldRect); + bool DoesNotContainOtherItems(Hull h) => thalamusItems.None(i => i.CurrentHull == h); + Hull brainHull = hulls.GetRandom(h => BaseCondition(h) && IsNotTooBig(h) && IsNotInFringes(h) && DoesNotContainOtherItems(h), Rand.RandSync.Server); + if (brainHull == null) { - if (me.Submarine != this) { continue; } - if (me is Item item) - { - if (item.GetComponent() != null) - { - item.Indestructible = true; - } - foreach (ItemComponent ic in item.Components) - { - if (ic is ConnectionPanel connectionPanel) - { - //prevent rewiring - connectionPanel.Locked = true; - } - else if (ic is Holdable holdable && holdable.Attached) - { - //prevent deattaching items from walls -#if CLIENT - if (GameMain.GameSession?.GameMode is TutorialMode) - { - continue; - } -#endif - holdable.CanBePicked = false; - holdable.CanBeSelected = false; - } - } - } - else if (me is Structure structure) - { - structure.Indestructible = true; - } + brainHull = hulls.GetRandom(h => BaseCondition(h) && IsNotInFringes(h) && DoesNotContainOtherItems(h), Rand.RandSync.Server); } + if (brainHull == null) + { + brainHull = hulls.GetRandom(h => BaseCondition(h) && (IsNotInFringes(h) || DoesNotContainOtherItems(h)), Rand.RandSync.Server); + } + if (brainHull == null) + { + brainHull = hulls.GetRandom(BaseCondition, Rand.RandSync.Server); + } + var thalamusStructs = StructurePrefab.Prefabs.Where(p => p.Category == MapEntityCategory.Thalamus); + if (brainHull == null) { return false; } + brainHull.WaterVolume = brainHull.Volume; + brain.SetTransform(brainHull.SimPosition, rotation: 0, findNewHull: false); + brain.CurrentHull = brainHull; + var backgroundPrefab = thalamusStructs.GetRandom(i => i.Tags.Contains("brainroombackground"), Rand.RandSync.Server); + if (backgroundPrefab != null) + { + new Structure(brainHull.Rect, backgroundPrefab, this); + } + var horizontalWallPrefab = thalamusStructs.GetRandom(p => p.Tags.Contains("thalamuswall_horizontal_decorative"), Rand.RandSync.Server); + if (horizontalWallPrefab != null) + { + int height = (int)horizontalWallPrefab.Size.Y; + int halfHeight = height / 2; + int quarterHeight = halfHeight / 2; + new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, this); + new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, this); + } + var verticalWallPrefab = thalamusStructs.GetRandom(p => p.Tags.Contains("thalamuswall_vertical_decorative"), Rand.RandSync.Server); + if (verticalWallPrefab != null) + { + int width = (int)verticalWallPrefab.Size.X; + int halfWidth = width / 2; + int quarterWidth = halfWidth / 2; + new Structure(new Rectangle(brainHull.Rect.Left - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, this); + new Structure(new Rectangle(brainHull.Rect.Right - halfWidth - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, this); + } + ThalamusAI = new WreckAI(this, brain, allItems); + return true; + } + + public void DisableThalamus() + { + var thalamusEntities = GetEntities(false, MapEntity.mapEntityList).FindAll(e => e.prefab.Category == MapEntityCategory.Thalamus || e.prefab.Tags.Contains("thalamus")).ToList(); + + foreach (var entity in thalamusEntities) + { + entity.Remove(); + } + ThalamusAI?.Kill(); + ThalamusAI = null; } /// /// Returns a rect that contains the borders of this sub and all subs docked to it /// - public Rectangle GetDockedBorders() + public Rectangle GetDockedBorders(List checkd = null) { - Rectangle dockedBorders = Borders; - dockedBorders.Y -= dockedBorders.Height; + if (checkd == null) { checkd = new List(); } + checkd.Add(this); - var connectedSubs = GetConnectedSubs(); + Rectangle dockedBorders = Borders; + + var connectedSubs = DockedTo.Where(s => !checkd.Contains(s) && !s.Info.IsOutpost).ToList(); foreach (Submarine dockedSub in connectedSubs) { - if (dockedSub == this) continue; + //use docking ports instead of world position to determine + //borders, as world position will not necessarily match where + //the subs are supposed to go + Vector2? expectedLocation = CalculateDockOffset(this, dockedSub); + if (expectedLocation == null) { continue; } - Vector2 diff = dockedSub.Submarine == this ? dockedSub.WorldPosition : dockedSub.WorldPosition - WorldPosition; - - Rectangle dockedSubBorders = dockedSub.Borders; - dockedSubBorders.Y -= dockedSubBorders.Height; - dockedSubBorders.Location += MathUtils.ToPoint(diff); + Rectangle dockedSubBorders = dockedSub.GetDockedBorders(checkd); + dockedSubBorders.Location += MathUtils.ToPoint(expectedLocation.Value); + dockedBorders.Y = -dockedBorders.Y; + dockedSubBorders.Y = -dockedSubBorders.Y; dockedBorders = Rectangle.Union(dockedBorders, dockedSubBorders); + dockedBorders.Y = -dockedBorders.Y; } - dockedBorders.Y += dockedBorders.Height; return dockedBorders; } @@ -612,6 +391,9 @@ namespace Barotrauma } } + /// + /// Attempt to find a spawn position close to the specified position where the sub doesn't collide with walls/ruins + /// public Vector2 FindSpawnPos(Vector2 spawnPos, Point? submarineSize = null, float subDockingPortOffset = 0.0f) { Rectangle dockedBorders = GetDockedBorders(); @@ -677,9 +459,11 @@ namespace Barotrauma return spawnPos - diffFromDockedBorders; } - public void UpdateTransform() + public void UpdateTransform(bool interpolate = true) { - DrawPosition = Timing.Interpolate(prevPosition, Position); + DrawPosition = interpolate ? + Timing.Interpolate(prevPosition, Position) : + Position; } //math/physics stuff ---------------------------------------------------- @@ -770,9 +554,9 @@ namespace Barotrauma public static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable ignoredBodies = null, Category? collisionCategory = null, bool ignoreSensors = true, Predicate customPredicate = null, bool allowInsideFixture = false) { - if (Vector2.DistanceSquared(rayStart, rayEnd) < 0.00001f) + if (Vector2.DistanceSquared(rayStart, rayEnd) < 0.0001f) { - rayEnd += Vector2.UnitX * 0.001f; + return null; } float closestFraction = 1.0f; @@ -1069,7 +853,12 @@ namespace Barotrauma { //if (PlayerInput.KeyHit(InputType.Crouch) && (this == MainSub)) FlipX(); - if (Level.Loaded == null || subBody == null) return; + if (Level.Loaded == null || subBody == null) { return; } + + if (Info.Type == SubmarineInfo.SubmarineType.Wreck) + { + ThalamusAI?.Update(deltaTime); + } if (WorldPosition.Y < Level.MaxEntityDepth && subBody.Body.Enabled && @@ -1118,7 +907,6 @@ namespace Barotrauma { networkUpdateTimer = 1.0f; } - } public void ApplyForce(Vector2 force) @@ -1131,23 +919,35 @@ namespace Barotrauma prevPosition = position; } - public void SetPosition(Vector2 position) + public void SetPosition(Vector2 position, List checkd=null) { if (!MathUtils.IsValid(position)) return; - + + if (checkd == null) { checkd = new List(); } + if (checkd.Contains(this)) { return; } + + checkd.Add(this); + subBody.SetPosition(position); + UpdateTransform(interpolate: false); - foreach (Submarine sub in loaded) + foreach (Submarine dockedSub in DockedTo) { - if (sub != this && sub.Submarine == this) - { - sub.SetPosition(position + sub.WorldPosition); - sub.Submarine = null; - } + Vector2? expectedLocation = CalculateDockOffset(this, dockedSub); + if (expectedLocation == null) { continue; } + dockedSub.SetPosition(position + expectedLocation.Value, checkd); + dockedSub.UpdateTransform(interpolate: false); } - //Level.Loaded.SetPosition(-position); - //prevPosition = position; + } + + public static Vector2? CalculateDockOffset(Submarine sub, Submarine dockedSub) + { + Item myPort = sub.ConnectedDockingPorts.ContainsKey(dockedSub) ? sub.ConnectedDockingPorts[dockedSub].Item : null; + if (myPort == null) { return null; } + Item theirPort = dockedSub.ConnectedDockingPorts.ContainsKey(sub) ? dockedSub.ConnectedDockingPorts[sub].Item : null; + if (theirPort == null) { return null; } + return (myPort.Position - sub.HiddenSubPosition) - (theirPort.Position - dockedSub.HiddenSubPosition); } public void Translate(Vector2 amount) @@ -1155,8 +955,6 @@ namespace Barotrauma if (amount == Vector2.Zero || !MathUtils.IsValid(amount)) return; subBody.SetPosition(subBody.Position + amount); - - //Level.Loaded.Move(-amount); } public static Submarine FindClosest(Vector2 worldPosition, bool ignoreOutposts = false, bool ignoreOutsideLevel = true) @@ -1165,7 +963,7 @@ namespace Barotrauma float closestDist = 0.0f; foreach (Submarine sub in loaded) { - if (ignoreOutposts && sub.IsOutpost) { continue; } + if (ignoreOutposts && sub.Info.IsOutpost) { continue; } if (ignoreOutsideLevel && Level.Loaded != null && sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } float dist = Vector2.DistanceSquared(worldPosition, sub.WorldPosition); if (closest == null || dist < closestDist) @@ -1186,6 +984,8 @@ namespace Barotrauma public List GetHulls(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Hull.hullList); public List GetGaps(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Gap.GapList); public List GetItems(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Item.ItemList); + public List GetWaypoints(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, WayPoint.WayPointList); + public List GetWalls(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Structure.WallList); public List GetEntities(bool includingConnectedSubs, List list) where T : MapEntity { @@ -1209,7 +1009,7 @@ namespace Barotrauma /// public static Submarine FindContaining(Vector2 position) { - foreach (Submarine sub in Submarine.Loaded) + foreach (Submarine sub in Loaded) { Rectangle subBorders = sub.Borders; subBorders.Location += MathUtils.ToPoint(sub.HiddenSubPosition) - new Microsoft.Xna.Framework.Point(0, sub.Borders.Height); @@ -1221,255 +1021,37 @@ namespace Barotrauma return null; } - - //saving/loading ---------------------------------------------------- - - public static void AddToSavedSubs(Submarine sub) + public static Rectangle GetBorders(XElement submarineElement) { - savedSubmarines.Add(sub); + Vector4 bounds = Vector4.Zero; + foreach (XElement element in submarineElement.Elements()) + { + if (element.Name != "Structure") { continue; } + + string name = element.GetAttributeString("name", ""); + string identifier = element.GetAttributeString("identifier", ""); + StructurePrefab prefab = Structure.FindPrefab(name, identifier); + if (prefab == null || !prefab.Body) { continue; } + + var rect = element.GetAttributeRect("rect", Rectangle.Empty); + bounds = new Vector4( + Math.Min(rect.X, bounds.X), + Math.Max(rect.Y, bounds.Y), + Math.Max(rect.Right, bounds.Z), + Math.Min(rect.Y - rect.Height, bounds.W)); + } + + return new Rectangle((int)bounds.X, (int)bounds.Y, (int)(bounds.Z - bounds.X), (int)(bounds.Y - bounds.W)); } - public static void RefreshSavedSub(string filePath) + public Submarine(SubmarineInfo info, bool showWarningMessages = true) : base(null) { - string fullPath = Path.GetFullPath(filePath); - for (int i = savedSubmarines.Count - 1; i >= 0; i--) - { - if (Path.GetFullPath(savedSubmarines[i].filePath) == fullPath) - { - savedSubmarines[i].Dispose(); - } - } - if (File.Exists(filePath)) - { - var sub = new Submarine(filePath); - if (!sub.IsFileCorrupted) - { - savedSubmarines.Add(sub); - } - savedSubmarines = savedSubmarines.OrderBy(s => s.filePath ?? "").ToList(); - } - } - - public static void RefreshSavedSubs() - { - var contentPackageSubs = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Submarine); - - for (int i = savedSubmarines.Count - 1; i>= 0; i--) - { - if (File.Exists(savedSubmarines[i].FilePath) && - savedSubmarines[i].LastModifiedTime == File.GetLastWriteTime(savedSubmarines[i].FilePath) && - (Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SavePath) || - contentPackageSubs.Any(fp => Path.GetFullPath(fp.Path).CleanUpPath() == Path.GetFullPath(savedSubmarines[i].FilePath).CleanUpPath()))) - { - continue; - } - savedSubmarines[i].Dispose(); - } - - if (!Directory.Exists(SavePath)) - { - try - { - Directory.CreateDirectory(SavePath); - } - catch (Exception e) - { - DebugConsole.ThrowError("Directory \"" + SavePath + "\" not found and creating the directory failed.", e); - return; - } - } - - List filePaths; - string[] subDirectories; - - try - { - filePaths = Directory.GetFiles(SavePath).ToList(); - subDirectories = Directory.GetDirectories(SavePath); - } - catch (Exception e) - { - DebugConsole.ThrowError("Couldn't open directory \"" + SavePath + "\"!", e); - return; - } - - foreach (string subDirectory in subDirectories) - { - try - { - filePaths.AddRange(Directory.GetFiles(subDirectory).ToList()); - } - catch (Exception e) - { - DebugConsole.ThrowError("Couldn't open subdirectory \"" + subDirectory + "\"!", e); - return; - } - } - - foreach (ContentFile subFile in contentPackageSubs) - { - if (!filePaths.Any(fp => Path.GetFullPath(fp) == Path.GetFullPath(subFile.Path))) - { - filePaths.Add(subFile.Path); - } - } - - filePaths.RemoveAll(p => savedSubmarines.Any(sub => sub.FilePath == p)); - - foreach (string path in filePaths) - { - var sub = new Submarine(path); - if (sub.IsFileCorrupted) - { -#if CLIENT - if (DebugConsole.IsOpen) { DebugConsole.Toggle(); } - var deleteSubPrompt = new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariable("SubLoadError", "[subname]", sub.name) +"\n"+ - TextManager.GetWithVariable("DeleteFileVerification", "[filename]", sub.name), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - - string filePath = path; - deleteSubPrompt.Buttons[0].OnClicked += (btn, userdata) => - { - try - { - File.Delete(filePath); - } - catch (Exception e) - { - DebugConsole.ThrowError($"Failed to delete file \"{filePath}\".", e); - } - deleteSubPrompt.Close(); - return true; - }; - deleteSubPrompt.Buttons[1].OnClicked += deleteSubPrompt.Close; -#endif - } - else - { - savedSubmarines.Add(sub); - } - } - } - - static readonly string TempFolder = Path.Combine("Submarine", "Temp"); - - public static XDocument OpenFile(string file) - { - return OpenFile(file, out _); - } - - public static XDocument OpenFile(string file, out Exception exception) - { - XDocument doc = null; - string extension = ""; - exception = null; - - try - { - extension = System.IO.Path.GetExtension(file); - } - catch - { - //no file extension specified: try using the default one - file += ".sub"; - } - - if (string.IsNullOrWhiteSpace(extension)) - { - extension = ".sub"; - file += ".sub"; - } - - if (extension == ".sub") - { - Stream stream = null; - try - { - stream = SaveUtil.DecompressFiletoStream(file); - } - catch (FileNotFoundException e) - { - exception = e; - DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (File not found)"); - return null; - } - catch (Exception e) - { - exception = e; - DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed!", e); - return null; - } - - try - { - stream.Position = 0; - doc = XDocument.Load(stream); //ToolBox.TryLoadXml(file); - stream.Close(); - stream.Dispose(); - } - - catch (Exception e) - { - exception = e; - DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (" + e.Message + ")"); - return null; - } - } - else if (extension == ".xml") - { - try - { - ToolBox.IsProperFilenameCase(file); - doc = XDocument.Load(file, LoadOptions.SetBaseUri); - } - - catch (Exception e) - { - exception = e; - DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (" + e.Message + ")"); - return null; - } - } - else - { - DebugConsole.ThrowError("Couldn't load submarine \"" + file + "! (Unrecognized file extension)"); - return null; - } - - return doc; - } - - public void Load(bool unloadPrevious, XElement submarineElement = null, bool showWarningMessages = true) - { - if (unloadPrevious) Unload(); - Loading = true; - if (submarineElement == null) - { - XDocument doc = null; - int maxLoadRetries = 4; - for (int i = 0; i <= maxLoadRetries; i++) - { - doc = OpenFile(filePath); - if (doc != null || i == maxLoadRetries || !File.Exists(filePath)) { break; } - DebugConsole.NewMessage("Loading the submarine \"" + Name + "\" failed, retrying in 250 ms..."); - Thread.Sleep(250); - } - if (doc == null || doc.Root == null) - { - IsFileCorrupted = true; - return; - } - submarineElement = doc.Root; - } + Info = new SubmarineInfo(info); + + ConnectedDockingPorts = new Dictionary(); - GameVersion = GameVersion ?? new Version(submarineElement.GetAttributeString("gameversion", "0.0.0.0")); - Description = submarineElement.GetAttributeString("description", ""); - Enum.TryParse(submarineElement.GetAttributeString("tags", ""), out tags); - //place the sub above the top of the level HiddenSubPosition = HiddenSubStartPosition; if (GameMain.GameSession != null && GameMain.GameSession.Level != null) @@ -1487,8 +1069,12 @@ namespace Barotrauma { IdOffset = Math.Max(IdOffset, me.ID); } - - var newEntities = MapEntity.LoadAll(this, submarineElement, filePath); + + List newEntities = new List(); + if (Info.SubmarineElement != null) + { + newEntities = MapEntity.LoadAll(this, Info.SubmarineElement, Info.FilePath); + } Vector2 center = Vector2.Zero; var matchingHulls = Hull.hullList.FindAll(h => h.Submarine == this); @@ -1530,10 +1116,53 @@ namespace Barotrauma MapEntity.mapEntityList[i].Move(-center); } } - } - subBody = new SubmarineBody(this, showWarningMessages); - subBody.SetPosition(HiddenSubPosition); + subBody = new SubmarineBody(this, showWarningMessages); + subBody.SetPosition(HiddenSubPosition); + + if (info.IsOutpost) + { + ShowSonarMarker = false; + PhysicsBody.FarseerBody.BodyType = BodyType.Static; + TeamID = Character.TeamType.FriendlyNPC; + + foreach (MapEntity me in MapEntity.mapEntityList) + { + if (me.Submarine != this) { continue; } + if (me is Item item) + { + if (item.GetComponent() != null) + { + item.Indestructible = true; + } + foreach (ItemComponent ic in item.Components) + { + if (ic is ConnectionPanel connectionPanel) + { + //prevent rewiring + connectionPanel.Locked = true; + } + else if (ic is Holdable holdable && holdable.Attached) + { + //prevent deattaching items from walls +#if CLIENT + if (GameMain.GameSession?.GameMode is TutorialMode) + { + continue; + } +#endif + holdable.CanBePicked = false; + holdable.CanBeSelected = false; + } + } + } + else if (me is Structure structure) + { + structure.Indestructible = true; + } + } + } + } loaded.Add(this); @@ -1556,7 +1185,7 @@ namespace Barotrauma foreach (Hull hull in matchingHulls) { - if (string.IsNullOrEmpty(hull.RoomName) || !hull.RoomName.ToLowerInvariant().Contains("roomname.")) + if (string.IsNullOrEmpty(hull.RoomName) || !hull.RoomName.Contains("roomname.", StringComparison.OrdinalIgnoreCase)) { hull.RoomName = hull.CreateRoomName(); } @@ -1567,9 +1196,9 @@ namespace Barotrauma #endif //if the sub was made using an older version, //halve the brightness of the lights to make them look (almost) right on the new lighting formula - if (showWarningMessages && Screen.Selected != GameMain.SubEditorScreen && (GameVersion == null || GameVersion < new Version("0.8.9.0"))) + if (showWarningMessages && Screen.Selected != GameMain.SubEditorScreen && (Info.GameVersion == null || Info.GameVersion < new Version("0.8.9.0"))) { - DebugConsole.ThrowError("The submarine \"" + Name + "\" was made using an older version of the Barotrauma that used a different formula to calculate the lighting. " + DebugConsole.ThrowError("The submarine \"" + Info.Name + "\" was made using an older version of the Barotrauma that used a different formula to calculate the lighting. " + "The game automatically adjusts the lights make them look better with the new formula, but it's recommended to open the submarine in the submarine editor and make sure everything looks right after the automatic conversion."); foreach (Item item in Item.ItemList) { @@ -1580,91 +1209,73 @@ namespace Barotrauma } } - ID = (ushort)(ushort.MaxValue - 1 - Submarine.loaded.IndexOf(this)); } - public static Submarine Load(XElement element, bool unloadPrevious) + public static Submarine Load(SubmarineInfo info, bool unloadPrevious) { if (unloadPrevious) Unload(); - //tryload -> false - - Submarine sub = new Submarine(element.GetAttributeString("name", ""), "", false); - sub.Load(unloadPrevious, element); + Submarine sub = new Submarine(info, false); return sub; } - public static Submarine Load(string fileName, bool unloadPrevious) + public void CheckSubsLeftBehind(XElement element = null) { - return Load(fileName, SavePath, unloadPrevious); - } + if (element == null) { element = Info.SubmarineElement; } - public static Submarine Load(string fileName, string folder, bool unloadPrevious) - { - if (unloadPrevious) Unload(); - - string path = string.IsNullOrWhiteSpace(folder) ? fileName : System.IO.Path.Combine(SavePath, fileName); - - Submarine sub = new Submarine(path); - sub.Load(unloadPrevious); - - return sub; - } - - public bool SaveAs(string filePath, MemoryStream previewImage = null) - { - name = Path.GetFileNameWithoutExtension(filePath); - - XDocument doc = new XDocument(new XElement("Submarine")); - SaveToXElement(doc.Root); - - if (previewImage != null) + subsLeftBehind = false; + LeftBehindSubDockingPortOccupied = false; + foreach (XElement subElement in element.Elements()) { - doc.Root.Add(new XAttribute("previewimage", Convert.ToBase64String(previewImage.ToArray()))); - } + if (!subElement.Name.ToString().Equals("linkedsubmarine")) { continue; } + if (subElement.Attribute("location") == null) { continue; } - try - { - SaveUtil.CompressStringToFile(filePath, doc.ToString()); + subsLeftBehind = true; + ushort targetDockingPortID = (ushort)subElement.GetAttributeInt("originallinkedto", 0); + XElement targetPortElement = targetDockingPortID == 0 ? null : + element.Elements().FirstOrDefault(e => e.GetAttributeInt("ID", 0) == targetDockingPortID); + if (targetPortElement != null && targetPortElement.GetAttributeIntArray("linked", new int[0]).Length > 0) + { + LeftBehindSubDockingPortOccupied = true; + } } - catch (Exception e) - { - DebugConsole.ThrowError("Saving submarine \"" + filePath + "\" failed!", e); - return false; - } - - hash = null; - hashTask = null; - Md5Hash.RemoveFromCache(filePath); - - return true; } public void SaveToXElement(XElement element) { - element.Add(new XAttribute("name", name)); - element.Add(new XAttribute("description", Description ?? "")); - element.Add(new XAttribute("tags", tags.ToString())); + element.Add(new XAttribute("name", Info.Name)); + element.Add(new XAttribute("description", Info.Description ?? "")); + element.Add(new XAttribute("tags", Info.Tags.ToString())); element.Add(new XAttribute("gameversion", GameMain.Version.ToString())); Rectangle dimensions = CalculateDimensions(); element.Add(new XAttribute("dimensions", XMLExtensions.Vector2ToString(dimensions.Size.ToVector2()))); - element.Add(new XAttribute("recommendedcrewsizemin", RecommendedCrewSizeMin)); - element.Add(new XAttribute("recommendedcrewsizemax", RecommendedCrewSizeMax)); - element.Add(new XAttribute("recommendedcrewexperience", RecommendedCrewExperience ?? "")); - element.Add(new XAttribute("requiredcontentpackages", string.Join(", ", RequiredContentPackages))); + element.Add(new XAttribute("recommendedcrewsizemin", Info.RecommendedCrewSizeMin)); + element.Add(new XAttribute("recommendedcrewsizemax", Info.RecommendedCrewSizeMax)); + element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience ?? "")); + element.Add(new XAttribute("requiredcontentpackages", string.Join(", ", Info.RequiredContentPackages))); - foreach (MapEntity e in MapEntity.mapEntityList) + foreach (MapEntity e in MapEntity.mapEntityList.OrderBy(e => e.ID)) { if (e.Submarine != this || !e.ShouldBeSaved) continue; + if (e is Item item && item.FindParentInventory(inv => inv is CharacterInventory) != null) continue; e.Save(element); } CheckSubsLeftBehind(element); } + public bool SaveAs(string filePath, MemoryStream previewImage = null) + { + var newInfo = new SubmarineInfo(this); + newInfo.FilePath = filePath; + newInfo.Name = Path.GetFileNameWithoutExtension(filePath); + Info.Dispose(); Info = newInfo; + + return newInfo.SaveAs(filePath, previewImage); + } public static bool Unloading { @@ -1682,7 +1293,8 @@ namespace Barotrauma if (GameMain.LightManager != null) GameMain.LightManager.ClearLights(); #endif - foreach (Submarine sub in loaded) + var _loaded = new List(loaded); + foreach (Submarine sub in _loaded) { sub.Remove(); } @@ -1728,21 +1340,25 @@ namespace Barotrauma subBody = null; + if (entityGrid != null) + { + Hull.EntityGrids.Remove(entityGrid); + entityGrid = null; + } + visibleEntities = null; if (MainSub == this) MainSub = null; if (MainSubs[1] == this) MainSubs[1] = null; - DockedTo?.Clear(); + ConnectedDockingPorts?.Clear(); + + loaded.Remove(this); } public void Dispose() { - savedSubmarines.Remove(this); -#if CLIENT - PreviewImage?.Remove(); - PreviewImage = null; -#endif + Remove(); } private List outdoorNodes; @@ -1757,6 +1373,7 @@ namespace Barotrauma return outdoorNodes; } } + private HashSet obstructedNodes = new HashSet(); /// @@ -1831,5 +1448,4 @@ namespace Barotrauma obstructedNodes.Clear(); } } - } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 15460c917..1cdcdabb4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -521,9 +521,16 @@ namespace Barotrauma Vector2 normalizedVel = character.AnimController.Collider.LinearVelocity == Vector2.Zero ? Vector2.Zero : Vector2.Normalize(character.AnimController.Collider.LinearVelocity); - Vector2 targetPos = ConvertUnits.ToDisplayUnits(points[0] - contactNormal); + //try to find the hull right next to the contact point + Vector2 targetPos = ConvertUnits.ToDisplayUnits(points[0] - contactNormal * 0.1f); Hull newHull = Hull.FindHull(targetPos, null); - + //not found, try searching a bit further + if (newHull == null) + { + targetPos = ConvertUnits.ToDisplayUnits(points[0] - contactNormal); + newHull = Hull.FindHull(targetPos, null); + } + //still not found, try searching in the direction the character is heading to if (newHull == null) { targetPos = ConvertUnits.ToDisplayUnits(points[0] + normalizedVel); @@ -771,7 +778,7 @@ namespace Barotrauma if (Character.Controlled != null && Character.Controlled.Submarine == submarine) { GameMain.GameScreen.Cam.Shake = impact * 2.0f; - if (!submarine.IsOutpost && !submarine.DockedTo.Any(s => s.IsOutpost)) + if (submarine.Info.Type == SubmarineInfo.SubmarineType.Player && !submarine.DockedTo.Any(s => s.Info.Type != SubmarineInfo.SubmarineType.Player)) { float angularVelocity = (impactPos.X - Body.SimPosition.X) / ConvertUnits.ToSimUnits(submarine.Borders.Width / 2) * impulse.Y diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs new file mode 100644 index 000000000..5300465af --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -0,0 +1,564 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace Barotrauma +{ + [Flags] + public enum SubmarineTag + { + [Description("Shuttle")] + Shuttle = 1, + [Description("Hide in menus")] + HideInMenus = 2 + } + + partial class SubmarineInfo : IDisposable + { + public const string SavePath = "Submarines"; + + private static List savedSubmarines = new List(); + public static IEnumerable SavedSubmarines + { + get { return savedSubmarines; } + } + + private Task hashTask; + private Md5Hash hash; + + public readonly DateTime LastModifiedTime; + + public SubmarineTag Tags { get; private set; } + + public int RecommendedCrewSizeMin = 1, RecommendedCrewSizeMax = 2; + public string RecommendedCrewExperience; + + public HashSet RequiredContentPackages = new HashSet(); + + public string Name + { + get; + set; + } + + public string DisplayName + { + get; + set; + } + + public string Description + { + get; + set; + } + + public Version GameVersion + { + get; + set; + } + + public bool IsOutpost => Type == SubmarineType.Outpost; + public bool IsWreck => Type == SubmarineType.Wreck; + + public enum SubmarineType { Player, Outpost, Wreck } + public SubmarineType Type { get; set; } + + public Md5Hash MD5Hash + { + get + { + if (hash == null) + { + XDocument doc = OpenFile(FilePath); + StartHashDocTask(doc); + hashTask.Wait(); + hashTask = null; + } + + return hash; + } + } + + public Vector2 Dimensions + { + get; + private set; + } + + public string FilePath + { + get; + set; + } + + public readonly XElement SubmarineElement; + + public override string ToString() + { + return "Barotrauma.SubmarineInfo (" + Name + ")"; + } + + public bool IsFileCorrupted + { + get; + private set; + } + + private bool? requiredContentPackagesInstalled; + public bool RequiredContentPackagesInstalled + { + get + { + if (requiredContentPackagesInstalled.HasValue) { return requiredContentPackagesInstalled.Value; } + return RequiredContentPackages.All(cp => GameMain.SelectedPackages.Any(cp2 => cp2.Name == cp)); + } + set + { + requiredContentPackagesInstalled = value; + } + } + + //constructors & generation ---------------------------------------------------- + public SubmarineInfo() + { + FilePath = null; + Name = DisplayName = TextManager.Get("UnspecifiedSubFileName"); + IsFileCorrupted = false; + RequiredContentPackages = new HashSet(); + } + + public SubmarineInfo(string filePath, string hash = "", XElement element = null) + { + FilePath = filePath; + if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath)) + { + LastModifiedTime = File.GetLastWriteTime(filePath); + } + try + { + Name = DisplayName = Path.GetFileNameWithoutExtension(filePath); + } + catch (Exception e) + { + DebugConsole.ThrowError("Error loading submarine " + filePath + "!", e); + } + + if (!string.IsNullOrWhiteSpace(hash)) + { + this.hash = new Md5Hash(hash); + } + + IsFileCorrupted = false; + + RequiredContentPackages = new HashSet(); + + if (element == null) + { + XDocument doc = null; + int maxLoadRetries = 4; + for (int i = 0; i <= maxLoadRetries; i++) + { + doc = OpenFile(filePath, out Exception e); + if (e != null && !(e is IOException)) { break; } + if (doc != null || i == maxLoadRetries || !File.Exists(filePath)) { break; } + DebugConsole.NewMessage("Opening submarine file \"" + filePath + "\" failed, retrying in 250 ms..."); + Thread.Sleep(250); + } + if (doc == null || doc.Root == null) + { + IsFileCorrupted = true; + return; + } + + if (string.IsNullOrWhiteSpace(hash)) + { + StartHashDocTask(doc); + } + + SubmarineElement = doc.Root; + } + else + { + SubmarineElement = element; + } + + Init(); + } + + public SubmarineInfo(Submarine sub) : this(sub.Info) + { + SubmarineElement = new XElement("Submarine"); + sub.SaveToXElement(SubmarineElement); + Init(); + } + + public SubmarineInfo(SubmarineInfo original) + { + Name = original.Name; + DisplayName = original.DisplayName; + Description = original.Description; + GameVersion = original.GameVersion; + Type = original.Type; + hash = !string.IsNullOrEmpty(original.FilePath) ? original.MD5Hash : null; + Dimensions = original.Dimensions; + FilePath = original.FilePath; + RequiredContentPackages = new HashSet(original.RequiredContentPackages); + IsFileCorrupted = original.IsFileCorrupted; + SubmarineElement = original.SubmarineElement; + RecommendedCrewExperience = original.RecommendedCrewExperience; + RecommendedCrewSizeMin = original.RecommendedCrewSizeMin; + RecommendedCrewSizeMax = original.RecommendedCrewSizeMax; + Tags = original.Tags; +#if CLIENT + PreviewImage = original.PreviewImage != null ? new Sprite(original.PreviewImage.Texture, null, null) : null; +#endif + } + + private void Init() + { + DisplayName = TextManager.Get("Submarine.Name." + Name, true); + if (string.IsNullOrEmpty(DisplayName)) { DisplayName = Name; } + + Description = TextManager.Get("Submarine.Description." + Name, true); + if (string.IsNullOrEmpty(Description)) { Description = SubmarineElement.GetAttributeString("description", ""); } + + GameVersion = new Version(SubmarineElement.GetAttributeString("gameversion", "0.0.0.0")); + if (Enum.TryParse(SubmarineElement.GetAttributeString("tags", ""), out SubmarineTag tags)) + { + Tags = tags; + } + Dimensions = SubmarineElement.GetAttributeVector2("dimensions", Vector2.Zero); + RecommendedCrewSizeMin = SubmarineElement.GetAttributeInt("recommendedcrewsizemin", 0); + RecommendedCrewSizeMax = SubmarineElement.GetAttributeInt("recommendedcrewsizemax", 0); + RecommendedCrewExperience = SubmarineElement.GetAttributeString("recommendedcrewexperience", "Unknown"); + + //backwards compatibility (use text tags instead of the actual text) + if (RecommendedCrewExperience == "Beginner") + { + RecommendedCrewExperience = "CrewExperienceLow"; + } + else if (RecommendedCrewExperience == "Intermediate") + { + RecommendedCrewExperience = "CrewExperienceMid"; + } + else if (RecommendedCrewExperience == "Experienced") + { + RecommendedCrewExperience = "CrewExperienceHigh"; + } + + RequiredContentPackages.Clear(); + string[] contentPackageNames = SubmarineElement.GetAttributeStringArray("requiredcontentpackages", new string[0]); + foreach (string contentPackageName in contentPackageNames) + { + RequiredContentPackages.Add(contentPackageName); + } + + InitProjectSpecific(); + } + + partial void InitProjectSpecific(); + + public void Dispose() + { + if (savedSubmarines.Contains(this)) { savedSubmarines.Remove(this); } + } + + public bool IsVanillaSubmarine() + { + var vanilla = GameMain.VanillaContent; + if (vanilla != null) + { + var vanillaSubs = vanilla.GetFilesOfType(ContentType.Submarine); + string pathToCompare = FilePath.Replace(@"\", @"/").ToLowerInvariant(); + if (vanillaSubs.Any(sub => sub.Replace(@"\", @"/").ToLowerInvariant() == pathToCompare)) + { + return true; + } + } + return false; + } + + public void StartHashDocTask(XDocument doc) + { + if (hash != null) { return; } + if (hashTask != null) { return; } + + hashTask = new Task(() => + { + hash = new Md5Hash(doc, FilePath); + }); + hashTask.Start(); + } + + public bool HasTag(SubmarineTag tag) + { + return Tags.HasFlag(tag); + } + + public void AddTag(SubmarineTag tag) + { + if (Tags.HasFlag(tag)) return; + + Tags |= tag; + } + + public void RemoveTag(SubmarineTag tag) + { + if (!Tags.HasFlag(tag)) return; + + Tags &= ~tag; + } + + //saving/loading ---------------------------------------------------- + public bool SaveAs(string filePath, MemoryStream previewImage=null) + { + var newElement = new XElement(SubmarineElement.Name, + SubmarineElement.Attributes().Where(a => !string.Equals(a.Name.LocalName, "previewimage", StringComparison.InvariantCultureIgnoreCase)), + SubmarineElement.Elements()); + XDocument doc = new XDocument(newElement); + + if (previewImage != null) + { + doc.Root.Add(new XAttribute("previewimage", Convert.ToBase64String(previewImage.ToArray()))); + } + + try + { + SaveUtil.CompressStringToFile(filePath, doc.ToString()); + } + catch (Exception e) + { + DebugConsole.ThrowError("Saving submarine \"" + filePath + "\" failed!", e); + return false; + } + + return true; + } + + public static void AddToSavedSubs(SubmarineInfo subInfo) + { + savedSubmarines.Add(subInfo); + } + + public static void RefreshSavedSub(string filePath) + { + string fullPath = Path.GetFullPath(filePath); + for (int i = savedSubmarines.Count - 1; i >= 0; i--) + { + if (Path.GetFullPath(savedSubmarines[i].FilePath) == fullPath) + { + savedSubmarines[i].Dispose(); + } + } + + if (File.Exists(filePath)) + { + var subInfo = new SubmarineInfo(filePath); + if (!subInfo.IsFileCorrupted) + { + savedSubmarines.Add(subInfo); + } + savedSubmarines = savedSubmarines.OrderBy(s => s.FilePath ?? "").ToList(); + } + } + + public static void RefreshSavedSubs() + { + var contentPackageSubs = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Submarine); + + for (int i = savedSubmarines.Count - 1; i >= 0; i--) + { + if (File.Exists(savedSubmarines[i].FilePath) && + savedSubmarines[i].LastModifiedTime == File.GetLastWriteTime(savedSubmarines[i].FilePath) && + (Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SavePath) || + contentPackageSubs.Any(fp => Path.GetFullPath(fp.Path).CleanUpPath() == Path.GetFullPath(savedSubmarines[i].FilePath).CleanUpPath()))) + { + continue; + } + savedSubmarines[i].Dispose(); + } + + if (!Directory.Exists(SavePath)) + { + try + { + Directory.CreateDirectory(SavePath); + } + catch (Exception e) + { + DebugConsole.ThrowError("Directory \"" + SavePath + "\" not found and creating the directory failed.", e); + return; + } + } + + List filePaths; + string[] subDirectories; + + try + { + filePaths = Directory.GetFiles(SavePath).ToList(); + subDirectories = Directory.GetDirectories(SavePath); + } + catch (Exception e) + { + DebugConsole.ThrowError("Couldn't open directory \"" + SavePath + "\"!", e); + return; + } + + foreach (string subDirectory in subDirectories) + { + try + { + filePaths.AddRange(Directory.GetFiles(subDirectory).ToList()); + } + catch (Exception e) + { + DebugConsole.ThrowError("Couldn't open subdirectory \"" + subDirectory + "\"!", e); + return; + } + } + + foreach (ContentFile subFile in contentPackageSubs) + { + if (!filePaths.Any(fp => Path.GetFullPath(fp) == Path.GetFullPath(subFile.Path))) + { + filePaths.Add(subFile.Path); + } + } + + filePaths.RemoveAll(p => savedSubmarines.Any(sub => sub.FilePath == p)); + + foreach (string path in filePaths) + { + var subInfo = new SubmarineInfo(path); + if (subInfo.IsFileCorrupted) + { +#if CLIENT + if (DebugConsole.IsOpen) { DebugConsole.Toggle(); } + var deleteSubPrompt = new GUIMessageBox( + TextManager.Get("Error"), + TextManager.GetWithVariable("SubLoadError", "[subname]", subInfo.Name) + "\n" + + TextManager.GetWithVariable("DeleteFileVerification", "[filename]", subInfo.Name), + new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + + string filePath = path; + deleteSubPrompt.Buttons[0].OnClicked += (btn, userdata) => + { + try + { + File.Delete(filePath); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to delete file \"{filePath}\".", e); + } + deleteSubPrompt.Close(); + return true; + }; + deleteSubPrompt.Buttons[1].OnClicked += deleteSubPrompt.Close; +#endif + } + else + { + savedSubmarines.Add(subInfo); + } + } + } + + static readonly string TempFolder = Path.Combine("Submarine", "Temp"); + + public static XDocument OpenFile(string file) + { + return OpenFile(file, out _); + } + + public static XDocument OpenFile(string file, out Exception exception) + { + XDocument doc = null; + string extension = ""; + exception = null; + + try + { + extension = System.IO.Path.GetExtension(file); + } + catch + { + //no file extension specified: try using the default one + file += ".sub"; + } + + if (string.IsNullOrWhiteSpace(extension)) + { + extension = ".sub"; + file += ".sub"; + } + + if (extension == ".sub") + { + Stream stream = null; + try + { + stream = SaveUtil.DecompressFiletoStream(file); + } + catch (FileNotFoundException e) + { + exception = e; + DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (File not found) " + Environment.StackTrace, e); + return null; + } + catch (Exception e) + { + exception = e; + DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed!", e); + return null; + } + + try + { + stream.Position = 0; + doc = XDocument.Load(stream); //ToolBox.TryLoadXml(file); + stream.Close(); + stream.Dispose(); + } + + catch (Exception e) + { + exception = e; + DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (" + e.Message + ")"); + return null; + } + } + else if (extension == ".xml") + { + try + { + ToolBox.IsProperFilenameCase(file); + doc = XDocument.Load(file, LoadOptions.SetBaseUri); + } + + catch (Exception e) + { + exception = e; + DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (" + e.Message + ")"); + return null; + } + } + else + { + DebugConsole.ThrowError("Couldn't load submarine \"" + file + "! (Unrecognized file extension)"); + return null; + } + + return doc; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index c8a2b2291..c3fe670a4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -4,13 +4,14 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using System.Xml.Linq; +using Barotrauma.RuinGeneration; +using Barotrauma.Extensions; namespace Barotrauma { - public enum SpawnType { Path, Human, Enemy, Cargo }; + public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 3, Corpse = 4 }; partial class WayPoint : MapEntity { public static List WayPointList = new List(); @@ -121,9 +122,16 @@ namespace Barotrauma idCardTags = new string[0]; #if CLIENT - if (iconTexture == null) + if (iconSprites == null) { - iconTexture = Sprite.LoadTexture("Content/Map/waypointIcons.png"); + iconSprites = new Dictionary() + { + { SpawnType.Path, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(0,0,128,128)) }, + { SpawnType.Human, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,0,128,128)) }, + { SpawnType.Enemy, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(256,0,128,128)) }, + { SpawnType.Cargo, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(384,0,128,128)) }, + { SpawnType.Corpse, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(512,0,128,128)) } + }; } #endif @@ -148,21 +156,12 @@ namespace Barotrauma return clone; } - public override bool IsMouseOn(Vector2 position) - { -#if CLIENT - if (IsHidden()) return false; -#endif - - return base.IsMouseOn(position); - } - - public static void GenerateSubWaypoints(Submarine submarine) + public static bool GenerateSubWaypoints(Submarine submarine) { if (!Hull.hullList.Any()) { DebugConsole.ThrowError("Couldn't generate waypoints: no hulls found."); - return; + return false; } List existingWaypoints = WayPointList.FindAll(wp => wp.spawnType == SpawnType.Path); @@ -465,6 +464,8 @@ namespace Barotrauma { door.Body.Enabled = false; } + + return true; } private WayPoint FindClosest(int dir, bool horizontalSearch, Vector2 tolerance, Body ignoredBody = null) @@ -520,22 +521,14 @@ namespace Barotrauma if (!wayPoint2.linkedTo.Contains(this)) wayPoint2.linkedTo.Add(this); } - public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, Job assignedJob = null, Submarine sub = null, bool useSyncedRand = false) + public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, Job assignedJob = null, Submarine sub = null, Ruin ruin = null, bool useSyncedRand = false) { - List wayPoints = new List(); - - foreach (WayPoint wp in WayPointList) - { - if (sub != null && wp.Submarine != sub) continue; - if (wp.spawnType != spawnType) continue; - if (assignedJob != null && wp.assignedJob != assignedJob.Prefab) continue; - - wayPoints.Add(wp); - } - - if (!wayPoints.Any()) return null; - - return wayPoints[Rand.Int(wayPoints.Count, (useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced))]; + return WayPointList.GetRandom(wp => + wp.Submarine == sub && + wp.ParentRuin == ruin && + wp.spawnType == spawnType && + (assignedJob == null || (assignedJob != null && wp.assignedJob == assignedJob.Prefab)) + , useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced); } public static WayPoint[] SelectCrewSpawnPoints(List crew, Submarine submarine) @@ -584,7 +577,7 @@ namespace Barotrauma if (assignedWayPoints[i] != null) continue; //everything else failed -> just give a random spawnpoint inside the sub - assignedWayPoints[i] = GetRandom(SpawnType.Human, null, submarine, true); + assignedWayPoints[i] = GetRandom(SpawnType.Human, null, submarine, useSyncedRand: true); } for (int i = 0; i < assignedWayPoints.Length; i++) @@ -654,7 +647,7 @@ namespace Barotrauma { w.assignedJob = JobPrefab.Get(jobIdentifier) ?? - JobPrefab.Prefabs.Find(jp => jp.Name.ToLowerInvariant() == jobIdentifier); + JobPrefab.Prefabs.Find(jp => jp.Name.Equals(jobIdentifier, StringComparison.OrdinalIgnoreCase)); } w.ladderId = (ushort)element.GetAttributeInt("ladders", 0); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index f6e856946..0ece35d95 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -233,7 +233,7 @@ namespace Barotrauma.Networking { writeStream?.Write(msg, 0, msg.Length); } - catch (IOException e) + catch (IOException) { shutDown = true; break; @@ -263,7 +263,7 @@ namespace Barotrauma.Networking lengthBytes[1] = (byte)0; writeStream?.Write(lengthBytes, 0, 2); } - catch (IOException e) + catch (IOException) { shutDown = true; break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index 98c06bf6d..c46241964 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -21,16 +21,27 @@ namespace Barotrauma.Networking private Character character; public Character Character { - get { return character; } + get + { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && (character?.ID ?? 0) != CharacterID) + { + Character = Entity.FindEntityByID(CharacterID) as Character; + } + return character; + } set { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { GameMain.NetworkMember.LastClientListUpdateID++; + if (value != null) + { + CharacterID = value.ID; + } } else { - if (value!=null) + if (value != null) { DebugConsole.NewMessage(value.Name, Microsoft.Xna.Framework.Color.Yellow); } @@ -52,6 +63,8 @@ namespace Barotrauma.Networking } } + public UInt16 CharacterID; + private Vector2 spectate_position; public Vector2? SpectatePos { @@ -98,7 +111,22 @@ namespace Barotrauma.Networking private set; } - public bool InGame; + private bool inGame; + public bool InGame + { + get + { + return inGame; + } + set + { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + { + GameMain.NetworkMember.LastClientListUpdateID++; + } + inGame = value; + } + } public bool HasSpawned; //has the client spawned as a character during the current round private List kickVoters; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs index c1fbc3ac3..f84dc043a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs @@ -51,7 +51,7 @@ namespace Barotrauma.Networking { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "command") continue; + if (!subElement.Name.ToString().Equals("command", StringComparison.OrdinalIgnoreCase)) { continue; } string commandName = subElement.GetAttributeString("name", ""); DebugConsole.Command command = DebugConsole.FindCommand(commandName); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index ed54d4672..5858d7f55 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -22,6 +22,8 @@ namespace Barotrauma.Networking RESPONSE_STARTGAME, //tell the server whether you're ready to start SERVER_COMMAND, //tell the server to end a round or kick/ban someone (special permissions required) + REQUEST_STARTGAMEFINALIZE, //tell the server you're ready to finalize round initialization + ERROR //tell the server that an error occurred } enum ClientNetObject @@ -60,6 +62,7 @@ namespace Barotrauma.Networking QUERY_STARTGAME, //ask the clients whether they're ready to start STARTGAME, //start a new round + STARTGAMEFINALIZE, //finalize round initialization ENDGAME, TRAITOR_MESSAGE, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index a42d9ce20..26b5da467 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -63,15 +63,14 @@ namespace Barotrauma.Networking public Submarine RespawnShuttle { get; private set; } - public RespawnManager(NetworkMember networkMember, Submarine shuttle) - : base(shuttle) + public RespawnManager(NetworkMember networkMember, SubmarineInfo shuttleInfo) + : base(null) { this.networkMember = networkMember; - if (shuttle != null) + if (shuttleInfo != null) { - RespawnShuttle = new Submarine(shuttle.FilePath, shuttle.MD5Hash.Hash, true); - RespawnShuttle.Load(false); + RespawnShuttle = new Submarine(shuttleInfo, true); RespawnShuttle.PhysicsBody.FarseerBody.OnCollision += OnShuttleCollision; //prevent wifi components from communicating between the respawn shuttle and other subs diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index aecaf8011..49d1c1ecf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -138,7 +138,7 @@ namespace Barotrauma.Networking { return (a == null) == (b == null); } - return a.ToString().Equals(b.ToString(), StringComparison.InvariantCulture); + return a.ToString().Equals(b.ToString(), StringComparison.OrdinalIgnoreCase); } } @@ -608,6 +608,13 @@ namespace Barotrauma.Networking set; } + [Serialize(false, true)] + public bool DisableBotConversations + { + get; + set; + } + public float SelectedLevelDifficulty { get { return selectedLevelDifficulty; } @@ -759,7 +766,7 @@ namespace Barotrauma.Networking private set; } - [Serialize(120.0f, true)] + [Serialize(600.0f, true)] public float KickAFKTime { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs index 9a5eada0c..956f8a3dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs @@ -19,6 +19,8 @@ namespace Barotrauma.Steam public const string MetadataFileName = "filelist.xml"; + public const string CopyIndicatorFileName = ".copying"; + private static readonly Dictionary tagCommonness = new Dictionary() { { "submarine", 10 }, @@ -70,27 +72,28 @@ namespace Barotrauma.Steam return Steamworks.SteamClient.Name; } - public static void OverlayCustomURL(string url) - { - if (!isInitialized || !Steamworks.SteamClient.IsValid) - { - return; - } - - Steamworks.SteamFriends.OpenWebOverlay(url); - } - - public static bool UnlockAchievement(string achievementName) + public static bool OverlayCustomURL(string url) { if (!isInitialized || !Steamworks.SteamClient.IsValid) { return false; } - DebugConsole.Log("Unlocked achievement \"" + achievementName + "\""); + Steamworks.SteamFriends.OpenWebOverlay(url); + return true; + } + + public static bool UnlockAchievement(string achievementIdentifier) + { + if (!isInitialized || !Steamworks.SteamClient.IsValid) + { + return false; + } + + DebugConsole.Log("Unlocked achievement \"" + achievementIdentifier + "\""); var achievements = Steamworks.SteamUserStats.Achievements.ToList(); - int achIndex = achievements.FindIndex(ach => ach.Name == achievementName); + int achIndex = achievements.FindIndex(ach => ach.Identifier == achievementIdentifier); bool unlocked = achIndex >= 0 ? achievements[achIndex].Trigger() : false; if (!unlocked) { @@ -99,7 +102,7 @@ namespace Barotrauma.Steam //(discovered[whateverbiomewasentered], kill[withwhateveritem], kill[somemonster] etc) so that we can add //some types of new achievements without the need for client-side changes. #if DEBUG - DebugConsole.NewMessage("Failed to unlock achievement \"" + achievementName + "\"."); + DebugConsole.NewMessage("Failed to unlock achievement \"" + achievementIdentifier + "\"."); #endif } @@ -159,8 +162,9 @@ namespace Barotrauma.Steam { if (string.IsNullOrWhiteSpace(str)) { return 0; } UInt64 retVal; + if (str.StartsWith("STEAM64_", StringComparison.InvariantCultureIgnoreCase)) { str = str.Substring(8); } if (UInt64.TryParse(str, out retVal) && retVal >(1<<52)) { return retVal; } - if (str.ToUpper().IndexOf("STEAM_") != 0) { return 0; } + if (!str.StartsWith("STEAM_", StringComparison.InvariantCultureIgnoreCase)) { return 0; } string[] split = str.Substring(6).Split(':'); if (split.Length != 3) { return 0; } @@ -179,7 +183,11 @@ namespace Barotrauma.Steam UInt64 accountNumber = (uint64 >> 1) & 0x7fffffff; UInt64 universe = (uint64 >> 56) & 0xff; - return "STEAM_" + universe.ToString() + ":" + y.ToString() + ":" + accountNumber.ToString(); + string retVal = "STEAM_" + universe.ToString() + ":" + y.ToString() + ":" + accountNumber.ToString(); + + if (SteamIDStringToUInt64(retVal) != uint64) { return "STEAM64_" + uint64.ToString(); } + + return retVal; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 15de71604..974d8e9a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -250,7 +250,9 @@ namespace Barotrauma /// /// Takes flipping (Dir) into account. /// - public float TransformedRotation => Dir < 0 ? Rotation - MathHelper.Pi : Rotation; + public float TransformedRotation => TransformRotation(Rotation, Dir); + + public static float TransformRotation(float rot, float dir) => dir < 0 ? rot - MathHelper.Pi : rot; public Vector2 LinearVelocity { @@ -351,9 +353,9 @@ namespace Barotrauma public PhysicsBody(LimbParams limbParams, Vector2 position) { - float radius = ConvertUnits.ToSimUnits(limbParams.Radius) * limbParams.Ragdoll.LimbScale; - float height = ConvertUnits.ToSimUnits(limbParams.Height) * limbParams.Ragdoll.LimbScale; - float width = ConvertUnits.ToSimUnits(limbParams.Width) * limbParams.Ragdoll.LimbScale; + float radius = ConvertUnits.ToSimUnits(limbParams.Radius) * limbParams.Scale * limbParams.Ragdoll.LimbScale; + float height = ConvertUnits.ToSimUnits(limbParams.Height) * limbParams.Scale * limbParams.Ragdoll.LimbScale; + float width = ConvertUnits.ToSimUnits(limbParams.Width) * limbParams.Scale * limbParams.Ragdoll.LimbScale; density = limbParams.Density; CreateBody(width, height, radius, density); FarseerBody.BodyType = BodyType.Dynamic; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index be0e35b25..1be30c7bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -36,7 +36,6 @@ namespace Barotrauma public GameScreen() { - Camera cam; #if CLIENT cam = new Camera(); cam.Translate(new Vector2(-10.0f, 50.0f)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 2526e7f8c..37b24658a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -679,7 +679,7 @@ namespace Barotrauma { foreach (XElement subElement in configElement.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "upgrade") { continue; } + if (!subElement.Name.ToString().Equals("upgrade", StringComparison.OrdinalIgnoreCase)) { continue; } var upgradeVersion = new Version(subElement.GetAttributeString("gameversion", "0.0.0.0")); if (savedVersion >= upgradeVersion) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 8daa9feef..3c3f636d2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -570,7 +570,7 @@ namespace Barotrauma { bool hexFailed = true; stringColor = stringColor.Trim(); - if (stringColor[0]=='#') + if (stringColor.Length > 0 && stringColor[0] == '#') { stringColor = stringColor.Substring(1); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs index cc3e4649d..e494349c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs @@ -329,10 +329,10 @@ namespace Barotrauma { foreach (XElement subElement in SourceElement.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "override") + if (subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { string language = subElement.GetAttributeString("language", ""); - if (TextManager.Language.ToLower() == language.ToLower()) + if (TextManager.Language.Equals(language, StringComparison.InvariantCultureIgnoreCase)) { return subElement; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index cddc9c97d..ae67f66f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -1,17 +1,27 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; namespace Barotrauma { class DelayedListElement { - public DelayedEffect Parent; - public Entity Entity; - public Vector2? WorldPosition; - public List Targets; + public readonly DelayedEffect Parent; + public readonly Entity Entity; + public readonly Vector2? WorldPosition; + public readonly List Targets; public float StartTimer; + + public DelayedListElement(DelayedEffect parentEffect, Entity parentEntity, IEnumerable targets, float delay, Vector2? worldPosition) + { + Parent = parentEffect; + Entity = parentEntity; + Targets = new List(targets); + StartTimer = delay; + WorldPosition = worldPosition; + } } class DelayedEffect : StatusEffect { @@ -27,28 +37,18 @@ namespace Barotrauma public override void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition = null) { - if (this.type != type || !HasRequiredItems(entity)) return; - if (!Stackable && DelayList.Any(d => d.Parent == this && d.Targets.FirstOrDefault() == target)) return; - - if (targetIdentifiers != null && !IsValidTarget(target)) return; - if (!HasRequiredConditions(new List() { target })) return; + if (this.type != type || !HasRequiredItems(entity)) { return; } + if (!Stackable && DelayList.Any(d => d.Parent == this && d.Targets.FirstOrDefault() == target)) { return; } + if (targetIdentifiers != null && !IsValidTarget(target)) { return; } + if (!HasRequiredConditions(target.ToEnumerable())) { return; } - DelayedListElement element = new DelayedListElement - { - Parent = this, - StartTimer = delay, - Entity = entity, - WorldPosition = worldPosition, - Targets = new List() { target } - }; - - DelayList.Add(element); + DelayList.Add(new DelayedListElement(this, entity, target.ToEnumerable(), delay, worldPosition)); } public override void Apply(ActionType type, float deltaTime, Entity entity, IEnumerable targets, Vector2? worldPosition = null) { - if (this.type != type || !HasRequiredItems(entity)) return; - if (!Stackable && DelayList.Any(d => d.Parent == this && d.Targets.SequenceEqual(targets))) return; + if (this.type != type || !HasRequiredItems(entity)) { return; } + if (!Stackable && DelayList.Any(d => d.Parent == this && d.Targets.SequenceEqual(targets))) { return; } currentTargets.Clear(); foreach (ISerializableEntity target in targets) @@ -61,18 +61,9 @@ namespace Barotrauma currentTargets.Add(target); } - if (!HasRequiredConditions(currentTargets)) return; + if (!HasRequiredConditions(currentTargets)) { return; } - DelayedListElement element = new DelayedListElement - { - Parent = this, - StartTimer = delay, - Entity = entity, - WorldPosition = worldPosition, - Targets = currentTargets - }; - - DelayList.Add(element); + DelayList.Add(new DelayedListElement(this, entity, currentTargets, delay, worldPosition)); } public static void Update(float deltaTime) @@ -88,7 +79,7 @@ namespace Barotrauma element.StartTimer -= deltaTime; - if (element.StartTimer > 0.0f) continue; + if (element.StartTimer > 0.0f) { continue; } element.Parent.Apply(1.0f, element.Entity, element.Targets, element.WorldPosition); DelayList.Remove(element); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index aeeea37c0..7eb494f1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -21,7 +21,8 @@ namespace Barotrauma HasTag, HasStatusTag, Affliction, - EntityType + EntityType, + LimbType } public enum Comparison @@ -51,7 +52,8 @@ namespace Barotrauma // Only used by attacks public readonly bool TargetSelf; - private readonly string[] afflictionNames = new string[] { "internaldamage", "bleeding", "burn", "oxygenlow", "bloodloss", "pressure", "stun", "husk", "afflictionhusk", "huskinfection" }; + // Only used by conditionals targeting an item (makes the conditional check the item/character whose inventory this item is inside) + public readonly bool TargetContainer; private readonly int cancelStatusEffect; @@ -62,6 +64,7 @@ namespace Barotrauma { case "targetitemcomponent": case "targetself": + case "targetcontainer": return false; default: return true; @@ -132,6 +135,7 @@ namespace Barotrauma } TargetItemComponentName = attribute.Parent.GetAttributeString("targetitemcomponent", ""); + TargetContainer = attribute.Parent.GetAttributeBool("targetcontainer", false); TargetSelf = attribute.Parent.GetAttributeBool("targetself", false); foreach (XElement subElement in attribute.Parent.Elements()) @@ -150,13 +154,9 @@ namespace Barotrauma if (!Enum.TryParse(AttributeName, true, out Type)) { - if (afflictionNames.Any(n => n == AttributeName)) + if (AfflictionPrefab.Prefabs.Any(p => p.Identifier.Equals(AttributeName, StringComparison.OrdinalIgnoreCase))) { Type = ConditionType.Affliction; - if (AttributeName == "husk" || AttributeName == "huskaffliction") - { - AttributeName = "huskinfection"; - } } else { @@ -173,8 +173,7 @@ namespace Barotrauma public bool Matches(ISerializableEntity target) { - string valStr = AttributeValue.ToString(); - + string valStr = AttributeValue.ToString(); switch (Type) { case ConditionType.PropertyValue: @@ -264,34 +263,47 @@ namespace Barotrauma default: return false; } - case ConditionType.Affliction: - if (target == null) { return Operator == OperatorType.NotEquals; } - - Character targetChar = target as Character; - if (target is Limb limb) { targetChar = limb.character; } - if (targetChar != null) + case ConditionType.LimbType: { - var health = targetChar.CharacterHealth; - if (health == null) { return false; } - var affliction = health.GetAffliction(AttributeName); - float afflictionStrength = affliction == null ? 0.0f : affliction.Strength; - if (FloatValue.HasValue) + if (!(target is Limb limb)) { - float value = FloatValue.Value; - switch (Operator) + return false; + } + else + { + return limb.type.ToString().Equals(valStr, StringComparison.OrdinalIgnoreCase); + } + } + case ConditionType.Affliction: + { + if (target == null) { return Operator == OperatorType.NotEquals; } + + Character targetChar = target as Character; + if (target is Limb limb) { targetChar = limb.character; } + if (targetChar != null) + { + var health = targetChar.CharacterHealth; + if (health == null) { return false; } + var affliction = health.GetAffliction(AttributeName); + float afflictionStrength = affliction == null ? 0.0f : affliction.Strength; + if (FloatValue.HasValue) { - case OperatorType.Equals: - return afflictionStrength == value; - case OperatorType.GreaterThan: - return afflictionStrength > value; - case OperatorType.GreaterThanEquals: - return afflictionStrength >= value; - case OperatorType.LessThan: - return afflictionStrength < value; - case OperatorType.LessThanEquals: - return afflictionStrength <= value; - case OperatorType.NotEquals: - return afflictionStrength != value; + float value = FloatValue.Value; + switch (Operator) + { + case OperatorType.Equals: + return afflictionStrength == value; + case OperatorType.GreaterThan: + return afflictionStrength > value; + case OperatorType.GreaterThanEquals: + return afflictionStrength >= value; + case OperatorType.LessThan: + return afflictionStrength < value; + case OperatorType.LessThanEquals: + return afflictionStrength <= value; + case OperatorType.NotEquals: + return afflictionStrength != value; + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index cee21264e..82b3b0c3d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -9,13 +10,29 @@ namespace Barotrauma { class DurationListElement { - public StatusEffect Parent; - public Entity Entity; - public List Targets; + public readonly StatusEffect Parent; + public readonly Entity Entity; + public readonly List Targets; + public Character User { get; private set; } + public float Timer; - public Character User; + + public DurationListElement(StatusEffect parentEffect, Entity parentEntity, IEnumerable targets, float duration, Character user) + { + Parent = parentEffect; + Entity = parentEntity; + Targets = new List(targets); + Timer = duration; + User = user; + } + + public void Reset(float duration, Character newUser) + { + Timer = duration; + User = newUser; + } } - + partial class StatusEffect { [Flags] @@ -54,10 +71,10 @@ namespace Barotrauma //backwards compatibility DebugConsole.ThrowError("Error in StatusEffect config (" + element.ToString() + ") - use item identifier instead of the name."); string itemPrefabName = element.GetAttributeString("name", ""); - ItemPrefab = ItemPrefab.Prefabs.Find(m => m.NameMatches(itemPrefabName) || m.Tags.Contains(itemPrefabName)); + ItemPrefab = ItemPrefab.Prefabs.Find(m => m.NameMatches(itemPrefabName, StringComparison.InvariantCultureIgnoreCase) || m.Tags.Contains(itemPrefabName)); if (ItemPrefab == null) { - DebugConsole.ThrowError("Error in StatusEffect \""+ parentDebugName + "\" - item prefab \"" + itemPrefabName + "\" not found."); + DebugConsole.ThrowError("Error in StatusEffect \"" + parentDebugName + "\" - item prefab \"" + itemPrefabName + "\" not found."); } } else @@ -75,7 +92,7 @@ namespace Barotrauma return; } } - + Speed = element.GetAttributeFloat("speed", 0.0f); Rotation = MathHelper.ToRadians(element.GetAttributeFloat("rotation", 0.0f)); @@ -94,11 +111,16 @@ namespace Barotrauma [Serialize("", false)] public string SpeciesName { get; private set; } + [Serialize(1, false)] public int Count { get; private set; } + [Serialize(0f, false)] public float Spread { get; private set; } + [Serialize("0,0", false)] + public Vector2 Offset { get; private set; } + public CharacterSpawnInfo(XElement element, string parentDebugName) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); @@ -113,7 +135,7 @@ namespace Barotrauma protected HashSet targetIdentifiers; private readonly List requiredItems; - + public readonly string[] propertyNames; private readonly object[] propertyEffects; @@ -121,11 +143,11 @@ namespace Barotrauma private readonly List propertyConditionals; private readonly bool setValue; - + private readonly bool disableDeltaTime; - + private readonly HashSet tags; - + private readonly float duration; private readonly float lifeTime; private float lifeTimer; @@ -137,7 +159,7 @@ namespace Barotrauma public readonly bool Stackable = true; //Can the same status effect be applied several times to the same targets? private readonly int useItemCount; - + private readonly bool removeItem, removeCharacter; public readonly ActionType type = ActionType.OnActive; @@ -151,15 +173,15 @@ namespace Barotrauma public readonly float FireSize; - public readonly LimbType targetLimb; - + public readonly LimbType[] targetLimbs; + public readonly float SeverLimbsProbability; public HashSet TargetIdentifiers { get { return targetIdentifiers; } } - + public List Afflictions { get; @@ -189,7 +211,6 @@ namespace Barotrauma string newTag = tag.Trim(); if (!tags.Contains(newTag)) tags.Add(newTag); } - } } @@ -213,10 +234,15 @@ namespace Barotrauma tags = new HashSet(element.GetAttributeString("tags", "").Split(',')); Range = element.GetAttributeFloat("range", 0.0f); - string targetLimbName = element.GetAttributeString("targetlimb", null); - if (targetLimbName != null) + string[] targetLimbNames = element.GetAttributeStringArray("targetlimb", null) ?? element.GetAttributeStringArray("targetlimbs", null); + if (targetLimbNames != null) { - Enum.TryParse(targetLimbName, out targetLimb); + List targetLimbs = new List(); + foreach (string targetLimbName in targetLimbNames) + { + if (Enum.TryParse(targetLimbName, out LimbType targetLimb)) { targetLimbs.Add(targetLimb); } + } + if (targetLimbs.Count > 0) { this.targetLimbs = targetLimbs.ToArray(); } } IEnumerable attributes = element.Attributes(); @@ -377,7 +403,7 @@ namespace Barotrauma float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength"); Afflictions.Add(afflictionPrefab.Instantiate(afflictionStrength)); - + break; case "reduceaffliction": if (subElement.Attribute("name") != null) @@ -476,10 +502,15 @@ namespace Barotrauma } } - public virtual bool HasRequiredConditions(List targets) + public bool HasRequiredConditions(IEnumerable targets) + { + return HasRequiredConditions(targets, targetingContainer: false); + } + + private bool HasRequiredConditions(IEnumerable targets, bool targetingContainer) { if (!propertyConditionals.Any()) { return true; } - if (requiredItems.Any() && requiredItems.All(ri => ri.MatchOnEmpty) && targets.Count == 0) { return true; } + if (requiredItems.Any() && requiredItems.All(ri => ri.MatchOnEmpty) && !targets.Any()) { return true; } switch (conditionalComparison) { case PropertyConditional.Comparison.Or: @@ -560,33 +591,26 @@ namespace Barotrauma public virtual void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition = null) { - if (this.type != type || !HasRequiredItems(entity)) return; + if (this.type != type || !HasRequiredItems(entity)) { return; } + + if (targetIdentifiers != null && !IsValidTarget(target)) { return; } - if (targetIdentifiers != null && !IsValidTarget(target)) return; - if (duration > 0.0f && !Stackable) { //ignore if not stackable and there's already an identical statuseffect DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.FirstOrDefault() == target); - if (existingEffect != null) - { - existingEffect.Timer = Math.Max(existingEffect.Timer, duration); - existingEffect.User = user; - return; - } + existingEffect?.Reset(Math.Max(existingEffect.Timer, duration), user); + return; } - List targets = new List { target }; - - if (!HasRequiredConditions(targets)) return; - - Apply(deltaTime, entity, targets, worldPosition); + if (!HasRequiredConditions(target.ToEnumerable())) { return; } + Apply(deltaTime, entity, target.ToEnumerable(), worldPosition); } protected readonly List currentTargets = new List(); public virtual void Apply(ActionType type, float deltaTime, Entity entity, IEnumerable targets, Vector2? worldPosition = null) { - if (this.type != type) return; + if (this.type != type) { return; } currentTargets.Clear(); foreach (ISerializableEntity target in targets) @@ -601,7 +625,7 @@ namespace Barotrauma if (targetIdentifiers != null && currentTargets.Count == 0) { return; } - if (!HasRequiredItems(entity) || !HasRequiredConditions(currentTargets)) return; + if (!HasRequiredItems(entity) || !HasRequiredConditions(currentTargets)) { return; } if (duration > 0.0f && !Stackable) { @@ -609,8 +633,7 @@ namespace Barotrauma DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.SequenceEqual(currentTargets)); if (existingEffect != null) { - existingEffect.Timer = Math.Max(existingEffect.Timer, duration); - existingEffect.User = user; + existingEffect?.Reset(Math.Max(existingEffect.Timer, duration), user); return; } } @@ -618,7 +641,7 @@ namespace Barotrauma Apply(deltaTime, entity, currentTargets, worldPosition); } - protected void Apply(float deltaTime, Entity entity, List targets, Vector2? worldPosition = null) + protected void Apply(float deltaTime, Entity entity, IEnumerable targets, Vector2? worldPosition = null) { if (lifeTime > 0) { @@ -637,11 +660,11 @@ namespace Barotrauma } Vector2 position = worldPosition ?? entity.WorldPosition; - if (targetLimb != LimbType.None) + if (targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) { if (entity is Character c) { - Limb limb = c.AnimController.GetLimb(targetLimb); + Limb limb = c.AnimController.GetLimb(l); if (limb != null && !limb.Removed) { position = limb.WorldPosition; @@ -666,13 +689,13 @@ namespace Barotrauma if (item.Removed) continue; item.Use(deltaTime, targetCharacter, targets.FirstOrDefault(t => t is Limb) as Limb); } - } + } if (removeItem) { foreach (var target in targets) { - if (target is Item item) { Entity.Spawner?.AddToRemoveQueue(item); } + if (target is Item item) { Entity.Spawner?.AddToRemoveQueue(item); } } } if (removeCharacter) @@ -685,16 +708,7 @@ namespace Barotrauma if (duration > 0.0f) { - DurationListElement element = new DurationListElement - { - Parent = this, - Timer = duration, - Entity = entity, - Targets = targets, - User = user - }; - - DurationList.Add(element); + DurationList.Add(new DurationListElement(this, entity, targets, duration, user)); } else { @@ -707,11 +721,11 @@ namespace Barotrauma for (int i = 0; i < propertyNames.Length; i++) { - if (target == null || target.SerializableProperties == null || + if (target == null || target.SerializableProperties == null || !target.SerializableProperties.TryGetValue(propertyNames[i], out SerializableProperty property)) continue; ApplyToProperty(target, property, propertyEffects[i], deltaTime); } - } + } } if (explosion != null && entity != null) @@ -732,7 +746,8 @@ namespace Barotrauma character.LastDamageSource = entity; foreach (Limb limb in character.AnimController.Limbs) { - limb.character.DamageLimb(position, limb, new List() { multipliedAffliction }, stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); + if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; } + limb.character.DamageLimb(position, limb, multipliedAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability); //only apply non-limb-specific afflictions to the first limb if (!affliction.Prefab.LimbSpecific) { break; } @@ -741,7 +756,7 @@ namespace Barotrauma else if (target is Limb limb) { if (limb.character.Removed || limb.Removed) { continue; } - limb.character.DamageLimb(position, limb, new List() { multipliedAffliction }, stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); + limb.character.DamageLimb(position, limb, multipliedAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability); } } @@ -776,7 +791,7 @@ namespace Barotrauma var fire = new FireSource(position, hull); fire.Size = new Vector2(FireSize, fire.Size.Y); } - + bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient; if (isNotClient && entity != null && Entity.Spawner != null) //clients are not allowed to spawn entities { @@ -785,15 +800,15 @@ namespace Barotrauma var characters = new List(); for (int i = 0; i < characterSpawnInfo.Count; i++) { - Entity.Spawner.AddToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Server), + Entity.Spawner.AddToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Server) + characterSpawnInfo.Offset, onSpawn: newCharacter => - { - characters.Add(newCharacter); - if (characters.Count == characterSpawnInfo.Count) { - SwarmBehavior.CreateSwarm(characters.Cast()); - } - }); + characters.Add(newCharacter); + if (characters.Count == characterSpawnInfo.Count) + { + SwarmBehavior.CreateSwarm(characters.Cast()); + } + }); } } foreach (ItemSpawnInfo itemSpawnInfo in spawnItems) @@ -804,7 +819,7 @@ namespace Barotrauma Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, position); break; case ItemSpawnInfo.SpawnPositionType.ThisInventory: - { + { if (entity is Character character) { if (character.Inventory != null && character.Inventory.Items.Any(it => it == null)) @@ -843,7 +858,7 @@ namespace Barotrauma Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, containedInventory); break; } - } + } } break; } @@ -853,7 +868,7 @@ namespace Barotrauma ApplyProjSpecific(deltaTime, entity, targets, hull, position); } - partial void ApplyProjSpecific(float deltaTime, Entity entity, List targets, Hull currentHull, Vector2 worldPosition); + partial void ApplyProjSpecific(float deltaTime, Entity entity, IEnumerable targets, Hull currentHull, Vector2 worldPosition); private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, object value, float deltaTime) { @@ -904,8 +919,8 @@ namespace Barotrauma continue; } - element.Targets.RemoveAll(t => - (t is Entity entity && entity.Removed) || + element.Targets.RemoveAll(t => + (t is Entity entity && entity.Removed) || (t is Limb limb && (limb.character == null || limb.character.Removed))); if (element.Targets.Count == 0) { @@ -934,12 +949,12 @@ namespace Barotrauma if (target is Character character) { if (character.Removed) { continue; } - character.AddDamage(character.WorldPosition, new List() { multipliedAffliction }, stun: 0.0f, playSound: false, attacker: element.User); + character.AddDamage(character.WorldPosition, multipliedAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attacker: element.User); } else if (target is Limb limb) { if (limb.character.Removed || limb.Removed) { continue; } - limb.character.DamageLimb(limb.WorldPosition, limb, new List() { multipliedAffliction }, stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: element.User); + limb.character.DamageLimb(limb.WorldPosition, limb, multipliedAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: element.User); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 3f58a0392..66b7a1473 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -222,7 +222,7 @@ namespace Barotrauma 1); } - roundData.Casualties.Add(character); + roundData?.Casualties.Add(character); UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName); if (character.CurrentHull != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs index 23b4288c2..8f57a7858 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs @@ -206,6 +206,12 @@ namespace Barotrauma { lock (mutex) { + if (textPacks == null) + { + DebugConsole.ThrowError($"Failed to get the text \"{textTag}\" (no text packs loaded)."); + return textTag; + } + if (!textPacks.ContainsKey(Language)) { DebugConsole.ThrowError("No text packs available for the selected language (" + Language + ")! Switching to English..."); @@ -216,6 +222,13 @@ namespace Barotrauma } } +#if DEBUG + if (GameMain.Config != null && GameMain.Config.TextManagerDebugModeEnabled) + { + return textTag; + } +#endif + foreach (TextPack textPack in textPacks[Language]) { string text = textPack.Get(textTag); diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs b/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs index ff7efc692..26ad16718 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs @@ -30,7 +30,7 @@ namespace Barotrauma { doc = XMLExtensions.TryLoadXml(filePath); if (doc != null) { break; } - if (filePath.ToLowerInvariant() == "content/texts/englishvanilla.xml") + if (filePath.Equals("content/texts/englishvanilla.xml", StringComparison.OrdinalIgnoreCase)) { //try fixing legacy EnglishVanilla path string newPath = "Content/Texts/English/EnglishVanilla.xml"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 83fe7cb41..30ab6888d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -60,7 +60,7 @@ namespace Barotrauma Directory.CreateDirectory(TempPath); try { - ClearFolder(TempPath, new string[] { GameMain.GameSession.Submarine.FilePath }); + ClearFolder(TempPath, new string[] { GameMain.GameSession.SubmarineInfo.FilePath }); } catch (Exception e) { @@ -69,23 +69,10 @@ namespace Barotrauma try { - if (Submarine.MainSub != null) + if (GameMain.GameSession.SubmarineInfo != null) { - string subPath = Path.Combine(TempPath, Submarine.MainSub.Name + ".sub"); - if (Submarine.Loaded.Contains(Submarine.MainSub)) - { - Submarine.MainSub.FilePath = subPath; - Submarine.MainSub.SaveAs(Submarine.MainSub.FilePath); - } - else if (Submarine.MainSub.FilePath != subPath) - { - if (File.Exists(subPath)) - { - File.Delete(subPath); - } - File.Copy(Submarine.MainSub.FilePath, subPath); - Submarine.MainSub.FilePath = subPath; - } + string subPath = Path.Combine(TempPath, GameMain.GameSession.SubmarineInfo.Name + ".sub"); + GameMain.GameSession.SubmarineInfo.SaveAs(subPath); } } catch (Exception e) @@ -123,7 +110,7 @@ namespace Barotrauma if (doc == null) { return; } string subPath = Path.Combine(TempPath, doc.Root.GetAttributeString("submarine", "")) + ".sub"; - Submarine selectedSub = new Submarine(subPath, ""); + SubmarineInfo selectedSub = new SubmarineInfo(subPath, ""); GameMain.GameSession = new GameSession(selectedSub, filePath, doc); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs index 11f1adaf1..92aa1568d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs @@ -26,22 +26,22 @@ namespace Barotrauma public static void Add(Task task, Action onCompletion) { - AddInternal(task, (Task t, object obj) => { onCompletion(t); }, null); + AddInternal(task, (Task t, object obj) => { onCompletion?.Invoke(t); }, null); } public static void Add(Task task, U userdata, Action onCompletion) where U : class { - AddInternal(task, (Task t, object obj) => { onCompletion(t, (U)obj); }, userdata); + AddInternal(task, (Task t, object obj) => { onCompletion?.Invoke(t, (U)obj); }, userdata); } public static void Add(Task task, Action> onCompletion) { - AddInternal(task, (Task t, object obj) => { onCompletion((Task)t); }, null); + AddInternal(task, (Task t, object obj) => { onCompletion?.Invoke((Task)t); }, null); } public static void Add(Task task, U userdata, Action, U> onCompletion) where U : class { - AddInternal(task, (Task t, object obj) => { onCompletion((Task)t, (U)obj); }, userdata); + AddInternal(task, (Task t, object obj) => { onCompletion?.Invoke((Task)t, (U)obj); }, userdata); } public static void Update() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 6f31de524..e8ff1a8e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -1,13 +1,14 @@ -using System; +using Barotrauma.Extensions; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; -using System.Security.Cryptography; using System.Reflection; +using System.Security.Cryptography; using System.Text; -using Microsoft.Xna.Framework; -using Barotrauma.Networking; -using System.Diagnostics; namespace Barotrauma { @@ -118,6 +119,10 @@ namespace Barotrauma for (int i = 0; i < subDirs.Length; i++) { + if (i == subDirs.Length - 1 && string.IsNullOrEmpty(subDirs[i])) + { + break; + } string enumPath = string.IsNullOrEmpty(filename) ? "./" : filename; List filePaths = Directory.GetFileSystemEntries(enumPath).Select(s => Path.GetFileName(s)).ToList(); if (filePaths.Any(s => s.Equals(subDirs[i], StringComparison.Ordinal))) @@ -147,7 +152,7 @@ namespace Barotrauma public static string RemoveInvalidFileNameChars(string fileName) { - var invalidChars = Path.GetInvalidFileNameChars(); + var invalidChars = Path.GetInvalidFileNameChars().Concat(new char[] {':', ';'}); foreach (char invalidChar in invalidChars) { fileName = fileName.Replace(invalidChar.ToString(), ""); @@ -571,5 +576,26 @@ namespace Barotrauma #endif return path; } + + public static float GetEasing(TransitionMode easing, float t) + { + return easing switch + { + TransitionMode.Smooth => MathUtils.SmoothStep(t), + TransitionMode.Smoother => MathUtils.SmootherStep(t), + TransitionMode.EaseIn => MathUtils.EaseIn(t), + TransitionMode.EaseOut => MathUtils.EaseOut(t), + TransitionMode.Exponential => t * t, + TransitionMode.Linear => t, + _ => t, + }; + } + + public static Rectangle GetWorldBounds(Point center, Point size) + { + Point halfSize = size.Divide(2); + Point topLeft = new Point(center.X - halfSize.X, center.Y + halfSize.Y); + return new Rectangle(topLeft, size); + } } } diff --git a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub new file mode 100644 index 000000000..43935b665 Binary files /dev/null and b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub index 4968e0976..a0e39932e 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 bb3483879..2e9861259 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 7aa3d0166..8b5737bfe 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 950e13ff9..4fa4b3639 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 0fefa6c94..81e94e034 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub and b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub index 7524f6b98..7ffd2212b 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca.sub b/Barotrauma/BarotraumaShared/Submarines/Orca.sub index d51353ce9..a3ca955c9 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Orca.sub and b/Barotrauma/BarotraumaShared/Submarines/Orca.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora.sub b/Barotrauma/BarotraumaShared/Submarines/Remora.sub index 9a91693e5..d3e55e1f8 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 4f355f37f..fa5eeb553 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub and b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub index 2925bd7cb..f6f1ec06f 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 20a6a4c51..d76eaff15 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 4ab81f579..54e6ada20 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,136 @@ +--------------------------------------------------------------------------------------------------------- +v0.9.9.0 +--------------------------------------------------------------------------------------------------------- + +- Added wrecked submarines to levels. +- Reimplemented carrier (now called Thalamus). +- New submarine, Azimuth. +- Option to disable bot conversations in multiplayer. +- Improvements to traitor missions (slightly simpler, with clearer instructions). +- Characters with insufficient skills can fail at mechanical repairs, causing minor injuries. +- Gaps that are inside a hull don't flood the sub, a warning icon is displayed on those gaps in the sub editor. +- Added submarine test mode to the sub editor. +- Moved the entity filter panel and "previously used" panel in the sub editor to the top left corner of the screen. +- Pressing enter after modifying the value of a text field in the sub editor is no longer required. +- "teleportsub" console command can be used to teleport the sub to the position of the cursor. +- The engine vibrates and plays a loud sound when it's damaged to indicate more clearly that it needs repairs. +- Added animated lights to alarm buzzers and sirens. +- Sonar beacon's label can be edited in-game. + +Workshop improvements: +- "Enabling" a mod through the ingame Workshop screen is no longer a thing; subscribing to a mod is all you need to do for the game to install it once it's downloaded. +- Loading preview images and installing mods is done asynchronously (= no lag spikes). +- Added notifications to the main menu to indicate when mods are being downloaded and have been installed. +- Files are automatically added to the Workshop item publish menu as they're added to a to-be-published content package's directory. + +Bugfixes: +- Fixed achievements not unlocking. +- Fixed positions of artifacts spawning on in caves and on level walls getting desynced between the server and clients. +- Fixed new wire node being created at an item client-side if connecting the wire fails due to an electric shock server-side. +- Fixed clients executing console commands they don't have permission to use. +- Fixed enablecheats command not being relayed to server. +- Fixed light component and alarm siren/buzzer states occasionally getting desynced. +- Fixed inability to enter the sub through very small hulls. +- Fixed antibiotics not giving husk infection resistance when shot from a syringe gun. +- Fixed text overflows in the player management panel in the server lobby in languages other than English. +- Fixed searchlight toggle doing nothing. +- Fixed hulls that have minuscule amounts of water in them (too small to be even rendered) being able to trigger InWater effects and water footstep sounds. +- Fixed pumps dicarding the previously received set_targetlevel signal after 0.1 seconds, preventing manual control systems from working if the pumps aren't receiving a continuous set_targetlevel signal. + +--------------------------------------------------------------------------------------------------------- +v0.9.8.0 +--------------------------------------------------------------------------------------------------------- + +Additions and changes: +- Added husked Crawler. +- Re-worked Crawler. +- Moloch: Changed behavior. Added attacks on the tentacles. Fixed some behavior and targeting related issues. +- Improved eating animations. +- Rebind monster attack to "R", because the middle mouse button is now reserved for the command interface. Fixes attacking while controlling a husk or some other monster. +- Calculate the direction from the limbs separately when attacking. Fixes some issues when an attack applies forces on multiple limbs. +- Added damage protection on Hammerhead Matriarch's and Moloch's skirts/tentacles. +- Changed the ai target calculations for characters. +- Adjusted the AI targets for: navterminal, sonar, flashlight, diving suit, underwater scooter, flare, fire extinguisher, and many weapons. +- Husk infection progress is now a bit different: The thresholds are different, and the damage is taken only in the final phase before turning. + The final phase now causes minor twitching in the body. +- Husk faces are now only visible on fully turned (human) husks. +- Fixed rapid turning into husk when taking a a lot of damage from husks. +- Monsters with "flee health threshold" defined now only flee when they are being chased. +- Adjusted Bonethresher's attacks. +- Adjusted the AI priorities for Hammerhead Spawn, Husk, Tigerthresher, Moloch, Mudraptor. +- Mudraptors don't flee anymore when on low health. +- Improvements and fixes on the medic AI. +- Add medical autonomous objectives for also other jobs than medics. +- Improvements and fixes on the extinguish fires behavior. +- The bots don't anymore report about low priority issues while fighting or fleeing. Also fixes bots always reporting theirself as the target for the required treatments. +- Movement speed modifiers now adjust the swimming animations and steering forces too. +- Removed deprecated Molochboss. The boss will be reimplemented later. +- Forced aggressive behavior for creatures that are spawned as mission targets, make an exception with the Matriarch. +- Pressing enter is not required after changing values in text fields in the sub editor. + +Modding: +- Husk affliction mechanics now fully support different species. +- Added "Visibility" parameter for the characters (similar to Noise). +- Added "Attack Force Multiplier" for all limbs. +- Implemented per joint and per limb scaling. +- New LimbType: "Jaw". Used for eating animations. +- Added "Attack Distance" parameter, which is currently only used for Aggressive and PassiveAggressive behaviors. +- Added "Constant Angle" and "Constant Torque" for keeping a limb constantly rotated in a desired orientation. +- Allow to define world forces for the root movement in three phases of the attack: start, middle, end. Set the main collider position to the main limb position when the attack ends to fix the rubberbanding behavior after the attacks. +- Implemented "After Attack Delay": A property for adding a delay after the attack has hit the target, before starting to update the after attack behavior (e.g. falling back). +- Renamed "AttackContext.NotDefined" to "AttackContext.Any". +- Added "Retreat" parameter for attacks. When enabled, the character tries to steer away from the target while attacking it. +- Accept the plural "requireditems" when overriding items. +- Replaced "identifiers" with "items" on "RequiredItems" elements. Both strings are handled in code -> no functional difference. Fixed just for clarity. + +Bugfixes: +- Fixes to crashes when autoupdating Workshop items during startup. +- Fixes to disconnections with the error message "expected old event" when loading a round takes a long time. +- Fixed crashing if the selected core content package contains errors (missing files, invalid XML files). +- Fixed splash screen causing a crash on some Mac systems. +- Fixed corrupted save files causing the game to crash during loading. +- Fixed clients being able to run the "enablecheats" command client-side without the permission to use the command. +- Fixed AI inputs not being synced with clients, preventing clients from seeing when the bots aim/shoot/attack. +- Don't allow using the "flipx" console command while playing online. +- Fixed portrait area & health bar being clickable (despite being hidden) when grabbing another character. +- Fixed a case of characters getting stuck facing one direction. Happened when you switched a character when the command interface was enabled. +- Fixed previous order disappearing from the crew list on resolution change. +- Fixed text not being colored according to message type (default/radio/pm/dead) in the chat input box. +- Fixed "collection was modified" exception if drawing a subinventory causes highlighted inventory slots to change. +- Fixed coilgun/railgun projectiles occasionally hitting the wall next to the turret when launched. +- Fixed text overlays "vibrating" when using the Health Scanner HUD while the sub is moving. +- Fixed level editor not saving level generation parameters that are inside an element. +- Fixed job icons overflowing from the job selection panel when using mods that add more jobs. +- Fixed clients being unable to cancel file transfers. +- Fixed StatusEffects with a delay or a duration "transferring" to another target when the same effect is applied on another entity (for example when using the same stabilozine syringe on multiple characters). +- Fixed particles spawned in StatusEffects only copying the emission angle of the parent item, but not the rotation when CopyEntityAngle is set to true. +- Fixed terminals not sending any outputs server-side. +- Fixed bots unequipping diving suits when they shouldn't. Only happened while doing a low prio objective. +- Fixed retreating bots attacking with adhoc weapons like tools. If the bots retreat, they shouldn't use weapons at all. +- Fixed the priority calculation for the rescue all objective. Should fix bots refusing to treat seriously injured characters and also fixes the priority of the targets. +- Fixed occasional rapid flipping (usually noticeable only in debug draw mode) when a bot has no valid path or the path has ended. +- Fixed bots sometimes not being able to climb ladders because they skip a node too early. Happened especially in Katrull. +- Fixed some cases where bots fail to release the ladders when they should. +- Fixed bots occasionally disgarding an autonomous objective with low priority after starting to follow it. +- Fixed bots ignoring unsafe hulls (= any threats) when they shouldn't and consequently heading into danger. +- Fixed some monster attacks causing internal damage instead of bitewounds. +- Fixed Hammerhead Matriarch's head damage modifier ignoring bitewounds. +- Fixed latched creatures not reacting on being damaged. +- Fixed monsters flipping constantly while swimming. +- Fixed Husks not regenerating health properly. +- Fixed AI targets not being decreased on inactive items. +- Fixed fire extinguisher definition (xml): the identifier and the tag shouldn't be the same string. +- Fixed monsters sometimes moving slower than they should when walking inside. +- Fixed charge indicator not being flipped in horizontally/vertically flipped batteries. +- Fixed fabricators occasionally getting stuck to the active/inactive state client-side when the fabrication is paused due to insufficient power. +- Fixed ancient weapon not cutting through holes in walls. +- Fixed ambient light being brighter in the submarine editor than in-game. +- Fixed walk speed not being affected by speed reductions. +- Fixed all enemies ignoring the speed modifiers. +- Fixed wires appearing as loose items on the floor after saving and reloading if they've been disconnected from both ends. +- Fixed non-stackable status effects with a duration having no effect. +>>>>>>> origin/feature/carrier + --------------------------------------------------------------------------------------------------------- v0.9.7.1 --------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaShared/liblinux_steam_env.so b/Barotrauma/BarotraumaShared/liblinux_steam_env.so new file mode 100644 index 000000000..db4fdec39 Binary files /dev/null and b/Barotrauma/BarotraumaShared/liblinux_steam_env.so differ diff --git a/Barotrauma/BarotraumaShared/serversettings.xml b/Barotrauma/BarotraumaShared/serversettings.xml index 4960cd465..fb58fd6df 100644 --- a/Barotrauma/BarotraumaShared/serversettings.xml +++ b/Barotrauma/BarotraumaShared/serversettings.xml @@ -43,7 +43,7 @@ endvoterequiredratio="0.6" kickvoterequiredratio="0.6" killdisconnectedtime="120" - kickafktime="120" + kickafktime="600" traitoruseratio="True" traitorratio="0.2" karmaenabled="False" diff --git a/Barotrauma/BarotraumaShared/steamclient.so b/Barotrauma/BarotraumaShared/steamclient.so new file mode 100644 index 000000000..214fabf4d Binary files /dev/null and b/Barotrauma/BarotraumaShared/steamclient.so differ diff --git a/Deploy/Linux/DeployLinux.sh b/Deploy/Linux/DeployLinux.sh index 822bdb7f8..c9f7e721b 100644 --- a/Deploy/Linux/DeployLinux.sh +++ b/Deploy/Linux/DeployLinux.sh @@ -3,8 +3,8 @@ cd ../../Barotrauma cd BarotraumaClient -dotnet publish LinuxClient.csproj -c Release --self-contained -r linux-x64 \/p:Platform="x64" +dotnet publish LinuxClient.csproj -c Release -clp:"ErrorsOnly;Summary" --self-contained -r linux-x64 \/p:Platform="x64" cd .. cd BarotraumaServer -dotnet publish LinuxServer.csproj -c Release --self-contained -r linux-x64 \/p:Platform="x64" +dotnet publish LinuxServer.csproj -c Release -clp:"ErrorsOnly;Summary" --self-contained -r linux-x64 \/p:Platform="x64" diff --git a/Deploy/Linux/DeployLinuxServer.sh b/Deploy/Linux/DeployLinuxServer.sh new file mode 100644 index 000000000..4d05a7814 --- /dev/null +++ b/Deploy/Linux/DeployLinuxServer.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +cd ../../Barotrauma/BarotraumaServer +dotnet publish LinuxServer.csproj -c Release --self-contained -r linux-x64 \/p:Platform="x64" diff --git a/Deploy/Linux/DeployLinuxUnstable.sh b/Deploy/Linux/DeployLinuxUnstable.sh index 7362f9057..015ae2cc7 100644 --- a/Deploy/Linux/DeployLinuxUnstable.sh +++ b/Deploy/Linux/DeployLinuxUnstable.sh @@ -3,8 +3,8 @@ cd ../../Barotrauma cd BarotraumaClient -dotnet publish LinuxClient.csproj -c Unstable --self-contained -r linux-x64 \/p:Platform="x64" +dotnet publish LinuxClient.csproj -c Unstable -clp:"ErrorsOnly;Summary" --self-contained -r linux-x64 \/p:Platform="x64" cd .. cd BarotraumaServer -dotnet publish LinuxServer.csproj -c Unstable --self-contained -r linux-x64 \/p:Platform="x64" +dotnet publish LinuxServer.csproj -c Unstable -clp:"ErrorsOnly;Summary" --self-contained -r linux-x64 \/p:Platform="x64" diff --git a/Deploy/Linux/DeployMac.sh b/Deploy/Linux/DeployMac.sh index 7a0c51c02..0bde38073 100644 --- a/Deploy/Linux/DeployMac.sh +++ b/Deploy/Linux/DeployMac.sh @@ -3,8 +3,8 @@ cd ../../Barotrauma cd BarotraumaClient -dotnet publish MacClient.csproj -c Release --self-contained -r osx-x64 \/p:Platform="x64" +dotnet publish MacClient.csproj -c Release -clp:"ErrorsOnly;Summary" --self-contained -r osx-x64 \/p:Platform="x64" cd .. cd BarotraumaServer -dotnet publish MacServer.csproj -c Release --self-contained -r osx-x64 \/p:Platform="x64" +dotnet publish MacServer.csproj -c Release -clp:"ErrorsOnly;Summary" --self-contained -r osx-x64 \/p:Platform="x64" diff --git a/Deploy/Linux/DeployMacUnstable.sh b/Deploy/Linux/DeployMacUnstable.sh index 683ad7973..ecccfe3fc 100644 --- a/Deploy/Linux/DeployMacUnstable.sh +++ b/Deploy/Linux/DeployMacUnstable.sh @@ -3,8 +3,8 @@ cd ../../Barotrauma cd BarotraumaClient -dotnet publish MacClient.csproj -c Unstable --self-contained -r osx-x64 \/p:Platform="x64" +dotnet publish MacClient.csproj -c Unstable -clp:"ErrorsOnly;Summary" --self-contained -r osx-x64 \/p:Platform="x64" cd .. cd BarotraumaServer -dotnet publish MacServer.csproj -c Unstable --self-contained -r osx-x64 \/p:Platform="x64" +dotnet publish MacServer.csproj -c Unstable -clp:"ErrorsOnly;Summary" --self-contained -r osx-x64 \/p:Platform="x64" diff --git a/Deploy/Linux/DeployWindows.sh b/Deploy/Linux/DeployWindows.sh index 8adb6aab6..527841ed4 100644 --- a/Deploy/Linux/DeployWindows.sh +++ b/Deploy/Linux/DeployWindows.sh @@ -3,8 +3,8 @@ cd ../../Barotrauma cd BarotraumaClient -dotnet publish WindowsClient.csproj -c Release --self-contained -r win-x64 \/p:Platform="x64" +dotnet publish WindowsClient.csproj -c Release -clp:"ErrorsOnly;Summary" --self-contained -r win-x64 \/p:Platform="x64" cd .. cd BarotraumaServer -dotnet publish WindowsServer.csproj -c Release --self-contained -r win-x64 \/p:Platform="x64" +dotnet publish WindowsServer.csproj -c Release -clp:"ErrorsOnly;Summary" --self-contained -r win-x64 \/p:Platform="x64" diff --git a/Deploy/Linux/DeployWindowsUnstable.sh b/Deploy/Linux/DeployWindowsUnstable.sh index e37eb630c..ccabb7aee 100644 --- a/Deploy/Linux/DeployWindowsUnstable.sh +++ b/Deploy/Linux/DeployWindowsUnstable.sh @@ -3,8 +3,8 @@ cd ../../Barotrauma cd BarotraumaClient -dotnet publish WindowsClient.csproj -c Unstable --self-contained -r win-x64 \/p:Platform="x64" +dotnet publish WindowsClient.csproj -c Unstable -clp:"ErrorsOnly;Summary" --self-contained -r win-x64 \/p:Platform="x64" cd .. cd BarotraumaServer -dotnet publish WindowsServer.csproj -c Unstable --self-contained -r win-x64 \/p:Platform="x64" +dotnet publish WindowsServer.csproj -c Unstable -clp:"ErrorsOnly;Summary" --self-contained -r win-x64 \/p:Platform="x64" diff --git a/Deploy/Windows/DeployLinux.bat b/Deploy/Windows/DeployLinux.bat index 337966b26..13cebab0c 100644 --- a/Deploy/Windows/DeployLinux.bat +++ b/Deploy/Windows/DeployLinux.bat @@ -3,10 +3,10 @@ cd ../../Barotrauma cd BarotraumaClient -dotnet publish LinuxClient.csproj -c Release --self-contained -r linux-x64 /p:Platform=x64 +dotnet publish LinuxClient.csproj -c Release -clp:ErrorsOnly;Summary --self-contained -r linux-x64 /p:Platform=x64 cd .. cd BarotraumaServer -dotnet publish LinuxServer.csproj -c Release --self-contained -r linux-x64 /p:Platform=x64 +dotnet publish LinuxServer.csproj -c Release -clp:ErrorsOnly;Summary --self-contained -r linux-x64 /p:Platform=x64 PAUSE diff --git a/Deploy/Windows/DeployLinuxUnstable.bat b/Deploy/Windows/DeployLinuxUnstable.bat index eb3279e76..ce7c60bc5 100644 --- a/Deploy/Windows/DeployLinuxUnstable.bat +++ b/Deploy/Windows/DeployLinuxUnstable.bat @@ -3,10 +3,10 @@ cd ../../Barotrauma cd BarotraumaClient -dotnet publish LinuxClient.csproj -c Unstable --self-contained -r linux-x64 /p:Platform=x64 +dotnet publish LinuxClient.csproj -c Unstable -clp:ErrorsOnly;Summary --self-contained -r linux-x64 /p:Platform=x64 cd .. cd BarotraumaServer -dotnet publish LinuxServer.csproj -c Unstable --self-contained -r linux-x64 /p:Platform=x64 +dotnet publish LinuxServer.csproj -c Unstable -clp:ErrorsOnly;Summary --self-contained -r linux-x64 /p:Platform=x64 PAUSE diff --git a/Deploy/Windows/DeployMac.bat b/Deploy/Windows/DeployMac.bat index 6b4b8ade6..c1488f7a3 100644 --- a/Deploy/Windows/DeployMac.bat +++ b/Deploy/Windows/DeployMac.bat @@ -3,10 +3,10 @@ cd ../../Barotrauma cd BarotraumaClient -dotnet publish MacClient.csproj -c Release --self-contained -r osx-x64 /p:Platform=x64 +dotnet publish MacClient.csproj -c Release -clp:ErrorsOnly;Summary --self-contained -r osx-x64 /p:Platform=x64 cd .. cd BarotraumaServer -dotnet publish MacServer.csproj -c Release --self-contained -r osx-x64 /p:Platform=x64 +dotnet publish MacServer.csproj -c Release -clp:ErrorsOnly;Summary --self-contained -r osx-x64 /p:Platform=x64 PAUSE diff --git a/Deploy/Windows/DeployMacUnstable.bat b/Deploy/Windows/DeployMacUnstable.bat index 321211736..771f83526 100644 --- a/Deploy/Windows/DeployMacUnstable.bat +++ b/Deploy/Windows/DeployMacUnstable.bat @@ -3,10 +3,10 @@ cd ../../Barotrauma cd BarotraumaClient -dotnet publish MacClient.csproj -c Unstable --self-contained -r osx-x64 /p:Platform=x64 +dotnet publish MacClient.csproj -c Unstable -clp:ErrorsOnly;Summary --self-contained -r osx-x64 /p:Platform=x64 cd .. cd BarotraumaServer -dotnet publish MacServer.csproj -c Unstable --self-contained -r osx-x64 /p:Platform=x64 +dotnet publish MacServer.csproj -c Unstable -clp:ErrorsOnly;Summary --self-contained -r osx-x64 /p:Platform=x64 PAUSE diff --git a/Deploy/Windows/DeployWindows.bat b/Deploy/Windows/DeployWindows.bat index 93331e334..cdd63185f 100644 --- a/Deploy/Windows/DeployWindows.bat +++ b/Deploy/Windows/DeployWindows.bat @@ -3,10 +3,10 @@ cd ../../Barotrauma cd BarotraumaClient -dotnet publish WindowsClient.csproj -c Release --self-contained -r win-x64 /p:Platform=x64 +dotnet publish WindowsClient.csproj -c Release -clp:ErrorsOnly;Summary --self-contained -r win-x64 /p:Platform=x64 cd .. cd BarotraumaServer -dotnet publish WindowsServer.csproj -c Release --self-contained -r win-x64 /p:Platform=x64 +dotnet publish WindowsServer.csproj -c Release -clp:ErrorsOnly;Summary --self-contained -r win-x64 /p:Platform=x64 PAUSE diff --git a/Deploy/Windows/DeployWindowsServer.bat b/Deploy/Windows/DeployWindowsServer.bat new file mode 100644 index 000000000..865d61d03 --- /dev/null +++ b/Deploy/Windows/DeployWindowsServer.bat @@ -0,0 +1,6 @@ +@ECHO OFF + +cd ../../Barotrauma/BarotraumaServer +dotnet publish WindowsServer.csproj -c Release --self-contained -r win-x64 /p:Platform=x64 + +PAUSE diff --git a/Deploy/Windows/DeployWindowsUnstable.bat b/Deploy/Windows/DeployWindowsUnstable.bat index 99847a16c..d8d4b4f7e 100644 --- a/Deploy/Windows/DeployWindowsUnstable.bat +++ b/Deploy/Windows/DeployWindowsUnstable.bat @@ -3,10 +3,10 @@ cd ../../Barotrauma cd BarotraumaClient -dotnet publish WindowsClient.csproj -c Unstable --self-contained -r win-x64 /p:Platform=x64 +dotnet publish WindowsClient.csproj -c Unstable -clp:ErrorsOnly;Summary --self-contained -r win-x64 /p:Platform=x64 cd .. cd BarotraumaServer -dotnet publish WindowsServer.csproj -c Unstable --self-contained -r win-x64 /p:Platform=x64 +dotnet publish WindowsServer.csproj -c Unstable -clp:ErrorsOnly;Summary --self-contained -r win-x64 /p:Platform=x64 PAUSE diff --git a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj index 6f3e0e832..6ab41d502 100644 --- a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj +++ b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj @@ -31,6 +31,22 @@ git + + 1701;1702;1591;1587 + + + + 1701;1702;1591;1587 + + + + 1701;1702;1591;1587 + + + + 1701;1702;1591;1587 + + diff --git a/Libraries/Facepunch.Steamworks/SteamUgc.cs b/Libraries/Facepunch.Steamworks/SteamUgc.cs index 5420d3796..4d4bf5c75 100644 --- a/Libraries/Facepunch.Steamworks/SteamUgc.cs +++ b/Libraries/Facepunch.Steamworks/SteamUgc.cs @@ -39,6 +39,7 @@ namespace Steamworks ItemInstalled_t.Install(x => { if (x.AppID == SteamClient.AppId) { + GlobalOnItemInstalled?.Invoke(x.PublishedFileId); if (onItemInstalled?.ContainsKey(x.PublishedFileId) ?? false) { onItemInstalled[x.PublishedFileId]?.Invoke(); @@ -92,5 +93,9 @@ namespace Steamworks } private static Dictionary onItemInstalled; + + public static event Action GlobalOnItemInstalled; + + public static uint NumSubscribedItems { get { return Internal.GetNumSubscribedItems(); } } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs b/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs index ddbce0946..6a46244b6 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs @@ -91,6 +91,13 @@ namespace Steamworks.Ugc return this; } + public Editor WithoutTag( string tag ) + { + if (Tags != null && Tags.Contains(tag)) Tags.Remove(tag); + + return this; + } + public async Task SubmitAsync( IProgress progress = null ) { var result = default( PublishResult ); diff --git a/Libraries/Facepunch.Steamworks/Utility/Helpers.cs b/Libraries/Facepunch.Steamworks/Utility/Helpers.cs index 3d1ea74c0..5c585e4df 100644 --- a/Libraries/Facepunch.Steamworks/Utility/Helpers.cs +++ b/Libraries/Facepunch.Steamworks/Utility/Helpers.cs @@ -9,31 +9,36 @@ namespace Steamworks { public const int MaxStringSize = 1024 * 32; + private static object mutex = new object(); private static IntPtr[] MemoryPool; private static int MemoryPoolIndex; public static unsafe IntPtr TakeMemory() { - if ( MemoryPool == null ) + IntPtr take = IntPtr.Zero; + lock (mutex) { - // - // The pool has 5 items. This should be safe because we shouldn't really - // ever be using more than 2 memory pools - // - MemoryPool = new IntPtr[5]; + if (MemoryPool == null) + { + // + // The pool has 5 items. This should be safe because we shouldn't really + // ever be using more than 2 memory pools + // + MemoryPool = new IntPtr[5]; - for ( int i = 0; i < MemoryPool.Length; i++ ) - MemoryPool[i] = Marshal.AllocHGlobal( MaxStringSize ); + for (int i = 0; i < MemoryPool.Length; i++) + MemoryPool[i] = Marshal.AllocHGlobal(MaxStringSize); + } + + MemoryPoolIndex++; + if (MemoryPoolIndex >= MemoryPool.Length) + MemoryPoolIndex = 0; + + take = MemoryPool[MemoryPoolIndex]; + + ((byte*)take)[0] = 0; } - MemoryPoolIndex++; - if ( MemoryPoolIndex >= MemoryPool.Length ) - MemoryPoolIndex = 0; - - var take = MemoryPool[MemoryPoolIndex]; - - ((byte*)take)[0] = 0; - return take; } diff --git a/Libraries/Lidgren.Network/Lidgren.NetStandard.csproj b/Libraries/Lidgren.Network/Lidgren.NetStandard.csproj index 66cb930fb..3e265503b 100644 --- a/Libraries/Lidgren.Network/Lidgren.NetStandard.csproj +++ b/Libraries/Lidgren.Network/Lidgren.NetStandard.csproj @@ -10,6 +10,22 @@ AnyCPU;x64 + + 1701;1702;3021 + + + + 1701;1702;3021 + + + + 1701;1702;3021 + + + + 1701;1702;3021 + + diff --git a/Libraries/Lidgren.Network/NetBuffer.cs b/Libraries/Lidgren.Network/NetBuffer.cs index 34c783fde..15903bb73 100644 --- a/Libraries/Lidgren.Network/NetBuffer.cs +++ b/Libraries/Lidgren.Network/NetBuffer.cs @@ -76,7 +76,7 @@ namespace Lidgren.Network MethodInfo[] methods = typeof(NetIncomingMessage).GetMethods(BindingFlags.Instance | BindingFlags.Public); foreach (MethodInfo mi in methods) { - if (mi.GetParameters().Length == 0 && mi.Name.StartsWith("Read", StringComparison.InvariantCulture) && mi.Name.Substring(4) == mi.ReturnType.Name) + if (mi.GetParameters().Length == 0 && mi.Name.StartsWith("Read", StringComparison.OrdinalIgnoreCase) && mi.Name.Substring(4) == mi.ReturnType.Name) { s_readMethods[mi.ReturnType] = mi; } @@ -86,7 +86,7 @@ namespace Lidgren.Network methods = typeof(NetOutgoingMessage).GetMethods(BindingFlags.Instance | BindingFlags.Public); foreach (MethodInfo mi in methods) { - if (mi.Name.Equals("Write", StringComparison.InvariantCulture)) + if (mi.Name.Equals("Write", StringComparison.OrdinalIgnoreCase)) { ParameterInfo[] pis = mi.GetParameters(); if (pis.Length == 1) diff --git a/Libraries/Lidgren.Network/NetUtility.cs b/Libraries/Lidgren.Network/NetUtility.cs index 349d1150b..0c437ce7f 100644 --- a/Libraries/Lidgren.Network/NetUtility.cs +++ b/Libraries/Lidgren.Network/NetUtility.cs @@ -401,7 +401,7 @@ namespace Lidgren.Network { if (j >= h) { - if (string.Compare(list[j - h].Name, tmp.Name, StringComparison.InvariantCulture) > 0) + if (string.Compare(list[j - h].Name, tmp.Name, StringComparison.OrdinalIgnoreCase) > 0) { list[j] = list[j - h]; j -= h; diff --git a/Libraries/SharpFont/Source/SharpFont/SharpFont.NetStandard.csproj b/Libraries/SharpFont/Source/SharpFont/SharpFont.NetStandard.csproj index f1c3685fa..6f2368424 100644 --- a/Libraries/SharpFont/Source/SharpFont/SharpFont.NetStandard.csproj +++ b/Libraries/SharpFont/Source/SharpFont/SharpFont.NetStandard.csproj @@ -20,6 +20,7 @@ TRACE;DEBUG;SHARPFONT_PORTABLE true + 1701;1702;3021 @@ -30,6 +31,7 @@ TRACE;SHARPFONT_PORTABLE true + 1701;1702;3021 diff --git a/Libraries/XNATypes/Rectangle.cs b/Libraries/XNATypes/Rectangle.cs index f8a73be11..c9c7b77a2 100644 --- a/Libraries/XNATypes/Rectangle.cs +++ b/Libraries/XNATypes/Rectangle.cs @@ -375,6 +375,15 @@ namespace Microsoft.Xna.Framework Height += (int)verticalAmount * 2; } + /// + /// Adjusts the edges of this by specified horizontal and vertical amounts. + /// + /// Value to adjust the edges. + public void Inflate(Vector2 amount) + { + Inflate(amount.X, amount.Y); + } + /// /// Gets whether or not the other intersects with this rectangle. /// diff --git a/Libraries/linux_steam_env/linux_steam_env.c b/Libraries/linux_steam_env/linux_steam_env.c new file mode 100644 index 000000000..149810be8 --- /dev/null +++ b/Libraries/linux_steam_env/linux_steam_env.c @@ -0,0 +1,7 @@ +#include + +void setLinuxEnv() +{ + putenv("SteamAppId=602960"); + putenv("SteamGameId=602960"); +} diff --git a/Libraries/webm_mem_playback/webm_mem_playback/Makefile.macos b/Libraries/webm_mem_playback/webm_mem_playback/Makefile.macos new file mode 100644 index 000000000..f9c8759fa --- /dev/null +++ b/Libraries/webm_mem_playback/webm_mem_playback/Makefile.macos @@ -0,0 +1,15 @@ +CXX=clang++ +CXXFLAGS= -std=c++11 -O3 -fPIC -Wall -I../libvpx_x64_macos -I../libwebm_x64_macos -I../opus/include + + +webm_mem_playback_x64: Exports.o AudioDecoder.o Video.o + $(CXX) $(CXXFLAGS) -shared -L../opus_x64_macos/.libs/ -L../libvpx_x64_macos/ -L../libwebm_x64_macos/ -Wl -lopus -lvpx -lwebm -lpthread -Wl -o libwebm_mem_playback_x64.dylib Exports.o AudioDecoder.o Video.o + +Exports.o: Exports.cpp Exports.h Video.h + $(CXX) $(CXXFLAGS) -c Exports.cpp + +Video.o: Video.cpp AudioDecoder.h Video.h + $(CXX) $(CXXFLAGS) -c Video.cpp + +AudioDecoder.o: AudioDecoder.cpp AudioDecoder.h + $(CXX) $(CXXFLAGS) -c AudioDecoder.cpp diff --git a/WindowsSolution.sln b/WindowsSolution.sln index eb92b3c70..9c1bdeae7 100644 --- a/WindowsSolution.sln +++ b/WindowsSolution.sln @@ -40,6 +40,9 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpFont.NetStandard", "Libraries\SharpFont\Source\SharpFont\SharpFont.NetStandard.csproj", "{6911872D-40EF-400C-B0A1-9985A19ED488}" EndProject Global + GlobalSection(SharedMSBuildProjectFiles) = preSolution + Libraries\GameAnalytics\GA-SDK-MONO-SHARED\GA-SDK-MONO-SHARED.projitems*{95c4d59d-9be4-4278-b4f8-46c0ba1a3916}*SharedItemsImports = 5 + EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 Release|x64 = Release|x64