diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 46589743e..187b87f7e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -175,11 +175,11 @@ namespace Barotrauma position += amount; } - public void ClientWrite(IWriteMessage msg) + public void ClientWrite(in SegmentTableWriter segmentTableWriter, IWriteMessage msg) { if (Character.Controlled != null && !Character.Controlled.IsDead) { return; } - msg.WriteByte((byte)ClientNetObject.SPECTATING_POS); + segmentTableWriter.StartNewSegment(ClientNetSegment.SpectatingPos); msg.WriteSingle(position.X); msg.WriteSingle(position.Y); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 4c18684c7..5a0f10683 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -673,15 +673,6 @@ namespace Barotrauma partial void UpdateProjSpecific(float deltaTime, Camera cam) { - if (InvisibleTimer > 0.0f) - { - if (Controlled == null || Controlled == this || (Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f) - { - InvisibleTimer = Math.Min(InvisibleTimer, 1.0f); - } - InvisibleTimer -= deltaTime; - } - foreach (GUIMessage message in guiMessages) { bool wasPending = message.Timer < 0.0f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index fb10600e2..fdc92b921 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -149,6 +149,7 @@ namespace Barotrauma public static bool ShouldRecreateHudTexts { get; set; } = true; private static bool heldDownShiftWhenGotHudTexts; + private static float timeHealthWindowClosed; public static bool IsCampaignInterfaceOpen => GameMain.GameSession?.Campaign != null && @@ -222,7 +223,8 @@ namespace Barotrauma if (character.Info != null && !character.ShouldLockHud() && character.SelectedCharacter == null && Screen.Selected != GameMain.SubEditorScreen) { bool mouseOnPortrait = MouseOnCharacterPortrait() && GUI.MouseOn == null; - if (mouseOnPortrait && PlayerInput.PrimaryMouseButtonClicked() && Inventory.DraggingItems.None()) + bool healthWindowOpen = CharacterHealth.OpenHealthWindow != null || timeHealthWindowClosed < 0.2f; + if (mouseOnPortrait && !healthWindowOpen && PlayerInput.PrimaryMouseButtonClicked() && Inventory.DraggingItems.None()) { CharacterHealth.OpenHealthWindow = character.CharacterHealth; } @@ -290,6 +292,15 @@ namespace Barotrauma } } } + + if (CharacterHealth.OpenHealthWindow != null) + { + timeHealthWindowClosed = 0.0f; + } + else + { + timeHealthWindowClosed += deltaTime; + } } public static void Draw(SpriteBatch spriteBatch, Character character, Camera cam) @@ -539,7 +550,17 @@ namespace Barotrauma { var item = character.Inventory.GetItemAt(i); if (item == null || character.Inventory.SlotTypes[i] == InvSlotType.Any) { continue; } - + //if the item is also equipped in another slot we already went through, don't draw the hud again + bool duplicateFound = false; + for (int j = 0; j < i; j++) + { + if (character.Inventory.SlotTypes[j] != InvSlotType.Any && character.Inventory.GetItemAt(j) == item) + { + duplicateFound = true; + break; + } + } + if (duplicateFound) { continue; } foreach (ItemComponent ic in item.Components) { if (ic.DrawHudWhenEquipped) { ic.DrawHUD(spriteBatch, character); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 8d5e63f26..23db24f69 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -113,9 +113,9 @@ namespace Barotrauma } } - public void ClientWriteInput(IWriteMessage msg) + public void ClientWriteInput(in SegmentTableWriter segmentTableWriter, IWriteMessage msg) { - msg.WriteByte((byte)ClientNetObject.CHARACTER_INPUT); + segmentTableWriter.StartNewSegment(ClientNetSegment.CharacterInput); if (memInput.Count > 60) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index a2dca3ea6..7c7cbab37 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -156,7 +156,7 @@ namespace Barotrauma Character.Controlled.DeselectCharacter(); } - Character.Controlled.ResetInteract = true; + Character.Controlled.DisableInteract = true; if (openHealthWindow != null) { if (value.Character.Info == null || value.Character == Character.Controlled || Character.Controlled.HasEquippedItem("healthscanner".ToIdentifier())) diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs index e59354ecb..92ce38770 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs @@ -51,11 +51,10 @@ namespace Barotrauma && p.InstallTime.TryUnwrap(out var installTime) && item.LatestUpdateTime <= installTime)) .ToArray(); - if (needInstalling.Any()) - { - await Task.WhenAll( - needInstalling.Select(SteamManager.Workshop.DownloadModThenEnqueueInstall)); - } + if (!needInstalling.Any()) { return Enumerable.Empty(); } + + await Task.WhenAll( + needInstalling.Select(SteamManager.Workshop.DownloadModThenEnqueueInstall)); return needInstalling; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 9be433ef9..7c88699e9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -128,21 +128,17 @@ namespace Barotrauma public static void Update(float deltaTime) { - lock (queuedMessages) + while (queuedMessages.TryDequeue(out var newMsg)) { - while (queuedMessages.Count > 0) - { - var newMsg = queuedMessages.Dequeue(); - AddMessage(newMsg); + AddMessage(newMsg); - if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) + { + unsavedMessages.Add(newMsg); + if (unsavedMessages.Count >= messagesPerFile) { - unsavedMessages.Add(newMsg); - if (unsavedMessages.Count >= messagesPerFile) - { - SaveLogs(); - unsavedMessages.Clear(); - } + SaveLogs(); + unsavedMessages.Clear(); } } } @@ -258,25 +254,21 @@ namespace Barotrauma public static void DequeueMessages() { - lock (queuedMessages) + while (queuedMessages.TryDequeue(out var newMsg)) { - while (queuedMessages.Count > 0) + if (listBox == null) { - var newMsg = queuedMessages.Dequeue(); - if (listBox == null) - { - //don't attempt to add to the listbox if it hasn't been created yet - Messages.Add(newMsg); - } - else - { - AddMessage(newMsg); - } + //don't attempt to add to the listbox if it hasn't been created yet + Messages.Add(newMsg); + } + else + { + AddMessage(newMsg); + } - if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) - { - unsavedMessages.Add(newMsg); - } + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) + { + unsavedMessages.Add(newMsg); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs index fdf6fe6cb..fdfc6dfdb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs @@ -12,6 +12,7 @@ namespace Barotrauma partial void OnStateChangedProjSpecific() { + SoundPlayer.ForceMusicUpdate(); if (Phase == MissionPhase.NoItemsDestroyed) { CoroutineManager.Invoke(() => @@ -31,6 +32,10 @@ namespace Barotrauma } else if (Phase == MissionPhase.BossKilled) { + if (!string.IsNullOrEmpty(endCinematicSound)) + { + SoundPlayer.PlaySound(endCinematicSound); + } CoroutineManager.Invoke(() => { new CameraTransition(boss, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: 3, fadeOut: false, endZoom: 0.1f * GUI.yScale) @@ -60,7 +65,7 @@ namespace Barotrauma if (limb.LightSource is Lights.LightSource light) { light.Enabled = true; - } + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index f49f005b2..57a6d591d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -115,6 +115,11 @@ namespace Barotrauma }; } + public Identifier GetOverrideMusicType() + { + return Prefab.GetOverrideMusicType(State); + } + public virtual void ClientRead(IReadMessage msg) { State = msg.ReadInt16(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs index c8172bfaa..0bddcc04f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs @@ -1,11 +1,16 @@ using Microsoft.Xna.Framework; using System; -using System.Xml.Linq; +using System.Collections.Generic; +using System.Collections.Immutable; namespace Barotrauma { partial class MissionPrefab : PrefabWithUintIdentifier { + private ImmutableArray portraits = new ImmutableArray(); + + public bool HasPortraits => portraits.Length > 0; + public Sprite Icon { get; @@ -49,24 +54,57 @@ namespace Barotrauma private Sprite hudIcon; private Color? hudIconColor; + private ImmutableDictionary overrideMusicOnState; + partial void InitProjSpecific(ContentXElement element) { DisplayTargetHudIcons = element.GetAttributeBool("displaytargethudicons", false); HudIconMaxDistance = element.GetAttributeFloat("hudiconmaxdistance", 1000.0f); + Dictionary overrideMusic = new Dictionary(); + List portraits = new List(); foreach (var subElement in element.Elements()) { - string name = subElement.Name.ToString(); - if (name.Equals("icon", StringComparison.OrdinalIgnoreCase)) + switch (subElement.Name.ToString().ToLowerInvariant()) { - Icon = new Sprite(subElement); - IconColor = subElement.GetAttributeColor("color", Color.White); - } - else if (name.Equals("hudicon", StringComparison.OrdinalIgnoreCase)) - { - hudIcon = new Sprite(subElement); - hudIconColor = subElement.GetAttributeColor("color"); + case "icon": + Icon = new Sprite(subElement); + IconColor = subElement.GetAttributeColor("color", Color.White); + break; + case "hudicon": + hudIcon = new Sprite(subElement); + hudIconColor = subElement.GetAttributeColor("color"); + break; + case "overridemusic": + overrideMusic.Add( + subElement.GetAttributeInt("state", 0), + subElement.GetAttributeIdentifier("type", Identifier.Empty)); + break; + case "portrait": + var portrait = new Sprite(subElement, lazyLoad: true); + if (portrait != null) + { + portraits.Add(portrait); + } + break; } } + this.portraits = portraits.ToImmutableArray(); + overrideMusicOnState = overrideMusic.ToImmutableDictionary(); + } + + public Identifier GetOverrideMusicType(int state) + { + if (overrideMusicOnState.TryGetValue(state, out Identifier id)) + { + return id; + } + return Identifier.Empty; + } + + public Sprite GetPortrait(int randomSeed) + { + if (portraits.Length == 0) { return null; } + return portraits[Math.Abs(randomSeed) % portraits.Length]; } partial void DisposeProjectSpecific() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 85db03a6b..1cc059039 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -335,7 +335,7 @@ namespace Barotrauma DrawString(spriteBatch, new Vector2(10, y), "FPS: " + Math.Round(GameMain.PerformanceCounter.AverageFramesPerSecond), Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); - if (GameMain.GameSession != null && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 1.0) + if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 1.0) { y += yStep; DrawString(spriteBatch, new Vector2(10, y), @@ -695,22 +695,24 @@ namespace Barotrauma } } - public static void DrawBackgroundSprite(SpriteBatch spriteBatch, Sprite backgroundSprite, Color color) + public static void DrawBackgroundSprite(SpriteBatch spriteBatch, Sprite backgroundSprite, Color color, Rectangle? drawArea = null, SpriteEffects spriteEffects = SpriteEffects.None) { - float scale = Math.Max( - (float)GameMain.GraphicsWidth / backgroundSprite.SourceRect.Width, - (float)GameMain.GraphicsHeight / backgroundSprite.SourceRect.Height) * 1.1f; - float paddingX = backgroundSprite.SourceRect.Width * scale - GameMain.GraphicsWidth; - float paddingY = backgroundSprite.SourceRect.Height * scale - GameMain.GraphicsHeight; + Rectangle area = drawArea ?? new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight); - double noiseT = (Timing.TotalTime * 0.02f); + float scale = Math.Max( + (float)area.Width / backgroundSprite.SourceRect.Width, + (float)area.Height / backgroundSprite.SourceRect.Height) * 1.1f; + float paddingX = backgroundSprite.SourceRect.Width * scale - area.Width; + float paddingY = backgroundSprite.SourceRect.Height * scale - area.Height; + + double noiseT = Timing.TotalTime * 0.02f; Vector2 pos = new Vector2((float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0) - 0.5f, (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0.5f) - 0.5f); pos = new Vector2(pos.X * paddingX, pos.Y * paddingY); spriteBatch.Draw(backgroundSprite.Texture, - new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 + pos, + area.Center.ToVector2() + pos, null, color, 0.0f, backgroundSprite.size / 2, - scale, SpriteEffects.None, 0.0f); + scale, spriteEffects, 0.0f); } #region Update list diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index 2d8523b58..96a646db8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -758,7 +758,7 @@ namespace Barotrauma toolTipBlock.DrawManually(spriteBatch); } - public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle targetElement) + public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle targetElement, Anchor anchor = Anchor.BottomCenter, Pivot pivot = Pivot.TopLeft) { if (ObjectiveManager.ContentRunning) { return; } @@ -775,7 +775,10 @@ namespace Barotrauma toolTipBlock.UserData = toolTip; } - toolTipBlock.RectTransform.AbsoluteOffset = new Point(targetElement.Center.X, targetElement.Bottom); + toolTipBlock.RectTransform.AbsoluteOffset = + RectTransform.CalculateAnchorPoint(anchor, targetElement) + + RectTransform.CalculatePivotOffset(pivot, toolTipBlock.RectTransform.NonScaledSize); + if (toolTipBlock.Rect.Right > GameMain.GraphicsWidth - 10) { toolTipBlock.RectTransform.AbsoluteOffset -= new Point(toolTipBlock.Rect.Width, 0); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs index 39d938f59..0c5c6cc17 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs @@ -11,9 +11,9 @@ namespace Barotrauma { class LoadingScreen { - private readonly Texture2D defaultBackgroundTexture, overlay; + private readonly Sprite defaultBackgroundTexture, overlay; private readonly SpriteSheet decorativeGraph, decorativeMap; - private Texture2D currentBackgroundTexture; + private Sprite currentBackgroundTexture; private readonly Sprite noiseSprite; private string randText = ""; @@ -24,6 +24,8 @@ namespace Barotrauma private Video currSplashScreen; private DateTime videoStartTime; + private bool mirrorBackground; + public struct PendingSplashScreen { public string Filename; @@ -112,12 +114,12 @@ namespace Barotrauma public LoadingScreen(GraphicsDevice graphics) { - defaultBackgroundTexture = TextureLoader.FromFile("Content/Map/LocationPortraits/AlienRuins.png"); + defaultBackgroundTexture = new Sprite("Content/Map/LocationPortraits/MainMenu1.png", Vector2.Zero); decorativeMap = new SpriteSheet("Content/Map/MapHUD.png", 6, 5, Vector2.Zero, sourceRect: new Rectangle(0, 0, 2048, 640)); decorativeGraph = new SpriteSheet("Content/Map/MapHUD.png", 4, 10, Vector2.Zero, sourceRect: new Rectangle(1025, 1259, 1024, 732)); - overlay = TextureLoader.FromFile("Content/UI/MainMenuVignette.png"); + overlay = new Sprite("Content/UI/MainMenuVignette.png", Vector2.Zero); noiseSprite = new Sprite("Content/UI/noise.png", Vector2.Zero); DrawLoadingText = true; SetSelectedTip(TextManager.Get("LoadingScreenTip")); @@ -143,24 +145,19 @@ namespace Barotrauma currentBackgroundTexture ??= defaultBackgroundTexture; + float overlayScale = Math.Min(GameMain.GraphicsWidth / overlay.size.X, GameMain.GraphicsHeight / overlay.size.Y); + + Rectangle drawArea = new Rectangle( + (int)(overlay.size.X * overlayScale / 2), 0, + (int)(GameMain.GraphicsWidth - overlay.size.X * overlayScale / 2), GameMain.GraphicsHeight); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, samplerState: GUI.SamplerState); - float scale = Math.Max( - (float)GameMain.GraphicsWidth / currentBackgroundTexture.Width, - (float)GameMain.GraphicsHeight / currentBackgroundTexture.Height) * 1.2f; - float paddingX = currentBackgroundTexture.Width * scale - GameMain.GraphicsWidth; - float paddingY = currentBackgroundTexture.Height * scale - GameMain.GraphicsHeight; - double noiseT = (Timing.TotalTime * 0.02f); - Vector2 pos = new Vector2((float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0) - 0.5f, (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0.5f) - 0.5f); - pos = new Vector2(pos.X * paddingX, pos.Y * paddingY); - - spriteBatch.Draw(currentBackgroundTexture, - new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 + pos, - null, Color.White, 0.0f, new Vector2(currentBackgroundTexture.Width / 2, currentBackgroundTexture.Height / 2), - scale, SpriteEffects.None, 0.0f); - - spriteBatch.Draw(overlay, Vector2.Zero, null, Color.White, 0.0f, Vector2.Zero, Math.Min(GameMain.GraphicsWidth / (float)overlay.Width, GameMain.GraphicsHeight / (float)overlay.Height), SpriteEffects.None, 0.0f); + GUI.DrawBackgroundSprite(spriteBatch, currentBackgroundTexture, Color.White, drawArea, + spriteEffects: mirrorBackground ? SpriteEffects.FlipHorizontally : SpriteEffects.None); + overlay.Draw(spriteBatch, Vector2.Zero, scale: overlayScale); + double noiseT = Timing.TotalTime * 0.02f; float noiseStrength = (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0); float noiseScale = (float)PerlinNoise.CalculatePerlin(noiseT * 5.0f, noiseT * 2.0f, 0) * 4.0f; noiseSprite.DrawTiled(spriteBatch, Vector2.Zero, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight), @@ -430,7 +427,12 @@ namespace Barotrauma drawn = false; LoadState = null; SetSelectedTip(TextManager.Get("LoadingScreenTip")); - currentBackgroundTexture = LocationType.Prefabs.Where(p => p.UsePortraitInRandomLoadingScreens).GetRandomUnsynced()?.GetPortrait(Rand.Int(int.MaxValue))?.Texture; + currentBackgroundTexture = LocationType.Prefabs.Where(p => p.UsePortraitInRandomLoadingScreens).GetRandomUnsynced()?.GetPortrait(Rand.Int(int.MaxValue)); + if (GameMain.GameSession?.GameMode?.Missions is { } missions && missions.Any(m => m.Prefab.HasPortraits)) + { + currentBackgroundTexture = missions.Where(m => m.Prefab.HasPortraits).First().Prefab.GetPortrait(Rand.Int(int.MaxValue)); + } + mirrorBackground = Rand.Range(0.0f, 1.0f) < 0.5f; while (!drawn) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 6e7157ab2..ecb4ddc04 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -134,7 +134,7 @@ namespace Barotrauma set => hadSellSubPermissions = value; } - private bool HasPermissionToUseTab(StoreTab tab) + private static bool HasPermissionToUseTab(StoreTab tab) { return tab switch { @@ -278,6 +278,7 @@ namespace Barotrauma RefreshBuying(updateOwned: false); RefreshSelling(updateOwned: false); RefreshSellingFromSub(updateOwned: false); + SetConfirmButtonBehavior(); needsRefresh = false; } @@ -881,18 +882,17 @@ namespace Barotrauma float prevBuyListScroll = storeBuyList.BarScroll; float prevShoppingCrateScroll = shoppingCrateBuyList.BarScroll; - int dailySpecialCount = ActiveStore.DailySpecials.Count; - if ((storeDailySpecialsGroup != null) != ActiveStore.DailySpecials.Any() || dailySpecialCount != prevDailySpecialCount) + int dailySpecialCount = ActiveStore?.DailySpecials.Count(s => s.CanCharacterBuy()) ?? 0; + if ((ActiveStore == null && storeDailySpecialsGroup != null) || (storeDailySpecialsGroup != null) != ActiveStore.DailySpecials.Any() || dailySpecialCount != prevDailySpecialCount) { - if (storeDailySpecialsGroup == null || dailySpecialCount != prevDailySpecialCount) + storeBuyList.RemoveChild(storeDailySpecialsGroup?.Parent); + if (ActiveStore != null && (storeDailySpecialsGroup == null || dailySpecialCount != prevDailySpecialCount)) { - storeBuyList.RemoveChild(storeDailySpecialsGroup?.Parent); storeDailySpecialsGroup = CreateDealsGroup(storeBuyList, dailySpecialCount); storeDailySpecialsGroup.Parent.SetAsFirstChild(); } else { - storeBuyList.RemoveChild(storeDailySpecialsGroup.Parent); storeDailySpecialsGroup = null; } storeBuyList.RecalculateChildren(); @@ -901,15 +901,17 @@ namespace Barotrauma bool hasPermissions = HasTabPermissions(StoreTab.Buy); var existingItemFrames = new HashSet(); - foreach (PurchasedItem item in ActiveStore.Stock) + if (ActiveStore != null) { - CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); - } - - foreach (ItemPrefab itemPrefab in ActiveStore.DailySpecials) - { - if (ActiveStore.Stock.Any(pi => pi.ItemPrefab == itemPrefab)) { continue; } - CreateOrUpdateItemFrame(itemPrefab, 0); + foreach (PurchasedItem item in ActiveStore.Stock) + { + CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); + } + foreach (ItemPrefab itemPrefab in ActiveStore.DailySpecials) + { + if (ActiveStore.Stock.Any(pi => pi.ItemPrefab == itemPrefab)) { continue; } + CreateOrUpdateItemFrame(itemPrefab, 0); + } } void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int quantity) @@ -969,11 +971,11 @@ namespace Barotrauma float prevSellListScroll = storeSellList.BarScroll; float prevShoppingCrateScroll = shoppingCrateSellList.BarScroll; - int requestedGoodsCount = ActiveStore.RequestedGoods.Count; - if ((storeRequestedGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevRequestedGoodsCount) + int requestedGoodsCount = ActiveStore?.RequestedGoods.Count ?? 0; + if ((ActiveStore == null && storeRequestedGoodGroup != null) || (storeRequestedGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevRequestedGoodsCount) { storeSellList.RemoveChild(storeRequestedGoodGroup?.Parent); - if (storeRequestedGoodGroup == null || requestedGoodsCount != prevRequestedGoodsCount) + if (ActiveStore != null && (storeRequestedGoodGroup == null || requestedGoodsCount != prevRequestedGoodsCount)) { storeRequestedGoodGroup = CreateDealsGroup(storeSellList, requestedGoodsCount); storeRequestedGoodGroup.Parent.SetAsFirstChild(); @@ -988,14 +990,17 @@ namespace Barotrauma bool hasPermissions = HasTabPermissions(StoreTab.Sell); var existingItemFrames = new HashSet(); - foreach (PurchasedItem item in itemsToSell) + if (ActiveStore != null) { - CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); - } - foreach (var requestedGood in ActiveStore.RequestedGoods) - { - if (itemsToSell.Any(pi => pi.ItemPrefab == requestedGood)) { continue; } - CreateOrUpdateItemFrame(requestedGood, 0); + foreach (PurchasedItem item in itemsToSell) + { + CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); + } + foreach (var requestedGood in ActiveStore.RequestedGoods) + { + if (itemsToSell.Any(pi => pi.ItemPrefab == requestedGood)) { continue; } + CreateOrUpdateItemFrame(requestedGood, 0); + } } void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity) @@ -1053,11 +1058,11 @@ namespace Barotrauma float prevSellListScroll = storeSellFromSubList.BarScroll; float prevShoppingCrateScroll = shoppingCrateSellFromSubList.BarScroll; - int requestedGoodsCount = ActiveStore.RequestedGoods.Count; - if ((storeRequestedSubGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevSubRequestedGoodsCount) + int requestedGoodsCount = ActiveStore?.RequestedGoods.Count ?? 0; + if ((ActiveStore == null && storeRequestedSubGoodGroup != null) || (storeRequestedSubGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevSubRequestedGoodsCount) { storeSellFromSubList.RemoveChild(storeRequestedSubGoodGroup?.Parent); - if (storeRequestedSubGoodGroup == null || requestedGoodsCount != prevSubRequestedGoodsCount) + if (ActiveStore != null && (storeRequestedSubGoodGroup == null || requestedGoodsCount != prevSubRequestedGoodsCount)) { storeRequestedSubGoodGroup = CreateDealsGroup(storeSellFromSubList, requestedGoodsCount); storeRequestedSubGoodGroup.Parent.SetAsFirstChild(); @@ -1072,14 +1077,17 @@ namespace Barotrauma bool hasPermissions = HasSellSubPermissions; var existingItemFrames = new HashSet(); - foreach (PurchasedItem item in itemsToSellFromSub) + if (ActiveStore != null) { - CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); - } - foreach (var requestedGood in ActiveStore.RequestedGoods) - { - if (itemsToSellFromSub.Any(pi => pi.ItemPrefab == requestedGood)) { continue; } - CreateOrUpdateItemFrame(requestedGood, 0); + foreach (PurchasedItem item in itemsToSellFromSub) + { + CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); + } + foreach (var requestedGood in ActiveStore.RequestedGoods) + { + if (itemsToSellFromSub.Any(pi => pi.ItemPrefab == requestedGood)) { continue; } + CreateOrUpdateItemFrame(requestedGood, 0); + } } void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity) @@ -1166,6 +1174,7 @@ namespace Barotrauma public void RefreshItemsToSell() { itemsToSell.Clear(); + if (ActiveStore == null) { return; } var playerItems = CargoManager.GetSellableItems(Character.Controlled); foreach (Item playerItem in playerItems) { @@ -1196,6 +1205,7 @@ namespace Barotrauma public void RefreshItemsToSellFromSub() { itemsToSellFromSub.Clear(); + if (ActiveStore == null) { return; } var subItems = CargoManager.GetSellableItemsFromSub(); foreach (Item subItem in subItems) { @@ -1229,52 +1239,55 @@ namespace Barotrauma bool hasPermissions = HasTabPermissions(tab); HashSet existingItemFrames = new HashSet(); int totalPrice = 0; - foreach (PurchasedItem item in items) + if (ActiveStore != null) { - if (!(item.ItemPrefab.GetPriceInfo(ActiveStore) is { } priceInfo)) { continue; } - GUINumberInput numInput = null; - if (!(listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier) is { } itemFrame)) + foreach (PurchasedItem item in items) { - itemFrame = CreateItemFrame(item, listBox, tab, forceDisable: !hasPermissions); - numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput; - } - else - { - itemFrame.UserData = item; - numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput; + if (!(item.ItemPrefab.GetPriceInfo(ActiveStore) is { } priceInfo)) { continue; } + GUINumberInput numInput = null; + if (!(listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier) is { } itemFrame)) + { + itemFrame = CreateItemFrame(item, listBox, tab, forceDisable: !hasPermissions); + numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput; + } + else + { + itemFrame.UserData = item; + numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput; + if (numInput != null) + { + numInput.UserData = item; + numInput.Enabled = hasPermissions; + numInput.MaxValueInt = GetMaxAvailable(item.ItemPrefab, tab); + } + SetOwnedText(itemFrame); + SetItemFrameStatus(itemFrame, hasPermissions); + } + existingItemFrames.Add(itemFrame); + + suppressBuySell = true; if (numInput != null) { - numInput.UserData = item; - numInput.Enabled = hasPermissions; - numInput.MaxValueInt = GetMaxAvailable(item.ItemPrefab, tab); + if (numInput.IntValue != item.Quantity) { itemFrame.Flash(GUIStyle.Green); } + numInput.IntValue = item.Quantity; } - SetOwnedText(itemFrame); - SetItemFrameStatus(itemFrame, hasPermissions); - } - existingItemFrames.Add(itemFrame); + suppressBuySell = false; - suppressBuySell = true; - if (numInput != null) - { - if (numInput.IntValue != item.Quantity) { itemFrame.Flash(GUIStyle.Green); } - numInput.IntValue = item.Quantity; - } - suppressBuySell = false; - - try - { - int price = tab switch + try { - StoreTab.Buy => ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo), - StoreTab.Sell => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), - StoreTab.SellSub => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), - _ => throw new NotImplementedException() - }; - totalPrice += item.Quantity * price; - } - catch (NotImplementedException e) - { - DebugConsole.LogError($"Error getting item price: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + int price = tab switch + { + StoreTab.Buy => ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo), + StoreTab.Sell => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), + StoreTab.SellSub => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), + _ => throw new NotImplementedException() + }; + totalPrice += item.Quantity * price; + } + catch (NotImplementedException e) + { + DebugConsole.LogError($"Error getting item price: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + } } } @@ -1311,7 +1324,7 @@ namespace Barotrauma private void SortItems(GUIListBox list, SortingMethod sortingMethod) { - if (CurrentLocation == null) { return; } + if (CurrentLocation == null || ActiveStore == null) { return; } if (sortingMethod == SortingMethod.AlphabeticalAsc || sortingMethod == SortingMethod.AlphabeticalDesc) { @@ -1707,6 +1720,8 @@ namespace Barotrauma { OwnedItems.Clear(); + if (ActiveStore == null) { return; } + // Add items on the sub(s) if (Submarine.MainSub?.GetItems(true) is List subItems) { @@ -2124,7 +2139,12 @@ namespace Barotrauma private void SetShoppingCrateTotalText() { - if (IsBuying) + if (ActiveStore == null) + { + shoppingCrateTotal.Text = TextManager.FormatCurrency(0); + shoppingCrateTotal.TextColor = Color.White; + } + else if (IsBuying) { shoppingCrateTotal.Text = TextManager.FormatCurrency(buyTotal); shoppingCrateTotal.TextColor = Balance < buyTotal ? Color.Red : Color.White; @@ -2144,7 +2164,11 @@ namespace Barotrauma private void SetConfirmButtonBehavior() { - if (IsBuying) + if (ActiveStore == null) + { + confirmButton.OnClicked = null; + } + else if (IsBuying) { confirmButton.ClickSound = GUISoundType.ConfirmTransaction; confirmButton.Text = TextManager.Get("CampaignStore.Purchase"); @@ -2172,6 +2196,7 @@ namespace Barotrauma private void SetConfirmButtonStatus() { confirmButton.Enabled = + ActiveStore != null && HasActiveTabPermissions() && ActiveShoppingCrateList.Content.RectTransform.Children.Any() && activeTab switch @@ -2181,6 +2206,7 @@ namespace Barotrauma StoreTab.SellSub => CurrentLocation != null && sellFromSubTotal <= ActiveStore.Balance, _ => false }; + confirmButton.Visible = ActiveStore != null; } private void SetClearAllButtonStatus() @@ -2254,29 +2280,32 @@ namespace Barotrauma prevBalance = currBalance; } } - if (needsItemsToSellRefresh) + if (ActiveStore != null) { - RefreshItemsToSell(); - } - if (needsItemsToSellFromSubRefresh) - { - RefreshItemsToSellFromSub(); - } - if (needsRefresh) - { - Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f); - } - if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy)) - { - RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f); - } - if (needsSellingRefresh || HavePermissionsChanged(StoreTab.Sell)) - { - RefreshSelling(updateOwned: ownedItemsUpdateTimer > 0.0f); - } - if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub)) - { - RefreshSellingFromSub(updateOwned: ownedItemsUpdateTimer > 0.0f, updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f); + if (needsItemsToSellRefresh) + { + RefreshItemsToSell(); + } + if (needsItemsToSellFromSubRefresh) + { + RefreshItemsToSellFromSub(); + } + if (needsRefresh) + { + Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f); + } + if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy)) + { + RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f); + } + if (needsSellingRefresh || HavePermissionsChanged(StoreTab.Sell)) + { + RefreshSelling(updateOwned: ownedItemsUpdateTimer > 0.0f); + } + if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub)) + { + RefreshSellingFromSub(updateOwned: ownedItemsUpdateTimer > 0.0f, updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f); + } } updateStopwatch.Stop(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 8b88bc72c..cd0680556 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -1491,7 +1491,11 @@ namespace Barotrauma GUIFrame missionFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); int padding = (int)(0.0245f * missionFrame.Rect.Height); GUIFrame missionFrameContent = new GUIFrame(new RectTransform(new Point(missionFrame.Rect.Width - padding * 2, missionFrame.Rect.Height - padding * 2), infoFrame.RectTransform, Anchor.Center), style: null); - Location location = GameMain.GameSession.EndLocation ?? GameMain.GameSession.StartLocation; + Location location = GameMain.GameSession.StartLocation; + if (Level.Loaded.Type == LevelData.LevelType.LocationConnection) + { + location ??= GameMain.GameSession.EndLocation; + } GUILayoutGroup locationInfoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), missionFrameContent.RectTransform)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs index 23d4eb390..16043b01c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs @@ -283,7 +283,7 @@ namespace Barotrauma break; case TalentTreeType.Specialization: talentList = GetSpecializationList(); - treeSize = new Vector2(0.333f, 1f); + treeSize = new Vector2(Math.Max(0.333f, 1.0f / tree.TalentSubTrees.Count(t => t.Type == TalentTreeType.Specialization)), 1f); break; default: throw new ArgumentOutOfRangeException($"Invalid TalentTreeType \"{subTree.Type}\""); @@ -325,7 +325,9 @@ namespace Barotrauma } var specializationList = GetSpecializationList(); - GetSpecializationList().Content.RectTransform.Resize(new Point(specializationList.Content.Children.Sum(static c => c.Rect.Width), specializationList.Rect.Height), resizeChildren: false); + //resize (scale up) children if there's less than 3 of them to make them cover the whole width of the menu + specializationList.Content.RectTransform.Resize(new Point(specializationList.Content.Children.Sum(static c => c.Rect.Width), specializationList.Rect.Height), + resizeChildren: specializationList.Content.Children.Count() < 3); GUITextBlock.AutoScaleAndNormalize(subTreeNames); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index d5a7f5cb4..3e9908ee4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -16,6 +16,7 @@ using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading; +using Barotrauma.Extensions; namespace Barotrauma { @@ -465,10 +466,11 @@ namespace Barotrauma LegacySteamUgcTransition.Prepare(); var contentPackageLoadRoutine = ContentPackageManager.Init(); - foreach (var progress in contentPackageLoadRoutine) + foreach (var progress in contentPackageLoadRoutine + .Select(p => p.Result).Successes()) { const float min = 1f, max = 70f; - TitleScreen.LoadState = MathHelper.Lerp(min, max, progress.Value); + TitleScreen.LoadState = MathHelper.Lerp(min, max, progress); yield return CoroutineStatus.Running; } @@ -1078,10 +1080,9 @@ namespace Barotrauma if (GameSession != null) { - double roundDuration = Timing.TotalTime - GameSession.RoundStartTime; GameAnalyticsManager.AddProgressionEvent(GameAnalyticsManager.ProgressionStatus.Fail, GameSession.GameMode?.Preset.Identifier.Value ?? "none", - roundDuration); + GameSession.RoundDuration); string eventId = "QuitRound:" + (GameSession.GameMode?.Preset.Identifier.Value ?? "none") + ":"; GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:CurrentIntensity", GameSession.EventManager.CurrentIntensity); foreach (var activeEvent in GameSession.EventManager.ActiveEvents) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index dd4ca114b..baf76b999 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -11,7 +11,15 @@ namespace Barotrauma private List SoldEntities { get; } = new List(); // The bag slot is intentionally left out since we want to be able to sell items from there - private readonly List equipmentSlots = new List() { InvSlotType.Head, InvSlotType.InnerClothes, InvSlotType.OuterClothes, InvSlotType.Headset, InvSlotType.Card }; + private static readonly HashSet equipmentSlots = new HashSet() + { + InvSlotType.Head, + InvSlotType.InnerClothes, + InvSlotType.OuterClothes, + InvSlotType.Headset, + InvSlotType.Card, + InvSlotType.HealthInterface + }; public IEnumerable GetSellableItems(Character character) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index a5964a235..6a09e313d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -432,7 +432,7 @@ namespace Barotrauma break; } - Map.ProgressWorld(this, transitionType, (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime)); + Map.ProgressWorld(this, transitionType, GameMain.GameSession.RoundDuration); GUI.ClearMessages(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs index 7e70ed7f5..ad65dcb8f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs @@ -296,7 +296,7 @@ static class ObjectiveManager }; for (int i = 0; i < activeObjectives.Count; i++) { - AddToObjectiveList(activeObjectives[i]); + AddToObjectiveList(activeObjectives[i], useExistingIndex: true); } screenSettings = new ScreenSettings(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Scale, GameSettings.CurrentConfig.Graphics.DisplayMode); } @@ -318,7 +318,7 @@ static class ObjectiveManager /// /// Adds the segment to the objective list /// - private static void AddToObjectiveList(Segment segment, bool connectExisting = false) + private static void AddToObjectiveList(Segment segment, bool connectExisting = false, bool useExistingIndex = false) { if (connectExisting) { @@ -338,8 +338,8 @@ static class ObjectiveManager if (parentSegment is not null) { // Add this child as the last child in case there are other existing children already - int totalChildren = activeObjectives.Count(s => s.ParentId == segment.ParentId); - int childIndex = activeObjectives.IndexOf(parentSegment) + totalChildren; + int childIndex = useExistingIndex ? activeObjectives.IndexOf(segment) : + activeObjectives.IndexOf(parentSegment) + activeObjectives.Count(s => s.ParentId == segment.ParentId); if (objectiveGroup.RectTransform.GetChildIndex(frameRt) != childIndex) { frameRt.RepositionChildInHierarchy(childIndex); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 714a96576..05a85c692 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -971,7 +971,7 @@ namespace Barotrauma //don't allow swapping if we're moving items into an item with 1 slot holding a stack of items //(in that case, the quick action should just fill up the stack) bool disallowSwapping = - heldItem.OwnInventory.Capacity == 1 && + (heldItem.OwnInventory.Capacity == 1 || heldItem.OwnInventory.Container.HasSubContainers) && heldItem.OwnInventory.GetItemAt(0)?.Prefab == item.Prefab && heldItem.OwnInventory.GetItemsAt(0).Count() > 1; if (heldItem.OwnInventory.TryPutItem(item, Character.Controlled) || @@ -1131,7 +1131,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, inventoryArea, new Color(30,30,30,100), isFilled: true); var lockIcon = GUIStyle.GetComponentStyle("LockIcon")?.GetDefaultSprite(); lockIcon?.Draw(spriteBatch, inventoryArea.Center.ToVector2(), scale: Math.Min(inventoryArea.Height / lockIcon.size.Y * 0.7f, 1.0f)); - if (inventoryArea.Contains(PlayerInput.MousePosition)) + if (inventoryArea.Contains(PlayerInput.MousePosition) && character.LockHands) { GUIComponent.DrawToolTip(spriteBatch, TextManager.Get("handcuffed"), new Rectangle(inventoryArea.Center - new Point(inventoryArea.Height / 2), new Point(inventoryArea.Height))); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index c0b717496..3fff886f3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -408,6 +408,8 @@ namespace Barotrauma.Items.Components var wire = it.GetComponent(); if (wire != null && wire.Connections.Any(c => c != null)) { return false; } + if (it.Container?.GetComponent() is { DrawInventory: false }) { return false; } + if (it.HasTag("traitormissionitem")) { return false; } return true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index b229b2142..f121895d5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs @@ -29,7 +29,11 @@ namespace Barotrauma.Items.Components private Sprite tempRangeIndicator; private Sprite graphLine; - //private GUIFrame graph; + private GUICustomComponent graph; + + private GUIFrame inventoryWindow; + private GUILayoutGroup buttonArea; + private GUIFrame infographic; private Color optimalRangeColor = new Color(74,238,104,255); private Color offRangeColor = Color.Orange; @@ -66,6 +70,8 @@ namespace Barotrauma.Items.Components }; public override bool RecreateGUIOnResolutionChange => true; + + public bool TriggerInfographic { get; set; } partial void InitProjSpecific(ContentXElement element) { @@ -122,7 +128,7 @@ namespace Barotrauma.Items.Components //left column //---------------------------------------------------------- - GUIFrame inventoryWindow = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.75f), GuiFrame.RectTransform, Anchor.TopLeft, Pivot.TopRight) + inventoryWindow = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.75f), GuiFrame.RectTransform, Anchor.TopLeft, Pivot.TopRight) { MinSize = new Point(85, 220), RelativeOffset = new Vector2(-0.02f, 0) @@ -255,7 +261,7 @@ namespace Barotrauma.Items.Components }; TurbineOutputScrollBar.Frame.UserData = UIHighlightAction.ElementId.TurbineOutputSlider; - var buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1, 0.2f), columnLeft.RectTransform)) + buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1, 0.2f), columnLeft.RectTransform)) { Stretch = true, RelativeSpacing = 0.02f @@ -436,8 +442,8 @@ namespace Barotrauma.Items.Components LocalizedString kW = TextManager.Get("kilowatt"); loadText.TextGetter += () => $"{loadStr.Replace("[kw]", ((int)Load).ToString())} {kW}"; - var graph = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), graphArea.RectTransform), style: "InnerFrameRed"); - new GUICustomComponent(new RectTransform(new Vector2(0.9f, 0.98f), graph.RectTransform, Anchor.Center), DrawGraph, null); + var graphFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), graphArea.RectTransform), style: "InnerFrameRed"); + graph = new GUICustomComponent(new RectTransform(new Vector2(0.9f, 0.98f), graphFrame.RectTransform, Anchor.Center), DrawGraph, null); var outputText = new GUITextBlock(new RectTransform(relativeTextSize, graphArea.RectTransform), "Output", textColor: outputColor, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) @@ -448,6 +454,22 @@ namespace Barotrauma.Items.Components outputText.TextGetter += () => $"{outputStr.Replace("[kw]", ((int)-currPowerConsumption).ToString())} {kW}"; InitInventoryUI(); + + // Infographic overlay --------------------- + int buttonHeight = (int)(GUIStyle.ItemFrameMargin.Y * 0.4f); + var helpButtonRt = new RectTransform(new Point(buttonHeight), parent: GuiFrame.RectTransform, anchor: Anchor.TopRight) + { + AbsoluteOffset = new Point(buttonHeight / 4), + MinSize = new Point(buttonHeight) + }; + new GUIButton(helpButtonRt, "", style: "HelpIcon") + { + OnClicked = (_, _) => + { + CreateInfrographic(); + return true; + } + }; } private void InitInventoryUI() @@ -469,7 +491,6 @@ namespace Barotrauma.Items.Components InitInventoryUI(); } - private void DrawTempMeter(SpriteBatch spriteBatch, GUICustomComponent container) { Vector2 meterPos = new Vector2(container.Rect.X, container.Rect.Y); @@ -518,7 +539,6 @@ namespace Barotrauma.Items.Components DrawGraph(loadGraph, spriteBatch, graphRect, Math.Max(10000.0f, maxLoad), xOffset, loadColor); } - private void UpdateGraph(float deltaTime) { graphTimer += deltaTime * 1000.0f; @@ -645,6 +665,12 @@ namespace Barotrauma.Items.Components } } } + + if (TriggerInfographic) + { + CreateInfrographic(); + TriggerInfographic = false; + } } private void DrawMeter(SpriteBatch spriteBatch, Rectangle rect, Sprite meterSprite, float value, Vector2 range, Vector2 optimalRange, Vector2 allowedRange) @@ -760,7 +786,96 @@ namespace Barotrauma.Items.Components spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; spriteBatch.Begin(SpriteSortMode.Deferred); } + + private enum InfographicArrowStyle { Straight, Curved }; + private void CreateInfrographic() + { + if (infographic != null) { return; } + var dimColor = Color.Lerp(Color.Black, Color.TransparentBlack, 0.25f); + // Dim reactor interface + infographic = new GUIFrame(new RectTransform(Vector2.One, GuiFrame.RectTransform)) + { + CanBeFocused = false, + Color = dimColor + }; + // Dim inventory window + new GUIFrame(new RectTransform(inventoryWindow.Rect.Size, infographic.RectTransform) { AbsoluteOffset = inventoryWindow.Rect.Location - GuiFrame.Rect.Location}, color: dimColor) + { + CanBeFocused = false + }; + int arrowSize = (int)(70 * GUI.Scale); + var arrows = new Dictionary() + { + { "fuelslots", CreateArrow(InfographicArrowStyle.Curved, inventoryWindow, Anchor.TopLeft, Pivot.TopRight, SpriteEffects.FlipVertically) }, + { "temperature", CreateArrow(InfographicArrowStyle.Straight, temperatureBoostDownButton, Anchor.Center, Pivot.Center) }, + { "automaticcontrol", CreateArrow(InfographicArrowStyle.Curved, AutoTempSwitch, Anchor.TopRight, Pivot.BottomRight, rotationDegrees: 90f) }, + { "power", CreateArrow(InfographicArrowStyle.Straight, PowerButton, Anchor.BottomCenter, Pivot.TopCenter) } + }; + CreateArrow(InfographicArrowStyle.Straight, FissionRateScrollBar, Anchor.Center, Pivot.Center); + CreateArrow(InfographicArrowStyle.Straight, TurbineOutputScrollBar, Anchor.Center, Pivot.Center); + CreateArrow(InfographicArrowStyle.Straight, graph, Anchor.TopLeft, Pivot.TopLeft, SpriteEffects.FlipHorizontally, additionalOffset: new Point(arrowSize / 2, 0)); + CreateArrow(InfographicArrowStyle.Straight, graph, Anchor.BottomLeft, Pivot.BottomLeft, SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically, additionalOffset: new Point(arrowSize / 2, 0)); + new GUICustomComponent(new RectTransform(Vector2.One, infographic.RectTransform), + onDraw: (sb, c) => + { + DrawToolTip("fuelslots", Anchor.TopLeft, Pivot.BottomCenter, arrows["fuelslots"]); + DrawToolTip("fissionrate", Anchor.TopCenter, Pivot.TopCenter, buttonArea); + DrawToolTip("temperature", Anchor.BottomLeft, Pivot.TopLeft, arrows["temperature"]); + DrawToolTip("automaticcontrol", Anchor.TopLeft, Pivot.CenterRight, arrows["automaticcontrol"]); + DrawToolTip("power", Anchor.BottomCenter, Pivot.TopCenter, arrows["power"]); + DrawToolTip("load", Anchor.CenterLeft, Pivot.CenterLeft, graph); + + void DrawToolTip(string textTag, Anchor anchor, Pivot pivot, GUIComponent targetComponent) + { + GUIComponent.DrawToolTip(sb, + TextManager.Get($"infographic.reactor.{textTag}"), + targetComponent.Rect, + anchor: anchor, + pivot: pivot); + } + }) + { + CanBeFocused = false + }; + var closeButtonRt = new RectTransform(new Point(200, 50).Multiply(GUI.Scale), infographic.RectTransform, Anchor.TopRight, Pivot.BottomRight) + { + AbsoluteOffset = new Point(0, -50).Multiply(GUI.Scale) + }; + new GUIButton(closeButtonRt, TextManager.Get("close")) + { + OnClicked = (_, _) => + { + CloseInfographic(Character.Controlled); + return true; + } + }; + item.OnDeselect += CloseInfographic; + + GUIImage CreateArrow(InfographicArrowStyle arrowStyle, GUIComponent parent, Anchor anchor, Pivot pivot, SpriteEffects spriteEffects = SpriteEffects.None, float rotationDegrees = 0f, Point? additionalOffset = null) + { + Point offset = (additionalOffset ?? Point.Zero) + RectTransform.CalculateAnchorPoint(anchor, parent.Rect) - GuiFrame.Rect.Location; + var rt = new RectTransform(new Point(arrowSize), infographic.RectTransform, pivot: pivot) + { + AbsoluteOffset = offset + }; + string style = arrowStyle == InfographicArrowStyle.Straight ? "InfographicArrow" : "InfographicArrowCurved"; + return new GUIImage(rt, style) + { + Rotation = MathHelper.ToRadians(rotationDegrees), + SpriteEffects = spriteEffects + }; + } + + void CloseInfographic(Character character) + { + if (character != Character.Controlled) { return; } + GuiFrame.RemoveChild(infographic); + infographic = null; + item.OnDeselect -= CloseInfographic; + } + } + protected override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 7f4247503..404ca1718 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -817,7 +817,7 @@ namespace Barotrauma.Items.Components if (distSqr > t.SoundRange * t.SoundRange * 2) { continue; } float dist = (float)Math.Sqrt(distSqr); - if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500) + if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500 && t.IsWithinSector(transducerCenter)) { int prevBlipCount = sonarBlips.Count; Ping(t.WorldPosition, transducerCenter, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index 63560b853..915e8f695 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -35,6 +35,13 @@ namespace Barotrauma.Items.Components if (GuiFrame == null) { return; } originalMaxSize = GuiFrame.RectTransform.MaxSize; originalRelativeSize = GuiFrame.RectTransform.RelativeSize; + CreateGUI(); + } + + protected override void CreateGUI() + { + if (GuiFrame == null) { return; } + CheckForLabelOverlap(); var content = new GUICustomComponent(new RectTransform(Vector2.One, GuiFrame.RectTransform), DrawConnections, null) { @@ -43,8 +50,8 @@ namespace Barotrauma.Items.Components content.RectTransform.SetAsFirstChild(); //prevents inputs from going through the GUICustomComponent to the drag handle - dragArea = new GUIFrame(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) - { AbsoluteOffset = GUIStyle.ItemFrameOffset }, style: null); + dragArea = new GUIFrame(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) + { AbsoluteOffset = GUIStyle.ItemFrameOffset }, style: null); } public void TriggerRewiringSound() @@ -121,12 +128,6 @@ namespace Barotrauma.Items.Components } } - protected override void OnResolutionChanged() - { - if (GuiFrame == null) { return; } - CheckForLabelOverlap(); - } - private void CheckForLabelOverlap() { GuiFrame.RectTransform.MaxSize = originalMaxSize; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index 17922b0b4..c97a2945e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -324,7 +324,7 @@ namespace Barotrauma.Items.Components if (Character.Controlled != null) { Character.Controlled.FocusedItem = null; - Character.Controlled.ResetInteract = true; + Character.Controlled.DisableInteract = true; Character.Controlled.ClearInputs(); } //cancel dragging @@ -401,7 +401,7 @@ namespace Barotrauma.Items.Components { if (Character.Controlled != null) { - Character.Controlled.ResetInteract = true; + Character.Controlled.DisableInteract = true; Character.Controlled.ClearInputs(); } int closestSectionIndex = selectedWire.GetClosestSectionIndex(mousePos, sectionSelectDist, out _); @@ -431,7 +431,7 @@ namespace Barotrauma.Items.Components { if (Character.Controlled != null) { - Character.Controlled.ResetInteract = true; + Character.Controlled.DisableInteract = true; Character.Controlled.ClearInputs(); } draggingWire = selectedWire; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index d75a364b7..b614bef01 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -1287,7 +1287,8 @@ namespace Barotrauma { foreach (ItemComponent ic in activeHUDs) { - if (ic.GuiFrame == null || !ic.CanBeSelected) { continue; } + if (ic.GuiFrame == null) { continue; } + if (!ic.CanBeSelected && !ic.DrawHudWhenEquipped) { continue; } ic.GuiFrame.RectTransform.ScreenSpaceOffset = Point.Zero; if (ic.UseAlternativeLayout) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 901784267..39ee585b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -73,6 +73,8 @@ namespace Barotrauma private RichString beaconStationActiveText, beaconStationInactiveText; + private GUIComponent locationInfoOverlay; + /*private (Rectangle targetArea, string tip)? connectionTooltip; private string sanitizedConnectionTooltip; private List connectionTooltipRichTextData; @@ -353,6 +355,124 @@ namespace Barotrauma } } + private void CreateLocationInfoOverlay(Location location) + { + locationInfoOverlay = new GUIFrame(new RectTransform(new Point(GUI.IntScale(350), GUI.IntScale(350)), GUI.Canvas), style: "GUIToolTip") + { + UserData = location + }; + locationInfoOverlay.Color *= 0.8f; + + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), locationInfoOverlay.RectTransform, Anchor.Center)) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + bool showReputation = hudVisibility > 0.0f && location.Type.HasOutpost && location.Reputation != null; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Name, font: GUIStyle.LargeFont) { Padding = Vector4.Zero }; + if (!location.Type.Name.IsNullOrEmpty()) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; + } + + CreateSpacing(10); + + if (!location.Type.Description.IsNullOrEmpty()) + { + CreateTextWithIcon(location.Type.Description, location.Type.Sprite); + } + + int highestSubTier = location.HighestSubmarineTierAvailable(); + List<(SubmarineClass subClass, int tier)> overrideTiers = null; + if (location.CanHaveSubsForSale()) + { + overrideTiers = new List<(SubmarineClass subClass, int tier)>(); + foreach (SubmarineClass subClass in Enum.GetValues(typeof(SubmarineClass))) + { + if (subClass == SubmarineClass.Undefined) { continue; } + int highestClassTier = location.HighestSubmarineTierAvailable(subClass); + if (highestClassTier > 0 && highestClassTier > highestSubTier) + { + overrideTiers.Add((subClass, highestClassTier)); + } + } + } + if (highestSubTier > 0) + { + CreateTextWithIcon(TextManager.GetWithVariable("advancedsub.all", "[tiernumber]", highestSubTier.ToString()), icon: null, style: "LocationOverlaySubmarineIcon"); + } + if (overrideTiers != null) + { + foreach (var (subClass, tier) in overrideTiers) + { + CreateTextWithIcon(TextManager.GetWithVariable($"advancedsub.{subClass}", "[tiernumber]", tier.ToString()), icon: null, style: "LocationOverlaySubmarineIcon"); + } + } + + CreateSpacing(10); + + void CreateTextWithIcon(LocalizedString text, Sprite icon, string style = null) + { + var textHolder = new GUILayoutGroup(new RectTransform(new Point(content.Rect.Width, (int)GUIStyle.Font.MeasureString(text).Y), content.RectTransform), isHorizontal: true) + { + Stretch = true, + CanBeFocused = true + }; + var guiIcon = + style == null ? + new GUIImage(new RectTransform(Vector2.One * 1.25f, textHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), icon) : + new GUIImage(new RectTransform(Vector2.One * 1.25f, textHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), style); + var textBlock = new GUITextBlock(new RectTransform(new Vector2(0.9f, 1.0f), textHolder.RectTransform), text); + textBlock.RectTransform.MinSize = new Point((int)textBlock.TextSize.X, 0); + textHolder.RectTransform.MinSize = new Point((int)textBlock.TextSize.X + guiIcon.Rect.Width, 0); + } + + void CreateSpacing(int height) + { + new GUIFrame(new RectTransform(new Point(content.Rect.Width, GUI.IntScale(height)), content.RectTransform), style: null); + } + + if (location.Faction != null) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), + RichString.Rich(TextManager.GetWithVariables("reputationgainnotification", + ("[value]", string.Empty), + ("[reputationname]", $"‖color:{XMLExtensions.ToStringHex(location.Faction.Prefab.IconColor)}‖{location.Faction.Prefab.Name}‖end‖")))) + { + Padding = Vector4.Zero + }; + + CreateSpacing(10); + + var repBarHolder = new GUILayoutGroup(new RectTransform(new Point(content.Rect.Width, GUI.IntScale(25)), content.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + new GUICustomComponent(new RectTransform(new Vector2(0.6f, 1.0f), repBarHolder.RectTransform), onDraw: (sb, component) => + { + RoundSummary.DrawReputationBar(sb, component.Rect, location.Reputation.NormalizedValue); + }); + + new GUITextBlock(new RectTransform(new Vector2(0.4f, 1.0f), repBarHolder.RectTransform), + location.Reputation.GetFormattedReputationText(), textAlignment: Alignment.CenterRight); + + new GUIImage(new RectTransform(new Vector2(0.25f, 0.5f), locationInfoOverlay.RectTransform, Anchor.BottomRight) { RelativeOffset = new Vector2(0.05f) }, + location.Faction.Prefab.Icon, scaleToFit: true) + { + Color = location.Faction.Prefab.IconColor * 0.5f + }; + CreateSpacing(20); + } + + locationInfoOverlay.RectTransform.NonScaledSize = + new Point( + Math.Max(locationInfoOverlay.Rect.Width, (int)(content.Children.Max(c => c is GUITextBlock textBlock ? textBlock.TextSize.X : c.RectTransform.MinSize.X) * 1.2f)), + (int)(content.Children.Sum(c => c.Rect.Height) / content.RectTransform.RelativeSize.Y)); + } + partial void ClearAnimQueue() { mapAnimQueue.Clear(); @@ -425,38 +545,81 @@ namespace Barotrauma Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y); Vector2 viewOffset = DrawOffset + drawOffsetNoise; - float closestDist = 0.0f; - HighlightedLocation = null; - if (GUI.MouseOn == null || GUI.MouseOn == mapContainer) + + bool cursorOnOverlay = false; + if (HighlightedLocation != null) { - for (int i = 0; i < Locations.Count; i++) + Vector2 highlightedLocationDrawPos = rectCenter + (HighlightedLocation.MapPosition + viewOffset) * zoom; + if (locationInfoOverlay == null || locationInfoOverlay.UserData != HighlightedLocation) { - Location location = Locations[i]; - if (IsInFogOfWar(location) && !(currentDisplayLocation?.Connections.Any(c => c.Locations.Contains(location)) ?? false) && !GameMain.DebugDraw) { continue; } + CreateLocationInfoOverlay(HighlightedLocation); + } - Vector2 pos = rectCenter + (location.MapPosition + viewOffset) * zoom; - if (!rect.Contains(pos)) { continue; } + Point offsetFromLocationIcon = new Point(GUI.IntScale(25)); + var locationInfoRt = locationInfoOverlay.RectTransform; + if (locationInfoRt.Pivot == Pivot.BottomLeft || locationInfoRt.Pivot == Pivot.BottomRight) + { + offsetFromLocationIcon.Y = -offsetFromLocationIcon.Y; + } + if (locationInfoRt.Pivot == Pivot.TopRight || locationInfoRt.Pivot == Pivot.BottomRight) + { + offsetFromLocationIcon.X = -offsetFromLocationIcon.X; + } + locationInfoRt.ScreenSpaceOffset = highlightedLocationDrawPos.ToPoint() + offsetFromLocationIcon; + if (locationInfoOverlay.Rect.Bottom > rect.Bottom) + { + locationInfoRt.Pivot = Pivot.BottomLeft; + } + if (locationInfoOverlay.Rect.Right > rect.Right) + { + locationInfoRt.Pivot = locationInfoRt.Pivot == Pivot.TopLeft ? Pivot.TopRight : Pivot.BottomRight; + } - Sprite locationSprite = location.IsCriticallyRadiated() ? location.Type.RadiationSprite ?? location.Type.Sprite : location.Type.Sprite; - float iconScale = generationParams.LocationIconSize / locationSprite.size.X; - if (location == currentDisplayLocation) { iconScale *= 1.2f; } + Rectangle highlightedLocationRect = new Rectangle(highlightedLocationDrawPos.ToPoint(), new Point(GUI.IntScale(25))); + Rectangle overlayRect = Rectangle.Union(highlightedLocationRect, locationInfoRt.Rect); + if (overlayRect.Contains(PlayerInput.MousePosition)) + { + cursorOnOverlay = true; + } + locationInfoOverlay?.AddToGUIUpdateList(order: 1); + } - Rectangle drawRect = locationSprite.SourceRect; - drawRect.Width = (int)(drawRect.Width * iconScale * zoom * 1.4f); - drawRect.Height = (int)(drawRect.Height * iconScale * zoom * 1.4f); - drawRect.X = (int)pos.X - drawRect.Width / 2; - drawRect.Y = (int)pos.Y - drawRect.Width / 2; - - if (!drawRect.Contains(PlayerInput.MousePosition)) { continue; } - - float dist = Vector2.Distance(PlayerInput.MousePosition, pos); - if (HighlightedLocation == null || dist < closestDist) + if (!cursorOnOverlay) + { + float closestDist = 0.0f; + HighlightedLocation = null; + if ((GUI.MouseOn == null || GUI.MouseOn == mapContainer)) + { + for (int i = 0; i < Locations.Count; i++) { - closestDist = dist; - HighlightedLocation = location; + Location location = Locations[i]; + if (IsInFogOfWar(location) && !(currentDisplayLocation?.Connections.Any(c => c.Locations.Contains(location)) ?? false) && !GameMain.DebugDraw) { continue; } + + Vector2 pos = rectCenter + (location.MapPosition + viewOffset) * zoom; + if (!rect.Contains(pos)) { continue; } + + Sprite locationSprite = location.IsCriticallyRadiated() ? location.Type.RadiationSprite ?? location.Type.Sprite : location.Type.Sprite; + float iconScale = generationParams.LocationIconSize / locationSprite.size.X; + if (location == currentDisplayLocation) { iconScale *= 1.2f; } + + Rectangle drawRect = locationSprite.SourceRect; + drawRect.Width = (int)(drawRect.Width * iconScale * zoom * 1.4f); + drawRect.Height = (int)(drawRect.Height * iconScale * zoom * 1.4f); + drawRect.X = (int)pos.X - drawRect.Width / 2; + drawRect.Y = (int)pos.Y - drawRect.Width / 2; + + if (!drawRect.Contains(PlayerInput.MousePosition)) { continue; } + + float dist = Vector2.Distance(PlayerInput.MousePosition, pos); + if (HighlightedLocation == null || dist < closestDist) + { + closestDist = dist; + HighlightedLocation = location; + } } } } + if (SelectedConnection != null) { @@ -779,106 +942,6 @@ namespace Barotrauma GUIComponent.DrawToolTip(spriteBatch, tooltip.Value.tip, tooltip.Value.targetArea); drawRadiationTooltip = false; } - else if (HighlightedLocation != null) - { - drawRadiationTooltip = false; - Vector2 pos = rectCenter + (HighlightedLocation.MapPosition + viewOffset) * zoom; - pos.X += 50 * zoom; - pos.X = (int)pos.X; - pos.Y = (int)pos.Y; - Vector2 nameSize = GUIStyle.LargeFont.MeasureString(HighlightedLocation.Name); - Vector2 typeSize = HighlightedLocation.Type.Name.IsNullOrEmpty() ? Vector2.Zero : GUIStyle.Font.MeasureString(HighlightedLocation.Type.Name); - Vector2 factionSize = HighlightedLocation.Faction == null ? Vector2.Zero : GUIStyle.Font.MeasureString(HighlightedLocation.Faction.Prefab.Name); - bool showReputation = hudVisibility > 0.0f && HighlightedLocation.Type.HasOutpost && HighlightedLocation.Reputation != null; - Vector2 descSize = HighlightedLocation.Type.Description.IsNullOrEmpty() ? Vector2.Zero : GUIStyle.SmallFont.MeasureString(HighlightedLocation.Type.Description); - Vector2 size = new Vector2(Math.Max(factionSize.X, Math.Max(nameSize.X, Math.Max(typeSize.X, descSize.X))), nameSize.Y + factionSize.Y+ typeSize.Y + descSize.Y); - - int highestSubTier = HighlightedLocation.HighestSubmarineTierAvailable(); - List<(SubmarineClass subClass, int tier)> overrideTiers = null; - if (HighlightedLocation.CanHaveSubsForSale()) - { - overrideTiers = new List<(SubmarineClass subClass, int tier)>(); - foreach (SubmarineClass subClass in Enum.GetValues(typeof(SubmarineClass))) - { - if (subClass == SubmarineClass.Undefined) { continue; } - int highestClassTier = HighlightedLocation.HighestSubmarineTierAvailable(subClass); - if (highestClassTier > 0 && highestClassTier > highestSubTier) - { - overrideTiers.Add((subClass, highestClassTier)); - } - } - } - int subAvailabilityTextCount = (highestSubTier > 0 ? 1 : 0) + (overrideTiers?.Count ?? 0); - size.Y += subAvailabilityTextCount * GUIStyle.SmallFont.MeasureString(TextManager.Get("advancedsub.all")).Y; - - LocalizedString repLabelText = null, repValueText = null; - Vector2 repLabelSize = Vector2.Zero, repBarSize = Vector2.Zero; - if (showReputation && HighlightedLocation.Reputation != null) - { - repLabelText = TextManager.Get("reputation"); - repLabelSize = GUIStyle.Font.MeasureString(repLabelText); - repBarSize = new Vector2(GUI.IntScale(200), repLabelSize.Y); - size.Y += 2 * repLabelSize.Y + GUI.IntScale(5) + repBarSize.Y; - repValueText = HighlightedLocation.Reputation.GetFormattedReputationText(addColorTags: false); - size.X = Math.Max(size.X, repBarSize.X + GUIStyle.Font.MeasureString(repValueText).X + GUI.IntScale(10)); - } - - GUIStyle.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0].Draw( - spriteBatch, - new Rectangle( - (int)(pos.X - 60 * GUI.Scale), - (int)(pos.Y - size.Y), - (int)(size.X + 120 * GUI.Scale), - (int)(size.Y * 2.0f)), - Color.Black * hudVisibility); - - var topLeftPos = pos - new Vector2(0.0f, size.Y / 2); - GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Name, GUIStyle.TextColorNormal * hudVisibility * 1.5f, font: GUIStyle.LargeFont); - topLeftPos += new Vector2(0.0f, nameSize.Y); - - if (!HighlightedLocation.Type.Name.IsNullOrEmpty()) - { - DrawText(HighlightedLocation.Type.Name); - topLeftPos += new Vector2(0.0f, typeSize.Y); - } - if (HighlightedLocation.Faction != null) - { - GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Faction.Prefab.Name, GUIStyle.TextColorNormal * hudVisibility * 1.5f); - topLeftPos += new Vector2(0.0f, factionSize.Y); - } - if (!HighlightedLocation.Type.Description.IsNullOrEmpty()) - { - DrawText(HighlightedLocation.Type.Description, font: GUIStyle.SmallFont); - topLeftPos += new Vector2(0.0f, descSize.Y); - } - if (highestSubTier > 0) - { - DrawSubAvailabilityText("advancedsub.all", highestSubTier); - } - if (overrideTiers != null) - { - foreach (var (subClass, tier) in overrideTiers) - { - DrawSubAvailabilityText($"advancedsub.{subClass}", tier); - } - } - void DrawSubAvailabilityText(string tag, int tier) - { - DrawText(TextManager.GetWithVariable(tag, "[tiernumber]", tier.ToString()), font: GUIStyle.SmallFont); - topLeftPos += new Vector2(0.0f, typeSize.Y); - } - if (showReputation) - { - //topLeftPos += new Vector2(0.0f, typeSize.Y + repLabelSize.Y); - DrawText(repLabelText.Value); - topLeftPos += new Vector2(0.0f, repLabelSize.Y + GUI.IntScale(10)); - Rectangle repBarRect = new Rectangle(new Point((int)topLeftPos.X, (int)topLeftPos.Y), new Point((int)repBarSize.X, (int)repBarSize.Y)); - RoundSummary.DrawReputationBar(spriteBatch, repBarRect, HighlightedLocation.Reputation.NormalizedValue); - GUI.DrawString(spriteBatch, new Vector2(repBarRect.Right + GUI.IntScale(5), repBarRect.Top), repValueText.Value, Reputation.GetReputationColor(HighlightedLocation.Reputation.NormalizedValue)); - } - - void DrawText(LocalizedString text, GUIFont font = null) => GUI.DrawString(spriteBatch, topLeftPos, text, GUIStyle.TextColorNormal * hudVisibility * 1.5f, font: font); - } if (drawRadiationTooltip) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 14aced877..10eb37bcd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -1055,19 +1055,11 @@ namespace Barotrauma protected static void PositionEditingHUD() { - int maxHeight = 100; - if (Screen.Selected == GameMain.SubEditorScreen) - { - editingHUD.RectTransform.SetPosition(Anchor.TopRight); - editingHUD.RectTransform.AbsoluteOffset = new Point(0, GameMain.SubEditorScreen.TopPanel.Rect.Bottom); - maxHeight = (GameMain.GraphicsHeight - GameMain.SubEditorScreen.EntityMenu.Rect.Height) - GameMain.SubEditorScreen.TopPanel.Rect.Bottom * 2 - 20; - } - else - { - editingHUD.RectTransform.SetPosition(Anchor.TopRight); - editingHUD.RectTransform.RelativeOffset = new Vector2(0.0f, (HUDLayoutSettings.CrewArea.Bottom + 10.0f) / (editingHUD.RectTransform.Parent ?? GUI.Canvas).Rect.Height); - maxHeight = HUDLayoutSettings.InventoryAreaLower.Y - HUDLayoutSettings.CrewArea.Bottom - 10; - } + int maxHeight = + Screen.Selected == GameMain.SubEditorScreen ? + GameMain.GraphicsHeight - GameMain.SubEditorScreen.EntityMenu.Rect.Height - GameMain.SubEditorScreen.TopPanel.Rect.Bottom * 2 - 20 : + HUDLayoutSettings.InventoryAreaLower.Y - HUDLayoutSettings.CrewArea.Bottom - 10; + var listBox = editingHUD.GetChild(); if (listBox != null) @@ -1087,6 +1079,17 @@ namespace Barotrauma MathHelper.Clamp(contentHeight + padding * 2, 50, maxHeight)), resizeChildren: false); listBox.RectTransform.Resize(new Point(listBox.RectTransform.NonScaledSize.X, editingHUD.RectTransform.NonScaledSize.Y - padding * 2), resizeChildren: false); } + editingHUD.RectTransform.SetPosition(Anchor.TopRight); + if (Screen.Selected == GameMain.SubEditorScreen) + { + editingHUD.RectTransform.AbsoluteOffset = new Point(0, GameMain.SubEditorScreen.TopPanel.Rect.Bottom); + } + else + { + editingHUD.RectTransform.AbsoluteOffset = new Point( + 0, + HUDLayoutSettings.HealthBarAfflictionArea.Y - editingHUD.Rect.Height - GUI.IntScale(10)); + } } public virtual void DrawEditing(SpriteBatch spriteBatch, Camera cam) { } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index 8c1319fe5..4ae286687 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -6,9 +6,9 @@ namespace Barotrauma.Networking { partial class ChatMessage { - public virtual void ClientWrite(IWriteMessage msg) + public virtual void ClientWrite(in SegmentTableWriter segmentTableWriter, IWriteMessage msg) { - msg.WriteByte((byte)ClientNetObject.CHAT_MESSAGE); + segmentTableWriter.StartNewSegment(ClientNetSegment.ChatMessage); msg.WriteUInt16(NetStateID); msg.WriteRangedInteger((int)Type, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); msg.WriteRangedInteger((int)ChatMode, 0, Enum.GetValues(typeof(ChatMode)).Length - 1); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index a11aeb023..8c1af60e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -485,15 +485,11 @@ namespace Barotrauma.Networking } catch (Exception e) { - string errorMsg = "Error while reading a message from server. {" + e + "}. "; + string errorMsg = "Error while reading a message from server. "; if (GameMain.Client == null) { errorMsg += "Client disposed."; } - errorMsg += "\n" + e.StackTrace.CleanupStackTrace(); - if (e.InnerException != null) - { - errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace(); - } + AppendExceptionInfo(ref errorMsg, e); GameAnalyticsManager.AddErrorEventOnce("GameClient.Update:CheckServerMessagesException" + e.TargetSite.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - DebugConsole.ThrowError("Error while reading a message from server.", e); + DebugConsole.ThrowError(errorMsg); new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariables("MessageReadError", ("[message]", e.Message), ("[targetsite]", e.TargetSite.ToString()))) { DisplayInLoadingScreens = true @@ -636,14 +632,8 @@ namespace Barotrauma.Networking } catch (Exception e) { - string errorMsg = "Error while reading an ingame update message from server. {" + e + "}\n" + e.StackTrace.CleanupStackTrace(); - if (e.InnerException != null) - { - errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace(); - } -#if DEBUG - DebugConsole.ThrowError("Error while reading an ingame update message from server.", e); -#endif + string errorMsg = "Error while reading an ingame update message from server."; + AppendExceptionInfo(ref errorMsg, e); GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadDataMessage:ReadIngameUpdate", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw; } @@ -872,22 +862,31 @@ namespace Barotrauma.Networking } byte missionCount = inc.ReadByte(); - if (missionCount != GameMain.GameSession.Missions.Count()) - { - string errorMsg = $"Mission equality check failed. Mission count doesn't match the server (server: {missionCount}, client: {GameMain.GameSession.Missions.Count()})"; - throw new Exception(errorMsg); - } List serverMissionIdentifiers = new List(); for (int i = 0; i < missionCount; i++) { serverMissionIdentifiers.Add(inc.ReadIdentifier()); } + if (missionCount != GameMain.GameSession.GameMode.Missions.Count()) + { + string errorMsg = + $"Mission equality check failed. Mission count doesn't match the server. " + + $"Server: {string.Join(", ", serverMissionIdentifiers)}, " + + $"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " + + $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})"; + GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsCountMismatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + throw new Exception(errorMsg); + } if (missionCount > 0) { - if (!GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier).OrderBy(id => id).SequenceEqual(serverMissionIdentifiers.OrderBy(id => id))) + if (!GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier).OrderBy(id => id).SequenceEqual(serverMissionIdentifiers.OrderBy(id => id))) { - string errorMsg = $"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server (server: {string.Join(", ", serverMissionIdentifiers)}, client: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})"; + string errorMsg = + $"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server " + + $"Server: {string.Join(", ", serverMissionIdentifiers)}, " + + $"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " + + $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})"; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } @@ -1883,12 +1882,11 @@ namespace Barotrauma.Networking private void ReadLobbyUpdate(IReadMessage inc) { - ServerNetObject objHeader; - while ((objHeader = (ServerNetObject)inc.ReadByte()) != ServerNetObject.END_OF_MESSAGE) + SegmentTableReader.Read(inc, (segment, inc) => { - switch (objHeader) + switch (segment) { - case ServerNetObject.SYNC_IDS: + case ServerNetSegment.SyncIds: bool lobbyUpdated = inc.ReadBoolean(); inc.ReadPadBits(); @@ -2020,17 +2018,19 @@ namespace Barotrauma.Networking lastSentChatMsgID = inc.ReadUInt16(); break; - case ServerNetObject.CLIENT_LIST: + case ServerNetSegment.ClientList: ReadClientList(inc); break; - case ServerNetObject.CHAT_MESSAGE: + case ServerNetSegment.ChatMessage: ChatMessage.ClientRead(inc); break; - case ServerNetObject.VOTE: + case ServerNetSegment.Vote: Voting.ClientRead(inc); break; } - } + + return SegmentTableReader.BreakSegmentReading.No; + }); } readonly List debugEntityList = new List(); @@ -2040,117 +2040,106 @@ namespace Barotrauma.Networking float sendingTime = inc.ReadSingle() - 0.0f;//TODO: reimplement inc.SenderConnection.RemoteTimeOffset; - ServerNetObject? prevObjHeader = null; - long prevBitPos = 0; - long prevBytePos = 0; - - long prevBitLength = 0; - long prevByteLength = 0; - - ServerNetObject? objHeader = null; - try + SegmentTableReader.Read(inc, + segmentDataReader: (segment, inc) => { - while ((objHeader = (ServerNetObject)inc.ReadByte()) != ServerNetObject.END_OF_MESSAGE) + switch (segment) { - switch (objHeader) - { - case ServerNetObject.SYNC_IDS: - lastSentChatMsgID = inc.ReadUInt16(); - LastSentEntityEventID = inc.ReadUInt16(); + case ServerNetSegment.SyncIds: + lastSentChatMsgID = inc.ReadUInt16(); + LastSentEntityEventID = inc.ReadUInt16(); - bool campaignUpdated = inc.ReadBoolean(); - inc.ReadPadBits(); - if (campaignUpdated) - { - MultiPlayerCampaign.ClientRead(inc); - } - else if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign) - { - GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null); - } - break; - case ServerNetObject.ENTITY_POSITION: - inc.ReadPadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly - - bool isItem = inc.ReadBoolean(); inc.ReadPadBits(); - UInt32 incomingUintIdentifier = inc.ReadUInt32(); - UInt16 id = inc.ReadUInt16(); - uint msgLength = inc.ReadVariableUInt32(); - int msgEndPos = (int)(inc.BitPosition + msgLength * 8); + bool campaignUpdated = inc.ReadBoolean(); + inc.ReadPadBits(); + if (campaignUpdated) + { + MultiPlayerCampaign.ClientRead(inc); + } + else if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign) + { + GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null); + } + break; + case ServerNetSegment.EntityPosition: + inc.ReadPadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly + + bool isItem = inc.ReadBoolean(); inc.ReadPadBits(); + UInt32 incomingUintIdentifier = inc.ReadUInt32(); + UInt16 id = inc.ReadUInt16(); + uint msgLength = inc.ReadVariableUInt32(); + int msgEndPos = (int)(inc.BitPosition + msgLength * 8); - var entity = Entity.FindEntityByID(id) as IServerPositionSync; - if (msgEndPos > inc.LengthBits) - { - DebugConsole.ThrowError($"Error while reading a position update for the entity \"({entity?.ToString() ?? "null"})\". Message length exceeds the size of the buffer."); - return; - } + var entity = Entity.FindEntityByID(id) as IServerPositionSync; + if (msgEndPos > inc.LengthBits) + { + DebugConsole.ThrowError($"Error while reading a position update for the entity \"({entity?.ToString() ?? "null"})\". Message length exceeds the size of the buffer."); + return SegmentTableReader.BreakSegmentReading.Yes; + } - debugEntityList.Add(entity); - if (entity != null) + debugEntityList.Add(entity); + if (entity != null) + { + if (entity is Item != isItem) { - if (entity is Item != isItem) - { - DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message. Entity type does not match (server entity is {(isItem ? "an item" : "not an item")}, client entity is {(entity?.GetType().ToString() ?? "null")}). Ignoring the message..."); - } - else if (entity is MapEntity { Prefab: { UintIdentifier: { } uintIdentifier } } me && - uintIdentifier != incomingUintIdentifier) - { - DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message." - +$"Entity identifier does not match (server entity is {MapEntityPrefab.List.FirstOrDefault(p => p.UintIdentifier == incomingUintIdentifier)?.Identifier.Value ?? "[not found]"}, " - +$"client entity is {me.Prefab.Identifier}). Ignoring the message..."); - } - else - { - entity.ClientReadPosition(inc, sendingTime); - } + DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message. Entity type does not match (server entity is {(isItem ? "an item" : "not an item")}, client entity is {(entity?.GetType().ToString() ?? "null")}). Ignoring the message..."); } - - //force to the correct position in case the entity doesn't exist - //or the message wasn't read correctly for whatever reason - inc.BitPosition = msgEndPos; - inc.ReadPadBits(); - break; - case ServerNetObject.CLIENT_LIST: - ReadClientList(inc); - break; - case ServerNetObject.ENTITY_EVENT: - case ServerNetObject.ENTITY_EVENT_INITIAL: - if (!EntityEventManager.Read(objHeader.Value, inc, sendingTime, debugEntityList)) + else if (entity is MapEntity { Prefab: { UintIdentifier: { } uintIdentifier } } me && + uintIdentifier != incomingUintIdentifier) { - return; + DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message." + +$"Entity identifier does not match (server entity is {MapEntityPrefab.List.FirstOrDefault(p => p.UintIdentifier == incomingUintIdentifier)?.Identifier.Value ?? "[not found]"}, " + +$"client entity is {me.Prefab.Identifier}). Ignoring the message..."); } - break; - case ServerNetObject.CHAT_MESSAGE: - ChatMessage.ClientRead(inc); - break; - default: - throw new Exception($"Unknown object header \"{objHeader}\"!)"); - } - prevBitLength = inc.BitPosition - prevBitPos; - prevByteLength = inc.BytePosition - prevBytePos; + else + { + entity.ClientReadPosition(inc, sendingTime); + } + } - prevObjHeader = objHeader; - prevBitPos = inc.BitPosition; - prevBytePos = inc.BytePosition; + //force to the correct position in case the entity doesn't exist + //or the message wasn't read correctly for whatever reason + inc.BitPosition = msgEndPos; + inc.ReadPadBits(); + break; + case ServerNetSegment.ClientList: + ReadClientList(inc); + break; + case ServerNetSegment.EntityEvent: + case ServerNetSegment.EntityEventInitial: + if (!EntityEventManager.Read(segment, inc, sendingTime, debugEntityList)) + { + return SegmentTableReader.BreakSegmentReading.Yes; + } + break; + case ServerNetSegment.ChatMessage: + ChatMessage.ClientRead(inc); + break; + default: + throw new Exception($"Unknown segment \"{segment}\"!)"); } - } - catch (Exception ex) + + return SegmentTableReader.BreakSegmentReading.No; + }, + exceptionHandler: (segment, prevSegments, ex) => { List errorLines = new List { ex.Message, "Message length: " + inc.LengthBits + " (" + inc.LengthBytes + " bytes)", "Read position: " + inc.BitPosition, - "Header: " + (objHeader != null ? objHeader.Value.ToString() : "Error occurred on the very first header!"), - prevObjHeader != null ? "Previous header: " + prevObjHeader : "Error occurred on the very first header!", - "Previous object was " + (prevBitLength) + " bits long (" + (prevByteLength) + " bytes)", - " " + $"Segment with error: {segment}" }; + if (prevSegments.Any()) + { + errorLines.Add("Prev segments: " + string.Join(", ", prevSegments)); + errorLines.Add(" "); + } errorLines.Add(ex.StackTrace.CleanupStackTrace()); errorLines.Add(" "); - if (prevObjHeader == ServerNetObject.ENTITY_EVENT || prevObjHeader == ServerNetObject.ENTITY_EVENT_INITIAL || - objHeader == ServerNetObject.ENTITY_EVENT || objHeader == ServerNetObject.ENTITY_EVENT_INITIAL || - objHeader == ServerNetObject.ENTITY_POSITION || prevObjHeader == ServerNetObject.ENTITY_POSITION) + if (prevSegments.Concat(segment.ToEnumerable()).Any(s => s.Identifier + is ServerNetSegment.EntityPosition + or ServerNetSegment.EntityEvent + or ServerNetSegment.EntityEventInitial)) { foreach (IServerSerializable ent in debugEntityList) { @@ -2164,34 +2153,18 @@ namespace Barotrauma.Networking } } - foreach (string line in errorLines) - { - DebugConsole.ThrowError(line); - } errorLines.Add("Last console messages:"); for (int i = DebugConsole.Messages.Count - 1; i > Math.Max(0, DebugConsole.Messages.Count - 20); i--) { errorLines.Add("[" + DebugConsole.Messages[i].Time + "] " + DebugConsole.Messages[i].Text); } GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadInGameUpdate", GameAnalyticsManager.ErrorSeverity.Critical, string.Join("\n", errorLines)); - - DebugConsole.ThrowError("Writing object data to \"networkerror_data.log\", please send this file to us at http://github.com/Regalis11/Barotrauma/issues"); - - using (FileStream fl = File.Open("networkerror_data.log", System.IO.FileMode.Create)) - { - using (System.IO.BinaryWriter bw = new System.IO.BinaryWriter(fl)) - using (System.IO.StreamWriter sw = new System.IO.StreamWriter(fl)) - { - bw.Write(inc.Buffer, (int)(prevBytePos - prevByteLength), (int)(prevByteLength)); - sw.WriteLine(""); - foreach (string line in errorLines) - { - sw.WriteLine(line); - } - } - } - throw new Exception("Read error: please send us \"networkerror_data.log\"!"); - } + + throw new Exception( + $"Exception thrown while reading segment {segment.Identifier} at position {segment.Pointer}." + + (prevSegments.Any() ? $" Previous segments: {string.Join(", ", prevSegments)}" : ""), + ex); + }); } private void SendLobbyUpdate() @@ -2199,50 +2172,51 @@ namespace Barotrauma.Networking IWriteMessage outmsg = new WriteOnlyMessage(); outmsg.WriteByte((byte)ClientPacketHeader.UPDATE_LOBBY); - outmsg.WriteByte((byte)ClientNetObject.SYNC_IDS); - outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID); - outmsg.WriteUInt16(ChatMessage.LastID); - outmsg.WriteUInt16(LastClientListUpdateID); - outmsg.WriteUInt16(nameId); - outmsg.WriteString(Name); - var jobPreferences = GameMain.NetLobbyScreen.JobPreferences; - if (jobPreferences.Count > 0) + using (var segmentTable = SegmentTableWriter.StartWriting(outmsg)) { - outmsg.WriteIdentifier(jobPreferences[0].Prefab.Identifier); - } - else - { - outmsg.WriteIdentifier(Identifier.Empty); - } - outmsg.WriteByte((byte)MultiplayerPreferences.Instance.TeamPreference); - - if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) - { - outmsg.WriteUInt16((UInt16)0); - } - else - { - outmsg.WriteUInt16(campaign.LastSaveID); - outmsg.WriteByte(campaign.CampaignID); - foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + segmentTable.StartNewSegment(ClientNetSegment.SyncIds); + outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID); + outmsg.WriteUInt16(ChatMessage.LastID); + outmsg.WriteUInt16(LastClientListUpdateID); + outmsg.WriteUInt16(nameId); + outmsg.WriteString(Name); + var jobPreferences = GameMain.NetLobbyScreen.JobPreferences; + if (jobPreferences.Count > 0) { - outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(netFlag)); + outmsg.WriteIdentifier(jobPreferences[0].Prefab.Identifier); } - outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); - } - - chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID)); - for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) - { - if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5) + else { - //no more room in this packet - break; + outmsg.WriteIdentifier(Identifier.Empty); } - chatMsgQueue[i].ClientWrite(outmsg); - } - outmsg.WriteByte((byte)ClientNetObject.END_OF_MESSAGE); + outmsg.WriteByte((byte)MultiplayerPreferences.Instance.TeamPreference); + if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) + { + outmsg.WriteUInt16((UInt16)0); + } + else + { + outmsg.WriteUInt16(campaign.LastSaveID); + outmsg.WriteByte(campaign.CampaignID); + foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(netFlag)); + } + outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); + } + + chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID)); + for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) + { + if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5) + { + //no more room in this packet + break; + } + chatMsgQueue[i].ClientWrite(segmentTable, outmsg); + } + } if (outmsg.LengthBytes > MsgConstants.MTU) { DebugConsole.ThrowError($"Maximum packet size exceeded ({outmsg.LengthBytes} > {MsgConstants.MTU})"); @@ -2258,44 +2232,47 @@ namespace Barotrauma.Networking outmsg.WriteBoolean(EntityEventManager.MidRoundSyncingDone); outmsg.WritePadBits(); - outmsg.WriteByte((byte)ClientNetObject.SYNC_IDS); - //outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID); - outmsg.WriteUInt16(ChatMessage.LastID); - outmsg.WriteUInt16(EntityEventManager.LastReceivedID); - outmsg.WriteUInt16(LastClientListUpdateID); + using (var segmentTable = SegmentTableWriter.StartWriting(outmsg)) + { + segmentTable.StartNewSegment(ClientNetSegment.SyncIds); + //outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID); + outmsg.WriteUInt16(ChatMessage.LastID); + outmsg.WriteUInt16(EntityEventManager.LastReceivedID); + outmsg.WriteUInt16(LastClientListUpdateID); - if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) - { - outmsg.WriteUInt16((UInt16)0); - } - else - { - outmsg.WriteUInt16(campaign.LastSaveID); - outmsg.WriteByte(campaign.CampaignID); - foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) { - outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(flag)); + outmsg.WriteUInt16((UInt16)0); } - outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); - } - - Character.Controlled?.ClientWriteInput(outmsg); - GameMain.GameScreen.Cam?.ClientWrite(outmsg); - - EntityEventManager.Write(outmsg, ClientPeer?.ServerConnection); - - chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID)); - for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) - { - if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5) + else { - //not enough room in this packet - break; - } - chatMsgQueue[i].ClientWrite(outmsg); - } + outmsg.WriteUInt16(campaign.LastSaveID); + outmsg.WriteByte(campaign.CampaignID); + foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(flag)); + } - outmsg.WriteByte((byte)ClientNetObject.END_OF_MESSAGE); + outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); + } + + Character.Controlled?.ClientWriteInput(segmentTable, outmsg); + GameMain.GameScreen.Cam?.ClientWrite(segmentTable, outmsg); + + EntityEventManager.Write(segmentTable, outmsg, ClientPeer?.ServerConnection); + + chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID)); + for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) + { + if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5) + { + //not enough room in this packet + break; + } + + chatMsgQueue[i].ClientWrite(segmentTable, outmsg); + } + } if (outmsg.LengthBytes > MsgConstants.MTU) { @@ -2608,7 +2585,6 @@ namespace Barotrauma.Networking IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ClientPacketHeader.UPDATE_CHARACTERINFO); WriteCharacterInfo(msg, newName); - msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); ClientPeer?.Send(msg, DeliveryMethod.Reliable); } @@ -2648,9 +2624,11 @@ namespace Barotrauma.Networking IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ClientPacketHeader.UPDATE_LOBBY); - msg.WriteByte((byte)ClientNetObject.VOTE); - Voting.ClientWrite(msg, voteType, data); - msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); + using (var segmentTable = SegmentTableWriter.StartWriting(msg)) + { + segmentTable.StartNewSegment(ClientNetSegment.Vote); + Voting.ClientWrite(msg, voteType, data); + } ClientPeer.Send(msg, DeliveryMethod.Reliable); } @@ -2763,7 +2741,6 @@ namespace Barotrauma.Networking msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); msg.WriteUInt16((UInt16)ClientPermissions.ManageCampaign); campaign.ClientWrite(msg); - msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); ClientPeer.Send(msg, DeliveryMethod.Reliable); } @@ -2812,7 +2789,6 @@ namespace Barotrauma.Networking msg.WriteUInt16((UInt16)ClientPermissions.SelectSub); msg.WriteBoolean(isShuttle); msg.WritePadBits(); msg.WriteString(sub.MD5Hash.StringRepresentation); - msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); ClientPeer.Send(msg, DeliveryMethod.Reliable); } @@ -2832,7 +2808,6 @@ namespace Barotrauma.Networking msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); msg.WriteUInt16((UInt16)ClientPermissions.SelectMode); msg.WriteUInt16((UInt16)modeIndex); - msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); ClientPeer.Send(msg, DeliveryMethod.Reliable); } @@ -3538,6 +3513,23 @@ namespace Barotrauma.Networking eventErrorWritten = true; } + private static void AppendExceptionInfo(ref string errorMsg, Exception e) + { + if (!errorMsg.EndsWith("\n")) { errorMsg += "\n"; } + errorMsg += e.Message + "\n"; + var innermostException = e.GetInnermost(); + if (innermostException != e) + { + // If available, only append the stacktrace of the innermost exception, + // because that's the most important one to fix + errorMsg += "Inner exception: " + innermostException.Message + "\n" + innermostException.StackTrace.CleanupStackTrace(); + } + else + { + errorMsg += e.StackTrace.CleanupStackTrace(); + } + } + #if DEBUG public void ForceTimeOut() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs index c6cce06c2..0d14de93b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -66,7 +66,7 @@ namespace Barotrauma.Networking events.Add(newEvent); } - public void Write(IWriteMessage msg, NetworkConnection serverConnection) + public void Write(in SegmentTableWriter segmentTable, IWriteMessage msg, NetworkConnection serverConnection) { if (events.Count == 0 || serverConnection == null) return; @@ -103,7 +103,7 @@ namespace Barotrauma.Networking eventLastSent[entityEvent.ID] = (float)Lidgren.Network.NetTime.Now; } - msg.WriteByte((byte)ClientNetObject.ENTITY_STATE); + segmentTable.StartNewSegment(ClientNetSegment.EntityState); Write(msg, eventsToSync, out _); } @@ -112,11 +112,11 @@ namespace Barotrauma.Networking /// /// Read the events from the message, ignoring ones we've already received. Returns false if reading the events fails. /// - public bool Read(ServerNetObject type, IReadMessage msg, float sendingTime, List entities) + public bool Read(ServerNetSegment type, IReadMessage msg, float sendingTime, List entities) { UInt16 unreceivedEntityEventCount = 0; - if (type == ServerNetObject.ENTITY_EVENT_INITIAL) + if (type == ServerNetSegment.EntityEventInitial) { unreceivedEntityEventCount = msg.ReadUInt16(); firstNewID = msg.ReadUInt16(); @@ -218,43 +218,20 @@ namespace Barotrauma.Networking Microsoft.Xna.Framework.Color.Green); } lastReceivedID++; - try + ReadEvent(msg, entity, sendingTime); + msg.ReadPadBits(); + + if (msg.BitPosition != msgPosition + msgLength * 8) { - ReadEvent(msg, entity, sendingTime); - msg.ReadPadBits(); + var prevEntity = entities.Count >= 2 ? entities[entities.Count - 2] : null; + ushort prevId = prevEntity is Entity p ? p.ID : (ushort)0; + string errorMsg = $"Message byte position incorrect after reading an event for the entity \"{entity}\" (ID {(entity is Entity e ? e.ID : 0)}). " + +$"The previous entity was \"{prevEntity}\" (ID {prevId}) " + +$"Read {msg.BitPosition - msgPosition} bits, expected message length was {msgLength * 8} bits."; - if (msg.BitPosition != msgPosition + msgLength * 8) - { - var prevEntity = entities.Count >= 2 ? entities[entities.Count - 2] : null; - ushort prevId = prevEntity is Entity p ? p.ID : (ushort)0; - string errorMsg = $"Message byte position incorrect after reading an event for the entity \"{entity}\" (ID {(entity is Entity e ? e.ID : 0)}). " - +$"The previous entity was \"{prevEntity}\" (ID {prevId}) " - +$"Read {msg.BitPosition - msgPosition} bits, expected message length was {msgLength * 8} bits."; - - DebugConsole.ThrowError(errorMsg); - - GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:BitPosMismatch", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - - //TODO: force the BitPosition to correct place? Having some entity in a potentially incorrect state is not as bad as a desync kick - //msg.BitPosition = (int)(msgPosition + msgLength * 8); - } - } - catch (Exception e) - { - string errorMsg = $"Failed to read event {thisEventID} for entity \"{entity}\"" + - $"{(entity is Entity { ID: var entityId } ? $", id {entityId}" : "")} "; - DebugConsole.ThrowError(errorMsg, e); - - errorMsg += $"({e.Message})! (MidRoundSyncing: {thisClient.MidRoundSyncing})\n{e.StackTrace.CleanupStackTrace()}"; - errorMsg += "\nPrevious entities:"; - for (int j = entities.Count - 2; j >= 0; j--) - { - errorMsg += "\n" + (entities[j] == null ? "NULL" : entities[j].ToString()); - } - - GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:ReadFailed" + entity.ToString(), - GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - msg.BitPosition = (int)(msgPosition + msgLength * 8); + GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:BitPosMismatch", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + + throw new Exception(errorMsg); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs index b21592b8d..9d7c0df0e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs @@ -4,9 +4,9 @@ namespace Barotrauma.Networking { partial class OrderChatMessage : ChatMessage { - public override void ClientWrite(IWriteMessage msg) + public override void ClientWrite(in SegmentTableWriter segmentTableWriter, IWriteMessage msg) { - msg.WriteByte((byte)ClientNetObject.CHAT_MESSAGE); + segmentTableWriter.StartNewSegment(ClientNetSegment.ChatMessage); msg.WriteUInt16(NetStateID); msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); msg.WriteRangedInteger((int)ChatMode.None, 0, Enum.GetValues(typeof(ChatMode)).Length - 1); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index bc78e1fe5..1d51cb8f1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -1766,9 +1766,15 @@ namespace Barotrauma.CharacterEditor var modProject = new ModProject(contentPackage); var newFile = ModProject.File.FromPath(configFilePath); modProject.AddFile(newFile); - modProject.Save(contentPackage.Path); - contentPackage = ContentPackageManager.ReloadContentPackage(contentPackage); + + var reloadResult = ContentPackageManager.ReloadContentPackage(contentPackage); + if (!reloadResult.TryUnwrapSuccess(out var newPackage)) + { + throw new Exception($"Failed to reload package", + reloadResult.TryUnwrapFailure(out var exception) ? exception : null); + } + contentPackage = newPackage; DebugConsole.NewMessage(GetCharacterEditorTranslation("ContentPackageSaved").Replace("[path]", contentPackage.Path)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 68fe0d1b2..fabe532e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -956,16 +956,21 @@ namespace Barotrauma backgroundSprite = new Sprite("Content/UnstableBackground.png", sourceRectangle: null); } - if (backgroundSprite != null) + var vignette = GUIStyle.GetComponentStyle("mainmenuvignette")?.GetDefaultSprite(); + float vignetteScale = Math.Min(GameMain.GraphicsWidth / vignette.size.X, GameMain.GraphicsHeight / vignette.size.Y); + + Rectangle drawArea = new Rectangle( + (int)(vignette.size.X * vignetteScale / 2), 0, + (int)(GameMain.GraphicsWidth - vignette.size.X * vignetteScale / 2), GameMain.GraphicsHeight); + + if (backgroundSprite?.Texture != null) { - GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite, Color.White); + GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite, Color.White, drawArea); } - var vignette = GUIStyle.GetComponentStyle("mainmenuvignette")?.GetDefaultSprite(); if (vignette != null) { - vignette.Draw(spriteBatch, Vector2.Zero, Color.White, Vector2.Zero, 0.0f, - new Vector2(Math.Min(GameMain.GraphicsWidth / vignette.size.X, GameMain.GraphicsHeight / vignette.size.Y))); + vignette.Draw(spriteBatch, Vector2.Zero, Color.White, Vector2.Zero, 0.0f, vignetteScale); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index 34d6e7466..8536cf80a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -1,6 +1,7 @@ #nullable enable using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Barotrauma.Extensions; using Barotrauma.IO; @@ -40,6 +41,13 @@ namespace Barotrauma } } + [DoesNotReturn] + private static void LogAndThrowException(string errorMsg, string analyticsId) + { + GameAnalyticsManager.AddErrorEventOnce(analyticsId, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + throw new InvalidOperationException(errorMsg); + } + public override void Select() { base.Select(); @@ -74,20 +82,11 @@ namespace Barotrauma } }; - if (!GameMain.Client.IsServerOwner) + if (!GameMain.Client.IsServerOwner && GameMain.Client.ClientPeer.ServerContentPackages.Length == 0) { - if (GameMain.Client.ClientPeer.ServerContentPackages.Length == 0) - { - string errorMsg = $"Error in ModDownloadScreen: the list of mods the server has enabled was empty. Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}"; - GameAnalyticsManager.AddErrorEventOnce("ModDownloadScreen.Select:NoContentPackages", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - throw new InvalidOperationException(errorMsg); - } - if (GameMain.Client.ClientPeer.ServerContentPackages.None(p => p.CorePackage != null)) - { - string errorMsg = $"Error in ModDownloadScreen: no core packages in the list of mods the server has enabled. Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}"; - GameAnalyticsManager.AddErrorEventOnce("ModDownloadScreen.Select:NoCorePackage", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - throw new InvalidOperationException(errorMsg); - } + LogAndThrowException("Error in ModDownloadScreen: the list of mods the server has enabled was empty. " + +$"Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}", + analyticsId: "ModDownloadScreen.Select:NoContentPackages"); } var missingPackages = GameMain.Client.ClientPeer.ServerContentPackages @@ -96,11 +95,18 @@ namespace Barotrauma { if (!GameMain.Client.IsServerOwner) { + var corePackage = GameMain.Client.ClientPeer.ServerContentPackages + .Select(p => p.CorePackage) + .OfType().FirstOrDefault(); + if (corePackage is null) + { + LogAndThrowException($"Error in ModDownloadScreen: no core packages in the list of mods the server has enabled. " + + $"Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}", + analyticsId: "ModDownloadScreen.Select:NoCorePackage"); + } + ContentPackageManager.EnabledPackages.BackUp(); - ContentPackageManager.EnabledPackages.SetCore( - GameMain.Client.ClientPeer.ServerContentPackages - .Select(p => p.CorePackage) - .OfType().First()); + ContentPackageManager.EnabledPackages.SetCore(corePackage); List regularPackages = GameMain.Client.ClientPeer.ServerContentPackages .Select(p => p.RegularPackage) @@ -113,6 +119,15 @@ namespace Barotrauma return; } + if (missingPackages.FirstOrDefault(p => p.IsVanilla) is { } mismatchedVanilla) + { + LogAndThrowException("Error in ModDownloadScreen: mismatched Vanilla package: " + +$"local hash is {ContentPackageManager.VanillaCorePackage?.Hash.StringRepresentation ?? "[NULL]"}, " + +$"remote hash is {mismatchedVanilla.Hash.StringRepresentation}. " + +$"Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}", + analyticsId: "ModDownloadScreen.Select:MismatchedVanilla"); + } + GUIMessageBox msgBox = new GUIMessageBox( TextManager.Get("ModDownloadTitle"), "", @@ -291,14 +306,23 @@ namespace Barotrauma var serverPackages = GameMain.Client.ClientPeer.ServerContentPackages; CorePackage corePackage = downloadedPackages.FirstOrDefault(p => p is CorePackage) as CorePackage - ?? serverPackages.FirstOrDefault(p => p.CorePackage != null) - ?.CorePackage + ?? serverPackages.FirstOrDefault(p => p.CorePackage != null)?.CorePackage ?? throw new Exception($"Failed to find core package to enable"); List regularPackages = new List(); foreach (var p in serverPackages) { - if (p.CorePackage != null) { continue; } + if (p.CorePackage != null) + { + // This package is one of our installed core packages + continue; + } + + if (corePackage.Hash.Equals(p.Hash)) + { + // This package is the core package we downloaded from the server + continue; + } RegularPackage? matchingPackage = p.RegularPackage ?? downloadedPackages.FirstOrDefault(d => d is RegularPackage && d.Hash.Equals(p.Hash)) as RegularPackage; if (matchingPackage is null) @@ -355,9 +379,13 @@ namespace Barotrauma string dir = path.RemoveFromEnd(ModReceiver.Extension, StringComparison.OrdinalIgnoreCase); SaveUtil.DecompressToDirectory(path, dir, file => { }); - ContentPackage newPackage - = ContentPackage.TryLoad($"{dir}/{ContentPackage.FileListFileName}") - ?? throw new Exception($"Failed to load downloaded mod \"{currentDownload.Name}\""); + var result = ContentPackage.TryLoad(Path.Combine(dir, ContentPackage.FileListFileName)); + + if (!result.TryUnwrapSuccess(out var newPackage)) + { + throw new Exception($"Failed to load downloaded mod \"{currentDownload.Name}\"", + result.TryUnwrapFailure(out var exception) ? exception : null); + } if (!currentDownload.Hash.Equals(newPackage.Hash)) { throw new Exception($"Hash mismatch for downloaded mod \"{currentDownload.Name}\" (expected {currentDownload.Hash}, got {newPackage.Hash})"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index c7857e3e8..a144f5e1f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -2740,6 +2740,7 @@ namespace Barotrauma } public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { + if (backgroundSprite?.Texture == null) { return; } graphics.Clear(Color.Black); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite, Color.White); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index 04a5eb0a7..64a7204c0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -1352,6 +1352,10 @@ namespace Barotrauma private void AddToServerList(ServerInfo serverInfo, bool skipPing = false) { + if (serverInfo.PlayerCount > serverInfo.MaxPlayers) { return; } + if (serverInfo.PlayerCount < 0) { return; } + if (serverInfo.MaxPlayers <= 0) { return; } + RemoveMsgFromServerList(MsgUserData.RefreshingServerList); RemoveMsgFromServerList(MsgUserData.NoServers); var serverFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), serverList.Content.RectTransform) { MinSize = new Point(0, 35) }, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs index 3d4b328ee..2990f151f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs @@ -165,6 +165,7 @@ namespace Barotrauma void DrawOverlay(Sprite sprite, Color color) { + if (sprite.Texture == null) { return; } GUI.DrawBackgroundSprite(spriteBatch, sprite, color); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index a6e6dc77f..deaab5168 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -3086,11 +3086,17 @@ namespace Barotrauma XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus)); doc.SaveSafe(filePath); - - var resultPackage = ContentPackageManager.ReloadContentPackage(existingContentPackage) as RegularPackage; - if (!ContentPackageManager.EnabledPackages.Regular.Contains(resultPackage)) + + var result = ContentPackageManager.ReloadContentPackage(existingContentPackage); + if (!result.TryUnwrapSuccess(out var resultPackage)) { - ContentPackageManager.EnabledPackages.EnableRegular(resultPackage); + throw new Exception($"Failed to reload content package \"{existingContentPackage.Name}\"", + result.TryUnwrapFailure(out var exception) ? exception : null); + } + if (resultPackage is RegularPackage regularPackage + && !ContentPackageManager.EnabledPackages.Regular.Contains(regularPackage)) + { + ContentPackageManager.EnabledPackages.EnableRegular(regularPackage); GameSettings.SaveCurrentConfig(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index e073add2a..22271def5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -517,6 +517,11 @@ namespace Barotrauma if (musicDisposed) { Thread.Sleep(60); } } + + public static void ForceMusicUpdate() + { + updateMusicTimer = 0.0f; + } private static void UpdateMusic(float deltaTime) { @@ -544,7 +549,7 @@ namespace Barotrauma IEnumerable suitableMusic = GetSuitableMusicClips(currentMusicType, currentIntensity); int mainTrackIndex = 0; - if (suitableMusic.Count() == 0) + if (suitableMusic.None()) { targetMusic[mainTrackIndex] = null; } @@ -611,10 +616,17 @@ namespace Barotrauma targetMusic[typeAmbienceTrackIndex] = suitableTypeAmbiences.GetRandomUnsynced(); } + IEnumerable suitableIntensityMusic = Enumerable.Empty(); + if (targetMusic[mainTrackIndex] is { MuteIntensityTracks: false } mainTrack && Screen.Selected == GameMain.GameScreen) + { + float intensity = currentIntensity; + if (mainTrack?.ForceIntensityTrack != null) + { + intensity = mainTrack.ForceIntensityTrack.Value; + } + suitableIntensityMusic = GetSuitableMusicClips("intensity".ToIdentifier(), intensity); + } //get the appropriate intensity layers for current situation - IEnumerable suitableIntensityMusic = Screen.Selected == GameMain.GameScreen ? - GetSuitableMusicClips("intensity".ToIdentifier(), currentIntensity) : - Enumerable.Empty(); int intensityTrackStartIndex = 3; for (int i = intensityTrackStartIndex; i < MaxMusicChannels; i++) { @@ -744,6 +756,14 @@ namespace Barotrauma firstTimeInMainMenu = false; + if (GameMain.GameSession != null) + { + foreach (var mission in GameMain.GameSession.Missions) + { + var missionMusic = mission.GetOverrideMusicType(); + if (!missionMusic.IsEmpty) { return missionMusic; } + } + } if (Character.Controlled != null) { @@ -833,7 +853,7 @@ namespace Barotrauma { return "levelend".ToIdentifier(); } - if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0 && + if (GameMain.GameSession.RoundDuration > 120.0 && Level.Loaded?.Type == LevelData.LevelType.LocationConnection) { return "start".ToIdentifier(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs index 14b0b68e2..bb3af5eee 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs @@ -238,17 +238,24 @@ namespace Barotrauma public readonly float Volume; public readonly Vector2 IntensityRange; + public readonly bool MuteIntensityTracks; + public readonly float? ForceIntensityTrack; public readonly bool ContinueFromPreviousTime; public int PreviousTime; public BackgroundMusic(ContentXElement element, SoundsFile file) : base(element, file, stream: true) { - Type = element.GetAttributeIdentifier("type", ""); - IntensityRange = element.GetAttributeVector2("intensityrange", new Vector2(0.0f, 100.0f)); - DuckVolume = element.GetAttributeBool("duckvolume", false); - this.Volume = element.GetAttributeFloat("volume", 1.0f); - ContinueFromPreviousTime = element.GetAttributeBool("continuefromprevioustime", false); + Type = element.GetAttributeIdentifier(nameof(Type), ""); + IntensityRange = element.GetAttributeVector2(nameof(IntensityRange), new Vector2(0.0f, 100.0f)); + DuckVolume = element.GetAttributeBool(nameof(DuckVolume), false); + MuteIntensityTracks = element.GetAttributeBool(nameof(MuteIntensityTracks), false); + if (element.GetAttribute(nameof(ForceIntensityTrack)) != null) + { + ForceIntensityTrack = element.GetAttributeFloat(nameof(ForceIntensityTrack), 0.0f); + } + Volume = element.GetAttributeFloat(nameof(Volume), 1.0f); + ContinueFromPreviousTime = element.GetAttributeBool(nameof(ContinueFromPreviousTime), false); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs index 66463fe83..26b32f26c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs @@ -73,7 +73,13 @@ namespace Barotrauma.Steam //This callback seems to take place when the item has been downloaded recently and an update //or a redownload has taken place - Steamworks.SteamUGC.OnDownloadItemResult += (result, id) => Workshop.OnItemDownloadComplete(id); + Steamworks.SteamUGC.OnDownloadItemResult += (result, id) => + { + if (result == Steamworks.Result.OK) + { + Workshop.OnItemDownloadComplete(id); + } + }; //Maybe I'm completely wrong! All I know is that we need to handle both! } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index 370b47f3f..124e77d1d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -177,7 +177,13 @@ namespace Barotrauma.Steam await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, PublishStagingDir, ShouldCorrectPaths.No); var stagingFileListPath = Path.Combine(PublishStagingDir, ContentPackage.FileListFileName); - ContentPackage tempPkg = ContentPackage.TryLoad(stagingFileListPath) ?? throw new Exception("Staging copy could not be loaded"); + + var result = ContentPackage.TryLoad(stagingFileListPath); + if (!result.TryUnwrapSuccess(out var tempPkg)) + { + throw new Exception("Staging copy could not be loaded", + result.TryUnwrapFailure(out var exception) ? exception : null); + } //Load filelist.xml and write the hash into it so anyone downloading this mod knows what it should be ModProject modProject = new ModProject(tempPkg) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs index 7cc29acce..8c43da578 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs @@ -40,7 +40,7 @@ namespace Barotrauma.Steam CanBeFocused = false, UserData = p }; - if (p.Errors.Any()) + if (p.FatalLoadErrors.Any()) { CreateModErrorInfo(p, regularBox, regularBox); regularBox.CanBeFocused = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs index afb6f8bd3..4331f34bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs @@ -49,21 +49,10 @@ namespace Barotrauma.Steam SteamManager.Workshop.DownloadModThenEnqueueInstall(item); } } - - TaskPool.Add("RemoveUnsubscribedItems", SteamManager.Workshop.GetPublishedItems(), t => + + SteamManager.Workshop.DeleteUnsubscribedMods(removedPackages => { - if (!t.TryGetResult(out ISet publishedItems)) { return; } - - var allRequiredInstalled = subscribedIds.Union(publishedItems.Select(it => it.Id)).ToHashSet(); - bool needsRefresh = false; - foreach (var id in installedIds.Where(id2 => !allRequiredInstalled.Contains(id2))) - { - Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id); - SteamManager.Workshop.Uninstall(item); - needsRefresh = true; - } - - if (needsRefresh) + if (removedPackages.Any()) { PopulateInstalledModLists(); } @@ -487,8 +476,9 @@ namespace Barotrauma.Steam { string str = modsListFilter.Text; enabledRegularModsList.Content.Children.Concat(disabledRegularModsList.Content.Children) - .ForEach(c => c.Visible = c.UserData is not ContentPackage p - || ModNameMatches(p, str) && ModMatchesTickboxes(p, c)); + .ForEach(c + => c.Visible = c.UserData is not ContentPackage p + || ModNameMatches(p, str) && ModMatchesTickboxes(p, c)); } private bool ModMatchesTickboxes(ContentPackage p, GUIComponent guiItem) @@ -550,7 +540,20 @@ namespace Barotrauma.Steam (p) => p.Name, ContentPackageManager.CorePackages.ToArray(), ContentPackageManager.EnabledPackages.Core!, - (p) => { }); + (p) => + { + enabledCoreDropdown.ButtonTextColor = + p.HasAnyErrors + ? GUIStyle.Red + : GUIStyle.TextColorNormal; + }); + enabledCoreDropdown.ListBox.Content.Children + .OfType() + .ForEach(tb => + CreateModErrorInfo( + (tb.UserData as ContentPackage)!, + tb, + tb)); void addRegularModToList(RegularPackage mod, GUIListBox list) { @@ -658,10 +661,7 @@ namespace Barotrauma.Steam { CanBeFocused = false }; - if (mod.Errors.Any()) - { - CreateModErrorInfo(mod, modFrame, modName); - } + CreateModErrorInfo(mod, modFrame, modName); if (ContentPackageManager.LocalPackages.Contains(mod)) { var editButton = new GUIButton(new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs index 3193aa9ef..b69efd9b9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs @@ -165,6 +165,10 @@ namespace Barotrauma.Steam .Select(c => c.UserData as RegularPackage).OfType().ToArray()); PopulateInstalledModLists(forceRefreshEnabled: true, refreshDisabled: true); ContentPackageManager.LogEnabledRegularPackageErrors(); + enabledCoreDropdown.ButtonTextColor = + EnabledCorePackage.HasAnyErrors + ? GUIStyle.Red + : GUIStyle.TextColorNormal; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs index 09bdaafe2..c3d167fed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs @@ -294,10 +294,11 @@ namespace Barotrauma.Steam { //Reload the package to force hash recalculation string packageName = localPackage.Name; - localPackage = ContentPackageManager.ReloadContentPackage(localPackage); - if (localPackage is null) + var result = ContentPackageManager.ReloadContentPackage(localPackage); + if (!result.TryUnwrapSuccess(out localPackage)) { - throw new Exception($"\"{packageName}\" was removed upon reload"); + throw new Exception($"\"{packageName}\" was removed upon reload", + result.TryUnwrapFailure(out var exception) ? exception : null); } //Set up the Ugc.Editor object that we'll need to publish diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs index cbe5fca4d..d99e8887b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs @@ -133,20 +133,29 @@ namespace Barotrauma.Steam return searchBox; } - protected void CreateModErrorInfo(ContentPackage mod, GUIComponent uiElement, GUITextBlock nameText) + protected static void CreateModErrorInfo(ContentPackage mod, GUIComponent uiElement, GUITextBlock nameText) { - if (mod.Errors.Any()) + uiElement.ToolTip = ""; + if (mod.FatalLoadErrors.Any()) { const int maxErrorsToShow = 5; nameText.TextColor = GUIStyle.Red; uiElement.ToolTip = - TextManager.GetWithVariable("contentpackagehaserrors", "[packagename]", mod.Name) - + '\n' + string.Join('\n', mod.Errors.Take(maxErrorsToShow).Select(e => e.Message)); - if (mod.Errors.Count() > maxErrorsToShow) + TextManager.GetWithVariable("ContentPackageHasFatalErrors", "[packagename]", mod.Name) + + '\n' + string.Join('\n', mod.FatalLoadErrors.Take(maxErrorsToShow).Select(e => e.Message)); + if (mod.FatalLoadErrors.Length > maxErrorsToShow) { - uiElement.ToolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (mod.Errors.Count() - maxErrorsToShow).ToString()); + uiElement.ToolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (mod.FatalLoadErrors.Count() - maxErrorsToShow).ToString()); } } + + if (mod.EnableError.IsSome()) + { + nameText.TextColor = GUIStyle.Red; + if (!uiElement.ToolTip.IsNullOrWhiteSpace()) { uiElement.ToolTip += "\n"; } + uiElement.ToolTip += TextManager.GetWithVariable( + "ContentPackageEnableError", "[packagename]", mod.Name); + } } } } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index de0c2af3d..c25074c41 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 100.5.0.0 + 100.6.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 1773db410..4a5f0a4fc 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 100.5.0.0 + 100.6.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 1ff090a6a..10786bca9 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 100.5.0.0 + 100.6.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index c4c68816f..80715d130 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 100.5.0.0 + 100.6.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 6ef931d67..b1bcb91ea 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 100.5.0.0 + 100.6.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 8a669f03e..086c12192 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -82,54 +82,50 @@ namespace Barotrauma } //dequeue messages - lock (queuedMessages) + if (queuedMessages.Count > 0) { - if (queuedMessages.Count > 0) + + if (!Console.IsOutputRedirected) { - - if (!Console.IsOutputRedirected) + Console.CursorLeft = 0; + } + while (queuedMessages.TryDequeue(out var msg)) + { + Messages.Add(msg); + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) { - Console.CursorLeft = 0; - } - while (queuedMessages.Count > 0) - { - ColoredText msg = queuedMessages.Dequeue(); - Messages.Add(msg); - if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) + unsavedMessages.Add(msg); + if (unsavedMessages.Count >= messagesPerFile) { - unsavedMessages.Add(msg); - if (unsavedMessages.Count >= messagesPerFile) - { - SaveLogs(); - unsavedMessages.Clear(); - } + SaveLogs(); + unsavedMessages.Clear(); } - - string msgTxt = msg.Text; - - if (msg.IsCommand) commandMemory.Add(msgTxt); - - if(!Console.IsOutputRedirected) - { - int paddingLen = consoleWidth - (msg.Text.Length % consoleWidth) - 1; - msgTxt += new string(' ', paddingLen > 0 ? paddingLen : 0); - - Console.ForegroundColor = XnaToConsoleColor.Convert(msg.Color); - } - Console.WriteLine(msgTxt); - - if (sw.ElapsedMilliseconds >= maxTime) { break; } } + + string msgTxt = msg.Text; + + if (msg.IsCommand) commandMemory.Add(msgTxt); + if(!Console.IsOutputRedirected) { - RewriteInputToCommandLine(input); + int paddingLen = consoleWidth - (msg.Text.Length % consoleWidth) - 1; + msgTxt += new string(' ', paddingLen > 0 ? paddingLen : 0); + + Console.ForegroundColor = XnaToConsoleColor.Convert(msg.Color); } + Console.WriteLine(msgTxt); + + if (sw.ElapsedMilliseconds >= maxTime) { break; } } - if (Messages.Count > MaxMessages) + if (!Console.IsOutputRedirected) { - Messages.RemoveRange(0, Messages.Count - MaxMessages); + RewriteInputToCommandLine(input); } } + if (Messages.Count > MaxMessages) + { + Messages.RemoveRange(0, Messages.Count - MaxMessages); + } // No good way to display input when console output is redirected, and can't read from redirected input using KeyAvailable. if(!Console.IsOutputRedirected && !Console.IsInputRedirected) @@ -272,26 +268,22 @@ namespace Barotrauma public static void Clear() { - lock (queuedMessages) + while (queuedMessages.TryDequeue(out var msg)) { - while (queuedMessages.Count > 0) + Messages.Add(msg); + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) { - var msg = queuedMessages.Dequeue(); - Messages.Add(msg); - if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) + unsavedMessages.Add(msg); + if (unsavedMessages.Count >= messagesPerFile) { - unsavedMessages.Add(msg); - if (unsavedMessages.Count >= messagesPerFile) - { - SaveLogs(); - unsavedMessages.Clear(); - } + SaveLogs(); + unsavedMessages.Clear(); } } - if (Messages.Count > MaxMessages) - { - Messages.RemoveRange(0, Messages.Count - MaxMessages); - } + } + if (Messages.Count > MaxMessages) + { + Messages.RemoveRange(0, Messages.Count - MaxMessages); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 05e6c6f4e..40f8f9f24 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -162,13 +162,13 @@ namespace Barotrauma } else { - name = doc.Root.GetAttributeString("name", "Server"); - port = doc.Root.GetAttributeInt("port", NetConfig.DefaultPort); - queryPort = doc.Root.GetAttributeInt("queryport", NetConfig.DefaultQueryPort); - publiclyVisible = doc.Root.GetAttributeBool("public", false); + name = doc.Root.GetAttributeString(nameof(ServerSettings.Name), "Server"); + port = doc.Root.GetAttributeInt(nameof(ServerSettings.Port), NetConfig.DefaultPort); + queryPort = doc.Root.GetAttributeInt(nameof(ServerSettings.QueryPort), NetConfig.DefaultQueryPort); + publiclyVisible = doc.Root.GetAttributeBool(nameof(ServerSettings.IsPublic), false); + enableUpnp = doc.Root.GetAttributeBool(nameof(ServerSettings.EnableUPnP), false); + maxPlayers = doc.Root.GetAttributeInt(nameof(ServerSettings.MaxPlayers), 10); password = doc.Root.GetAttributeString("password", ""); - enableUpnp = doc.Root.GetAttributeBool("enableupnp", false); - maxPlayers = doc.Root.GetAttributeInt("maxplayers", 10); ownerKey = Option.None(); } @@ -361,7 +361,7 @@ namespace Barotrauma if (prevUpdateRates.Count >= 10) { int avgUpdateRate = (int)prevUpdateRates.Average(); - if (avgUpdateRate < Timing.FixedUpdateRate * 0.98 && GameSession != null && Timing.TotalTime > GameSession.RoundStartTime + 1.0) + if (avgUpdateRate < Timing.FixedUpdateRate * 0.98 && GameSession != null && GameSession.RoundDuration > 1.0) { DebugConsole.AddWarning($"Running slowly ({avgUpdateRate} updates/s)!"); if (Server != null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 017d68101..e71800251 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -335,7 +335,7 @@ namespace Barotrauma break; } - Map.ProgressWorld(this, transitionType, (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime)); + Map.ProgressWorld(this, transitionType, GameMain.GameSession.RoundDuration); bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); if (success) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index 07748966a..c285765e3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -200,9 +200,9 @@ namespace Barotrauma.Networking return length; } - public virtual void ServerWrite(IWriteMessage msg, Client c) + public virtual void ServerWrite(in SegmentTableWriter segmentTable, IWriteMessage msg, Client c) { - msg.WriteByte((byte)ServerNetObject.CHAT_MESSAGE); + segmentTable.StartNewSegment(ServerNetSegment.ChatMessage); msg.WriteUInt16(NetStateID); msg.WriteRangedInteger((int)Type, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); msg.WriteByte((byte)ChangeType); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 666cd3039..a1c8e5e02 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -1028,12 +1028,11 @@ namespace Barotrauma.Networking return; } - ClientNetObject objHeader; - while ((objHeader = (ClientNetObject)inc.ReadByte()) != ClientNetObject.END_OF_MESSAGE) + SegmentTableReader.Read(inc, (segment, inc) => { - switch (objHeader) + switch (segment) { - case ClientNetObject.SYNC_IDS: + case ClientNetSegment.SyncIds: //TODO: might want to use a clever class for this c.LastRecvLobbyUpdate = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvLobbyUpdate, GameMain.NetLobbyScreen.LastUpdateID); if (c.HasPermission(ClientPermissions.ManageSettings) && @@ -1072,19 +1071,21 @@ namespace Barotrauma.Networking } } break; - case ClientNetObject.CHAT_MESSAGE: + case ClientNetSegment.ChatMessage: ChatMessage.ServerRead(inc, c); break; - case ClientNetObject.VOTE: + case ClientNetSegment.Vote: Voting.ServerRead(inc, c); break; default: - return; + return SegmentTableReader.BreakSegmentReading.Yes; } //don't read further messages if the client has been disconnected (kicked due to spam for example) - if (!connectedClients.Contains(c)) break; - } + return connectedClients.Contains(c) + ? SegmentTableReader.BreakSegmentReading.No + : SegmentTableReader.BreakSegmentReading.Yes; + }); } private void ClientReadIngame(IReadMessage inc) @@ -1109,13 +1110,12 @@ namespace Barotrauma.Networking } } - ClientNetObject objHeader; - while ((objHeader = (ClientNetObject)inc.ReadByte()) != ClientNetObject.END_OF_MESSAGE) + SegmentTableReader.Read(inc, (segment, inc) => { - switch (objHeader) + switch (segment) { - case ClientNetObject.SYNC_IDS: - //TODO: might want to use a clever class for this + case ClientNetSegment.SyncIds: + //TODO: switch this to INetSerializableStruct UInt16 lastRecvChatMsgID = inc.ReadUInt16(); UInt16 lastRecvEntityEventID = inc.ReadUInt16(); @@ -1218,10 +1218,10 @@ namespace Barotrauma.Networking } break; - case ClientNetObject.CHAT_MESSAGE: + case ClientNetSegment.ChatMessage: ChatMessage.ServerRead(inc, c); break; - case ClientNetObject.CHARACTER_INPUT: + case ClientNetSegment.CharacterInput: if (c.Character != null) { c.Character.ServerReadInput(inc, c); @@ -1231,22 +1231,24 @@ namespace Barotrauma.Networking DebugConsole.AddWarning($"Received character inputs from a client who's not controlling a character ({c.Name})."); } break; - case ClientNetObject.ENTITY_STATE: + case ClientNetSegment.EntityState: entityEventManager.Read(inc, c); break; - case ClientNetObject.VOTE: + case ClientNetSegment.Vote: Voting.ServerRead(inc, c); break; - case ClientNetObject.SPECTATING_POS: + case ClientNetSegment.SpectatingPos: c.SpectatePos = new Vector2(inc.ReadSingle(), inc.ReadSingle()); break; default: - return; + return SegmentTableReader.BreakSegmentReading.Yes; } //don't read further messages if the client has been disconnected (kicked due to spam for example) - if (!connectedClients.Contains(c)) { break; } - } + return connectedClients.Contains(c) + ? SegmentTableReader.BreakSegmentReading.No + : SegmentTableReader.BreakSegmentReading.Yes; + }); } private void ReadCrewMessage(IReadMessage inc, Client sender) @@ -1701,78 +1703,78 @@ namespace Barotrauma.Networking IWriteMessage outmsg = new WriteOnlyMessage(); outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME); - outmsg.WriteSingle((float)NetTime.Now); - outmsg.WriteByte((byte)ServerNetObject.SYNC_IDS); - outmsg.WriteUInt16(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server - outmsg.WriteUInt16(c.LastSentEntityEventID); - - if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) + using (var segmentTable = SegmentTableWriter.StartWriting(outmsg)) { - outmsg.WriteBoolean(true); - outmsg.WritePadBits(); - campaign.ServerWrite(outmsg, c); - } - else - { - outmsg.WriteBoolean(false); - outmsg.WritePadBits(); - } + segmentTable.StartNewSegment(ServerNetSegment.SyncIds); + outmsg.WriteUInt16(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server + outmsg.WriteUInt16(c.LastSentEntityEventID); - int clientListBytes = outmsg.LengthBytes; - WriteClientList(c, outmsg); - clientListBytes = outmsg.LengthBytes - clientListBytes; - - int chatMessageBytes = outmsg.LengthBytes; - WriteChatMessages(outmsg, c); - chatMessageBytes = outmsg.LengthBytes - chatMessageBytes; - - //write as many position updates as the message can fit (only after midround syncing is done) - int positionUpdateBytes = outmsg.LengthBytes; - while (!c.NeedsMidRoundSync && c.PendingPositionUpdates.Count > 0) - { - var entity = c.PendingPositionUpdates.Peek(); - if (!(entity is IServerPositionSync entityPositionSync) || - entity.Removed || - (entity is Item item && float.IsInfinity(item.PositionUpdateInterval))) + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) { + outmsg.WriteBoolean(true); + outmsg.WritePadBits(); + campaign.ServerWrite(outmsg, c); + } + else + { + outmsg.WriteBoolean(false); + outmsg.WritePadBits(); + } + + int clientListBytes = outmsg.LengthBytes; + WriteClientList(segmentTable, c, outmsg); + clientListBytes = outmsg.LengthBytes - clientListBytes; + + int chatMessageBytes = outmsg.LengthBytes; + WriteChatMessages(segmentTable, outmsg, c); + chatMessageBytes = outmsg.LengthBytes - chatMessageBytes; + + //write as many position updates as the message can fit (only after midround syncing is done) + int positionUpdateBytes = outmsg.LengthBytes; + while (!c.NeedsMidRoundSync && c.PendingPositionUpdates.Count > 0) + { + var entity = c.PendingPositionUpdates.Peek(); + if (!(entity is IServerPositionSync entityPositionSync) || + entity.Removed || + (entity is Item item && float.IsInfinity(item.PositionUpdateInterval))) + { + c.PendingPositionUpdates.Dequeue(); + continue; + } + + IWriteMessage tempBuffer = new ReadWriteMessage(); + tempBuffer.WriteBoolean(entity is Item); tempBuffer.WritePadBits(); + tempBuffer.WriteUInt32(entity is MapEntity me ? me.Prefab.UintIdentifier : (UInt32)0); + entityPositionSync.ServerWritePosition(tempBuffer, c); + + //no more room in this packet + if (outmsg.LengthBytes + tempBuffer.LengthBytes > MsgConstants.MTU - 100) + { + break; + } + + segmentTable.StartNewSegment(ServerNetSegment.EntityPosition); + outmsg.WritePadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly + outmsg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); + outmsg.WritePadBits(); + + c.PositionUpdateLastSent[entity] = (float)NetTime.Now; c.PendingPositionUpdates.Dequeue(); - continue; } + positionUpdateBytes = outmsg.LengthBytes - positionUpdateBytes; - IWriteMessage tempBuffer = new ReadWriteMessage(); - tempBuffer.WriteBoolean(entity is Item); tempBuffer.WritePadBits(); - tempBuffer.WriteUInt32(entity is MapEntity me ? me.Prefab.UintIdentifier : (UInt32)0); - entityPositionSync.ServerWritePosition(tempBuffer, c); - - //no more room in this packet - if (outmsg.LengthBytes + tempBuffer.LengthBytes > MsgConstants.MTU - 100) + if (outmsg.LengthBytes > MsgConstants.MTU) { - break; + string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n"; + errorMsg += + " Client list size: " + clientListBytes + " bytes\n" + + " Chat message size: " + chatMessageBytes + " bytes\n" + + " Position update size: " + positionUpdateBytes + " bytes\n\n"; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } - - outmsg.WriteByte((byte)ServerNetObject.ENTITY_POSITION); - outmsg.WritePadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly - outmsg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); - outmsg.WritePadBits(); - - c.PositionUpdateLastSent[entity] = (float)NetTime.Now; - c.PendingPositionUpdates.Dequeue(); - } - positionUpdateBytes = outmsg.LengthBytes - positionUpdateBytes; - - outmsg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); - - if (outmsg.LengthBytes > MsgConstants.MTU) - { - string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n"; - errorMsg += - " Client list size: " + clientListBytes + " bytes\n" + - " Chat message size: " + chatMessageBytes + " bytes\n" + - " Position update size: " + positionUpdateBytes + " bytes\n\n"; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable); @@ -1785,46 +1787,50 @@ namespace Barotrauma.Networking outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME); outmsg.WriteSingle((float)Lidgren.Network.NetTime.Now); - int eventManagerBytes = outmsg.LengthBytes; - entityEventManager.Write(c, outmsg, out List sentEvents); - eventManagerBytes = outmsg.LengthBytes - eventManagerBytes; - - if (sentEvents.Count == 0) + using (var segmentTable = SegmentTableWriter.StartWriting(outmsg)) { - break; - } + int eventManagerBytes = outmsg.LengthBytes; + entityEventManager.Write(segmentTable, c, outmsg, out List sentEvents); + eventManagerBytes = outmsg.LengthBytes - eventManagerBytes; - outmsg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); - - if (outmsg.LengthBytes > MsgConstants.MTU) - { - string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n"; - errorMsg += - " Event size: " + eventManagerBytes + " bytes\n"; - - if (sentEvents != null && sentEvents.Count > 0) + if (sentEvents.Count == 0) { - errorMsg += "Sent events: \n"; - foreach (var entityEvent in sentEvents) - { - errorMsg += " - " + (entityEvent.Entity?.ToString() ?? "null") + "\n"; - } + break; } - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame2:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + if (outmsg.LengthBytes > MsgConstants.MTU) + { + string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + + MsgConstants.MTU + ")\n"; + errorMsg += + " Event size: " + eventManagerBytes + " bytes\n"; + + if (sentEvents != null && sentEvents.Count > 0) + { + errorMsg += "Sent events: \n"; + foreach (var entityEvent in sentEvents) + { + errorMsg += " - " + (entityEvent.Entity?.ToString() ?? "null") + "\n"; + } + } + + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce( + "GameServer.ClientWriteIngame2:PacketSizeExceeded" + outmsg.LengthBytes, + GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + } } serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable); } } - private void WriteClientList(Client c, IWriteMessage outmsg) + private void WriteClientList(in SegmentTableWriter segmentTable, Client c, IWriteMessage outmsg) { bool hasChanged = NetIdUtils.IdMoreRecent(LastClientListUpdateID, c.LastRecvClientListUpdate); if (!hasChanged) { return; } - outmsg.WriteByte((byte)ServerNetObject.CLIENT_LIST); + segmentTable.StartNewSegment(ServerNetSegment.ClientList); outmsg.WriteUInt16(LastClientListUpdateID); outmsg.WriteByte((byte)connectedClients.Count); @@ -1861,133 +1867,135 @@ namespace Barotrauma.Networking IWriteMessage outmsg = new WriteOnlyMessage(); outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_LOBBY); - outmsg.WriteByte((byte)ServerNetObject.SYNC_IDS); - - int settingsBytes = outmsg.LengthBytes; - int initialUpdateBytes = 0; - - if (ServerSettings.UnsentFlags() != ServerSettings.NetFlags.None) + bool messageTooLarge; + using (var segmentTable = SegmentTableWriter.StartWriting(outmsg)) { - GameMain.NetLobbyScreen.LastUpdateID++; - } - - IWriteMessage settingsBuf = null; - if (NetIdUtils.IdMoreRecent(GameMain.NetLobbyScreen.LastUpdateID, c.LastRecvLobbyUpdate)) - { - outmsg.WriteBoolean(true); - outmsg.WritePadBits(); + segmentTable.StartNewSegment(ServerNetSegment.SyncIds); - outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID); + int settingsBytes = outmsg.LengthBytes; + int initialUpdateBytes = 0; - settingsBuf = new ReadWriteMessage(); - ServerSettings.ServerWrite(settingsBuf, c); - outmsg.WriteUInt16((UInt16)settingsBuf.LengthBytes); - outmsg.WriteBytes(settingsBuf.Buffer, 0, settingsBuf.LengthBytes); - - outmsg.WriteBoolean(c.LastRecvLobbyUpdate < 1); - if (c.LastRecvLobbyUpdate < 1) + if (ServerSettings.UnsentFlags() != ServerSettings.NetFlags.None) { - isInitialUpdate = true; - initialUpdateBytes = outmsg.LengthBytes; - ClientWriteInitial(c, outmsg); - initialUpdateBytes = outmsg.LengthBytes - initialUpdateBytes; + GameMain.NetLobbyScreen.LastUpdateID++; } - outmsg.WriteString(GameMain.NetLobbyScreen.SelectedSub.Name); - outmsg.WriteString(GameMain.NetLobbyScreen.SelectedSub.MD5Hash.ToString()); - outmsg.WriteBoolean(IsUsingRespawnShuttle()); - var selectedShuttle = GameStarted && RespawnManager != null && RespawnManager.UsingShuttle ? - RespawnManager.RespawnShuttle.Info : - GameMain.NetLobbyScreen.SelectedShuttle; - outmsg.WriteString(selectedShuttle.Name); - outmsg.WriteString(selectedShuttle.MD5Hash.ToString()); - outmsg.WriteBoolean(ServerSettings.AllowSubVoting); - outmsg.WriteBoolean(ServerSettings.AllowModeVoting); - - outmsg.WriteBoolean(ServerSettings.VoiceChatEnabled); - - outmsg.WriteBoolean(ServerSettings.AllowSpectating); - - outmsg.WriteRangedInteger((int)ServerSettings.TraitorsEnabled, 0, 2); - - outmsg.WriteRangedInteger((int)GameMain.NetLobbyScreen.MissionType, 0, (int)MissionType.All); - - outmsg.WriteByte((byte)GameMain.NetLobbyScreen.SelectedModeIndex); - outmsg.WriteString(GameMain.NetLobbyScreen.LevelSeed); - outmsg.WriteSingle(ServerSettings.SelectedLevelDifficulty); - - outmsg.WriteByte((byte)ServerSettings.BotCount); - outmsg.WriteBoolean(ServerSettings.BotSpawnMode == BotSpawnMode.Fill); - - outmsg.WriteBoolean(ServerSettings.AutoRestart); - if (ServerSettings.AutoRestart) + IWriteMessage settingsBuf = null; + if (NetIdUtils.IdMoreRecent(GameMain.NetLobbyScreen.LastUpdateID, c.LastRecvLobbyUpdate)) { - outmsg.WriteSingle(autoRestartTimerRunning ? ServerSettings.AutoRestartTimer : 0.0f); + outmsg.WriteBoolean(true); + outmsg.WritePadBits(); + + outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID); + + settingsBuf = new ReadWriteMessage(); + ServerSettings.ServerWrite(settingsBuf, c); + outmsg.WriteUInt16((UInt16)settingsBuf.LengthBytes); + outmsg.WriteBytes(settingsBuf.Buffer, 0, settingsBuf.LengthBytes); + + outmsg.WriteBoolean(c.LastRecvLobbyUpdate < 1); + if (c.LastRecvLobbyUpdate < 1) + { + isInitialUpdate = true; + initialUpdateBytes = outmsg.LengthBytes; + ClientWriteInitial(c, outmsg); + initialUpdateBytes = outmsg.LengthBytes - initialUpdateBytes; + } + outmsg.WriteString(GameMain.NetLobbyScreen.SelectedSub.Name); + outmsg.WriteString(GameMain.NetLobbyScreen.SelectedSub.MD5Hash.ToString()); + outmsg.WriteBoolean(IsUsingRespawnShuttle()); + var selectedShuttle = GameStarted && RespawnManager != null && RespawnManager.UsingShuttle ? + RespawnManager.RespawnShuttle.Info : + GameMain.NetLobbyScreen.SelectedShuttle; + outmsg.WriteString(selectedShuttle.Name); + outmsg.WriteString(selectedShuttle.MD5Hash.ToString()); + + outmsg.WriteBoolean(ServerSettings.AllowSubVoting); + outmsg.WriteBoolean(ServerSettings.AllowModeVoting); + + outmsg.WriteBoolean(ServerSettings.VoiceChatEnabled); + + outmsg.WriteBoolean(ServerSettings.AllowSpectating); + + outmsg.WriteRangedInteger((int)ServerSettings.TraitorsEnabled, 0, 2); + + outmsg.WriteRangedInteger((int)GameMain.NetLobbyScreen.MissionType, 0, (int)MissionType.All); + + outmsg.WriteByte((byte)GameMain.NetLobbyScreen.SelectedModeIndex); + outmsg.WriteString(GameMain.NetLobbyScreen.LevelSeed); + outmsg.WriteSingle(ServerSettings.SelectedLevelDifficulty); + + outmsg.WriteByte((byte)ServerSettings.BotCount); + outmsg.WriteBoolean(ServerSettings.BotSpawnMode == BotSpawnMode.Fill); + + outmsg.WriteBoolean(ServerSettings.AutoRestart); + if (ServerSettings.AutoRestart) + { + outmsg.WriteSingle(autoRestartTimerRunning ? ServerSettings.AutoRestartTimer : 0.0f); + } } - } - else - { - outmsg.WriteBoolean(false); - outmsg.WritePadBits(); - } - settingsBytes = outmsg.LengthBytes - settingsBytes; - - int campaignBytes = outmsg.LengthBytes; - var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; - if (outmsg.LengthBytes < MsgConstants.MTU - 500 && - campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) - { - outmsg.WriteBoolean(true); - outmsg.WritePadBits(); - campaign.ServerWrite(outmsg, c); - } - else - { - outmsg.WriteBoolean(false); - outmsg.WritePadBits(); - } - campaignBytes = outmsg.LengthBytes - campaignBytes; - - outmsg.WriteUInt16(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server - - int clientListBytes = outmsg.LengthBytes; - if (outmsg.LengthBytes < MsgConstants.MTU - 500) - { - WriteClientList(c, outmsg); - } - clientListBytes = outmsg.LengthBytes - clientListBytes; - - int chatMessageBytes = outmsg.LengthBytes; - WriteChatMessages(outmsg, c); - chatMessageBytes = outmsg.LengthBytes - chatMessageBytes; - - outmsg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); - - bool messageTooLarge = outmsg.LengthBytes > MsgConstants.MTU; - if (messageTooLarge && !isInitialUpdate) - { - string warningMsg = "Maximum packet size exceeded, will send using reliable mode (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n"; - warningMsg += - " Client list size: " + clientListBytes + " bytes\n" + - " Chat message size: " + chatMessageBytes + " bytes\n" + - " Campaign size: " + campaignBytes + " bytes\n" + - " Settings size: " + settingsBytes + " bytes\n"; - if (initialUpdateBytes > 0) + else { + outmsg.WriteBoolean(false); + outmsg.WritePadBits(); + } + settingsBytes = outmsg.LengthBytes - settingsBytes; + + int campaignBytes = outmsg.LengthBytes; + var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; + if (outmsg.LengthBytes < MsgConstants.MTU - 500 && + campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) + { + outmsg.WriteBoolean(true); + outmsg.WritePadBits(); + campaign.ServerWrite(outmsg, c); + } + else + { + outmsg.WriteBoolean(false); + outmsg.WritePadBits(); + } + campaignBytes = outmsg.LengthBytes - campaignBytes; + + outmsg.WriteUInt16(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server + + int clientListBytes = outmsg.LengthBytes; + if (outmsg.LengthBytes < MsgConstants.MTU - 500) + { + WriteClientList(segmentTable, c, outmsg); + } + clientListBytes = outmsg.LengthBytes - clientListBytes; + + int chatMessageBytes = outmsg.LengthBytes; + WriteChatMessages(segmentTable, outmsg, c); + chatMessageBytes = outmsg.LengthBytes - chatMessageBytes; + + messageTooLarge = outmsg.LengthBytes > MsgConstants.MTU; + if (messageTooLarge && !isInitialUpdate) + { + string warningMsg = "Maximum packet size exceeded, will send using reliable mode (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n"; warningMsg += - " Initial update size: " + settingsBuf.LengthBytes + " bytes\n"; - } - if (settingsBuf != null) - { - warningMsg += - " Settings buffer size: " + settingsBuf.LengthBytes + " bytes\n"; - } + " Client list size: " + clientListBytes + " bytes\n" + + " Chat message size: " + chatMessageBytes + " bytes\n" + + " Campaign size: " + campaignBytes + " bytes\n" + + " Settings size: " + settingsBytes + " bytes\n"; + if (initialUpdateBytes > 0) + { + warningMsg += + " Initial update size: " + initialUpdateBytes + " bytes\n"; + } + if (settingsBuf != null) + { + warningMsg += + " Settings buffer size: " + settingsBuf.LengthBytes + " bytes\n"; + } #if DEBUG || UNSTABLE - DebugConsole.ThrowError(warningMsg); + DebugConsole.ThrowError(warningMsg); #else - if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.AddWarning(warningMsg); } + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.AddWarning(warningMsg); } #endif - GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:ClientWriteLobby" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); + GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:ClientWriteLobby" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); + } } if (isInitialUpdate || messageTooLarge) @@ -2014,7 +2022,7 @@ namespace Barotrauma.Networking } } - private void WriteChatMessages(IWriteMessage outmsg, Client c) + private void WriteChatMessages(in SegmentTableWriter segmentTable, IWriteMessage outmsg, Client c) { c.ChatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, c.LastRecvChatMsgID)); for (int i = 0; i < c.ChatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) @@ -2024,7 +2032,7 @@ namespace Barotrauma.Networking //not enough room in this packet return; } - c.ChatMsgQueue[i].ServerWrite(outmsg, c); + c.ChatMsgQueue[i].ServerWrite(segmentTable, outmsg, c); } } @@ -2417,7 +2425,6 @@ namespace Barotrauma.Networking spawnedCharacter.Info.InventoryData = new XElement("inventory"); spawnedCharacter.Info.StartItemsGiven = true; spawnedCharacter.SaveInventory(); - // talents are only avilable for players in online sessions, but modders or someone else might want to have them loaded anyway spawnedCharacter.LoadTalents(); } } @@ -3327,9 +3334,11 @@ namespace Barotrauma.Networking IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ServerPacketHeader.UPDATE_LOBBY); - msg.WriteByte((byte)ServerNetObject.VOTE); - Voting.ServerWrite(msg); - msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); + using (var segmentTable = SegmentTableWriter.StartWriting(msg)) + { + segmentTable.StartNewSegment(ServerNetSegment.Vote); + Voting.ServerWrite(msg); + } foreach (var c in recipients) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index f49b85fd2..6bf35ee39 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -123,7 +123,7 @@ namespace Barotrauma.Networking //remove old events that have been sent to all clients, they are redundant now // keep at least one event in the list (lastSentToAll == e.ID) so we can use it to keep track of the latest ID // and events less than 15 seconds old to give disconnected clients a bit of time to reconnect without getting desynced - if (Timing.TotalTime > GameMain.GameSession.RoundStartTime + NetConfig.RoundStartSyncDuration) + if (GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration) { events.RemoveAll(e => (NetIdUtils.IdMoreRecent(lastSentToAll, e.ID) || !inGameClientsPresent) && @@ -217,7 +217,7 @@ namespace Barotrauma.Networking if (Timing.TotalTime - lastWarningTime > 5.0 && Timing.TotalTime - lastSentToAnyoneTime > 10.0 && - Timing.TotalTime > GameMain.GameSession.RoundStartTime + NetConfig.RoundStartSyncDuration) + GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration) { lastWarningTime = Timing.TotalTime; GameServer.Log("WARNING: ServerEntityEventManager is lagging behind! Last sent id: " + lastSentToAnyone.ToString() + ", latest create id: " + ID.ToString(), ServerLog.MessageType.ServerMessage); @@ -229,7 +229,7 @@ namespace Barotrauma.Networking ServerEntityEvent firstEventToResend = events.Find(e => e.ID == (ushort)(lastSentToAll + 1)); if (firstEventToResend != null && - Timing.TotalTime > GameMain.GameSession.RoundStartTime + NetConfig.RoundStartSyncDuration && + GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration && ((lastSentToAnyoneTime - firstEventToResend.CreateTime) > NetConfig.OldReceivedEventKickTime || (Timing.TotalTime - firstEventToResend.CreateTime) > NetConfig.OldEventKickTime)) { // This event is 10 seconds older than the last one we've successfully sent, @@ -295,15 +295,15 @@ namespace Barotrauma.Networking /// /// Writes all the events that the client hasn't received yet into the outgoing message /// - public void Write(Client client, IWriteMessage msg) + public void Write(in SegmentTableWriter segmentTable, Client client, IWriteMessage msg) { - Write(client, msg, out _); + Write(segmentTable, client, msg, out _); } /// /// Writes all the events that the client hasn't received yet into the outgoing message /// - public void Write(Client client, IWriteMessage msg, out List sentEvents) + public void Write(in SegmentTableWriter segmentTable, Client client, IWriteMessage msg, out List sentEvents) { List eventsToSync = GetEventsToSync(client); @@ -315,7 +315,7 @@ namespace Barotrauma.Networking //too many events for one packet //(normal right after a round has just started, don't show a warning if it's been less than 10 seconds) - if (eventsToSync.Count > 200 && GameMain.GameSession != null && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 10.0) + if (eventsToSync.Count > 200 && GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 10.0) { if (eventsToSync.Count > 200 && !client.NeedsMidRoundSync && Timing.TotalTime > lastEventCountHighWarning + 2.0) { @@ -345,7 +345,7 @@ namespace Barotrauma.Networking if (client.NeedsMidRoundSync) { - msg.WriteByte((byte)ServerNetObject.ENTITY_EVENT_INITIAL); + segmentTable.StartNewSegment(ServerNetSegment.EntityEventInitial); msg.WriteUInt16(client.UnreceivedEntityEventCount); msg.WriteUInt16(client.FirstNewEventID); @@ -353,7 +353,7 @@ namespace Barotrauma.Networking } else { - msg.WriteByte((byte)ServerNetObject.ENTITY_EVENT); + segmentTable.StartNewSegment(ServerNetSegment.EntityEvent); Write(msg, eventsToSync, out sentEvents, client); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs index 21d3f9ae8..a02d01ebe 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs @@ -1,13 +1,13 @@ -using Barotrauma.Steam; +using Barotrauma.Steam; using System; namespace Barotrauma.Networking { partial class OrderChatMessage : ChatMessage { - public override void ServerWrite(IWriteMessage msg, Client c) + public override void ServerWrite(in SegmentTableWriter segmentTable, IWriteMessage msg, Client c) { - msg.WriteByte((byte)ServerNetObject.CHAT_MESSAGE); + segmentTable.StartNewSegment(ServerNetSegment.ChatMessage); msg.WriteUInt16(NetStateID); msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); msg.WriteString(SenderName); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index b8f298682..4f93198d9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -44,7 +44,7 @@ namespace Barotrauma.Networking } } - private bool IsRespawnPromptPendingForClient(Client c) + private static bool IsRespawnPromptPendingForClient(Client c) { if (!UseRespawnPrompt || !(GameMain.GameSession.GameMode is MultiPlayerCampaign campaign)) { return false; } @@ -67,7 +67,7 @@ namespace Barotrauma.Networking return false; } - private List GetBotsToRespawn() + private static List GetBotsToRespawn() { if (GameMain.Server.ServerSettings.BotSpawnMode == BotSpawnMode.Normal) { @@ -110,7 +110,7 @@ namespace Barotrauma.Networking return ShouldStartRespawnCountdown(characterToRespawnCount); } - private int GetMinCharactersToRespawn() + private static int GetMinCharactersToRespawn() { return Math.Max((int)(GameMain.Server.ConnectedClients.Count * GameMain.Server.ServerSettings.MinRespawnRatio), 1); } @@ -464,7 +464,7 @@ namespace Barotrauma.Networking } } - if (!(GameMain.GameSession.GameMode is CampaignMode)) + if (GameMain.GameSession.GameMode is not CampaignMode) { if (scooterPrefab != null) { diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index b2e6d45e7..7f6f9ebb1 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 100.5.0.0 + 100.6.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 6ee984592..64afef81a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -1625,7 +1625,7 @@ namespace Barotrauma float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage); maxAccumulatedDamage = Math.Max(accumulatedDamage, maxAccumulatedDamage); - if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation != null) + if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation != null && character.IsPlayer) { var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage; GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index cf6e08e81..b53e09b45 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -77,11 +77,11 @@ namespace Barotrauma { if (Level.Loaded.Type == LevelData.LevelType.LocationConnection) { - if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 30.0f) { currentFlags.Add("Initial".ToIdentifier()); } + if (GameMain.GameSession.RoundDuration > 30.0f) { currentFlags.Add("Initial".ToIdentifier()); } } else if (Level.Loaded.Type == LevelData.LevelType.Outpost) { - if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0f && + if (GameMain.GameSession.RoundDuration > 120.0f && speaker?.CurrentHull != null && (speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) && Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 06b7c21cb..6fc41a1ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -212,7 +212,7 @@ namespace Barotrauma public float Stun { get; private set; } [Serialize(false, IsPropertySaveable.Yes, description: "Can damage only Humans."), Editable] - public bool OnlyHumans { get; private set; } + public bool OnlyHumans { get; set; } [Serialize("", IsPropertySaveable.Yes), Editable] public string ApplyForceOnLimbs @@ -328,7 +328,7 @@ namespace Barotrauma List multipliedAfflictions = new List(); foreach (Affliction affliction in Afflictions.Keys) { - multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier, affliction.Probability)); + multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier, affliction)); } return multipliedAfflictions; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index c31330098..95fa182a6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -540,7 +540,7 @@ namespace Barotrauma private Color speechBubbleColor; private float speechBubbleTimer; - public bool ResetInteract; + public bool DisableInteract { get; set; } //text displayed when the character is highlighted if custom interact is set public LocalizedString CustomInteractHUDText { get; private set; } @@ -809,6 +809,7 @@ namespace Barotrauma public float MaxHealth => MaxVitality; public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle; public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached; + public float EmpVulnerability => Params.Health.EmpVulnerability; public float Bloodloss { @@ -856,6 +857,12 @@ namespace Barotrauma set; } + public bool IgnoreMeleeWeapons + { + get; + set; + } + /// /// Current speed of the character's collider. Can be used by status effects to check if the character is moving. /// @@ -906,6 +913,10 @@ namespace Barotrauma { itemSelectedTime = Timing.TotalTime; } + if (prevSelectedItem != _selectedItem && prevSelectedItem?.OnDeselect != null) + { + prevSelectedItem.OnDeselect(this); + } } } /// @@ -1592,7 +1603,7 @@ namespace Barotrauma { DebugConsole.ThrowError($"Failed to give job items for the character \"{Name}\" - could not find human prefab with the id \"{info.HumanPrefabIds.NpcIdentifier}\" from \"{info.HumanPrefabIds.NpcSetIdentifier}\"."); } - else if (humanPrefab.GiveItems(this, Submarine)) + else if (humanPrefab.GiveItems(this, Submarine, spawnPoint)) { return; } @@ -2681,9 +2692,9 @@ namespace Barotrauma return; } - if (ResetInteract) + if (DisableInteract) { - ResetInteract = false; + DisableInteract = false; return; } @@ -2704,25 +2715,31 @@ namespace Barotrauma { if (!IsMouseOnUI && (ViewTarget == null || ViewTarget == this)) { - if ((findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen) && (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld)) + if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen) { - FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null; - if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; } - float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f); - if (HeldItems.Any(it => it?.GetComponent()?.IsActive ?? false)) + if (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld) { - //disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes - aimAssist = 0.0f; - } + FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null; + if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; } + float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f); + if (HeldItems.Any(it => it?.GetComponent()?.IsActive ?? false)) + { + //disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes + aimAssist = 0.0f; + } - var item = FindItemAtPosition(mouseSimPos, aimAssist); - - focusedItem = CanInteract ? item : null; - if (focusedItem != null && focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None) - { - FocusedCharacter = null; + focusedItem = CanInteract ? FindItemAtPosition(mouseSimPos, aimAssist) : null; + if (focusedItem != null && focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None) + { + FocusedCharacter = null; + } + findFocusedTimer = 0.05f; + } + else + { + if (focusedItem != null && !CanInteractWith(focusedItem)) { focusedItem = null; } + if (FocusedCharacter != null && !CanInteractWith(FocusedCharacter)) { FocusedCharacter = null; } } - findFocusedTimer = 0.05f; } } else @@ -2953,6 +2970,15 @@ namespace Barotrauma { UpdateProjSpecific(deltaTime, cam); + if (InvisibleTimer > 0.0f) + { + if (Controlled == null || Controlled == this || (Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f) + { + InvisibleTimer = Math.Min(InvisibleTimer, 1.0f); + } + InvisibleTimer -= deltaTime; + } + KnockbackCooldownTimer -= deltaTime; if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && this == Controlled && !isSynced) { return; } @@ -2992,6 +3018,7 @@ namespace Barotrauma } HideFace = false; + IgnoreMeleeWeapons = false; UpdateSightRange(deltaTime); UpdateSoundRange(deltaTime); @@ -4096,7 +4123,13 @@ namespace Barotrauma { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; } if (Screen.Selected != GameMain.GameScreen) { return; } - if (newStun > 0 && Params.Health.StunImmunity) { return; } + if (newStun > 0 && Params.Health.StunImmunity) + { + if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0) + { + return; + } + } if ((newStun <= Stun && !allowStunDecrease) || !MathUtils.IsValid(newStun)) { return; } if (Math.Sign(newStun) != Math.Sign(Stun)) { @@ -4345,6 +4378,9 @@ namespace Barotrauma { foreach (Item heldItem in HeldItems.ToList()) { + //if the item is both wearable and holdable, and currently worn, don't drop the item + var wearable = heldItem.GetComponent(); + if (wearable is { IsActive: true }) { continue; } heldItem.Drop(this); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index ca1b15991..9eb073d9d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -1241,7 +1241,7 @@ namespace Barotrauma partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel); - public void GiveExperience(int amount, bool isMissionExperience = false) + public void GiveExperience(int amount) { int prevAmount = ExperiencePoints; @@ -1295,6 +1295,18 @@ namespace Barotrauma return experienceRequired + ExperienceRequiredPerLevel(level); } + public int GetExperienceRequiredForLevel(int level) + { + int currentLevel = GetCurrentLevel(out int experienceRequired); + if (currentLevel >= level) { return 0; } + int required = experienceRequired; + for (int i = currentLevel + 1; i <= level; i++) + { + required += ExperienceRequiredPerLevel(i); + } + return required; + } + public int GetCurrentLevel() { return GetCurrentLevel(out _); @@ -1312,7 +1324,7 @@ namespace Barotrauma return level; } - private int ExperienceRequiredPerLevel(int level) + private static int ExperienceRequiredPerLevel(int level) { return BaseExperienceRequired + AddedExperienceRequiredPerLevel * level; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 12f81e8ed..33368e40a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -61,6 +61,9 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes, description: "Explosion damage is applied per each affected limb. Should this affliction damage be divided by the count of affected limbs (1-15) or applied in full? Default: true. Only affects explosions."), Editable] public bool DivideByLimbCount { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Is the damage relative to the max vitality (percentage) or absolute (normal)"), Editable] + public bool MultiplyByMaxVitality { get; private set; } + public float DamagePerSecond; public float DamagePerSecondTimer; public float PreviousVitalityDecrease; @@ -104,6 +107,15 @@ namespace Barotrauma } } + /// + /// Copy properties here instead of using SerializableProperties (with reflection). + /// + public void CopyProperties(Affliction source) + { + Probability = source.Probability; + DivideByLimbCount = source.DivideByLimbCount; + MultiplyByMaxVitality = source.MultiplyByMaxVitality; + } public void Serialize(XElement element) { @@ -115,10 +127,10 @@ namespace Barotrauma SerializableProperties = SerializableProperty.DeserializeProperties(this, element); } - public Affliction CreateMultiplied(float multiplier, float probability) + public Affliction CreateMultiplied(float multiplier, Affliction affliction) { var instance = Prefab.Instantiate(NonClampedStrength * multiplier, Source); - instance.Probability = probability; + instance.CopyProperties(affliction); return instance; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 2c50593e7..1ca464c9b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -693,8 +693,15 @@ namespace Barotrauma if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; } if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } - if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") { return; } + if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") + { + if (Character.EmpVulnerability <= 0 || GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0) + { + return; + } + } if (Character.Params.Health.PoisonImmunity && newAffliction.Prefab.AfflictionType == "poison") { return; } + if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == "emp") { return; } if (newAffliction.Prefab.TargetSpecies.Any() && newAffliction.Prefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; } Affliction existingAffliction = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index cf0392448..26f40125b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -185,7 +185,7 @@ namespace Barotrauma } } - public bool GiveItems(Character character, Submarine submarine, Rand.RandSync randSync = Rand.RandSync.Unsynced, bool createNetworkEvents = true) + public bool GiveItems(Character character, Submarine submarine, WayPoint spawnPoint, Rand.RandSync randSync = Rand.RandSync.Unsynced, bool createNetworkEvents = true) { if (ItemSets == null || !ItemSets.Any()) { return false; } var spawnItems = ToolBox.SelectWeightedRandom(ItemSets, it => it.commonness, randSync).element; @@ -196,7 +196,7 @@ namespace Barotrauma int amount = itemElement.GetAttributeInt("amount", 1); for (int i = 0; i < amount; i++) { - InitializeItem(character, itemElement, submarine, this, createNetworkEvents: createNetworkEvents); + InitializeItem(character, itemElement, submarine, this, spawnPoint, createNetworkEvents: createNetworkEvents); } } } @@ -234,7 +234,7 @@ namespace Barotrauma return characterInfo; } - public static void InitializeItem(Character character, XElement itemElement, Submarine submarine, HumanPrefab humanPrefab, Item parentItem = null, bool createNetworkEvents = true) + public static void InitializeItem(Character character, XElement itemElement, Submarine submarine, HumanPrefab humanPrefab, WayPoint spawnPoint = null, Item parentItem = null, bool createNetworkEvents = true) { ItemPrefab itemPrefab; string itemIdentifier = itemElement.GetAttributeString("identifier", ""); @@ -278,7 +278,7 @@ namespace Barotrauma IdCard idCardComponent = item.GetComponent(); if (idCardComponent != null) { - idCardComponent.Initialize(null, character); + idCardComponent.Initialize(spawnPoint, character); if (submarine != null && (submarine.Info.IsWreck || submarine.Info.IsOutpost)) { idCardComponent.SubmarineSpecificID = submarine.SubmarineSpecificIDTag; @@ -301,7 +301,7 @@ namespace Barotrauma int amount = childItemElement.GetAttributeInt("amount", 1); for (int i = 0; i < amount; i++) { - InitializeItem(character, childItemElement, submarine, humanPrefab, item, createNetworkEvents); + InitializeItem(character, childItemElement, submarine, humanPrefab, spawnPoint, item, createNetworkEvents); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 9c7b7501e..7e8075078 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -757,6 +757,10 @@ namespace Barotrauma } if (!foundMatchingModifier && random > affliction.Probability) { continue; } float finalDamageModifier = damageMultiplier; + if (affliction.Prefab.AfflictionType == "emp" && character.EmpVulnerability > 0) + { + finalDamageModifier *= character.EmpVulnerability; + } foreach (DamageModifier damageModifier in tempModifiers) { float damageModifierValue = damageModifier.DamageMultiplier; @@ -766,9 +770,13 @@ namespace Barotrauma } finalDamageModifier *= damageModifierValue; } + if (affliction.MultiplyByMaxVitality) + { + finalDamageModifier *= character.MaxVitality / 100f; + } if (!MathUtils.NearlyEqual(finalDamageModifier, 1.0f)) { - newAffliction = affliction.CreateMultiplied(finalDamageModifier, affliction.Probability); + newAffliction = affliction.CreateMultiplied(finalDamageModifier, affliction); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 504a6ca22..7dadbaf86 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -504,6 +504,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes), Editable] public bool PoisonImmunity { get; set; } + [Serialize(0f, IsPropertySaveable.Yes), Editable] + public float EmpVulnerability { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Can afflictions affect the face/body tint of the character."), Editable] public bool ApplyAfflictionColors { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs index 6aff8ee8e..87446e016 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs @@ -39,8 +39,18 @@ namespace Barotrauma.Abilities { if (factions.Any()) { - // FIXME there's probably a better way to check the faction affiliated with the mission later - return mission.ReputationRewards.Keys.Any(factionIdentifier => factions.Contains(factionIdentifier)); + if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; } + + foreach (var (factionIdentifier, amount) in mission.ReputationRewards) + { + if (amount <= 0) { continue; } + if (factions.FirstOrDefault(faction => factionIdentifier == faction.Prefab.Identifier) is Faction faction && + Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) + { + return true; + } + } + return false; } return missionType.Contains(mission.Prefab.Type); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs index c4017a87f..237e15b5f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs @@ -23,7 +23,6 @@ protected override bool MatchesConditionSpecific() { Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier); - return character.Info.GetSavedStatValue(statType, identifier) >= min; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs index 5b7d50aa0..5686f777a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs @@ -5,15 +5,33 @@ internal sealed class CharacterAbilityGiveExperience : CharacterAbility public override bool AppliesEffectOnIntervalUpdate => true; private readonly int amount; + private readonly int level; public CharacterAbilityGiveExperience(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { amount = abilityElement.GetAttributeInt("amount", 0); + level = abilityElement.GetAttributeInt("level", 0); + + if (amount == 0 && level == 0) + { + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - no exp amount or level defined in {nameof(CharacterAbilityGiveExperience)}."); + } + if (amount > 0 && level > 0) + { + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - {nameof(CharacterAbilityGiveExperience)} defines both an exp amount and a level."); + } } private void ApplyEffectSpecific(Character targetCharacter) { - targetCharacter.Info?.GiveExperience(amount); + if (amount != 0) + { + targetCharacter.Info?.GiveExperience(amount); + } + if (level > 0) + { + targetCharacter.Info?.GiveExperience(targetCharacter.Info.GetExperienceRequiredForLevel(level)); + } } protected override void ApplyEffect(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs deleted file mode 100644 index 4ed14321d..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Barotrauma.Abilities -{ - class CharacterAbilityRevive : CharacterAbility - { - public override bool AppliesEffectOnIntervalUpdate => true; - - public CharacterAbilityRevive(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) - { - } - - private void ApplyEffectSpecific() - { - Character.Revive(removeAllAfflictions: false); - } - - protected override void ApplyEffect() - { - ApplyEffectSpecific(); - } - - protected override void ApplyEffect(AbilityObject abilityObject) - { - ApplyEffectSpecific(); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs index ba5d91c21..287234d11 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs @@ -58,10 +58,8 @@ namespace Barotrauma.Abilities foreach (Identifier identifier in selectedTalentTree) { if (Character.HasTalent(identifier)) { continue; } - if (Character.GiveTalent(identifier)) - { - Character.Info.AdditionalTalentPoints++; - } + + Character.GiveTalent(identifier); } static bool IsShowCaseTalent(Identifier identifier, TalentOption option) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index fb4ab9822..d14c9df8e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -141,23 +141,13 @@ namespace Barotrauma { if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier)) { - return TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents); + return !talentOptionStage.HasMaxTalents(selectedTalents) && TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents); } bool optionStageCompleted = talentOptionStage.HasEnoughTalents(selectedTalents); if (!optionStageCompleted) { break; } - - /*bool hasTalentInThisTier = talentOptionStage.HasMaxTalents(selectedTalents); - if (!hasTalentInThisTier) - { - if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier)) - { - return TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents); - } - break; - }*/ } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs index 736f5053e..15d0bb76a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs @@ -67,10 +67,10 @@ namespace Barotrauma .ToImmutableHashSet(); } - public static Result CreateFromXElement(ContentPackage contentPackage, XElement element) + public static Result CreateFromXElement(ContentPackage contentPackage, XElement element) { - static Result fail(string error, Exception? exception = null) - => Result.Failure(new LoadError(error, exception)); + static Result fail(string error, Exception? exception = null) + => Result.Failure(new ContentPackage.LoadError(error, exception)); Identifier elemName = element.NameAsIdentifier(); var type = Types.FirstOrDefault(t => t.Names.Contains(elemName)); @@ -83,6 +83,8 @@ namespace Barotrauma { return fail($"No content path defined for file of type \"{elemName}\""); } + + using var errorCatcher = DebugConsole.ErrorCatcher.Create(); try { filePath = type.MutateContentPath(filePath); @@ -90,10 +92,16 @@ namespace Barotrauma { return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": file not found."); } + var file = type.CreateInstance(contentPackage, filePath); - return file is null - ? throw new Exception($"Content type is not implemented correctly") - : Result.Success(file); + if (file is null) { return fail($"Content type {type.Type.Name} is not implemented correctly"); } + + if (errorCatcher.Errors.Any()) + { + return fail( + $"Errors were issued to the debug console when loading \"{filePath}\" of type \"{elemName}\""); + } + return Result.Success(file); } catch (Exception e) { @@ -123,23 +131,5 @@ namespace Barotrauma } public bool NotSyncedInMultiplayer => Types.Any(t => t.Type == GetType() && t.NotSyncedInMultiplayer); - - public readonly struct LoadError - { - public readonly string Message; - public readonly Exception? Exception; - - public LoadError(string message, Exception? exception) - { - Message = message; - Exception = exception; - } - - public override string ToString() - => Message - + (Exception is { StackTrace: var stackTrace } - ? '\n' + stackTrace.CleanupStackTrace() - : string.Empty); - } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index 1e2dccfad..a53d13328 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Security.Cryptography; -using System.Text; using System.Threading.Tasks; using System.Xml.Linq; @@ -14,6 +13,15 @@ namespace Barotrauma { public abstract class ContentPackage { + public readonly record struct LoadError(string Message, Exception? Exception) + { + public override string ToString() + => Message + + (Exception is { StackTrace: var stackTrace } + ? '\n' + stackTrace.CleanupStackTrace() + : string.Empty); + } + public static readonly Version MinimumHashCompatibleVersion = new Version(0, 18, 13, 0); public const string LocalModsDir = "LocalMods"; @@ -37,12 +45,30 @@ namespace Barotrauma public readonly Option InstallTime; public ImmutableArray Files { get; private set; } - public ImmutableArray Errors { get; private set; } + + /// + /// Errors that occurred when loading this content package. + /// Currently, all errors are considered fatal and the game + /// will refuse to load a content package that has any errors. + /// + public ImmutableArray FatalLoadErrors { get; private set; } + + /// + /// An error that occurred when trying to enable this mod. + /// This field doesn't directly affect whether or not this mod + /// can be enabled, but if it's been set to anything other than + /// Option.None then the game has already refused to enable it + /// at least once. + /// + public Option EnableError { get; private set; } + = Option.None; + + public bool HasAnyErrors => FatalLoadErrors.Length > 0 || EnableError.IsSome(); public async Task IsUpToDate() { if (!UgcId.TryUnwrap(out var ugcId)) { return true; } - if (!(ugcId is SteamWorkshopId steamWorkshopId)) { return true; } + if (ugcId is not SteamWorkshopId steamWorkshopId) { return true; } if (!InstallTime.TryUnwrap(out var installTime)) { return true; } Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value); @@ -55,20 +81,25 @@ namespace Barotrauma /// /// Does the content package include some content that needs to match between all players in multiplayer. /// - public bool HasMultiplayerSyncedContent { get; private set; } + public bool HasMultiplayerSyncedContent { get; } protected ContentPackage(XDocument doc, string path) { + using var errorCatcher = DebugConsole.ErrorCatcher.Create(); + Path = path.CleanUpPathCrossPlatform(); XElement rootElement = doc.Root ?? throw new NullReferenceException("XML document is invalid: root element is null."); Name = rootElement.GetAttributeString("name", "").Trim(); AltNames = rootElement.GetAttributeStringArray("altnames", Array.Empty()) .Select(n => n.Trim()).ToImmutableArray(); - AssertCondition(!string.IsNullOrEmpty(Name), "Name is null or empty"); - UInt64 steamWorkshopId = rootElement.GetAttributeUInt64("steamworkshopid", 0); - + + if (Name.IsNullOrWhiteSpace() && AltNames.Any()) + { + Name = AltNames.First(); + } + UgcId = steamWorkshopId != 0 ? Option.Some(new SteamWorkshopId(steamWorkshopId)) : Option.None(); @@ -85,23 +116,31 @@ namespace Barotrauma .ToArray(); Files = fileResults - .OfType>() - .Select(f => f.Value) + .Successes() .ToImmutableArray(); - Errors = fileResults - .OfType>() - .Select(f => f.Error) + FatalLoadErrors = fileResults + .Failures() .ToImmutableArray(); + AssertCondition(!string.IsNullOrEmpty(Name), $"{nameof(Name)} is null or empty"); + HasMultiplayerSyncedContent = Files.Any(f => !f.NotSyncedInMultiplayer); Hash = CalculateHash(); var expectedHash = rootElement.GetAttributeString("expectedhash", ""); if (HashMismatches(expectedHash)) { - DebugConsole.ThrowError($"Hash calculation for content package \"{Name}\" didn't match expected hash ({Hash.StringRepresentation} != {expectedHash})"); + FatalLoadErrors = FatalLoadErrors.Add( + new LoadError( + Message: $"Hash calculation returned {Hash.StringRepresentation}, expected {expectedHash}", + Exception: null + )); } + + FatalLoadErrors = FatalLoadErrors + .Concat(errorCatcher.Errors.Select(err => new LoadError(err.Text, null))) + .ToImmutableArray(); } public bool HashMismatches(string expectedHash) @@ -122,21 +161,21 @@ namespace Barotrauma public bool NameMatches(string name) => NameMatches(name.ToIdentifier()); - public static ContentPackage? TryLoad(string path) + public static Result TryLoad(string path) { + var (success, failure) = Result.GetFactoryMethods(); + XDocument doc = XMLExtensions.TryLoadXml(path); try { - return doc.Root.GetAttributeBool("corepackage", false) - ? (ContentPackage)new CorePackage(doc, path) - : new RegularPackage(doc, path); + return success(doc.Root.GetAttributeBool("corepackage", false) + ? new CorePackage(doc, path) + : new RegularPackage(doc, path)); } catch (Exception e) { - e = e.GetInnermost(); - DebugConsole.ThrowError($"{e.Message}: {e.StackTrace}"); - return null; + return failure(e.GetInnermost()); } } @@ -181,7 +220,7 @@ namespace Barotrauma { if (!condition) { - throw new InvalidOperationException($"Failed to load \"{Name}\" at {Path}: {errorMsg}"); + FatalLoadErrors = FatalLoadErrors.Add(new LoadError(errorMsg, null)); } } @@ -201,17 +240,19 @@ namespace Barotrauma Failure } - public LoadResult LoadPackage() + public LoadResult LoadContent() { - foreach (var p in LoadPackageEnumerable()) + foreach (var p in LoadContentEnumerable()) { - if (p.Exception != null) { return LoadResult.Failure; } + if (p.Result.IsFailure) { return LoadResult.Failure; } } return LoadResult.Success; } - public IEnumerable LoadPackageEnumerable() + public IEnumerable LoadContentEnumerable() { + using var errorCatcher = DebugConsole.ErrorCatcher.Create(); + ContentFile[] getFilesToLoad(Predicate predicate) => Files.Where(predicate.Invoke).ToArray() #if DEBUG @@ -227,6 +268,7 @@ namespace Barotrauma for (int i = 0; i < filesToLoad.Length; i++) { Exception? exception = null; + try { //do not allow exceptions thrown here to crash the game @@ -234,42 +276,53 @@ namespace Barotrauma } catch (Exception e) { + var innermost = e.GetInnermost(); + DebugConsole.LogError($"Failed to load \"{filesToLoad[i].Path}\": {innermost.Message}\n{innermost.StackTrace}"); exception = e; } if (exception != null) { yield return ContentPackageManager.LoadProgress.Failure(exception); - break; + yield break; } - yield return new ContentPackageManager.LoadProgress((i + indexOffset) / (float)Files.Length); + + if (errorCatcher.Errors.Any()) + { + yield return ContentPackageManager.LoadProgress.Failure( + ContentPackageManager.LoadProgress.Error + .Reason.ConsoleErrorsThrown); + yield break; + } + yield return ContentPackageManager.LoadProgress.Progress((i + indexOffset) / (float)Files.Length); } } //Load the UI and text files first. This is to allow the game //to render the text in the loading screen as soon as possible. - var priorityFiles = getFilesToLoad(f => f is UIStyleFile || f is TextFile); + var priorityFiles = getFilesToLoad(f => f is UIStyleFile or TextFile); var remainder = getFilesToLoad(f => !priorityFiles.Contains(f)); var loadEnumerable = loadFiles(priorityFiles, 0) .Concat(loadFiles(remainder, priorityFiles.Length)); - + foreach (var p in loadEnumerable) { - if (p.Exception != null) + if (p.Result.TryUnwrapFailure(out var failure)) { - HandleLoadException(p.Exception); + errorCatcher.Dispose(); + UnloadContent(); + EnableError = Option.Some(failure); yield return p; - break; + yield break; } yield return p; } + errorCatcher.Dispose(); } - protected abstract void HandleLoadException(Exception e); - - public void UnloadPackage() + public void UnloadContent() { Files.ForEach(f => f.UnloadFile()); } @@ -284,21 +337,16 @@ namespace Barotrauma .Select(e => ContentFile.CreateFromXElement(this, e)) .ToArray(); - foreach (var result in fileResults) + foreach (var file in fileResults.Successes()) { - switch (result) + if (file is BaseSubFile or ItemAssemblyFile) { - case Success { Value: var file }: - if (file is BaseSubFile || file is ItemAssemblyFile) - { - newFileList.Add(file); - } - else - { - var existingFile = Files.FirstOrDefault(f => f.Path == file.Path); - newFileList.Add(existingFile ?? file); - } - break; + newFileList.Add(file); + } + else + { + var existingFile = Files.FirstOrDefault(f => f.Path == file.Path); + newFileList.Add(existingFile ?? file); } } @@ -331,16 +379,16 @@ namespace Barotrauma public void LogErrors() { - if (!Errors.Any()) + if (!FatalLoadErrors.Any()) { return; } DebugConsole.AddWarning( $"The following errors occurred while loading the content package \"{Name}\". The package might not work correctly.\n" + - string.Join('\n', Errors.Select(errorToStr))); + string.Join('\n', FatalLoadErrors.Select(errorToStr))); - static string errorToStr(ContentFile.LoadError error) + static string errorToStr(LoadError error) => error.ToString(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs index 02a6f4401..123e01943 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs @@ -43,10 +43,5 @@ namespace Barotrauma "Core package requires at least one of the following content types: " + string.Join(", ", missingFileTypes.Select(t => t.Type.Name))); } - - protected override void HandleLoadException(Exception e) - { - throw new Exception($"An exception was thrown while loading \"{Name}\"", e); - } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/RegularPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/RegularPackage.cs index b66bb5c10..aa77dcd35 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/RegularPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/RegularPackage.cs @@ -1,4 +1,3 @@ -using System; using System.Xml.Linq; namespace Barotrauma @@ -9,11 +8,5 @@ namespace Barotrauma { AssertCondition(!doc.Root.GetAttributeBool("corepackage", false), "Expected a regular package, got a core package"); } - - protected override void HandleLoadException(Exception e) - { - UnloadPackage(); - DebugConsole.ThrowError($"Failed to load package \"{Name}\"", e); - } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 11862c85b..81056e5eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -47,18 +47,19 @@ namespace Barotrauma { var oldCore = Core; if (newCore == oldCore) { yield break; } - Core?.UnloadPackage(); + if (newCore.FatalLoadErrors.Any()) { yield break; } + Core?.UnloadContent(); Core = newCore; - foreach (var p in newCore.LoadPackageEnumerable()) { yield return p; } + foreach (var p in newCore.LoadContentEnumerable()) { yield return p; } SortContent(); - yield return new LoadProgress(1.0f); + yield return LoadProgress.Progress(1.0f); } public static void ReloadCore() { if (Core == null) { return; } - Core.UnloadPackage(); - Core.LoadPackage(); + Core.UnloadContent(); + Core.LoadContent(); SortContent(); } @@ -79,10 +80,14 @@ namespace Barotrauma if (ReferenceEquals(inNewRegular, regular)) { yield break; } if (inNewRegular.SequenceEqual(regular)) { yield break; } ThrowIfDuplicates(inNewRegular); - var newRegular = inNewRegular.ToList(); + var newRegular = inNewRegular + // Refuse to enable packages with load errors + // so people are forced away from broken mods + .Where(r => !r.FatalLoadErrors.Any()) + .ToList(); IEnumerable toUnload = regular.Where(r => !newRegular.Contains(r)); RegularPackage[] toLoad = newRegular.Where(r => !regular.Contains(r)).ToArray(); - toUnload.ForEach(r => r.UnloadPackage()); + toUnload.ForEach(r => r.UnloadContent()); Range loadingRange = new Range(0.0f, 1.0f); @@ -90,9 +95,9 @@ namespace Barotrauma { var package = toLoad[i]; loadingRange = new Range(i / (float)toLoad.Length, (i + 1) / (float)toLoad.Length); - foreach (var progress in package.LoadPackageEnumerable()) + foreach (var progress in package.LoadContentEnumerable()) { - if (progress.Exception != null) + if (progress.Result.IsFailure) { //If an exception was thrown while loading this package, refuse to add it to the list of enabled packages newRegular.Remove(package); @@ -103,7 +108,7 @@ namespace Barotrauma } regular.Clear(); regular.AddRange(newRegular); SortContent(); - yield return new LoadProgress(1.0f); + yield return LoadProgress.Progress(1.0f); } public static void ThrowIfDuplicates(IEnumerable pkgs) @@ -231,10 +236,12 @@ namespace Barotrauma public sealed partial class PackageSource : ICollection { private readonly Predicate? skipPredicate; + private readonly Action? onLoadFail; - public PackageSource(string dir, Predicate? skipPredicate) + public PackageSource(string dir, Predicate? skipPredicate, Action? onLoadFail) { this.skipPredicate = skipPredicate; + this.onLoadFail = onLoadFail; directory = dir; Directory.CreateDirectory(directory); } @@ -278,25 +285,30 @@ namespace Barotrauma { var fileListPath = Path.Combine(subDir, ContentPackage.FileListFileName).CleanUpPathCrossPlatform(); if (this.Any(p => p.Path.Equals(fileListPath, StringComparison.OrdinalIgnoreCase))) { continue; } - if (File.Exists(fileListPath)) - { - if (skipPredicate?.Invoke(fileListPath) is true) { continue; } - - ContentPackage? newPackage = ContentPackage.TryLoad(fileListPath); - if (newPackage is CorePackage corePackage) - { - corePackages.Add(corePackage); - } - else if (newPackage is RegularPackage regularPackage) - { - regularPackages.Add(regularPackage); - } - if (!(newPackage is null)) - { - Debug.WriteLine($"Loaded \"{newPackage.Name}\""); - } + if (!File.Exists(fileListPath)) { continue; } + if (skipPredicate?.Invoke(fileListPath) is true) { continue; } + + var result = ContentPackage.TryLoad(fileListPath); + if (!result.TryUnwrapSuccess(out var newPackage)) + { + onLoadFail?.Invoke( + fileListPath, + result.TryUnwrapFailure(out var exception) ? exception : throw new Exception("unreachable")); + continue; } + + switch (newPackage) + { + case CorePackage corePackage: + corePackages.Add(corePackage); + break; + case RegularPackage regularPackage: + regularPackages.Add(regularPackage); + break; + } + + Debug.WriteLine($"Loaded \"{newPackage.Name}\""); } } @@ -348,8 +360,20 @@ namespace Barotrauma public bool IsReadOnly => true; } - public static readonly PackageSource LocalPackages = new PackageSource(ContentPackage.LocalModsDir, skipPredicate: null); - public static readonly PackageSource WorkshopPackages = new PackageSource(ContentPackage.WorkshopModsDir, skipPredicate: SteamManager.Workshop.IsInstallingToPath); + public static readonly PackageSource LocalPackages + = new PackageSource( + ContentPackage.LocalModsDir, + skipPredicate: null, + onLoadFail: null); + public static readonly PackageSource WorkshopPackages = new PackageSource( + ContentPackage.WorkshopModsDir, + skipPredicate: SteamManager.Workshop.IsInstallingToPath, + onLoadFail: (fileListPath, exception) => + { + // Delete Workshop mods that fail to load to + // force a reinstall on next launch if necessary + Directory.TryDelete(Path.GetDirectoryName(fileListPath)!); + }); public static CorePackage? VanillaCorePackage { get; private set; } = null; @@ -373,63 +397,77 @@ namespace Barotrauma EnabledPackages.DisableRemovedMods(); } - public static ContentPackage? ReloadContentPackage(ContentPackage p) + public static Result ReloadContentPackage(ContentPackage p) { - ContentPackage? newPackage = ContentPackage.TryLoad(p.Path); - if (newPackage is CorePackage core) - { - if (EnabledPackages.Core == p) { EnabledPackages.SetCore(core); } - } - else if (newPackage is RegularPackage regular) - { - int index = EnabledPackages.Regular.IndexOf(p); - if (index >= 0) - { - var newRegular = EnabledPackages.Regular.ToArray(); - newRegular[index] = regular; - EnabledPackages.SetRegular(newRegular); - } - } + var result = ContentPackage.TryLoad(p.Path); - if (newPackage != null) + if (result.TryUnwrapSuccess(out var newPackage)) { + switch (newPackage) + { + case CorePackage core: + { + if (EnabledPackages.Core == p) { EnabledPackages.SetCore(core); } + + break; + } + case RegularPackage regular: + { + int index = EnabledPackages.Regular.IndexOf(p); + if (index >= 0) + { + var newRegular = EnabledPackages.Regular.ToArray(); + newRegular[index] = regular; + EnabledPackages.SetRegular(newRegular); + } + + break; + } + } + LocalPackages.SwapPackage(p, newPackage); WorkshopPackages.SwapPackage(p, newPackage); } EnabledPackages.DisableRemovedMods(); - return newPackage; + return result; } - public readonly struct LoadProgress + public readonly record struct LoadProgress(Result Result) { - public readonly float Value; - public readonly Exception? Exception; - - public LoadProgress(float value) + public readonly record struct Error( + Error.Reason ErrorReason, + Option Exception) { - Value = value; - Exception = null; - } + public enum Reason { Exception, ConsoleErrorsThrown } - private LoadProgress(Exception exception) - { - Value = -1f; - Exception = exception; + public Error(Reason reason) : this(reason, Option.None) { } + public Error(Exception exception) : this(Reason.Exception, Option.Some(exception)) { } } public static LoadProgress Failure(Exception exception) - => new LoadProgress(exception); + => new LoadProgress( + Result.Failure(new Error(exception))); + + public static LoadProgress Failure(Error.Reason reason) + => new LoadProgress( + Result.Failure(new Error(reason))); + + public static LoadProgress Progress(float value) + => new LoadProgress( + Result.Success(value)); public LoadProgress Transform(Range range) - => Exception != null - ? this - : new LoadProgress(MathHelper.Lerp(range.Start, range.End, Value)); + => Result.TryUnwrapSuccess(out var value) + ? new LoadProgress( + Result.Success( + MathHelper.Lerp(range.Start, range.End, value))) + : this; } public static void LoadVanillaFileList() { VanillaCorePackage = new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList); - foreach (ContentFile.LoadError error in VanillaCorePackage.Errors) + foreach (ContentPackage.LoadError error in VanillaCorePackage.FatalLoadErrors) { DebugConsole.ThrowError(error.ToString()); } @@ -444,6 +482,8 @@ namespace Barotrauma if (VanillaCorePackage is null) { LoadVanillaFileList(); } + SteamManager.Workshop.DeleteUnsubscribedMods(); + CorePackage enabledCorePackage = VanillaCorePackage!; List enabledRegularPackages = new List(); @@ -512,7 +552,7 @@ namespace Barotrauma yield return p.Transform(loadingRange); } - yield return new LoadProgress(1.0f); + yield return LoadProgress.Progress(1.0f); } public static void LogEnabledRegularPackageErrors() diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index d0ac15a8c..12cb907f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -5,6 +5,7 @@ using Barotrauma.Steam; using FarseerPhysics; using Microsoft.Xna.Framework; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; @@ -15,12 +16,12 @@ using Barotrauma.MapCreatures.Behavior; namespace Barotrauma { - struct ColoredText + readonly struct ColoredText { - public string Text; - public Color Color; - public bool IsCommand; - public bool IsError; + public readonly string Text; + public readonly Color Color; + public readonly bool IsCommand; + public readonly bool IsError; public readonly string Time; @@ -31,7 +32,7 @@ namespace Barotrauma this.IsCommand = isCommand; this.IsError = isError; - Time = DateTime.Now.ToString(); + Time = DateTime.Now.ToString(CultureInfo.InvariantCulture); } } @@ -91,13 +92,57 @@ namespace Barotrauma } } - private static readonly Queue queuedMessages = new Queue(); + private static readonly ConcurrentQueue queuedMessages + = new ConcurrentQueue(); + public static readonly NamedEvent MessageHandler = new NamedEvent(); + + public struct ErrorCatcher : IDisposable + { + private readonly List errors; + private readonly bool wasConsoleOpen; + private Identifier handlerId; + public IReadOnlyList Errors => errors; + + private ErrorCatcher(Identifier handlerId) + { + this.handlerId = handlerId; +#if CLIENT + this.wasConsoleOpen = IsOpen; +#else + this.wasConsoleOpen = false; +#endif + this.errors = new List(); + + //create a local variable that can be captured by lambdas + var errs = this.errors; + + MessageHandler.Register(handlerId, msg => + { + if (!msg.IsError) { return; } + errs.Add(msg); + }); + } + + public static ErrorCatcher Create() + => new ErrorCatcher(ToolBox.RandomSeed(25).ToIdentifier()); + + public void Dispose() + { + if (handlerId.IsEmpty) { return; } + MessageHandler.Deregister(handlerId); + handlerId = Identifier.Empty; +#if CLIENT + DebugConsole.IsOpen = wasConsoleOpen; +#endif + } + } + static partial void ShowHelpMessage(Command command); const int MaxMessages = 300; - public static List Messages = new List(); + public static readonly List Messages = new List(); public delegate void QuestionCallback(string answer); private static QuestionCallback activeQuestionCallback; @@ -2290,11 +2335,10 @@ namespace Barotrauma private static void NewMessage(string msg, Color color, bool isCommand, bool isError) { if (string.IsNullOrEmpty(msg)) { return; } - - lock (queuedMessages) - { - queuedMessages.Enqueue(new ColoredText(msg, color, isCommand, isError)); - } + + var newMsg = new ColoredText(msg, color, isCommand, isError); + queuedMessages.Enqueue(newMsg); + MessageHandler.Invoke(newMsg); } public static void ShowQuestionPrompt(string question, QuestionCallback onAnswered, string[] args = null, int argCount = -1) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 21f2242f4..695b9697f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -159,7 +159,7 @@ namespace Barotrauma newCharacter.HumanPrefab = humanPrefab; newCharacter.TeamID = TeamID; newCharacter.EnableDespawn = false; - humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); + humanPrefab.GiveItems(newCharacter, newCharacter.Submarine, spawnPos as WayPoint); if (LootingIsStealing) { foreach (Item item in newCharacter.Inventory.FindAllItems(recursive: true)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs index b3af7964b..c480ceccd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs @@ -25,6 +25,8 @@ namespace Barotrauma private readonly Identifier spawnPointTag; private readonly Identifier destructibleItemTag; + private readonly string endCinematicSound; + private ImmutableArray minions; private readonly int minionCount; private readonly float minionScatter; @@ -134,7 +136,7 @@ namespace Barotrauma spawnPointTag = prefab.ConfigElement.GetAttributeIdentifier(nameof(spawnPointTag), Identifier.Empty); destructibleItemTag = prefab.ConfigElement.GetAttributeIdentifier(nameof(destructibleItemTag), Identifier.Empty); - + endCinematicSound = prefab.ConfigElement.GetAttributeString(nameof(endCinematicSound), string.Empty); startCinematicDistance = prefab.ConfigElement.GetAttributeFloat(nameof(startCinematicDistance), 0); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 9c8d0e7df..49572092e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -407,7 +407,7 @@ namespace Barotrauma { var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); - info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value), isMissionExperience: true); + info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value)); } // apply money gains afterwards to prevent them from affecting XP gains @@ -561,8 +561,7 @@ namespace Barotrauma Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false); spawnedCharacter.HumanPrefab = humanPrefab; humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn); - humanPrefab.GiveItems(spawnedCharacter, submarine, Rand.RandSync.ServerAndClient, createNetworkEvents: false); - + humanPrefab.GiveItems(spawnedCharacter, submarine, positionToStayIn as WayPoint, Rand.RandSync.ServerAndClient, createNetworkEvents: false); characters.Add(spawnedCharacter); characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index dab6af924..8351473af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -15,11 +15,17 @@ namespace Barotrauma { public Item Item; + /// + /// Note that the integer values matter here: the state of the target can't go back to a smaller value, + /// and a larger or equal value than the RequiredRetrievalState means the item counts as retrieved + /// (if the item needs to be picked up to be considered retrieved, it's also considered retrieved if it's in the sub) + /// public enum RetrievalState - { + { None = 0, - PickedUp = 1, - RetrievedToSub = 2 + Interact = 1, + PickedUp = 2, + RetrievedToSub = 3 } public readonly ItemPrefab ItemPrefab; @@ -41,10 +47,19 @@ namespace Barotrauma public readonly bool HideLabelAfterRetrieved; - public bool Retrieved => - RequiredRetrievalState == RetrievalState.RetrievedToSub ? - State == RetrievalState.RetrievedToSub : - State != RetrievalState.None; + public bool Retrieved + { + get + { + return RequiredRetrievalState switch + { + RetrievalState.None => true, + RetrievalState.Interact or RetrievalState.PickedUp => State >= RequiredRetrievalState, + RetrievalState.RetrievedToSub => State == RetrievalState.RetrievedToSub, + _ => throw new NotImplementedException(), + }; + } + } private RetrievalState state; public RetrievalState State @@ -60,6 +75,8 @@ namespace Barotrauma } } + public bool Interacted; + private readonly SalvageMission mission; /// @@ -268,6 +285,13 @@ namespace Barotrauma target.Item.body.SetTransformIgnoreContacts(target.Item.body.SimPosition, target.Item.body.Rotation); target.Item.body.FarseerBody.BodyType = BodyType.Kinematic; } + else if (target.RequiredRetrievalState == Target.RetrievalState.Interact) + { + target.Item.OnInteract += () => + { + target.Interacted = true; + }; + } for (int i = 0; i < target.StatusEffects.Count; i++) { List effectList = target.StatusEffects[i]; @@ -352,22 +376,33 @@ namespace Barotrauma switch (target.State) { case Target.RetrievalState.None: + if (target.Interacted) + { + TrySetRetrievalState(Target.RetrievalState.Interact); + } var root = target.Item?.GetRootContainer() ?? target.Item; if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1) { - target.State = Target.RetrievalState.PickedUp; - if (target.Retrieved) { State = i + 1 ; } + TrySetRetrievalState(Target.RetrievalState.PickedUp); } break; case Target.RetrievalState.PickedUp: Submarine parentSub = target.Item.CurrentHull?.Submarine ?? target.Item.GetRootInventoryOwner()?.Submarine; if (parentSub != null && parentSub.Info.Type == SubmarineType.Player) { - target.State = Target.RetrievalState.RetrievedToSub; - if (target.Retrieved) { State = i + 1; } + TrySetRetrievalState(Target.RetrievalState.RetrievedToSub); } break; } + + void TrySetRetrievalState(Target.RetrievalState retrievalState) + { + if (retrievalState < target.State) { return; } + bool wasRetrieved = false; + target.State = retrievalState; + //increment the mission state if the target became retrieved + if (!wasRetrieved && target.Retrieved) { State = i + 1; } + } } if (targets.All(t => t.Retrieved)) { @@ -383,7 +418,7 @@ namespace Barotrauma protected override void EndMissionSpecific(bool completed) { //consider failed (can't attempt again) if we picked up any of the items but failed to bring them out of the level - failed = !completed && targets.Any(t => t.State != Target.RetrievalState.None); + failed = !completed && targets.Any(t => t.State >= Target.RetrievalState.PickedUp); foreach (var target in targets) { if (target.RemoveItem) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 1bdbade40..d8ccd3753 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -594,7 +594,7 @@ namespace Barotrauma { GameAnalyticsManager.AddDesignEvent( $"MonsterSpawn:{GameMain.GameSession.GameMode?.Preset?.Identifier.Value ?? "none"}:{Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"}:{SpawnPosType}:{SpeciesName}", - value: Timing.TotalTime - GameMain.GameSession.RoundStartTime); + value: GameMain.GameSession.RoundDuration); } }, delayBetweenSpawns * i); i++; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index d33271bdf..786425547 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -316,5 +316,17 @@ namespace Barotrauma.Extensions => source .OfType>() .Select(some => some.Value); + + public static IEnumerable Successes( + this IEnumerable> source) + => source + .OfType>() + .Select(s => s.Value); + + public static IEnumerable Failures( + this IEnumerable> source) + => source + .OfType>() + .Select(f => f.Error); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs index a3e33ab98..6ca24aab7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs @@ -57,6 +57,7 @@ namespace Barotrauma if (!data.ContainsKey(identifier)) { data.Add(identifier, value); + SteamAchievementManager.OnCampaignMetadataSet(identifier, value); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index fa72f23cb..5b8f8bacd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -65,7 +65,7 @@ namespace Barotrauma float reputationGainMultiplier = 1f; foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) { - reputationGainMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationGainMultiplier); + reputationGainMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationGainMultiplier, includeSaved: false); reputationGainMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationGainMultiplier, Identifier) ?? 0; } reputationChange *= reputationGainMultiplier; @@ -75,7 +75,7 @@ namespace Barotrauma float reputationLossMultiplier = 1f; foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) { - reputationLossMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationLossMultiplier); + reputationLossMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationLossMultiplier, includeSaved: false); reputationLossMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationLossMultiplier, Identifier) ?? 0; } reputationChange *= reputationLossMultiplier; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 25fe21a47..b86106a94 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -79,7 +79,7 @@ namespace Barotrauma public bool DisableEvents { - get { return IsFirstRound && Timing.TotalTime < GameMain.GameSession.RoundStartTime + FirstRoundEventDelay; } + get { return IsFirstRound && GameMain.GameSession.RoundDuration > FirstRoundEventDelay; } } public bool CheatsEnabled; @@ -248,7 +248,7 @@ namespace Barotrauma { for (int i = 0; i < wall.SectionCount; i++) { - wall.SetDamage(i, 0, createNetworkEvent: false); + wall.SetDamage(i, 0, createNetworkEvent: false, createExplosionEffect: false); } } } @@ -1260,6 +1260,7 @@ namespace Barotrauma if (item.Components.None(c => c is Pickable)) { continue; } if (item.Components.Any(c => c is Pickable p && p.IsAttached)) { continue; } if (item.Components.Any(c => c is Wire w && w.Connections.Any(c => c != null))) { continue; } + if (item.Container?.GetComponent() is { DrawInventory: false }) { continue; } itemsToTransfer.Add((item, item.Container)); item.Submarine = null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 1ba565c1e..2a42ffabb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -29,7 +29,10 @@ namespace Barotrauma private Location[]? dummyLocations; public CrewManager? CrewManager; - public double RoundStartTime; + public float RoundDuration + { + get; private set; + } public double TimeSpentCleaning, TimeSpentPainting; @@ -392,6 +395,7 @@ namespace Barotrauma #if DEBUG DateTime startTime = DateTime.Now; #endif + RoundDuration = 0.0f; AfflictionPrefab.LoadAllEffects(); MirrorLevel = mirrorLevel; @@ -543,7 +547,7 @@ namespace Barotrauma RoundSummary = new RoundSummary(GameMode, Missions, StartLocation, EndLocation); - if (!(GameMode is TutorialMode) && !(GameMode is TestGameMode)) + if (GameMode is not TutorialMode && GameMode is not TestGameMode) { GUI.AddMessage("", Color.Transparent, 3.0f, playSound: false); if (EndLocation != null && levelData != null) @@ -610,9 +614,9 @@ namespace Barotrauma GameMode.Start(); foreach (Mission mission in missions) { - int prevEntityCount = Entity.GetEntities().Count(); + int prevEntityCount = Entity.GetEntities().Count; mission.Start(Level.Loaded); - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count() != prevEntityCount) + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count != prevEntityCount) { DebugConsole.ThrowError( $"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " + @@ -651,7 +655,7 @@ namespace Barotrauma CreatureMetrics.Instance.RecentlyEncountered.Clear(); GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition; - RoundStartTime = Timing.TotalTime; + RoundDuration = 0.0f; GameMain.ResetFrameTime(); IsRunning = true; } @@ -762,6 +766,7 @@ namespace Barotrauma public void Update(float deltaTime) { + RoundDuration += deltaTime; EventManager?.Update(deltaTime); GameMode?.Update(deltaTime); //backwards for loop because the missions may get completed and removed from the list in Update() @@ -914,17 +919,16 @@ namespace Barotrauma #else 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?.Preset.Identifier.Value ?? "none", - roundDuration); + RoundDuration); string eventId = "EndRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":"; LogEndRoundStats(eventId); if (GameMode is CampaignMode campaignMode) { GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", GetAmountOfMoney(crewCharacters) - prevMoney); - campaignMode.TotalPlayTime += roundDuration; + campaignMode.TotalPlayTime += RoundDuration; } #if CLIENT HintManager.OnRoundEnded(); @@ -950,21 +954,20 @@ 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.Value ?? "none"), roundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), roundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), RoundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name.Value ?? "none"), RoundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), RoundDuration); 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 + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), RoundDuration); } if (Level.Loaded != null) { Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ? Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier : Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier(); - GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + levelId), roundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"), roundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + levelId), RoundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"), RoundDuration); } if (Submarine.MainSub != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index b1254de84..e7d089563 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -315,6 +315,7 @@ namespace Barotrauma.Items.Components if (f2.Body.UserData is Limb targetLimb) { if (targetLimb.IsSevered || targetLimb.character == null || targetLimb.character == User) { return false; } + if (targetLimb.character.IgnoreMeleeWeapons) { return false; } var targetCharacter = targetLimb.character; if (targetCharacter == picker) { return false; } if (AllowHitMultiple) @@ -330,6 +331,7 @@ namespace Barotrauma.Items.Components else if (f2.Body.UserData is Character targetCharacter) { if (targetCharacter == picker || targetCharacter == User) { return false; } + if (targetCharacter.IgnoreMeleeWeapons) { return false; } targetLimb = targetCharacter.AnimController.GetLimb(LimbType.Torso); //Otherwise armor can be bypassed in strange ways if (AllowHitMultiple) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 61abd740f..0a7def403 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -809,7 +809,14 @@ namespace Barotrauma.Items.Components } else { - hasRequiredItems = itemList.Any(Predicate); + if (itemList.Any(Predicate)) + { + hasRequiredItems = !relatedItem.RequireEmpty; + } + else + { + hasRequiredItems = relatedItem.MatchOnEmpty || relatedItem.RequireEmpty; + } if (!hasRequiredItems) { shouldBreak = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index e7b7cb691..30107af1e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -107,7 +107,7 @@ namespace Barotrauma.Items.Components [Serialize(100, IsPropertySaveable.No, description: "How many items are placed in a row before starting a new row.")] public int ItemsPerRow { get; set; } - [Serialize(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected.")] + [Serialize(true, IsPropertySaveable.No, description: "Should the contents in the item's inventory be visible? Disabled on items like magazines that spawn the contents as needed.")] public bool DrawInventory { get; @@ -135,9 +135,6 @@ namespace Barotrauma.Items.Components set; } - [Serialize(true, IsPropertySaveable.No)] - public bool AllowAccess { get; set; } - [Serialize(false, IsPropertySaveable.No)] public bool AccessOnlyWhenBroken { get; set; } @@ -530,12 +527,12 @@ namespace Barotrauma.Items.Components public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) { - return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); + return DrawInventory && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); } public override bool Select(Character character) { - if (!AllowAccess) { return false; } + if (!DrawInventory) { return false; } if (item.Container != null) { return false; } if (AccessOnlyWhenBroken) { @@ -571,7 +568,7 @@ namespace Barotrauma.Items.Components public override bool Pick(Character picker) { - if (!AllowAccess) { return false; } + if (!DrawInventory) { return false; } if (AccessOnlyWhenBroken) { if (item.Condition > 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 4a4775208..70006e1fe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -539,7 +539,7 @@ namespace Barotrauma.Items.Components var prevUser = user; CancelFabricating(); - amountRemaining--; + amountRemaining--; if (amountRemaining > 0 && CanBeFabricated(prevFabricatedItem, availableIngredients, prevUser)) { //keep fabricating if we can fabricate more @@ -745,7 +745,7 @@ namespace Barotrauma.Items.Components itemList.AddRange(container.Inventory.AllItems); } } - if (user?.Inventory != null) + if (user?.Inventory != null && user.SelectedItem == item) { itemList.AddRange(user.Inventory.AllItems); linkedInventories.Add(user.Inventory); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index 9e0fcb5e5..85e995e5f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -65,14 +65,7 @@ namespace Barotrauma.Items.Components { get { - if (GameMain.GameSession != null) - { - return (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime); - } - else - { - return 0.0f; - } + return GameMain.GameSession?.RoundDuration ?? 0.0f; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 60bc40f62..8d953aa76 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -677,7 +677,13 @@ namespace Barotrauma.Items.Components ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent(); if (projectileContainer != null && projectileContainer.Item != item) { - projectileContainer?.Item.Use(deltaTime, null); + projectileContainer.Item.Use(deltaTime, null); + //Use root container (e.g. loader) too in case it needs to react to firing somehow + var rootContainer = projectileContainer.Item.GetRootContainer(); + if (rootContainer != projectileContainer.Item) + { + rootContainer.Use(deltaTime, null); + } } } else @@ -687,7 +693,7 @@ namespace Barotrauma.Items.Components var e = item.linkedTo[(j + currentLoaderIndex) % item.linkedTo.Count]; //use linked projectile containers in case they have to react to the turret being launched somehow //(play a sound, spawn more projectiles) - if (!(e is Item linkedItem)) { continue; } + if (e is not Item linkedItem) { continue; } if (!item.Prefab.IsLinkAllowed(e.Prefab)) { continue; } if (linkedItem.Condition <= 0.0f) { @@ -737,7 +743,7 @@ namespace Barotrauma.Items.Components foreach (MapEntity e in item.linkedTo) { - if (!(e is Item linkedItem)) { continue; } + if (e is not Item linkedItem) { continue; } if (!((MapEntity)item).Prefab.IsLinkAllowed(e.Prefab)) { continue; } if (linkedItem.GetComponent() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag("turretammosource")) { @@ -917,7 +923,7 @@ namespace Barotrauma.Items.Components partial void LaunchProjSpecific(); - private void ShiftItemsInProjectileContainer(ItemContainer container) + private static void ShiftItemsInProjectileContainer(ItemContainer container) { if (container == null) { return; } bool moved; @@ -1122,8 +1128,8 @@ namespace Barotrauma.Items.Components character.AIController.SelectTarget(null); } - bool canShoot = true; - if (!HasPowerToShoot()) + bool canShoot = HasPowerToShoot(); + if (!canShoot) { List batteries = GetDirectlyConnectedBatteries(); float lowestCharge = 0.0f; @@ -1148,7 +1154,6 @@ namespace Barotrauma.Items.Components character.Speak(TextManager.Get("DialogSupercapacitorIsBroken").Value, identifier: "supercapacitorisbroken".ToIdentifier(), minDurationBetweenSimilar: 30.0f); - canShoot = false; } } } @@ -1163,7 +1168,6 @@ namespace Barotrauma.Items.Components character.Speak(TextManager.Get("DialogTurretHasNoPower").Value, identifier: "turrethasnopower".ToIdentifier(), minDurationBetweenSimilar: 30.0f); - canShoot = false; } } @@ -1342,7 +1346,7 @@ namespace Barotrauma.Items.Components closestDistance = shootDistance; foreach (var wall in Level.Loaded.ExtraWalls) { - if (!(wall is DestructibleLevelWall destructibleWall) || destructibleWall.Destroyed) { continue; } + if (wall is not DestructibleLevelWall destructibleWall || destructibleWall.Destroyed) { continue; } foreach (var cell in wall.Cells) { if (cell.DoesDamage) @@ -1464,19 +1468,18 @@ namespace Barotrauma.Items.Components Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value); // Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target. Body worldTarget = CheckLineOfSight(start, end); - bool shoot; if (closestEnemy != null && closestEnemy.Submarine != null) { start -= closestEnemy.Submarine.SimPosition; end -= closestEnemy.Submarine.SimPosition; Body transformedTarget = CheckLineOfSight(start, end); - shoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character)); + canShoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character)); } else { - shoot = CanShoot(worldTarget, character); + canShoot = CanShoot(worldTarget, character); } - if (!shoot) { return false; } + if (!canShoot) { return false; } if (character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogFireTurret").Value, @@ -1530,6 +1533,7 @@ namespace Barotrauma.Items.Components { if (targetBody.UserData is ISpatialEntity e) { + if (e is Structure s && s.Indestructible) { return false; } Submarine sub = e.Submarine ?? e as Submarine; if (!targetSubmarines && e is Submarine) { return false; } if (sub == null) { return false; } @@ -1618,7 +1622,7 @@ namespace Barotrauma.Items.Components return projectiles; } - private void CheckProjectileContainer(Item projectileContainer, List projectiles, out bool stopSearching) + private static void CheckProjectileContainer(Item projectileContainer, List projectiles, out bool stopSearching) { stopSearching = false; if (projectileContainer.Condition <= 0.0f) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 7cc76c57f..7fdde0f9b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -141,6 +141,8 @@ namespace Barotrauma private readonly bool[] hasStatusEffectsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length]; private readonly Dictionary> statusEffectLists; + public Action OnInteract; + public Dictionary SerializableProperties { get; protected set; } private bool? hasInGameEditableProperties; @@ -867,6 +869,8 @@ namespace Barotrauma } } + public Action OnDeselect; + public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true) : this(new Rectangle( (int)(position.X - itemPrefab.Sprite.size.X / 2 * itemPrefab.Scale), @@ -2166,12 +2170,15 @@ namespace Barotrauma if (projectile.ShouldIgnoreSubmarineCollision(f2, contact)) { return false; } } - contact.GetWorldManifold(out Vector2 normal, out _); - if (contact.FixtureA.Body == f1.Body) { normal = -normal; } - float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal); + if (GameMain.GameSession == null || GameMain.GameSession.RoundDuration > 1.0f) + { + contact.GetWorldManifold(out Vector2 normal, out _); + if (contact.FixtureA.Body == f1.Body) { normal = -normal; } + float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal); + impactQueue ??= new ConcurrentQueue(); + impactQueue.Enqueue(impact); + } - impactQueue ??= new ConcurrentQueue(); - impactQueue.Enqueue(impact); isActive = true; return true; @@ -2611,12 +2618,14 @@ namespace Barotrauma if (user == Character.Controlled) { GUI.ForceMouseOn(null); } if (tempRequiredSkill != null) { requiredSkill = tempRequiredSkill; } #endif - if (ic.CanBeSelected && !(ic is Door)) { selected = true; } + if (ic.CanBeSelected && ic is not Door) { selected = true; } } } if (!picked) { return false; } + OnInteract?.Invoke(); + if (user != null) { if (user.SelectedItem == this) @@ -2785,6 +2794,7 @@ namespace Barotrauma if (GameMain.NetworkMember is { IsServer: true }) { GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(conditionalActionType, ic, character, targetLimb)); + GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnUse, ic, character, targetLimb)); } if (ic.DeleteOnUse) { remove = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index d681e5a8f..1bf731566 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -1156,10 +1156,10 @@ namespace Barotrauma public bool CanBeBoughtFrom(Location.StoreInfo store, out PriceInfo priceInfo) { priceInfo = GetPriceInfo(store); - return - priceInfo is { CanBeBought: true } && - (store.Location?.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty && - (!priceInfo.MinReputation.Any() || priceInfo.MinReputation.Any(p => store.Location?.Faction?.Prefab.Identifier == p.Key || store.Location?.SecondaryFaction?.Prefab.Identifier == p.Key)); + return + priceInfo is { CanBeBought: true } && + (store?.Location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty && + (!priceInfo.MinReputation.Any() || priceInfo.MinReputation.Any(p => store?.Location.Faction?.Prefab.Identifier == p.Key || store?.Location.SecondaryFaction?.Prefab.Identifier == p.Key)); } public bool CanBeBoughtFrom(Location location) @@ -1170,17 +1170,17 @@ namespace Barotrauma var priceInfo = GetPriceInfo(store.Value); if (priceInfo == null) { continue; } if (!priceInfo.CanBeBought) { continue; } - if ((location.LevelData?.Difficulty ?? 0) < priceInfo.MinLevelDifficulty) { continue; } + if (location.LevelData.Difficulty < priceInfo.MinLevelDifficulty) { continue; } if (priceInfo.MinReputation.Any()) { - if (!priceInfo.MinReputation.Any(p => - location?.Faction?.Prefab.Identifier == p.Key || - location?.SecondaryFaction?.Prefab.Identifier == p.Key)) + if (!priceInfo.MinReputation.Any(p => + location?.Faction?.Prefab.Identifier == p.Key || + location?.SecondaryFaction?.Prefab.Identifier == p.Key)) { continue; - } + } } - return true; + return true; } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index b37bf65ed..1ca3b27bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -27,13 +27,14 @@ namespace Barotrauma private readonly bool applyFireEffects; private readonly string[] ignoreFireEffectsForTags; private readonly bool ignoreCover; - private readonly bool onlyInside,onlyOutside; private readonly float flashDuration; private readonly float? flashRange; private readonly string decal; private readonly float decalSize; private readonly bool applyToSelf; + public bool OnlyInside, OnlyOutside; + private readonly float itemRepairStrength; public readonly HashSet IgnoredSubmarines = new HashSet(); @@ -81,8 +82,8 @@ namespace Barotrauma ignoreFireEffectsForTags = element.GetAttributeStringArray("ignorefireeffectsfortags", Array.Empty(), convertToLowerInvariant: true); ignoreCover = element.GetAttributeBool("ignorecover", false); - onlyInside = element.GetAttributeBool("onlyinside", false); - onlyOutside = element.GetAttributeBool("onlyoutside", false); + OnlyInside = element.GetAttributeBool("onlyinside", false); + OnlyOutside = element.GetAttributeBool("onlyoutside", false); flash = element.GetAttributeBool("flash", showEffects); flashDuration = element.GetAttributeFloat("flashduration", 0.05f); @@ -181,8 +182,7 @@ namespace Barotrauma { float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition); if (distSqr > displayRangeSqr) { continue; } - - float distFactor = 1.0f - (float)Math.Sqrt(distSqr) / displayRange; + float distFactor = CalculateDistanceFactor(distSqr, displayRange); //damage repairable power-consuming items var powered = item.GetComponent(); @@ -199,6 +199,7 @@ namespace Barotrauma powerContainer.Charge -= powerContainer.GetCapacity() * EmpStrength * distFactor; } } + static float CalculateDistanceFactor(float distSqr, float displayRange) => 1.0f - MathF.Sqrt(distSqr) / displayRange; } if (itemRepairStrength > 0.0f) @@ -292,10 +293,16 @@ namespace Barotrauma { continue; } - if (c == attacker && !applyToSelf) { continue; } + //if (c == attacker && !applyToSelf) { continue; } - if (onlyInside && c.Submarine == null) { continue; } - else if (onlyOutside && c.Submarine != null) { continue; } + if (OnlyInside && c.Submarine == null) + { + continue; + } + else if (OnlyOutside && c.Submarine != null) + { + continue; + } Vector2 explosionPos = worldPosition; if (c.Submarine != null) { explosionPos -= c.Submarine.Position; } @@ -341,15 +348,19 @@ namespace Barotrauma modifiedAfflictions.Clear(); foreach (Affliction affliction in attack.Afflictions.Keys) { - // Shouldn't go above 15, or the damage can be unexpectedly low -> doesn't break armor - // Effectively this makes large explosions more effective against large creatures (because more limbs are affected), but I don't think that's necessarily a bad thing. - float limbCountFactor = Math.Min(distFactors.Count, 15); float dmgMultiplier = distFactor; if (affliction.DivideByLimbCount) { + float limbCountFactor = distFactors.Count; + if (affliction.Prefab.LimbSpecific && affliction.Prefab.AfflictionType == "damage") + { + // Shouldn't go above 15, or the damage can be unexpectedly low -> doesn't break armor + // Effectively this makes large explosions more effective against large creatures (because more limbs are affected), but I don't think that's necessarily a bad thing. + limbCountFactor = Math.Min(distFactors.Count, 15); + } dmgMultiplier /= limbCountFactor; } - modifiedAfflictions.Add(affliction.CreateMultiplied(dmgMultiplier, affliction.Probability)); + modifiedAfflictions.Add(affliction.CreateMultiplied(dmgMultiplier, affliction)); } c.LastDamageSource = damageSource; if (attacker == null) @@ -357,29 +368,29 @@ namespace Barotrauma if (damageSource is Item item) { attacker = item.GetComponent()?.User; - if (attacker == null) - { - attacker = item.GetComponent()?.User; - } + attacker ??= item.GetComponent()?.User; } } if (attack.Afflictions.Any() || attack.Stun > 0.0f) { - AbilityAttackData attackData = new AbilityAttackData(Attack, c, attacker); - if (attackData.Afflictions != null) + if (!attack.OnlyHumans || c.IsHuman) { - modifiedAfflictions.AddRange(attackData.Afflictions); - } + AbilityAttackData attackData = new AbilityAttackData(Attack, c, attacker); + if (attackData.Afflictions != null) + { + modifiedAfflictions.AddRange(attackData.Afflictions); + } - //use a position slightly from the limb's position towards the explosion - //ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods - Vector2 dir = worldPosition - limb.WorldPosition; - Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f; - AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier); - damages.Add(limb, attackResult.Damage); + //use a position slightly from the limb's position towards the explosion + //ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods + Vector2 dir = worldPosition - limb.WorldPosition; + Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f; + AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier); + damages.Add(limb, attackResult.Damage); + } } - + if (attack.StatusEffects != null && attack.StatusEffects.Any()) { attack.SetUser(attacker); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index a226d5e6e..b7f384cea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -549,21 +549,24 @@ namespace Barotrauma if (hull1.WaterVolume < hull1.Volume / Hull.MaxCompress && hull1.Surface < rect.Y) { + //create a wave from the side of the hull the water is leaking from if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f) { - float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1])) * 6.0f; - vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f); - - hull1.WaveVel[hull1.WaveY.Length - 1] += vel * deltaTime; - hull1.WaveVel[hull1.WaveY.Length - 2] += vel * deltaTime; + CreateWave(rect, hull1, hull1.WaveY.Length - 1, hull1.WaveY.Length - 2, flowForce, deltaTime); } else { - float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[0])) * 6.0f; + CreateWave(rect, hull1, 0, 1, flowForce, deltaTime); + } + static void CreateWave(Rectangle rect, Hull hull1, int index1, int index2, Vector2 flowForce, float deltaTime) + { + float vel = (rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[index1]); vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f); - - hull1.WaveVel[0] += vel * deltaTime; - hull1.WaveVel[1] += vel * deltaTime; + if (vel > 0.0f) + { + hull1.WaveVel[index1] += vel * deltaTime; + hull1.WaveVel[index2] += vel * deltaTime; + } } } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index c83766268..9cd4e0147 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -4397,7 +4397,7 @@ namespace Barotrauma corpse.AnimController.FindHull(worldPos, setSubmarine: true); corpse.TeamID = CharacterTeamType.None; corpse.EnableDespawn = false; - selectedPrefab.GiveItems(corpse, wreck); + selectedPrefab.GiveItems(corpse, wreck, sp); corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false); corpse.CharacterHealth.ApplyAffliction(corpse.AnimController.MainLimb, AfflictionPrefab.OxygenLow.Instantiate(200)); bool applyBurns = Rand.Value() < 0.1f; @@ -4428,7 +4428,6 @@ namespace Barotrauma } } corpse.CharacterHealth.ForceUpdateVisuals(); - corpse.GiveIdCardTags(sp); bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true }; if (isServerOrSingleplayer && selectedPrefab.MinMoney >= 0 && selectedPrefab.MaxMoney > 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 107fe5125..58ae53c65 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -405,7 +405,7 @@ namespace Barotrauma if (wall.Submarine != sub) { continue; } for (int i = 0; i < wall.SectionCount; i++) { - wall.SetDamage(i, 0, createNetworkEvent: false); + wall.SetDamage(i, 0, createNetworkEvent: false, createExplosionEffect: false); } } foreach (Hull hull in Hull.HullList) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index c6e2faefb..c792573f6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -323,7 +323,7 @@ namespace Barotrauma var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); if (characters.Any()) { - price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier)); + price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier, includeSaved: false)); price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, tag))); } @@ -675,7 +675,7 @@ namespace Barotrauma return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations); } - public void ChangeType(CampaignMode campaign, LocationType newType) + public void ChangeType(CampaignMode campaign, LocationType newType, bool createStores = true) { if (newType == Type) { return; } @@ -709,7 +709,10 @@ namespace Barotrauma UnlockInitialMissions(Rand.RandSync.Unsynced); - CreateStores(force: true); + if (createStores) + { + CreateStores(force: true); + } } public void UnlockInitialMissions(Rand.RandSync randSync = Rand.RandSync.ServerAndClient) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index 234c33721..0db5cb3fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -13,8 +13,8 @@ namespace Barotrauma { public static readonly PrefabCollection Prefabs = new PrefabCollection(); - private readonly List names; - private readonly List portraits = new List(); + private readonly ImmutableArray names; + private readonly ImmutableArray portraits; // private readonly ImmutableArray<(Identifier Name, float Commonness)> hireableJobs; @@ -41,12 +41,6 @@ namespace Barotrauma public bool IsEnterable { get; private set; } - public bool UsePortraitInMainMenu - { - get; - private set; - } - public bool UsePortraitInRandomLoadingScreens { get; @@ -118,7 +112,6 @@ namespace Barotrauma BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f); - UsePortraitInMainMenu = element.GetAttributeBool(nameof(UsePortraitInMainMenu), element.GetAttributeBool("useinmainmenu", false)); UsePortraitInRandomLoadingScreens = element.GetAttributeBool(nameof(UsePortraitInRandomLoadingScreens), true); HasOutpost = element.GetAttributeBool("hasoutpost", true); IsEnterable = element.GetAttributeBool("isenterable", HasOutpost); @@ -146,7 +139,7 @@ namespace Barotrauma else { string[] rawNamePaths = element.GetAttributeStringArray("namefile", new string[] { "Content/Map/locationNames.txt" }); - names = new List(); + var names = new List(); foreach (string rawPath in rawNamePaths) { try @@ -163,6 +156,7 @@ namespace Barotrauma { names.Add("ERROR: No names found"); } + this.names = names.ToImmutableArray(); } string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", Array.Empty()); @@ -192,7 +186,7 @@ namespace Barotrauma } MinCountPerZone[zoneIndex] = minCount; } - + var portraits = new List(); var hireableJobs = new List<(Identifier, float)>(); foreach (var subElement in element.Elements()) { @@ -233,6 +227,7 @@ namespace Barotrauma break; } } + this.portraits = portraits.ToImmutableArray(); this.hireableJobs = hireableJobs.ToImmutableArray(); } @@ -249,10 +244,10 @@ namespace Barotrauma return null; } - public Sprite GetPortrait(int portraitId) + public Sprite GetPortrait(int randomSeed) { - if (portraits.Count == 0) { return null; } - return portraits[Math.Abs(portraitId) % portraits.Count]; + if (portraits.Length == 0) { return null; } + return portraits[Math.Abs(randomSeed) % portraits.Length]; } public string GetRandomName(Random rand, IEnumerable existingLocations) @@ -265,7 +260,7 @@ namespace Barotrauma return unusedNames[rand.Next() % unusedNames.Count]; } } - return names[rand.Next() % names.Count]; + return names[rand.Next() % names.Length]; } public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 504ee1646..4ccf8d657 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -622,7 +622,8 @@ namespace Barotrauma { leftMostLocation.ChangeType( campaign, - LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned")); + LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned"), + createStores: false); } leftMostLocation.IsGateBetweenBiomes = true; Connections[i].Locked = true; @@ -706,6 +707,7 @@ namespace Barotrauma location.Faction ??= campaign.GetRandomFaction(Rand.RandSync.ServerAndClient); location.SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient); } + location.CreateStores(force: true); } foreach (LocationConnection connection in Connections) @@ -837,7 +839,7 @@ namespace Barotrauma if (LocationType.Prefabs.TryGet("none", out LocationType locationType)) { - previousToEndLocation.ChangeType(campaign, locationType); + previousToEndLocation.ChangeType(campaign, locationType, createStores: false); } //remove all locations from the end biome except the end location diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 23d869ccc..b51eb65f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -1691,13 +1691,12 @@ namespace Barotrauma { npc.CharacterHealth.Unkillable = true; } - humanPrefab.GiveItems(npc, outpost, Rand.RandSync.ServerAndClient); + humanPrefab.GiveItems(npc, outpost, gotoTarget as WayPoint, Rand.RandSync.ServerAndClient); foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true)) { item.AllowStealing = outpost.Info.OutpostGenerationParams.AllowStealing; item.SpawnedInCurrentOutpost = true; } - npc.GiveIdCardTags(gotoTarget as WayPoint); humanPrefab.InitializeCharacter(npc, gotoTarget); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 36d7e96a5..8cd78e3f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -56,6 +56,8 @@ namespace Barotrauma //dimensions of the wall sections' physics bodies (only used for debug rendering) private readonly List bodyDebugDimensions = new List(); + private static Explosion explosionOnBroken; + #if DEBUG [Serialize(false, IsPropertySaveable.Yes), Editable] #else @@ -1083,7 +1085,7 @@ namespace Barotrauma return new AttackResult(damageAmount, null); } - public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true) + public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true, bool createExplosionEffect = true) { if (Submarine != null && Submarine.GodMode || Indestructible) { return; } if (!Prefab.Body) { return; } @@ -1128,6 +1130,7 @@ namespace Barotrauma } else { + float prevGapOpenState = Sections[sectionIndex].gap?.Open ?? 0.0f; if (Sections[sectionIndex].gap == null) { Rectangle gapRect = Sections[sectionIndex].rect; @@ -1204,8 +1207,15 @@ namespace Barotrauma #endif } + var gap = Sections[sectionIndex].gap; float gapOpen = MaxHealth <= 0.0f ? 0.0f : (damage / MaxHealth - LeakThreshold) * (1.0f / (1.0f - LeakThreshold)); - Sections[sectionIndex].gap.Open = gapOpen; + gap.Open = gapOpen; + + //gap appeared or became much larger -> explosion effect + if (gapOpen - prevGapOpenState > 0.25f && createExplosionEffect && !gap.IsRoomToRoom) + { + CreateWallDamageExplosion(gap, attacker); + } } float damageDiff = damage - Sections[sectionIndex].damage; @@ -1234,6 +1244,59 @@ namespace Barotrauma UpdateSections(); } + private void CreateWallDamageExplosion(Gap gap, Character attacker) + { + const float explosionRange = 750.0f; + float explosionStrength = gap.Open; + + var linkedHull = gap.linkedTo.FirstOrDefault() as Hull; + if (linkedHull != null) + { + //existing, nearby gaps leading to the same hull reduce the strength of the explosion + // -> the first breached section does most (or all) of the damage, making it more consistent + // (otherwise the damage would depend on how many structures and sections happen to be breached) + foreach (var otherGap in linkedHull.ConnectedGaps) + { + if (otherGap == gap || otherGap.IsRoomToRoom || otherGap.Open < 0.25f) { continue; } + explosionStrength -= Math.Max(0, explosionRange - Vector2.Distance(otherGap.WorldPosition, gap.WorldPosition)) / explosionRange; + if (explosionStrength <= 0.0f) { return; } + } + } + + if (explosionOnBroken == null) + { + explosionOnBroken = new Explosion(explosionRange * gap.Open, force: 10.0f, damage: 0.0f, structureDamage: 0.0f, itemDamage: 0.0f); + if (AfflictionPrefab.Prefabs.TryGet("lacerations".ToIdentifier(), out AfflictionPrefab lacerations)) + { + explosionOnBroken.Attack.Afflictions.Add(lacerations.Instantiate(50.0f), null); + } + else + { + explosionOnBroken.Attack.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(5.0f), null); + } + explosionOnBroken.OnlyInside = true; + explosionOnBroken.DisableParticles(); + } + + explosionOnBroken.Attack.DamageMultiplier = explosionStrength; + explosionOnBroken?.Explode(gap.WorldPosition, damageSource: null, attacker: attacker); +#if CLIENT + if (linkedHull != null) + { + for (int i = 0; i <= 50; i++) + { + Vector2 particlePos = new Vector2(Rand.Range(gap.WorldRect.X, gap.WorldRect.Right), Rand.Range(gap.WorldRect.Y - gap.WorldRect.Height, gap.WorldRect.Y)); + var velocity = gap.IsHorizontal ? + gap.linkedTo[0].WorldPosition.X < gap.WorldPosition.X ? -Vector2.UnitX : Vector2.UnitX : + gap.linkedTo[0].WorldPosition.Y < gap.WorldPosition.Y ? -Vector2.UnitY : Vector2.UnitY; + velocity = new Vector2(velocity.X + Rand.Range(-0.2f, 0.2f), velocity.Y + Rand.Range(-0.2f, 0.2f)); + var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, velocity * Rand.Range(100.0f, 3000.0f), collisionIgnoreTimer: 0.1f); + if (particle == null) { break; } + } + } +#endif + } + partial void OnHealthChangedProjSpecific(Character attacker, float damageAmount); public void SetCollisionCategory(Category collisionCategory) @@ -1570,7 +1633,7 @@ namespace Barotrauma { for (int i = 0; i < Sections.Length; i++) { - SetDamage(i, Sections[i].damage, createNetworkEvent: false); + SetDamage(i, Sections[i].damage, createNetworkEvent: false, createExplosionEffect: false); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 62f2fa541..5d988fd51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -738,7 +738,7 @@ namespace Barotrauma private void HandleLevelCollision(Impact impact, VoronoiCell cell = null) { - if (GameMain.GameSession != null && Timing.TotalTime < GameMain.GameSession.RoundStartTime + 10) + if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 10) { //ignore level collisions for the first 10 seconds of the round in case the sub spawns in a way that causes it to hit a wall //(e.g. level without outposts to dock to and an incorrectly configured ballast that makes the sub go up) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index a1505a420..ac4b80df1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -38,15 +38,15 @@ namespace Barotrauma.Networking READY_CHECK, READY_TO_SPAWN } - enum ClientNetObject + + enum ClientNetSegment { - END_OF_MESSAGE, //self-explanatory - SYNC_IDS, //ids of the last changes the client knows about - CHAT_MESSAGE, //also self-explanatory - VOTE, //you get the idea - CHARACTER_INPUT, - ENTITY_STATE, - SPECTATING_POS + SyncIds, //ids of the last changes the client knows about + ChatMessage, //also self-explanatory + Vote, //you get the idea + CharacterInput, + EntityState, + SpectatingPos } enum ClientNetError @@ -88,16 +88,15 @@ namespace Barotrauma.Networking MONEY, READY_CHECK //start, end and update a ready check } - enum ServerNetObject + enum ServerNetSegment { - END_OF_MESSAGE, - SYNC_IDS, - CHAT_MESSAGE, - VOTE, - CLIENT_LIST, - ENTITY_POSITION, - ENTITY_EVENT, - ENTITY_EVENT_INITIAL + SyncIds, + ChatMessage, + Vote, + ClientList, + EntityPosition, + EntityEvent, + EntityEventInitial } enum TraitorMessageType diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs index 2ef9e10ec..bcc5c90b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs @@ -272,6 +272,9 @@ namespace Barotrauma.Networking [NetworkSerialize] public bool IsMandatory; + [NetworkSerialize] + public bool IsVanilla; + private Md5Hash? cachedHash; private DateTime? cachedDateTime; @@ -305,6 +308,7 @@ namespace Barotrauma.Networking ? ugcId.StringRepresentation : ""; IsMandatory = !contentPackage.Files.All(f => f is SubmarineFile); + IsVanilla = contentPackage == ContentPackageManager.VanillaCorePackage; InstallTimeDiffInSeconds = contentPackage.InstallTime.TryUnwrap(out var installTime) ? (uint)(installTime - referenceTime).TotalSeconds diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index 15a041953..e19220255 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -65,7 +65,7 @@ namespace Barotrauma.Networking public State CurrentState { get; private set; } - public bool UseRespawnPrompt + public static bool UseRespawnPrompt { get { @@ -191,6 +191,7 @@ namespace Barotrauma.Networking public void ForceRespawn() { ResetShuttle(); + RespawnCountdownStarted = true; RespawnTime = DateTime.Now; CurrentState = State.Waiting; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 2b85369cf..f093b31e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -246,11 +246,13 @@ namespace Barotrauma { public readonly Identifier SkillIdentifier; public readonly float Amount; + public readonly bool TriggerTalents; public GiveSkill(XElement element, string parentDebugName) { - SkillIdentifier = element.GetAttributeIdentifier("skillidentifier", Identifier.Empty); - Amount = element.GetAttributeFloat("amount", 0); + SkillIdentifier = element.GetAttributeIdentifier(nameof(SkillIdentifier), Identifier.Empty); + Amount = element.GetAttributeFloat(nameof(Amount), 0); + TriggerTalents = element.GetAttributeBool(nameof(TriggerTalents), true); if (SkillIdentifier == Identifier.Empty) { @@ -427,7 +429,7 @@ namespace Barotrauma private set; } - private readonly bool multiplyAfflictionsByMaxVitality; + private readonly bool? multiplyAfflictionsByMaxVitality; public IEnumerable SpawnCharacters { @@ -495,7 +497,11 @@ namespace Barotrauma talentTriggers = new List(); giveExperiences = new List(); giveSkills = new List(); - multiplyAfflictionsByMaxVitality = element.GetAttributeBool("multiplyafflictionsbymaxvitality", false); + var multiplyAfflictionsElement = element.GetAttribute(nameof(multiplyAfflictionsByMaxVitality)); + if (multiplyAfflictionsElement != null) + { + multiplyAfflictionsByMaxVitality = multiplyAfflictionsElement.GetAttributeBool(false); + } tags = new HashSet(element.GetAttributeString("tags", "").Split(',')); OnlyInside = element.GetAttributeBool("onlyinside", false); @@ -1305,7 +1311,7 @@ namespace Barotrauma } for (int i = 0; i < targets.Count; i++) { - if (!(targets[i] is Item item)) { continue; } + if (targets[i] is not Item item) { continue; } for (int j = 0; j < useItemCount; j++) { if (item.Removed) { continue; } @@ -1584,9 +1590,7 @@ namespace Barotrauma if (targetCharacter != null && !targetCharacter.Removed) { Identifier skillIdentifier = giveSkill.SkillIdentifier == "randomskill" ? GetRandomSkill() : giveSkill.SkillIdentifier; - - targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, giveSkill.Amount); - + targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, giveSkill.Amount, !giveSkill.TriggerTalents); Identifier GetRandomSkill() { return targetCharacter.Info?.Job?.GetSkills().GetRandomUnsynced()?.Identifier ?? Identifier.Empty; @@ -2170,10 +2174,10 @@ namespace Barotrauma return multiplier * AfflictionMultiplier; } - private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool modifyByMaxVitality) + private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool? multiplyByMaxVitality) { float afflictionMultiplier = GetAfflictionMultiplier(entity, targetCharacter, deltaTime); - if (modifyByMaxVitality) + if (multiplyByMaxVitality ?? affliction.MultiplyByMaxVitality) { afflictionMultiplier *= targetCharacter.MaxVitality / 100f; } @@ -2192,7 +2196,7 @@ namespace Barotrauma if (!MathUtils.NearlyEqual(afflictionMultiplier, 1.0f)) { - return affliction.CreateMultiplied(afflictionMultiplier, affliction.Probability); + return affliction.CreateMultiplied(afflictionMultiplier, affliction); } return affliction; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs index f8815e014..d9dd7a1eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs @@ -89,7 +89,7 @@ namespace Barotrauma.Steam { if (!IsInitialized || !Steamworks.SteamClient.IsValid) { - return new PublishedFileId[0]; + return Array.Empty(); } return Steamworks.SteamUGC.GetSubscribedItems(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index d83ebf7af..c9cde9d90 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -61,6 +61,12 @@ namespace Barotrauma.Steam if (set.Count == prevSize) { break; } prevSize = set.Count; } + + // Remove items that do not have the correct consumer app ID, + // which can happen on items that are not visible to the currently + // logged in player (i.e. private & friends-only items) + set.RemoveWhere(it => it.ConsumerApp != AppID); + return set; } @@ -276,6 +282,62 @@ namespace Barotrauma.Steam } } } + + public static ISet GetInstalledItems() + => ContentPackageManager.WorkshopPackages + .Select(p => p.UgcId) + .NotNone() + .OfType() + .Select(id => id.Value) + .ToHashSet(); + + public static async Task> GetPublishedAndSubscribedItems() + { + var allItems = (await GetAllSubscribedItems()).ToHashSet(); + allItems.UnionWith(await GetPublishedItems()); + + // This is a hack that eliminates subscribed mods that have been + // made private. Players cannot download updates for these, so + // we treat them as if they were deleted. + allItems = (await Task.WhenAll(allItems.Select(it => GetItem(it.Id.Value)))) + .NotNull() + .Where(it => it.ConsumerApp == AppID) + .ToHashSet(); + + return allItems; + } + + public static void DeleteUnsubscribedMods(Action? callback = null) + { +#if SERVER + // Servers do not run this because they can't subscribe to anything + return; +#endif + //If Steamworks isn't initialized then we can't know what the user has unsubscribed from + if (!IsInitialized) { return; } + if (!Steamworks.SteamClient.IsValid) { return; } + if (!Steamworks.SteamClient.IsLoggedOn) { return; } + + TaskPool.Add("DeleteUnsubscribedMods", GetPublishedAndSubscribedItems().WaitForLoadingScreen(), t => + { + if (!t.TryGetResult(out ISet items)) { return; } + var ids = items.Select(it => it.Id.Value).ToHashSet(); + var toUninstall = ContentPackageManager.WorkshopPackages + .Where(pkg + => !pkg.UgcId.TryUnwrap(out SteamWorkshopId workshopId) + || !ids.Contains(workshopId.Value)) + .ToArray(); + if (toUninstall.Any()) + { + foreach (var pkg in toUninstall) + { + Directory.TryDelete(pkg.Dir, recursive: true); + } + ContentPackageManager.UpdateContentPackageList(); + } + callback?.Invoke(toUninstall); + }); + } public static bool IsInstallingToPath(string path) => File.Exists(Path.Combine(Path.GetDirectoryName(path)!, ContentPackageManager.CopyIndicatorFileName)); @@ -457,7 +519,7 @@ namespace Barotrauma.Steam } catch (Exception e) { - DebugConsole.ThrowError( + DebugConsole.AddWarning( $"An exception was thrown when attempting to copy \"{from}\" to \"{to}\": {e.Message}\n{e.StackTrace}"); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 9f3239e41..55e19dfad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -6,7 +6,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Text; namespace Barotrauma { @@ -44,8 +43,9 @@ namespace Barotrauma roundData = new RoundData(); foreach (Item item in Item.ItemList) { + if (item.Submarine == null || item.Submarine.Info.Type != SubmarineType.Player) { continue; } Reactor reactor = item.GetComponent(); - if (reactor != null) { roundData.Reactors.Add(reactor); } + if (reactor != null && reactor.Item.Condition > 0.0f) { roundData.Reactors.Add(reactor); } } pathFinder = new PathFinder(WayPoint.WayPointList, false); cachedDistances.Clear(); @@ -73,7 +73,7 @@ namespace Barotrauma { if (c.IsDead) { continue; } //achievement for descending below crush depth and coming back - if (Timing.TotalTime > GameMain.GameSession.RoundStartTime + 30.0f) + if (GameMain.GameSession.RoundDuration > 30.0f) { if (c.Submarine != null && c.Submarine.AtDamageDepth || Level.Loaded.GetRealWorldDepth(c.WorldPosition.Y) > Level.Loaded.RealWorldCrushDepth) { @@ -97,7 +97,7 @@ namespace Barotrauma //get an achievement if they're still alive at the end of the round foreach (Character c in Character.CharacterList) { - if (!c.IsDead && c.Submarine == sub) roundData.ReactorMeltdown.Add(c); + if (!c.IsDead && c.Submarine == sub) { roundData.ReactorMeltdown.Add(c); } } } } @@ -113,7 +113,7 @@ namespace Barotrauma //achievement for descending ridiculously deep float realWorldDepth = sub.RealWorldDepth; - if (realWorldDepth > 5000.0f && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 30.0f) + if (realWorldDepth > 5000.0f && GameMain.GameSession.RoundDuration > 30.0f) { //all conscious characters inside the sub get an achievement UnlockAchievement("subdeep".ToIdentifier(), true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious); @@ -219,6 +219,12 @@ namespace Barotrauma UnlockAchievement($"discover{biome.Identifier.Value.Replace(" ", "")}".ToIdentifier()); } + public static void OnCampaignMetadataSet(Identifier identifier, object value) + { + if (identifier.IsEmpty || value is null) { return; } + UnlockAchievement($"campaignmetadata_{identifier}_{value}".ToIdentifier()); + } + public static void OnItemRepaired(Item item, Character fixer) { #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs index 740abe381..8d863767f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs @@ -36,9 +36,7 @@ namespace Barotrauma public EitherT(T value) { Value = value; } public override string? ToString() - { - return Value.ToString(); - } + => $"Either<{typeof(T).NameWithGenerics()}, {typeof(U).NameWithGenerics()}>({Value}: {typeof(T).NameWithGenerics()})"; public override bool TryGet(out T t) { t = Value; return true; } public override bool TryGet(out U u) { u = default!; return false; } @@ -75,9 +73,7 @@ namespace Barotrauma public EitherU(U value) { Value = value; } public override string? ToString() - { - return Value.ToString(); - } + => $"Either<{typeof(T).NameWithGenerics()}, {typeof(U).NameWithGenerics()}>({Value}: {typeof(U).NameWithGenerics()})"; public override bool TryGet(out T t) { t = default!; return false; } public override bool TryGet(out U u) { u = Value; return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs index 3d19c61ab..9aff08c3f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs @@ -63,5 +63,21 @@ namespace Barotrauma => !(a == b); public abstract override string ToString(); + + public static implicit operator Option(Option.UnspecifiedNone _) + => None(); + } + + public static class Option + { + public sealed class UnspecifiedNone + { + private UnspecifiedNone() { } + internal static readonly UnspecifiedNone Instance = new(); + } + + public static UnspecifiedNone None => UnspecifiedNone.Instance; + + public static Option Some(T value) => Option.Some(value); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs index f65b53aab..3c6b11b64 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs @@ -59,5 +59,14 @@ namespace Barotrauma return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome()) ?? none(); } + + public static string NameWithGenerics(this Type t) + { + if (!t.IsGenericType) { return t.Name; } + + string result = t.Name[..t.Name.IndexOf('`')]; + result += $"<{string.Join(", ", t.GetGenericArguments().Select(NameWithGenerics))}>"; + return result; + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs index bb1950e0d..d6cc6a92f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs @@ -1,4 +1,7 @@ #nullable enable +using System; +using System.Diagnostics.CodeAnalysis; + namespace Barotrauma { public abstract class Result @@ -13,6 +16,14 @@ namespace Barotrauma public static Failure Failure(TError error) => new Failure(error); + + public abstract bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value); + public abstract bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TError value); + + public abstract override string? ToString(); + + public static (Func> Success, Func> Failure) GetFactoryMethods() + => (Success, Failure); } public sealed class Success : Result @@ -22,6 +33,21 @@ namespace Barotrauma public readonly T Value; public override bool IsSuccess => true; + public override bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value) + { + value = Value; + return true; + } + + public override bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TError value) + { + value = default; + return false; + } + + public override string ToString() + => $"Success<{typeof(T).NameWithGenerics()}, {typeof(TError).NameWithGenerics()}>({Value})"; + public Success(T value) { Value = value; @@ -35,6 +61,21 @@ namespace Barotrauma public readonly TError Error; public override bool IsSuccess => false; + + public override bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value) + { + value = default; + return false; + } + + public override bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TError value) + { + value = Error; + return true; + } + + public override string ToString() + => $"Failure<{typeof(T).NameWithGenerics()}, {typeof(TError).NameWithGenerics()}>({Error})"; public Failure(TError error) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index 49b231dfe..af4033b2b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -314,6 +314,19 @@ namespace Barotrauma.IO //TODO: validate recursion? System.IO.Directory.Delete(path, recursive); } + + public static bool TryDelete(string path, bool recursive = true) + { + try + { + Directory.Delete(path, recursive); + return true; + } + catch + { + return false; + } + } public static DateTime GetLastWriteTime(string path) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SegmentTable.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SegmentTable.cs new file mode 100644 index 000000000..63f652f57 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SegmentTable.cs @@ -0,0 +1,336 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Networking; + +/* + * What are segment tables for? + * + * Segment tables help make our networking packet reading code more robust by + * clearly stating where part of a message begins. Previously we would've done + * something like: + * + * msg.WriteByte(SegmentType.A); + * ... + * msg.WriteByte(SegmentType.B); + * ... + * msg.WriteByte(SegmentType.EndOfMessage); + * + * The problem with this design is that it's hard to debug when the writing and reading + * code do not align for whatever reason. INetSerializableStruct is an awesome way + * of avoiding that problem, but deploying it on a broad scale means rewriting most + * of the netcode. That isn't going to happen any time soon, so this exists as an easier + * way of increasing robustness. + * + * A segment table is laid out as follows: + * + * [TablePointer: UInt16] + * [Segment: arbitrary] + * ... + * [Segment: arbitrary] + * [NumberOfSegments: UInt16] + * [(Identifier, SegmentPointer): (T, UInt16)] + * ... + * [(Identifier, SegmentPointer): (T, UInt16)] + * + * A pointer in this context is an offset relative to the BitPosition where the TablePointer is written. + * + * It is used as follows: + * + * using (var segmentTable = SegmentTableWriter.StartWriting(outMsg)) + * { + * segmentTable.StartNewSegment(T.A); + * ... write segment to outMsg ... + * segmentTable.StartNewSegment(T.B); + * ... write segment to outMsg ... + * } + * peer.SendMessage(outMsg); + * + * ... + * + * SegmentTableReader.Read(inc, + * segmentDataReader: (segment, inc) => + * { + * switch (segment) + * { + * ... read segments ... + * } + * } + * } + * + * The advantages of this approach are: + * - If a message is truncated or corrupted near the end, it becomes far more obvious because the table + * would not be read properly and look like garbage when printed to the console. + * - If the reading and writing code for a segment disagree on something, issues will be isolated to that + * one segment. + * - The code no longer has to fiddle with padding and temporary buffers because the segment table is able + * to handle content that is not byte-aligned just fine. + * - Exception handling is far easier when using a segment table, when combined with a using statement + * any uncaught exception will result in the entire table being skipped, allowing the remainder of the + * message to still be read. + * - It's harder to make mistakes in the implementation of segments themselves with this approach. By using + * the SegmentTableWriter and SegmentTableReader types, you get a type-safe way of delimiting segments + * and it's harder to forget to finalize a packet. + */ + +[NetworkSerialize] +public readonly record struct Segment(T Identifier, UInt16 Pointer) : INetSerializableStruct where T : struct; + +readonly ref struct SegmentTableWriter where T : struct +{ + private readonly IWriteMessage message; + private readonly List> segments; + public readonly int PointerLocation; + private SegmentTableWriter(IWriteMessage message, int pointerLocation) + { + this.message = message; + this.PointerLocation = pointerLocation; + this.segments = new List>(); + } + + public static SegmentTableWriter StartWriting(IWriteMessage msg) + { + var retVal = new SegmentTableWriter(msg, msg.BitPosition); + msg.WriteUInt16(0); //reserve space for the table pointer + return retVal; + } + + private void ThrowOnInvalidState() + { + if (segments.Count >= UInt16.MaxValue) + { + throw new InvalidOperationException($"Too many segments in SegmentTable<{typeof(T).Name}>"); + } + + if (message.BitPosition - PointerLocation > UInt16.MaxValue) + { + throw new OverflowException( + $"Too much data is being stored in SegmentTable<{typeof(T).Name}> ({segments.Count} segments)"); + } + } + + public void StartNewSegment(T value) + { + ThrowOnInvalidState(); + segments.Add(new Segment(value, (UInt16)(message.BitPosition-PointerLocation))); + } + + public void Dispose() + { + ThrowOnInvalidState(); + int tablePosition = message.BitPosition; + + //rewrite the table pointer now that we know where the table ends + message.BitPosition = PointerLocation; + message.WriteUInt16((UInt16)(tablePosition-PointerLocation)); + + //write the table + message.BitPosition = tablePosition; + message.WriteUInt16((UInt16)segments.Count); + foreach (var segment in segments) + { + message.WriteNetSerializableStruct(segment); + } + } +} + +readonly ref struct SegmentTableReader where T : struct +{ + private class SegmentReadMsg : IReadMessage + { + private readonly IReadMessage underlyingMsg; + private readonly IReadOnlyList> segments; + private readonly int segmentIndex; + private readonly int offset; + private readonly int lengthBits; + public SegmentReadMsg(IReadMessage underlyingMsg, IReadOnlyList> segments, int segmentIndex, int offset, int lengthBits) + { + this.underlyingMsg = underlyingMsg; + this.segments = segments; + this.segmentIndex = segmentIndex; + this.offset = offset; + this.lengthBits = lengthBits; + + if (offset + lengthBits >= underlyingMsg.LengthBits) + { + throw new Exception( + $"Segment table is corrupt, segment length is invalid: {offset} + {lengthBits} >= {underlyingMsg.LengthBits}"); + } + } + + private void Check() + { + if (BitPosition > lengthBits) + { + throw new Exception($"Tried to read too much data from segment."); + } + } + + private TRead Check(TRead v) + { + Check(); + return v; + } + + public bool ReadBoolean() => Check(underlyingMsg.ReadBoolean()); + + public void ReadPadBits() + { + Check(); underlyingMsg.ReadPadBits(); + } + + public byte ReadByte() => Check(underlyingMsg.ReadByte()); + + public byte PeekByte() => Check(underlyingMsg.PeekByte()); + + public ushort ReadUInt16() => Check(underlyingMsg.ReadUInt16()); + + public short ReadInt16() => Check(underlyingMsg.ReadInt16()); + + public uint ReadUInt32() => Check(underlyingMsg.ReadUInt32()); + + public int ReadInt32() => Check(underlyingMsg.ReadInt32()); + + public ulong ReadUInt64() => Check(underlyingMsg.ReadUInt64()); + + public long ReadInt64() => Check(underlyingMsg.ReadInt64()); + + public float ReadSingle() => Check(underlyingMsg.ReadSingle()); + + public double ReadDouble() => Check(underlyingMsg.ReadDouble()); + + public uint ReadVariableUInt32() => Check(underlyingMsg.ReadVariableUInt32()); + + public string ReadString() => Check(underlyingMsg.ReadString()); + + public Identifier ReadIdentifier() => Check(underlyingMsg.ReadIdentifier()); + + public Color ReadColorR8G8B8() => Check(underlyingMsg.ReadColorR8G8B8()); + + public Color ReadColorR8G8B8A8() => Check(underlyingMsg.ReadColorR8G8B8A8()); + + public int ReadRangedInteger(int min, int max) => Check(underlyingMsg.ReadRangedInteger(min, max)); + + public float ReadRangedSingle(float min, float max, int bitCount) => Check(underlyingMsg.ReadRangedSingle(min, max, bitCount)); + + public byte[] ReadBytes(int numberOfBytes) => Check(underlyingMsg.ReadBytes(numberOfBytes)); + + public int BitPosition + { + get => underlyingMsg.BitPosition - offset; + set => Check(underlyingMsg.BitPosition = value + offset); + } + + public int BytePosition => BitPosition / 8; + + public byte[] Buffer => underlyingMsg.Buffer; + + public int LengthBits + { + get => lengthBits; + set => throw new InvalidOperationException($"Cannot resize {nameof(SegmentReadMsg)}"); + } + + public int LengthBytes => lengthBits / 8; + + public NetworkConnection Sender => underlyingMsg.Sender; + } + + private readonly IReadMessage message; + private readonly List> segments; + private readonly int exitLocation; + public readonly int PointerLocation; + private SegmentTableReader(IReadMessage message, List> segments, int pointerLocation, int exitLocation) + { + this.message = message; + this.segments = segments; + this.PointerLocation = pointerLocation; + this.exitLocation = exitLocation; + } + + public IReadOnlyList> Segments => segments; + + public enum BreakSegmentReading + { + No, + Yes + } + + public delegate BreakSegmentReading SegmentDataReader( + T segmentHeader, + IReadMessage incMsg); + + public delegate void ExceptionHandler( + Segment segmentWithError, + Segment[] previousSegments, + Exception exceptionThrown); + + public static void Read( + IReadMessage msg, + SegmentDataReader segmentDataReader, + ExceptionHandler? exceptionHandler = null) + { + int pointerLocation = msg.BitPosition; + int tablePointer = msg.ReadUInt16(); + int tableLocation = pointerLocation + tablePointer; + + int returnPosition = msg.BitPosition; + + //read the table + var segments = new List>(); + msg.BitPosition = tableLocation; + int numSegments = msg.ReadUInt16(); + for (int i = 0; i < numSegments; i++) + { + segments.Add(INetSerializableStruct.Read>(msg)); + } + + //store the exit location and go back to the top + int exitLocation = msg.BitPosition; + msg.BitPosition = returnPosition; + using var segmentTable = new SegmentTableReader(msg, segments, pointerLocation, exitLocation); + + for (int i = 0; i < segmentTable.Segments.Count; i++) + { + var segment = segmentTable.Segments[i]; + msg.BitPosition = segmentTable.PointerLocation + segment.Pointer; + try + { + if (segmentDataReader(segment.Identifier, new SegmentReadMsg( + msg, + segments, + i, + offset: segmentTable.PointerLocation + segment.Pointer, + lengthBits: (i < segmentTable.Segments.Count - 1 ? segments[i + 1].Pointer : tablePointer) - + segment.Pointer)) + is BreakSegmentReading.Yes) + { + break; + } + } + catch (Exception e) + { + var prevSegments = segments.Take(i).ToArray(); + if (exceptionHandler is not null) + { + exceptionHandler(segment, prevSegments, e); + } + else + { + throw new Exception( + $"Exception thrown while reading segment {segment.Identifier} at position {segment.Pointer}." + + (prevSegments.Any() ? $" Previous segments: {string.Join(", ", prevSegments)}." : ""), + e); + } + } + } + } + + public void Dispose() + { + message.BitPosition = exitLocation; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskExtensions.cs index 1b4d14dda..022fcdb3f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskExtensions.cs @@ -14,5 +14,17 @@ namespace Barotrauma result = default; return false; } + + public static async Task WaitForLoadingScreen(this Task task) + { + var result = await task; +#if CLIENT + while (GameMain.Instance.LoadingScreenOpen) + { + await Task.Delay((int)(1000 * Timing.Step)); + } +#endif + return result; + } } } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 04c307a02..b198a53a9 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,21 @@ +--------------------------------------------------------------------------------------------------------- +v100.6.0.0 +--------------------------------------------------------------------------------------------------------- + +- Some new music and sounds for the final levels. +- Some new loading screen portraits and improvements to the existing ones. +- Visual improvements to the final levels. +- Visual improvements to the location info overlays on the campaign map. +- The "exit" sonar marker doesn't appear in the end levels until you've pressed the switch that opens the exit, and once you do, the switch marker disappears. +- Fixed reputation loss when a character other than the player (e.g. crawlers in the 'crawleroutbreak' event) damages the outpost walls. +- Made some talent items purchaseable with high faction rep. +- Adjustments to outpost events' reputation rewards. +- Adjustments to the reputation thresholds at which some husk cult events can trigger. Previously you needed at least 15 rep to start the main event chain, which was very hard to reach because there's so few ways to gain cultist rep outside the event chain. +- Gave Captain Hognose some experience points. +- Ritual lanterns heal all afflictions of the type "damage", not just organ damage and burns. +- Fixed cultist robes hiding iron helmet. +- Fixed FB3000 being set to autoinject, making it use up the fuel rod when below 50% health. + --------------------------------------------------------------------------------------------------------- v100.5.0.0 --------------------------------------------------------------------------------------------------------- @@ -65,6 +83,68 @@ Test version of the faction overhaul: - There's now always two paths from biome to another, one controlled by the Coalition and one by Separatists. - Improvements to the campaign map. +--------------------------------------------------------------------------------------------------------- +v0.20.8.0 +--------------------------------------------------------------------------------------------------------- + +Changes and additions: +- Added various new loot items to different creatures. +- Large monsters (Abyss monsters, Moloch, Watcher) drop items upon death. +- Husk eggs now come in two forms: Husk eggs with actual egg-like appearance and the syringe version. +- Breaches through the submarine's outer hull throws shrapnels that can cause minor damage to nearby characters, making monsters that can't get inside more of a threat to the crew (as opposed to just the submarine itself). +- Emp damage now stuns and damages electrical characters (Fractalguardian and Defensebot). Modders: implemented as an affliction, so it's not tied to the "empstrength" attribute defined for explosions. +- Addressed "overactive hammerheads" (#10072) by increasing the perception ranges of the monster mission variants. +- Added a new honking scary random event to beacon stations. +- Added a reactor infographic designed to help new players better understand the reactor interface. It's accessible through a help button on the top right corner of the interface. +- Added some particle, sound and light effects to water-sensitive materials and made them explode when they've been in water for 3 seconds, not immediately. + +Unstable only: +- Fixed status effects with OnUse not always working in the multiplayer mode. Fixes the sounds not playing when the medical items were used on the health interface. +- Fixed mutually exclusive talents being both selectable. +- Fixed "Medical Expertise" not boosting bandages, but applying heals to the entire body instead. +- Fixed "Graduation Ceremony" granting extra talent points. +- Fixed skillbooks triggering talent effects, allowing enormous XP gains when combined with certain captain talents. +- Don't allow laying in beds when wearing an exosuit. +- Added sounds for Petraptor and Defensebot. +- Added sounds for the new weapons. +- Adjusted Shotgun and Scrap Cannon spread. +- Fixed interacting with a wrecked periscope doing nothing. +- Fixed double-clicking on a stack of ammo not reloading the equipped weapon. +- Fixed fabricator pulling materials from the previous user's inventory even if the character is no longer at the fabricator. +- Adjusted item editing hud position to prevent it from overlapping with the affliction icons. +- Fixed active tutorial child objectives not being re-added to the list after resolution change. +- Fixed wiring interfaces becoming blank after changing resolution. +- Fixed changing the selected location on the campaign map changing what outpost you are currently at according to the tab menu. +- Fixed players who've been inside a clown crate becoming impossible to grab/heal for the rest of the round. +- Fixed medical syringes (or other melee weapons) hitting characters inside a clown crate. +- Fixed some inconsistencies with turret loader box positions. +- Fixed items that should only be available in stores past a certain difficulty not being available at all. +- Fixed dedicated server not using the "ispublic" setting configured in serversettings.xml. + +Modding: +- Implemented the status effect type "OnSuccess" where "OnUse" was used instead. Changed "OnUse" to be neutral: always triggers, regardless of the (skill) requirements. You may need to switch using "OnSuccess" instead of "OnUse", if it's intended for the status effect to trigger only when the requirements are matched. +- Status effects of type "OnUse" on projectiles now trigger when the projectile is launched. Previously it launched when the projectile hit the target. Use OnImpact (or OnSuccess/OnFailure) when you want something to happen when the projectile hits the target. +- Added an option to multiply the damage by max vitality (relative damage) per affliction definition, in addition to the "multiplyAfflictionsByMaxVitality" attribute defined for the status effects. If you want to define it for an affliction separately, leave the status effect level definition off, because it'd override the affliction specific value. + +Bugfixes: +- Attempt to fix occasional "mission equality to check failed" errors + added some extra data to the error message to help diagnose it if it still occurs. +- Fixed focus staying on the highlighted item/character indefinitely if you keep holding LMB, even if you're outside interaction range. +- Fixed "no core packages in the list of mods the server has enabled" error when trying to join a server that's using a different version of the core package you have enabled. +- Prevented spawning of genetic materials outside creature inventories when the inventory size was too small, by increasing inventory sizes. +- Removed most of the debug console error spam seen when launching the game or opening the settings menu when faulty mods are installed. +- Fixed mods failing to show up in the mods list at all when they have certain kinds of errors. +- Mods with errors can no longer be enabled. +- Fixed character portrait and health bar buttons being clickable (despite being hidden) when the health interface is open. +- Attempt to fix occasional crashes due to location store being null when teleporting from location to another with console commands. +- Fixes to impact-sensitive items exploding at the start of the round (e.g. at the start of explosive transport missions or when purchasing explosives). +- Attempt to fix bots occasionally being unable to operate turrets when starting a new round until they're re-ordered to man the turret. +- The "respawnnow" console command forces a respawn even if there's less than the minimum amount of players waiting for a respawn. +- Fixed water level sometimes "flickering" up and down when water is leaking to a room from the left or right. +- Fixed resetting UI position doing nothing to equipped items' UIs (e.g. handheld status monitor). +- Fixed items equipped in the health interface slot being sellable. +- Fixed inconsistent view ranges of large turrets. +- Fixed SMG magazine shape being inconsistent with the shape of the mag well on the SMG sprite. + --------------------------------------------------------------------------------------------------------- v0.20.7.0 --------------------------------------------------------------------------------------------------------- @@ -104,11 +184,12 @@ Bugfixes: - Fixed certain genetic effects (such as regeneration from Hammerhead Matriarch genes) not working properly when multiple characters have the same effect. - Adjusted railgun, coilgun and double coilgun firing offsets to make the projectile spawn closer to the end of the barrel. - Fixed incorrect tags in the vending machine in EngineeringModule_01_Colony, causing engineering gear to spawn in the output slot. +- Fixed hair being visible under hats where it shouldn't be. Only happened after loading a saved game (#10396). Modding: - Made it possible to check if some value is null or not with PropertyConditionals (e.g. CurrentHull="eq null"). - Added UseEnvironment.None to Propulsion component. ->>>>>>> feature/wip +- Fixed the debug console command "head" causing the character to disappear. The command can be used for changing the appearance of the character at runtime. --------------------------------------------------------------------------------------------------------- v0.20.6.0 @@ -225,6 +306,9 @@ Balance: Bugfixes: - Fixed a rounding error that caused Health Scanner HUD to display every level of bleeding below 100% as "minor". +- Fixed speech impediment from the husk infection making the bots unable to register any new targets autonomously (= without orders). +- Fix bots having unintentionally long reaction times on reporting the issues, causing them to ignore any new enemies when they first envounter them. +- Fixed the default aim assist being 50% instead of 5%. Fixed aim assist not resetting when the reset button is pressed on the settings window. Modding: - Allow 'launchimpulse' on RangedWeapon to affect projectile's speed (sum of launch impulses). @@ -253,10 +337,16 @@ Unstable only: - Add missing recipe unlocks: Ceremonial Sword, Handcannon. - Moved "Steady Tune" to tier 2, added a new "Trickle Down" talent to the 3rd tier. +Bugfixes: +- Fixed status effects targeting "NearbyCharacters" or "NearbyItems" being applied twice. Modders: if you used this, double the effects (e.g. damage) to get the same results as previously. + --------------------------------------------------------------------------------------------------------- v0.20.1.0 --------------------------------------------------------------------------------------------------------- +Changes: +- Flashlight can now be attached on all the ranged weapons held with two hands. + Tutorial improvements: - A new campaign-integrated tutorial that teaches the basics of the campaign mode in the first outpost. 1st version: feedback and issue reports are much appreciated! - Various fixes and improvements to the Basic and Role tutorials. @@ -474,7 +564,7 @@ Changes and additions: - Added a warning if a new keybind overlaps with any of the player's existing binds. - Overvoltage makes devices perform better, increasing the output of engines, making fabricators, deconstructors and pumps operate faster, electrical discharge coils do more damage, batteries recharge faster and oxygen generators generate more oxygen. Encourages operating the reactor manually and hopefully makes it a little more engaging. - Added more randomness to junction box overvoltage damage, and made partially damaged boxes take more damage from overvoltage. Prevents all boxes from breaking at the same time, making overvoltage less of a pain to deal with and intentionally overvolting devices more worthwhile. -- Added manual temperature adjustment buttons which immediately increase/decrease the temperature of the reactor for a brief amount of time on manual control (bumps the gauge up/down by a fifth, and the boost fades out in 20 seconds). Allows reacting to load fluctuations very quickly, and conserving fuel by operating the reactor at a lower fission rate – a new benefit to operating reactors manually. +- Added manual temperature adjustment buttons which immediately increase/decrease the temperature of the reactor for a brief amount of time on manual control (bumps the gauge up/down by a fifth, and the boost fades out in 20 seconds). Allows reacting to load fluctuations very quickly, and conserving fuel by operating the reactor at a lower fission rate � a new benefit to operating reactors manually. - Signals no longer set the fission and turbine rates of the reactor instantaneously, making automated reactor circuits less overpowered. They are still viable, but especially now with the addition of the extra incentives for operating the reactor manually, they're no longer as clearly the best and most efficient way to operate the reactor, making manual operation more worthwhile. - Made the "distort" camera effect a little less obtrusive and glitchy-looking (smoother texture + less heavy effect). - Made water-sensitive materials (lithium, potassium, sodium) spawn in waterproof chemical crates. diff --git a/Libraries/Facepunch.Steamworks/SteamUgc.cs b/Libraries/Facepunch.Steamworks/SteamUgc.cs index 059f3f071..21c51a991 100644 --- a/Libraries/Facepunch.Steamworks/SteamUgc.cs +++ b/Libraries/Facepunch.Steamworks/SteamUgc.cs @@ -69,77 +69,71 @@ namespace Steamworks /// The ID of the file you want to download /// An optional callback /// Allows you to send a message to cancel the download anywhere during the process - /// How often to call the progress function + /// How often to call the progress function /// true if downloaded and installed correctly - public static async Task DownloadAsync( PublishedFileId fileId, Action progress = null, int milisecondsUpdateDelay = 60, CancellationToken ct = default ) + public static async Task DownloadAsync( + PublishedFileId fileId, + Action progress = null, + int millisecondsUpdateDelay = 60, + CancellationToken? ct = null) { var item = new Steamworks.Ugc.Item( fileId ); - if ( ct == default ) - ct = new CancellationTokenSource( TimeSpan.FromSeconds( 60 ) ).Token; + var cancellationToken = ct ?? new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token; + async Task waitOrCancel() + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Delay(millisecondsUpdateDelay); + } + progress?.Invoke( 0.0f ); - if ( Download( fileId, highPriority: true ) == false ) - return item.IsInstalled; + Result downloadStartResult = Result.None; - // Steam docs about Download: - // If the return value is true then register and wait - // for the Callback DownloadItemResult_t before calling - // GetItemInstallInfo or accessing the workshop item on disk. - - // Wait for DownloadItemResult_t + void onDownloadFinished(Result r, ulong id) { - Action onDownloadStarted = null; + if (id != item.Id) { return; } + downloadStartResult = r; + } + OnDownloadItemResult += onDownloadFinished; - try + if (!Download(fileId, highPriority: true)) { return item.IsInstalled; } + + await Task.Delay(500); + + try + { + while (true) { - var downloadStarted = false; - - onDownloadStarted = (r, id) => downloadStarted = true; - OnDownloadItemResult += onDownloadStarted; + cancellationToken.ThrowIfCancellationRequested(); - int iters = 0; - while ( downloadStarted == false ) + progress?.Invoke(item.DownloadAmount); + + if (downloadStartResult != Result.None) { - ct.ThrowIfCancellationRequested(); - - iters++; - if (iters >= 1000 / milisecondsUpdateDelay) - { - if (!item.IsDownloading && !item.IsInstalled) - { - //force download to start if it's not started - if ( Download( fileId, highPriority: true ) == false ) - return item.IsInstalled; - } - iters = 0; - } - await Task.Delay( milisecondsUpdateDelay ); + if (downloadStartResult != Result.OK) { return false; } + break; } - } - finally - { - OnDownloadItemResult -= onDownloadStarted; + + if (!item.IsDownloadPending && !item.IsDownloading) + { + if (item.IsInstalled) + { + break; + } + if (!Download(fileId, highPriority: true)) + { + return item.IsInstalled; + } + } + + await Task.Delay( millisecondsUpdateDelay ); } } - - progress?.Invoke( 0.2f ); - await Task.Delay( milisecondsUpdateDelay ); - - //Wait for downloading completion + finally { - while ( true ) - { - ct.ThrowIfCancellationRequested(); - - progress?.Invoke( 0.2f + item.DownloadAmount * 0.8f ); - - if ( !item.IsDownloading && item.IsInstalled ) - break; - - await Task.Delay( milisecondsUpdateDelay ); - } + OnDownloadItemResult -= onDownloadFinished; } progress?.Invoke( 1.0f ); diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs index 742cfbf83..6eb0a9776 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs @@ -224,7 +224,7 @@ namespace Steamworks.Ugc } } - private ItemState State => (ItemState) SteamUGC.Internal.GetItemState( Id ); + private ItemState State => (ItemState)(SteamUGC.Internal?.GetItemState( Id ) ?? 0); public static async Task GetAsync( PublishedFileId id, int maxageseconds = 60 * 30 ) {