diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 0f93c19b5..ff196c018 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -9,7 +9,7 @@ using System.Linq; namespace Barotrauma { - class CharacterHUD + partial class CharacterHUD { const float BossHealthBarDuration = 120.0f; @@ -99,8 +99,8 @@ namespace Barotrauma } } - public static bool ShouldRecreateHudTexts { get; set; } = true; - private static bool heldDownShiftWhenGotHudTexts; + public static bool RecreateHudTexts { get; set; } = true; + private static bool lastHudTextsContextual; private static float timeHealthWindowClosed; public static bool IsCampaignInterfaceOpen => @@ -218,7 +218,7 @@ namespace Barotrauma if (focusedItemOverlayTimer <= 0.0f) { focusedItem = null; - ShouldRecreateHudTexts = true; + RecreateHudTexts = true; } } } @@ -340,7 +340,7 @@ namespace Barotrauma if (focusedItem != character.FocusedItem) { focusedItemOverlayTimer = Math.Min(1.0f, focusedItemOverlayTimer); - ShouldRecreateHudTexts = true; + RecreateHudTexts = true; } focusedItem = character.FocusedItem; } @@ -364,14 +364,14 @@ namespace Barotrauma if (!GUI.DisableItemHighlights && !Inventory.DraggingItemToWorld) { - bool shiftDown = PlayerInput.IsShiftDown(); - if (ShouldRecreateHudTexts || heldDownShiftWhenGotHudTexts != shiftDown) + bool hudTextsContextual = PlayerInput.IsShiftDown(); + if (RecreateHudTexts || lastHudTextsContextual != hudTextsContextual) { - ShouldRecreateHudTexts = true; - heldDownShiftWhenGotHudTexts = shiftDown; + RecreateHudTexts = true; + lastHudTextsContextual = hudTextsContextual; } - var hudTexts = focusedItem.GetHUDTexts(character, ShouldRecreateHudTexts); - ShouldRecreateHudTexts = false; + var hudTexts = focusedItem.GetHUDTexts(character, RecreateHudTexts); + RecreateHudTexts = false; int dir = Math.Sign(focusedItem.WorldPosition.X - character.WorldPosition.X); @@ -790,5 +790,25 @@ namespace Barotrauma Vector2 drawPos = objectiveEntity.Entity.WorldPosition;// + Vector2.UnitX * objectiveEntity.Sprite.size.X * 1.5f; GUI.DrawIndicator(spriteBatch, drawPos, cam, 100.0f, objectiveEntity.Sprite, objectiveEntity.Color * iconAlpha); } + + static partial void RecreateHudTextsIfControllingProjSpecific(Character character) + { + if (character == Character.Controlled) + { + RecreateHudTexts = true; + } + } + + static partial void RecreateHudTextsIfFocusedProjSpecific(params Item[] items) + { + foreach (var item in items) + { + if (item == Character.Controlled?.FocusedItem) + { + RecreateHudTexts = true; + break; + } + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index a012d1595..e95326468 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -30,7 +30,7 @@ namespace Barotrauma public void ClientExecute(string[] args) { - bool allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is EditorScreen); + bool allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is { IsEditor: true }); if (!allowCheats && !CheatsEnabled && IsCheat) { NewMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + names[0] + "\".", Color.Red); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs index 42fe6d8b3..354ef4180 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs @@ -269,6 +269,8 @@ namespace Barotrauma selectedTalents = info.GetUnlockedTalentsInTree().ToHashSet(); + var specializationCount = tree.TalentSubTrees.Count(t => t.Type == TalentTreeType.Specialization); + List subTreeNames = new List(); foreach (var subTree in tree.TalentSubTrees) { @@ -310,7 +312,7 @@ namespace Barotrauma for (int i = 0; i < optionAmount; i++) { TalentOption option = subTree.TalentOptionStages[i]; - CreateTalentOption(subTreeLayoutGroup, subTree, i, option, info); + CreateTalentOption(subTreeLayoutGroup, subTree, i, option, info, specializationCount); } subTreeLayoutGroup.RectTransform.Resize(new Point(subTreeLayoutGroup.Rect.Width, subTreeLayoutGroup.Children.Sum(c => c.Rect.Height + subTreeLayoutGroup.AbsoluteSpacing))); @@ -327,7 +329,12 @@ namespace Barotrauma var specializationList = GetSpecializationList(); //resize (scale up) children if there's less than 3 of them to make them cover the whole width of the menu specializationList.Content.RectTransform.Resize(new Point(specializationList.Content.Children.Sum(static c => c.Rect.Width), specializationList.Rect.Height), - resizeChildren: specializationList.Content.Children.Count() < 3); + resizeChildren: specializationCount < 3); + //make room for scrollbar if there's more than the default amount of specializations + if (specializationCount > 3) + { + specializationList.RectTransform.MinSize = new Point(specializationList.Rect.Width, specializationList.Content.Rect.Height + (int)(specializationList.ScrollBar.Rect.Height * 0.9f)); + } GUITextBlock.AutoScaleAndNormalize(subTreeNames); @@ -337,17 +344,17 @@ namespace Barotrauma { return specList; } - GUIListBox newSpecializationList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), mainList.Content.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null); return newSpecializationList; } } - private void CreateTalentOption(GUIComponent parent, TalentSubTree subTree, int index, TalentOption talentOption, CharacterInfo info) + private void CreateTalentOption(GUIComponent parent, TalentSubTree subTree, int index, TalentOption talentOption, CharacterInfo info, int specializationCount) { int elementPadding = GUI.IntScale(8); + int height = GUI.IntScale((GameMain.GameSession?.Campaign == null ? 65 : 60) * (specializationCount > 3 ? 0.97f : 1.0f)); GUIFrame talentOptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.01f), parent.RectTransform, anchor: Anchor.TopCenter) - { MinSize = new Point(0, GUI.IntScale(65)) }, style: null); + { MinSize = new Point(0, height) }, style: null); Point talentFrameSize = talentOptionFrame.RectTransform.NonScaledSize; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs index eed8abac4..9da10c685 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -1,6 +1,7 @@ #nullable enable using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Barotrauma.Networking; using Microsoft.Xna.Framework; @@ -117,7 +118,7 @@ namespace Barotrauma private void UpdateBar() { double elapsedTime = (DateTime.Now - startTime).TotalSeconds; - if (msgBox != null && !msgBox.Closed && GUIMessageBox.MessageBoxes.Contains(msgBox)) + if (msgBox is { Closed: false } && GUIMessageBox.MessageBoxes.Contains(msgBox)) { if (msgBox.FindChild(TimerData, true) is GUIProgressBar bar) { @@ -129,7 +130,7 @@ namespace Barotrauma int second = (int)Math.Ceiling(elapsedTime); if (second > lastSecond) { - if (msgBox != null && !msgBox.Closed) + if (msgBox is { Closed: false }) { SoundPlayer.PlayUISound(GUISoundType.PopupMenu); } @@ -137,6 +138,19 @@ namespace Barotrauma } } + private static void CloseLingeringPopups() + { + foreach (GUIComponent box in GUIMessageBox.MessageBoxes.ToImmutableArray()) + { + if (box is not GUIMessageBox msgBox) { continue; } + + if (msgBox.UserData is PromptData or ResultData) + { + msgBox.Close(); + } + } + } + public static void ClientRead(IReadMessage inc) { ReadyCheckState state = (ReadyCheckState)inc.ReadByte(); @@ -154,6 +168,8 @@ namespace Barotrauma switch (state) { case ReadyCheckState.Start: + CloseLingeringPopups(); + bool isOwn = false; byte authorId = 0; @@ -175,8 +191,8 @@ namespace Barotrauma clients.Add(inc.ReadByte()); } - ReadyCheck rCheck = new ReadyCheck(clients, - DateTimeOffset.FromUnixTimeSeconds(startTime).LocalDateTime, + ReadyCheck rCheck = new ReadyCheck(clients, + DateTimeOffset.FromUnixTimeSeconds(startTime).LocalDateTime, DateTimeOffset.FromUnixTimeSeconds(endTime).LocalDateTime); crewManager.ActiveReadyCheck = rCheck; @@ -224,7 +240,7 @@ namespace Barotrauma if (IsFinished) { return; } IsFinished = true; - int readyCount = Clients.Count(pair => pair.Value == ReadyStatus.Yes); + int readyCount = Clients.Count(static pair => pair.Value == ReadyStatus.Yes); int totalCount = Clients.Count; GameMain.Client.AddChatMessage(ChatMessage.Create(string.Empty, readyCheckStatus(readyCount, totalCount).Value, ChatMessageType.Server, null)); } @@ -238,31 +254,29 @@ namespace Barotrauma if (resultsBox == null || resultsBox.Closed || !GUIMessageBox.MessageBoxes.Contains(resultsBox)) { return; } - if (resultsBox.Content.FindChild(UserListData) is GUIListBox userList) + if (resultsBox.Content.FindChild(UserListData) is not GUIListBox userList) { return; } + + // for some reason FindChild doesn't work here? + foreach (GUIComponent child in userList.Content.Children) { - // for some reason FindChild doesn't work here? - foreach (GUIComponent child in userList.Content.Children) + if (child.UserData is not byte b || b != id) { continue; } + + if (child.GetChild().FindChild(ReadySpriteData) is not GUIImage image) { continue; } + + string style; + switch (status) { - if (!(child.UserData is byte b) || b != id) { continue; } - - if (child.GetChild().FindChild(ReadySpriteData) is GUIImage image) - { - string style; - switch (status) - { - case ReadyStatus.Yes: - style = "MissionCompletedIcon"; - break; - case ReadyStatus.No: - style = "MissionFailedIcon"; - break; - default: - return; - } - - image.ApplyStyle(GUIStyle.GetComponentStyle(style)); - } + case ReadyStatus.Yes: + style = "MissionCompletedIcon"; + break; + case ReadyStatus.No: + style = "MissionFailedIcon"; + break; + default: + return; } + + image.ApplyStyle(GUIStyle.GetComponentStyle(style)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 473e52099..3debe66f7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -327,17 +327,20 @@ namespace Barotrauma.Items.Components var item1 = c1.GUIComponent.UserData as FabricationRecipe; var item2 = c2.GUIComponent.UserData as FabricationRecipe; - int itemPlacement1 = FabricationDegreeOfSuccess(character, item1.RequiredSkills) >= 0.5f ? 0 : -1; - int itemPlacement2 = FabricationDegreeOfSuccess(character, item2.RequiredSkills) >= 0.5f ? 0 : -1; - - itemPlacement1 += item1.RequiresRecipe && !character.HasRecipeForItem(item1.TargetItem.Identifier) ? -2 : 0; - itemPlacement2 += item2.RequiresRecipe && !character.HasRecipeForItem(item2.TargetItem.Identifier) ? -2 : 0; - + int itemPlacement1 = calculatePlacement(item1); + int itemPlacement2 = calculatePlacement(item2); if (itemPlacement1 != itemPlacement2) { return itemPlacement1 > itemPlacement2 ? -1 : 1; } + int calculatePlacement(FabricationRecipe recipe) + { + int placement = FabricationDegreeOfSuccess(character, recipe.RequiredSkills) >= 0.5f ? 0 : -1; + placement += recipe.RequiresRecipe && !AnyOneHasRecipeForItem(character, recipe.TargetItem) ? -2 : 0; + return placement; + } + return string.Compare(item1.DisplayName.Value, item2.DisplayName.Value); }); @@ -372,7 +375,9 @@ namespace Barotrauma.Items.Components AutoScaleHorizontal = true, CanBeFocused = false }; - var firstRequiresRecipe = itemList.Content.Children.FirstOrDefault(c => c.UserData is FabricationRecipe fabricableItem && (fabricableItem.RequiresRecipe && !character.HasRecipeForItem(fabricableItem.TargetItem.Identifier))); + var firstRequiresRecipe = itemList.Content.Children.FirstOrDefault(c => + c.UserData is FabricationRecipe fabricableItem && + fabricableItem.RequiresRecipe && !AnyOneHasRecipeForItem(character, fabricableItem.TargetItem)); if (firstRequiresRecipe != null) { requiresRecipeText.RectTransform.RepositionChildInHierarchy(itemList.Content.RectTransform.GetChildIndex(firstRequiresRecipe.RectTransform)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 3fff886f3..3a4b44dc9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -144,6 +144,9 @@ namespace Barotrauma.Items.Components partial class MiniMap : Powered { + private Dictionary hullDatas; + private DateTime resetDataTime; + private GUIFrame submarineContainer; private GUIFrame? hullInfoFrame; @@ -226,6 +229,8 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific() { + hullDatas = new Dictionary(); + SetDefaultMode(); noPowerTip = TextManager.Get("SteeringNoPowerTip"); @@ -551,6 +556,34 @@ namespace Barotrauma.Items.Components CreateHUD(); } + //reset data if we haven't received anything in a while + //(so that outdated hull info won't be shown if detectors stop sending signals) + if (DateTime.Now > resetDataTime) + { + foreach (HullData hullData in hullDatas.Values) + { + if (!hullData.Distort) + { + if (Timing.TotalTime > hullData.LastOxygenDataTime + 1.0) { hullData.ReceivedOxygenAmount = null; } + if (Timing.TotalTime > hullData.LastWaterDataTime + 1.0) { hullData.ReceivedWaterAmount = null; } + } + } + resetDataTime = DateTime.Now + new TimeSpan(0, 0, 1); + } + + if (cardRefreshTimer > cardRefreshDelay) + { + if (item.Submarine is { } sub) + { + UpdateIDCards(sub); + } + cardRefreshTimer = 0; + } + else + { + cardRefreshTimer += deltaTime; + } + if (scissorComponent != null) { if (PlayerInput.PrimaryMouseButtonDown() && currentMode != MiniMapMode.HullStatus) @@ -1736,6 +1769,67 @@ namespace Barotrauma.Items.Components return new MiniMapHullData(scaledPolygon, worldRect, parentRect.Size, snappedRectangles, hullRefs.ToImmutableArray()); } + public override void ReceiveSignal(Signal signal, Connection connection) + { + Item source = signal.source; + if (source == null || source.CurrentHull == null) { return; } + + Hull sourceHull = source.CurrentHull; + if (!hullDatas.TryGetValue(sourceHull, out HullData? hullData)) + { + hullData = new HullData(); + hullDatas.Add(sourceHull, hullData); + } + + if (hullData.Distort) { return; } + + switch (connection.Name) + { + case "water_data_in": + //cheating a bit because water detectors don't actually send the water level + bool fromWaterDetector = source.GetComponent() != null; + hullData.ReceivedWaterAmount = null; + hullData.LastWaterDataTime = Timing.TotalTime; + if (fromWaterDetector) + { + hullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(sourceHull); + } + foreach (var linked in sourceHull.linkedTo) + { + if (linked is not Hull linkedHull) { continue; } + if (!hullDatas.TryGetValue(linkedHull, out HullData? linkedHullData)) + { + linkedHullData = new HullData(); + hullDatas.Add(linkedHull, linkedHullData); + } + linkedHullData.ReceivedWaterAmount = null; + if (fromWaterDetector) + { + linkedHullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(linkedHull); + } + } + break; + case "oxygen_data_in": + if (!float.TryParse(signal.value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float oxy)) + { + oxy = Rand.Range(0.0f, 100.0f); + } + hullData.ReceivedOxygenAmount = oxy; + hullData.LastOxygenDataTime = Timing.TotalTime; + foreach (var linked in sourceHull.linkedTo) + { + if (linked is not Hull linkedHull) { continue; } + if (!hullDatas.TryGetValue(linkedHull, out HullData? linkedHullData)) + { + linkedHullData = new HullData(); + hullDatas.Add(linkedHull, linkedHullData); + } + linkedHullData.ReceivedOxygenAmount = oxy; + } + break; + } + } + protected override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index f121895d5..9f5e58d99 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs @@ -396,7 +396,9 @@ namespace Barotrauma.Items.Components ToolTip = TextManager.Get("reactor.temperatureboostup"), OnClicked = (_, __) => { - applyTemperatureBoost(TemperatureBoostAmount, temperatureBoostSoundUp); + unsentChanges = true; + sendUpdateTimer = 0.0f; + ApplyTemperatureBoost(TemperatureBoostAmount); return true; } }; @@ -407,25 +409,13 @@ namespace Barotrauma.Items.Components ToolTip = TextManager.Get("reactor.temperatureboostdown"), OnClicked = (_, __) => { - applyTemperatureBoost(-TemperatureBoostAmount, temperatureBoostSoundDown); + unsentChanges = true; + sendUpdateTimer = 0.0f; + ApplyTemperatureBoost(-TemperatureBoostAmount); return true; } }; - void applyTemperatureBoost(float amount, RoundSound sound) - { - temperatureBoost = amount; - if (sound != null) - { - SoundPlayer.PlaySound( - sound.Sound, - item.WorldPosition, - sound.Volume, - sound.Range, - hullGuess: item.CurrentHull); - } - } - var graphArea = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 1.0f), bottomRightArea.RectTransform)) { Stretch = true, @@ -471,6 +461,24 @@ namespace Barotrauma.Items.Components } }; } + private void ApplyTemperatureBoost(float amount) + { + if (Math.Abs(temperatureBoost) <= TemperatureBoostAmount * 0.9f && + Math.Abs(amount) > TemperatureBoostAmount * 0.9f) + { + var sound = amount > 0 ? temperatureBoostSoundUp : temperatureBoostSoundDown; + if (sound != null) + { + SoundPlayer.PlaySound( + sound.Sound, + item.WorldPosition, + sound.Volume, + sound.Range, + hullGuess: item.CurrentHull); + } + } + temperatureBoost = amount; + } private void InitInventoryUI() { @@ -895,6 +903,7 @@ namespace Barotrauma.Items.Components msg.WriteBoolean(PowerOn); msg.WriteRangedSingle(TargetFissionRate, 0.0f, 100.0f, 8); msg.WriteRangedSingle(TargetTurbineOutput, 0.0f, 100.0f, 8); + msg.WriteRangedSingle(temperatureBoost, -TemperatureBoostAmount, TemperatureBoostAmount, 8); correctionTimer = CorrectionDelay; } @@ -903,7 +912,7 @@ namespace Barotrauma.Items.Components { if (correctionTimer > 0.0f) { - StartDelayedCorrection(msg.ExtractBits(1 + 1 + 8 + 8 + 8 + 8), sendingTime); + StartDelayedCorrection(msg.ExtractBits(1 + 1 + 8 + 8 + 8 + 8 + 8), sendingTime); return; } @@ -913,6 +922,7 @@ namespace Barotrauma.Items.Components TargetFissionRate = msg.ReadRangedSingle(0.0f, 100.0f, 8); TargetTurbineOutput = msg.ReadRangedSingle(0.0f, 100.0f, 8); degreeOfSuccess = msg.ReadRangedSingle(0.0f, 1.0f, 8); + ApplyTemperatureBoost(msg.ReadRangedSingle(-TemperatureBoostAmount, TemperatureBoostAmount, 8)); if (Math.Abs(FissionRateScrollBar.BarScroll - TargetFissionRate / 100.0f) > 0.01f) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index bf417f645..587951085 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -61,6 +61,7 @@ namespace Barotrauma.Items.Components private float displayBorderSize; private List sonarBlips; + private readonly HashSet prevBlips = new HashSet(); private float prevPassivePingRadius; @@ -812,24 +813,33 @@ namespace Barotrauma.Items.Components if (distSqr > t.SoundRange * t.SoundRange * 2) { continue; } float dist = (float)Math.Sqrt(distSqr); - if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500 && t.IsWithinSector(transducerCenter)) + if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500) { - int prevBlipCount = sonarBlips.Count; + prevBlips.Clear(); + foreach (var blip in sonarBlips) + { + prevBlips.Add(blip); + } + Ping(t.WorldPosition, transducerCenter, - Math.Min(t.SoundRange, range * 0.5f) * displayScale, 0, displayScale, Math.Min(t.SoundRange, range * 0.5f), + t.SoundRange * displayScale, 0, displayScale, range, passive: true, pingStrength: 0.5f); - sonarBlips.Add(new SonarBlip(t.WorldPosition, 1.0f, 1.0f)); //remove blips that weren't in the AITarget's sector if (t.HasSector()) { - for (int i = sonarBlips.Count - 1; i >= prevBlipCount; i--) + for (int i = sonarBlips.Count - 1; i >= 0; i--) { + if (prevBlips.Contains(sonarBlips[i])) { continue; } if (!t.IsWithinSector(sonarBlips[i].Position)) { sonarBlips.RemoveAt(i); } } } + if (t.IsWithinSector(transducerCenter)) + { + sonarBlips.Add(new SonarBlip(t.WorldPosition, fadeTimer: 1.0f, scale: MathHelper.Clamp(t.SoundRange / 2000, 1.0f, 5.0f))); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs index c3f44c01d..13e9d9ffc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs @@ -97,7 +97,7 @@ namespace Barotrauma.Items.Components var chargeText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), chargeTextContainer.RectTransform, Anchor.CenterRight), "", textColor: GUIStyle.TextColorNormal, font: GUIStyle.Font, textAlignment: Alignment.CenterRight) { - TextGetter = () => $"{(int)MathF.Round(charge)}/{(int)capacity} {kWmin} ({(int)MathF.Round(MathUtils.Percentage(charge, capacity))} %)" + TextGetter = () => $"{(int)MathF.Round(charge)}/{(int)adjustedCapacity} {kWmin} ({(int)MathF.Round(MathUtils.Percentage(charge, adjustedCapacity))} %)" }; if (chargeText.TextSize.X > chargeText.Rect.Width) { chargeText.Font = GUIStyle.SmallFont; } @@ -108,7 +108,7 @@ namespace Barotrauma.Items.Components { ProgressGetter = () => { - return capacity <= 0.0f ? 1.0f : charge / capacity; + return adjustedCapacity <= 0.0f ? 1.0f : charge / adjustedCapacity; } }; } @@ -126,7 +126,7 @@ namespace Barotrauma.Items.Components { if (chargeIndicator != null) { - float chargeRatio = charge / capacity; + float chargeRatio = charge / adjustedCapacity; chargeIndicator.Color = ToolBox.GradientLerp(chargeRatio, Color.Red, Color.Orange, Color.Green); } } @@ -144,9 +144,9 @@ namespace Barotrauma.Items.Components Matrix rotate = Matrix.CreateRotationZ(item.RotationRad); Vector2 center = Vector2.Transform((indicatorPos + (scaledIndicatorSize * 0.5f)) * flip, rotate) + itemPosition; - if (charge > 0 && capacity > 0) + if (charge > 0 && adjustedCapacity > 0) { - float chargeRatio = MathHelper.Clamp(charge / capacity, 0.0f, 1.0f); + float chargeRatio = MathHelper.Clamp(charge / adjustedCapacity, 0.0f, 1.0f); Color indicatorColor = ToolBox.GradientLerp(chargeRatio, Color.Red, Color.Orange, Color.Green); Vector2 indicatorCenter = (indicatorPos + (scaledIndicatorSize * 0.5f)) * flip; Vector2 indicatorSize; @@ -193,7 +193,7 @@ namespace Barotrauma.Items.Components rechargeSpeedSlider.BarScroll = rechargeRate; } #endif - Charge = msg.ReadRangedSingle(0.0f, 1.0f, 8) * capacity; + Charge = msg.ReadRangedSingle(0.0f, 1.0f, 8) * adjustedCapacity; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 0694c838a..a11245fe4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1608,28 +1608,79 @@ namespace Barotrauma { containedState = item.Condition / item.MaxCondition; } - else if (itemContainer.ShowTotalStackCapacityInContainedStateIndicator) - { - int ignoredItems = itemContainer.AllSubContainableItems == null ? 0 : itemContainer.AllSubContainableItems.Count; - int itemCount = itemContainer.Inventory.AllItems.Count() - ignoredItems; - containedState = itemCount / (float)(itemContainer.GetMaxStackSize(0) * itemContainer.MainContainerCapacity); - } else { - int targetSlot = Math.Max(itemContainer.ContainedStateIndicatorSlot, 0); - var containedItem = itemContainer.Inventory.slots[targetSlot].FirstOrDefault(); - - containedState = itemContainer.Inventory.Capacity == 1 || itemContainer.ContainedStateIndicatorSlot > -1 ? - (containedItem == null ? 0.0f : containedItem.Condition / containedItem.MaxCondition) : - itemContainer.Inventory.slots.Count(i => !i.Empty()) / (float)itemContainer.Inventory.capacity; - - if (containedItem != null && (itemContainer.Inventory.Capacity == 1 || itemContainer.HasSubContainers)) + ItemSlot containedItemSlot = null; + if (targetSlot < itemContainer.Inventory.slots.Length) { - int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.GetMaxStackSize(targetSlot)); - if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar) + containedItemSlot = itemContainer.Inventory.slots[targetSlot]; + } + if (containedItemSlot != null) + { + Item containedItem = containedItemSlot.FirstOrDefault(); + if (itemContainer.ShowTotalStackCapacityInContainedStateIndicator) { - containedState = itemContainer.Inventory.slots[targetSlot].Items.Count / (float)maxStackSize; + if (containedItem == null) + { + // No item on the defined slot, check if the items on other slots can be used. + containedItem = containedItemSlot.FirstOrDefault() ?? itemContainer.Inventory.AllItems.FirstOrDefault(it => itemContainer.CanBeContained(it, targetSlot)); + } + if (containedItem != null) + { + int ignoredItemCount = 0; + var subContainableItems = itemContainer.AllSubContainableItems; + float capacity = itemContainer.GetMaxStackSize(targetSlot); + if (subContainableItems != null) + { + bool useMainContainerCapacity = true; + foreach (Item it in itemContainer.Inventory.AllItems) + { + // Ignore all items in the sub containers. + foreach (RelatedItem ri in subContainableItems) + { + if (ri.MatchesItem(containedItem)) + { + // The target item is in a subcontainer -> inverse the logic. + useMainContainerCapacity = false; + break; + } + if (ri.MatchesItem(it)) + { + ignoredItemCount++; + } + } + if (!useMainContainerCapacity) { break; } + } + if (useMainContainerCapacity) + { + capacity *= itemContainer.MainContainerCapacity; + } + else + { + // Ignore all items in the main container. + ignoredItemCount = itemContainer.Inventory.AllItems.Count(it => subContainableItems.Any(ri => !ri.MatchesItem(it))); + capacity *= itemContainer.Capacity - itemContainer.MainContainerCapacity; + } + } + int itemCount = itemContainer.Inventory.AllItems.Count() - ignoredItemCount; + containedState = Math.Min(itemCount / Math.Max(capacity, 1), 1); + } + } + else + { + containedState = itemContainer.Inventory.Capacity == 1 || itemContainer.ContainedStateIndicatorSlot > -1 ? + (containedItem == null ? 0.0f : containedItem.Condition / containedItem.MaxCondition) : + itemContainer.Inventory.slots.Count(i => !i.Empty()) / (float)itemContainer.Inventory.capacity; + + if (containedItem != null && (itemContainer.Inventory.Capacity == 1 || itemContainer.HasSubContainers)) + { + int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.GetMaxStackSize(targetSlot)); + if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar) + { + containedState = containedItemSlot.Items.Count / (float)maxStackSize; + } + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 0a4fe162a..460e63dce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -1251,11 +1251,9 @@ namespace Barotrauma { foreach (ItemComponent ic in components) { - if (ic.DisplayMsg.IsNullOrEmpty()) { continue; } if (!ic.CanBePicked && !ic.CanBeSelected) { continue; } if (ic is Holdable holdable && !holdable.CanBeDeattached()) { continue; } if (ic is ConnectionPanel connectionPanel && !connectionPanel.CanRewire()) { continue; } - Color color = Color.Gray; if (ic.HasRequiredItems(character, false)) { @@ -1268,6 +1266,7 @@ namespace Barotrauma color = Color.Cyan; } } + if (ic.DisplayMsg.IsNullOrEmpty()) { continue; } texts.Add(new ColoredText(ic.DisplayMsg.Value, color, false, false)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs index f235624b7..89abf5bf8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs @@ -1,12 +1,15 @@ using System.Diagnostics; using System.IO.Pipes; using System.Linq; +using System.Threading; namespace Barotrauma.Networking { static partial class ChildServerRelay { public static Process Process; + public static bool IsProcessAlive => Process is { HasExited: false }; + private static bool localHandlesDisposed; private static AnonymousPipeServerStream writePipe; private static AnonymousPipeServerStream readPipe; @@ -44,18 +47,27 @@ namespace Barotrauma.Networking localHandlesDisposed = true; } - public static void ClosePipes() + public static void AttemptGracefulShutDown(int maxAttempts = 20) { - writePipe?.Dispose(); writePipe = null; - readPipe?.Dispose(); readPipe = null; - shutDown = true; + status = StatusEnum.RequestedShutDown; + writeManualResetEvent?.Set(); + int checks = 0; + while (Process is { HasExited: false }) + { + if (checks >= maxAttempts) + { + DebugConsole.AddWarning("Server could not be shut down gracefully"); + break; + } + Thread.Sleep(100); + checks++; + } + ForceShutDown(); } - - public static void ShutDown() + + public static void ForceShutDown() { Process?.Kill(); Process = null; - writePipe = null; readPipe = null; - PrivateShutDown(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 8c1af60e7..98fd63aa1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -332,10 +332,27 @@ namespace Barotrauma.Networking }; } + public void CreateServerCrashMessage() + { + // Close any message boxes that say "The server has crashed." + var basicServerCrashMsg = TextManager.Get($"{nameof(DisconnectReason)}.{nameof(DisconnectReason.ServerCrashed)}"); + GUIMessageBox.MessageBoxes + .OfType() + .Where(mb => mb.Text?.Text == basicServerCrashMsg) + .ToArray() + .ForEach(mb => mb.Close()); + + // Open a new message box with the crash report path + if (GUIMessageBox.MessageBoxes.All( + mb => (mb as GUIMessageBox)?.Text?.Text != ChildServerRelay.CrashMessage)) + { + var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); + msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; + } + } + private bool ReturnToPreviousMenu(GUIButton button, object obj) { - Quit(); - Submarine.Unload(); GameMain.Client = null; GameMain.GameSession = null; @@ -531,14 +548,10 @@ namespace Barotrauma.Networking { if (GameMain.WindowActive) { - if (ChildServerRelay.Process?.HasExited ?? true) + if (!ChildServerRelay.IsProcessAlive) { Quit(); - if (!GUIMessageBox.MessageBoxes.Any(mb => (mb as GUIMessageBox)?.Text?.Text == ChildServerRelay.CrashMessage)) - { - var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); - msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; - } + CreateServerCrashMessage(); } } } @@ -942,13 +955,6 @@ namespace Barotrauma.Networking GUI.ClearCursorWait(); - ChildServerRelay.ShutDown(); - - if (SteamManager.IsInitialized) - { - Steamworks.SteamFriends.ClearRichPresence(); - } - if (disconnectPacket.ShouldCreateAnalyticsEvent) { GameAnalyticsManager.AddErrorEventOnce( @@ -974,11 +980,43 @@ namespace Barotrauma.Networking } else { - ReturnToPreviousMenu(null, null); - new GUIMessageBox(TextManager.Get(wasConnected ? "ConnectionLost" : "CouldNotConnectToServer"), disconnectPacket.PopupMessage) + if (ClientPeer is SteamP2PClientPeer or SteamP2POwnerPeer) { - DisplayInLoadingScreens = true - }; + SteamManager.LeaveLobby(); + } + + GameMain.ModDownloadScreen.Reset(); + ContentPackageManager.EnabledPackages.Restore(); + + CampaignMode.StartRoundCancellationToken?.Cancel(); + + if (SteamManager.IsInitialized) + { + Steamworks.SteamFriends.ClearRichPresence(); + } + foreach (var fileTransfer in FileReceiver.ActiveTransfers.ToArray()) + { + FileReceiver.StopTransfer(fileTransfer, deleteFile: true); + } + + ChildServerRelay.AttemptGracefulShutDown(); + GUIMessageBox.MessageBoxes.RemoveAll(c => c?.UserData is RoundSummary); + + characterInfo?.Remove(); + + VoipClient?.Dispose(); + VoipClient = null; + GameMain.Client = null; + GameMain.GameSession = null; + + ReturnToPreviousMenu(null, null); + if (disconnectPacket.DisconnectReason != DisconnectReason.Disconnected) + { + new GUIMessageBox(TextManager.Get(wasConnected ? "ConnectionLost" : "CouldNotConnectToServer"), disconnectPacket.PopupMessage) + { + DisplayInLoadingScreens = true + }; + } } } @@ -2538,46 +2576,9 @@ namespace Barotrauma.Networking public void Quit() { - if (ClientPeer is SteamP2PClientPeer || ClientPeer is SteamP2POwnerPeer) - { - SteamManager.LeaveLobby(); - } - - GameMain.ModDownloadScreen.Reset(); - ContentPackageManager.EnabledPackages.Restore(); - - CampaignMode.StartRoundCancellationToken?.Cancel(); - ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); - ClientPeer = null; - - foreach (var fileTransfer in FileReceiver.ActiveTransfers.ToArray()) - { - FileReceiver.StopTransfer(fileTransfer, deleteFile: true); - } - - if (ChildServerRelay.Process != null) - { - int checks = 0; - while (ChildServerRelay.Process is { HasExited: false }) - { - if (checks > 10) - { - ChildServerRelay.ShutDown(); - } - Thread.Sleep(100); - checks++; - } - } - ChildServerRelay.ShutDown(); + GUIMessageBox.MessageBoxes.RemoveAll(c => c?.UserData is RoundSummary); - - characterInfo?.Remove(); - - VoipClient?.Dispose(); - VoipClient = null; - GameMain.Client = null; - GameMain.GameSession = null; } public void SendCharacterInfo(string newName = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index 262f608bc..8e599ee17 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -91,15 +91,11 @@ namespace Barotrauma.Networking ToolBox.ThrowIfNull(netClient); ToolBox.ThrowIfNull(incomingLidgrenMessages); - if (isOwner && !(ChildServerRelay.Process is { HasExited: false })) + if (isOwner && !ChildServerRelay.IsProcessAlive) { + var gameClient = GameMain.Client; Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed)); - var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); - msgBox.Buttons[0].OnClicked += (btn, obj) => - { - GameMain.MainMenuScreen.Select(); - return false; - }; + gameClient?.CreateServerCrashMessage(); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index 48fe24b0e..54c4d7ca2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -187,15 +187,11 @@ namespace Barotrauma.Networking { if (!isActive) { return; } - if (ChildServerRelay.HasShutDown || !(ChildServerRelay.Process is { HasExited: false })) + if (ChildServerRelay.HasShutDown || !ChildServerRelay.IsProcessAlive) { + var gameClient = GameMain.Client; Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed)); - var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); - msgBox.Buttons[0].OnClicked += (btn, obj) => - { - GameMain.MainMenuScreen.Select(); - return false; - }; + gameClient?.CreateServerCrashMessage(); return; } @@ -401,8 +397,6 @@ namespace Barotrauma.Networking ClosePeerSession(remotePeers[i]); } - ChildServerRelay.ClosePipes(); - callbacks.OnDisconnect.Invoke(peerDisconnectPacket); SteamManager.LeaveLobby(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index 5cf536458..cde031084 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -17,9 +17,7 @@ namespace Barotrauma.Networking get; private set; } - - public static IReadOnlyList CaptureDeviceNames => - Alc.GetStringList(IntPtr.Zero, OpenAL.Alc.CaptureDeviceSpecifier); + private readonly IntPtr captureDevice; @@ -169,6 +167,11 @@ namespace Barotrauma.Networking Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); } + public static IReadOnlyList GetCaptureDeviceNames() + { + return Alc.GetStringList(IntPtr.Zero, OpenAL.Alc.CaptureDeviceSpecifier); + } + IntPtr nativeBuffer; readonly short[] uncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; readonly short[] prevUncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 90f0e3f1b..dd9aba825 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -10,8 +10,6 @@ namespace Barotrauma { partial class GameScreen : Screen { - public override bool IsEditor => GameMain.GameSession?.GameMode is TestGameMode; - private RenderTarget2D renderTargetBackground; private RenderTarget2D renderTarget; private RenderTarget2D renderTargetWater; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 0ae5127d6..01553f630 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -47,7 +47,14 @@ namespace Barotrauma private GUITextBox serverNameBox, passwordBox, maxPlayersBox; private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox; private GUIDropDown serverExecutableDropdown; - private readonly GUIButton joinServerButton, hostServerButton, steamWorkshopButton; + private readonly GUIButton joinServerButton, hostServerButton; + + private readonly GUIFrame modsButtonContainer; + private readonly GUIButton modsButton, modUpdatesButton; + private Task> modUpdateTask; + private float modUpdateTimer = 0.0f; + private const float ModUpdateInterval = 60.0f; + private readonly GameMain game; private GUIImage playstyleBanner; @@ -268,15 +275,29 @@ namespace Barotrauma RelativeSpacing = 0.035f }; -#if USE_STEAM - steamWorkshopButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("settingstab.mods"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") + modsButtonContainer = new GUIFrame(new RectTransform(Vector2.One, customizeList.RectTransform), + style: null); + + modsButton = new GUIButton(new RectTransform(Vector2.One, modsButtonContainer.RectTransform), + TextManager.Get("settingstab.mods"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { ForceUpperCase = ForceUpperCase.Yes, Enabled = true, UserData = Tab.SteamWorkshop, OnClicked = SelectTab }; -#endif + + modUpdatesButton = new GUIButton(new RectTransform(Vector2.One * 0.95f, modsButtonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIUpdateButton") + { + ToolTip = TextManager.Get("ModUpdatesAvailable"), + OnClicked = (_, _) => + { + BulkDownloader.PrepareUpdates(); + return false; + }, + Visible = false + }; new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("SubEditorButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { @@ -525,6 +546,8 @@ namespace Barotrauma #region Selection public override void Select() { + ResetModUpdateButton(); + if (WorkshopItemsToUpdate.Any()) { while (WorkshopItemsToUpdate.TryDequeue(out ulong workshopId)) @@ -711,6 +734,13 @@ namespace Barotrauma } #endregion + public void ResetModUpdateButton() + { + modUpdateTask = null; + modUpdateTimer = 0; + modUpdatesButton.Visible = false; + } + public void QuickStart(bool fixedSeed = false, Identifier sub = default, float difficulty = 50, LevelGenerationParams levelGenerationParams = null) { if (fixedSeed) @@ -930,15 +960,32 @@ namespace Barotrauma public override void Update(double deltaTime) { -#if !DEBUG && USE_STEAM + modUpdateTimer -= (float)deltaTime; + if (modUpdateTimer <= 0.0f && modUpdateTask is not { IsCompleted: false }) + { + modUpdateTask = BulkDownloader.GetItemsThatNeedUpdating(); + modUpdateTimer = ModUpdateInterval; + } + if (GameSettings.CurrentConfig.UseSteamMatchmaking) { hostServerButton.Enabled = Steam.SteamManager.IsInitialized; } - steamWorkshopButton.Enabled = Steam.SteamManager.IsInitialized; -#elif USE_STEAM - steamWorkshopButton.Enabled = true; -#endif + + if (modUpdateTask is { IsCompletedSuccessfully: true }) + { + modUpdatesButton.Visible = modUpdateTask.Result.Count > 0; + } + + if (modUpdatesButton.Visible) + { + var modButtonLabelSize = + modsButton.Font.MeasureString(modsButton.Text).ToPoint() + + new Point(GUI.IntScale(25)); + modUpdatesButton.RectTransform.AbsoluteOffset = + (modButtonLabelSize.X, modsButton.Rect.Height / 2 - modUpdatesButton.Rect.Height / 2); + } + switch (selectedTab) { case Tab.NewGame: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 6d966c50e..c6956035d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -103,6 +103,10 @@ namespace Barotrauma public GUIFrame JobPreferenceContainer; public GUIListBox JobList; + private Identifier micIconStyle; + private float micCheckTimer; + const float MicCheckInterval = 1.0f; + private float autoRestartTimer; //persistent characterinfo provided by the server @@ -2656,27 +2660,9 @@ namespace Barotrauma public override void Update(double deltaTime) { - base.Update(deltaTime); - if (GameMain.Client == null) { return; } - Identifier currMicStyle = micIcon.Style.Element.NameAsIdentifier(); - - Identifier targetMicStyle = "GUIMicrophoneEnabled".ToIdentifier(); - var voipCaptureDeviceNames = VoipCapture.CaptureDeviceNames; - if (voipCaptureDeviceNames.Count == 0) - { - targetMicStyle = "GUIMicrophoneUnavailable".ToIdentifier(); - } - else if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) - { - targetMicStyle = "GUIMicrophoneDisabled".ToIdentifier(); - } - - if (targetMicStyle != currMicStyle) - { - GUIStyle.Apply(micIcon, targetMicStyle); - } + UpdateMicIcon((float)deltaTime); foreach (GUIComponent child in PlayerList.Content.Children) { @@ -2738,6 +2724,35 @@ namespace Barotrauma if (!mouseRect.Contains(PlayerInput.MousePosition)) { jobVariantTooltip = null; } } } + + private void UpdateMicIcon(float deltaTime) + { + micCheckTimer -= deltaTime; + if (micCheckTimer > 0.0f) { return; } + + Identifier newMicIconStyle = "GUIMicrophoneEnabled".ToIdentifier(); + if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) + { + newMicIconStyle = "GUIMicrophoneDisabled".ToIdentifier(); + } + else + { + var voipCaptureDeviceNames = VoipCapture.GetCaptureDeviceNames(); + if (voipCaptureDeviceNames.Count == 0) + { + newMicIconStyle = "GUIMicrophoneUnavailable".ToIdentifier(); + } + } + + if (newMicIconStyle != micIconStyle) + { + micIconStyle = newMicIconStyle; + GUIStyle.Apply(micIcon, newMicIconStyle); + } + + micCheckTimer = MicCheckInterval; + } + public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { graphics.Clear(Color.Black); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index ec122659f..2bd642ac6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -354,9 +354,24 @@ namespace Barotrauma ToolTip = RichString.Rich(TextManager.Get("SaveSubButton") + "‖color:125,125,125‖\nCtrl + S‖color:end‖"), OnClicked = (btn, data) => { +#if DEBUG + if (ContentPackageManager.EnabledPackages.All.Any(cp => cp != ContentPackageManager.VanillaCorePackage && cp.Files.Any(f => f is not BaseSubFile))) + { + var msgBox = new GUIMessageBox("DEBUG-ONLY WARNING", "You currently have some mods enabled. Are you sure you want to save the submarine? If the mods override any vanilla content, saving the submarine may cause unintended changes.", + new LocalizedString[] { "Yes, I know what I'm doing", "Cancel" }); + msgBox.Buttons[0].OnClicked = (btn, data) => + { + msgBox.Close(); + loadFrame = null; + CreateSaveScreen(); + return true; + }; + msgBox.Buttons[1].OnClicked += msgBox.Close; + return false; + } +#endif loadFrame = null; CreateSaveScreen(); - return true; } }; @@ -3096,7 +3111,7 @@ namespace Barotrauma return false; } - private void SnapToGrid() + private static void SnapToGrid() { // First move components foreach (MapEntity e in MapEntity.SelectedList) @@ -3109,6 +3124,10 @@ namespace Barotrauma var wire = item.GetComponent(); if (wire != null) { continue; } item.Move(offset); + if (item.GetComponent()?.LinkedGap is Gap linkedGap) + { + linkedGap.Move(item.Position - linkedGap.Position); + } } else if (e is Structure structure) { @@ -3133,7 +3152,7 @@ namespace Barotrauma } } - private IEnumerable GetLoadableSubs() + private static IEnumerable GetLoadableSubs() { string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); return SubmarineInfo.SavedSubmarines.Where(s @@ -3520,9 +3539,18 @@ namespace Barotrauma public void LoadSub(SubmarineInfo info) { Submarine.Unload(); - var selectedSub = new Submarine(info); - MainSub = selectedSub; - MainSub.UpdateTransform(interpolate: false); + Submarine selectedSub = null; + try + { + selectedSub = new Submarine(info); + MainSub = selectedSub; + MainSub.UpdateTransform(interpolate: false); + } + catch (Exception e) + { + DebugConsole.ThrowError("Failed to load the submarine. The submarine file might be corrupted.", e); + return; + } ClearUndoBuffer(); CreateDummyCharacter(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 08b538501..8eee8bff2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -716,8 +716,7 @@ namespace Barotrauma if (Screen.Selected == null) { return "menu".ToIdentifier(); } - if ((Screen.Selected?.IsEditor ?? false) - || (Screen.Selected == GameMain.NetLobbyScreen)) + if (Screen.Selected is { IsEditor: true } || GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected == GameMain.NetLobbyScreen) { return "editor".ToIdentifier(); } @@ -814,7 +813,7 @@ namespace Barotrauma { return "levelend".ToIdentifier(); } - if (GameMain.GameSession.RoundDuration > 120.0 && + if (GameMain.GameSession.RoundDuration < 120.0 && Level.Loaded?.Type == LevelData.LevelType.LocationConnection) { return "start".ToIdentifier(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs index da5f50871..27c008170 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs @@ -64,7 +64,7 @@ namespace Barotrauma.Steam }); } - private static async Task> GetItemsThatNeedUpdating() + public static async Task> GetItemsThatNeedUpdating() { var determiningTasks = ContentPackageManager.WorkshopPackages.Select(async p => (p, await p.IsUpToDate())); (ContentPackage Package, bool IsUpToDate)[] outOfDatePackages = await Task.WhenAll(determiningTasks); @@ -126,6 +126,7 @@ namespace Barotrauma.Steam { mutableWorkshopMenu.PopulateInstalledModLists(forceRefreshEnabled: true); } + GameMain.MainMenuScreen.ResetModUpdateButton(); }); } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index b7edecc65..d494aed4a 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.20.8.0 + 0.20.9.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index a568a7a92..0d49c9d86 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.20.8.0 + 0.20.9.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 9b9002c76..eb0c23892 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.20.8.0 + 0.20.9.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index c1419ac1e..d4f7016e2 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.20.8.0 + 0.20.9.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index a84112284..ed6ba0f49 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.20.8.0 + 0.20.9.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs index 0cfbe9026..c9cad1019 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs @@ -18,20 +18,22 @@ namespace Barotrauma.Items.Components bool powerOn = msg.ReadBoolean(); float fissionRate = msg.ReadRangedSingle(0.0f, 100.0f, 8); float turbineOutput = msg.ReadRangedSingle(0.0f, 100.0f, 8); + float temperatureBoostAmount = msg.ReadRangedSingle(-TemperatureBoostAmount, TemperatureBoostAmount, 8); if (!item.CanClientAccess(c)) { return; } IsActive = true; - if (!autoTemp && AutoTemp) blameOnBroken = c; - if (turbineOutput < TargetTurbineOutput) blameOnBroken = c; - if (fissionRate > TargetFissionRate) blameOnBroken = c; - if (!_powerOn && powerOn) blameOnBroken = c; + if (!autoTemp && AutoTemp) { blameOnBroken = c; } + if (turbineOutput < TargetTurbineOutput) { blameOnBroken = c; } + if (fissionRate > TargetFissionRate) { blameOnBroken = c; } + if (!_powerOn && powerOn) { blameOnBroken = c; } AutoTemp = autoTemp; _powerOn = powerOn; TargetFissionRate = fissionRate; TargetTurbineOutput = turbineOutput; + if (AllowTemperatureBoost) { temperatureBoost = temperatureBoostAmount; } LastUser = c.Character; if (nextServerLogWriteTime == null) @@ -51,6 +53,7 @@ namespace Barotrauma.Items.Components msg.WriteRangedSingle(TargetFissionRate, 0.0f, 100.0f, 8); msg.WriteRangedSingle(TargetTurbineOutput, 0.0f, 100.0f, 8); msg.WriteRangedSingle(degreeOfSuccess, 0.0f, 1.0f, 8); + msg.WriteRangedSingle(temperatureBoost, -TemperatureBoostAmount, TemperatureBoostAmount, 8); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs index e0cf10812..5529c4af3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs @@ -24,7 +24,7 @@ namespace Barotrauma.Items.Components { msg.WriteRangedInteger((int)(rechargeSpeed / MaxRechargeSpeed * 10), 0, 10); - float chargeRatio = MathHelper.Clamp(charge / capacity, 0.0f, 1.0f); + float chargeRatio = MathHelper.Clamp(charge / adjustedCapacity, 0.0f, 1.0f); msg.WriteRangedSingle(chargeRatio, 0.0f, 1.0f, 8); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index d3cc498d1..b62e6032d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -335,9 +335,9 @@ namespace Barotrauma.Networking #endif if (!started) { return; } - if (OwnerConnection != null && ChildServerRelay.HasShutDown) + if (ChildServerRelay.HasShutDown) { - Quit(); + GameMain.Instance.CloseServer(); return; } @@ -3939,6 +3939,7 @@ namespace Barotrauma.Networking public void Quit() { + if (started) { started = false; diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index adf4b4d43..208a78b24 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.20.8.0 + 0.20.9.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 83e3ca0c4..0e959efd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -1838,7 +1838,7 @@ namespace Barotrauma } if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged) { - bool advance = !canAttack && Character.InWater || distance > attackLimb.attack.Range * 0.9f; + bool advance = !canAttack && Character.CurrentHull == null || distance > attackLimb.attack.Range * 0.9f; bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f); if (fallBack) { @@ -1856,10 +1856,18 @@ namespace Barotrauma SteeringManager.SteeringSeek(steerPos, 10); } } - else if (!Character.InWater) + else { - SteeringManager.Reset(); - FaceTarget(SelectedAiTarget.Entity); + if (Character.CurrentHull == null && !canAttack) + { + SteeringManager.SteeringWander(); + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); + } + else + { + SteeringManager.Reset(); + FaceTarget(SelectedAiTarget.Entity); + } } } else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100)) @@ -1882,7 +1890,7 @@ namespace Barotrauma } Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity; IDamageable damageTarget = targetEntity as IDamageable; - if (AttackLimb?.attack is Attack { Ranged: true} attack && targetEntity != null) + if (AttackLimb?.attack is Attack { Ranged: true} attack) { AimRangedAttack(attack, targetEntity); } @@ -1901,7 +1909,7 @@ namespace Barotrauma public void AimRangedAttack(Attack attack, Entity targetEntity) { - if (attack == null || attack.Ranged == false || targetEntity == null) { return; } + if (attack is not { Ranged: true } || targetEntity is not { Removed: false }) { return; } Character.SetInput(InputType.Aim, false, true); Limb limb = GetLimbToRotate(attack); if (limb != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index a50997a87..32ce66986 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -1907,8 +1907,8 @@ namespace Barotrauma visibleHulls = VisibleHulls; } bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective(); - bool ignoreWater = HasDivingSuit(character); - bool ignoreOxygen = ignoreWater || HasDivingMask(character); + bool ignoreWater = character.IsProtectedFromPressure(); + bool ignoreOxygen = HasDivingGear(character); bool ignoreEnemies = ObjectiveManager.IsCurrentOrder() || ObjectiveManager.IsCurrentObjective(); float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies); if (isCurrentHull) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index afdaf4bdc..4ddfe05c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -77,11 +77,11 @@ namespace Barotrauma { if (Level.Loaded.Type == LevelData.LevelType.LocationConnection) { - if (GameMain.GameSession.RoundDuration > 30.0f) { currentFlags.Add("Initial".ToIdentifier()); } + if (GameMain.GameSession.RoundDuration < 30.0f) { currentFlags.Add("Initial".ToIdentifier()); } } else if (Level.Loaded.Type == LevelData.LevelType.Outpost) { - if (GameMain.GameSession.RoundDuration > 120.0f && + if (GameMain.GameSession.RoundDuration < 120.0f && speaker?.CurrentHull != null && (speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) && Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 0a509fdd6..e277590bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -876,7 +876,20 @@ namespace Barotrauma TargetName = Enemy.DisplayName, AlwaysUseEuclideanDistance = false }, - onAbandon: () => Abandon = true); + onAbandon: () => + { + if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull)) + { + // If in the same room with an enemy -> don't try to escape because we'd want to fight it + SteeringManager.Reset(); + RemoveSubObjective(ref followTargetObjective); + } + else + { + // else abandon and fall back to find safety mode + Abandon = true; + } + }); if (followTargetObjective == null) { return; } if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 8a5c606b3..8db97cb9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -25,6 +25,8 @@ namespace Barotrauma private readonly Item item; public Item ItemToContain { get; private set; } + public int? TargetSlot; + private AIObjectiveGetItem getItemObjective; private AIObjectiveGoTo goToObjective; @@ -126,7 +128,19 @@ namespace Barotrauma } if (character.CanInteractWith(container.Item, checkLinked: false)) { - if (RemoveExisting || (RemoveExistingWhenNecessary && !container.Inventory.CanBePut(ItemToContain))) + static bool CanBePut(Inventory inventory, int? targetSlot, Item itemToContain) + { + if (targetSlot.HasValue) + { + return inventory.CanBePutInSlot(itemToContain, targetSlot.Value); + } + else + { + return inventory.CanBePut(itemToContain); + } + } + + if (RemoveExisting || (RemoveExistingWhenNecessary && !CanBePut(container.Inventory, TargetSlot, ItemToContain))) { HumanAIController.UnequipContainedItems(container.Item, predicate: RemoveExistingPredicate, unequipMax: RemoveMax); } @@ -136,7 +150,20 @@ namespace Barotrauma } Inventory originalInventory = ItemToContain.ParentInventory; var slots = originalInventory?.FindIndices(ItemToContain); - if (container.Inventory.TryPutItem(ItemToContain, null)) + + static bool TryPutItem(Inventory inventory, int? targetSlot, Item itemToContain) + { + if (targetSlot.HasValue) + { + return inventory.TryPutItem(itemToContain, targetSlot.Value, allowSwapping: false, allowCombine: false, user: null); + } + else + { + return inventory.TryPutItem(itemToContain, user: null); + } + } + + if (TryPutItem(container.Inventory, TargetSlot, ItemToContain)) { if (MoveWholeStack && slots != null) { @@ -144,7 +171,7 @@ namespace Barotrauma { foreach (Item item in originalInventory.GetItemsAt(slot).ToList()) { - container.Inventory.TryPutItem(item, null); + TryPutItem(container.Inventory, TargetSlot, item); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index 0704b91c6..e525b613c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -23,9 +23,8 @@ namespace Barotrauma protected override float TargetEvaluation() { - if (!character.IsOnPlayerTeam) { return Targets.None() ? 0 : 100; } - int totalEnemies = Targets.Count(); - if (totalEnemies == 0) { return 0; } + if (Targets.None()) { return 0; } + if (!character.IsOnPlayerTeam) { return 100; } if (character.IsSecurity) { return 100; } if (objectiveManager.IsOrder(this)) { return 100; } // If there's any security officers onboard, leave fighting for them. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index c2c9f8ee5..f1a8b68f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -103,13 +103,19 @@ namespace Barotrauma character.Speak(TextManager.Get("DialogGetOxygenTank").Value, null, 0, "getoxygentank".ToIdentifier(), 30.0f); } } - return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) + var container = targetItem.GetComponent(); + var objective = new AIObjectiveContainItem(character, OXYGEN_SOURCE, container, objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) { AllowToFindDivingGear = false, AllowDangerousPressure = true, ConditionLevel = MIN_OXYGEN, RemoveExistingWhenNecessary = true }; + if (container.HasSubContainers) + { + objective.TargetSlot = container.FindSuitableSubContainerIndex(OXYGEN_SOURCE); + } + return objective; }, onAbandon: () => { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index d8607e729..684713e02 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -262,7 +262,7 @@ namespace Barotrauma { Character followTarget = Target as Character; bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure); - bool needsDivingGear = (needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit)); + bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit); if (Mimic) { if (HumanAIController.HasDivingSuit(followTarget)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs index 24f0ef67d..4f64dac4d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs @@ -435,7 +435,7 @@ namespace Barotrauma spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandomUnsynced(); spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition; } - var pet = Character.Create(speciesName, spawnPos, seed); + var pet = Character.Create(speciesName, spawnPos, seed, spawnInitialItems: false); var petBehavior = (pet?.AIController as EnemyAIController)?.PetBehavior; if (petBehavior != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs index 7981478e6..48ad56141 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs @@ -11,8 +11,8 @@ namespace Barotrauma get { return aiController; } } - public AICharacter(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isNetworkPlayer = false, RagdollParams ragdoll = null) - : base(prefab, position, seed, characterInfo, id: id, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll) + public AICharacter(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isNetworkPlayer = false, RagdollParams ragdoll = null, bool spawnInitialItems = true) + : base(prefab, position, seed, characterInfo, id: id, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll, spawnInitialItems) { InitProjSpecific(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index bee4209fa..44b96063c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -529,8 +529,17 @@ namespace Barotrauma private Color speechBubbleColor; private float speechBubbleTimer; + /// + /// Prevents the character from interacting with items or characters + /// public bool DisableInteract { get; set; } + /// + /// Prevents the character from highlighting items or characters with the cursor, + /// meaning it can't interact with anything but the things it has currently selected/equipped + /// + public bool DisableFocusingOnEntities { get; set; } + //text displayed when the character is highlighted if custom interact is set public LocalizedString CustomInteractHUDText { get; private set; } private Action onCustomInteract; @@ -1088,9 +1097,9 @@ namespace Barotrauma /// Is the character controlled by a remote player. /// Is the character controlled by AI. /// Ragdoll configuration file. If null, will select the default. - public static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, RagdollParams ragdoll = null) + public static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, RagdollParams ragdoll = null, bool spawnInitialItems = true) { - return Create(characterInfo.SpeciesName, position, seed, characterInfo, id, isRemotePlayer, hasAi, true, ragdoll); + return Create(characterInfo.SpeciesName, position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent: true, ragdoll, spawnInitialItems); } /// @@ -1105,16 +1114,16 @@ namespace Barotrauma /// Is the character controlled by AI. /// Should clients receive a network event about the creation of this character? /// Ragdoll configuration file. If null, will select the default. - public static Character Create(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool throwErrorIfNotFound = true) + public static Character Create(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool throwErrorIfNotFound = true, bool spawnInitialItems = true) { if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) { speciesName = Path.GetFileNameWithoutExtension(speciesName); } - return Create(speciesName.ToIdentifier(), position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll, throwErrorIfNotFound); + return Create(speciesName.ToIdentifier(), position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll, throwErrorIfNotFound, spawnInitialItems); } - public static Character Create(Identifier speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool throwErrorIfNotFound = true) + public static Character Create(Identifier speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool throwErrorIfNotFound = true, bool spawnInitialItems = true) { var prefab = CharacterPrefab.FindBySpeciesName(speciesName); if (prefab == null) @@ -1131,29 +1140,29 @@ namespace Barotrauma return null; } - return Create(prefab, position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll); + return Create(prefab, position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll, spawnInitialItems); } - public static Character Create(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null) + public static Character Create(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool spawnInitialItems = true) { Character newCharacter = null; if (prefab.Identifier != CharacterPrefab.HumanSpeciesName) { - var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll); + var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll, spawnInitialItems); var ai = new EnemyAIController(aiCharacter, seed); aiCharacter.SetAI(ai); newCharacter = aiCharacter; } else if (hasAi) { - var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll); + var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll, spawnInitialItems); var ai = new HumanAIController(aiCharacter); aiCharacter.SetAI(ai); newCharacter = aiCharacter; } else { - newCharacter = new Character(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll); + newCharacter = new Character(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll, spawnInitialItems); } #if SERVER @@ -1170,7 +1179,7 @@ namespace Barotrauma wallet = new Wallet(Option.Some(this)); } - protected Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null) + protected Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null, bool spawnInitialItems = true) : this(null, id) { this.Seed = seed; @@ -1280,7 +1289,8 @@ namespace Barotrauma { Inventory = new CharacterInventory( inventoryElements.Count == 1 ? inventoryElements[0] : ToolBox.SelectWeightedRandom(inventoryElements, inventoryCommonness, random), - this); + this, + spawnInitialItems); } if (healthElements.Count == 0) { @@ -2719,7 +2729,7 @@ namespace Barotrauma #if CLIENT if (isLocalPlayer) { - if (!IsMouseOnUI && (ViewTarget == null || ViewTarget == this)) + if (!IsMouseOnUI && (ViewTarget == null || ViewTarget == this) && !DisableFocusingOnEntities) { if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen) { @@ -2754,6 +2764,7 @@ namespace Barotrauma focusedItem = null; } findFocusedTimer -= deltaTime; + DisableFocusingOnEntities = false; } #endif var head = AnimController.GetLimb(LimbType.Head); @@ -4249,11 +4260,14 @@ namespace Barotrauma if (!isNetworkMessage) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (GameMain.NetworkMember is { IsClient: true }) { return; } } CharacterHealth.ApplyAffliction(null, new Affliction(AfflictionPrefab.Pressure, AfflictionPrefab.Pressure.MaxStrength)); - if (isNetworkMessage && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Vitality <= CharacterHealth.MinVitality) { Kill(CauseOfDeathType.Pressure, null, isNetworkMessage: true); } + if (GameMain.NetworkMember is not { IsClient: true } || isNetworkMessage) + { + Kill(CauseOfDeathType.Pressure, null, isNetworkMessage: true); + } if (IsDead) { BreakJoints(); @@ -4930,7 +4944,7 @@ namespace Barotrauma { foreach (TalentOption talentOption in talentSubTree.TalentOptionStages) { - if (talentOption.TalentIdentifiers.None(HasTalent)) + if (!talentOption.HasMaxTalents(info.UnlockedTalents)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterHUD.cs new file mode 100644 index 000000000..9970fa388 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterHUD.cs @@ -0,0 +1,12 @@ +namespace Barotrauma; + +partial class CharacterHUD +{ + static partial void RecreateHudTextsIfControllingProjSpecific(Character character); + + static partial void RecreateHudTextsIfFocusedProjSpecific(params Item[] items); + + public static void RecreateHudTextsIfControlling(Character character) => RecreateHudTextsIfControllingProjSpecific(character); + + public static void RecreateHudTextsIfFocused(params Item[] items) => RecreateHudTextsIfFocusedProjSpecific(items); +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 93b64d455..1d2ff2bf4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -132,7 +132,7 @@ namespace Barotrauma public bool IsUnconscious { - get { return (Vitality <= 0.0f || Character.IsDead) && !Character.HasAbilityFlag(AbilityFlags.AlwaysStayConscious); } + get { return Character.IsDead || (Vitality <= 0.0f && !Character.HasAbilityFlag(AbilityFlags.AlwaysStayConscious)); } } public float PressureKillDelay { get; private set; } = 5.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index cb0cec8e7..5db14d6ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -72,7 +72,7 @@ namespace Barotrauma bool allowCheats = false; #if CLIENT - allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is EditorScreen); + allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is { IsEditor: true }); #endif if (!allowCheats && !CheatsEnabled && IsCheat) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 2eafaf60e..baa0aecd3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -150,7 +150,7 @@ namespace Barotrauma MaxAttachableCount, ExplosionRadiusMultiplier, ExplosionDamageMultiplier, - FabricateMedicineSpeedMultiplier, + FabricationSpeed, BallastFloraDamageMultiplier, HoldBreathMultiplier, Apprenticeship, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index 4269be922..e84bd7676 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -1,3 +1,4 @@ +using Barotrauma.Extensions; using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; @@ -53,6 +54,11 @@ namespace Barotrauma break; } conditionals = conditionalList; + + if (itemTags.None() && ItemIdentifiers.None()) + { + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(CheckItemAction)} does't define either tags or identifiers of the item to check."); + } } protected override bool? DetermineSuccess() @@ -100,7 +106,22 @@ namespace Barotrauma { if (inventory == null) { return false; } int count = 0; - foreach (Item item in inventory.FindAllItems(it => itemTags.Any(it.HasTag) || itemIdentifierSplit.Contains(it.Prefab.Identifier), recursive: Recursive)) + HashSet eventTargets = new HashSet(); + foreach (Identifier tag in itemTags) + { + foreach (var target in ParentEvent.GetTargets(tag)) + { + if (target is Item item) + { + eventTargets.Add(item); + } + } + } + foreach (Item item in inventory.FindAllItems(it => + itemTags.Any(it.HasTag) || + itemIdentifierSplit.Contains(it.Prefab.Identifier) || + eventTargets.Contains(it), + recursive: Recursive)) { if (!ConditionalsMatch(item, character)) { continue; } count++; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 3af128651..06a0884f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -80,7 +80,7 @@ namespace Barotrauma public bool DisableEvents { - get { return IsFirstRound && GameMain.GameSession.RoundDuration > FirstRoundEventDelay; } + get { return IsFirstRound && GameMain.GameSession.RoundDuration < FirstRoundEventDelay; } } public bool CheatsEnabled; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 267cc2b79..5933cf203 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -764,7 +764,7 @@ namespace Barotrauma /// public static ImmutableHashSet GetSessionCrewCharacters(CharacterType type) { - if (GameMain.GameSession.CrewManager is not { } crewManager) { return ImmutableHashSet.Empty; } + if (GameMain.GameSession?.CrewManager is not { } crewManager) { return ImmutableHashSet.Empty; } IEnumerable players; IEnumerable bots; @@ -907,7 +907,7 @@ namespace Barotrauma { GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), RoundDuration); GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name.Value ?? "none"), RoundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), RoundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count ?? 0), RoundDuration); foreach (Mission mission in missions) { GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), RoundDuration); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/ReadyCheck.cs index 689569ff4..f1e1bc55f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/ReadyCheck.cs @@ -56,6 +56,10 @@ namespace Barotrauma } EndReadyCheck(); + +#if CLIENT + msgBox?.Close(); +#endif } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index 6872f409c..a9eabf2c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -51,7 +51,7 @@ namespace Barotrauma return slotString == null ? Array.Empty() : slotString.Split(','); } - public CharacterInventory(XElement element, Character character) + public CharacterInventory(XElement element, Character character, bool spawnInitialItems) : base(character, ParseSlotTypes(element).Length) { this.character = character; @@ -84,6 +84,8 @@ namespace Barotrauma InitProjSpecific(element); + if (!spawnInitialItems) { return; } + #if CLIENT //clients don't create items until the server says so if (GameMain.Client != null) { return; } @@ -94,7 +96,7 @@ namespace Barotrauma if (!subElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; } string itemIdentifier = subElement.GetAttributeString("identifier", ""); - if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab)) + if (!ItemPrefab.Prefabs.TryGet(itemIdentifier, out var itemPrefab)) { DebugConsole.ThrowError("Error in character inventory \"" + character.SpeciesName + "\" - item \"" + itemIdentifier + "\" not found."); continue; @@ -200,6 +202,7 @@ namespace Barotrauma #if CLIENT CreateSlots(); #endif + CharacterHUD.RecreateHudTextsIfControlling(character); //if the item was equipped and there are more items in the same stack, equip one of those items if (tryEquipFromSameStack && wasEquipped) { @@ -498,6 +501,7 @@ namespace Barotrauma HintManager.OnObtainedItem(character, item); } #endif + CharacterHUD.RecreateHudTextsIfControlling(character); if (item.CampaignInteractionType == CampaignMode.InteractionType.Cargo) { item.CampaignInteractionType = CampaignMode.InteractionType.None; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 30107af1e..d76db314d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -47,6 +47,13 @@ namespace Barotrauma.Items.Components { return ContainableItems == null || ContainableItems.Count == 0 || ContainableItems.Any(c => c.MatchesItem(itemPrefab)); } + + public bool MatchesItem(Identifier identifierOrTag) + { + return + ContainableItems == null || ContainableItems.Count == 0 || + ContainableItems.Any(c => c.Identifiers.Contains(identifierOrTag) && !c.ExcludedIdentifiers.Contains(identifierOrTag)); + } } public readonly NamedEvent OnContainedItemsChanged = new NamedEvent(); @@ -374,7 +381,7 @@ namespace Barotrauma.Items.Components // Set the contained items active if there's an item inserted inside the container. Enables e.g. the rifle flashlight when it's attached to the rifle (put inside of it). SetContainedActive(true); } - + CharacterHUD.RecreateHudTextsIfFocused(item, containedItem); OnContainedItemsChanged.Invoke(this); } @@ -386,9 +393,9 @@ namespace Barotrauma.Items.Components public void OnItemRemoved(Item containedItem) { activeContainedItems.RemoveAll(i => i.Item == containedItem); - //deactivate if the inventory is empty IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); + CharacterHUD.RecreateHudTextsIfFocused(item, containedItem); OnContainedItemsChanged.Invoke(this); } @@ -664,6 +671,18 @@ namespace Barotrauma.Items.Components return relatedItem; } + /// + /// Returns the index of the first slot whose restrictions match the specified tag or identifier + /// + public int? FindSuitableSubContainerIndex(Identifier itemTagOrIdentifier) + { + for (int i = 0; i < slotRestrictions.Length; i++) + { + if (slotRestrictions[i].MatchesItem(itemTagOrIdentifier)) { return i; } + } + return null; + } + public override void ReceiveSignal(Signal signal, Connection connection) { switch (connection.Name) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 70006e1fe..0c41133e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -603,6 +603,13 @@ namespace Barotrauma.Items.Components } partial void UpdateRequiredTimeProjSpecific(); + + private static bool AnyOneHasRecipeForItem(Character user, ItemPrefab item) + { + return + (user != null && user.HasRecipeForItem(item.Identifier)) || + GameSession.GetSessionCrewCharacters(CharacterType.Bot).Any(c => c.HasRecipeForItem(item.Identifier)); + } private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary> availableIngredients, Character character) { @@ -610,8 +617,7 @@ namespace Barotrauma.Items.Components if (fabricableItem.RequiresRecipe) { if (character == null) { return false; } - if (!character.HasRecipeForItem(fabricableItem.TargetItem.Identifier) && - GameSession.GetSessionCrewCharacters(CharacterType.Bot).None(c => c.HasRecipeForItem(fabricableItem.TargetItem.Identifier))) + if (!AnyOneHasRecipeForItem(character, fabricableItem.TargetItem)) { return false; } @@ -678,9 +684,10 @@ namespace Barotrauma.Items.Components //fabricating takes 100 times longer if degree of success is close to 0 //characters with a higher skill than required can fabricate up to 100% faster float time = fabricableItem.RequiredTime / item.StatManager.GetAdjustedValue(ItemTalentStats.FabricationSpeed, FabricationSpeed) / MathHelper.Clamp(t, 0.01f, 2.0f); - if (user is not null && fabricableItem.TargetItem is { } it && it.Tags.Contains("medical")) + + if (user?.Info is { } info && fabricableItem.TargetItem is { } it) { - time *= 1f + user.GetStatValue(StatTypes.FabricateMedicineSpeedMultiplier); + time /= 1f + it.Tags.Sum(tag => info.GetSavedStatValue(StatTypes.FabricationSpeed, tag)); } return time; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs index 8a3cd4d15..c7d8b37e3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs @@ -25,12 +25,8 @@ namespace Barotrauma.Items.Components public List LinkedHulls = new List(); } - private DateTime resetDataTime; - private bool hasPower; - private readonly Dictionary hullDatas; - [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Does the machine require inputs from water detectors in order to show the water levels inside rooms.")] public bool RequireWaterDetectors { @@ -77,7 +73,6 @@ namespace Barotrauma.Items.Components : base(item, element) { IsActive = true; - hullDatas = new Dictionary(); InitProjSpecific(); } @@ -85,37 +80,6 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - //reset data if we haven't received anything in a while - //(so that outdated hull info won't be shown if detectors stop sending signals) - if (DateTime.Now > resetDataTime) - { - foreach (HullData hullData in hullDatas.Values) - { - if (!hullData.Distort) - { - if (Timing.TotalTime > hullData.LastOxygenDataTime + 1.0) { hullData.ReceivedOxygenAmount = null; } - if (Timing.TotalTime > hullData.LastWaterDataTime + 1.0) { hullData.ReceivedWaterAmount = null; } - } - } - resetDataTime = DateTime.Now + new TimeSpan(0, 0, 1); - } - -#if CLIENT - if (cardRefreshTimer > cardRefreshDelay) - { - if (item.Submarine is { } sub) - { - UpdateIDCards(sub); - } - - cardRefreshTimer = 0; - } - else - { - cardRefreshTimer += deltaTime; - } -#endif - hasPower = Voltage > MinVoltage; if (hasPower) { @@ -140,67 +104,5 @@ namespace Barotrauma.Items.Components { return picker != null; } - - public override void ReceiveSignal(Signal signal, Connection connection) - { - Item source = signal.source; - if (source == null || source.CurrentHull == null) { return; } - - Hull sourceHull = source.CurrentHull; - if (!hullDatas.TryGetValue(sourceHull, out HullData hullData)) - { - hullData = new HullData(); - hullDatas.Add(sourceHull, hullData); - } - - if (hullData.Distort) { return; } - - switch (connection.Name) - { - case "water_data_in": - //cheating a bit because water detectors don't actually send the water level - bool fromWaterDetector = source.GetComponent() != null; - hullData.ReceivedWaterAmount = null; - hullData.LastWaterDataTime = Timing.TotalTime; - if (fromWaterDetector) - { - hullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(sourceHull); - } - foreach (var linked in sourceHull.linkedTo) - { - if (!(linked is Hull linkedHull)) { continue; } - if (!hullDatas.TryGetValue(linkedHull, out HullData linkedHullData)) - { - linkedHullData = new HullData(); - hullDatas.Add(linkedHull, linkedHullData); - } - linkedHullData.ReceivedWaterAmount = null; - if (fromWaterDetector) - { - linkedHullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(linkedHull); - } - } - break; - case "oxygen_data_in": - if (!float.TryParse(signal.value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float oxy)) - { - oxy = Rand.Range(0.0f, 100.0f); - } - hullData.ReceivedOxygenAmount = oxy; - hullData.LastOxygenDataTime = Timing.TotalTime; - foreach (var linked in sourceHull.linkedTo) - { - if (linked is not Hull linkedHull) { continue; } - if (!hullDatas.TryGetValue(linkedHull, out HullData linkedHullData)) - { - linkedHullData = new HullData(); - hullDatas.Add(linkedHull, linkedHullData); - } - linkedHullData.ReceivedOxygenAmount = oxy; - } - break; - } - } - } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 8a70da4c7..d19f9d7dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Items.Components { const float NetworkUpdateIntervalHigh = 0.5f; - const float TemperatureBoostAmount = 20; + const float TemperatureBoostAmount = 25; //the rate at which the reactor is being run on (higher rate -> higher temperature) private float fissionRate; @@ -26,10 +26,6 @@ namespace Barotrauma.Items.Components //amount of power generated balanced with the load) private bool autoTemp; - //automatical adjustment to the power output when - //turbine output and temperature are in the optimal range - private float autoAdjustAmount; - private float fuelConsumptionRate; private float meltDownTimer, meltDownDelay; @@ -53,6 +49,8 @@ namespace Barotrauma.Items.Components private float temperatureBoost; + public bool AllowTemperatureBoost => Math.Abs(temperatureBoost) < TemperatureBoostAmount * 0.9f; + private bool _powerOn; [Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes)] @@ -314,7 +312,7 @@ namespace Barotrauma.Items.Components Temperature += MathHelper.Clamp(Math.Sign(temperatureDiff) * 10.0f * deltaTime, -Math.Abs(temperatureDiff), Math.Abs(temperatureDiff)); temperatureBoost = adjustValueWithoutOverShooting(temperatureBoost, 0.0f, deltaTime); #if CLIENT - temperatureBoostUpButton.Enabled = temperatureBoostDownButton.Enabled = Math.Abs(temperatureBoost) < TemperatureBoostAmount * 0.9f; + temperatureBoostUpButton.Enabled = temperatureBoostDownButton.Enabled = AllowTemperatureBoost; #endif FissionRate = MathHelper.Lerp(fissionRate, Math.Min(TargetFissionRate, AvailableFuel), deltaTime); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs index 24a780d6e..2e432496a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs @@ -248,6 +248,7 @@ namespace Barotrauma.Items.Components if (container?.Inventory == null) { return; } + bool recreateHudTexts = false; for (var i = 0; i < container.Inventory.Capacity; i++) { if (i < 0 || GrowableSeeds.Length <= i) { continue; } @@ -257,6 +258,7 @@ namespace Barotrauma.Items.Components if (growable != null) { + recreateHudTexts |= GrowableSeeds[i] != growable; GrowableSeeds[i] = growable; growable.IsActive = true; } @@ -267,11 +269,14 @@ namespace Barotrauma.Items.Components // Kill the plant if it's somehow removed oldGrowable.Decayed = true; oldGrowable.IsActive = false; + recreateHudTexts = true; } - GrowableSeeds[i] = null; } } +#if CLIENT + CharacterHUD.RecreateHudTexts |= recreateHudTexts; +#endif // server handles this if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index b1ce29df7..4c90945ba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -9,6 +9,7 @@ namespace Barotrauma.Items.Components { //[power/min] private float capacity; + private float adjustedCapacity; private float charge, prevCharge; @@ -66,7 +67,11 @@ namespace Barotrauma.Items.Components public float Capacity { get => capacity; - set { capacity = Math.Max(value, 1.0f); } + set + { + capacity = Math.Max(value, 1.0f); + adjustedCapacity = GetCapacity(); + } } [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "The current charge of the device.")] @@ -76,10 +81,10 @@ namespace Barotrauma.Items.Components set { if (!MathUtils.IsValid(value)) return; - charge = MathHelper.Clamp(value, 0.0f, capacity); + charge = MathHelper.Clamp(value, 0.0f, adjustedCapacity); //send a network event if the charge has changed by more than 5% - if (Math.Abs(charge - lastSentCharge) / capacity > 0.05f) + if (Math.Abs(charge - lastSentCharge) / adjustedCapacity > 0.05f) { #if SERVER if (GameMain.Server != null && (!item.Submarine?.Loading ?? true)) { item.CreateServerEvent(this); } @@ -89,7 +94,7 @@ namespace Barotrauma.Items.Components } } - public float ChargePercentage => MathUtils.Percentage(Charge, GetCapacity()); + public float ChargePercentage => MathUtils.Percentage(Charge, adjustedCapacity); [Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "How fast the device can be recharged. For example, a recharge speed of 100 kW and a capacity of 1000 kW*min would mean it takes 10 minutes to fully charge the device.")] public float MaxRechargeSpeed @@ -157,6 +162,7 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { + adjustedCapacity = GetCapacity(); if (item.Connections == null) { IsActive = false; @@ -164,7 +170,7 @@ namespace Barotrauma.Items.Components } isRunning = true; - float chargeRatio = charge / capacity; + float chargeRatio = charge / adjustedCapacity; if (chargeRatio > 0.0f) { @@ -180,7 +186,7 @@ namespace Barotrauma.Items.Components item.SendSignal(((int)Math.Round(CurrPowerOutput)).ToString(), "power_value_out"); item.SendSignal(((int)Math.Round(loadReading)).ToString(), "load_value_out"); item.SendSignal(((int)Math.Round(Charge)).ToString(), "charge"); - item.SendSignal(((int)Math.Round(Charge / capacity * 100)).ToString(), "charge_%"); + item.SendSignal(((int)Math.Round(Charge / adjustedCapacity * 100)).ToString(), "charge_%"); item.SendSignal(((int)Math.Round(RechargeSpeed / maxRechargeSpeed * 100)).ToString(), "charge_rate"); } @@ -193,16 +199,16 @@ namespace Barotrauma.Items.Components if (connection == powerIn) { //Don't draw power if fully charged - if (charge >= capacity) + if (charge >= adjustedCapacity) { - charge = capacity; + charge = adjustedCapacity; return 0; } else { if (item.Condition <= 0.0f) { return 0.0f; } - float missingCharge = capacity - charge; + float missingCharge = adjustedCapacity - charge; float targetRechargeSpeed = rechargeSpeed; if (ExponentialRechargeSpeed) @@ -239,7 +245,7 @@ namespace Barotrauma.Items.Components if (connection == powerOut) { float maxOutput; - float chargeRatio = prevCharge / capacity; + float chargeRatio = prevCharge / adjustedCapacity; if (chargeRatio < 0.1f) { maxOutput = Math.Max(chargeRatio * 10.0f, 0.0f) * MaxOutPut; @@ -292,7 +298,7 @@ namespace Barotrauma.Items.Components else { //Decrease charge based on how much power is leaving the device - Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, GetCapacity()); + Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, adjustedCapacity); prevCharge = Charge; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 43a3d73a7..97f5cfd01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -639,13 +639,16 @@ namespace Barotrauma.Items.Components ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent(); if (projectileContainer != null && projectileContainer.Item != item) { - projectileContainer.Item.Use(deltaTime, null); //Use root container (e.g. loader) too in case it needs to react to firing somehow var rootContainer = projectileContainer.Item.GetRootContainer(); - if (rootContainer != projectileContainer.Item) + if (rootContainer != null && rootContainer != projectileContainer.Item) { rootContainer.Use(deltaTime, null); } + else + { + projectileContainer.Item.Use(deltaTime, null); + } } } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 2a9d2ff17..c03be21c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -576,11 +576,13 @@ namespace Barotrauma if (selectedSlot?.Inventory == this) { selectedSlot.ForceTooltipRefresh = true; } } #endif + CharacterHUD.RecreateHudTextsIfControlling(user); if (item.body != null) { item.body.Enabled = false; item.body.BodyType = FarseerPhysics.BodyType.Dynamic; + item.SetTransform(item.SimPosition, rotation: 0.0f, findNewHull: false); } #if SERVER @@ -924,6 +926,7 @@ namespace Barotrauma if (selectedSlot?.Inventory == this) { selectedSlot.ForceTooltipRefresh = true; } } #endif + CharacterHUD.RecreateHudTextsIfFocused(item); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 7dbb1f923..9dc10527d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -1092,6 +1092,12 @@ namespace Barotrauma } } + var holdables = components.Where(c => c is Holdable); + if (holdables.Count() > 1) + { + DebugConsole.AddWarning($"Item {Prefab.Identifier} has multiple {nameof(Holdable)} components ({string.Join(", ", holdables.Select(h => h.GetType().Name))})."); + } + InsertToList(); ItemList.Add(this); if (Prefab.IsDangerous) { dangerousItems.Add(this); } @@ -1104,7 +1110,6 @@ namespace Barotrauma if (HasTag("logic")) { isLogic = true; } ApplyStatusEffects(ActionType.OnSpawn, 1.0f); - Components.ForEach(c => c.ApplyStatusEffects(ActionType.OnSpawn, 1.0f)); RecalculateConditionValues(); #if CLIENT Submarine.ForceVisibilityRecheck(); @@ -2881,11 +2886,14 @@ namespace Barotrauma } foreach (ItemComponent ic in components) { ic.Equip(character); } + + CharacterHUD.RecreateHudTextsIfControlling(character); } public void Unequip(Character character) { foreach (ItemComponent ic in components) { ic.Unequip(character); } + CharacterHUD.RecreateHudTextsIfControlling(character); } public List<(object obj, SerializableProperty property)> GetProperties() @@ -3427,12 +3435,15 @@ namespace Barotrauma //if the item was in full condition considering the unmodified health //(not taking possible HealthMultipliers added by mods into account), //make sure it stays in full condition - bool wasFullCondition = prevCondition >= item.Prefab.Health; - if (wasFullCondition) + if (item.condition > 0) { - item.condition = item.MaxCondition; + bool wasFullCondition = prevCondition >= item.Prefab.Health; + if (wasFullCondition) + { + item.condition = item.MaxCondition; + } + item.condition = MathHelper.Clamp(item.condition, 0, item.MaxCondition); } - item.condition = MathHelper.Clamp(item.condition, 0, item.MaxCondition); } item.lastSentCondition = item.condition; item.RecalculateConditionValues(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 5de6d463e..41e4f333c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -27,7 +27,7 @@ namespace Barotrauma public bool IgnoreInEditor { get; set; } - private ImmutableHashSet excludedIdentifiers; + public ImmutableHashSet ExcludedIdentifiers { get; private set; } private RelationType type; @@ -87,20 +87,20 @@ namespace Barotrauma public string JoinedExcludedIdentifiers { - get { return string.Join(",", excludedIdentifiers); } + get { return string.Join(",", ExcludedIdentifiers); } set { if (value == null) return; - excludedIdentifiers = value.Split(',').Select(s => s.Trim()).ToIdentifiers().ToImmutableHashSet(); + ExcludedIdentifiers = value.Split(',').Select(s => s.Trim()).ToIdentifiers().ToImmutableHashSet(); } } public bool MatchesItem(Item item) { if (item == null) { return false; } - if (excludedIdentifiers.Contains(item.Prefab.Identifier)) { return false; } - foreach (var excludedIdentifier in excludedIdentifiers) + if (ExcludedIdentifiers.Contains(item.Prefab.Identifier)) { return false; } + foreach (var excludedIdentifier in ExcludedIdentifiers) { if (item.HasTag(excludedIdentifier)) { return false; } } @@ -118,8 +118,8 @@ namespace Barotrauma public bool MatchesItem(ItemPrefab itemPrefab) { if (itemPrefab == null) { return false; } - if (excludedIdentifiers.Contains(itemPrefab.Identifier)) { return false; } - foreach (var excludedIdentifier in excludedIdentifiers) + if (ExcludedIdentifiers.Contains(itemPrefab.Identifier)) { return false; } + foreach (var excludedIdentifier in ExcludedIdentifiers) { if (itemPrefab.Tags.Contains(excludedIdentifier)) { return false; } } @@ -138,7 +138,7 @@ namespace Barotrauma public RelatedItem(Identifier[] identifiers, Identifier[] excludedIdentifiers) { this.Identifiers = identifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet(); - this.excludedIdentifiers = excludedIdentifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet(); + this.ExcludedIdentifiers = excludedIdentifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet(); statusEffects = new List(); } @@ -230,7 +230,7 @@ namespace Barotrauma element.Add(new XAttribute(nameof(ItemPos), ItemPos.Value)); } - if (excludedIdentifiers.Count > 0) + if (ExcludedIdentifiers.Count > 0) { element.Add(new XAttribute("excludedidentifiers", JoinedExcludedIdentifiers)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs index 4daa4282a..d4c14c59e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs @@ -228,6 +228,9 @@ namespace Barotrauma continue; } + Vector2 edgeDiff = edge.Point2 - edge.Point1; + Vector2 edgeDir = Vector2.Normalize(edgeDiff); + //If the edge is next to an empty cell and there's another solid cell at the other side of the empty one, //don't touch this edge. Otherwise we may end up closing off small passages between cells. var adjacentEmptyCell = edge.AdjacentCell(cell); @@ -238,8 +241,10 @@ namespace Barotrauma //find the edge at the opposite side of the adjacent cell foreach (GraphEdge otherEdge in adjacentEmptyCell.Edges) { - if (Vector2.Dot(adjacentEmptyCell.Center - edge.Center, adjacentEmptyCell.Center - otherEdge.Center) > 0 && - otherEdge.AdjacentCell(adjacentEmptyCell)?.CellType != CellType.Solid) + if (otherEdge == edge || otherEdge.AdjacentCell(adjacentEmptyCell)?.CellType != CellType.Solid) { continue; } + Vector2 otherEdgeDir = Vector2.Normalize(otherEdge.Point2 - otherEdge.Point1); + //dot product is > 0.7 if the edges are roughly parallel + if (Math.Abs(Vector2.Dot(otherEdgeDir, edgeDir)) > 0.7f) { adjacentEdge = otherEdge; break; @@ -251,13 +256,11 @@ namespace Barotrauma continue; } } - List edgePoints = new List(); Vector2 edgeNormal = edge.GetNormal(cell); float edgeLength = Vector2.Distance(edge.Point1, edge.Point2); int pointCount = (int)Math.Max(Math.Ceiling(edgeLength / minEdgeLength), 1); - Vector2 edgeDir = edge.Point2 - edge.Point1; for (int i = 0; i <= pointCount; i++) { if (i == 0) @@ -275,7 +278,7 @@ namespace Barotrauma float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.ServerAndClient); Vector2 extrudedPoint = edge.Point1 + - edgeDir * (i / (float)pointCount) + + edgeDiff * (i / (float)pointCount) + edgeNormal * edgeLength * (roundingAmount + randomVariance) * centerF; var nearbyCells = Level.Loaded.GetCells(extrudedPoint, searchDepth: 2); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 241138e99..eec99f20b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -709,7 +709,7 @@ namespace Barotrauma if (Rand.Range(0, 10, Rand.RandSync.ServerAndClient) != 0) { continue; } } - if (!TooClose(siteX, siteY)) + if (!TooCloseToOtherSites(siteX, siteY)) { siteCoordsX.Add(siteX); siteCoordsY.Add(siteY); @@ -717,14 +717,14 @@ namespace Barotrauma if (closeToCave) { - for (int x2 = x; x2 < x + siteInterval.X; x2 += caveSiteInterval) + for (int x2 = x - siteInterval.X; x2 < x + siteInterval.X; x2 += caveSiteInterval) { - for (int y2 = y; y2 < y + siteInterval.Y; y2 += caveSiteInterval) + for (int y2 = y - siteInterval.Y; y2 < y + siteInterval.Y; y2 += caveSiteInterval) { int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient); int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient); - if (!TooClose(caveSiteX, caveSiteY)) + if (!TooCloseToOtherSites(caveSiteX, caveSiteY, caveSiteInterval)) { siteCoordsX.Add(caveSiteX); siteCoordsY.Add(caveSiteY); @@ -735,11 +735,12 @@ namespace Barotrauma } } - bool TooClose(double siteX, double siteY) + bool TooCloseToOtherSites(double siteX, double siteY, float minDistance = 10.0f) { + float minDistanceSqr = minDistance * minDistance; for (int i = 0; i < siteCoordsX.Count; i++) { - if (MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteX, siteY) < 10.0f * 10.0f) + if (MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteX, siteY) < minDistanceSqr) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 51b91d8ad..fa7470faa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -720,7 +720,7 @@ namespace Barotrauma private void HandleLevelCollision(Impact impact, VoronoiCell cell = null) { - if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 10) + if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration < 10) { //ignore level collisions for the first 10 seconds of the round in case the sub spawns in a way that causes it to hit a wall //(e.g. level without outposts to dock to and an incorrectly configured ballast that makes the sub go up) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index cd1b6ad8a..4f8157b45 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -23,13 +23,22 @@ namespace Barotrauma.Networking { Success = 0x00, Heartbeat = 0x01, + RequestShutdown = 0xCC, Crash = 0xFF } private static ManualResetEvent writeManualResetEvent; - private static volatile bool shutDown; - public static bool HasShutDown => shutDown; + private enum StatusEnum + { + NeverStarted, + Active, + RequestedShutDown, + ShutDown + } + + private static volatile StatusEnum status = StatusEnum.NeverStarted; + public static bool HasShutDown => status is StatusEnum.ShutDown; private const int ReadBufferSize = MsgConstants.MTU * 2; private static byte[] readTempBytes; @@ -38,7 +47,7 @@ namespace Barotrauma.Networking private static ConcurrentQueue msgsToWrite; private static ConcurrentQueue errorsToWrite; - + private static ConcurrentQueue msgsToRead; private static Thread readThread; @@ -48,6 +57,8 @@ namespace Barotrauma.Networking private static void PrivateStart() { + status = StatusEnum.Active; + readIncOffset = 0; readIncTotal = 0; @@ -58,8 +69,6 @@ namespace Barotrauma.Networking msgsToRead = new ConcurrentQueue(); - shutDown = false; - readCancellationToken = new CancellationTokenSource(); writeManualResetEvent = new ManualResetEvent(false); @@ -80,7 +89,13 @@ namespace Barotrauma.Networking private static void PrivateShutDown() { - shutDown = true; + if (Thread.CurrentThread != GameMain.MainThread) + { + throw new InvalidOperationException( + $"Cannot call {nameof(ChildServerRelay)}.{nameof(PrivateShutDown)} from a thread other than the main one"); + } + if (status is StatusEnum.NeverStarted) { return; } + status = StatusEnum.ShutDown; writeManualResetEvent?.Set(); readCancellationToken?.Cancel(); readThread?.Join(); readThread = null; @@ -93,18 +108,18 @@ namespace Barotrauma.Networking } - private static int ReadIncomingMsgs() + private static Option ReadIncomingMsgs() { Task readTask = readStream?.ReadAsync(readTempBytes, 0, readTempBytes.Length, readCancellationToken.Token); - if (readTask is null) { return -1; } + if (readTask is null) { return Option.None(); } int timeOutMilliseconds = 100; for (int i = 0; i < 150; i++) { - if (shutDown) + if (status is StatusEnum.ShutDown) { readCancellationToken?.Cancel(); - return -1; + return Option.None(); } try { @@ -115,36 +130,36 @@ namespace Barotrauma.Networking } catch (AggregateException aggregateException) { - if (aggregateException.InnerException is OperationCanceledException) { return -1; } + if (aggregateException.InnerException is OperationCanceledException) { return Option.None(); } throw; } catch (OperationCanceledException) { - return -1; + return Option.None(); } } - if (readTask.Status != TaskStatus.RanToCompletion) + if (readTask.Status == TaskStatus.RanToCompletion) { - bool swallowException = shutDown - && ((readTask.Exception?.InnerException is ObjectDisposedException) - || (readTask.Exception?.InnerException is System.IO.IOException)); - if (swallowException) - { - readCancellationToken?.Cancel(); - return -1; - } - throw new Exception( - $"ChildServerRelay readTask did not run to completion: status was {readTask.Status}.", - readTask.Exception); + return Option.Some(readTask.Result); } - return readTask.Result; + bool swallowException = + status is not StatusEnum.Active + && readTask.Exception?.InnerException is ObjectDisposedException or System.IO.IOException; + if (swallowException) + { + readCancellationToken?.Cancel(); + return Option.None(); + } + throw new Exception( + $"ChildServerRelay readTask did not run to completion: status was {readTask.Status}.", + readTask.Exception); } private static void CheckPipeConnected(string name, PipeType pipe) { - if (!(pipe is { IsConnected: true })) + if (status is StatusEnum.Active && pipe is not { IsConnected: true }) { throw new Exception($"{name} was disconnected unexpectedly"); } @@ -155,7 +170,7 @@ namespace Barotrauma.Networking private static void UpdateRead() { Span msgLengthSpan = stackalloc byte[4 + 1]; - while (!shutDown) + while (!HasShutDown) { CheckPipeConnected(nameof(readStream), readStream); @@ -165,10 +180,9 @@ namespace Barotrauma.Networking { if (readIncOffset >= readIncTotal) { - readIncTotal = ReadIncomingMsgs(); + if (!ReadIncomingMsgs().TryUnwrap(out readIncTotal)) { return false; } readIncOffset = 0; if (readIncTotal == 0) { Thread.Yield(); continue; } - if (readIncTotal < 0) { return false; } } readTo[i] = readTempBytes[readIncOffset]; readIncOffset++; @@ -176,7 +190,7 @@ namespace Barotrauma.Networking return true; } - if (!readBytes(msgLengthSpan)) { shutDown = true; break; } + if (!readBytes(msgLengthSpan)) { status = StatusEnum.ShutDown; break; } int msgLength = msgLengthSpan[0] | (msgLengthSpan[1] << 8) @@ -184,24 +198,24 @@ namespace Barotrauma.Networking | (msgLengthSpan[3] << 24); WriteStatus writeStatus = (WriteStatus)msgLengthSpan[4]; - if (msgLength > 0) - { - byte[] msg = new byte[msgLength]; - if (!readBytes(msg.AsSpan())) { shutDown = true; break; } + byte[] msg = msgLength > 0 ? new byte[msgLength] : Array.Empty(); + if (msg.Length > 0 && !readBytes(msg.AsSpan())) { status = StatusEnum.ShutDown; break; } - switch (writeStatus) - { - case WriteStatus.Success: - msgsToRead.Enqueue(msg); - break; - case WriteStatus.Heartbeat: - //do nothing - break; - case WriteStatus.Crash: - HandleCrashString(Encoding.UTF8.GetString(msg)); - shutDown = true; - break; - } + switch (writeStatus) + { + case WriteStatus.Success: + msgsToRead.Enqueue(msg); + break; + case WriteStatus.Heartbeat: + //do nothing + break; + case WriteStatus.RequestShutdown: + status = StatusEnum.ShutDown; + break; + case WriteStatus.Crash: + HandleCrashString(Encoding.UTF8.GetString(msg)); + status = StatusEnum.ShutDown; + break; } Thread.Yield(); @@ -210,13 +224,11 @@ namespace Barotrauma.Networking private static void UpdateWrite() { - while (!shutDown) + while (!HasShutDown) { CheckPipeConnected(nameof(writeStream), writeStream); - byte[] msg; - - void writeMsg(WriteStatus writeStatus) + void writeMsg(WriteStatus writeStatus, byte[] msg) { // It's SUPER IMPORTANT that this stack allocation // remains in this local function and is never inlined, @@ -224,21 +236,19 @@ namespace Barotrauma.Networking // when the function returns; placing it in the loop // this method is based around would lead to a stack // overflow real quick! - Span bytesToWrite = stackalloc byte[4 + 1 + msg.Length]; + Span headerBytes = stackalloc byte[4 + 1]; - bytesToWrite[0] = (byte)(msg.Length & 0xFF); - bytesToWrite[1] = (byte)((msg.Length >> 8) & 0xFF); - bytesToWrite[2] = (byte)((msg.Length >> 16) & 0xFF); - bytesToWrite[3] = (byte)((msg.Length >> 24) & 0xFF); + headerBytes[0] = (byte)(msg.Length & 0xFF); + headerBytes[1] = (byte)((msg.Length >> 8) & 0xFF); + headerBytes[2] = (byte)((msg.Length >> 16) & 0xFF); + headerBytes[3] = (byte)((msg.Length >> 24) & 0xFF); - bytesToWrite[4] = (byte)writeStatus; - Span msgSlice = bytesToWrite.Slice(4 + 1, msg.Length); - - msg.AsSpan().CopyTo(msgSlice); + headerBytes[4] = (byte)writeStatus; try { - writeStream?.Write(bytesToWrite); + writeStream?.Write(headerBytes); + writeStream?.Write(msg); } catch (Exception exception) { @@ -246,7 +256,7 @@ namespace Barotrauma.Networking { case ObjectDisposedException _: case System.IO.IOException _: - if (!shutDown) { throw; } + if (!HasShutDown) { throw; } break; default: throw; @@ -254,29 +264,34 @@ namespace Barotrauma.Networking } } + if (status is StatusEnum.RequestedShutDown) + { + writeMsg(WriteStatus.RequestShutdown, Array.Empty()); + status = StatusEnum.ShutDown; + } + while (errorsToWrite.TryDequeue(out var error)) { - msg = Encoding.UTF8.GetBytes(error); - writeMsg(WriteStatus.Crash); - shutDown = true; + writeMsg(WriteStatus.Crash, Encoding.UTF8.GetBytes(error)); + status = StatusEnum.ShutDown; } - while (msgsToWrite.TryDequeue(out msg)) + while (msgsToWrite.TryDequeue(out var msg)) { - writeMsg(WriteStatus.Success); + writeMsg(WriteStatus.Success, msg); - if (shutDown) { break; } + if (HasShutDown) { break; } } - if (!shutDown) + if (!HasShutDown) { writeManualResetEvent.Reset(); if (!writeManualResetEvent.WaitOne(1000)) { - if (shutDown) { return; } + if (HasShutDown) { return; } //heartbeat to keep the other end alive - msg = Array.Empty(); writeMsg(WriteStatus.Heartbeat); + writeMsg(WriteStatus.Heartbeat, Array.Empty()); } } } @@ -284,7 +299,7 @@ namespace Barotrauma.Networking public static void Write(byte[] msg) { - if (shutDown) { return; } + if (HasShutDown) { return; } if (msg.Length > 0x1fff_ffff) { @@ -298,7 +313,7 @@ namespace Barotrauma.Networking public static bool Read(out byte[] msg) { - if (shutDown) { msg = null; return false; } + if (HasShutDown) { msg = null; return false; } return msgsToRead.TryDequeue(out msg); } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 04adf2e25..c5f999537 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,62 @@ +--------------------------------------------------------------------------------------------------------- +v0.20.9.0 +--------------------------------------------------------------------------------------------------------- + +Changes and additions: +- Added a button to the main menu that can be used to update all installed mods when there's updates available. +- Optimized the server lobby: there was an issue in the logic that updates the microphone icon that caused the game to check available audio devices every frame. +- Optimized status monitors: previously some parts of their UI were always updated regardless if anyone is viewing the UI. +- Made flak cannons a bit more quiet (so they don't drown out all other sounds). + +Unstable only: +- Don't allow placing most of the new creature loot in cabinets/containers. +- Fixed some more vanilla subs' reactors having 1000 condition instead of 100. +- Adjustments to Harpoon Coil-Rifle sounds. +- Two new music tracks (one "default" one for missions and another for colonies). +- Fixed no events triggering in the first campaign outpost. +- Fixed crashing when firing a turret that doesn't use ammo boxes + ammo boxes getting used twice when firing. +- Fixed water-reactant items emitting particles when submerged even when inside a waterproof container + optimized the effects a bit. +- Allow recycling ammo boxes that are below 10% full. +- Fixed autoshotgun's ammunition indicator showing 1 item less than it should when there's no flashlight in the flashlight slot. +- Fixed "censorship" event (in which a clown asks you to retrieve a confiscated crate) being impossible to complete. +- Fixed crash when opening the fabricator UI in the sub editor. +- Improvements to the new beacon station clown event. +- Fixed passive sonar still revealing more than it should when there's directional pings active. +- Fixed inventory being visible when using a periscope in the sub editor test mode. +- Fixed inability to get out of the clown crate in multiplayer. +- Fixed "Lab Contacts" increasing the medical fabrication speed instead of reducing it. +- Fixed "All talents unlocked" showing before you had unlocked all talents. +- Fixed "Tinkerer" not working at all. +- Fixed "Quickfixer" not doubling the repair speed. +- Fixed exosuit draining the battery when not worn. +- Fixed exosuit consuming all contained tanks when there's a battery in it. +- Fixed bots failing to swap tanks in the exosuit. +- Removed the flashlight slot from the machine pistol. +- Fixed Defensebot's movement outside of the submarine. +- Petraptors poop. +- Fix a console error after a loaded game when a defense bot is present. Happened because we tried to spawn the initial items in the inventory, which was full. +- Fixed softlock if you get killed by barotrauma when under the effect of Miracle Worker. +- Fixed fabricator displaying items that can be crafted using a bot's talents as requiring a recipe to craft. +- Fixed fabricating nuclear shells and nuclear depth charges with the cheaper recipe unlocked by "Nuclear Option" not fully using up the fuel rods. + +Bugfixes: +- Fixed occasional crashes when shutting down a server (for example with the error messages "pipe is broken" or "ChildServerRelay readTask did not run to completion") +- Fixed junction boxes not getting damaged by water since the power rework. +- Fixed opiate withdrawal only reducing down to 20%, but never fully healing by itself. +- Fixed engines reverting back to the non-damaged sprite when they're damaged badly enough that the sprite starts shaking. +- Fixed walls being set up incorrectly in vertical abandoned outpost hallway modules, causing them to stick out into the connected modules. +- Attempt to fix items sometimes ending up rotated inside a container (e.g. diving suit sprite appearing rotated on a diving suit locker). +- Fixed "man and his raptor" outpost event giving 1000 marks in an incorrect branch of the dialog (the one where you immediately accept the NPC on board, instead of the one where the NPC says they'll pay you 1000 mk). +- Fixed cases of interaction texts for focused item (most notoriously, the planter) not being updated correctly. +- Fixed "snap to grid" causing door gaps to get misaligned. +- Fixed weird equipping behavior on fruit and paints, causing them to be equipped in both hands when trying to unequip. +- Yet another fix to cave tunnels sometimes being too narrow to pass through. +- Fixed minerals sometimes being placed outside the level in mineral missions. +- Fixed reactor temperature boost not working in multiplayer. + +Modding: +- Fixed item's OnSpawn effects being applied twice. + --------------------------------------------------------------------------------------------------------- v0.20.8.0 ---------------------------------------------------------------------------------------------------------