diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Objectives/AIObjective.cs index db9309bbe..0c21b2956 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Objectives/AIObjective.cs @@ -1,7 +1,11 @@ -namespace Barotrauma +using Microsoft.Xna.Framework; + +namespace Barotrauma { abstract partial class AIObjective { + public static Color ObjectiveIconColor => Color.LightGray; + public static Sprite GetSprite(string identifier, string option, Entity targetEntity) { if (string.IsNullOrEmpty(identifier)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 83e1255cb..f129f04ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -154,7 +154,7 @@ namespace Barotrauma public bool PlaySound; - public GUIMessage(string rawText, Color color, float delay, string identifier = null, int? value = null) + public GUIMessage(string rawText, Color color, float delay, string identifier = null, int? value = null, float lifeTime = 3.0f) { RawText = Text = rawText; if (value.HasValue) @@ -166,7 +166,7 @@ namespace Barotrauma Size = GUI.Font.MeasureString(Text); Color = color; Identifier = identifier; - Lifetime = 3.0f; + Lifetime = lifeTime; } } @@ -997,7 +997,7 @@ namespace Barotrauma return nameColor; } - public void AddMessage(string rawText, Color color, bool playSound, string identifier = null, int? value = null) + public void AddMessage(string rawText, Color color, bool playSound, string identifier = null, int? value = null, float lifetime = 3.0f) { GUIMessage existingMessage = null; @@ -1026,7 +1026,7 @@ namespace Barotrauma } if (existingMessage == null || !value.HasValue) { - var newMessage = new GUIMessage(rawText, color, delay, identifier, value); + var newMessage = new GUIMessage(rawText, color, delay, identifier, value, lifetime); guiMessages.Insert(0, newMessage); if (playSound) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 26704a982..0446de782 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -113,7 +113,7 @@ namespace Barotrauma return character?.Inventory != null && - character.AllowInput && + !character.Removed && !character.IsKnockedDown && (controller?.User != character || !controller.HideHUD) && !IsCampaignInterfaceOpen && !ConversationAction.FadeScreenToBlack; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 754c97f22..025aadcd0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -16,6 +16,7 @@ namespace Barotrauma private static Sprite infoAreaPortraitBG; public bool LastControlled; + public int CrewListIndex { get; set; } = -1; #warning TODO: Refactor private Sprite disguisedPortrait; @@ -831,7 +832,7 @@ namespace Barotrauma }; new GUIFrame( - new RectTransform(new Vector2(1.25f, 1.25f), HeadSelectionList.RectTransform, Anchor.Center), + new RectTransform(new Vector2(1.25f, 1.25f), HeadSelectionList.ContentBackground.RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black) { UserData = "outerglow", @@ -966,10 +967,15 @@ namespace Barotrauma foreach (Sprite sprite in characterSprites) { sprite.Remove(); } characterSprites.Clear(); } - + public void Dispose() { ClearSprites(); + if (HeadSelectionList != null) + { + HeadSelectionList.RectTransform.Parent = null; + HeadSelectionList = null; + } } ~AppearanceCustomizationMenu() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs index e08502aa9..20249f49b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs @@ -143,7 +143,7 @@ namespace Barotrauma Hull fireHull = Hull.hullList.GetRandom(h => h.Submarine == character.Submarine); if (fireHull != null) { - var fakeFire = new DummyFireSource(Vector2.One * 500.0f, new Vector2(Rand.Range(fireHull.WorldRect.X, fireHull.WorldRect.Right), fireHull.WorldPosition.Y), fireHull, isNetworkMessage: true) + var fakeFire = new DummyFireSource(Vector2.One * 500.0f, new Vector2(Rand.Range(fireHull.WorldRect.X, fireHull.WorldRect.Right), fireHull.WorldPosition.Y + 1), fireHull, isNetworkMessage: true) { CausedByPsychosis = true, DamagesItems = false, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index acf9b7fd7..7d5753278 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -2018,6 +2018,27 @@ namespace Barotrauma limbIndicatorOverlay?.Remove(); limbIndicatorOverlay = null; + + if (healthWindow != null) + { + healthWindow.RectTransform.Parent = null; + healthWindow = null; + } + if (healthBarHolder != null) + { + healthBarHolder.RectTransform.Parent = null; + healthBarHolder = null; + } + if (SuicideButton != null) + { + SuicideButton.RectTransform.Parent = null; + SuicideButton = null; + } + if (afflictionTooltip != null) + { + afflictionTooltip.RectTransform.Parent = null; + afflictionTooltip = null; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 5f6afa2e3..2dc2d7ec9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -389,6 +389,12 @@ namespace Barotrauma DrawString(spriteBatch, new Vector2(10, 10), "FPS: " + Math.Round(GameMain.PerformanceCounter.AverageFramesPerSecond), Color.White, Color.Black * 0.5f, 0, SmallFont); + if (GameMain.GameSession != null && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 1.0) + { + DrawString(spriteBatch, new Vector2(10, 25), + $"Physics: {GameMain.CurrentUpdateRate}", + (GameMain.CurrentUpdateRate < Timing.FixedUpdateRate) ? Color.Red : Color.White, Color.Black * 0.5f, 0, SmallFont); + } } if (GameMain.ShowPerf) @@ -397,7 +403,7 @@ namespace Barotrauma DrawString(spriteBatch, new Vector2(300, y), "Draw - Avg: " + GameMain.PerformanceCounter.DrawTimeGraph.Average().ToString("0.00") + " ms" + " Max: " + GameMain.PerformanceCounter.DrawTimeGraph.LargestValue().ToString("0.00") + " ms", - GUI.Style.Green, Color.Black * 0.8f, font: SmallFont); + Style.Green, Color.Black * 0.8f, font: SmallFont); y += 15; GameMain.PerformanceCounter.DrawTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), color: Style.Green); y += 50; @@ -408,7 +414,6 @@ namespace Barotrauma Color.LightBlue, Color.Black * 0.8f, font: SmallFont); y += 15; GameMain.PerformanceCounter.UpdateTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), color: Color.LightBlue); - GameMain.PerformanceCounter.UpdateIterationsGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), maxValue: 20, color: Style.Red); y += 50; foreach (string key in GameMain.PerformanceCounter.GetSavedIdentifiers) { @@ -431,7 +436,7 @@ namespace Barotrauma } } - if (GameMain.DebugDraw) + if (GameMain.DebugDraw && !Submarine.Unloading && !(Screen.Selected is RoundSummaryScreen)) { DrawString(spriteBatch, new Vector2(10, 25), "Physics: " + GameMain.World.UpdateTime, @@ -2435,6 +2440,11 @@ namespace Barotrauma private static bool TogglePauseMenu(GUIButton button, object obj) { pauseMenuOpen = !pauseMenuOpen; + if (!pauseMenuOpen && PauseMenu != null) + { + PauseMenu.RectTransform.Parent = null; + PauseMenu = null; + } return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs index a88ea3e89..c555ce055 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs @@ -22,13 +22,14 @@ namespace Barotrauma { GameMain.Instance.ResolutionChanged += RecalculateSize; } - _instance.ItemComponentHolder = new GUIFrame(new RectTransform(Vector2.One, _instance, Anchor.Center)).RectTransform; + _instance.ChildrenChanged += OnChildrenChanged; } return _instance; } } - public RectTransform ItemComponentHolder; + //GUICanvas stores the children as weak references, to allow elements that we no longer need to get garbage collected + private readonly List> childrenWeakRef = new List>(); private static Vector2 size => new Vector2(GameMain.GraphicsWidth / (float)GUI.UIWidth, 1f); @@ -36,16 +37,41 @@ namespace Barotrauma private enum ResizeAxis { Both = 0, X = 1, Y = 2 } + private static void OnChildrenChanged(RectTransform _) + { + //add weak reference if we don't have one yet + foreach (var child in _instance.Children) + { + if (!_instance.childrenWeakRef.Any(c => c.TryGetTarget(out var existingChild) && existingChild == child)) + { + _instance.childrenWeakRef.Add(new WeakReference(child)); + } + } + //get rid of strong references + _instance.children.Clear(); + //remove dead children + for (int i = _instance.childrenWeakRef.Count - 2; i >= 0; i--) + { + if (!_instance.childrenWeakRef[i].TryGetTarget(out var child) || child.Parent != _instance) + { + _instance.childrenWeakRef.RemoveAt(i); + } + } + } + // Turn public, if there is a need to call this manually. private static void RecalculateSize() { Vector2 recalculatedSize = size; // Scale children that are supposed to encompass the whole screen so that they are properly scaled on ultrawide as well - for (int i = 0; i < Instance.Children.Count(); i++) + for (int i = 0; i < Instance.childrenWeakRef.Count; i++) { - RectTransform target = Instance.GetChild(i); - if (target == null || target.RelativeSize.X < 1 && target.RelativeSize.Y < 1) continue; + if (!_instance.childrenWeakRef[i].TryGetTarget(out RectTransform target) || target == null) { continue; }; + + _instance.children.Add(target); + + if (target.RelativeSize.X < 1 && target.RelativeSize.Y < 1) { continue; } ResizeAxis axis; @@ -80,6 +106,7 @@ namespace Barotrauma Instance.Resize(size, resizeChildren: true); Instance.GetAllChildren().Select(c => c.GUIComponent as GUITextBlock).ForEach(t => t?.SetTextPos()); + _instance.children.Clear(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index ba0faa08c..6c0c03e91 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -362,6 +362,14 @@ namespace Barotrauma RectTransform.ScaleChanged += () => dimensionsNeedsRecalculation = true; RectTransform.SizeChanged += () => dimensionsNeedsRecalculation = true; UpdateDimensions(); + + rectT.ChildrenChanged += CheckForChildren; + } + + private void CheckForChildren(RectTransform rectT) + { + if (rectT == ScrollBar.RectTransform || rectT == Content.RectTransform || rectT == ContentBackground.RectTransform) { return; } + throw new InvalidOperationException($"Children were added to {nameof(GUIListBox)}, Add them to {nameof(GUIListBox)}.{nameof(Content)} instead."); } public void UpdateDimensions() @@ -827,13 +835,6 @@ namespace Barotrauma protected override void Update(float deltaTime) { - foreach (GUIComponent child in Children) - { - if (child == ScrollBar || child == Content || child == ContentBackground) { continue; } - - throw new InvalidOperationException($"Children were found in {nameof(GUIListBox)}, Add them to {nameof(GUIListBox)}.{nameof(Content)} instead."); - } - if (!Visible) { return; } UpdateChildrenRect(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index 62ebe85bb..4ec5929f3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -618,6 +618,7 @@ namespace Barotrauma public bool Close(GUIButton button, object obj) { + RectTransform.Parent = null; Close(); return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index dc6321e84..4a7000b31 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -347,7 +347,7 @@ namespace Barotrauma // sum up all the afflictions and their strengths Dictionary afflictionAndStrength = new Dictionary(); - foreach (Affliction affliction in health.GetAllAfflictions().Where(a => !a.Prefab.IsBuff && a.Strength > 0)) + foreach (Affliction affliction in health.GetAllAfflictions().Where(a => MedicalClinic.IsHealable(a))) { if (afflictionAndStrength.TryGetValue(affliction.Prefab, out float strength)) { @@ -581,7 +581,6 @@ namespace Barotrauma OnClicked = (button, _) => { button.Enabled = false; - ClosePopup(); medicalClinic.HealAllButtonAction(request => { switch (request.HealResult) @@ -595,7 +594,9 @@ namespace Barotrauma } button.Enabled = true; + ClosePopup(); }); + ClosePopup(); return true; } }; @@ -993,7 +994,7 @@ namespace Barotrauma } } - private void ClosePopup() + public void ClosePopup() { if (selectedCrewElement is { } popup) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs index 045547d48..5a750998d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs @@ -58,7 +58,7 @@ namespace Barotrauma } } - private readonly List children = new List(); + protected readonly List children = new List(); public IEnumerable Children => children; public int CountChildren => children.Count; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 22f06e9cc..863202f87 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -20,7 +20,7 @@ namespace Barotrauma private static Sprite ownerIcon, moderatorIcon; public enum InfoFrameTab { Crew, Mission, Reputation, Traitor, Submarine, Talents }; - public static InfoFrameTab selectedTab; + public static InfoFrameTab SelectedTab { get; private set; } private GUIFrame infoFrame, contentFrame; private readonly List tabButtons = new List(); @@ -130,8 +130,8 @@ namespace Barotrauma { if (!initialized) { Initialize(); } - CreateInfoFrame(selectedTab); - SelectInfoFrameTab(null, selectedTab); + CreateInfoFrame(SelectedTab); + SelectInfoFrameTab(SelectedTab); } public void Update() @@ -147,8 +147,8 @@ namespace Barotrauma } } - if (selectedTab != InfoFrameTab.Crew) return; - if (linkedGUIList == null) return; + if (SelectedTab != InfoFrameTab.Crew) { return; } + if (linkedGUIList == null) { return; } if (GameMain.IsMultiplayer) { @@ -226,7 +226,7 @@ namespace Barotrauma { UserData = tab, ToolTip = TextManager.Get(textTag), - OnClicked = SelectInfoFrameTab + OnClicked = (btn, userData) => { SelectInfoFrameTab((InfoFrameTab)userData); return true; } }; tabButtons.Add(newButton); return newButton; @@ -277,16 +277,16 @@ namespace Barotrauma talentsButton.Enabled = Character.Controlled?.Info != null; if (!talentsButton.Enabled && selectedTab == InfoFrameTab.Talents) { - SelectInfoFrameTab(null, InfoFrameTab.Crew); + SelectInfoFrameTab(InfoFrameTab.Crew); } }; talentPointNotification = GameSession.CreateTalentIconNotification(talentsButton); } - private bool SelectInfoFrameTab(GUIButton button, object userData) + public void SelectInfoFrameTab(InfoFrameTab selectedTab) { - selectedTab = (InfoFrameTab)userData; + SelectedTab = selectedTab; CreateInfoFrame(selectedTab); tabButtons.ForEach(tb => tb.Selected = (InfoFrameTab)tb.UserData == selectedTab); @@ -310,7 +310,7 @@ namespace Barotrauma case InfoFrameTab.Traitor: TraitorMissionPrefab traitorMission = GameMain.Client.TraitorMission; Character traitor = GameMain.Client.Character; - if (traitor == null || traitorMission == null) return false; + if (traitor == null || traitorMission == null) { return; } CreateTraitorInfo(infoFrameHolder, traitorMission, traitor); break; case InfoFrameTab.Submarine: @@ -320,8 +320,6 @@ namespace Barotrauma CreateTalentInfo(infoFrameHolder); break; } - - return true; } private const float jobColumnWidthPercentage = 0.138f; @@ -859,7 +857,7 @@ namespace Barotrauma string msg = ChatMessage.GetTimeStamp() + message.TextWithSender; storedMessages.Add(new Pair(msg, message.ChangeType)); - if (GameSession.IsTabMenuOpen && selectedTab == InfoFrameTab.Crew) + if (GameSession.IsTabMenuOpen && SelectedTab == InfoFrameTab.Crew) { TabMenu instance = GameSession.TabMenuInstance; instance.AddLineToLog(msg, message.ChangeType); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 67dcfc6c1..0536aa334 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -433,6 +433,7 @@ namespace Barotrauma if (AvailableMoney >= hullRepairCost) { Campaign.Money -= hullRepairCost; + GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); Campaign.PurchasedHullRepairs = true; button.Enabled = false; SelectTab(UpgradeTab.Repairs); @@ -467,6 +468,7 @@ namespace Barotrauma if (AvailableMoney >= itemRepairCost && !Campaign.PurchasedItemRepairs) { Campaign.Money -= itemRepairCost; + GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); Campaign.PurchasedItemRepairs = true; button.Enabled = false; SelectTab(UpgradeTab.Repairs); @@ -512,6 +514,7 @@ namespace Barotrauma if (AvailableMoney >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) { Campaign.Money -= shuttleRetrieveCost; + GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); Campaign.PurchasedLostShuttles = true; button.Enabled = false; SelectTab(UpgradeTab.Repairs); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs index 03683f3f8..8f0c177f2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs @@ -5,7 +5,7 @@ using System.Linq; namespace Barotrauma { - public static partial class GameAnalyticsManager + static partial class GameAnalyticsManager { static partial void CreateConsentPrompt() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 15f703b8f..09af81a07 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -30,6 +30,13 @@ namespace Barotrauma public static PerformanceCounter PerformanceCounter; + private static Stopwatch performanceCounterTimer; + private static int updateCount = 0; + public static int CurrentUpdateRate + { + get; private set; + } + public static readonly Version Version = Assembly.GetEntryAssembly().GetName().Version; public static string[] ConsoleArguments; @@ -122,6 +129,12 @@ namespace Barotrauma private bool exiting; + public static bool IsFirstLaunch + { + get; + private set; + } + public static GameMain Instance { get; @@ -349,6 +362,8 @@ namespace Barotrauma Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Item)); Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Items.Components.ItemComponent)); Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Hull)); + + performanceCounterTimer = Stopwatch.StartNew(); } /// @@ -579,6 +594,18 @@ namespace Barotrauma { Steamworks.SteamFriends.OnGameRichPresenceJoinRequested += OnInvitedToGame; Steamworks.SteamFriends.OnGameLobbyJoinRequested += OnLobbyJoinRequested; + + if (SteamManager.TryGetUnlockedAchievements(out List achievements)) + { + //check the achievements too, so we don't consider people who've played the game before this "gamelaunchcount" stat was added as being 1st-time-players + //(people who have played previous versions, but not unlocked any achievements, will be incorrectly considered 1st-time-players, but that should be a small enough group to not skew the statistics) + if (!achievements.Any() && SteamManager.GetStatInt("gamelaunchcount") <= 0) + { + IsFirstLaunch = true; + GameAnalyticsManager.AddDesignEvent("FirstLaunch"); + } + } + SteamManager.IncrementStat("gamelaunchcount", 1); } #endif @@ -696,11 +723,10 @@ namespace Barotrauma protected override void Update(GameTime gameTime) { Timing.Accumulator += gameTime.ElapsedGameTime.TotalSeconds; - int updateIterations = (int)Math.Floor(Timing.Accumulator / Timing.Step); - if (Timing.Accumulator > Timing.Step * 6.0) + if (Timing.Accumulator > Timing.AccumulatorMax) { - //if the game's running too slowly then we have no choice - //but to skip a bunch of steps + //prevent spiral of death: + //if the game's running too slowly then we have no choice but to skip a bunch of steps //otherwise it snowballs and becomes unplayable Timing.Accumulator = Timing.Step; } @@ -736,7 +762,6 @@ namespace Barotrauma PlayerInput.Update(Timing.Step); - if (loadingScreenOpen) { //reset accumulator if loading @@ -975,13 +1000,21 @@ namespace Barotrauma Timing.Accumulator -= Timing.Step; + updateCount++; + sw.Stop(); PerformanceCounter.AddElapsedTicks("Update total", sw.ElapsedTicks); PerformanceCounter.UpdateTimeGraph.Update(sw.ElapsedTicks * 1000.0f / (float)Stopwatch.Frequency); - PerformanceCounter.UpdateIterationsGraph.Update(updateIterations); } - if (!Paused) Timing.Alpha = Timing.Accumulator / Timing.Step; + if (!Paused) { Timing.Alpha = Timing.Accumulator / Timing.Step; } + + if (performanceCounterTimer.ElapsedMilliseconds > 1000) + { + CurrentUpdateRate = (int)Math.Round(updateCount / (double)(performanceCounterTimer.ElapsedMilliseconds / 1000.0)); + performanceCounterTimer.Restart(); + updateCount = 0; + } } public static void ResetFrameTime() @@ -1086,8 +1119,15 @@ namespace Barotrauma { double roundDuration = Timing.TotalTime - GameSession.RoundStartTime; GameAnalyticsManager.AddProgressionEvent(GameAnalyticsManager.ProgressionStatus.Fail, - GameSession.GameMode?.Name ?? "none", + GameSession.GameMode?.Preset.Identifier ?? "none", roundDuration); + string eventId = "QuitRound:" + (GameSession.GameMode?.Preset.Identifier ?? "none") + ":"; + GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:CurrentIntensity", GameSession.EventManager.CurrentIntensity); + foreach (var activeEvent in GameSession.EventManager.ActiveEvents) + { + GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:ActiveEvents:" + activeEvent.ToString()); + } + GameSession.LogEndRoundStats(eventId); if (Tutorial.Initialized) { ((TutorialMode)GameSession.GameMode).Tutorial?.Stop(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index a92ac9d93..f64fef7c7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -232,6 +232,8 @@ namespace Barotrauma Location.StoreCurrentBalance -= itemValue; campaign.Money += itemValue; + GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier); + // Remove from the sell crate // TODO: Simplify duplicate logic? if (sellingMode == Store.StoreTab.Sell && ItemsInSellCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } inventoryItem) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 8b0534b26..38d4ab93f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -83,7 +83,7 @@ namespace Barotrauma partial void InitProjectSpecific() { - guiFrame = new GUIFrame(new RectTransform(Vector2.One, GUICanvas.Instance), null, Color.Transparent) + guiFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), null, Color.Transparent) { CanBeFocused = false }; @@ -302,7 +302,7 @@ namespace Barotrauma /// /// The character to remove /// If the character info is also removed, the character will not be visible in the round summary. - public void RemoveCharacter(Character character, bool removeInfo = false) + public void RemoveCharacter(Character character, bool removeInfo = false, bool resetCrewListIndex = true) { if (character == null) { @@ -311,14 +311,15 @@ namespace Barotrauma } characters.Remove(character); if (removeInfo) { characterInfos.Remove(character.Info); } + if (resetCrewListIndex) { ResetCrewListIndex(character); } } /// /// Add character to the list without actually adding it to the crew /// - public void AddCharacterToCrewList(Character character) + public GUIComponent AddCharacterToCrewList(Character character) { - if (character == null) { return; } + if (character == null) { return null; } var background = new GUIFrame( new RectTransform(crewListEntrySize, parent: crewList.Content.RectTransform, anchor: Anchor.TopRight), @@ -510,6 +511,8 @@ namespace Barotrauma return true; } }; + + return background; } private void SetCharacterComponentTooltip(GUIComponent characterComponent) @@ -549,13 +552,13 @@ namespace Barotrauma if (characterInfos.Contains(revivedCharacter.Info)) { AddCharacter(revivedCharacter); } } - public void KillCharacter(Character killedCharacter) + public void KillCharacter(Character killedCharacter, bool resetCrewListIndex = true) { if (crewList.Content.GetChildByUserData(killedCharacter) is GUIComponent characterComponent) { CoroutineManager.StartCoroutine(KillCharacterAnim(characterComponent)); } - RemoveCharacter(killedCharacter); + RemoveCharacter(killedCharacter, resetCrewListIndex: resetCrewListIndex); } private IEnumerable KillCharacterAnim(GUIComponent component) @@ -601,9 +604,53 @@ namespace Barotrauma { if (crewList != this.crewList) { return; } if (!(draggedElementData is Character)) { return; } - if (crewList.HasDraggedElementIndexChanged) { return; } if (!IsSinglePlayer) { return; } - CharacterClicked(crewList.DraggedElement, draggedElementData); + if (crewList.HasDraggedElementIndexChanged) + { + UpdateCrewListIndices(); + } + else + { + CharacterClicked(crewList.DraggedElement, draggedElementData); + } + } + + private void ResetCrewListIndex(Character c) + { + if (c?.Info == null) { return; } + c.Info.CrewListIndex = -1; + UpdateCrewListIndices(); + } + + private void UpdateCrewListIndices() + { + if (crewList == null) { return; } + for (int i = 0; i < crewList.Content.CountChildren; i++) + { + var characterComponent = crewList.Content.GetChild(i); + if (!(characterComponent?.UserData is Character c)) { continue; } + if (c.Info == null) { continue; } + c.Info.CrewListIndex = i; + } + } + + private void SortCrewList() + { + if (crewList == null) { return; } + crewList.Content.RectTransform.SortChildren((x, y) => + { + var infoX = (x.GUIComponent.UserData as Character)?.Info?.CrewListIndex; + var infoY = (y.GUIComponent.UserData as Character)?.Info?.CrewListIndex; + if (infoX.HasValue) + { + return infoY.HasValue ? infoX.Value.CompareTo(infoY.Value) : -1; + } + else + { + return infoY.HasValue ? 1 : 0; + } + }); + UpdateCrewListIndices(); } #endregion @@ -1077,7 +1124,13 @@ namespace Barotrauma private string CreateOrderTooltip(Order orderPrefab, string option, Entity targetEntity) { if (orderPrefab == null) { return ""; } - if (!string.IsNullOrEmpty(option)) + if (orderPrefab.DisplayGiverInTooltip && orderPrefab.OrderGiver != null) + { + return TextManager.GetWithVariables("crewlistordericontooltip", + new string[2] { "[ordername]", "[orderoption]" }, + new string[2] { orderPrefab.Name, orderPrefab.OrderGiver.DisplayName }); + } + else if (!string.IsNullOrEmpty(option)) { return TextManager.GetWithVariables("crewlistordericontooltip", new string[2] { "[ordername]", "[orderoption]" }, @@ -1210,6 +1263,10 @@ namespace Barotrauma DisableCommandUI(); Character.Controlled = character; HintManager.OnChangeCharacter(); + if (GameSession.TabMenuInstance != null && TabMenu.SelectedTab == TabMenu.InfoFrameTab.Talents) + { + GameSession.TabMenuInstance.SelectInfoFrameTab(TabMenu.SelectedTab); + } } private int TryAdjustIndex(int amount) @@ -1566,10 +1623,28 @@ namespace Barotrauma { crewList.Select(character, force: true); } + // Icon colors might change based on the target so we check if they need to be updated + if (GetCurrentOrderIconList(characterComponent) is GUIListBox currentOrderIconList) + { + foreach (var orderIcon in currentOrderIconList.Content.Children) + { + if (!(orderIcon.UserData is OrderInfo orderInfo)) { continue; } + if (!(orderInfo.Order is Order order)) { continue; } + if (order.ColoredWhenControllingGiver && order.OrderGiver != Character.Controlled) + { + orderIcon.Color = AIObjective.ObjectiveIconColor; + } + else + { + orderIcon.Color = order.Color; + } + } + } + // Only update the order highlights and objective icons here in singleplayer + // The server will let the clients know when they need to update in multiplayer if (GameMain.IsSingleplayer && character.IsBot && character.AIController is HumanAIController controller && controller.ObjectiveManager is AIObjectiveManager objectiveManager) { - // In multiplayer, these are set through character networking (the server lets the clients now when these are updated) if (objectiveManager.CurrentObjective is AIObjective currentObjective) { if (objectiveManager.IsOrder(currentObjective)) @@ -1638,8 +1713,8 @@ namespace Barotrauma bool foundMatch = false; foreach (var orderIcon in currentOrderIconList.Content.Children) { - var glowComponent = orderIcon.GetChildByUserData("glow"); - if (glowComponent == null) { continue; } + if (!(orderIcon.GetChildByUserData("glow") is GUIComponent glowComponent)) { continue; } + glowComponent.Color = orderIcon.Color; if (foundMatch) { glowComponent.Visible = false; @@ -1684,11 +1759,11 @@ namespace Barotrauma objectiveIconFrame.ClearChildren(); if (sprite != null) { - var objectiveIcon = CreateNodeIcon(Vector2.One, objectiveIconFrame.RectTransform, sprite, Color.LightGray, tooltip: tooltip); + var objectiveIcon = CreateNodeIcon(Vector2.One, objectiveIconFrame.RectTransform, sprite, AIObjective.ObjectiveIconColor, tooltip: tooltip); new GUIFrame(new RectTransform(new Vector2(1.5f), objectiveIcon.RectTransform, anchor: Anchor.Center), style: "OuterGlowCircular") { CanBeFocused = false, - Color = Color.LightGray + Color = AIObjective.ObjectiveIconColor }; objectiveIconFrame.Visible = true; } @@ -1909,7 +1984,7 @@ namespace Barotrauma ScaleCommandUI(); commandFrame = new GUIFrame( - new RectTransform(Vector2.One, GUICanvas.Instance, anchor: Anchor.Center), + new RectTransform(Vector2.One, GUI.Canvas, anchor: Anchor.Center), style: null, color: Color.Transparent); background = new GUIImage( @@ -3530,12 +3605,14 @@ namespace Barotrauma public void Save(XElement parentElement) { XElement element = new XElement("crew"); - foreach (CharacterInfo ci in characterInfos) + for (int i = 0; i < characterInfos.Count; i++) { + var ci = characterInfos[i]; var infoElement = ci.Save(element); if (ci.InventoryData != null) { infoElement.Add(ci.InventoryData); } if (ci.HealthData != null) { infoElement.Add(ci.HealthData); } if (ci.OrderData != null) { infoElement.Add(ci.OrderData); } + infoElement.Add(new XAttribute("crewlistindex", ci.CrewListIndex)); if (ci.LastControlled) { infoElement.Add(new XAttribute("lastcontrolled", true)); } } SaveActiveOrders(element); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index a0876e3da..3b1fc8b2b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -97,7 +97,7 @@ namespace Barotrauma partial void InitProjSpecific() { - var buttonContainer = new GUILayoutGroup(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.ButtonAreaTop, GUICanvas.Instance), + var buttonContainer = new GUILayoutGroup(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.ButtonAreaTop, GUI.Canvas), isHorizontal: true, childAnchor: Anchor.CenterRight) { CanBeFocused = false @@ -108,7 +108,7 @@ namespace Barotrauma buttonCenter = buttonHeight / 2, screenMiddle = GameMain.GraphicsWidth / 2; - endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle - buttonWidth / 2, HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonCenter, buttonWidth, buttonHeight), GUICanvas.Instance), + endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle - buttonWidth / 2, HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonCenter, buttonWidth, buttonHeight), GUI.Canvas), TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") { Pulse = true, @@ -145,7 +145,7 @@ namespace Barotrauma int readyButtonHeight = buttonHeight; int readyButtonWidth = (int) (GUI.Scale * 50); - ReadyCheckButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle + (buttonWidth / 2) + GUI.IntScale(16), HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonCenter, readyButtonWidth, readyButtonHeight), GUICanvas.Instance), + ReadyCheckButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle + (buttonWidth / 2) + GUI.IntScale(16), HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonCenter, readyButtonWidth, readyButtonHeight), GUI.Canvas), style: "RepairBuyButton") { ToolTip = TextManager.Get("ReadyCheck.Tooltip"), diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 3c376f453..1cc228e61 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -108,6 +108,9 @@ namespace Barotrauma case "pets": petsElement = subElement; break; + case "stats": + LoadStats(subElement); + break; } } @@ -167,7 +170,7 @@ namespace Barotrauma int buttonHeight = (int)(GUI.Scale * 40); int buttonWidth = GUI.IntScale(450); - endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle((GameMain.GraphicsWidth / 2) - (buttonWidth / 2), HUDLayoutSettings.ButtonAreaTop.Center.Y - (buttonHeight / 2), buttonWidth, buttonHeight), GUICanvas.Instance), + endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle((GameMain.GraphicsWidth / 2) - (buttonWidth / 2), HUDLayoutSettings.ButtonAreaTop.Center.Y - (buttonHeight / 2), buttonWidth, buttonHeight), GUI.Canvas), TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") { Pulse = true, @@ -412,6 +415,10 @@ namespace Barotrauma break; case TransitionType.ProgressToNextLocation: Map.MoveToNextLocation(); + TotalPassedLevels++; + break; + case TransitionType.ProgressToNextEmptyLocation: + TotalPassedLevels++; break; } @@ -728,6 +735,7 @@ namespace Barotrauma new XAttribute("purchaseditemrepairs", PurchasedItemRepairs), new XAttribute("cheatsenabled", CheatsEnabled)); modeElement.Add(Settings.Save()); + modeElement.Add(SaveStats()); //save and remove all items that are in someone's inventory so they don't get included in the sub file as well foreach (Character c in Character.CharacterList) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs index c99349d18..26f8f67b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs @@ -205,6 +205,11 @@ namespace Barotrauma return; } + if (campaign?.CampaignUI?.MedicalClinic is { } ui) + { + ui.ClosePopup(); + } + healAllRequests.Add(new RequestAction(onReceived, GetTimeout())); ClientSend(null, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index a71217c8d..79bc45113 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -600,6 +600,10 @@ namespace Barotrauma OnClicked = (bt, userdata) => { SelectTab((Tab)userdata); return true; } }; tabButtons[(int)tab].Text = ToolBox.LimitString(buttonText, tabButtons[(int)tab].Font, (int)(0.75f * tabWidth * tabButtonHolder.Rect.Width)); + if (tabButtons[(int)tab].Text != buttonText) + { + tabButtons[(int)tab].ToolTip = buttonText; + } } /// Graphics tab -------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index f7bc9048c..aa86255c8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -796,7 +796,7 @@ namespace Barotrauma if (quickUseAction != QuickUseAction.Drop) { slot.QuickUseButtonToolTip = quickUseAction == QuickUseAction.None ? - "" : TextManager.GetWithVariable("QuickUseAction." + quickUseAction.ToString(), "[equippeditem]", item?.Name); + "" : TextManager.GetWithVariable("QuickUseAction." + quickUseAction.ToString(), "[equippeditem]", character.HeldItems.FirstOrDefault()?.Name ?? item?.Name); if (PlayerInput.PrimaryMouseButtonDown()) { slot.EquipButtonState = GUIComponent.ComponentState.Pressed; } if (PlayerInput.PrimaryMouseButtonClicked()) { @@ -970,7 +970,9 @@ namespace Barotrauma { return QuickUseAction.TakeFromCharacter; } - else if (character.HeldItems.Any(i => i.OwnInventory != null && i.OwnInventory.CanBePut(item)) && allowInventorySwap) + else if (character.HeldItems.Any(i => + i.OwnInventory != null && + (i.OwnInventory.CanBePut(item) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) { return QuickUseAction.PutToEquippedItem; } @@ -1136,7 +1138,8 @@ namespace Barotrauma foreach (Item heldItem in character.HeldItems) { if (heldItem.OwnInventory != null && - heldItem.OwnInventory.TryPutItem(item, Character.Controlled)) + heldItem.OwnInventory.TryPutItem(item, Character.Controlled) || + (heldItem.OwnInventory.Capacity == 1 && heldItem.OwnInventory.TryPutItem(item, 0, allowSwapping: true, allowCombine: false, user: Character.Controlled))) { success = true; for (int j = 0; j < capacity; j++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs index d57f1ee61..b3057fb3e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs @@ -256,6 +256,10 @@ namespace Barotrauma.Items.Components { targetHull.IncreaseSectionColorOrStrength(targetSections[i], color, sizeAdjustedSprayStrength * deltaTime, true, false); } + if (GameMain.GameSession != null) + { + GameMain.GameSession.TimeSpentCleaning += deltaTime; + } } else { @@ -263,6 +267,10 @@ namespace Barotrauma.Items.Components { targetHull.CleanSection(targetSections[i], -sizeAdjustedSprayStrength * deltaTime, true); } + if (GameMain.GameSession != null) + { + GameMain.GameSession.TimeSpentPainting += deltaTime; + } } Vector2 particleStartPos = item.WorldPosition + ConvertUnits.ToDisplayUnits(TransformedBarrelPos); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 0e4b9b743..49d0725d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -143,7 +143,7 @@ namespace Barotrauma.Items.Components } } - public GUIFrame GuiFrame { get; protected set; } + public GUIFrame GuiFrame { get; set; } [Serialize(false, false)] public bool AllowUIOverlap @@ -554,7 +554,7 @@ namespace Barotrauma.Items.Components color = GuiFrameSource.GetAttributeColor("color", Color.White); } string style = GuiFrameSource.Attribute("style") == null ? null : GuiFrameSource.GetAttributeString("style", ""); - GuiFrame = new GUIFrame(RectTransform.Load(GuiFrameSource, GUI.Canvas.ItemComponentHolder, Anchor.Center), style, color); + GuiFrame = new GUIFrame(RectTransform.Load(GuiFrameSource, GUI.Canvas, Anchor.Center), style, color); DefaultLayout = GUILayoutSettings.Load(GuiFrameSource); if (GuiFrame != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index e087b99fa..2154f25ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -76,6 +76,9 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false, description: "If true, the contained state indicator calculates how full the item is based on the total amount of items that can be stacked inside it, as opposed to how many of the inventory slots are occupied.")] + public bool ShowTotalStackCapacityInContainedStateIndicator { get; set; } + [Serialize(false, false, description: "Should the inventory of this item be kept open when the item is equipped by a character.")] public bool KeepOpenWhenEquipped { get; set; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index a5901c440..ae6905a0b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -63,7 +63,7 @@ namespace Barotrauma.Items.Components Stretch = true, RelativeSpacing = 0.05f }; - var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, inputLabelArea.RectTransform), TextManager.Get("uilabel.input"), font: GUI.SubHeadingFont) { Padding = Vector4.Zero }; + var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, inputLabelArea.RectTransform), TextManager.Get("deconstructor.input", fallBackTag: "uilabel.input"), font: GUI.SubHeadingFont) { Padding = Vector4.Zero }; inputLabel.RectTransform.Resize(new Point((int) inputLabel.Font.MeasureString(inputLabel.Text).X, inputLabel.RectTransform.Rect.Height)); new GUIFrame(new RectTransform(Vector2.One, inputLabelArea.RectTransform), style: "HorizontalLine"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 57217ed9f..495315dff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -132,7 +132,7 @@ namespace Barotrauma.Items.Components Stretch = true, RelativeSpacing = 0.03f }; - var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, separatorArea.RectTransform), TextManager.Get("uilabel.input"), font: GUI.SubHeadingFont) { Padding = Vector4.Zero }; + var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, separatorArea.RectTransform), TextManager.Get("fabricator.input", fallBackTag: "uilabel.input"), font: GUI.SubHeadingFont) { Padding = Vector4.Zero }; inputLabel.RectTransform.Resize(new Point((int) inputLabel.Font.MeasureString(inputLabel.Text).X, inputLabel.RectTransform.Rect.Height)); new GUIFrame(new RectTransform(Vector2.One, separatorArea.RectTransform), style: "HorizontalLine"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index ace0a070e..3421dbb47 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -507,7 +507,7 @@ namespace Barotrauma.Items.Components { Vector2 origin = weaponSprite.Origin; float scale = parentWidth / Math.Max(weaponSprite.size.X, weaponSprite.size.Y); - Color color = !hasPower ? NoPowerColor : turret.ActiveUser is null ? GUI.Style.Red : GUI.Style.Green; + Color color = !hasPower ? NoPowerColor : turret.ActiveUser is null ? Color.DimGray : GUI.Style.Green; weaponSprite.Draw(batch, center, color, origin, rotation, scale, it.SpriteEffects); } }); @@ -1335,7 +1335,7 @@ namespace Barotrauma.Items.Components RectangleF entityRect = ScaleRectToUI(structure, parent, border); Vector2 spriteScale = new Vector2(entityRect.Size.X / sprite.size.X, entityRect.Size.Y / sprite.size.Y); - sprite.Draw(spriteBatch, new Vector2(entityRect.Location.X + inflate, entityRect.Location.Y + inflate), structure.SpriteColor, Vector2.Zero, 0f, spriteScale, structure.SpriteEffects); + sprite.Draw(spriteBatch, new Vector2(entityRect.Location.X + inflate, entityRect.Location.Y + inflate), structure.SpriteColor, Vector2.Zero, 0f, spriteScale, sprite.effects ^ structure.SpriteEffects); } private static RectangleF ScaleRectToUI(MapEntity entity, RectangleF parentRect, RectangleF worldBorders) @@ -1718,5 +1718,20 @@ namespace Barotrauma.Items.Components return new MiniMapHullData(scaledPolygon, worldRect, parentRect.Size, snappedRectangles, hullRefs.ToImmutableArray()); } + + protected override void RemoveComponentSpecific() + { + base.RemoveComponentSpecific(); + if (searchAutoComplete != null) + { + searchAutoComplete.RectTransform.Parent = null; + searchAutoComplete = null; + } + if (hullInfoFrame != null) + { + hullInfoFrame.RectTransform.Parent = null; + hullInfoFrame = null; + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs index 80e5671a5..a3724b8a3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Items.Components if (selectionUI == null) { - selectionUI = new SubmarineSelection(true, null, GUICanvas.Instance.ItemComponentHolder); + selectionUI = new SubmarineSelection(true, null, GUI.Canvas); } GuiFrame = selectionUI.GuiFrame; @@ -35,5 +35,15 @@ namespace Barotrauma.Items.Components selectionUI?.Update(); } + + protected override void RemoveComponentSpecific() + { + base.RemoveComponentSpecific(); + if (selectionUI != null) + { + selectionUI.GuiFrame.RectTransform.Parent = null; + selectionUI = null; + } + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index be316aff4..16a2fa540 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -348,6 +348,27 @@ namespace Barotrauma.Items.Components } } + private Vector2 GetTransducerPos() + { + if (!UseTransducers || connectedTransducers.Count == 0) + { + //use the position of the sub if the item is static (no body) and inside a sub + return item.Submarine != null && item.body == null ? item.Submarine.WorldPosition : item.WorldPosition; + } + + Vector2 transducerPosSum = Vector2.Zero; + foreach (ConnectedTransducer transducer in connectedTransducers) + { + if (transducer.Transducer.Item.Submarine != null && !CenterOnTransducers) + { + return transducer.Transducer.Item.Submarine.WorldPosition; + } + transducerPosSum += transducer.Transducer.Item.WorldPosition; + } + return transducerPosSum / connectedTransducers.Count; + } + + public override void OnItemLoaded() { base.OnItemLoaded(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs index 643b2042e..f8962c80e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs @@ -10,6 +10,12 @@ namespace Barotrauma.Items.Components private GUIProgressBar chargeIndicator; private GUIScrollBar rechargeSpeedSlider; + [Serialize(0.0f, true)] + public float RechargeWarningIndicatorLow { get; set; } + + [Serialize(0.0f, true)] + public float RechargeWarningIndicatorHigh { get; set; } + public Vector2 DrawSize { //use the extents of the item as the draw size @@ -28,19 +34,38 @@ namespace Barotrauma.Items.Components var upperArea = new GUIFrame(new RectTransform(new Vector2(1, 0.4f), paddedFrame.RectTransform, Anchor.TopCenter), style: null); var lowerArea = new GUIFrame(new RectTransform(new Vector2(1, 0.6f), paddedFrame.RectTransform, Anchor.BottomCenter), style: null); - - string rechargeStr = TextManager.Get("PowerContainerRechargeRate"); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), upperArea.RectTransform, Anchor.TopCenter), - "RechargeRate", textColor: GUI.Style.TextColor, font: GUI.SubHeadingFont, textAlignment: Alignment.Center) + var rechargeRateContainer = new GUIFrame(new RectTransform(new Vector2(1, 0.4f), upperArea.RectTransform), style: null); + var rechargeLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 0.0f), rechargeRateContainer.RectTransform, Anchor.CenterLeft), + TextManager.Get("rechargerate"), textColor: GUI.Style.TextColor, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft); + string kW = TextManager.Get("kilowatt"); + var rechargeText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), rechargeRateContainer.RectTransform, Anchor.CenterRight), + "", textColor: GUI.Style.TextColor, font: GUI.Font, textAlignment: Alignment.CenterRight) { - TextGetter = () => - { - return rechargeStr.Replace("[rate]", ((int)((rechargeSpeed / maxRechargeSpeed) * 100.0f)).ToString()); - } + TextGetter = () => $"{(int)MathF.Round(currPowerConsumption)} {kW} ({(int)MathF.Round(RechargeRatio * 100)} %)" }; + if (rechargeText.TextSize.X > rechargeText.Rect.Width) { rechargeText.Font = GUI.SmallFont; } - rechargeSpeedSlider = new GUIScrollBar(new RectTransform(new Vector2(0.9f, 0.4f), upperArea.RectTransform, Anchor.BottomCenter), - barSize: 0.15f, style: "DeviceSlider") + var rechargeSliderContainer = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.4f), upperArea.RectTransform, Anchor.BottomCenter)); + + if (RechargeWarningIndicatorLow > 0.0f || RechargeWarningIndicatorHigh > 0.0f) + { + var rechargeSliderFill = new GUICustomComponent(new RectTransform(new Vector2(0.95f, 0.9f), rechargeSliderContainer.RectTransform, Anchor.Center), (SpriteBatch sb, GUICustomComponent c) => + { + if (RechargeWarningIndicatorLow > 0.0f) + { + float warningLow = c.Rect.Width * RechargeWarningIndicatorLow; + GUI.DrawRectangle(sb, new Vector2(c.Rect.X + warningLow, c.Rect.Y), new Vector2(c.Rect.Width - warningLow, c.Rect.Height), GUI.Style.Orange, isFilled: true); + } + if (RechargeWarningIndicatorHigh > 0.0f) + { + float warningHigh = c.Rect.Width * RechargeWarningIndicatorHigh; + GUI.DrawRectangle(sb, new Vector2(c.Rect.X + warningHigh, c.Rect.Y), new Vector2(c.Rect.Width - warningHigh, c.Rect.Height), GUI.Style.Red, isFilled: true); + } + }); + } + + rechargeSpeedSlider = new GUIScrollBar(new RectTransform(Vector2.One, rechargeSliderContainer.RectTransform, Anchor.Center), + barSize: 0.15f, style: "DeviceSliderSeeThrough") { Step = 0.1f, OnMoved = (GUIScrollBar scrollBar, float barScroll) => @@ -61,17 +86,17 @@ namespace Barotrauma.Items.Components // lower area -------------------------- - var textArea = new GUIFrame(new RectTransform(new Vector2(1, 0.4f), lowerArea.RectTransform), style: null); - var chargeLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 0.0f), textArea.RectTransform, Anchor.CenterLeft), + var chargeTextContainer = new GUIFrame(new RectTransform(new Vector2(1, 0.4f), lowerArea.RectTransform), style: null); + var chargeLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 0.0f), chargeTextContainer.RectTransform, Anchor.CenterLeft), TextManager.Get("charge"), textColor: GUI.Style.TextColor, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft) { ToolTip = TextManager.Get("PowerTransferTipPower") }; string kWmin = TextManager.Get("kilowattminute"); - var chargeText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), textArea.RectTransform, Anchor.CenterRight), + var chargeText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), chargeTextContainer.RectTransform, Anchor.CenterRight), "", textColor: GUI.Style.TextColor, font: GUI.Font, textAlignment: Alignment.CenterRight) { - TextGetter = () => $"{(int)Math.Round(charge)}/{(int)capacity} {kWmin} ({(int)Math.Round(MathUtils.Percentage(charge, capacity))} %)" + TextGetter = () => $"{(int)MathF.Round(charge)}/{(int)capacity} {kWmin} ({(int)MathF.Round(MathUtils.Percentage(charge, capacity))} %)" }; if (chargeText.TextSize.X > chargeText.Rect.Width) { chargeText.Font = GUI.SmallFont; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs index 99da96f2a..dd8c385eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace Barotrauma.Items.Components @@ -19,5 +20,10 @@ namespace Barotrauma.Items.Components ShapeExtensions.DrawLine(spriteBatch, pos + Vector2.UnitX * range, pos - Vector2.UnitX * range, Color.Cyan * 0.5f, 2); ShapeExtensions.DrawCircle(spriteBatch, pos, range, 32, Color.Cyan * 0.5f, 3); } + + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + Channel = msg.ReadRangedInteger(MinChannel, MaxChannel); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 0d443f65d..86116fbbd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1584,6 +1584,10 @@ namespace Barotrauma { containedState = item.Condition / item.MaxCondition; } + else if (itemContainer.ShowTotalStackCapacityInContainedStateIndicator) + { + containedState = itemContainer.Inventory.AllItems.Count() / (float)(itemContainer.GetMaxStackSize(0) * itemContainer.Capacity); + } else { var containedItem = itemContainer.Inventory.slots[Math.Max(itemContainer.ContainedStateIndicatorSlot, 0)].FirstOrDefault(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs index 540d896d0..04169a6fa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs @@ -315,7 +315,7 @@ namespace Barotrauma.MapCreatures.Behavior } else { - RemoveClaim(itemId); + RemoveClaim(item); } } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index 430059ff6..ffeed153b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -128,13 +128,13 @@ namespace Barotrauma { //no flow particles between linked hulls (= rooms consisting of multiple hulls) if (hull1.linkedTo.Contains(hull2)) { return; } - foreach (Hull h in hull1.linkedTo) + foreach (var linkedEntity in hull1.linkedTo) { - if (h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2)) { return; } + if (linkedEntity is Hull h && h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2)) { return; } } - foreach (Hull h in hull2.linkedTo) + foreach (var linkedEntity in hull2.linkedTo) { - if (h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2)) { return; } + if (linkedEntity is Hull h && h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2)) { return; } } } @@ -244,7 +244,7 @@ namespace Barotrauma float emitInterval = 1.0f / particlesPerSec; while (particleTimer > emitInterval) { - pos.X = Rand.Range(rect.X, rect.X + rect.Width); + pos.X = Rand.Range(rect.X, rect.X + rect.Width + 1); Vector2 velocity = new Vector2( lerpedFlowForce.X * Rand.Range(0.5f, 0.7f), MathHelper.Clamp(lerpedFlowForce.Y, -500.0f, 1000.0f) * Rand.Range(0.5f, 0.7f)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs index 70d2d2358..b8ef5800c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs @@ -95,7 +95,7 @@ namespace Barotrauma var prefab = ToolBox.SelectWeightedRandom(availablePrefabs, availablePrefabs.Select(p => p.GetCommonness(level.GenerationParams)).ToList(), Rand.RandSync.ClientOnly); if (prefab == null) { break; } - int amount = Rand.Range(prefab.SwarmMin, prefab.SwarmMax, Rand.RandSync.ClientOnly); + int amount = Rand.Range(prefab.SwarmMin, prefab.SwarmMax + 1, Rand.RandSync.ClientOnly); List swarmMembers = new List(); for (int n = 0; n < amount; n++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 642813f4f..c92f51088 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Barotrauma.Lights; @@ -36,9 +37,6 @@ namespace Barotrauma private static List highlightedList = new List(); - // Test feature. Not yet saved. - public static Dictionary> SelectionGroups { get; private set; } = new Dictionary>(); - private static float highlightTimer; private static GUIListBox highlightedListBox; @@ -197,7 +195,7 @@ namespace Barotrauma { Paste(cam.ScreenToWorld(PlayerInput.MousePosition)); } - else if (PlayerInput.KeyHit(Keys.G)) + /*else if (PlayerInput.KeyHit(Keys.G)) { if (SelectedList.Any()) { @@ -217,7 +215,7 @@ namespace Barotrauma } } } - } + }*/ } } @@ -360,14 +358,15 @@ namespace Barotrauma { if (highLightedEntity != null) { - if (SelectionGroups.TryGetValue(highLightedEntity, out HashSet group)) + if (SubEditorScreen.IsLayerLinked(highLightedEntity)/*SelectionGroups.TryGetValue(highLightedEntity, out HashSet group)*/) { - foreach (MapEntity entity in group.Where(e => !newSelection.Contains(e))) + ImmutableHashSet entitiesInSameLayer = SubEditorScreen.GetEntitiesInSameLayer(highLightedEntity); + foreach (MapEntity entity in entitiesInSameLayer.Where(e => !newSelection.Contains(e))) { newSelection.Add(entity); } - foreach (MapEntity entity in group) + foreach (MapEntity entity in entitiesInSameLayer) { entity.IsIncludedInSelection = true; } @@ -1197,14 +1196,24 @@ namespace Barotrauma Rectangle selectionRect = Submarine.AbsRect(pos, size); - foreach (MapEntity e in mapEntityList) + foreach (MapEntity entity in mapEntityList) { - if (!e.SelectableInEditor) continue; + if (!entity.SelectableInEditor) { continue; } - if (Submarine.RectsOverlap(selectionRect, e.rect)) + if (Submarine.RectsOverlap(selectionRect, entity.rect)) { - foundEntities.Add(e); - e.IsIncludedInSelection = true; + foundEntities.Add(entity); + entity.IsIncludedInSelection = true; + + if (SubEditorScreen.IsLayerLinked(entity)) + { + ImmutableHashSet entitiesInSameLayer = SubEditorScreen.GetEntitiesInSameLayer(entity); + foreach (MapEntity layerEntity in entitiesInSameLayer.Where(e => !foundEntities.Contains(e))) + { + foundEntities.Add(layerEntity); + layerEntity.IsIncludedInSelection = true; + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index 5083fe7c1..9eff76d75 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -651,7 +651,11 @@ namespace Barotrauma public void Dispose() { - previewFrame = null; + if (previewFrame != null) + { + previewFrame.RectTransform.Parent = null; + previewFrame = null; + } spriteRecorder?.Dispose(); isDisposed = true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index 629752439..0e853822f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -46,6 +46,13 @@ namespace Barotrauma.Networking senderName = senderCharacter.Name; } } + + Color? textColor = null; + if (msg.ReadBoolean()) + { + textColor = msg.ReadColorR8G8B8A8(); + } + msg.ReadPadBits(); switch (type) @@ -135,14 +142,18 @@ namespace Barotrauma.Networking //only show the message box if the text differs from the text in the currently visible box if ((GUIMessageBox.VisibleBox as GUIMessageBox)?.Text?.Text != txt) { - new GUIMessageBox("", txt); + GUIMessageBox messageBox = new GUIMessageBox("", txt); + if (textColor != null) { messageBox.Text.TextColor = textColor.Value; } } break; case ChatMessageType.ServerMessageBoxInGame: - new GUIMessageBox("", txt, new string[0], type: GUIMessageBox.Type.InGame, iconStyle: styleSetting); + { + GUIMessageBox messageBox = new GUIMessageBox("", txt, new string[0], type: GUIMessageBox.Type.InGame, iconStyle: styleSetting); + if (textColor != null) { messageBox.Text.TextColor = textColor.Value; } + } break; case ChatMessageType.Console: - DebugConsole.NewMessage(txt, MessageColor[(int)ChatMessageType.Console]); + DebugConsole.NewMessage(txt, textColor == null ? MessageColor[(int)ChatMessageType.Console] : textColor.Value); break; case ChatMessageType.ServerLog: if (!Enum.TryParse(senderName, out ServerLog.MessageType messageType)) @@ -152,7 +163,7 @@ namespace Barotrauma.Networking GameMain.Client.ServerSettings.ServerLog?.WriteLine(txt, messageType); break; default: - GameMain.Client.AddChatMessage(txt, type, senderName, senderClient, senderCharacter, changeType); + GameMain.Client.AddChatMessage(txt, type, senderName, senderClient, senderCharacter, changeType, textColor: textColor); break; } LastID = id; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 1c3b81c40..f27c6daa0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -857,7 +857,7 @@ namespace Barotrauma.Networking } break; case ServerPacketHeader.STARTGAMEFINALIZE: - DebugConsole.Log("Received STARTGAMEFINALIZE packet."); + DebugConsole.NewMessage("Received STARTGAMEFINALIZE packet. Round init status: " + roundInitStatus); if (roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize) { //waiting for a save file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index d4c3b4793..445af25ff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -1,13 +1,10 @@ -using Barotrauma.Tutorials; +using Barotrauma.Extensions; +using Barotrauma.IO; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using Barotrauma.IO; -using System.Linq; -using System.Xml.Linq; using System.Globalization; -using Barotrauma.Extensions; -using Barotrauma.Networking; +using System.Linq; namespace Barotrauma { @@ -64,6 +61,7 @@ namespace Barotrauma maxMissionCount = MathHelper.Clamp(maxMissionCount, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit); + maxMissionCountText.Text = maxMissionCount.ToString(CultureInfo.InvariantCulture); } maxMissionCountButtons[1] diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 472063ab4..e8bf31049 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -151,6 +151,7 @@ namespace Barotrauma if (Campaign.Money >= CampaignMode.HullRepairCost) { Campaign.Money -= CampaignMode.HullRepairCost; + GameAnalyticsManager.AddMoneySpentEvent(CampaignMode.HullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); Campaign.PurchasedHullRepairs = true; } } @@ -196,6 +197,7 @@ namespace Barotrauma if (Campaign.Money >= CampaignMode.ItemRepairCost) { Campaign.Money -= CampaignMode.ItemRepairCost; + GameAnalyticsManager.AddMoneySpentEvent(CampaignMode.ItemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); Campaign.PurchasedItemRepairs = true; } } @@ -248,6 +250,7 @@ namespace Barotrauma if (Campaign.Money >= CampaignMode.ShuttleReplaceCost) { Campaign.Money -= CampaignMode.ShuttleReplaceCost; + GameAnalyticsManager.AddMoneySpentEvent(CampaignMode.ShuttleReplaceCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); Campaign.PurchasedLostShuttles = true; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index f54abb64b..bc639deb0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -54,7 +54,7 @@ namespace Barotrauma private void CreateGUI() { - GuiFrame = new GUIFrame(new RectTransform(new Vector2(0.2f, 0.4f), GUICanvas.Instance) { MinSize = new Point(300, 400) }); + GuiFrame = new GUIFrame(new RectTransform(new Vector2(0.2f, 0.4f), GUI.Canvas) { MinSize = new Point(300, 400) }); GUILayoutGroup layoutGroup = new GUILayoutGroup(RectTransform(0.9f, 0.9f, GuiFrame, Anchor.Center)) { Stretch = true }; // === BUTTONS === // diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 188749541..c5b3ff6b7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -501,8 +501,6 @@ namespace Barotrauma ResetButtonStates(null); - GameAnalyticsManager.SetCustomDimension01(""); - if (GameMain.SteamWorkshopScreen != null) { CoroutineManager.StartCoroutine(GameMain.SteamWorkshopScreen.RefreshDownloadState()); @@ -1019,7 +1017,10 @@ namespace Barotrauma if (backgroundSprite == null) { - backgroundSprite = (LocationType.List.Where(l => l.UseInMainMenu).GetRandom())?.GetPortrait(0); +#if UNSTABLE + backgroundSprite = new Sprite("Content/UnstableBackground.png", sourceRectangle: null); +#endif + backgroundSprite ??= LocationType.List.Where(l => l.UseInMainMenu).GetRandom()?.GetPortrait(0); } if (backgroundSprite != null) @@ -1159,7 +1160,7 @@ namespace Barotrauma //GameMain.LobbyScreen.Select(); } - #region UI Methods +#region UI Methods private void CreateCampaignSetupUI() { menuTabs[(int)Tab.NewGame].ClearChildren(); @@ -1432,7 +1433,7 @@ namespace Barotrauma playstyleDescription.TextAlignment = playstyleDescription.WrappedText.Contains('\n') ? Alignment.CenterLeft : Alignment.Center; } - #endregion +#endregion private void FetchRemoteContent() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs index 6d2513216..3093092c7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs @@ -14,7 +14,7 @@ namespace Barotrauma { if (frame == null) { - frame = new GUIFrame(new RectTransform(GUICanvas.Instance.RelativeSize, GUICanvas.Instance), style: null) + frame = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas), style: null) { CanBeFocused = false }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index e90cfc275..1fc248426 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Xml.Linq; @@ -21,6 +22,38 @@ namespace Barotrauma { class SubEditorScreen : EditorScreen { + private enum LayerVisibility + { + Visible, + Invisible + } + + private enum LayerLinkage + { + Unlinked, + Linked + } + + private readonly struct LayerData + { + public readonly LayerVisibility Visible; + public readonly LayerLinkage Linkage; + + public static readonly LayerData Default = new LayerData(LayerVisibility.Visible, LayerLinkage.Unlinked); + + public LayerData(LayerVisibility visible, LayerLinkage linkage) + { + Visible = visible; + Linkage = linkage; + } + + public void Deconstruct(out LayerVisibility isvisible, out LayerLinkage islinked) + { + isvisible = Visible; + islinked = Linkage; + } + } + private static readonly string[] crewExperienceLevels = { "CrewExperienceLow", @@ -94,6 +127,7 @@ namespace Barotrauma private GUIFrame hullVolumeFrame; private GUIFrame saveAssemblyFrame; + private GUIFrame snapToGridFrame; const int PreviouslyUsedCount = 10; private GUIFrame previouslyUsedPanel; @@ -238,7 +272,7 @@ namespace Barotrauma public bool WiringMode => mode == Mode.Wiring; - public static readonly Dictionary Layers = new Dictionary(); + private static readonly Dictionary Layers = new Dictionary(); public SubEditorScreen() { @@ -507,7 +541,7 @@ namespace Barotrauma //----------------------------------------------- - layerPanel = new GUIFrame(new RectTransform(new Vector2(0.175f, 0.4f), GUI.Canvas)) + layerPanel = new GUIFrame(new RectTransform(new Vector2(0.2f, 0.4f), GUI.Canvas)) { Visible = false }; @@ -520,6 +554,7 @@ namespace Barotrauma AutoHideScrollBar = false, OnSelected = (component, o) => { + if (GUI.MouseOn is GUITickBox) { return false; } // lol if (!(o is string layer)) { return false; } MapEntity.SelectedList.Clear(); @@ -847,6 +882,19 @@ namespace Barotrauma }; saveAssemblyFrame.RectTransform.MinSize = new Point(saveAssemblyFrame.Rect.Width, (int)(saveAssemblyButton.Rect.Height / saveAssemblyButton.RectTransform.RelativeSize.Y)); + snapToGridFrame = new GUIFrame(new RectTransform(new Vector2(0.08f, 0.5f), TopPanel.RectTransform, Anchor.BottomLeft, Pivot.TopLeft) + { MinSize = new Point((int)(250 * GUI.Scale), (int)(80 * GUI.Scale)), AbsoluteOffset = new Point((int)(10 * GUI.Scale), -saveAssemblyFrame.Rect.Height - entityCountPanel.Rect.Height - (int)(10 * GUI.Scale)) }, "InnerFrame") + { + Visible = false + }; + var saveStampButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.8f), snapToGridFrame.RectTransform, Anchor.Center), TextManager.Get("subeditor.snaptogrid", fallBackTag: "spriteeditor.snaptogrid")); + saveStampButton.TextBlock.AutoScaleHorizontal = true; + saveStampButton.OnClicked += (btn, userdata) => + { + SnapToGrid(); + return true; + }; + snapToGridFrame.RectTransform.MinSize = new Point(snapToGridFrame.Rect.Width, (int)(saveStampButton.Rect.Height / saveStampButton.RectTransform.RelativeSize.Y)); //Entity menu //------------------------------------------------ @@ -941,7 +989,7 @@ namespace Barotrauma }; paddedTab.Recalculate(); - + UpdateLayerPanel(); screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } @@ -1161,6 +1209,7 @@ namespace Barotrauma { CanBeFocused = false, LoadAsynchronously = true, + SpriteEffects = icon.effects, Color = legacy ? iconColor * 0.6f : iconColor }; } @@ -1344,8 +1393,8 @@ namespace Barotrauma } ImageManager.OnEditorSelected(); + ReconstructLayers(); - GameAnalyticsManager.SetCustomDimension01("editor"); if (!GameMain.Config.EditorDisclaimerShown) { GameMain.Instance.ShowEditorDisclaimer(); @@ -1455,7 +1504,6 @@ namespace Barotrauma loadFrame = null; MapEntity.DeselectAll(); - MapEntity.SelectionGroups.Clear(); ClearUndoBuffer(); SetMode(Mode.Default); @@ -2205,7 +2253,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), priceGroup.RectTransform), TextManager.Get("subeditor.price"), textAlignment: Alignment.CenterLeft, wrap: true); - int basePrice = GameMain.DebugDraw ? 0 : Submarine.MainSub?.CalculateBasePrice() ?? 1000; + int basePrice = (GameMain.DebugDraw ? 0 : Submarine.MainSub?.CalculateBasePrice()) ?? 1000; new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), priceGroup.RectTransform), GUINumberInput.NumberType.Int, hidePlusMinusButtons: true) { IntValue = Math.Max(Submarine.MainSub?.Info?.Price ?? basePrice, basePrice), @@ -2682,6 +2730,38 @@ namespace Barotrauma return false; } + private void SnapToGrid() + { + // First move components + foreach (Item item in MapEntity.SelectedList.Where(entity => entity is Item).Cast()) + { + var wire = item.GetComponent(); + if (wire == null) + { + // Items snap to centre of nearest grid square + Vector2 offset = item.Position; + offset = new Vector2((MathF.Floor(offset.X / Submarine.GridSize.X) + .5f) * Submarine.GridSize.X - offset.X, (MathF.Floor(offset.Y / Submarine.GridSize.Y) + .5f) * Submarine.GridSize.Y - offset.Y); + item.Move(offset); + } + } + + // Then move wires, separated as moving components also moves the start and end node of wires + foreach (Item item in MapEntity.SelectedList.Where(entity => entity is Item).Cast()) + { + var wire = item.GetComponent(); + if (wire != null) + { + for (int i = 0; i < wire.GetNodes().Count; i++) + { + // Items wire nodes to centre of nearest grid square + Vector2 offset = wire.GetNodes()[i] + Submarine.MainSub.HiddenSubPosition; + offset = new Vector2((MathF.Floor(offset.X / Submarine.GridSize.X) + .5f) * Submarine.GridSize.X - offset.X, (MathF.Floor(offset.Y / Submarine.GridSize.Y) + .5f) * Submarine.GridSize.Y - offset.Y); + wire.MoveNode(i, offset); + } + } + } + } + private void CreateLoadScreen() { CloseItem(); @@ -3232,7 +3312,6 @@ namespace Barotrauma }), new ContextMenuOption("editor.layer.openlayermenu", isEnabled: true, onSelected: () => { - if (visibilityButton is null) { return; } previouslyUsedPanel.Visible = false; undoBufferPanel.Visible = false; showEntitiesPanel.Visible = false; @@ -3289,7 +3368,7 @@ namespace Barotrauma MoveToLayer(name, content); } - Layers.Add(name, true); + Layers.Add(name, LayerData.Default); UpdateLayerPanel(); } @@ -3304,7 +3383,7 @@ namespace Barotrauma if (!string.IsNullOrWhiteSpace(newName)) { - Layers.TryAdd(newName, true); + Layers.TryAdd(newName, LayerData.Default); } UpdateLayerPanel(); } @@ -3316,7 +3395,7 @@ namespace Barotrauma { if (!string.IsNullOrWhiteSpace(entity.Layer)) { - Layers.TryAdd(entity.Layer, true); + Layers.TryAdd(entity.Layer, LayerData.Default); } } UpdateLayerPanel(); @@ -4352,8 +4431,13 @@ namespace Barotrauma layerList.Content.ClearChildren(); layerList.Deselect(); + GUILayoutGroup buttonHeaders = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.075f), layerList.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.BottomLeft); - foreach (var (layer, isVisible) in Layers) + new GUIButton(new RectTransform(new Vector2(0.25f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headervisible"), style: "GUIButtonSmallFreeScale") { CanBeFocused = false, ForceUpperCase = true }; + new GUIButton(new RectTransform(new Vector2(0.15f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headerlink"), style: "GUIButtonSmallFreeScale") { CanBeFocused = false, ForceUpperCase = true }; + new GUIButton(new RectTransform(new Vector2(0.65f, 1f), buttonHeaders.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale") { CanBeFocused = false, ForceUpperCase = true }; + + foreach (var (layer, (visibility, linkage)) in Layers) { GUIFrame parent = new GUIFrame(new RectTransform(new Vector2(1f, 0.1f), layerList.Content.RectTransform), style: "ListBoxElement") { @@ -4362,33 +4446,54 @@ namespace Barotrauma GUILayoutGroup layerGroup = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - GUITickBox layerVisibleButton = new GUITickBox(new RectTransform(Vector2.One, layerGroup.RectTransform, scaleBasis: ScaleBasis.BothHeight), string.Empty) + GUILayoutGroup layerVisibilityLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.25f, 1f), layerGroup.RectTransform), childAnchor: Anchor.Center); + GUITickBox layerVisibleButton = new GUITickBox(new RectTransform(Vector2.One, layerVisibilityLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), string.Empty) { - Selected = isVisible, + Selected = visibility == LayerVisibility.Visible, OnSelected = box => { - if (!Layers.TryGetValue(layer, out bool _)) + if (!Layers.TryGetValue(layer, out LayerData data)) { UpdateLayerPanel(); return false; } - Layers[layer] = box.Selected; + Layers[layer] = new LayerData(box.Selected ? LayerVisibility.Visible : LayerVisibility.Invisible, data.Linkage); + return true; + } + }; + + GUILayoutGroup layerChainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.15f, 1f), layerGroup.RectTransform), childAnchor: Anchor.Center); + GUITickBox layerChainButton = new GUITickBox(new RectTransform(Vector2.One, layerChainLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), string.Empty) + { + Selected = linkage == LayerLinkage.Linked, + OnSelected = box => + { + if (!Layers.TryGetValue(layer, out LayerData data)) + { + UpdateLayerPanel(); + return false; + } + + Layers[layer] = new LayerData(data.Visible, box.Selected ? LayerLinkage.Linked : LayerLinkage.Unlinked); return true; } }; layerGroup.Recalculate(); - new GUITextBlock(new RectTransform(new Vector2(1.0f - layerVisibleButton.RectTransform.RelativeSize.X, 1f), layerGroup.RectTransform), layer, textAlignment: Alignment.CenterLeft) + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), layerGroup.RectTransform), layer, textAlignment: Alignment.CenterLeft) { CanBeFocused = false }; layerGroup.Recalculate(); + layerChainLayout.Recalculate(); + layerVisibilityLayout.Recalculate(); } layerList.RecalculateChildren(); + buttonHeaders.Recalculate(); } public void UpdateUndoHistoryPanel() @@ -4454,6 +4559,7 @@ namespace Barotrauma saveFrame = null; loadFrame = null; saveAssemblyFrame = null; + snapToGridFrame = null; CreateUI(); UpdateEntityList(); } @@ -4501,6 +4607,7 @@ namespace Barotrauma hullVolumeFrame.Visible = MapEntity.SelectedList.Any(s => s is Hull); hullVolumeFrame.RectTransform.AbsoluteOffset = new Point(Math.Max(showEntitiesPanel.Rect.Right, previouslyUsedPanel.Rect.Right), 0); saveAssemblyFrame.Visible = MapEntity.SelectedList.Count > 0; + snapToGridFrame.Visible = MapEntity.SelectedList.Count > 0; var offset = cam.WorldView.Top - cam.ScreenToWorld(new Vector2(0, GameMain.GraphicsHeight - EntityMenu.Rect.Top)).Y; @@ -4962,7 +5069,8 @@ namespace Barotrauma MouseDragStart = Vector2.Zero; } - if (!saveAssemblyFrame.Rect.Contains(PlayerInput.MousePosition) && dummyCharacter?.SelectedConstruction == null && !WiringMode && GUI.MouseOn == null) + if (!saveAssemblyFrame.Rect.Contains(PlayerInput.MousePosition) && !snapToGridFrame.Rect.Contains(PlayerInput.MousePosition) && + dummyCharacter?.SelectedConstruction == null && !WiringMode && GUI.MouseOn == null) { if (layerList is { Visible: true } && GUI.KeyboardDispatcher.Subscriber == layerList) { @@ -5338,17 +5446,34 @@ namespace Barotrauma public static bool IsLayerVisible(MapEntity entity) { - if (!IsSubEditor()) { return true; } + if (!IsSubEditor() || string.IsNullOrWhiteSpace(entity.Layer)) { return true; } - if (string.IsNullOrWhiteSpace(entity.Layer)) { return true; } - - if (!Layers.TryGetValue(entity.Layer, out bool isVisible)) + if (!Layers.TryGetValue(entity.Layer, out LayerData data)) { - Layers.TryAdd(entity.Layer, true); + Layers.TryAdd(entity.Layer, LayerData.Default); return true; } - return isVisible; + return data.Visible == LayerVisibility.Visible; + } + + public static bool IsLayerLinked(MapEntity entity) + { + if (!IsSubEditor() || string.IsNullOrWhiteSpace(entity.Layer)) { return false; } + + if (!Layers.TryGetValue(entity.Layer, out LayerData data)) + { + Layers.TryAdd(entity.Layer, LayerData.Default); + return true; + } + + return data.Linkage == LayerLinkage.Linked; + } + + public static ImmutableHashSet GetEntitiesInSameLayer(MapEntity entity) + { + if (string.IsNullOrWhiteSpace(entity.Layer)) { return ImmutableHashSet.Empty; } + return MapEntity.mapEntityList.Where(me => me.Layer == entity.Layer).ToImmutableHashSet(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index 55af5dcb5..4d71cfc0a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -65,7 +65,6 @@ namespace Barotrauma Character.Controlled = dummyCharacter; GameMain.World.ProcessChanges(); - TabMenu.selectedTab = TabMenu.InfoFrameTab.Talents; tabMenu = new TabMenu(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 9c8e2f0f4..32f365887 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -1157,7 +1157,7 @@ namespace Barotrauma public static void PlaySplashSound(Vector2 worldPosition, float strength) { if (SplashSounds.Count == 0) { return; } - int splashIndex = MathHelper.Clamp((int)(strength + Rand.Range(-2, 2)), 0, SplashSounds.Count - 1); + int splashIndex = MathHelper.Clamp((int)(strength + Rand.Range(-2.0f, 2.0f)), 0, SplashSounds.Count - 1); float range = 800.0f; var channel = SplashSounds[splashIndex].Play(1.0f, range, worldPosition, muffle: ShouldMuffleSound(Character.Controlled, worldPosition, range, null)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index ac6b119c5..818255063 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -148,14 +148,8 @@ namespace Barotrauma return t; } string fullPath = Path.GetFullPath(file); - foreach (Sprite s in LoadedSprites) - { - if (s.FullPath == fullPath && s.texture != null && !s.texture.IsDisposed) - { - reusedSprite = s; - return s.texture; - } - } + reusedSprite = FindMatchingSprite(fullPath, requireTexture: true); + if (reusedSprite != null) { return reusedSprite.texture; } if (File.Exists(file)) { @@ -176,6 +170,22 @@ namespace Barotrauma return null; } + private static Sprite FindMatchingSprite(string fullPath, bool requireTexture) + { + lock (list) + { + foreach (var wRef in list) + { + if (wRef.TryGetTarget(out Sprite sprite)) + { + bool hasTexture = sprite.texture != null && !sprite.texture.IsDisposed; + if (sprite.FullPath == fullPath && (hasTexture || !requireTexture)) { return sprite; } + } + } + } + return null; + } + public void Draw(ISpriteBatch spriteBatch, Vector2 pos, float rotate = 0.0f, float scale = 1.0f, SpriteEffects spriteEffect = SpriteEffects.None) { this.Draw(spriteBatch, pos, Color.White, rotate, scale, spriteEffect); @@ -371,15 +381,9 @@ namespace Barotrauma //check if another sprite is using the same texture if (!string.IsNullOrEmpty(FilePath)) //file can be empty if the sprite is created directly from a Texture2D instance { - lock (list) - { - foreach (Sprite s in LoadedSprites) - { - if (s.FullPath == FullPath) { return; } - } - } + if (FindMatchingSprite(FullPath, requireTexture: false) != null) { return; } } - + //if not, free the texture if (texture != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs index d8abe8504..34aa5e0ca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs @@ -89,7 +89,17 @@ namespace Barotrauma for (int j = 0; j < infoTextFiles.Count; j++) { - List xmlContent = ConvertInfoTextToXML(File.ReadAllLines(infoTextFiles[j], Encoding.UTF8), language); + + List xmlContent = null; + try + { + xmlContent = ConvertInfoTextToXML(File.ReadAllLines(infoTextFiles[j], Encoding.UTF8), language); + } + catch (Exception e) + { + DebugConsole.ThrowError("InfoText Localization .csv to .xml conversion failed for: " + infoTextFiles[j], e); + continue; + } if (xmlContent == null) { DebugConsole.ThrowError("InfoText Localization .csv to .xml conversion failed for: " + infoTextFiles[j]); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs index 3ac7dd677..3dade74cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs @@ -122,7 +122,7 @@ namespace Barotrauma { int width = 4096; int height = 4096; - Rectangle subDimensions = sub.Borders; + Rectangle subDimensions = sub.CalculateDimensions(false); Vector2 viewPos = subDimensions.Center.ToVector2(); float scale = Math.Min(width / (float)subDimensions.Width, height / (float)subDimensions.Height); diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 444cd3bc8..d22c31aa4 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.16.0.0 + 0.16.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index a5084ad6e..4261f3b2e 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.16.0.0 + 0.16.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 98f23b7fa..923513bc1 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.16.0.0 + 0.16.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 0bb97a69c..d71e5fa62 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.16.0.0 + 0.16.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 6f73823c1..d4d5886f3 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.16.0.0 + 0.16.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index a2b85a64c..7dad8ea2b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -28,11 +28,11 @@ namespace Barotrauma if (!CheatsEnabled && IsCheat) { NewMessage("Client \"" + client.Name + "\" attempted to use the command \"" + names[0] + "\". Cheats must be enabled using \"enablecheats\" before the command can be used.", Color.Red); - GameMain.Server.SendConsoleMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + names[0] + "\".", client); + GameMain.Server.SendConsoleMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + names[0] + "\".", client, Color.Red); #if USE_STEAM NewMessage("Enabling cheats will disable Steam achievements during this play session.", Color.Red); - GameMain.Server.SendConsoleMessage("Enabling cheats will disable Steam achievements during this play session.", client); + GameMain.Server.SendConsoleMessage("Enabling cheats will disable Steam achievements during this play session.", client, Color.Red); #endif return; @@ -367,7 +367,7 @@ namespace Barotrauma } else { - GameMain.Server.SendConsoleMessage("\"" + args[0] + "\" is not a valid bot spawn mode. (Valid modes are Fill and Normal)", client); + GameMain.Server.SendConsoleMessage("\"" + args[0] + "\" is not a valid bot spawn mode. (Valid modes are Fill and Normal)", client, Color.Red); } }); @@ -1046,12 +1046,12 @@ namespace Barotrauma })); AssignOnClientRequestExecute("clientlist", (Client client, Vector2 cursorWorldPos, string[] args) => { - GameMain.Server.SendConsoleMessage("***************", client); + GameMain.Server.SendConsoleMessage("***************", client, Color.Cyan); foreach (Client c in GameMain.Server.ConnectedClients) { - GameMain.Server.SendConsoleMessage("- " + c.ID.ToString() + ": " + c.Name + ", " + c.Connection.EndPointString + $", ping {c.Ping} ms", client); + GameMain.Server.SendConsoleMessage("- " + c.ID.ToString() + ": " + c.Name + ", " + c.Connection.EndPointString + $", ping {c.Ping} ms", client, Color.Cyan); } - GameMain.Server.SendConsoleMessage("***************", client); + GameMain.Server.SendConsoleMessage("***************", client, Color.Cyan); }); commands.Add(new Command("enablecheats", "enablecheats: Enables cheat commands and disables Steam achievements during this play session.", (string[] args) => @@ -1164,13 +1164,13 @@ namespace Barotrauma if (GameMain.Server == null || args.Length == 0) return; if (!int.TryParse(args[0], out int maxPlayers)) { - GameMain.Server.SendConsoleMessage(args[0] + " is not a valid player count.", client); + GameMain.Server.SendConsoleMessage(args[0] + " is not a valid player count.", client, Color.Red); } else { if (maxPlayers > NetConfig.MaxPlayers) { - GameMain.Server.SendConsoleMessage($"Setting the maximum amount of players to {maxPlayers} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead.", client); + GameMain.Server.SendConsoleMessage($"Setting the maximum amount of players to {maxPlayers} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead.", client, Color.Red); maxPlayers = NetConfig.MaxPlayers; } @@ -1455,7 +1455,7 @@ namespace Barotrauma } else { - GameMain.Server.SendConsoleMessage("\"" + args[1] + "\" is not a valid ban duration.", client); + GameMain.Server.SendConsoleMessage("\"" + args[1] + "\" is not a valid ban duration.", client, Color.Red); return; } } @@ -1575,7 +1575,7 @@ namespace Barotrauma } else { - GameMain.Server.SendConsoleMessage("\"" + args[0] + "\" is not a valid bot spawn mode. (Valid modes are Fill and Normal)", client); + GameMain.Server.SendConsoleMessage("\"" + args[0] + "\" is not a valid bot spawn mode. (Valid modes are Fill and Normal)", client, Color.Red); } } ); @@ -1599,7 +1599,7 @@ namespace Barotrauma if (Submarine.MainSub == null || Level.Loaded == null) return; if (Level.Loaded.Type == LevelData.LevelType.Outpost) { - GameMain.Server.SendConsoleMessage("The teleportsub command is unavailable in outpost levels!", client); + GameMain.Server.SendConsoleMessage("The teleportsub command is unavailable in outpost levels!", client, Color.Red); return; } @@ -1623,7 +1623,7 @@ namespace Barotrauma { if (!(GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)) { - GameMain.Server.SendConsoleMessage("No campaign active.", client); + GameMain.Server.SendConsoleMessage("No campaign active.", client, Color.Red); return; } mpCampaign.LastUpdateID++; @@ -1671,13 +1671,13 @@ namespace Barotrauma a.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab == null) { - GameMain.Server.SendConsoleMessage("Affliction \"" + args[0] + "\" not found.", client); + GameMain.Server.SendConsoleMessage("Affliction \"" + args[0] + "\" not found.", client, Color.Red); return; } if (!float.TryParse(args[1], out float afflictionStrength)) { - GameMain.Server.SendConsoleMessage("\"" + args[1] + "\" is not a valid affliction strength.", client); + GameMain.Server.SendConsoleMessage("\"" + args[1] + "\" is not a valid affliction strength.", client, Color.Red); return; } @@ -1760,7 +1760,7 @@ namespace Barotrauma c.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase)); if (talentPrefab == null) { - GameMain.Server.SendConsoleMessage("Couldn't find the talent \"" + args[0] + "\".", client); + GameMain.Server.SendConsoleMessage("Couldn't find the talent \"" + args[0] + "\".", client, Color.Red); return; } targetCharacter.GiveTalent(talentPrefab); @@ -1786,12 +1786,12 @@ namespace Barotrauma var job = JobPrefab.Prefabs.Find(jp => jp.Name != null && jp.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)); if (job == null) { - GameMain.Server.SendConsoleMessage($"Failed to find the job \"{args[0]}\".", client); + GameMain.Server.SendConsoleMessage($"Failed to find the job \"{args[0]}\".", client, Color.Red); return; } if (!TalentTree.JobTalentTrees.TryGetValue(job.Identifier, out TalentTree talentTree)) { - GameMain.Server.SendConsoleMessage($"No talents configured for the job \"{args[0]}\".", client); + GameMain.Server.SendConsoleMessage($"No talents configured for the job \"{args[0]}\".", client, Color.Red); return; } talentTrees.Add(talentTree); @@ -1858,6 +1858,10 @@ namespace Barotrauma (Client client, Vector2 cursorWorldPos, string[] args) => { Character killedCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args); + if (killedCharacter == null) + { + GameMain.Server.SendConsoleMessage("Could not find the specified character.", client, Color.Red); + } killedCharacter?.SetAllDamage(200.0f, 0.0f, 0.0f); } ); @@ -1873,6 +1877,10 @@ namespace Barotrauma GameMain.Server.SetClientCharacter(client, character); client.SpectateOnly = false; } + else + { + GameMain.Server.SendConsoleMessage("Could not find the specified character.", client, Color.Red); + } } ); @@ -1899,7 +1907,7 @@ namespace Barotrauma } else { - GameMain.Server.SendConsoleMessage(args[0] + " is not a valid difficulty setting (enter a value between 0-100)", client); + GameMain.Server.SendConsoleMessage(args[0] + " is not a valid difficulty setting (enter a value between 0-100)", client, Color.Red); NewMessage(args[0] + " is not a valid difficulty setting (enter a value between 0-100)", Color.Red); } } @@ -1923,7 +1931,7 @@ namespace Barotrauma ClientPermissions permission = ClientPermissions.None; if (!Enum.TryParse(perm, true, out permission)) { - GameMain.Server.SendConsoleMessage(perm + " is not a valid permission!", senderClient); + GameMain.Server.SendConsoleMessage(perm + " is not a valid permission!", senderClient, Color.Red); return; } @@ -1955,7 +1963,7 @@ namespace Barotrauma } if (client.Connection == GameMain.Server.OwnerConnection) { - GameMain.Server.SendConsoleMessage("Cannot revoke permissions from the server owner!", senderClient); + GameMain.Server.SendConsoleMessage("Cannot revoke permissions from the server owner!", senderClient, Color.Red); return; } @@ -1964,7 +1972,7 @@ namespace Barotrauma ClientPermissions permission = ClientPermissions.None; if (!Enum.TryParse(perm, true, out permission)) { - GameMain.Server.SendConsoleMessage(perm + " is not a valid permission!", senderClient); + GameMain.Server.SendConsoleMessage(perm + " is not a valid permission!", senderClient, Color.Red); return; } client.RemovePermission(permission); @@ -1988,7 +1996,7 @@ namespace Barotrauma } if (client.Connection == GameMain.Server.OwnerConnection) { - GameMain.Server.SendConsoleMessage("Cannot modify the rank of the server owner!", senderClient); + GameMain.Server.SendConsoleMessage("Cannot modify the rank of the server owner!", senderClient, Color.Red); return; } @@ -1996,7 +2004,7 @@ namespace Barotrauma PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.Equals(rank, StringComparison.OrdinalIgnoreCase)); if (preset == null) { - GameMain.Server.SendConsoleMessage("Rank \"" + rank + "\" not found.", senderClient); + GameMain.Server.SendConsoleMessage("Rank \"" + rank + "\" not found.", senderClient, Color.Red); return; } @@ -2016,12 +2024,12 @@ namespace Barotrauma var client = FindClient(args[0]); if (client == null) { - GameMain.Server.SendConsoleMessage("Client \"" + args[0] + "\" not found.", senderClient); + GameMain.Server.SendConsoleMessage("Client \"" + args[0] + "\" not found.", senderClient, Color.Red); return; } if (client.Connection == GameMain.Server.OwnerConnection) { - GameMain.Server.SendConsoleMessage("Cannot modify the command permissions of the server owner!", senderClient); + GameMain.Server.SendConsoleMessage("Cannot modify the command permissions of the server owner!", senderClient, Color.Red); return; } @@ -2040,7 +2048,7 @@ namespace Barotrauma Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); if (matchingCommand == null) { - GameMain.Server.SendConsoleMessage("Could not find the command \"" + splitCommands[i] + "\"!", senderClient); + GameMain.Server.SendConsoleMessage("Could not find the command \"" + splitCommands[i] + "\"!", senderClient, Color.Red); } else { @@ -2078,7 +2086,7 @@ namespace Barotrauma } if (client.Connection == GameMain.Server.OwnerConnection) { - GameMain.Server.SendConsoleMessage("Cannot revoke command permissions from the server owner!", senderClient); + GameMain.Server.SendConsoleMessage("Cannot revoke command permissions from the server owner!", senderClient, Color.Red); return; } List revokedCommands = new List(); @@ -2096,7 +2104,7 @@ namespace Barotrauma Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); if (matchingCommand == null) { - GameMain.Server.SendConsoleMessage("Could not find the command \"" + splitCommands[i] + "\"!", senderClient); + GameMain.Server.SendConsoleMessage("Could not find the command \"" + splitCommands[i] + "\"!", senderClient, Color.Red); } else { @@ -2176,7 +2184,7 @@ namespace Barotrauma { if (args.Length < 2) { - GameMain.Server.SendConsoleMessage("Invalid parameters. The command should be formatted as \"setclientcharacter [client] [character]\". If the names consist of multiple words, you should surround them with quotation marks.", senderClient); + GameMain.Server.SendConsoleMessage("Invalid parameters. The command should be formatted as \"setclientcharacter [client] [character]\". If the names consist of multiple words, you should surround them with quotation marks.", senderClient, Color.Red); return; } @@ -2189,7 +2197,7 @@ namespace Barotrauma var client = GameMain.Server.ConnectedClients.Find(c => c.Name == args[0]); if (client == null) { - GameMain.Server.SendConsoleMessage("Client \"" + args[0] + "\" not found.", senderClient); + GameMain.Server.SendConsoleMessage("Client \"" + args[0] + "\" not found.", senderClient, Color.Red); return; } @@ -2206,17 +2214,18 @@ namespace Barotrauma if (args.Length == 0) { return; } if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)) { - GameMain.Server.SendConsoleMessage("No campaign active!", senderClient); + GameMain.Server.SendConsoleMessage("No campaign active!", senderClient, Color.Red); return; } if (int.TryParse(args[0], out int money)) { campaign.Money += money; + GameAnalyticsManager.AddMoneyGainedEvent(money, GameAnalyticsManager.MoneySource.Cheat, "console"); campaign.LastUpdateID++; } else { - GameMain.Server.SendConsoleMessage($"\"{args[0]}\" is not a valid numeric value.", senderClient); + GameMain.Server.SendConsoleMessage($"\"{args[0]}\" is not a valid numeric value.", senderClient, Color.Red); } } ); @@ -2226,7 +2235,7 @@ namespace Barotrauma { if (!(GameMain.GameSession?.GameMode is CampaignMode campaign)) { - GameMain.Server.SendConsoleMessage("No campaign active!", senderClient); + GameMain.Server.SendConsoleMessage("No campaign active!", senderClient, Color.Red); return; } @@ -2234,7 +2243,7 @@ namespace Barotrauma if (args.Length < 1 || !int.TryParse(args[0], out destinationIndex)) return; if (destinationIndex < 0 || destinationIndex >= campaign.Map.CurrentLocation.Connections.Count) { - GameMain.Server.SendConsoleMessage("Index out of bounds!", senderClient); + GameMain.Server.SendConsoleMessage("Index out of bounds!", senderClient, Color.Red); return; } Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation); @@ -2251,14 +2260,41 @@ namespace Barotrauma NewMessage(tag, Color.Yellow); } })); - + + commands.Add(new Command("sendchatmessage", "Sends a chat message with specified type and color.", (string[] args) => + { + if (args.Length < 2) { return; } + + ChatMessageType chatMessageType = ChatMessageType.Default; + Color? chatMessageColor = null; + + if (args.Length >= 3 && int.TryParse(args[2], out int result)) + { + chatMessageType = (ChatMessageType)result; + } + + if (args.Length >= 7 && + int.TryParse(args[3], out int r) && + int.TryParse(args[4], out int g) && + int.TryParse(args[5], out int b) && + int.TryParse(args[6], out int a)) + { + chatMessageColor = new Color(r, g, b, a); + } + + foreach (var client in GameMain.Server.ConnectedClients) + { + GameMain.Server.SendDirectChatMessage(ChatMessage.Create(args[0], args[1], chatMessageType, null, null, textColor: chatMessageColor), client); + } + })); + AssignOnClientRequestExecute( "setskill", (senderClient, cursorWorldPos, args) => { if (args.Length < 2) { - GameMain.Server.SendConsoleMessage($"Missing arguments. Expected at least 2 but got {args.Length} (skill, level, name)", senderClient); + GameMain.Server.SendConsoleMessage($"Missing arguments. Expected at least 2 but got {args.Length} (skill, level, name)", senderClient, Color.Red); return; } @@ -2268,7 +2304,7 @@ namespace Barotrauma if (character?.Info?.Job == null) { - GameMain.Server.SendConsoleMessage("Character is not valid.", senderClient); + GameMain.Server.SendConsoleMessage("Character is not valid.", senderClient, Color.Red); return; } @@ -2295,7 +2331,7 @@ namespace Barotrauma } else { - GameMain.Server.SendConsoleMessage($"{levelString} is not a valid level. Expected number or \"max\".", senderClient); + GameMain.Server.SendConsoleMessage($"{levelString} is not a valid level. Expected number or \"max\".", senderClient, Color.Red); } } ); @@ -2389,7 +2425,7 @@ namespace Barotrauma if (string.IsNullOrWhiteSpace(command)) return; if (!client.HasPermission(ClientPermissions.ConsoleCommands) && client.Connection != GameMain.Server.OwnerConnection) { - GameMain.Server.SendConsoleMessage("You are not permitted to use console commands!", client); + GameMain.Server.SendConsoleMessage("You are not permitted to use console commands!", client, Color.Red); GameServer.Log(GameServer.ClientLogName(client) + " attempted to execute the console command \"" + command + "\" without a permission to use console commands.", ServerLog.MessageType.ConsoleUsage); return; } @@ -2398,19 +2434,19 @@ namespace Barotrauma Command matchingCommand = commands.Find(c => c.names.Contains(splitCommand[0].ToLowerInvariant())); if (matchingCommand != null && !client.PermittedConsoleCommands.Contains(matchingCommand) && client.Connection != GameMain.Server.OwnerConnection) { - GameMain.Server.SendConsoleMessage("You are not permitted to use the command\"" + matchingCommand.names[0] + "\"!", client); + GameMain.Server.SendConsoleMessage("You are not permitted to use the command\"" + matchingCommand.names[0] + "\"!", client, Color.Red); GameServer.Log(GameServer.ClientLogName(client) + " attempted to execute the console command \"" + command + "\" without a permission to use the command.", ServerLog.MessageType.ConsoleUsage); return; } else if (matchingCommand == null) { - GameMain.Server.SendConsoleMessage("Command \"" + splitCommand[0] + "\" not found.", client); + GameMain.Server.SendConsoleMessage("Command \"" + splitCommand[0] + "\" not found.", client, Color.Red); return; } if (!MathUtils.IsValid(cursorWorldPos)) { - GameMain.Server.SendConsoleMessage("Could not execute command \"" + command + "\" - invalid cursor position.", client); + GameMain.Server.SendConsoleMessage("Could not execute command \"" + command + "\" - invalid cursor position.", client, Color.Red); NewMessage(GameServer.ClientLogName(client) + " attempted to execute the console command \"" + command + "\" with invalid cursor position.", Color.White); return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 63652614b..fd9b7b4d2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -36,7 +36,7 @@ namespace Barotrauma public static GameServer Server; public static NetworkMember NetworkMember { - get { return Server as NetworkMember; } + get { return Server; } } public static GameSession GameSession; @@ -64,6 +64,9 @@ namespace Barotrauma private static Stopwatch stopwatch; + private static Queue prevUpdateRates = new Queue(); + private static int updateCount = 0; + private static ContentPackage vanillaContent; public static ContentPackage VanillaContent { @@ -364,6 +367,8 @@ namespace Barotrauma DebugConsole.NewMessage("WARNING: Stopwatch frequency under 1500 ticks per second. Expect significant syncing accuracy issues.", Color.Yellow); } + Stopwatch performanceCounterTimer = Stopwatch.StartNew(); + stopwatch = Stopwatch.StartNew(); long prevTicks = stopwatch.ElapsedTicks; while (ShouldRun) @@ -371,9 +376,11 @@ namespace Barotrauma long currTicks = stopwatch.ElapsedTicks; double elapsedTime = Math.Max(currTicks - prevTicks, 0) / frequency; Timing.Accumulator += elapsedTime; - if (Timing.Accumulator > 1.0) + if (Timing.Accumulator > Timing.AccumulatorMax) { - //prevent spiral of death + //prevent spiral of death: + //if the game's running too slowly then we have no choice but to skip a bunch of steps + //otherwise it snowballs and becomes unplayable Timing.Accumulator = Timing.Step; } prevTicks = currTicks; @@ -392,6 +399,7 @@ namespace Barotrauma CoroutineManager.Update((float)Timing.Step, (float)Timing.Step); Timing.Accumulator -= Timing.Step; + updateCount++; } #if !DEBUG @@ -407,10 +415,34 @@ namespace Barotrauma DebugConsole.UpdateCommandLine((int)(Timing.Accumulator * 800)); #endif - int frameTime = (int)(((double)(stopwatch.ElapsedTicks - prevTicks) / frequency) * 1000.0); + int frameTime = (int)((stopwatch.ElapsedTicks - prevTicks) / frequency * 1000.0); frameTime = Math.Max(0, frameTime); Thread.Sleep(Math.Max(((int)(Timing.Step * 1000.0) - frameTime) / 2, 0)); + + if (performanceCounterTimer.ElapsedMilliseconds > 1000) + { + int updateRate = (int)Math.Round(updateCount / (double)(performanceCounterTimer.ElapsedMilliseconds / 1000.0)); + prevUpdateRates.Enqueue(updateRate); + if (prevUpdateRates.Count >= 10) + { + int avgUpdateRate = (int)prevUpdateRates.Average(); + if (avgUpdateRate < Timing.FixedUpdateRate * 0.98 && GameSession != null && Timing.TotalTime > GameSession.RoundStartTime + 1.0) + { + DebugConsole.AddWarning($"Running slowly ({avgUpdateRate} updates/s)!"); + foreach (Client c in Server.ConnectedClients) + { + if (c.Connection == Server.OwnerConnection || c.Permissions != ClientPermissions.None) + { + Server.SendConsoleMessage($"Server running slowly ({avgUpdateRate} updates/s)!", c, Color.Orange); + } + } + } + prevUpdateRates.Clear(); + } + performanceCounterTimer.Restart(); + updateCount = 0; + } } stopwatch.Stop(); @@ -429,8 +461,9 @@ namespace Barotrauma public static void ResetFrameTime() { Timing.Accumulator = 0.0f; - stopwatch?.Reset(); - stopwatch?.Start(); + stopwatch?.Restart(); + prevUpdateRates.Clear(); + updateCount = 0; } public CoroutineHandle ShowLoading(IEnumerable loader, bool waitKeyHit = true) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs index fff520d8f..90d2abec4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs @@ -55,6 +55,7 @@ namespace Barotrauma SoldItems.Add(item); Location.StoreCurrentBalance -= itemValue; campaign.Money += itemValue; + GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier); } OnSoldItemsChanged?.Invoke(); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 65ff44e2c..1af2f9dc4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -285,11 +285,15 @@ namespace Barotrauma break; case TransitionType.ProgressToNextLocation: Map.MoveToNextLocation(); + TotalPassedLevels++; break; case TransitionType.End: EndCampaign(); IsFirstRound = true; break; + case TransitionType.ProgressToNextEmptyLocation: + TotalPassedLevels++; + break; } Map.ProgressWorld(transitionType, (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime)); @@ -697,6 +701,7 @@ namespace Barotrauma { this.PurchasedHullRepairs = true; Money -= hullRepairCost; + GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); } else if (!purchasedHullRepairs) { @@ -710,6 +715,7 @@ namespace Barotrauma { this.PurchasedItemRepairs = true; Money -= itemRepairCost; + GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); } else if (!purchasedItemRepairs) { @@ -728,6 +734,7 @@ namespace Barotrauma { this.PurchasedLostShuttles = true; Money -= shuttleRetrieveCost; + GameAnalyticsManager.AddMoneySpentEvent(shuttleRetrieveCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); } else if (!purchasedItemRepairs) { @@ -998,7 +1005,9 @@ namespace Barotrauma new XAttribute("purchasedhullrepairs", PurchasedHullRepairs), new XAttribute("purchaseditemrepairs", PurchasedItemRepairs), new XAttribute("cheatsenabled", CheatsEnabled)); + modeElement.Add(Settings.Save()); + modeElement.Add(SaveStats()); CampaignMetadata?.Save(modeElement); Map.Save(modeElement); CargoManager?.SavePurchasedItems(modeElement); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs index 16fe7f125..b4490c121 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs @@ -1,13 +1,14 @@ -using System; -using System.ComponentModel; -using System.Linq; +using Barotrauma.Networking; +using System; using System.Xml.Linq; -using Barotrauma.Networking; namespace Barotrauma.Items.Components { internal partial class Growable { + private const int serverHealthUpdateDelay = 10; + private int serverHealthUpdateTimer; + partial void LoadVines(XElement element) { foreach (XElement subElement in element.Elements()) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/WifiComponent.cs new file mode 100644 index 000000000..333028165 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/WifiComponent.cs @@ -0,0 +1,12 @@ +using Barotrauma.Networking; + +namespace Barotrauma.Items.Components +{ + partial class WifiComponent + { + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + msg.WriteRangedInteger(Channel, MinChannel, MaxChannel); + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index 28f0cc043..bc37d9df3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -212,6 +212,11 @@ namespace Barotrauma.Networking { msg.Write(Sender.ID); } + msg.Write(customTextColor != null); + if (customTextColor != null) + { + msg.WriteColorR8G8B8A8(customTextColor.Value); + } msg.WritePadBits(); if (Type == ChatMessageType.ServerMessageBoxInGame) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 34ae3de09..38ae2e076 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -726,7 +726,15 @@ namespace Barotrauma.Networking } break; case ClientPacketHeader.REQUEST_STARTGAMEFINALIZE: - if (gameStarted && connectedClient != null) + if (connectedClient == null) + { + DebugConsole.AddWarning("Received a REQUEST_STARTGAMEFINALIZE message. Client not connected, ignoring the message."); + } + else if (!gameStarted) + { + DebugConsole.AddWarning("Received a REQUEST_STARTGAMEFINALIZE message. Game not started, ignoring the message."); + } + else { SendRoundStartFinalize(connectedClient); } @@ -746,7 +754,7 @@ namespace Barotrauma.Networking string seed = inc.ReadString(); string subName = inc.ReadString(); string subHash = inc.ReadString(); - CampaignSettings settings = new CampaignSettings(inc); + CampaignSettings settings = new CampaignSettings(inc); var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash); @@ -769,6 +777,7 @@ namespace Barotrauma.Networking { ServerSettings.RadiationEnabled = settings.RadiationEnabled; ServerSettings.MaxMissionCount = settings.MaxMissionCount; + ServerSettings.SaveSettings(); MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings); } } @@ -1904,7 +1913,7 @@ namespace Barotrauma.Networking int chatMessageBytes = outmsg.LengthBytes; WriteChatMessages(outmsg, c); - chatMessageBytes = outmsg.LengthBytes - outmsg.LengthBytes; + chatMessageBytes = outmsg.LengthBytes - chatMessageBytes; outmsg.Write((byte)ServerNetObject.END_OF_MESSAGE); @@ -1927,7 +1936,11 @@ namespace Barotrauma.Networking warningMsg += " Settings buffer size: " + settingsBuf.LengthBytes + " bytes\n"; } - if (GameSettings.VerboseLogging) { DebugConsole.AddWarning(warningMsg); } +#if DEBUG || UNSTABLE + DebugConsole.ThrowError(warningMsg); +#else + if (GameSettings.VerboseLogging) { DebugConsole.AddWarning(warningMsg); } +#endif GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:ClientWriteLobby" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); } @@ -1943,12 +1956,16 @@ namespace Barotrauma.Networking //these large initial messages until the client acknowledges receiving them c.LastRecvLobbyUpdate++; - SendVoteStatus(new List() { c }); } else { serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable); } + + if (isInitialUpdate) + { + SendVoteStatus(new List() { c }); + } } private void WriteChatMessages(IWriteMessage outmsg, Client c) @@ -2893,9 +2910,9 @@ namespace Barotrauma.Networking SendDirectChatMessage(msg, recipient); } - public void SendConsoleMessage(string txt, Client recipient) + public void SendConsoleMessage(string txt, Client recipient, Color? color = null) { - ChatMessage msg = ChatMessage.Create("", txt, ChatMessageType.Console, null); + ChatMessage msg = ChatMessage.Create("", txt, ChatMessageType.Console, sender: null, textColor: color); SendDirectChatMessage(msg, recipient); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 604c897f3..d81789f78 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -115,10 +115,14 @@ namespace Barotrauma.Networking return ShouldStartRespawnCountdown(characterToRespawnCount); } + private int GetMinCharactersToRespawn() + { + return Math.Max((int)(GameMain.Server.ConnectedClients.Count * GameMain.Server.ServerSettings.MinRespawnRatio), 1); + } + private bool ShouldStartRespawnCountdown(int characterToRespawnCount) { - int totalCharacterCount = GameMain.Server.ConnectedClients.Count; - return (float)characterToRespawnCount >= Math.Max((float)totalCharacterCount * GameMain.Server.ServerSettings.MinRespawnRatio, 1.0f); + return characterToRespawnCount >= GetMinCharactersToRespawn(); } partial void UpdateWaiting(float deltaTime) @@ -129,7 +133,7 @@ namespace Barotrauma.Networking } pendingRespawnCount = GetClientsToRespawn().Count(); - requiredRespawnCount = (int)Math.Max((float)GameMain.Server.ConnectedClients.Count * GameMain.Server.ServerSettings.MinRespawnRatio, 1.0f); + requiredRespawnCount = GetMinCharactersToRespawn(); if (pendingRespawnCount != prevPendingRespawnCount || requiredRespawnCount != prevRequiredRespawnCount) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 439891f62..9f9e10a7c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -223,11 +223,6 @@ namespace Barotrauma.Networking AutoRestart = autoRestart; } - RadiationEnabled = incMsg.ReadBoolean(); - - int maxMissionCount = MaxMissionCount + incMsg.ReadByte() - 1; - MaxMissionCount = MathHelper.Clamp(maxMissionCount, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit); - changed |= true; UpdateFlag(NetFlags.Misc); } diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 3dfa62e6d..18c2713cb 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.16.0.0 + 0.16.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 64c069f5f..eab4fbc2a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -340,7 +340,7 @@ namespace Barotrauma { targetingTag = "dead"; } - else if (AIParams.TryGetTarget(targetCharacter.CharacterHealth.GetActiveAfflictionTags(), out CharacterParams.TargetParams tp) && tp.Threshold > Character.GetDamageDoneByAttacker(targetCharacter)) + else if (AIParams.TryGetTarget(targetCharacter.CharacterHealth.GetActiveAfflictionTags(), out CharacterParams.TargetParams tp) && tp.Threshold >= Character.GetDamageDoneByAttacker(targetCharacter)) { targetingTag = tp.Tag; } @@ -678,7 +678,10 @@ namespace Barotrauma return a.Damage >= selectedTargetingParams.Threshold; } Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character; - if (attacker != null) + //if the attacker has the same targeting tag as the character we're protecting, we can't change the TargetState + //otherwise e.g. a pet that's set to follow humans would start attacking all humans (and other pets, since they're considered part of the same group) when a hostile human attacks it + //TODO: a way for pets to differentiate hostile and friendly humans? + if (attacker?.AiTarget != null && !targetCharacter.SpeciesName.Equals(GetTargetingTag(attacker.AiTarget), StringComparison.OrdinalIgnoreCase)) { // Attack the character that attacked the target we are protecting ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2); @@ -1598,7 +1601,7 @@ namespace Barotrauma } else { - sweepTimer = Rand.Range(-1000, 1000) * selectedTargetingParams.SweepSpeed; + sweepTimer = Rand.Range(-1000f, 1000f) * selectedTargetingParams.SweepSpeed; } } break; @@ -2305,7 +2308,7 @@ namespace Barotrauma if (item.Condition <= 0.0f) { - if (!wasBroken) { PetBehavior?.OnEat(item.GetTags(), 1.0f); } + if (!wasBroken) { PetBehavior?.OnEat(item); } Entity.Spawner.AddToRemoveQueue(item); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index c9e8d616d..498f633aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -565,7 +565,7 @@ namespace Barotrauma Character.AnimController.HeadInWater || Character.Submarine == null || (Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) || - ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOn) || + !ObjectiveManager.IsCurrentObjective() && ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOn) || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) || Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10; bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character; @@ -1090,7 +1090,6 @@ namespace Barotrauma private void RespondToAttack(Character attacker, AttackResult attackResult) { - float minorDamageThreshold = 10; float healAmount = 0.0f; if (attacker != null) { @@ -1099,7 +1098,7 @@ namespace Barotrauma // excluding poisons etc float realDamage = attackResult.Damage - healAmount; // including poisons etc - float totalDamage = realDamage - healAmount; + float totalDamage = realDamage; if (attackResult.Afflictions != null) { foreach (Affliction affliction in attackResult.Afflictions) @@ -1140,6 +1139,13 @@ namespace Barotrauma } bool isAttackerInfected = false; bool isAttackerFightingEnemy = false; + float minorDamageThreshold = 1; + float majorDamageThreshold = 20; + if (attacker.TeamID == Character.TeamID) + { + minorDamageThreshold = 10; + majorDamageThreshold = 40; + } if (IsFriendly(attacker)) { if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character) @@ -1148,11 +1154,11 @@ namespace Barotrauma // Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate). return; } - float cumulativeDamage = Character.GetDamageDoneByAttacker(attacker); + float cumulativeDamage = realDamage + Character.GetDamageDoneByAttacker(attacker); bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null; if (isAccidental) { - if (!Character.IsSecurity && cumulativeDamage > 1) + if (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold) { AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker); } @@ -1161,7 +1167,7 @@ namespace Barotrauma { isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength("alieninfection") > 0; // Inform other NPCs - if (isAttackerInfected || cumulativeDamage > 1 || totalDamage >= minorDamageThreshold) + if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold) { if (GameMain.IsMultiplayer || !attacker.IsPlayer || Character.TeamID != attacker.TeamID) { @@ -1170,27 +1176,36 @@ namespace Barotrauma } if (Character.IsBot) { - if (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; } - if (attacker.IsPlayer) + var combatMode = DetermineCombatMode(Character, cumulativeDamage); + if (attacker.IsPlayer && !Character.IsInstigator && !ObjectiveManager.IsCurrentObjective()) { - if (Character.IsSecurity) + switch (combatMode) { - if (attacker.TeamID != Character.TeamID && cumulativeDamage > 1 || cumulativeDamage > minorDamageThreshold) - { - Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.50f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 30.0f); - } - else - { - Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.50f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 30.0f); - } - } - else if (!Character.IsInstigator && cumulativeDamage > 1) - { - Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.50f, "attackedbyfriendly", minDurationBetweenSimilar: 30.0f); + case AIObjectiveCombat.CombatMode.Defensive: + case AIObjectiveCombat.CombatMode.Retreat: + if (Character.IsSecurity) + { + Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.5f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 10.0f); + } + else + { + Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.5f, "attackedbyfriendly", minDurationBetweenSimilar: 10.0f); + } + break; + case AIObjectiveCombat.CombatMode.Offensive: + case AIObjectiveCombat.CombatMode.Arrest: + Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.5f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 10.0f); + break; + case AIObjectiveCombat.CombatMode.None: + if (Character.IsSecurity && realDamage > 1) + { + Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.5f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 10.0f); + } + break; } } // If the attacker is using a low damage and high frequency weapon like a repair tool, we shouldn't use any delay. - AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage), attacker, delay: realDamage > 1 ? GetReactionTime() : 0); + AddCombatObjective(combatMode, attacker, delay: realDamage > 1 ? GetReactionTime() : 0); } if (!isAttackerFightingEnemy) { @@ -1203,15 +1218,15 @@ namespace Barotrauma if (Character.Submarine != null && Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine)) { // Non-friendly - InformOtherNPCs(Character.GetDamageDoneByAttacker(attacker)); + InformOtherNPCs(); } if (Character.IsBot) { - AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage: realDamage), attacker); + AddCombatObjective(DetermineCombatMode(Character), attacker); } } - void InformOtherNPCs(float cumulativeDamage) + void InformOtherNPCs(float cumulativeDamage = 0) { foreach (Character otherCharacter in Character.CharacterList) { @@ -1238,7 +1253,7 @@ namespace Barotrauma } } - AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage, bool isWitnessing = false) + AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage = 0, bool isWitnessing = false) { if (!IsFriendly(attacker)) { @@ -1258,7 +1273,6 @@ namespace Barotrauma } else { - float dmgThreshold = attacker.TeamID == Character.TeamID ? 50 : minorDamageThreshold; if (isAttackerInfected) { cumulativeDamage = 100; @@ -1266,8 +1280,7 @@ namespace Barotrauma if (GameMain.IsSingleplayer && attacker.IsPlayer && Character.TeamID == attacker.TeamID) { // Bots in the player team never act aggressively in single player when attacked by the player - dmgThreshold = minorDamageThreshold; - return cumulativeDamage > dmgThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None; + return cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None; } if (Character.Submarine == null || !Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine)) { @@ -1308,21 +1321,25 @@ namespace Barotrauma // Already targeting the attacker -> treat as a more serious threat. cumulativeDamage *= 2; } - if (cumulativeDamage > dmgThreshold) + if (cumulativeDamage > majorDamageThreshold) { if (c.IsSecurity) { - return c.IsSecurity ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest; + return AIObjectiveCombat.CombatMode.Offensive; } else { return c == Character ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat; } } - else + else if (cumulativeDamage > minorDamageThreshold) { return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat; } + else + { + return AIObjectiveCombat.CombatMode.None; + } } Character FindInstigator() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index ac40cf6ad..0fa953f73 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -30,7 +30,7 @@ namespace Barotrauma private float holdFireTimer; private bool hasAimed; private bool isLethalWeapon; - private bool AllowCoolDown => !IsOffensiveOrArrest || Mode != initialMode; + private bool AllowCoolDown => !IsOffensiveOrArrest || Mode != initialMode || character.TeamID == Enemy.TeamID; public Character Enemy { get; private set; } public bool HoldPosition { get; set; } @@ -143,7 +143,7 @@ namespace Barotrauma { Mode = CombatMode.Retreat; } - spreadTimer = Rand.Range(-10, 10); + spreadTimer = Rand.Range(-10f, 10f); HumanAIController.SortTimer = 0; } @@ -1177,7 +1177,7 @@ namespace Barotrauma } private void SpeakNoWeapons() => Speak("dialogcombatnoweapons", delay: 0, minDuration: 30); - private void AskHelp() => Speak("dialogcombatretreating", delay: Rand.Range(0, 1), minDuration: 20); + private void AskHelp() => Speak("dialogcombatretreating", delay: Rand.Range(0f, 1f), minDuration: 20); private void Speak(string textIdentifier, float delay, float minDuration) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 6e1c554c4..d06835684 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -43,7 +43,6 @@ namespace Barotrauma private readonly float minDistance = 50; private readonly float seekGapsInterval = 1; private float seekGapsTimer; - private bool cannotFollow; /// /// Display units @@ -52,6 +51,11 @@ namespace Barotrauma { get { + if (IsFollowOrderObjective && Target is Character targetCharacter && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null)) + { + // Keep close when the target is going inside/outside + return minDistance; + } float dist = _closeEnough * CloseEnoughMultiplier; float extraMultiplier = Math.Clamp(CloseEnoughMultiplier * 0.6f, 1, 3); if (character.AnimController.InWater) @@ -288,28 +292,16 @@ namespace Barotrauma { TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), onAbandon: () => Abandon = true, - onCompleted: () => - { - cannotFollow = false; - RemoveSubObjective(ref findDivingGear); - }); + onCompleted: () => RemoveSubObjective(ref findDivingGear)); } else { TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), onAbandon: () => Abandon = true, - onCompleted: () => - { - cannotFollow = false; - RemoveSubObjective(ref findDivingGear); - }); + onCompleted: () => RemoveSubObjective(ref findDivingGear)); } return; } - else - { - cannotFollow = false; - } } if (repeat) { @@ -735,7 +727,6 @@ namespace Barotrauma findDivingGear = null; seekGapsTimer = 0; TargetGap = null; - cannotFollow = false; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index d521f19f7..95b96f372 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -408,7 +408,7 @@ namespace Barotrauma if (orderGiver == null) { return null; } newObjective = new AIObjectiveGoTo(orderGiver, character, this, repeat: true, priorityModifier: priorityModifier) { - CloseEnough = Rand.Range(80, 100), + CloseEnough = Rand.Range(80f, 100f), CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountCrew(c => c.ObjectiveManager.HasOrder(o => o.Target == orderGiver), onlyBots: true) * Rand.Range(0.8f, 1f), 4), ExtraDistanceOutsideSub = 100, ExtraDistanceWhileSwimming = 100, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index ad361a9e1..1db23dee1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -221,6 +221,9 @@ namespace Barotrauma /// public int AssignmentPriority { get; } + public bool ColoredWhenControllingGiver { get; } + public bool DisplayGiverInTooltip { get; } + public static void Init() { Prefabs = new Dictionary(); @@ -406,6 +409,8 @@ namespace Barotrauma DrawIconWhenContained = orderElement.GetAttributeBool("displayiconwhencontained", false); AutoDismiss = orderElement.GetAttributeBool("autodismiss", Category == OrderCategory.Movement); AssignmentPriority = Math.Clamp(orderElement.GetAttributeInt("assignmentpriority", 100), 0, 100); + ColoredWhenControllingGiver = orderElement.GetAttributeBool("coloredwhencontrollinggiver", false); + DisplayGiverInTooltip = orderElement.GetAttributeBool("displaygiverintooltip", false); } /// @@ -441,6 +446,8 @@ namespace Barotrauma Hidden = prefab.Hidden; IgnoreAtOutpost = prefab.IgnoreAtOutpost; AssignmentPriority = prefab.AssignmentPriority; + ColoredWhenControllingGiver = prefab.ColoredWhenControllingGiver; + DisplayGiverInTooltip = prefab.DisplayGiverInTooltip; OrderGiver = orderGiver; TargetEntity = targetEntity; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs index 07104f091..079b59da6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs @@ -134,6 +134,7 @@ namespace Barotrauma aggregate += Items[i].Commonness; if (aggregate >= r && Items[i].Prefab != null) { + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetProducedItem:" + pet.AiController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier); Entity.Spawner.AddToSpawnQueue(Items[i].Prefab, pet.AiController.Character.WorldPosition); break; } @@ -200,6 +201,8 @@ namespace Barotrauma break; } } + + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetSpawned:" + aiController.Character.SpeciesName); } public StatusIndicatorType GetCurrentStatusIndicatorType() @@ -210,23 +213,44 @@ namespace Barotrauma return StatusIndicatorType.None; } - public bool OnEat(IEnumerable tags, float amount) + public bool OnEat(Item item) + { + bool success = OnEat(item.GetTags()); + if (success) + { + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + item.prefab.Identifier); + } + return success; + } + + public bool OnEat(Character character) + { + if (character == null || !character.IsDead) { return false; } + bool success = OnEat("dead"); + if (success) + { + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + character.SpeciesName); + } + return success; + } + + private bool OnEat(IEnumerable tags) { foreach (string tag in tags) { - if (OnEat(tag, amount)) { return true; } + if (OnEat(tag)) { return true; } } return false; } - public bool OnEat(string tag, float amount) + private bool OnEat(string tag) { for (int i = 0; i < foods.Count; i++) { if (tag.Equals(foods[i].Tag, System.StringComparison.OrdinalIgnoreCase)) { - Hunger += foods[i].Hunger * amount; - Happiness += foods[i].Happiness * amount; + Hunger += foods[i].Hunger; + Happiness += foods[i].Happiness; #if CLIENT AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs index fb2aa21b9..c227317ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs @@ -22,7 +22,13 @@ namespace Barotrauma public override void CalculateImportanceSpecific() { - if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot()) { return; } + if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot()) + { + //operate (= recharge the turrets) with low priority if they're out of power + //if something else (issues with reactor or the electrical grid) is preventing them from being charged, fixing those issues should take priority + Importance = ShipCommandManager.MinimumIssueThreshold * 1.05f; + return; + } targetingImportances.Clear(); foreach (Character character in shipCommandManager.EnemyCharacters) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs index 83b02089e..2cc21e90e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs @@ -51,7 +51,7 @@ namespace Barotrauma private const float RamTimerMax = 17.5f; public readonly List ShipIssueWorkers = new List(); - private const float MinimumIssueThreshold = 10f; + public const float MinimumIssueThreshold = 10f; private const float IssueDevotionBuffer = 5f; private float decisionTimer = 6f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index dc6e181ae..3fe39c57b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -254,7 +254,7 @@ namespace Barotrauma private void SpawnInitialCells() { - int brainRoomCells = Rand.Range(MinCellsPerBrainRoom, MaxCellsPerRoom); + int brainRoomCells = Rand.Range(MinCellsPerBrainRoom, MaxCellsPerRoom + 1); if (brain.CurrentHull?.WaterPercentage >= MinWaterLevel) { for (int i = 0; i < brainRoomCells; i++) @@ -262,12 +262,12 @@ namespace Barotrauma if (!TrySpawnCell(out _, brain.CurrentHull)) { break; } } } - int cellsInside = Rand.Range(MinCellsInside, MaxCellsInside); + int cellsInside = Rand.Range(MinCellsInside, MaxCellsInside + 1); for (int i = 0; i < cellsInside; i++) { if (!TrySpawnCell(out _)) { break; } } - int cellsOutside = Rand.Range(MinCellsOutside, MaxCellsOutside); + int cellsOutside = Rand.Range(MinCellsOutside, MaxCellsOutside + 1); // If we failed to spawn some of the cells in the brainroom/inside, spawn some extra cells outside. cellsOutside = Math.Clamp(cellsOutside + brainRoomCells + cellsInside - protectiveCells.Count, cellsOutside, MaxCellsOutside); for (int i = 0; i < cellsOutside; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 26732196b..28a9b6bde 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -420,7 +420,7 @@ namespace Barotrauma if (Character.AIController is EnemyAIController enemyAi) { - enemyAi.PetBehavior?.OnEat("dead", 1.0f); + enemyAi.PetBehavior?.OnEat(target); } character.SelectedCharacter = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 68a2054e4..eae78c6fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -533,6 +533,8 @@ namespace Barotrauma bool onSlope = Math.Abs(movement.X) > 0.01f && Math.Abs(floorNormal.X) > 0.1f && Math.Sign(floorNormal.X) != Math.Sign(movement.X); + bool movingHorizontally = !MathUtils.NearlyEqual(targetMovement.X, 0.0f); + if (Stairs != null || onSlope) { torso.PullJointWorldAnchorB = new Vector2( @@ -562,10 +564,8 @@ namespace Barotrauma if (!torso.Disabled) { - if (TorsoPosition.HasValue) - { - y += TorsoPosition.Value; - } + if (TorsoPosition.HasValue) { y += TorsoPosition.Value; } + if (Crouching && !movingHorizontally) { y -= HumanCrouchParams.MoveDownAmountWhenStationary; } torso.PullJointWorldAnchorB = MathUtils.SmoothStep(torso.SimPosition, new Vector2(footMid + movement.X * TorsoLeanAmount, y), getUpForce); @@ -574,10 +574,8 @@ namespace Barotrauma if (!head.Disabled) { y = colliderPos.Y + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier; - if (HeadPosition.HasValue) - { - y += HeadPosition.Value; - } + if (HeadPosition.HasValue) { y += HeadPosition.Value; } + if (Crouching && !movingHorizontally) { y -= HumanCrouchParams.MoveDownAmountWhenStationary; } head.PullJointWorldAnchorB = MathUtils.SmoothStep(head.SimPosition, new Vector2(footMid + movement.X * HeadLeanAmount, y), getUpForce * 1.2f); @@ -593,12 +591,15 @@ namespace Barotrauma { float torsoAngle = TorsoAngle.Value; float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); + if (Crouching && !movingHorizontally) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; } torsoAngle -= herpesStrength / 150.0f; torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque); } if (HeadAngle.HasValue) { - head.body.SmoothRotate(HeadAngle.Value * Dir, CurrentGroundedParams.HeadTorque); + float headAngle = HeadAngle.Value; + if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; } + head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque); } if (!onGround) @@ -616,8 +617,7 @@ namespace Barotrauma Vector2 waistPos = waist != null ? waist.SimPosition : torso.SimPosition; - //moving horizontally - if (TargetMovement.X != 0.0f) + if (movingHorizontally) { //progress the walking animation WalkPos -= MathHelper.ToRadians(CurrentAnimationParams.CycleSpeed) * walkCycleMultiplier * movement.X; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index ecccfd1db..8078d4931 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -261,9 +261,16 @@ namespace Barotrauma public AttackResult LastDamage; + public Dictionary ItemSelectedDurations + { + get { return itemSelectedDurations; } + } + private readonly Dictionary itemSelectedDurations = new Dictionary(); + private double itemSelectedTime; + public float InvisibleTimer; - private CharacterPrefab prefab; + private readonly CharacterPrefab prefab; public readonly CharacterParams Params; public string SpeciesName => Params?.SpeciesName ?? "null"; @@ -700,7 +707,7 @@ namespace Barotrauma { get { - if (!CanSpeak || IsUnconscious || Stun > 0.0f || IsDead) { return 100.0f; } + if (!CanSpeak || IsUnconscious || IsKnockedDown) { return 100.0f; } return speechImpediment; } set @@ -737,9 +744,7 @@ namespace Barotrauma get => _selectedConstruction; set { -#if CLIENT var prevSelectedConstruction = _selectedConstruction; -#endif _selectedConstruction = value; #if CLIENT HintManager.OnSetSelectedConstruction(this, prevSelectedConstruction, _selectedConstruction); @@ -755,6 +760,19 @@ namespace Barotrauma } } #endif + if (prevSelectedConstruction == null && _selectedConstruction != null) + { + itemSelectedTime = Timing.TotalTime; + } + else if (prevSelectedConstruction != null && _selectedConstruction == null && itemSelectedTime > 0) + { + if (!itemSelectedDurations.ContainsKey(prevSelectedConstruction.Prefab)) + { + itemSelectedDurations.Add(prevSelectedConstruction.Prefab, 0); + } + itemSelectedDurations[prevSelectedConstruction.Prefab] += Timing.TotalTime - itemSelectedTime; + itemSelectedTime = 0; + } } } @@ -3950,27 +3968,44 @@ namespace Barotrauma AnimController.Frozen = false; - if (GameAnalyticsManager.SendUserStatistics) - { - string characterType = "Unknown"; - - if (this == Controlled) - characterType = "Player"; - else if (IsRemotePlayer) - characterType = "RemotePlayer"; - else if (AIController is EnemyAIController) - characterType = "Enemy"; - else if (AIController is HumanAIController) - characterType = "AICrew"; - - string causeOfDeathStr = causeOfDeathAffliction == null ? - causeOfDeath.ToString() : causeOfDeathAffliction.Prefab.Name.Replace(" ", ""); - GameAnalyticsManager.AddDesignEvent("Kill:" + characterType + ":" + SpeciesName + ":" + causeOfDeathStr); - } - CauseOfDeath = new CauseOfDeath( causeOfDeath, causeOfDeathAffliction?.Prefab, - causeOfDeathAffliction?.Source ?? LastAttacker, LastDamageSource); + causeOfDeathAffliction?.Source, LastDamageSource); + + if (GameAnalyticsManager.SendUserStatistics) + { + string causeOfDeathStr = causeOfDeathAffliction == null ? + causeOfDeath.ToString() : causeOfDeathAffliction.Prefab.Identifier.Replace(" ", ""); + + string characterType = GetCharacterType(this); + GameAnalyticsManager.AddDesignEvent("Kill:" + characterType + ":" + causeOfDeathStr); + if (CauseOfDeath.Killer != null) + { + GameAnalyticsManager.AddDesignEvent("Kill:" + characterType + ":Killer:" + GetCharacterType(CauseOfDeath.Killer)); + } + if (CauseOfDeath.DamageSource != null) + { + string damageSourceStr = CauseOfDeath.DamageSource.ToString(); + if (CauseOfDeath.DamageSource is Item damageSourceItem) { damageSourceStr = damageSourceItem.ToString(); } + GameAnalyticsManager.AddDesignEvent("Kill:" + characterType + ":DamageSource:" + damageSourceStr); + } + + static string GetCharacterType(Character character) + { + if (character.IsPlayer) + return "Player"; + else if (character.AIController is EnemyAIController) + return "Enemy" + character.SpeciesName; + else if (character.AIController is HumanAIController && character.TeamID == CharacterTeamType.Team2) + return "EnemyHuman"; + else if (character.Info != null && character.TeamID == CharacterTeamType.Team1) + return "AICrew"; + else if (character.Info != null && character.TeamID == CharacterTeamType.FriendlyNPC) + return "FriendlyNPC"; + return "Unknown"; + } + } + OnDeath?.Invoke(this, CauseOfDeath); var abilityCharacterKiller = new AbilityCharacterKiller(CauseOfDeath.Killer); @@ -4097,7 +4132,7 @@ namespace Barotrauma info?.Remove(); #if CLIENT - GameMain.GameSession?.CrewManager?.KillCharacter(this); + GameMain.GameSession?.CrewManager?.KillCharacter(this, resetCrewListIndex: false); #endif CharacterList.Remove(this); @@ -4112,6 +4147,8 @@ namespace Barotrauma } } + itemSelectedDurations.Clear(); + DisposeProjSpecific(); aiTarget?.Remove(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 5edfd3d8d..fa99a10d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -1527,7 +1527,7 @@ namespace Barotrauma orderTargetElement.Add(new XAttribute("hullid", (uint)ot.Hull.ID)); position -= ot.Hull.WorldPosition; } - orderTargetElement.Add(new XAttribute("position", $"{position.X},{position.Y}")); + orderTargetElement.Add(new XAttribute("position", XMLExtensions.Vector2ToString(position))); orderElement.Add(orderTargetElement); break; case Order.OrderTargetType.WallSection when targetAvailableInNextLevel && order.TargetEntity is Structure s && order.WallSectionIndex.HasValue: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index a3c9eccc1..1f9ed8f30 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -106,7 +106,7 @@ namespace Barotrauma { if (State != InfectionState.Active && stun) { - character.SetStun(Rand.Range(2, 4)); + character.SetStun(Rand.Range(2f, 3f)); } State = InfectionState.Active; ActivateHusk(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index fda2666a1..087627e9e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -363,7 +363,9 @@ namespace Barotrauma public readonly string Name, Description; public readonly string TranslationOverride; public readonly bool IsBuff; + public readonly bool HealableInMedicalClinic; public readonly float HealCostMultiplier; + public readonly int BaseHealCost; public readonly string CauseOfDeathDescription, SelfCauseOfDeathDescription; @@ -656,7 +658,13 @@ namespace Barotrauma Name = TextManager.Get("AfflictionName." + translationId, true) ?? element.GetAttributeString("name", ""); Description = TextManager.Get("AfflictionDescription." + translationId, true) ?? element.GetAttributeString("description", ""); IsBuff = element.GetAttributeBool("isbuff", false); + + HealableInMedicalClinic = element.GetAttributeBool("healableinmedicalclinic", + !IsBuff && + !AfflictionType.Equals("geneticmaterialbuff", StringComparison.OrdinalIgnoreCase) && + !AfflictionType.Equals("geneticmaterialdebuff", StringComparison.OrdinalIgnoreCase)); HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier).ToLowerInvariant(), 1f); + BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost).ToLowerInvariant(), 0); if (element.Attribute("nameidentifier") != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index d56202294..d6f55b65d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -764,6 +764,7 @@ namespace Barotrauma if (applyAffliction) { afflictionsCopy.Add(newAffliction); + newAffliction.Source ??= attacker; } appliedDamageModifiers.AddRange(tempModifiers); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs index 4192bd331..43ed47198 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs @@ -26,6 +26,15 @@ namespace Barotrauma class HumanCrouchParams : HumanGroundedParams { + [Serialize(0.0f, true, description: "How much lower the character's head and torso move when stationary."), Editable(MinValueFloat = 0, MaxValueFloat = 2, DecimalCount = 2)] + public float MoveDownAmountWhenStationary { get; set; } + + [Serialize(0.0f, true), Editable(-360f, 360f)] + public float ExtraHeadAngleWhenStationary { get; set; } + + [Serialize(0.0f, true), Editable(-360f, 360f)] + public float ExtraTorsoAngleWhenStationary { get; set; } + public static HumanCrouchParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.Crouch); public static HumanCrouchParams GetAnimParams(Character character, string fileName = null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs index 9c4dd581a..11aa9934e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs @@ -23,7 +23,9 @@ namespace Barotrauma.Abilities multiplier = 0 + Character.Info.GetSavedStatValue(StatTypes.None, scalingStatIdentifier); } - targetCharacter.GiveMoney((int)(multiplier * amount)); + int totalAmount = (int)(multiplier * amount); + targetCharacter.GiveMoney(totalAmount); + GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier); } protected override void ApplyEffect(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs index 417b93972..00c39c0e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs @@ -16,7 +16,9 @@ namespace Barotrauma.Abilities { if ((abilityObject as IAbilityCharacter)?.Character is Character character) { - Character.GiveMoney((int)(vitalityPercentage * character.MaxVitality)); + int totalAmount = (int)(vitalityPercentage * character.MaxVitality); + Character.GiveMoney(totalAmount); + GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs index 8bfe07b9d..66a39efaa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs @@ -30,6 +30,7 @@ namespace Barotrauma.Abilities if (!enemyCharacter.LockHands) { continue; } if (timesGiven > max) { continue; } Character.GiveMoney(moneyAmount); + GameAnalyticsManager.AddMoneyGainedEvent(moneyAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier); foreach (Character character in Character.GetFriendlyCrew(Character)) { character.Info?.GiveExperience(experienceAmount); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs index 94a89cc5b..fe4073afc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs @@ -12,8 +12,6 @@ namespace Barotrauma.Abilities private readonly int moneyPerMission; - private static List clientsAlreadyUsed = new List(); - public CharacterAbilityInsurancePolicy(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { moneyPerMission = abilityElement.GetAttributeInt("moneypermission", 0); @@ -23,7 +21,9 @@ namespace Barotrauma.Abilities { if (Character?.Info is CharacterInfo info) { - Character.GiveMoney(moneyPerMission * info.MissionsCompletedSinceDeath); + int totalAmount = moneyPerMission * info.MissionsCompletedSinceDeath; + Character.GiveMoney(totalAmount); + GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 90e0402f0..1da7cf86c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -1534,6 +1534,7 @@ namespace Barotrauma if (int.TryParse(args[0], out int money)) { campaign.Money += money; + GameAnalyticsManager.AddMoneyGainedEvent(money, GameAnalyticsManager.MoneySource.Cheat, "console"); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs index 8ffc28be3..5dfe8e9f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs @@ -28,6 +28,7 @@ namespace Barotrauma if (GameMain.GameSession?.GameMode is CampaignMode campaign) { campaign.Money += Amount; + GameAnalyticsManager.AddMoneyGainedEvent(Amount, GameAnalyticsManager.MoneySource.Event, ParentEvent.Prefab.Identifier); #if SERVER (campaign as MultiPlayerCampaign).LastUpdateID++; #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 53bbb3732..32a2d3c6c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -152,7 +152,7 @@ namespace Barotrauma if (spawnPoint is WayPoint wp && wp.CurrentHull != null && wp.CurrentHull.Rect.Width > 100) { spawnPos = new Vector2( - MathHelper.Clamp(wp.WorldPosition.X + Rand.Range(-200, 200), wp.CurrentHull.WorldRect.X + 50, wp.CurrentHull.WorldRect.Right - 50), + MathHelper.Clamp(wp.WorldPosition.X + Rand.Range(-200, 201), wp.CurrentHull.WorldRect.X + 50, wp.CurrentHull.WorldRect.Right - 50), wp.CurrentHull.WorldRect.Y - wp.CurrentHull.Rect.Height + 16.0f); } var item = new Item(itemPrefab, spawnPos, null); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs index bffec355a..b2e27d3ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -319,6 +319,19 @@ namespace Barotrauma } } + foreach (Character character in characters) + { + if (character.Inventory == null) { continue; } + foreach (Item item in character.Inventory.AllItemsMod) + { + //item didn't spawn with the characters -> drop it + if (!characterItems.Any(c => c.Value.Contains(item))) + { + item.Drop(character); + } + } + } + // characters that survived will take their items with them, in case players tried to be crafty and steal them // this needs to run here in case players abort the mission by going back home // TODO: I think this might feel like a bug. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 718ad830a..3e8a55495 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -378,7 +378,10 @@ namespace Barotrauma crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier)); crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); - campaign.Money += (int)(reward * missionMoneyGainMultiplier.Value); + int totalReward = (int)(reward * missionMoneyGainMultiplier.Value); + campaign.Money += totalReward; + + GameAnalyticsManager.AddMoneyGainedEvent(totalReward, GameAnalyticsManager.MoneySource.MissionReward, Prefab.Identifier); foreach (Character character in crewCharacters) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 0735f65f6..e1c8e0a14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -328,7 +328,7 @@ namespace Barotrauma } else { - dir = new Vector2(1, Rand.Range(-1, 1)); + dir = new Vector2(1, Rand.Range(-1f, 1f)); } Vector2 targetPos = spawnPos.Value + dir * offset; var targetWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, targetPos)).FirstOrDefault(); @@ -475,6 +475,7 @@ namespace Barotrauma { scatterAmount = 0; } + for (int i = 0; i < amount; i++) { string seed = Level.Loaded.Seed + i.ToString(); @@ -540,6 +541,13 @@ namespace Barotrauma SwarmBehavior.CreateSwarm(monsters.Cast()); DebugConsole.NewMessage($"Spawned: {ToString()}. Strength: {StringFormatter.FormatZeroDecimal(monsters.Sum(m => m.Params.AI.CombatStrength))}.", Color.LightBlue, debugOnly: true); } + + if (GameMain.GameSession != null) + { + GameAnalyticsManager.AddDesignEvent( + $"MonsterSpawn:{GameMain.GameSession.GameMode?.Preset?.Identifier ?? "none"}:{Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"}:{SpawnPosType}:{speciesName}", + value: Timing.TotalTime - GameMain.GameSession.RoundStartTime); + } }, delayBetweenSpawns * i); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs index b4f8652c1..e8d0be2ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs @@ -1,12 +1,11 @@ -using System; using Barotrauma.Steam; using RestSharp; +using System; using System.Net; -using System.Threading.Tasks; namespace Barotrauma { - public static partial class GameAnalyticsManager + static partial class GameAnalyticsManager { public enum Consent { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs index 7a0033bce..7081b424e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs @@ -1,5 +1,6 @@ #nullable enable using Barotrauma.IO; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -9,7 +10,7 @@ using System.Text; namespace Barotrauma { - public static partial class GameAnalyticsManager + static partial class GameAnalyticsManager { public enum ErrorSeverity { @@ -29,6 +30,61 @@ namespace Barotrauma Fail = 3 } + public enum CustomDimensions01 + { + Vanilla, + Modded + } + + public enum CustomDimensions02 + { + None, + Difficulty0to10, + Difficulty10to20, + Difficulty20to30, + Difficulty30to40, + Difficulty40to50, + Difficulty50to60, + Difficulty60to70, + Difficulty70to80, + Difficulty80to90, + Difficulty90to100, + } + + public enum ResourceCurrency + { + Money + } + + public enum ResourceFlowType + { + Undefined = 0, + Source = 1, + Sink = 2 + } + + public enum MoneySource + { + Unknown, + MissionReward, + Store, + Event, + Ability, + Cheat + } + + public enum MoneySink + { + Unknown, + Store, + Service, + Crew, + SubmarineUpgrade, + SubmarineWeapon, + SubmarinePurchase, + SubmarineSwitch + } + private readonly static HashSet sentEventIdentifiers = new HashSet(); private class Implementation : IDisposable @@ -69,13 +125,29 @@ namespace Barotrauma internal void AddProgressionEvent(ProgressionStatus status, string progression01, string progression02, string progression03) => addProgressionEvent03(status, progression01, progression02, progression03); + private readonly Action addResourceEvent; + internal void AddResourceEvent(ResourceFlowType flowType, string currency, float amount, string itemType, string itemId) + => addResourceEvent(flowType, currency, amount, itemType, itemId); + private readonly Action setCustomDimension01; internal void SetCustomDimension01(string dimension01) => setCustomDimension01(dimension01); private readonly Action configureAvailableCustomDimensions01; - internal void ConfigureAvailableCustomDimensions01(params string[] customDimensions) - => configureAvailableCustomDimensions01(customDimensions); + internal void ConfigureAvailableCustomDimensions01(params CustomDimensions01[] customDimensions) + => configureAvailableCustomDimensions01(customDimensions.Select(d => d.ToString()).ToArray()); + + private readonly Action setCustomDimension02; + internal void SetCustomDimension02(string dimension02) + => setCustomDimension02(dimension02); + + private readonly Action configureAvailableCustomDimensions02; + internal void ConfigureAvailableCustomDimensions02(params CustomDimensions02[] customDimensions) + => configureAvailableCustomDimensions02(customDimensions.Select(d => d.ToString()).ToArray()); + + private readonly Action configureAvailableResourceCurrencies; + internal void ConfigureAvailableResourceCurrencies(params ResourceCurrency[] customDimensions) + => configureAvailableResourceCurrencies(customDimensions.Select(d => d.ToString()).ToArray()); private readonly Action setEnabledInfoLog; internal void SetEnabledInfoLog(bool enabled) @@ -94,6 +166,7 @@ namespace Barotrauma private readonly object?[] args2 = new object?[2]; private readonly object?[] args3 = new object?[3]; private readonly object?[] args4 = new object?[4]; + private readonly object?[] args5 = new object?[5]; private Action Call(MethodInfo methodInfo) => () => methodInfo?.Invoke(null, null); @@ -131,6 +204,17 @@ namespace Barotrauma args4[3] = arg4; methodInfo.Invoke(null, args4); }; + + private Action Call(MethodInfo methodInfo) + => (T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => + { + args5[0] = arg1; + args5[1] = arg2; + args5[2] = arg3; + args5[3] = arg4; + args5[4] = arg5; + methodInfo.Invoke(null, args5); + }; #endregion private AssemblyLoadContext? loadContext; @@ -165,9 +249,15 @@ namespace Barotrauma var mainClass = getType(MainClass); var errorSeverityEnumType = getType($"{EnumPrefix}{nameof(ErrorSeverity)}"); var progressionStatusEnumType = getType($"{EnumPrefix}{nameof(ProgressionStatus)}"); + var resourceFlowTypeEnumType = getType($"{EnumPrefix}{nameof(ResourceFlowType)}"); MethodInfo getMethod(string name, Type[] types) { + foreach (var me in mainClass.GetMethods()) + { + var aksjdnakjsdnf = me; + } + return mainClass?.GetMethod(name, BindingFlags.Public | BindingFlags.Static, binder: null, types: types, modifiers: null) ?? throw new Exception($"Could not find method \"{name}\" with types {string.Join(',', types.Select(t => t.Name))}"); } @@ -190,10 +280,20 @@ namespace Barotrauma new Type[] { progressionStatusEnumType, typeof(string), typeof(string) })); addProgressionEvent03 = Call(getMethod(nameof(AddProgressionEvent), new Type[] { progressionStatusEnumType, typeof(string), typeof(string), typeof(string) })); + setCustomDimension01 = Call(getMethod(nameof(SetCustomDimension01), new Type[] { typeof(string) })); configureAvailableCustomDimensions01 = Call(getMethod(nameof(ConfigureAvailableCustomDimensions01), new Type[] { typeof(string[]) })); + setCustomDimension02 = Call(getMethod(nameof(SetCustomDimension02), + new Type[] { typeof(string) })); + configureAvailableCustomDimensions02 = Call(getMethod(nameof(ConfigureAvailableCustomDimensions02), + new Type[] { typeof(string[]) })); + + configureAvailableResourceCurrencies = Call(getMethod(nameof(ConfigureAvailableResourceCurrencies), + new Type[] { typeof(string[]) })); + addResourceEvent = Call(getMethod(nameof(AddResourceEvent), + new Type[] { resourceFlowTypeEnumType, typeof(string), typeof(float), typeof(string), typeof(string) })); setEnabledInfoLog = Call(getMethod(nameof(SetEnabledInfoLog), new Type[] { typeof(bool) })); @@ -204,8 +304,7 @@ namespace Barotrauma private void OnQuit() { try - { - + { if (assembly != null) { onQuit?.Invoke(); } } catch (Exception e) @@ -298,10 +397,40 @@ namespace Barotrauma loadedImplementation?.AddProgressionEvent(progressionStatus, progression01, progression02, progression03); } - public static void SetCustomDimension01(string dimension) + public static void SetCustomDimension01(CustomDimensions01 dimension) { if (!SendUserStatistics) { return; } - loadedImplementation?.SetCustomDimension01(dimension); + loadedImplementation?.SetCustomDimension01(dimension.ToString()); + } + + public static void SetCurrentLevel(LevelData levelData) + { + if (!SendUserStatistics) { return; } + + CustomDimensions02 customDimension = CustomDimensions02.None; + if (levelData != null) + { + float levelDifficulty = levelData.Difficulty; + customDimension = (CustomDimensions02)MathHelper.Clamp((int)(levelDifficulty / 10) + 1, 0, Enum.GetValues(typeof(CustomDimensions02)).Length - 1); + } + + loadedImplementation?.SetCustomDimension02(customDimension.ToString()); + } + + public static void AddMoneyGainedEvent(int amount, MoneySource moneySource, string eventId) + { + AddResourceEvent(ResourceFlowType.Source, ResourceCurrency.Money, amount, moneySource.ToString(), eventId); + } + + public static void AddMoneySpentEvent(int amount, MoneySink moneySink, string eventId) + { + AddResourceEvent(ResourceFlowType.Sink, ResourceCurrency.Money, amount, moneySink.ToString(), eventId); + } + + private static void AddResourceEvent(ResourceFlowType flowType, ResourceCurrency currency, float amount, string eventType, string eventId) + { + if (!SendUserStatistics) { return; } + loadedImplementation?.AddResourceEvent(flowType, currency.ToString(), amount, eventType, eventId); } private static void Init() @@ -359,7 +488,8 @@ namespace Barotrauma + exeName + ":" + AssemblyInfo.GitRevision + ":" + buildConfiguration); - loadedImplementation?.ConfigureAvailableCustomDimensions01("singleplayer", "multiplayer", "editor"); + loadedImplementation?.ConfigureAvailableCustomDimensions01(Enum.GetValues(typeof(CustomDimensions01)).Cast().ToArray()); + loadedImplementation?.ConfigureAvailableResourceCurrencies(Enum.GetValues(typeof(ResourceCurrency)).Cast().ToArray()); InitKeys(); @@ -380,15 +510,16 @@ namespace Barotrauma var allPackages = GameMain.Config?.AllEnabledPackages.ToList(); if (allPackages?.Count > 0) { - StringBuilder sb = new StringBuilder("ContentPackage: "); - int i = 0; + List packageNames = new List(); foreach (ContentPackage cp in allPackages) { - string trimmedName = cp.Name.Replace(":", "").Replace(" ", ""); - sb.Append(trimmedName.Substring(0, Math.Min(32, trimmedName.Length))); - if (i < allPackages.Count - 1) { sb.Append(" "); } + string sanitizedName = cp.Name.Replace(":", "").Replace(" ", ""); + sanitizedName = sanitizedName.Substring(0, Math.Min(32, sanitizedName.Length)); + packageNames.Add(sanitizedName); + loadedImplementation?.AddDesignEvent("ContentPackage:" + sanitizedName); } - loadedImplementation?.AddDesignEvent(sb.ToString()); + packageNames.Sort(); + loadedImplementation?.AddDesignEvent("AllContentPackages:" + string.Join(", ", packageNames)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 1d7049542..9742c0287 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -132,6 +132,7 @@ namespace Barotrauma // Exchange money var itemValue = item.Quantity * buyValues[item.ItemPrefab]; campaign.Money -= itemValue; + GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier); Location.StoreCurrentBalance += itemValue; if (removeFromCrate) @@ -291,7 +292,7 @@ namespace Barotrauma float floorPos = hull.Rect.Y - hull.Rect.Height; Vector2 position = new Vector2( - hull.Rect.Width > 40 ? Rand.Range(hull.Rect.X + 20, hull.Rect.Right - 20) : hull.Rect.Center.X, + hull.Rect.Width > 40 ? Rand.Range(hull.Rect.X + 20f, hull.Rect.Right - 20f) : hull.Rect.Center.X, floorPos); //check where the actual floor structure is in case the bottom of the hull extends below it diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index aac23f1d6..bbd0ca3f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -37,7 +37,6 @@ namespace Barotrauma { IsSinglePlayer = isSinglePlayer; conversationTimer = 5.0f; - InitProjectSpecific(); } @@ -100,10 +99,10 @@ namespace Barotrauma foreach (XElement characterElement in element.Elements()) { if (!characterElement.Name.ToString().Equals("character", StringComparison.OrdinalIgnoreCase)) { continue; } - CharacterInfo characterInfo = new CharacterInfo(characterElement); #if CLIENT if (characterElement.GetAttributeBool("lastcontrolled", false)) { characterInfo.LastControlled = true; } + characterInfo.CrewListIndex = characterElement.GetAttributeInt("crewlistindex", -1); #endif characterInfos.Add(characterInfo); foreach (XElement subElement in characterElement.Elements()) @@ -133,7 +132,7 @@ namespace Barotrauma characterInfos.Remove(characterInfo); } - public void AddCharacter(Character character) + public void AddCharacter(Character character, bool sortCrewList = true) { if (character.Removed) { @@ -155,7 +154,11 @@ namespace Barotrauma characterInfos.Add(character.Info); } #if CLIENT - AddCharacterToCrewList(character); + var characterComponent = AddCharacterToCrewList(character); + if (sortCrewList) + { + SortCrewList(); + } if (character.CurrentOrders != null) { foreach (var order in character.CurrentOrders) @@ -254,12 +257,16 @@ namespace Barotrauma } } - AddCharacter(character); + AddCharacter(character, sortCrewList: false); #if CLIENT if (IsSinglePlayer && (Character.Controlled == null || character.Info.LastControlled)) { Character.Controlled = character; } #endif } +#if CLIENT + if (IsSinglePlayer) { SortCrewList(); } +#endif + //longer delay in multiplayer to prevent the server from triggering NPC conversations while the players are still loading the round conversationTimer = IsSinglePlayer ? Rand.Range(5.0f, 10.0f) : Rand.Range(45.0f, 60.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs index b63562956..d22a8e83c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs @@ -14,7 +14,7 @@ namespace Barotrauma public Faction(CampaignMetadata metadata, FactionPrefab prefab) { Prefab = prefab; - Reputation = new Reputation(metadata, $"faction.{prefab.Identifier}", prefab.MinReputation, prefab.MaxReputation, prefab.InitialReputation); + Reputation = new Reputation(metadata, this, prefab.MinReputation, prefab.MaxReputation, prefab.InitialReputation); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index 85a3ef2f4..ada24867c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -35,9 +35,22 @@ namespace Barotrauma private set { if (MathUtils.NearlyEqual(Value, value)) { return; } + + float prevValue = Value; + Metadata.SetValue(metaDataIdentifier, Math.Clamp(value, MinReputation, MaxReputation)); OnReputationValueChanged?.Invoke(); OnAnyReputationValueChanged?.Invoke(); +#if CLIENT + int increase = (int)Value - (int)prevValue; + if (increase != 0 && Character.Controlled != null) + { + Character.Controlled.AddMessage( + TextManager.GetWithVariable("reputationgainnotification", "[reputationname]", Location?.Name ?? Faction.Prefab.Name), + increase > 0 ? GUI.Style.Green : GUI.Style.Red, + playSound: true, Identifier, increase, lifetime: 5.0f); + } +#endif } } @@ -63,15 +76,32 @@ namespace Barotrauma public Action OnReputationValueChanged; public static Action OnAnyReputationValueChanged; - public Reputation(CampaignMetadata metadata, string identifier, int minReputation, int maxReputation, int initialReputation) + public readonly Faction Faction; + public readonly Location Location; + + + public Reputation(CampaignMetadata metadata, Location location, string identifier, int minReputation, int maxReputation, int initialReputation) + : this(metadata, null, location, identifier, minReputation, maxReputation, initialReputation) + { + } + + public Reputation(CampaignMetadata metadata, Faction faction, int minReputation, int maxReputation, int initialReputation) + : this(metadata, faction, null, $"faction.{faction.Prefab.Identifier}", minReputation, maxReputation, initialReputation) + { + } + + private Reputation(CampaignMetadata metadata, Faction faction, Location location, string identifier, int minReputation, int maxReputation, int initialReputation) { System.Diagnostics.Debug.Assert(metadata != null); + System.Diagnostics.Debug.Assert(faction != null || location != null); Metadata = metadata; Identifier = identifier.ToLowerInvariant(); metaDataIdentifier = $"reputation.{Identifier}"; MinReputation = minReputation; MaxReputation = maxReputation; InitialReputation = initialReputation; + Faction = faction; + Location = location; } public string GetReputationName() diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 5602ed210..fc92c4147 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -78,6 +78,9 @@ namespace Barotrauma //there can be no events before this time has passed during the 1st campaign round const float FirstRoundEventDelay = 0.0f; + public double TotalPlayTime; + public int TotalPassedLevels; + public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Repair, Upgrade, PurchaseSub, MedicalClinic } public readonly CargoManager CargoManager; @@ -92,7 +95,7 @@ namespace Barotrauma public CampaignSettings Settings; - private List extraMissions = new List(); + private readonly List extraMissions = new List(); public enum TransitionType { @@ -690,11 +693,13 @@ namespace Barotrauma GameAnalyticsManager.AddProgressionEvent( GameAnalyticsManager.ProgressionStatus.Complete, - Name ?? "none"); + Preset?.Identifier ?? "none"); string eventId = "FinishCampaign:"; GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none")); GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0)); - GameAnalyticsManager.AddDesignEvent(eventId + "Money", Money); + GameAnalyticsManager.AddDesignEvent(eventId + "Money", Money); + GameAnalyticsManager.AddDesignEvent(eventId + "Playtime", TotalPlayTime); + GameAnalyticsManager.AddDesignEvent(eventId + "PassedLevels", TotalPassedLevels); } protected virtual void EndCampaignProjSpecific() { } @@ -707,12 +712,14 @@ namespace Barotrauma location.RemoveHireableCharacter(characterInfo); CrewManager.AddCharacterInfo(characterInfo); Money -= characterInfo.Salary; + GameAnalyticsManager.AddMoneySpentEvent(characterInfo.Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.Job?.Prefab.Identifier ?? "unknown"); return true; } private void NPCInteract(Character npc, Character interactor) { if (!npc.AllowCustomInteract) { return; } + GameAnalyticsManager.AddDesignEvent("CampaignInteraction:" + Preset.Identifier + ":" + npc.CampaignInteractionType); NPCInteractProjSpecific(npc, interactor); string coroutineName = "DoCharacterWait." + (npc?.ID ?? Entity.NullEntityID); if (!CoroutineManager.IsCoroutineRunning(coroutineName)) @@ -876,6 +883,19 @@ namespace Barotrauma } public abstract void Save(XElement element); + + protected void LoadStats(XElement element) + { + TotalPlayTime = element.GetAttributeDouble(nameof(TotalPlayTime).ToLowerInvariant(), 0); + TotalPassedLevels = element.GetAttributeInt(nameof(TotalPassedLevels).ToLowerInvariant(), 0); + } + + protected XElement SaveStats() + { + return new XElement("stats", + new XAttribute(nameof(TotalPlayTime).ToLowerInvariant(), TotalPlayTime), + new XAttribute(nameof(TotalPassedLevels).ToLowerInvariant(), TotalPassedLevels)); + } public void LogState() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 785f21700..533a3773e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -126,6 +126,10 @@ namespace Barotrauma { case "campaignsettings": Settings = new CampaignSettings(subElement); +#if CLIENT + GameMain.NetworkMember.ServerSettings.MaxMissionCount = Settings.MaxMissionCount; + GameMain.NetworkMember.ServerSettings.RadiationEnabled = Settings.RadiationEnabled; +#endif break; case "map": if (map == null) @@ -159,6 +163,9 @@ namespace Barotrauma case "pets": petsElement = subElement; break; + case "stats": + LoadStats(subElement); + break; #if SERVER case "savedexperiencepoints": foreach (XElement savedExp in subElement.Elements()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 58a58c0a7..cd5d9118f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -22,6 +22,8 @@ namespace Barotrauma public double RoundStartTime; + public double TimeSpentCleaning, TimeSpentPainting; + private readonly List missions = new List(); public IEnumerable Missions { get { return missions; } } @@ -276,6 +278,7 @@ namespace Barotrauma } Campaign.Money -= cost; + GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name); ((CampaignMode)GameMode).PendingSubmarineSwitch = newSubmarine; return newSubmarine; @@ -288,6 +291,7 @@ namespace Barotrauma if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { Campaign.Money -= newSubmarine.Price; + GameAnalyticsManager.AddMoneySpentEvent(newSubmarine.Price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name); OwnedSubmarines.Add(newSubmarine); } } @@ -409,7 +413,7 @@ namespace Barotrauma GameAnalyticsManager.AddProgressionEvent( GameAnalyticsManager.ProgressionStatus.Start, - GameMode?.Name ?? "none"); + GameMode?.Preset?.Identifier ?? "none"); string eventId = "StartRound:" + (GameMode?.Preset?.Identifier ?? "none") + ":"; GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none")); @@ -419,17 +423,39 @@ namespace Barotrauma { GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier); } - GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none")); + if (Level.Loaded != null) + { + string levelId = Level.Loaded.Type == LevelData.LevelType.Outpost ? + Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier : + Level.Loaded.GenerationParams?.Identifier; + GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + Level.Loaded.Type.ToString() + ":" + (levelId ?? "null")); + } GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none")); +#if CLIENT + if (GameMode is TutorialMode tutorialMode) + { + GameAnalyticsManager.AddDesignEvent(eventId + tutorialMode.Tutorial.Identifier); + if (GameMain.IsFirstLaunch) + { + GameAnalyticsManager.AddDesignEvent("FirstLaunch:" + eventId + tutorialMode.Tutorial.Identifier); + } + } +#endif if (GameMode is CampaignMode campaignMode) { if (campaignMode.Map?.Radiation != null && campaignMode.Map.Radiation.Enabled) { - GameAnalyticsManager.AddDesignEvent(eventId + "RadiationEnabled"); + GameAnalyticsManager.AddDesignEvent(eventId + "Radiation:Enabled"); } else { - GameAnalyticsManager.AddDesignEvent(eventId + "RadiationDisabled"); + GameAnalyticsManager.AddDesignEvent(eventId + "Radiation:Disabled"); + } + bool firstTimeInBiome = Map != null && !Map.Connections.Any(c => c.Passed && c.Biome == LevelData.Biome); + if (firstTimeInBiome) + { + GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none") + "Discovered:Playtime", campaignMode.TotalPlayTime); + GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none") + "Discovered:PassedLevels", campaignMode.TotalPassedLevels); } } @@ -759,38 +785,29 @@ namespace Barotrauma GameMode?.End(transitionType); EventManager?.EndRound(); StatusEffect.StopAll(); - missions.Clear(); IsRunning = false; - - bool success = false; #if CLIENT - success = CrewManager.GetCharacters().Any(c => !c.IsDead); + bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); #else - success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); + bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); #endif double roundDuration = Timing.TotalTime - RoundStartTime; GameAnalyticsManager.AddProgressionEvent( success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail, GameMode?.Name ?? "none", roundDuration); - string eventId = "EndRound:GameMode:" + (GameMode?.Name ?? "none") + ":"; - GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), roundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none"), roundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), roundDuration); - foreach (Mission mission in missions) - { - GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), roundDuration); - } - GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none"), roundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"), roundDuration); + string eventId = "EndRound:" + (GameMode?.Preset?.Identifier ?? "none") + ":"; + LogEndRoundStats(eventId); if (GameMode is CampaignMode campaignMode) { GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", campaignMode.Money - prevMoney); + campaignMode.TotalPlayTime += roundDuration; } #if CLIENT HintManager.OnRoundEnded(); #endif + missions.Clear(); } finally { @@ -798,6 +815,82 @@ namespace Barotrauma } } + public void LogEndRoundStats(string eventId) + { + double roundDuration = Timing.TotalTime - RoundStartTime; + GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), roundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none"), roundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), roundDuration); + foreach (Mission mission in missions) + { + GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), roundDuration); + } + if (Level.Loaded != null) + { + string levelId = Level.Loaded.Type == LevelData.LevelType.Outpost ? + Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier : + Level.Loaded.GenerationParams?.Identifier; + GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + (levelId ?? "null")), roundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"), roundDuration); + } + + if (Submarine.MainSub != null) + { + Dictionary submarineInventory = new Dictionary(); + foreach (Item item in Item.ItemList) + { + var rootContainer = item.GetRootContainer() ?? item; + if (rootContainer.Submarine?.Info == null || rootContainer.Submarine.Info.Type != SubmarineType.Player) { continue; } + if (rootContainer.Submarine != Submarine.MainSub && !Submarine.MainSub.DockedTo.Contains(rootContainer.Submarine)) { continue; } + + var holdable = item.GetComponent(); + if (holdable == null || holdable.Attached) { continue; } + var wire = item.GetComponent(); + if (wire != null && wire.Connections.Any(c => c != null)) { continue; } + + if (!submarineInventory.ContainsKey(item.Prefab)) + { + submarineInventory.Add(item.Prefab, 0); + } + submarineInventory[item.Prefab]++; + } + foreach (var subItem in submarineInventory) + { + GameAnalyticsManager.AddDesignEvent(eventId + "SubmarineInventory:" + subItem.Key.Identifier, subItem.Value); + } + } + + foreach (Character c in GetSessionCrewCharacters()) + { + foreach (var itemSelectedDuration in c.ItemSelectedDurations) + { + string characterType = "Unknown"; + if (c.IsBot) + { + characterType = "Bot"; + } + else if (c.IsPlayer) + { + characterType = "Player"; + } + GameAnalyticsManager.AddDesignEvent("TimeSpentOnDevices:" + (GameMode?.Preset?.Identifier ?? "none") + ":" + characterType + ":" + (c.Info?.Job?.Prefab.Identifier ?? "NoJob") + ":" + itemSelectedDuration.Key.Identifier, itemSelectedDuration.Value); + } + } +#if CLIENT + if (GameMode is TutorialMode tutorialMode) + { + GameAnalyticsManager.AddDesignEvent(eventId + tutorialMode.Tutorial.Identifier); + if (GameMain.IsFirstLaunch) + { + GameAnalyticsManager.AddDesignEvent("FirstLaunch:" + eventId + tutorialMode.Tutorial.Identifier); + } + } + GameAnalyticsManager.AddDesignEvent(eventId + "TimeSpentCleaning", TimeSpentCleaning); + GameAnalyticsManager.AddDesignEvent(eventId + "TimeSpentPainting", TimeSpentPainting); + TimeSpentCleaning = TimeSpentPainting = 0.0; +#endif + } + public void KillCharacter(Character character) { #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs index 1847ebf36..80fe1bb35 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs @@ -102,7 +102,7 @@ namespace Barotrauma { Identifier = value.Identifier; Strength = (ushort)Math.Ceiling(value.Strength); - Price = (ushort)(Strength * value.Prefab.HealCostMultiplier); + Price = (ushort)(value.Prefab.BaseHealCost + Strength * value.Prefab.HealCostMultiplier); } } @@ -276,9 +276,15 @@ namespace Barotrauma PendingHeals.Add(crewMember); } + public static bool IsHealable(Affliction affliction) + { + return affliction.Prefab.HealableInMedicalClinic && affliction.Strength > GetShowTreshold(affliction); + static float GetShowTreshold(Affliction affliction) => Math.Max(0, Math.Min(affliction.Prefab.ShowIconToOthersThreshold, affliction.Prefab.ShowInHealthScannerThreshold)); + } + private NetAffliction[] GetAllAfflictions(CharacterHealth health) { - IEnumerable rawAfflictions = health.GetAllAfflictions().Where(a => !a.Prefab.IsBuff && a.Strength > GetShowTreshold(a)); + IEnumerable rawAfflictions = health.GetAllAfflictions().Where(a => IsHealable(a)); List afflictions = new List(); @@ -289,7 +295,7 @@ namespace Barotrauma { afflictions.Remove(foundAffliction); foundAffliction.Strength += (ushort)affliction.Strength; - foundAffliction.Price += (ushort)GetAdjustedPrice((int)(affliction.Prefab.HealCostMultiplier * affliction.Strength)); + foundAffliction.Price += (ushort)GetAdjustedPrice(GetHealPrice(affliction)); newAffliction = foundAffliction; } else @@ -303,7 +309,7 @@ namespace Barotrauma return afflictions.ToArray(); - static float GetShowTreshold(Affliction affliction) => Math.Max(0, Math.Min(affliction.Prefab.ShowIconToOthersThreshold, affliction.Prefab.ShowInHealthScannerThreshold)); + static int GetHealPrice(Affliction affliction) => (int)(affliction.Prefab.BaseHealCost + (affliction.Prefab.HealCostMultiplier * affliction.Strength)); } public int GetTotalCost() => PendingHeals.SelectMany(h => h.Afflictions).Aggregate(0, (current, affliction) => current + affliction.Price); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index 63c55fad1..5c0429327 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -225,6 +225,7 @@ namespace Barotrauma } Campaign.Money -= price; + GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineUpgrade, prefab.Identifier); PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category); @@ -323,6 +324,7 @@ namespace Barotrauma } Campaign.Money -= price; + GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineWeapon, itemToInstall.Identifier); foreach (Item itemToSwap in linkedItems) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs index 28a6d9571..c8b5cd5e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs @@ -488,7 +488,11 @@ namespace Barotrauma var sortedSelected = enabledRegularPackages .OrderBy(p => -ContentPackage.RegularPackages.IndexOf(p)) .ToList(); - if (previousEnabledRegularPackages.SequenceEqual(sortedSelected)) { return; } + if (previousEnabledRegularPackages.SequenceEqual(sortedSelected)) + { + CheckModded(); + return; + } enabledRegularPackages.Clear(); enabledRegularPackages.AddRange(sortedSelected); CharacterPrefab.Prefabs.SortAll(); @@ -508,6 +512,20 @@ namespace Barotrauma { RefreshContentPackageItems(AllEnabledPackages.SelectMany(p => p.Files)); } + + CheckModded(); + + void CheckModded() + { + if (AllEnabledPackages.Any(p => p != GameMain.VanillaContent && p.HasMultiplayerIncompatibleContent)) + { + GameAnalyticsManager.SetCustomDimension01(GameAnalyticsManager.CustomDimensions01.Modded); + } + else + { + GameAnalyticsManager.SetCustomDimension01(GameAnalyticsManager.CustomDimensions01.Vanilla); + } + } } public void EnableContentPackageItems(IEnumerable unorderedFiles) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs index 194f60d6d..c8b8782ba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs @@ -74,7 +74,6 @@ namespace Barotrauma.Items.Components a.Identifier.Equals(Effect, StringComparison.OrdinalIgnoreCase) || a.AfflictionType.Equals(Effect, StringComparison.OrdinalIgnoreCase)).GetRandom(); } - Tainted = true; } [Serialize(3.0f, false)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index 9e35e423b..a9bd794cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -1,13 +1,13 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; using Barotrauma.Extensions; using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; using Vector2 = Microsoft.Xna.Framework.Vector2; using Vector4 = Microsoft.Xna.Framework.Vector4; @@ -398,8 +398,6 @@ namespace Barotrauma.Items.Components private int flowerVariants; private int leafVariants; private int[] flowerTiles; - private const int serverHealthUpdateDelay = 10; - private int serverHealthUpdateTimer; public float Health { @@ -553,19 +551,21 @@ namespace Barotrauma.Items.Components if (spawnProduct && ProducedItems.Any()) { - SpawnItem(ProducedItems.RandomElementByWeight(it => it.Probability), spawnPos); + SpawnItem(Item, ProducedItems.RandomElementByWeight(it => it.Probability), spawnPos); return; } if (spawnSeed) { - SpawnItem(ProducedSeed, spawnPos); + SpawnItem(Item, ProducedSeed, spawnPos); } - static void SpawnItem(ProducedItem producedItem, Vector2 pos) + static void SpawnItem(Item thisItem, ProducedItem producedItem, Vector2 pos) { if (producedItem.Prefab == null) { return; } + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":GardeningProduce:" + thisItem.prefab.Identifier + ":" + producedItem.Prefab.Identifier); + Entity.Spawner?.AddToSpawnQueue(producedItem.Prefab, pos, onSpawned: it => { foreach (StatusEffect effect in producedItem.StatusEffects) @@ -586,8 +586,13 @@ namespace Barotrauma.Items.Components { if (Decayed) { return true; } - if (0 >= Health) + if (Health <= 0) { + if (!Decayed) + { + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":GardeningDied:" + item.prefab.Identifier); + } + Decayed = true; #if CLIENT foreach (VineTile vine in Vines) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 99ef8136d..7d623bc39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -237,10 +237,13 @@ namespace Barotrauma.Items.Components } } + private bool loadedFromXml; public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); + loadedFromXml = true; + if (usePrefabValues) { //this needs to be loaded regardless @@ -536,7 +539,16 @@ namespace Barotrauma.Items.Components else { attachTargetCell = GetAttachTargetCell(150.0f); - if (attachTargetCell != null) { IsActive = true; } + if (attachTargetCell != null && attachTargetCell.IsDestructible) + { + attachTargetCell.OnDestroyed += () => + { + if (attachTargetCell != null && attachTargetCell.CellType != Voronoi2.CellType.Solid) + { + Drop(dropConnectedWires: true, dropper: null); + } + }; + } } } @@ -562,7 +574,7 @@ namespace Barotrauma.Items.Components public void DeattachFromWall() { - if (!attachable) return; + if (!attachable) { return; } Attached = false; attachTargetCell = null; @@ -733,15 +745,6 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (attachTargetCell != null) - { - if (attachTargetCell.CellType != Voronoi2.CellType.Solid) - { - Drop(dropConnectedWires: true, dropper: null); - } - return; - } - if (item.body == null || !item.body.Enabled) { return; } if (picker == null || !picker.HasEquippedItem(item)) { @@ -838,15 +841,25 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { - if (item.Submarine != null && item.Submarine.Loading) return; + if (item.Submarine != null && item.Submarine.Loading) { return; } OnMapLoaded(); item.SetActiveSprite(); } public override void OnMapLoaded() { - if (!attachable) return; + if (!attachable) { return; } + //a mod has overridden the item, and the base item didn't have a Holdable component = a mod made the item movable/detachable + if (item.Prefab.IsOverride && !loadedFromXml) + { + if (attachedByDefault) + { + AttachToWall(); + return; + } + } + if (Attached) { AttachToWall(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index c45a4894d..76eca095e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -51,7 +51,11 @@ namespace Barotrauma.Items.Components #else if (deattachTimer >= DeattachDuration) { - holdable.DeattachFromWall(); + if (holdable.Attached) + { + GameAnalyticsManager.AddDesignEvent("ResourceCollected:" + (GameMain.GameSession?.GameMode?.Name ?? "none") + ":" + item.Prefab.Identifier); + holdable.DeattachFromWall(); + } trigger.Enabled = false; } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index ef6174f36..cebf5f018 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -855,6 +855,10 @@ namespace Barotrauma.Items.Components { currentTargets.Add(structure); } + if (character != null) + { + currentTargets.Add(character); + } effect.Apply(actionType, deltaTime, item, currentTargets); } else if (effect.HasTargetType(StatusEffect.TargetType.Character)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 45c52ad50..e7ac5e34e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -570,6 +570,7 @@ namespace Barotrauma.Items.Components { GUI.RemoveFromUpdateList(GuiFrame, true); GuiFrame.RectTransform.Parent = null; + GuiFrame = null; } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index d172c7553..e32673000 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -304,7 +304,12 @@ namespace Barotrauma.Items.Components } } } - } + } + + if (item.GetComponent() != null) + { + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":GardeningPlanted:" + containedItem.prefab.Identifier); + } //no need to Update() if this item has no statuseffects and no physics body IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 8e4a78fab..77323447c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -370,29 +370,18 @@ namespace Barotrauma.Items.Components public Item GetFocusTarget() { - Item focusTarget = null; - for (int c = 0; c < 2; c++) - { - //try finding the item to focus on using trigger_out, and if that fails, using position_out - string connectionName = c == 0 ? "trigger_out" : "position_out"; - string signal = c == 0 ? "0" : MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture); - if (!item.SendSignal(new Signal(signal, sender: user), connectionName) || focusTarget != null) - { - continue; - } + item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), "position_out"); - for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--) + for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--) + { + if (item.LastSentSignalRecipients[i].Item.Condition <= 0.0f || item.LastSentSignalRecipients[i].IsPower) { continue; } + if (item.LastSentSignalRecipients[i].Item.Prefab.FocusOnSelected) { - if (item.LastSentSignalRecipients[i].Item.Condition <= 0.0f || item.LastSentSignalRecipients[i].IsPower) { continue; } - if (item.LastSentSignalRecipients[i].Item.Prefab.FocusOnSelected) - { - focusTarget = item.LastSentSignalRecipients[i].Item; - break; - } + return item.LastSentSignalRecipients[i].Item; } } - - return focusTarget; + + return null; } public override bool Pick(Character picker) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index b887cd743..310aa6405 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -300,6 +300,8 @@ namespace Barotrauma.Items.Components } } + GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Name ?? "none") + ":" + targetItem.prefab.Identifier); + if (targetItem.AllowDeconstruct && allowRemove) { //drop all items that are inside the deconstructed item diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 6c3bf0ae7..291778bdd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -397,6 +397,7 @@ namespace Barotrauma.Items.Components for (int i = 0; i < (int)fabricationitemAmount.Value; i++) { float outCondition = fabricatedItem.OutCondition; + GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Name ?? "none") + ":" + fabricatedItem.TargetItem.Identifier); if (i < amountFittingContainer) { Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, quality, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index cf63c3894..ec4fd008e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -314,26 +314,6 @@ namespace Barotrauma.Items.Components return TextManager.GetWithVariable("roomname.subdiroclock", "[dir]", clockDir.ToString()); } - private Vector2 GetTransducerPos() - { - if (!UseTransducers || connectedTransducers.Count == 0) - { - //use the position of the sub if the item is static (no body) and inside a sub - return item.Submarine != null && item.body == null ? item.Submarine.WorldPosition : item.WorldPosition; - } - - Vector2 transducerPosSum = Vector2.Zero; - foreach (ConnectedTransducer transducer in connectedTransducers) - { - if (transducer.Transducer.Item.Submarine != null && CenterOnTransducers) - { - return transducer.Transducer.Item.Submarine.WorldPosition; - } - transducerPosSum += transducer.Transducer.Item.WorldPosition; - } - return transducerPosSum / connectedTransducers.Count; - } - public override void ReceiveSignal(Signal signal, Connection connection) { base.ReceiveSignal(signal, connection); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 9d7002ac2..576ab086d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -113,6 +113,9 @@ namespace Barotrauma.Items.Components [Serialize(false, true, description: "If true, the recharge speed (and power consumption) of the device goes up exponentially as the recharge rate is increased.")] public bool ExponentialRechargeSpeed { get; set; } + [Editable(minValue: 0.0f, maxValue: 10.0f, decimals: 2), Serialize(0.5f, true)] + public float RechargeAdjustSpeed { get; set; } + private float efficiency; [Editable(minValue: 0.0f, maxValue: 1.0f, decimals: 2), Serialize(0.95f, true, description: "The amount of power you can get out of a item relative to the amount of power that's put into it.")] public float Efficiency @@ -199,7 +202,14 @@ namespace Barotrauma.Items.Components { targetRechargeSpeed *= missingCharge; } - currPowerConsumption = MathHelper.Lerp(currPowerConsumption, targetRechargeSpeed, 0.05f); + if (currPowerConsumption < targetRechargeSpeed) + { + currPowerConsumption = Math.Min(currPowerConsumption + deltaTime * maxRechargeSpeed * RechargeAdjustSpeed, targetRechargeSpeed); + } + else + { + currPowerConsumption = Math.Max(currPowerConsumption - deltaTime * maxRechargeSpeed * RechargeAdjustSpeed, targetRechargeSpeed); + } Charge += currPowerConsumption * Math.Min(Voltage, 1.0f) / 3600.0f * efficiency; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index b69425e0e..33f934b24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -244,7 +244,6 @@ namespace Barotrauma.Items.Components return picker != null; } - private static readonly HashSet tempConnected = new HashSet(); protected void RefreshConnections() { var connections = item.Connections; @@ -260,41 +259,51 @@ namespace Barotrauma.Items.Components } //find all connections that are connected to this one (directly or via another PowerTransfer) - tempConnected.Clear(); + HashSet tempConnected; + if (!connectedRecipients.ContainsKey(c)) + { + tempConnected = new HashSet(); + connectedRecipients.Add(c, tempConnected); + } + else + { + tempConnected = connectedRecipients[c]; + tempConnected.Clear(); + //mark all previous recipients as dirty + foreach (Connection recipient in tempConnected) + { + var pt = recipient.Item.GetComponent(); + if (pt != null) { pt.connectionDirty[recipient] = true; } + } + } + + tempConnected.Add(c); if (item.Condition > 0.0f) { - if (!connectedRecipients.ContainsKey(c)) - { - connectedRecipients.Add(c, tempConnected); - } - else - { - //mark all previous recipients as dirty - foreach (Connection recipient in connectedRecipients[c]) - { - var pt = recipient.Item.GetComponent(); - if (pt != null) pt.connectionDirty[recipient] = true; - } - } - - tempConnected.Add(c); GetConnected(c, tempConnected); - } - connectedRecipients[c] = tempConnected; - - //go through all the PowerTransfers that we're connected to and set their connections to match the ones we just calculated - //(no need to go through the recursive GetConnected method again) - foreach (Connection recipient in tempConnected) - { - if (recipient == c) { continue; } - var recipientPowerTransfer = recipient.Item.GetComponent(); - if (recipientPowerTransfer == null) { continue; } - if (!connectedRecipients.ContainsKey(recipient)) + //go through all the PowerTransfers that we're connected to and set their connections to match the ones we just calculated + //(no need to go through the recursive GetConnected method again) + foreach (Connection recipient in tempConnected) { - connectedRecipients.Add(recipient, tempConnected); + if (recipient == c) { continue; } + var recipientPowerTransfer = recipient.Item.GetComponent(); + if (recipientPowerTransfer == null) { continue; } + if (!recipientPowerTransfer.connectedRecipients.ContainsKey(recipient)) + { + recipientPowerTransfer.connectedRecipients.Add(recipient, new HashSet()); + } + else + { + recipientPowerTransfer.connectedRecipients[recipient].Clear(); + } + foreach (var connection in tempConnected) + { + recipientPowerTransfer.connectedRecipients[recipient].Add(connection); + } + recipientPowerTransfer.connectionDirty[recipient] = false; } - recipientPowerTransfer.connectionDirty[recipient] = false; } + connectionDirty[c] = false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index aa31c365e..1283caeec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -543,6 +543,7 @@ namespace Barotrauma.Items.Components { if (fixture.Body.UserData is VoronoiCell) { return -1; } if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { return -1; } + if (fixture.Body.UserData is Limb limb && limb.character?.Submarine != submarine) { return -1; } } //ignore level cells if the item and the point of impact are inside a sub diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs index 8cc329fce..9ae8bede2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs @@ -87,7 +87,11 @@ namespace Barotrauma.Items.Components } string signalOut = sendOutput ? output : falseOutput; - if (string.IsNullOrEmpty(signalOut)) { return; } + if (string.IsNullOrEmpty(signalOut)) + { + IsActive = false; + return; + } item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out"); } @@ -100,11 +104,13 @@ namespace Barotrauma.Items.Components if (signal.value == "0") { return; } timeSinceReceived[0] = 0.0f; signalSender[0] = signal.sender; + IsActive = true; break; case "signal_in2": if (signal.value == "0") { return; } timeSinceReceived[1] = 0.0f; signalSender[1] = signal.sender; + IsActive = true; break; case "set_output": output = signal.value; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs index a6b340070..38489ca15 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs @@ -23,7 +23,6 @@ namespace Barotrauma.Items.Components public ButtonTerminal(Item item, XElement element) : base(item, element) { - IsActive = true; RequiredSignalCount = element.GetChildElements("TerminalButton").Count(c => c.GetAttribute("style") != null); if (RequiredSignalCount < 1) { @@ -88,13 +87,13 @@ namespace Barotrauma.Items.Components } } - var containers = item.GetComponents().ToList(); - if (containers.Count != 1) + var containers = item.GetComponents(); + if (containers.Count() != 1) { DebugConsole.ThrowError($"Error in item \"{item.Name}\": the ButtonTerminal component requires exactly one ItemContainer component!"); return; } - Container = containers[0]; + Container = containers.FirstOrDefault(); OnItemLoadedProjSpecific(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs index b3bdf5f2c..c0c7c2872 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs @@ -66,11 +66,16 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { + if (signalQueue.Count == 0) + { + IsActive = false; + return; + } + foreach (var val in signalQueue) { val.SendTimer -= 1; } - while (signalQueue.Count > 0 && signalQueue.Peek().SendTimer <= 0) { var signalOut = signalQueue.Peek(); @@ -114,6 +119,7 @@ namespace Barotrauma.Items.Components SendDuration = 1 }; signalQueue.Enqueue(prevQueuedSignal); + IsActive = true; break; case "set_delay": if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float newDelay)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index e74aaff0d..e83fe10f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -26,6 +26,8 @@ namespace Barotrauma.Items.Components public PhysicsBody ParentBody; + private bool isOn; + private Turret turret; [Serialize(100.0f, true, description: "The range of the emitted light. Higher values are more performance-intensive.", alwaysUseInstanceValues: true), @@ -85,12 +87,13 @@ namespace Barotrauma.Items.Components [Editable, Serialize(false, true, description: "Is the light currently on.", alwaysUseInstanceValues: true)] public bool IsOn { - get { return IsActive; } + get { return isOn; } set { - if (IsActive == value) { return; } + if (isOn == value && IsActive == value) { return; } - IsActive = value; + IsActive = isOn = value; + SetLightSourceState(value, value ? lightBrightness : 0.0f); OnStateChanged(); } } @@ -200,9 +203,8 @@ namespace Barotrauma.Items.Components set { if (base.IsActive == value) { return; } - base.IsActive = value; - - SetLightSourceState(value, value ? lightBrightness : 0.0f); + base.IsActive = isOn = value; + SetLightSourceState(value, value ? lightBrightness : 0.0f); } } @@ -237,6 +239,23 @@ namespace Barotrauma.Items.Components turret = item.GetComponent(); } + public override void OnMapLoaded() + { + if (item.body == null && powerConsumption <= 0.0f && Parent == null && turret == null && + (statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) && + (IsActiveConditionals == null || IsActiveConditionals.Count == 0)) + { + lightBrightness = 1.0f; + SetLightSourceState(true, lightBrightness); + SetLightSourceTransformProjSpecific(); + base.IsActive = false; + isOn = true; +#if CLIENT + Light.ParentSub = item.Submarine; +#endif + } + } + public override void Update(float deltaTime, Camera cam) { if (item.AiTarget != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs index b5c242653..abbd0b603 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs @@ -20,7 +20,11 @@ namespace Barotrauma.Items.Components } string signalOut = sendOutput ? output : falseOutput; - if (string.IsNullOrEmpty(signalOut)) { return; } + if (string.IsNullOrEmpty(signalOut)) + { + IsActive = false; + return; + } item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index 8f3b6cebb..ec74d3894 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -8,12 +8,15 @@ using System.Xml.Linq; namespace Barotrauma.Items.Components { - partial class WifiComponent : ItemComponent + partial class WifiComponent : ItemComponent, IServerSerializable { private static readonly List list = new List(); const int ChannelMemorySize = 10; + private const int MinChannel = 0; + private const int MaxChannel = 10000; + private float range; private int channel; @@ -49,7 +52,7 @@ namespace Barotrauma.Items.Components get { return channel; } set { - channel = MathHelper.Clamp(value, 0, 10000); + channel = MathHelper.Clamp(value, MinChannel, MaxChannel); } } @@ -295,7 +298,14 @@ namespace Barotrauma.Items.Components case "set_channel": if (int.TryParse(signal.value, out int newChannel)) { + int prevChannel = Channel; Channel = newChannel; + if (prevChannel != Channel) + { +#if SERVER + item.CreateServerEvent(this); +#endif + } } break; case "set_range": diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs index d608e046a..61d782ffb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs @@ -20,7 +20,11 @@ namespace Barotrauma.Items.Components } string signalOut = sendOutput == 1 ? output : falseOutput; - if (string.IsNullOrEmpty(signalOut)) { return; } + if (string.IsNullOrEmpty(signalOut)) + { + IsActive = false; + return; + } item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index f63c0dc9d..6a818a572 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -1442,6 +1442,11 @@ namespace Barotrauma.Items.Components crosshairPointerSprite?.Remove(); crosshairPointerSprite = null; moveSoundChannel?.Dispose(); moveSoundChannel = null; WeaponIndicatorSprite?.Remove(); WeaponIndicatorSprite = null; + if (powerIndicator != null) + { + powerIndicator.RectTransform.Parent = null; + powerIndicator = null; + } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index fbfb7a2ce..69f16d500 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -479,7 +479,17 @@ namespace Barotrauma { if (i < 0 || i >= slots.Length) { - string errorMsg = $"Inventory.TryPutItem failed: index was out of range (item: {(item?.Name ?? "null")}, inventory: {(Owner?.ToString() ?? "null")}).\n" + Environment.StackTrace.CleanupStackTrace(); + string thisItemStr = item?.prefab.Identifier ?? "null"; + string ownerStr = "null"; + if (Owner is Item ownerItem) + { + ownerStr = ownerItem.prefab.Identifier; + } + else if (Owner is Character ownerCharacter) + { + ownerStr = ownerCharacter.SpeciesName; + } + string errorMsg = $"Inventory.TryPutItem failed: index was out of range (item: {thisItemStr}, inventory: {ownerStr})."; GameAnalyticsManager.AddErrorEventOnce("Inventory.TryPutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 872ae1adf..604aed27d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -1741,7 +1741,7 @@ namespace Barotrauma } else { - if (updateableComponents.Count == 0 && aiTarget == null && !hasStatusEffectsOfType[(int)ActionType.Always] && (body == null || !body.Enabled)) + if (updateableComponents.Count == 0 && !hasStatusEffectsOfType[(int)ActionType.Always] && (body == null || !body.Enabled)) { #if CLIENT positionBuffer.Clear(); @@ -2088,19 +2088,18 @@ namespace Barotrauma return controller != null; } - public bool SendSignal(string signal, string connectionName) + public void SendSignal(string signal, string connectionName) { - return SendSignal(new Signal(signal), connectionName); + SendSignal(new Signal(signal), connectionName); } - public bool SendSignal(Signal signal, string connectionName) + public void SendSignal(Signal signal, string connectionName) { - if (connections == null) { return false; } - if (!connections.TryGetValue(connectionName, out Connection connection)) { return false; } + if (connections == null) { return; } + if (!connections.TryGetValue(connectionName, out Connection connection)) { return; } signal.source ??= this; SendSignal(signal, connection); - return true; } private readonly HashSet<(Signal Signal, Connection Connection)> delayedSignals = new HashSet<(Signal Signal, Connection Connection)>(); @@ -3248,6 +3247,9 @@ namespace Barotrauma foreach (ItemComponent ic in components) { ic.Remove(); +#if CLIENT + ic.GuiFrame = null; +#endif } ItemList.Remove(this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index b4a905486..de0f565e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -18,8 +18,8 @@ namespace Barotrauma.MapCreatures.Behavior public readonly BallastFloraBehavior? ParentBallastFlora; public int ID = -1; - public ushort ClaimedItem; - public bool HasClaimedItem; + public Item ClaimedItem; + public int ClaimedItemId = -1; public float MaxHealth = 100f; public float Health = 100f; @@ -271,10 +271,29 @@ namespace Barotrauma.MapCreatures.Behavior { ClaimTarget(item, Branches.FirstOrDefault(b => b.ID == branchid), true); } + else + { + string errorMsg = $"Error in BallastFloraBehavior.OnMapLoaded: could not find the item claimed by the ballast flora."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("BallastFloraBehavior.OnMapLoaded:ClaimedItemNotFound", GameAnalyticsManager.ErrorSeverity.Warning, errorMsg); + } } foreach (BallastFloraBranch branch in Branches) { + if (branch.ClaimedItemId > -1) + { + if (Entity.FindEntityByID((ushort)branch.ClaimedItemId) is Item item) + { + branch.ClaimedItem = item; + } + else + { + string errorMsg = $"Error in BallastFloraBehavior.OnMapLoaded: could not find the item claimed by a branch."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("BallastFloraBehavior.OnMapLoaded:BranchClaimedItemNotFound", GameAnalyticsManager.ErrorSeverity.Warning, errorMsg); + } + } UpdateConnections(branch); CreateBody(branch); } @@ -335,9 +354,9 @@ namespace Barotrauma.MapCreatures.Behavior new XAttribute("sides", (int)branch.Sides), new XAttribute("blockedsides", (int)branch.BlockedSides)); - if (branch.HasClaimedItem) + if (branch.ClaimedItem != null) { - be.Add(new XAttribute("claimed", (int)branch.ClaimedItem)); + be.Add(new XAttribute("claimed", (int)(branch.ClaimedItem?.ID ?? -1))); } saveElement.Add(be); @@ -345,6 +364,13 @@ namespace Barotrauma.MapCreatures.Behavior foreach (Item target in ClaimedTargets) { + if (target.Infector == null) + { + string errorMsg = $"Error in BallastFloraBehavior.Save: claimed target \"{target.Prefab.Identifier}\" had no infector set."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("BallastFloraBehavior.Save:InfectorNull", GameAnalyticsManager.ErrorSeverity.Warning, errorMsg); + continue; + } XElement te = new XElement("ClaimedTarget", new XAttribute("id", target.ID), new XAttribute("branchId", target.Infector.ID)); saveElement.Add(te); } @@ -352,7 +378,7 @@ namespace Barotrauma.MapCreatures.Behavior element.Add(saveElement); } - public void LoadSave(XElement element) + public void LoadSave(XElement element, IdRemap idRemap) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); Offset = element.GetAttributeVector2("offset", Vector2.Zero); @@ -361,21 +387,20 @@ namespace Barotrauma.MapCreatures.Behavior switch (subElement.Name.ToString().ToLowerInvariant()) { case "branch": - LoadBranch(subElement); + LoadBranch(subElement, idRemap); break; - case "claimedtarget": int id = subElement.GetAttributeInt("id", -1); int branchId = subElement.GetAttributeInt("branchId", -1); if (id > 0) { - tempClaimedTargets.Add(Tuple.Create((UInt16)id, branchId)); + tempClaimedTargets.Add(Tuple.Create(idRemap.GetOffsetId(id), branchId)); } break; } } - void LoadBranch(XElement branchElement) + void LoadBranch(XElement branchElement, IdRemap idRemap) { Vector2 pos = branchElement.GetAttributeVector2("pos", Vector2.Zero); bool isRoot = branchElement.GetAttributeBool("isroot", false); @@ -400,8 +425,7 @@ namespace Barotrauma.MapCreatures.Behavior if (claimedId > -1) { - newBranch.HasClaimedItem = true; - newBranch.ClaimedItem = (ushort) claimedId; + newBranch.ClaimedItemId = idRemap.GetOffsetId((ushort)claimedId); } Branches.Add(newBranch); @@ -767,8 +791,7 @@ namespace Barotrauma.MapCreatures.Behavior if (branch != null) { - branch.ClaimedItem = target.ID; - branch.HasClaimedItem = true; + branch.ClaimedItem = target; } #if SERVER @@ -977,7 +1000,7 @@ namespace Barotrauma.MapCreatures.Behavior if (isClient) { return; } - if (branch.HasClaimedItem) + if (branch.ClaimedItem != null) { RemoveClaim(branch.ClaimedItem); } @@ -995,41 +1018,34 @@ namespace Barotrauma.MapCreatures.Behavior #endif } - public void RemoveClaim(ushort id) + public void RemoveClaim(Item item) { - ClaimedTargets.ForEachMod(item => + if (!IgnoredTargets.ContainsKey(item)) { - if (item.ID == id) + IgnoredTargets.Add(item, 10); + } + + ClaimedTargets.Remove(item); + item.Infector = null; + + ClaimedJunctionBoxes.ForEachMod(jb => + { + if (jb.Item == item) { - if (!IgnoredTargets.ContainsKey(item)) - { - IgnoredTargets.Add(item, 10); - } - - ClaimedTargets.Remove(item); - item.Infector = null; - - ClaimedJunctionBoxes.ForEachMod(jb => - { - if (jb.Item == item) - { - ClaimedJunctionBoxes.Remove(jb); - } - }); - - ClaimedBatteries.ForEachMod(bat => - { - if (bat.Item == item) - { - ClaimedBatteries.Remove(bat); - } - }); - -#if SERVER - SendNetworkMessage(this, NetworkHeader.Infect, item.ID, false); -#endif + ClaimedJunctionBoxes.Remove(jb); } }); + + ClaimedBatteries.ForEachMod(bat => + { + if (bat.Item == item) + { + ClaimedBatteries.Remove(bat); + } + }); +#if SERVER + SendNetworkMessage(this, NetworkHeader.Infect, item.ID, false); +#endif } public void Kill() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 033dae146..7007094d8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -1540,7 +1540,7 @@ namespace Barotrauma if (prefab != null) { hull.BallastFlora = new BallastFloraBehavior(hull, prefab, Vector2.Zero); - hull.BallastFlora.LoadSave(subElement); + hull.BallastFlora.LoadSave(subElement, idRemap); } break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs index a9fb2d699..f513fb078 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs @@ -205,6 +205,8 @@ namespace Barotrauma foreach (var cell in Cells) { cell.CellType = CellType.Removed; + cell.OnDestroyed?.Invoke(); + cell.OnDestroyed = null; } GameMain.World.Remove(Body); Dispose(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 2a888ef49..6e6f1118a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -25,7 +25,17 @@ namespace Barotrauma /// public const int MaxSubmarineWidth = 16000; - public static Level Loaded { get; private set; } + private static Level loaded; + public static Level Loaded + { + get { return loaded; } + private set + { + if (loaded == value) { return; } + loaded = value; + GameAnalyticsManager.SetCurrentLevel(loaded?.LevelData); + } + } [Flags] public enum PositionType @@ -578,8 +588,8 @@ namespace Barotrauma { for (int y = siteInterval.Y / 2; y < borders.Height - siteInterval.Y / 2; y += siteInterval.Y) { - int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X, Rand.RandSync.Server); - int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y, Rand.RandSync.Server); + int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X + 1, Rand.RandSync.Server); + int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y + 1, Rand.RandSync.Server); bool closeToTunnel = false; bool closeToCave = false; @@ -1776,12 +1786,12 @@ namespace Barotrauma new Point(0, BottomPos) }; - int mountainCount = Rand.Range(GenerationParams.MountainCountMin, GenerationParams.MountainCountMax, Rand.RandSync.Server); + int mountainCount = Rand.Range(GenerationParams.MountainCountMin, GenerationParams.MountainCountMax + 1, Rand.RandSync.Server); for (int i = 0; i < mountainCount; i++) { bottomPositions.Add( new Point(Size.X / (mountainCount + 1) * (i + 1), - BottomPos + Rand.Range(GenerationParams.MountainHeightMin, GenerationParams.MountainHeightMax, Rand.RandSync.Server))); + BottomPos + Rand.Range(GenerationParams.MountainHeightMin, GenerationParams.MountainHeightMax + 1, Rand.RandSync.Server))); } bottomPositions.Add(new Point(Size.X, BottomPos)); @@ -1794,7 +1804,7 @@ namespace Barotrauma bottomPositions.Insert(i + 1, new Point( (bottomPositions[i].X + bottomPositions[i + 1].X) / 2, - (bottomPositions[i].Y + bottomPositions[i + 1].Y) / 2 + Rand.Range(0, GenerationParams.SeaFloorVariance, Rand.RandSync.Server))); + (bottomPositions[i].Y + bottomPositions[i + 1].Y) / 2 + Rand.Range(0, GenerationParams.SeaFloorVariance + 1, Rand.RandSync.Server))); i++; } @@ -1881,7 +1891,7 @@ namespace Barotrauma Tunnels.Add(tunnel); caveBranches.Add(tunnel); - int branches = Rand.Range(caveParams.MinBranchCount, caveParams.MaxBranchCount, Rand.RandSync.Server); + int branches = Rand.Range(caveParams.MinBranchCount, caveParams.MaxBranchCount + 1, Rand.RandSync.Server); for (int j = 0; j < branches; j++) { Tunnel parentBranch = caveBranches.GetRandom(Rand.RandSync.Server); @@ -2441,7 +2451,7 @@ namespace Barotrauma }, randSync: Rand.RandSync.Server); if (location.Cell == null || location.Edge == null) { break; } - int clusterSize = Rand.Range(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y, Rand.RandSync.Server); + int clusterSize = Rand.Range(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y + 1, Rand.RandSync.Server); PlaceResources(itemPrefab, clusterSize, location, out var abyssResources); var abyssClusterLocation = new ClusterLocation(location.Cell, location.Edge, initializeResourceList: true); abyssClusterLocation.Resources.AddRange(abyssResources); @@ -3310,13 +3320,6 @@ namespace Barotrauma return pathCells; } - public string GetWreckIDTag(string originalTag, Submarine wreck) - { - string shortSeed = ToolBox.StringToInt(LevelData.Seed + wreck?.Info.Name).ToString(); - if (shortSeed.Length > 6) { shortSeed = shortSeed.Substring(0, 6); } - return originalTag + "_" + shortSeed; - } - public bool IsCloseToStart(Vector2 position, float minDist) => IsCloseToStart(position.ToPoint(), minDist); public bool IsCloseToEnd(Vector2 position, float minDist) => IsCloseToEnd(position.ToPoint(), minDist); @@ -4058,7 +4061,7 @@ namespace Barotrauma foreach (Submarine wreck in Wrecks) { - int corpseCount = Rand.Range(Loaded.GenerationParams.MinCorpseCount, Loaded.GenerationParams.MaxCorpseCount); + int corpseCount = Rand.Range(Loaded.GenerationParams.MinCorpseCount, Loaded.GenerationParams.MaxCorpseCount + 1); var allSpawnPoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == wreck && wp.CurrentHull != null); var pathPoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Path); pathPoints.Shuffle(Rand.RandSync.Unsynced); @@ -4115,6 +4118,7 @@ namespace Barotrauma corpse.EnableDespawn = false; selectedPrefab.GiveItems(corpse, wreck); corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false); + corpse.GiveIdCardTags(sp); spawnCounter++; static CorpsePrefab GetCorpsePrefab(Func predicate) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 1d6643992..dfd89203d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -304,7 +304,7 @@ namespace Barotrauma foreach (LevelObjectPrefab.ChildObject child in prefab.ChildObjects) { - int childCount = Rand.Range(child.MinCount, child.MaxCount, Rand.RandSync.Server); + int childCount = Rand.Range(child.MinCount, child.MaxCount + 1, Rand.RandSync.Server); for (int j = 0; j < childCount; j++) { var matchingPrefabs = LevelObjectPrefab.List.Where(p => child.AllowedNames.Contains(p.Name)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index c6b2c0fb7..23c1bc77f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -436,7 +436,7 @@ namespace Barotrauma /// /// Are there any active contacts between the physics body and the target entity /// - public static bool CheckContactsForEntity(PhysicsBody triggerBody, Entity separatingEntity) + public static bool CheckContactsForEntity(PhysicsBody triggerBody, Entity targetEntity) { foreach (Fixture fixture in triggerBody.FarseerBody.FixtureList) { @@ -447,10 +447,11 @@ namespace Barotrauma contactEdge.Contact.Enabled && contactEdge.Contact.IsTouching) { - if (contactEdge.Contact.FixtureA != fixture && contactEdge.Contact.FixtureB != fixture) - { - if (GetEntity(contactEdge.Contact.FixtureB) == separatingEntity || GetEntity(contactEdge.Contact.FixtureA) == separatingEntity) { return true; } - } + if ((contactEdge.Contact.FixtureA.Body == triggerBody.FarseerBody && GetEntity(contactEdge.Contact.FixtureB) == targetEntity) || + (contactEdge.Contact.FixtureB.Body == triggerBody.FarseerBody && GetEntity(contactEdge.Contact.FixtureA) == targetEntity)) + { + return true; + } } contactEdge = contactEdge.Next; } @@ -560,6 +561,8 @@ namespace Barotrauma foreach (Entity triggerer in triggerers) { + if (triggerer.Removed) { continue; } + ApplyStatusEffects(statusEffects, worldPosition, triggerer, deltaTime, targets); if (triggerer is IDamageable damageable) @@ -691,6 +694,8 @@ namespace Barotrauma private void ApplyForce(PhysicsBody body) { + if (body == null) { return; } + float distFactor = 1.0f; if (ForceFalloff) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 771e43965..082ad2521 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -352,6 +352,7 @@ namespace Barotrauma if (hull.Submarine != sub) { continue; } hull.WaterVolume = 0.0f; hull.OxygenPercentage = 100.0f; + hull.BallastFlora?.Kill(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index b080d772b..d3275a394 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -782,7 +782,7 @@ namespace Barotrauma { if (priceInfo.MaxAvailableAmount > priceInfo.MinAvailableAmount) { - quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount); + quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount + 1); } else { @@ -1010,7 +1010,7 @@ namespace Barotrauma private void GenerateRandomPriceModifier() { - StorePriceModifier = Rand.Range(-StorePriceModifierRange, StorePriceModifierRange); + StorePriceModifier = Rand.Range(-StorePriceModifierRange, StorePriceModifierRange + 1); } private void CreateStoreSpecials() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index d4a2ba6e5..dabc6e22c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -112,7 +112,7 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(!Locations.Contains(null)); for (int i = 0; i < Locations.Count; i++) { - Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, $"location.{i}", -100, 100, Rand.Range(-10, 10, Rand.RandSync.Server)); + Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}", -100, 100, Rand.Range(-10, 11, Rand.RandSync.Server)); } List connectionElements = new List(); @@ -214,7 +214,7 @@ namespace Barotrauma for (int i = 0; i < Locations.Count; i++) { - Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, $"location.{i}", -100, 100, Rand.Range(-10, 10, Rand.RandSync.Server)); + Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}", -100, 100, Rand.Range(-10, 11, Rand.RandSync.Server)); } foreach (Location location in Locations) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 322b98b77..c68763505 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -115,7 +115,7 @@ namespace Barotrauma private float? maxHealth; - [Serialize(100.0f, true), Editable] + [Serialize(100.0f, true), Editable(MinValueFloat = 0)] public float MaxHealth { get => maxHealth ?? Prefab.Health; @@ -898,8 +898,8 @@ namespace Barotrauma { var worldRect = section.WorldRect; Vector2 particlePos = new Vector2( - Rand.Range(worldRect.X, worldRect.Right), - Rand.Range(worldRect.Y - worldRect.Height, worldRect.Y)); + Rand.Range(worldRect.X, worldRect.Right + 1), + Rand.Range(worldRect.Y - worldRect.Height, worldRect.Y + 1)); var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f); if (particle == null) break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index 4f74e2fb5..622795eb4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -79,9 +79,19 @@ namespace Barotrauma.Networking public readonly string SenderName; + private Color? customTextColor; public Color Color { - get { return MessageColor[(int)Type]; } + get + { + if (customTextColor != null) { return customTextColor.Value; } + return MessageColor[(int)Type]; + } + + set + { + customTextColor = value; + } } public static string GetTimeStamp() @@ -105,7 +115,7 @@ namespace Barotrauma.Networking set; } - protected ChatMessage(string senderName, string text, ChatMessageType type, Character sender, Client client, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None) + protected ChatMessage(string senderName, string text, ChatMessageType type, Character sender, Client client, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, Color? textColor = null) { Text = text; Type = type; @@ -115,11 +125,13 @@ namespace Barotrauma.Networking SenderName = senderName; ChangeType = changeType; - } - public static ChatMessage Create(string senderName, string text, ChatMessageType type, Character sender, Client client = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None) + customTextColor = textColor; + } + + public static ChatMessage Create(string senderName, string text, ChatMessageType type, Character sender, Client client = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, Color? textColor = null) { - return new ChatMessage(senderName, text, type, sender, client ?? GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character != null && c.Character == sender), changeType); + return new ChatMessage(senderName, text, type, sender, client ?? GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character != null && c.Character == sender), changeType, textColor); } public static string GetChatMessageCommand(string message, out string messageWithoutCommand) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index 032971460..dd3b80645 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -234,24 +234,24 @@ namespace Barotrauma.Networking var radio = sender.Inventory.AllItems.FirstOrDefault(i => i.GetComponent() != null); if (radio == null || !sender.HasEquippedItem(radio)) { return false; } - + var radioComponent = radio.GetComponent(); if (radioComponent == null) { return false; } return radioComponent.HasRequiredContainedItems(sender, addMessage: false); } - public void AddChatMessage(string message, ChatMessageType type, string senderName = "", Client senderClient = null, Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None) + public void AddChatMessage(string message, ChatMessageType type, string senderName = "", Client senderClient = null, Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, Color? textColor = null) { - AddChatMessage(ChatMessage.Create(senderName, message, type, senderCharacter, senderClient, changeType: changeType)); + AddChatMessage(ChatMessage.Create(senderName, message, type, senderCharacter, senderClient, changeType: changeType, textColor: textColor)); } public virtual void AddChatMessage(ChatMessage message) { if (string.IsNullOrEmpty(message.Text)) { return; } - + if (message.Sender != null && !message.Sender.IsDead) { - message.Sender.ShowSpeechBubble(2.0f, ChatMessage.MessageColor[(int)message.Type]); + message.Sender.ShowSpeechBubble(2.0f, message.Color); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs index f4a80591c..347af34f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs @@ -124,7 +124,6 @@ namespace Barotrauma.Steam return unlocked; } - public static bool IncrementStat(string statName, int increment) { if (!isInitialized || !Steamworks.SteamClient.IsValid) { return false; } @@ -161,6 +160,12 @@ namespace Barotrauma.Steam return success; } + public static int GetStatInt(string statName) + { + if (!isInitialized || !Steamworks.SteamClient.IsValid) { return 0; } + return Steamworks.SteamUserStats.GetStatInt(statName); + } + public static bool StoreStats() { if (!isInitialized || !Steamworks.SteamClient.IsValid) { return false; } @@ -175,6 +180,17 @@ namespace Barotrauma.Steam return success; } + public static bool TryGetUnlockedAchievements(out List achievements) + { + if (!isInitialized || !Steamworks.SteamClient.IsValid) + { + achievements = null; + return false; + } + achievements = Steamworks.SteamUserStats.Achievements.Where(a => a.State).ToList(); + return true; + } + public static void Update(float deltaTime) { if (!isInitialized) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/FrameCounter.cs b/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs similarity index 68% rename from Barotrauma/BarotraumaShared/SharedSource/FrameCounter.cs rename to Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs index 0b4d5c016..6915d1dfc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/FrameCounter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -7,20 +6,18 @@ namespace Barotrauma { public class PerformanceCounter { - public long TotalFrames { get; private set; } - public double TotalSeconds { get; private set; } public double AverageFramesPerSecond { get; private set; } public double CurrentFramesPerSecond { get; private set; } public const int MaximumSamples = 10; - private Queue sampleBuffer = new Queue(); + private readonly Queue sampleBuffer = new Queue(); - private Dictionary> elapsedTicks = new Dictionary>(); - private Dictionary avgTicksPerFrame = new Dictionary(); + private readonly Dictionary> elapsedTicks = new Dictionary>(); + private readonly Dictionary avgTicksPerFrame = new Dictionary(); #if CLIENT - internal Graph UpdateTimeGraph = new Graph(500), UpdateIterationsGraph = new Graph(500), DrawTimeGraph = new Graph(500); + internal Graph UpdateTimeGraph = new Graph(500), DrawTimeGraph = new Graph(500); #endif public IEnumerable GetSavedIdentifiers @@ -50,7 +47,7 @@ namespace Barotrauma { if (deltaTime == 0.0f) { return false; } - CurrentFramesPerSecond = (1.0 / deltaTime); + CurrentFramesPerSecond = 1.0 / deltaTime; sampleBuffer.Enqueue(CurrentFramesPerSecond); @@ -64,12 +61,7 @@ namespace Barotrauma AverageFramesPerSecond = CurrentFramesPerSecond; } - if (AverageFramesPerSecond < 0 || AverageFramesPerSecond > 500) { } - - TotalFrames++; - TotalSeconds += deltaTime; return true; } - } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs index 8c461594f..62b8c8056 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs @@ -53,6 +53,7 @@ using Barotrauma; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; @@ -157,6 +158,11 @@ namespace Voronoi2 public bool IsDestructible; public bool DoesDamage; + /// + /// Executed when the cell is destroyed (only applies to destructible level walls) + /// + public Action OnDestroyed; + public Vector2 Center { get { return new Vector2((float)Site.Coord.X, (float)Site.Coord.Y) + Translation; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 25cedf801..46ba2fae3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -556,7 +556,14 @@ namespace Barotrauma switch (Name) { case nameof(Powered.Voltage): - if (parentObject is Powered powered) { value = powered.Voltage; return true; } + { + if (parentObject is Powered powered) { value = powered.Voltage; return true; } + } + break; + case nameof(Powered.CurrPowerConsumption): + { + if (parentObject is Powered powered) { value = powered.CurrPowerConsumption; return true; } + } break; case nameof(PowerContainer.Charge): { @@ -568,6 +575,11 @@ namespace Barotrauma if (parentObject is PowerContainer powerContainer) { value = powerContainer.ChargePercentage; return true; } } break; + case nameof(PowerContainer.RechargeRatio): + { + if (parentObject is PowerContainer powerContainer) { value = powerContainer.RechargeRatio; return true; } + } + break; case nameof(Reactor.AvailableFuel): { if (parentObject is Reactor reactor) { value = reactor.AvailableFuel; return true; } } break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index c5ee6f977..91e9b82a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -262,6 +262,28 @@ namespace Barotrauma return val; } + public static double GetAttributeDouble(this XElement element, string name, double defaultValue) + { + if (element?.Attribute(name) == null) { return defaultValue; } + + double val = defaultValue; + try + { + string strVal = element.Attribute(name).Value; + if (strVal.LastOrDefault() == 'f') + { + strVal = strVal.Substring(0, strVal.Length - 1); + } + val = double.Parse(strVal, CultureInfo.InvariantCulture); + } + catch (Exception e) + { + DebugConsole.ThrowError("Error in " + element + "!", e); + } + + return val; + } + public static float[] GetAttributeFloatArray(this XElement element, string name, float[] defaultValue) { if (element?.Attribute(name) == null) { return defaultValue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs index db53a6db6..4cbd51d6c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs @@ -34,7 +34,7 @@ namespace Barotrauma } } - private static List> list = new List>(); + private readonly static List> list = new List>(); /// /// Reference to the xml element from where the sprite was created. Can be null if the sprite was not defined in xml! diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index b73d3f280..b8ca1a030 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -329,7 +329,7 @@ namespace Barotrauma private bool MatchesTagCondition(ISerializableEntity target) { - if (!(target is Item item)) { return false; } + if (!(target is Item item)) { return Operator == OperatorType.NotEquals; } int matches = 0; foreach (string tag in SplitAttributeValue) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Timing.cs b/Barotrauma/BarotraumaShared/SharedSource/Timing.cs index 96916546f..6685423d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Timing.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Timing.cs @@ -12,6 +12,7 @@ namespace Barotrauma public static double Accumulator; public const int FixedUpdateRate = 60; public const double Step = 1.0 / FixedUpdateRate; + public const double AccumulatorMax = 0.25f; private static int frameLimit; /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs index e9d8f3727..7f0193f6e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs @@ -62,6 +62,9 @@ namespace Barotrauma return (sync == RandSync.Unsynced ? localRandom : (syncedRandom[(int)sync])).NextDouble() * (maximum - minimum) + minimum; } + /// + /// Min inclusive, Max exclusive! + /// public static int Range(int minimum, int maximum, RandSync sync = RandSync.Unsynced) { CheckRandThreadSafety(sync); diff --git a/Barotrauma/BarotraumaShared/Submarines/Barsuk.sub b/Barotrauma/BarotraumaShared/Submarines/Barsuk.sub index 420580824..efc7bddea 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Barsuk.sub and b/Barotrauma/BarotraumaShared/Submarines/Barsuk.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Herja.sub b/Barotrauma/BarotraumaShared/Submarines/Herja.sub new file mode 100644 index 000000000..4110c936d Binary files /dev/null and b/Barotrauma/BarotraumaShared/Submarines/Herja.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca2.sub b/Barotrauma/BarotraumaShared/Submarines/Orca2.sub index d9a6efe4e..47dbe7d35 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Orca2.sub and b/Barotrauma/BarotraumaShared/Submarines/Orca2.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/R-29.sub b/Barotrauma/BarotraumaShared/Submarines/R-29.sub index e46392048..aa9ad8e1e 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/R-29.sub and b/Barotrauma/BarotraumaShared/Submarines/R-29.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora.sub b/Barotrauma/BarotraumaShared/Submarines/Remora.sub index 119454742..33047bde9 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub index f1401653e..26ec00c8f 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Winterhalter.sub b/Barotrauma/BarotraumaShared/Submarines/Winterhalter.sub new file mode 100644 index 000000000..13a1215b9 Binary files /dev/null and b/Barotrauma/BarotraumaShared/Submarines/Winterhalter.sub differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 02fc875fe..57d31717c 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,71 @@ +--------------------------------------------------------------------------------------------------------- +v0.16.1.0 +--------------------------------------------------------------------------------------------------------- + +Changes and additions: +- Added 2 new subs: Herja and Winterhalter. +- Improvements and fixes to Barsuk or Orca 2. +- Improvements to clothing and headgear sprites. +- Improvements to the human sprites. +- Item update optimizations. +- The order of the crew list is saved between rounds in single player. +- Pulse laser ammo can be bought from outposts and cities. +- Indicate when a bot is following someone else than you on the crew list's order icons. +- Bots that follow a character who's going inside/outside stick closer to the character they're following. Helps the bots to get back inside Barsuk with you. +- Wired Herja and Barsuk diving suit lockers (unstable only). +- Chaingun tweaks: doubled turning speed when firing, reduced charging up time, reduced ammo consumption and made the ammo boxes more expensive. +- Simplified Remora drone docking system. +- Show notifications about reputation changes mid-round. +- Escorted NPCs drop the items they took from the sub (like suits) at the end of the round. +- Added some extra logging to diagnose the "did not receive STARTGAMEFINALIZE message from the server" errors. +- Added warnings when the game fails to run the physics at the desired 60 updates per second (which would cause rubberbanding in multiplayer). The warnings are shown below the FPS when the client is running slowly, and in the debug console of the host/moderators/admins when the server is. +- Additions to supercapacitors to make the new high recharge speed work better: warning indicators on the capacitor UI, high recharge speed makes the capacitors emit a red glow and smoke, show the current recharge speed (in kW, not just percentage) in the UI, made the recharge speed adjust more slowly (to make it less easy to wreck the electrical grid with rapid capacitor adjustments). +- Adjusted supercapacitors: reduced the max power consumption, increased efficiency (less load, the time it takes to recharge is still the same). Unstable only. +- Made turret icons on the minimap gray instead of red when not manned (easy to think there's something wrong with the turret when it's red). +- Abandoned outpost's oxygen generators now consume power. +- Adjusted affliction heal costs in the medical clinic (unstable only). +- Made characters crouch a little lower (enough to make it possible to shoot while standing behind a crouching character). +- Added a "sendchatmessage" console command with an option to configure the color of the message. +- Added button to align selected items and wire nodes to grid to the sub editor. + +Fixes: +- Misc localization fixes and improvements. +- Fixed tab menu's character tab not refreshing when switching to another character. +- Fixed ballast flora still being present when you replace an infested lost shuttle in an outpost. +- Fixed occasional crashes and entity ID errors when entering a new level with a ballast flora infested sub. +- Fixed inability to swap SMG magazines (or other items that go inside the held item) by double-clicking. +- Fixed "atmos machine" not spawning psychosis artifacts. +- Removed "periscopes determine which turret to focus on using the trigger_out connection instead of position_out" from the changelog (decided not to change this after all, because it causes problems and the position_out behavior can be worked around using relays). (unstable only) +- Fixed "center on sonar transducer" setting working backwards (unstable only). +- Fixed ability to remove tainted genetic materials' negative effects in the clinic (unstable only). +- Fixed genetic materials being tainted by default (unstable only). +- Fixed pets becoming hostile towards the crew and other pets if a human attacks the character they're protecting. +- Fixed bots reacting (fleeing/attacking) to any amount of damage their crewmates do to them in multiplayer (unstable only). +- Fixed security bots pointing their guns at you indefinitely when you inflict a small amount of damage on them (unstable only). +- Fixed "failed to parse the string to Vector2" when loading bot orders that have been saved on a system that uses comma as a decimal separator. +- Fixed sub editor's group list being empty when re-entering the editor. +- Fixed rounding error in RespawnManager that caused it to require 1 extra dead player to trigger a respawn (e.g. 9 players and a minimum of 30% players to respawn required 3 players, but the client-side texts showed 2). +- Fixed crashing when opening the head dropdown lists in the single player campaign's character customization menu (unstable only). +- Fixed wikiimage_sub outputting an empty image (unstable only). +- Fixed "stairs left" appearing mirrored in the status monitor's sub blueprint and in the sub editor's entity selection menu. +- Fixed pirates not operating turrets when they have no power. +- Fixed Wifi Component's "set_channel" input not working when sending signals to it via chat in multiplayer. +- Fixed autoshotgun not taking stacks into account in the ammo indicator below the inventory slot (= displaying it as being full when there's one shell in each slot, even though more could be stacked on the slots). +- Fixed hitscan turrets sometimes hitting targets inside your own sub when there's linked subs present. +- Fixed bots not unequipping diving suits when they have an order but not actively following it (i.e. they are on idle). +- Fixed broken Outpost Wall 3 sprite (unstable only). +- Fixed junction boxes' signal components not working (unstable only). +- Fixed artifact's effects not working when the artifact is held by a character (unstable only). +- Fixed faraday and nasonov artifacts' periodic explosions stopping if the round is ended during their 0.5s "reset" period. +- Fixed oxygenite shards not exploding in depth charge shells. +- Fixed endworm not having a burn damage modifier in the right tooth. +- Fixed killer sometimes being determined incorrectly when a character gets killed by something else than another character: e.g. if a character got crushed by pressure, the character who last did damage to them was considered to be the killer, which could for example lead to achievements being unlocked in inappropriate situations. + +Modding: +- If a mod makes a vanilla item movable/detachable and sets it as being attached by default, attach it to a wall when loading a sub that already had those items placed. I.e. making static devices movable doesn't cause them to deattach in existing subs. +- Fixed monster AI's targeting priorities doing nothing if the threshold is 0 and the target hasn't done any damage. +- Fixed custom ID card tags not working in wrecks. + --------------------------------------------------------------------------------------------------------- v0.16.0.0 --------------------------------------------------------------------------------------------------------- @@ -5,7 +73,7 @@ v0.16.0.0 Changes and additions: - Added a medical clinic to outposts that allows you to heal your crew for a price. - Added Barsuk, a small beginner-level sub. -- Added photoshop-like layers to submarine editor. +- Entities can be grouped together and the groups selectively hidden in the submarine editor. - Added some new decorative items and structures. - Adjusted medical items' effects on bleeding and burns. Bandages, plastiseal and antibiotic glue are now much more effective at treating them, and morphine & fentanyl only heal them by a negligible amount. - Heavily increased supercapacitor's power consumption and made the recharge speed increase exponentially when the recharge rate is increased. @@ -19,7 +87,6 @@ Changes and additions: - Made ballast flora toxins more visible and made them emit a sound. - Health scanner doesn't show buffs from talents. - Made impacts toss items around less effectively, especially when the item is heavy. -- Periscopes determine which turret to focus on using the trigger_out connection instead of position_out (making it easier to build circuits that switch which turret a periscope controls). - Gave spinelings and threshers inventories (+ added alien blood as loot) to make it possible for "gene harvester" and "deep sea slayer" to spawn loot. - Monsters' burns don't heal by themselves. - "Allow rewiring" server setting doesn't affect wrecks, pirate subs or ruins.