Faction Test 100.6.0.0

This commit is contained in:
Markus Isberg
2022-11-25 19:55:45 +02:00
parent c44fb0ad3a
commit 0057f5bfce
130 changed files with 2771 additions and 1509 deletions
@@ -175,11 +175,11 @@ namespace Barotrauma
position += amount; position += amount;
} }
public void ClientWrite(IWriteMessage msg) public void ClientWrite(in SegmentTableWriter<ClientNetSegment> segmentTableWriter, IWriteMessage msg)
{ {
if (Character.Controlled != null && !Character.Controlled.IsDead) { return; } 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.X);
msg.WriteSingle(position.Y); msg.WriteSingle(position.Y);
} }
@@ -673,15 +673,6 @@ namespace Barotrauma
partial void UpdateProjSpecific(float deltaTime, Camera cam) 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) foreach (GUIMessage message in guiMessages)
{ {
bool wasPending = message.Timer < 0.0f; bool wasPending = message.Timer < 0.0f;
@@ -149,6 +149,7 @@ namespace Barotrauma
public static bool ShouldRecreateHudTexts { get; set; } = true; public static bool ShouldRecreateHudTexts { get; set; } = true;
private static bool heldDownShiftWhenGotHudTexts; private static bool heldDownShiftWhenGotHudTexts;
private static float timeHealthWindowClosed;
public static bool IsCampaignInterfaceOpen => public static bool IsCampaignInterfaceOpen =>
GameMain.GameSession?.Campaign != null && GameMain.GameSession?.Campaign != null &&
@@ -222,7 +223,8 @@ namespace Barotrauma
if (character.Info != null && !character.ShouldLockHud() && character.SelectedCharacter == null && Screen.Selected != GameMain.SubEditorScreen) if (character.Info != null && !character.ShouldLockHud() && character.SelectedCharacter == null && Screen.Selected != GameMain.SubEditorScreen)
{ {
bool mouseOnPortrait = MouseOnCharacterPortrait() && GUI.MouseOn == null; 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; 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) public static void Draw(SpriteBatch spriteBatch, Character character, Camera cam)
@@ -539,7 +550,17 @@ namespace Barotrauma
{ {
var item = character.Inventory.GetItemAt(i); var item = character.Inventory.GetItemAt(i);
if (item == null || character.Inventory.SlotTypes[i] == InvSlotType.Any) { continue; } 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) foreach (ItemComponent ic in item.Components)
{ {
if (ic.DrawHudWhenEquipped) { ic.DrawHUD(spriteBatch, character); } if (ic.DrawHudWhenEquipped) { ic.DrawHUD(spriteBatch, character); }
@@ -113,9 +113,9 @@ namespace Barotrauma
} }
} }
public void ClientWriteInput(IWriteMessage msg) public void ClientWriteInput(in SegmentTableWriter<ClientNetSegment> segmentTableWriter, IWriteMessage msg)
{ {
msg.WriteByte((byte)ClientNetObject.CHARACTER_INPUT); segmentTableWriter.StartNewSegment(ClientNetSegment.CharacterInput);
if (memInput.Count > 60) if (memInput.Count > 60)
{ {
@@ -156,7 +156,7 @@ namespace Barotrauma
Character.Controlled.DeselectCharacter(); Character.Controlled.DeselectCharacter();
} }
Character.Controlled.ResetInteract = true; Character.Controlled.DisableInteract = true;
if (openHealthWindow != null) if (openHealthWindow != null)
{ {
if (value.Character.Info == null || value.Character == Character.Controlled || Character.Controlled.HasEquippedItem("healthscanner".ToIdentifier())) if (value.Character.Info == null || value.Character == Character.Controlled || Character.Controlled.HasEquippedItem("healthscanner".ToIdentifier()))
@@ -51,11 +51,10 @@ namespace Barotrauma
&& p.InstallTime.TryUnwrap(out var installTime) && p.InstallTime.TryUnwrap(out var installTime)
&& item.LatestUpdateTime <= installTime)) && item.LatestUpdateTime <= installTime))
.ToArray(); .ToArray();
if (needInstalling.Any()) if (!needInstalling.Any()) { return Enumerable.Empty<Steamworks.Ugc.Item>(); }
{
await Task.WhenAll( await Task.WhenAll(
needInstalling.Select(SteamManager.Workshop.DownloadModThenEnqueueInstall)); needInstalling.Select(SteamManager.Workshop.DownloadModThenEnqueueInstall));
}
return needInstalling; return needInstalling;
} }
@@ -128,21 +128,17 @@ namespace Barotrauma
public static void Update(float deltaTime) public static void Update(float deltaTime)
{ {
lock (queuedMessages) while (queuedMessages.TryDequeue(out var newMsg))
{ {
while (queuedMessages.Count > 0) AddMessage(newMsg);
{
var newMsg = queuedMessages.Dequeue();
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); SaveLogs();
if (unsavedMessages.Count >= messagesPerFile) unsavedMessages.Clear();
{
SaveLogs();
unsavedMessages.Clear();
}
} }
} }
} }
@@ -258,25 +254,21 @@ namespace Barotrauma
public static void DequeueMessages() public static void DequeueMessages()
{ {
lock (queuedMessages) while (queuedMessages.TryDequeue(out var newMsg))
{ {
while (queuedMessages.Count > 0) if (listBox == null)
{ {
var newMsg = queuedMessages.Dequeue(); //don't attempt to add to the listbox if it hasn't been created yet
if (listBox == null) Messages.Add(newMsg);
{ }
//don't attempt to add to the listbox if it hasn't been created yet else
Messages.Add(newMsg); {
} AddMessage(newMsg);
else }
{
AddMessage(newMsg);
}
if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging)
{ {
unsavedMessages.Add(newMsg); unsavedMessages.Add(newMsg);
}
} }
} }
} }
@@ -12,6 +12,7 @@ namespace Barotrauma
partial void OnStateChangedProjSpecific() partial void OnStateChangedProjSpecific()
{ {
SoundPlayer.ForceMusicUpdate();
if (Phase == MissionPhase.NoItemsDestroyed) if (Phase == MissionPhase.NoItemsDestroyed)
{ {
CoroutineManager.Invoke(() => CoroutineManager.Invoke(() =>
@@ -31,6 +32,10 @@ namespace Barotrauma
} }
else if (Phase == MissionPhase.BossKilled) else if (Phase == MissionPhase.BossKilled)
{ {
if (!string.IsNullOrEmpty(endCinematicSound))
{
SoundPlayer.PlaySound(endCinematicSound);
}
CoroutineManager.Invoke(() => CoroutineManager.Invoke(() =>
{ {
new CameraTransition(boss, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: 3, fadeOut: false, endZoom: 0.1f * GUI.yScale) 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) if (limb.LightSource is Lights.LightSource light)
{ {
light.Enabled = true; light.Enabled = true;
} }
} }
} }
} }
@@ -115,6 +115,11 @@ namespace Barotrauma
}; };
} }
public Identifier GetOverrideMusicType()
{
return Prefab.GetOverrideMusicType(State);
}
public virtual void ClientRead(IReadMessage msg) public virtual void ClientRead(IReadMessage msg)
{ {
State = msg.ReadInt16(); State = msg.ReadInt16();
@@ -1,11 +1,16 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Xml.Linq; using System.Collections.Generic;
using System.Collections.Immutable;
namespace Barotrauma namespace Barotrauma
{ {
partial class MissionPrefab : PrefabWithUintIdentifier partial class MissionPrefab : PrefabWithUintIdentifier
{ {
private ImmutableArray<Sprite> portraits = new ImmutableArray<Sprite>();
public bool HasPortraits => portraits.Length > 0;
public Sprite Icon public Sprite Icon
{ {
get; get;
@@ -49,24 +54,57 @@ namespace Barotrauma
private Sprite hudIcon; private Sprite hudIcon;
private Color? hudIconColor; private Color? hudIconColor;
private ImmutableDictionary<int, Identifier> overrideMusicOnState;
partial void InitProjSpecific(ContentXElement element) partial void InitProjSpecific(ContentXElement element)
{ {
DisplayTargetHudIcons = element.GetAttributeBool("displaytargethudicons", false); DisplayTargetHudIcons = element.GetAttributeBool("displaytargethudicons", false);
HudIconMaxDistance = element.GetAttributeFloat("hudiconmaxdistance", 1000.0f); HudIconMaxDistance = element.GetAttributeFloat("hudiconmaxdistance", 1000.0f);
Dictionary<int, Identifier> overrideMusic = new Dictionary<int, Identifier>();
List<Sprite> portraits = new List<Sprite>();
foreach (var subElement in element.Elements()) foreach (var subElement in element.Elements())
{ {
string name = subElement.Name.ToString(); switch (subElement.Name.ToString().ToLowerInvariant())
if (name.Equals("icon", StringComparison.OrdinalIgnoreCase))
{ {
Icon = new Sprite(subElement); case "icon":
IconColor = subElement.GetAttributeColor("color", Color.White); Icon = new Sprite(subElement);
} IconColor = subElement.GetAttributeColor("color", Color.White);
else if (name.Equals("hudicon", StringComparison.OrdinalIgnoreCase)) break;
{ case "hudicon":
hudIcon = new Sprite(subElement); hudIcon = new Sprite(subElement);
hudIconColor = subElement.GetAttributeColor("color"); 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() partial void DisposeProjectSpecific()
@@ -335,7 +335,7 @@ namespace Barotrauma
DrawString(spriteBatch, new Vector2(10, y), DrawString(spriteBatch, new Vector2(10, y),
"FPS: " + Math.Round(GameMain.PerformanceCounter.AverageFramesPerSecond), "FPS: " + Math.Round(GameMain.PerformanceCounter.AverageFramesPerSecond),
Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); 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; y += yStep;
DrawString(spriteBatch, new Vector2(10, y), 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( Rectangle area = drawArea ?? new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight);
(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;
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); 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); pos = new Vector2(pos.X * paddingX, pos.Y * paddingY);
spriteBatch.Draw(backgroundSprite.Texture, spriteBatch.Draw(backgroundSprite.Texture,
new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 + pos, area.Center.ToVector2() + pos,
null, color, 0.0f, backgroundSprite.size / 2, null, color, 0.0f, backgroundSprite.size / 2,
scale, SpriteEffects.None, 0.0f); scale, spriteEffects, 0.0f);
} }
#region Update list #region Update list
@@ -758,7 +758,7 @@ namespace Barotrauma
toolTipBlock.DrawManually(spriteBatch); 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; } if (ObjectiveManager.ContentRunning) { return; }
@@ -775,7 +775,10 @@ namespace Barotrauma
toolTipBlock.UserData = toolTip; 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) if (toolTipBlock.Rect.Right > GameMain.GraphicsWidth - 10)
{ {
toolTipBlock.RectTransform.AbsoluteOffset -= new Point(toolTipBlock.Rect.Width, 0); toolTipBlock.RectTransform.AbsoluteOffset -= new Point(toolTipBlock.Rect.Width, 0);
@@ -11,9 +11,9 @@ namespace Barotrauma
{ {
class LoadingScreen class LoadingScreen
{ {
private readonly Texture2D defaultBackgroundTexture, overlay; private readonly Sprite defaultBackgroundTexture, overlay;
private readonly SpriteSheet decorativeGraph, decorativeMap; private readonly SpriteSheet decorativeGraph, decorativeMap;
private Texture2D currentBackgroundTexture; private Sprite currentBackgroundTexture;
private readonly Sprite noiseSprite; private readonly Sprite noiseSprite;
private string randText = ""; private string randText = "";
@@ -24,6 +24,8 @@ namespace Barotrauma
private Video currSplashScreen; private Video currSplashScreen;
private DateTime videoStartTime; private DateTime videoStartTime;
private bool mirrorBackground;
public struct PendingSplashScreen public struct PendingSplashScreen
{ {
public string Filename; public string Filename;
@@ -112,12 +114,12 @@ namespace Barotrauma
public LoadingScreen(GraphicsDevice graphics) 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)); 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)); 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); noiseSprite = new Sprite("Content/UI/noise.png", Vector2.Zero);
DrawLoadingText = true; DrawLoadingText = true;
SetSelectedTip(TextManager.Get("LoadingScreenTip")); SetSelectedTip(TextManager.Get("LoadingScreenTip"));
@@ -143,24 +145,19 @@ namespace Barotrauma
currentBackgroundTexture ??= defaultBackgroundTexture; 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); 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); GUI.DrawBackgroundSprite(spriteBatch, currentBackgroundTexture, Color.White, drawArea,
Vector2 pos = new Vector2((float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0) - 0.5f, (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0.5f) - 0.5f); spriteEffects: mirrorBackground ? SpriteEffects.FlipHorizontally : SpriteEffects.None);
pos = new Vector2(pos.X * paddingX, pos.Y * paddingY); overlay.Draw(spriteBatch, Vector2.Zero, scale: overlayScale);
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);
double noiseT = Timing.TotalTime * 0.02f;
float noiseStrength = (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0); float noiseStrength = (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0);
float noiseScale = (float)PerlinNoise.CalculatePerlin(noiseT * 5.0f, noiseT * 2.0f, 0) * 4.0f; 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), noiseSprite.DrawTiled(spriteBatch, Vector2.Zero, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight),
@@ -430,7 +427,12 @@ namespace Barotrauma
drawn = false; drawn = false;
LoadState = null; LoadState = null;
SetSelectedTip(TextManager.Get("LoadingScreenTip")); 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) while (!drawn)
{ {
@@ -134,7 +134,7 @@ namespace Barotrauma
set => hadSellSubPermissions = value; set => hadSellSubPermissions = value;
} }
private bool HasPermissionToUseTab(StoreTab tab) private static bool HasPermissionToUseTab(StoreTab tab)
{ {
return tab switch return tab switch
{ {
@@ -278,6 +278,7 @@ namespace Barotrauma
RefreshBuying(updateOwned: false); RefreshBuying(updateOwned: false);
RefreshSelling(updateOwned: false); RefreshSelling(updateOwned: false);
RefreshSellingFromSub(updateOwned: false); RefreshSellingFromSub(updateOwned: false);
SetConfirmButtonBehavior();
needsRefresh = false; needsRefresh = false;
} }
@@ -881,18 +882,17 @@ namespace Barotrauma
float prevBuyListScroll = storeBuyList.BarScroll; float prevBuyListScroll = storeBuyList.BarScroll;
float prevShoppingCrateScroll = shoppingCrateBuyList.BarScroll; float prevShoppingCrateScroll = shoppingCrateBuyList.BarScroll;
int dailySpecialCount = ActiveStore.DailySpecials.Count; int dailySpecialCount = ActiveStore?.DailySpecials.Count(s => s.CanCharacterBuy()) ?? 0;
if ((storeDailySpecialsGroup != null) != ActiveStore.DailySpecials.Any() || dailySpecialCount != prevDailySpecialCount) 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 = CreateDealsGroup(storeBuyList, dailySpecialCount);
storeDailySpecialsGroup.Parent.SetAsFirstChild(); storeDailySpecialsGroup.Parent.SetAsFirstChild();
} }
else else
{ {
storeBuyList.RemoveChild(storeDailySpecialsGroup.Parent);
storeDailySpecialsGroup = null; storeDailySpecialsGroup = null;
} }
storeBuyList.RecalculateChildren(); storeBuyList.RecalculateChildren();
@@ -901,15 +901,17 @@ namespace Barotrauma
bool hasPermissions = HasTabPermissions(StoreTab.Buy); bool hasPermissions = HasTabPermissions(StoreTab.Buy);
var existingItemFrames = new HashSet<GUIComponent>(); var existingItemFrames = new HashSet<GUIComponent>();
foreach (PurchasedItem item in ActiveStore.Stock) if (ActiveStore != null)
{ {
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); foreach (PurchasedItem item in ActiveStore.Stock)
} {
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
foreach (ItemPrefab itemPrefab in ActiveStore.DailySpecials) }
{ foreach (ItemPrefab itemPrefab in ActiveStore.DailySpecials)
if (ActiveStore.Stock.Any(pi => pi.ItemPrefab == itemPrefab)) { continue; } {
CreateOrUpdateItemFrame(itemPrefab, 0); if (ActiveStore.Stock.Any(pi => pi.ItemPrefab == itemPrefab)) { continue; }
CreateOrUpdateItemFrame(itemPrefab, 0);
}
} }
void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int quantity) void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int quantity)
@@ -969,11 +971,11 @@ namespace Barotrauma
float prevSellListScroll = storeSellList.BarScroll; float prevSellListScroll = storeSellList.BarScroll;
float prevShoppingCrateScroll = shoppingCrateSellList.BarScroll; float prevShoppingCrateScroll = shoppingCrateSellList.BarScroll;
int requestedGoodsCount = ActiveStore.RequestedGoods.Count; int requestedGoodsCount = ActiveStore?.RequestedGoods.Count ?? 0;
if ((storeRequestedGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevRequestedGoodsCount) if ((ActiveStore == null && storeRequestedGoodGroup != null) || (storeRequestedGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevRequestedGoodsCount)
{ {
storeSellList.RemoveChild(storeRequestedGoodGroup?.Parent); storeSellList.RemoveChild(storeRequestedGoodGroup?.Parent);
if (storeRequestedGoodGroup == null || requestedGoodsCount != prevRequestedGoodsCount) if (ActiveStore != null && (storeRequestedGoodGroup == null || requestedGoodsCount != prevRequestedGoodsCount))
{ {
storeRequestedGoodGroup = CreateDealsGroup(storeSellList, requestedGoodsCount); storeRequestedGoodGroup = CreateDealsGroup(storeSellList, requestedGoodsCount);
storeRequestedGoodGroup.Parent.SetAsFirstChild(); storeRequestedGoodGroup.Parent.SetAsFirstChild();
@@ -988,14 +990,17 @@ namespace Barotrauma
bool hasPermissions = HasTabPermissions(StoreTab.Sell); bool hasPermissions = HasTabPermissions(StoreTab.Sell);
var existingItemFrames = new HashSet<GUIComponent>(); var existingItemFrames = new HashSet<GUIComponent>();
foreach (PurchasedItem item in itemsToSell) if (ActiveStore != null)
{ {
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); foreach (PurchasedItem item in itemsToSell)
} {
foreach (var requestedGood in ActiveStore.RequestedGoods) CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
{ }
if (itemsToSell.Any(pi => pi.ItemPrefab == requestedGood)) { continue; } foreach (var requestedGood in ActiveStore.RequestedGoods)
CreateOrUpdateItemFrame(requestedGood, 0); {
if (itemsToSell.Any(pi => pi.ItemPrefab == requestedGood)) { continue; }
CreateOrUpdateItemFrame(requestedGood, 0);
}
} }
void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity) void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity)
@@ -1053,11 +1058,11 @@ namespace Barotrauma
float prevSellListScroll = storeSellFromSubList.BarScroll; float prevSellListScroll = storeSellFromSubList.BarScroll;
float prevShoppingCrateScroll = shoppingCrateSellFromSubList.BarScroll; float prevShoppingCrateScroll = shoppingCrateSellFromSubList.BarScroll;
int requestedGoodsCount = ActiveStore.RequestedGoods.Count; int requestedGoodsCount = ActiveStore?.RequestedGoods.Count ?? 0;
if ((storeRequestedSubGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevSubRequestedGoodsCount) if ((ActiveStore == null && storeRequestedSubGoodGroup != null) || (storeRequestedSubGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevSubRequestedGoodsCount)
{ {
storeSellFromSubList.RemoveChild(storeRequestedSubGoodGroup?.Parent); storeSellFromSubList.RemoveChild(storeRequestedSubGoodGroup?.Parent);
if (storeRequestedSubGoodGroup == null || requestedGoodsCount != prevSubRequestedGoodsCount) if (ActiveStore != null && (storeRequestedSubGoodGroup == null || requestedGoodsCount != prevSubRequestedGoodsCount))
{ {
storeRequestedSubGoodGroup = CreateDealsGroup(storeSellFromSubList, requestedGoodsCount); storeRequestedSubGoodGroup = CreateDealsGroup(storeSellFromSubList, requestedGoodsCount);
storeRequestedSubGoodGroup.Parent.SetAsFirstChild(); storeRequestedSubGoodGroup.Parent.SetAsFirstChild();
@@ -1072,14 +1077,17 @@ namespace Barotrauma
bool hasPermissions = HasSellSubPermissions; bool hasPermissions = HasSellSubPermissions;
var existingItemFrames = new HashSet<GUIComponent>(); var existingItemFrames = new HashSet<GUIComponent>();
foreach (PurchasedItem item in itemsToSellFromSub) if (ActiveStore != null)
{ {
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); foreach (PurchasedItem item in itemsToSellFromSub)
} {
foreach (var requestedGood in ActiveStore.RequestedGoods) CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
{ }
if (itemsToSellFromSub.Any(pi => pi.ItemPrefab == requestedGood)) { continue; } foreach (var requestedGood in ActiveStore.RequestedGoods)
CreateOrUpdateItemFrame(requestedGood, 0); {
if (itemsToSellFromSub.Any(pi => pi.ItemPrefab == requestedGood)) { continue; }
CreateOrUpdateItemFrame(requestedGood, 0);
}
} }
void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity) void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity)
@@ -1166,6 +1174,7 @@ namespace Barotrauma
public void RefreshItemsToSell() public void RefreshItemsToSell()
{ {
itemsToSell.Clear(); itemsToSell.Clear();
if (ActiveStore == null) { return; }
var playerItems = CargoManager.GetSellableItems(Character.Controlled); var playerItems = CargoManager.GetSellableItems(Character.Controlled);
foreach (Item playerItem in playerItems) foreach (Item playerItem in playerItems)
{ {
@@ -1196,6 +1205,7 @@ namespace Barotrauma
public void RefreshItemsToSellFromSub() public void RefreshItemsToSellFromSub()
{ {
itemsToSellFromSub.Clear(); itemsToSellFromSub.Clear();
if (ActiveStore == null) { return; }
var subItems = CargoManager.GetSellableItemsFromSub(); var subItems = CargoManager.GetSellableItemsFromSub();
foreach (Item subItem in subItems) foreach (Item subItem in subItems)
{ {
@@ -1229,52 +1239,55 @@ namespace Barotrauma
bool hasPermissions = HasTabPermissions(tab); bool hasPermissions = HasTabPermissions(tab);
HashSet<GUIComponent> existingItemFrames = new HashSet<GUIComponent>(); HashSet<GUIComponent> existingItemFrames = new HashSet<GUIComponent>();
int totalPrice = 0; int totalPrice = 0;
foreach (PurchasedItem item in items) if (ActiveStore != null)
{ {
if (!(item.ItemPrefab.GetPriceInfo(ActiveStore) is { } priceInfo)) { continue; } foreach (PurchasedItem item in items)
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); if (!(item.ItemPrefab.GetPriceInfo(ActiveStore) is { } priceInfo)) { continue; }
numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput; GUINumberInput numInput = null;
} if (!(listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier) is { } itemFrame))
else {
{ itemFrame = CreateItemFrame(item, listBox, tab, forceDisable: !hasPermissions);
itemFrame.UserData = item; numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput;
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) if (numInput != null)
{ {
numInput.UserData = item; if (numInput.IntValue != item.Quantity) { itemFrame.Flash(GUIStyle.Green); }
numInput.Enabled = hasPermissions; numInput.IntValue = item.Quantity;
numInput.MaxValueInt = GetMaxAvailable(item.ItemPrefab, tab);
} }
SetOwnedText(itemFrame); suppressBuySell = false;
SetItemFrameStatus(itemFrame, hasPermissions);
}
existingItemFrames.Add(itemFrame);
suppressBuySell = true; try
if (numInput != null)
{
if (numInput.IntValue != item.Quantity) { itemFrame.Flash(GUIStyle.Green); }
numInput.IntValue = item.Quantity;
}
suppressBuySell = false;
try
{
int price = tab switch
{ {
StoreTab.Buy => ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo), int price = tab switch
StoreTab.Sell => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), {
StoreTab.SellSub => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), StoreTab.Buy => ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo),
_ => throw new NotImplementedException() StoreTab.Sell => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo),
}; StoreTab.SellSub => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo),
totalPrice += item.Quantity * price; _ => throw new NotImplementedException()
} };
catch (NotImplementedException e) totalPrice += item.Quantity * price;
{ }
DebugConsole.LogError($"Error getting item price: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); 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) private void SortItems(GUIListBox list, SortingMethod sortingMethod)
{ {
if (CurrentLocation == null) { return; } if (CurrentLocation == null || ActiveStore == null) { return; }
if (sortingMethod == SortingMethod.AlphabeticalAsc || sortingMethod == SortingMethod.AlphabeticalDesc) if (sortingMethod == SortingMethod.AlphabeticalAsc || sortingMethod == SortingMethod.AlphabeticalDesc)
{ {
@@ -1707,6 +1720,8 @@ namespace Barotrauma
{ {
OwnedItems.Clear(); OwnedItems.Clear();
if (ActiveStore == null) { return; }
// Add items on the sub(s) // Add items on the sub(s)
if (Submarine.MainSub?.GetItems(true) is List<Item> subItems) if (Submarine.MainSub?.GetItems(true) is List<Item> subItems)
{ {
@@ -2124,7 +2139,12 @@ namespace Barotrauma
private void SetShoppingCrateTotalText() 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.Text = TextManager.FormatCurrency(buyTotal);
shoppingCrateTotal.TextColor = Balance < buyTotal ? Color.Red : Color.White; shoppingCrateTotal.TextColor = Balance < buyTotal ? Color.Red : Color.White;
@@ -2144,7 +2164,11 @@ namespace Barotrauma
private void SetConfirmButtonBehavior() private void SetConfirmButtonBehavior()
{ {
if (IsBuying) if (ActiveStore == null)
{
confirmButton.OnClicked = null;
}
else if (IsBuying)
{ {
confirmButton.ClickSound = GUISoundType.ConfirmTransaction; confirmButton.ClickSound = GUISoundType.ConfirmTransaction;
confirmButton.Text = TextManager.Get("CampaignStore.Purchase"); confirmButton.Text = TextManager.Get("CampaignStore.Purchase");
@@ -2172,6 +2196,7 @@ namespace Barotrauma
private void SetConfirmButtonStatus() private void SetConfirmButtonStatus()
{ {
confirmButton.Enabled = confirmButton.Enabled =
ActiveStore != null &&
HasActiveTabPermissions() && HasActiveTabPermissions() &&
ActiveShoppingCrateList.Content.RectTransform.Children.Any() && ActiveShoppingCrateList.Content.RectTransform.Children.Any() &&
activeTab switch activeTab switch
@@ -2181,6 +2206,7 @@ namespace Barotrauma
StoreTab.SellSub => CurrentLocation != null && sellFromSubTotal <= ActiveStore.Balance, StoreTab.SellSub => CurrentLocation != null && sellFromSubTotal <= ActiveStore.Balance,
_ => false _ => false
}; };
confirmButton.Visible = ActiveStore != null;
} }
private void SetClearAllButtonStatus() private void SetClearAllButtonStatus()
@@ -2254,29 +2280,32 @@ namespace Barotrauma
prevBalance = currBalance; prevBalance = currBalance;
} }
} }
if (needsItemsToSellRefresh) if (ActiveStore != null)
{ {
RefreshItemsToSell(); if (needsItemsToSellRefresh)
} {
if (needsItemsToSellFromSubRefresh) RefreshItemsToSell();
{ }
RefreshItemsToSellFromSub(); if (needsItemsToSellFromSubRefresh)
} {
if (needsRefresh) RefreshItemsToSellFromSub();
{ }
Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f); if (needsRefresh)
} {
if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy)) Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f);
{ }
RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f); if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy))
} {
if (needsSellingRefresh || HavePermissionsChanged(StoreTab.Sell)) RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f);
{ }
RefreshSelling(updateOwned: ownedItemsUpdateTimer > 0.0f); if (needsSellingRefresh || HavePermissionsChanged(StoreTab.Sell))
} {
if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub)) RefreshSelling(updateOwned: ownedItemsUpdateTimer > 0.0f);
{ }
RefreshSellingFromSub(updateOwned: ownedItemsUpdateTimer > 0.0f, updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f); if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub))
{
RefreshSellingFromSub(updateOwned: ownedItemsUpdateTimer > 0.0f, updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f);
}
} }
updateStopwatch.Stop(); updateStopwatch.Stop();
@@ -1491,7 +1491,11 @@ namespace Barotrauma
GUIFrame missionFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); GUIFrame missionFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox");
int padding = (int)(0.0245f * missionFrame.Rect.Height); 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); 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)) GUILayoutGroup locationInfoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), missionFrameContent.RectTransform))
{ {
@@ -283,7 +283,7 @@ namespace Barotrauma
break; break;
case TalentTreeType.Specialization: case TalentTreeType.Specialization:
talentList = GetSpecializationList(); 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; break;
default: default:
throw new ArgumentOutOfRangeException($"Invalid TalentTreeType \"{subTree.Type}\""); throw new ArgumentOutOfRangeException($"Invalid TalentTreeType \"{subTree.Type}\"");
@@ -325,7 +325,9 @@ namespace Barotrauma
} }
var specializationList = GetSpecializationList(); 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); GUITextBlock.AutoScaleAndNormalize(subTreeNames);
@@ -16,6 +16,7 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading; using System.Threading;
using Barotrauma.Extensions;
namespace Barotrauma namespace Barotrauma
{ {
@@ -465,10 +466,11 @@ namespace Barotrauma
LegacySteamUgcTransition.Prepare(); LegacySteamUgcTransition.Prepare();
var contentPackageLoadRoutine = ContentPackageManager.Init(); 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; 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; yield return CoroutineStatus.Running;
} }
@@ -1078,10 +1080,9 @@ namespace Barotrauma
if (GameSession != null) if (GameSession != null)
{ {
double roundDuration = Timing.TotalTime - GameSession.RoundStartTime;
GameAnalyticsManager.AddProgressionEvent(GameAnalyticsManager.ProgressionStatus.Fail, GameAnalyticsManager.AddProgressionEvent(GameAnalyticsManager.ProgressionStatus.Fail,
GameSession.GameMode?.Preset.Identifier.Value ?? "none", GameSession.GameMode?.Preset.Identifier.Value ?? "none",
roundDuration); GameSession.RoundDuration);
string eventId = "QuitRound:" + (GameSession.GameMode?.Preset.Identifier.Value ?? "none") + ":"; string eventId = "QuitRound:" + (GameSession.GameMode?.Preset.Identifier.Value ?? "none") + ":";
GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:CurrentIntensity", GameSession.EventManager.CurrentIntensity); GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:CurrentIntensity", GameSession.EventManager.CurrentIntensity);
foreach (var activeEvent in GameSession.EventManager.ActiveEvents) foreach (var activeEvent in GameSession.EventManager.ActiveEvents)
@@ -11,7 +11,15 @@ namespace Barotrauma
private List<SoldEntity> SoldEntities { get; } = new List<SoldEntity>(); private List<SoldEntity> SoldEntities { get; } = new List<SoldEntity>();
// The bag slot is intentionally left out since we want to be able to sell items from there // The bag slot is intentionally left out since we want to be able to sell items from there
private readonly List<InvSlotType> equipmentSlots = new List<InvSlotType>() { InvSlotType.Head, InvSlotType.InnerClothes, InvSlotType.OuterClothes, InvSlotType.Headset, InvSlotType.Card }; private static readonly HashSet<InvSlotType> equipmentSlots = new HashSet<InvSlotType>()
{
InvSlotType.Head,
InvSlotType.InnerClothes,
InvSlotType.OuterClothes,
InvSlotType.Headset,
InvSlotType.Card,
InvSlotType.HealthInterface
};
public IEnumerable<Item> GetSellableItems(Character character) public IEnumerable<Item> GetSellableItems(Character character)
{ {
@@ -432,7 +432,7 @@ namespace Barotrauma
break; break;
} }
Map.ProgressWorld(this, transitionType, (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime)); Map.ProgressWorld(this, transitionType, GameMain.GameSession.RoundDuration);
GUI.ClearMessages(); GUI.ClearMessages();
@@ -296,7 +296,7 @@ static class ObjectiveManager
}; };
for (int i = 0; i < activeObjectives.Count; i++) 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); screenSettings = new ScreenSettings(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Scale, GameSettings.CurrentConfig.Graphics.DisplayMode);
} }
@@ -318,7 +318,7 @@ static class ObjectiveManager
/// <summary> /// <summary>
/// Adds the segment to the objective list /// Adds the segment to the objective list
/// </summary> /// </summary>
private static void AddToObjectiveList(Segment segment, bool connectExisting = false) private static void AddToObjectiveList(Segment segment, bool connectExisting = false, bool useExistingIndex = false)
{ {
if (connectExisting) if (connectExisting)
{ {
@@ -338,8 +338,8 @@ static class ObjectiveManager
if (parentSegment is not null) if (parentSegment is not null)
{ {
// Add this child as the last child in case there are other existing children already // 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 = useExistingIndex ? activeObjectives.IndexOf(segment) :
int childIndex = activeObjectives.IndexOf(parentSegment) + totalChildren; activeObjectives.IndexOf(parentSegment) + activeObjectives.Count(s => s.ParentId == segment.ParentId);
if (objectiveGroup.RectTransform.GetChildIndex(frameRt) != childIndex) if (objectiveGroup.RectTransform.GetChildIndex(frameRt) != childIndex)
{ {
frameRt.RepositionChildInHierarchy(childIndex); frameRt.RepositionChildInHierarchy(childIndex);
@@ -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 //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) //(in that case, the quick action should just fill up the stack)
bool disallowSwapping = bool disallowSwapping =
heldItem.OwnInventory.Capacity == 1 && (heldItem.OwnInventory.Capacity == 1 || heldItem.OwnInventory.Container.HasSubContainers) &&
heldItem.OwnInventory.GetItemAt(0)?.Prefab == item.Prefab && heldItem.OwnInventory.GetItemAt(0)?.Prefab == item.Prefab &&
heldItem.OwnInventory.GetItemsAt(0).Count() > 1; heldItem.OwnInventory.GetItemsAt(0).Count() > 1;
if (heldItem.OwnInventory.TryPutItem(item, Character.Controlled) || 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); GUI.DrawRectangle(spriteBatch, inventoryArea, new Color(30,30,30,100), isFilled: true);
var lockIcon = GUIStyle.GetComponentStyle("LockIcon")?.GetDefaultSprite(); var lockIcon = GUIStyle.GetComponentStyle("LockIcon")?.GetDefaultSprite();
lockIcon?.Draw(spriteBatch, inventoryArea.Center.ToVector2(), scale: Math.Min(inventoryArea.Height / lockIcon.size.Y * 0.7f, 1.0f)); 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))); GUIComponent.DrawToolTip(spriteBatch, TextManager.Get("handcuffed"), new Rectangle(inventoryArea.Center - new Point(inventoryArea.Height / 2), new Point(inventoryArea.Height)));
} }
@@ -408,6 +408,8 @@ namespace Barotrauma.Items.Components
var wire = it.GetComponent<Wire>(); var wire = it.GetComponent<Wire>();
if (wire != null && wire.Connections.Any(c => c != null)) { return false; } if (wire != null && wire.Connections.Any(c => c != null)) { return false; }
if (it.Container?.GetComponent<ItemContainer>() is { DrawInventory: false }) { return false; }
if (it.HasTag("traitormissionitem")) { return false; } if (it.HasTag("traitormissionitem")) { return false; }
return true; return true;
@@ -29,7 +29,11 @@ namespace Barotrauma.Items.Components
private Sprite tempRangeIndicator; private Sprite tempRangeIndicator;
private Sprite graphLine; 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 optimalRangeColor = new Color(74,238,104,255);
private Color offRangeColor = Color.Orange; private Color offRangeColor = Color.Orange;
@@ -66,6 +70,8 @@ namespace Barotrauma.Items.Components
}; };
public override bool RecreateGUIOnResolutionChange => true; public override bool RecreateGUIOnResolutionChange => true;
public bool TriggerInfographic { get; set; }
partial void InitProjSpecific(ContentXElement element) partial void InitProjSpecific(ContentXElement element)
{ {
@@ -122,7 +128,7 @@ namespace Barotrauma.Items.Components
//left column //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), MinSize = new Point(85, 220),
RelativeOffset = new Vector2(-0.02f, 0) RelativeOffset = new Vector2(-0.02f, 0)
@@ -255,7 +261,7 @@ namespace Barotrauma.Items.Components
}; };
TurbineOutputScrollBar.Frame.UserData = UIHighlightAction.ElementId.TurbineOutputSlider; 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, Stretch = true,
RelativeSpacing = 0.02f RelativeSpacing = 0.02f
@@ -436,8 +442,8 @@ namespace Barotrauma.Items.Components
LocalizedString kW = TextManager.Get("kilowatt"); LocalizedString kW = TextManager.Get("kilowatt");
loadText.TextGetter += () => $"{loadStr.Replace("[kw]", ((int)Load).ToString())} {kW}"; 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"); var graphFrame = 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); 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), var outputText = new GUITextBlock(new RectTransform(relativeTextSize, graphArea.RectTransform),
"Output", textColor: outputColor, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) "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}"; outputText.TextGetter += () => $"{outputStr.Replace("[kw]", ((int)-currPowerConsumption).ToString())} {kW}";
InitInventoryUI(); 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() private void InitInventoryUI()
@@ -469,7 +491,6 @@ namespace Barotrauma.Items.Components
InitInventoryUI(); InitInventoryUI();
} }
private void DrawTempMeter(SpriteBatch spriteBatch, GUICustomComponent container) private void DrawTempMeter(SpriteBatch spriteBatch, GUICustomComponent container)
{ {
Vector2 meterPos = new Vector2(container.Rect.X, container.Rect.Y); 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); DrawGraph(loadGraph, spriteBatch, graphRect, Math.Max(10000.0f, maxLoad), xOffset, loadColor);
} }
private void UpdateGraph(float deltaTime) private void UpdateGraph(float deltaTime)
{ {
graphTimer += deltaTime * 1000.0f; 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) 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.GraphicsDevice.ScissorRectangle = prevScissorRect;
spriteBatch.Begin(SpriteSortMode.Deferred); 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<string, GUIImage>()
{
{ "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() protected override void RemoveComponentSpecific()
{ {
base.RemoveComponentSpecific(); base.RemoveComponentSpecific();
@@ -817,7 +817,7 @@ namespace Barotrauma.Items.Components
if (distSqr > t.SoundRange * t.SoundRange * 2) { continue; } if (distSqr > t.SoundRange * t.SoundRange * 2) { continue; }
float dist = (float)Math.Sqrt(distSqr); 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; int prevBlipCount = sonarBlips.Count;
Ping(t.WorldPosition, transducerCenter, Ping(t.WorldPosition, transducerCenter,
@@ -35,6 +35,13 @@ namespace Barotrauma.Items.Components
if (GuiFrame == null) { return; } if (GuiFrame == null) { return; }
originalMaxSize = GuiFrame.RectTransform.MaxSize; originalMaxSize = GuiFrame.RectTransform.MaxSize;
originalRelativeSize = GuiFrame.RectTransform.RelativeSize; originalRelativeSize = GuiFrame.RectTransform.RelativeSize;
CreateGUI();
}
protected override void CreateGUI()
{
if (GuiFrame == null) { return; }
CheckForLabelOverlap(); CheckForLabelOverlap();
var content = new GUICustomComponent(new RectTransform(Vector2.One, GuiFrame.RectTransform), DrawConnections, null) var content = new GUICustomComponent(new RectTransform(Vector2.One, GuiFrame.RectTransform), DrawConnections, null)
{ {
@@ -43,8 +50,8 @@ namespace Barotrauma.Items.Components
content.RectTransform.SetAsFirstChild(); content.RectTransform.SetAsFirstChild();
//prevents inputs from going through the GUICustomComponent to the drag handle //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) dragArea = new GUIFrame(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center)
{ AbsoluteOffset = GUIStyle.ItemFrameOffset }, style: null); { AbsoluteOffset = GUIStyle.ItemFrameOffset }, style: null);
} }
public void TriggerRewiringSound() public void TriggerRewiringSound()
@@ -121,12 +128,6 @@ namespace Barotrauma.Items.Components
} }
} }
protected override void OnResolutionChanged()
{
if (GuiFrame == null) { return; }
CheckForLabelOverlap();
}
private void CheckForLabelOverlap() private void CheckForLabelOverlap()
{ {
GuiFrame.RectTransform.MaxSize = originalMaxSize; GuiFrame.RectTransform.MaxSize = originalMaxSize;
@@ -324,7 +324,7 @@ namespace Barotrauma.Items.Components
if (Character.Controlled != null) if (Character.Controlled != null)
{ {
Character.Controlled.FocusedItem = null; Character.Controlled.FocusedItem = null;
Character.Controlled.ResetInteract = true; Character.Controlled.DisableInteract = true;
Character.Controlled.ClearInputs(); Character.Controlled.ClearInputs();
} }
//cancel dragging //cancel dragging
@@ -401,7 +401,7 @@ namespace Barotrauma.Items.Components
{ {
if (Character.Controlled != null) if (Character.Controlled != null)
{ {
Character.Controlled.ResetInteract = true; Character.Controlled.DisableInteract = true;
Character.Controlled.ClearInputs(); Character.Controlled.ClearInputs();
} }
int closestSectionIndex = selectedWire.GetClosestSectionIndex(mousePos, sectionSelectDist, out _); int closestSectionIndex = selectedWire.GetClosestSectionIndex(mousePos, sectionSelectDist, out _);
@@ -431,7 +431,7 @@ namespace Barotrauma.Items.Components
{ {
if (Character.Controlled != null) if (Character.Controlled != null)
{ {
Character.Controlled.ResetInteract = true; Character.Controlled.DisableInteract = true;
Character.Controlled.ClearInputs(); Character.Controlled.ClearInputs();
} }
draggingWire = selectedWire; draggingWire = selectedWire;
@@ -1287,7 +1287,8 @@ namespace Barotrauma
{ {
foreach (ItemComponent ic in activeHUDs) 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; ic.GuiFrame.RectTransform.ScreenSpaceOffset = Point.Zero;
if (ic.UseAlternativeLayout) if (ic.UseAlternativeLayout)
{ {
@@ -73,6 +73,8 @@ namespace Barotrauma
private RichString beaconStationActiveText, beaconStationInactiveText; private RichString beaconStationActiveText, beaconStationInactiveText;
private GUIComponent locationInfoOverlay;
/*private (Rectangle targetArea, string tip)? connectionTooltip; /*private (Rectangle targetArea, string tip)? connectionTooltip;
private string sanitizedConnectionTooltip; private string sanitizedConnectionTooltip;
private List<RichTextData> connectionTooltipRichTextData; private List<RichTextData> 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() partial void ClearAnimQueue()
{ {
mapAnimQueue.Clear(); mapAnimQueue.Clear();
@@ -425,38 +545,81 @@ namespace Barotrauma
Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y); Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y);
Vector2 viewOffset = DrawOffset + drawOffsetNoise; Vector2 viewOffset = DrawOffset + drawOffsetNoise;
float closestDist = 0.0f;
HighlightedLocation = null; bool cursorOnOverlay = false;
if (GUI.MouseOn == null || GUI.MouseOn == mapContainer) 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]; CreateLocationInfoOverlay(HighlightedLocation);
if (IsInFogOfWar(location) && !(currentDisplayLocation?.Connections.Any(c => c.Locations.Contains(location)) ?? false) && !GameMain.DebugDraw) { continue; } }
Vector2 pos = rectCenter + (location.MapPosition + viewOffset) * zoom; Point offsetFromLocationIcon = new Point(GUI.IntScale(25));
if (!rect.Contains(pos)) { continue; } 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; Rectangle highlightedLocationRect = new Rectangle(highlightedLocationDrawPos.ToPoint(), new Point(GUI.IntScale(25)));
float iconScale = generationParams.LocationIconSize / locationSprite.size.X; Rectangle overlayRect = Rectangle.Union(highlightedLocationRect, locationInfoRt.Rect);
if (location == currentDisplayLocation) { iconScale *= 1.2f; } if (overlayRect.Contains(PlayerInput.MousePosition))
{
cursorOnOverlay = true;
}
locationInfoOverlay?.AddToGUIUpdateList(order: 1);
}
Rectangle drawRect = locationSprite.SourceRect; if (!cursorOnOverlay)
drawRect.Width = (int)(drawRect.Width * iconScale * zoom * 1.4f); {
drawRect.Height = (int)(drawRect.Height * iconScale * zoom * 1.4f); float closestDist = 0.0f;
drawRect.X = (int)pos.X - drawRect.Width / 2; HighlightedLocation = null;
drawRect.Y = (int)pos.Y - drawRect.Width / 2; if ((GUI.MouseOn == null || GUI.MouseOn == mapContainer))
{
if (!drawRect.Contains(PlayerInput.MousePosition)) { continue; } for (int i = 0; i < Locations.Count; i++)
float dist = Vector2.Distance(PlayerInput.MousePosition, pos);
if (HighlightedLocation == null || dist < closestDist)
{ {
closestDist = dist; Location location = Locations[i];
HighlightedLocation = location; 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) if (SelectedConnection != null)
{ {
@@ -779,106 +942,6 @@ namespace Barotrauma
GUIComponent.DrawToolTip(spriteBatch, tooltip.Value.tip, tooltip.Value.targetArea); GUIComponent.DrawToolTip(spriteBatch, tooltip.Value.tip, tooltip.Value.targetArea);
drawRadiationTooltip = false; 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) if (drawRadiationTooltip)
{ {
@@ -1055,19 +1055,11 @@ namespace Barotrauma
protected static void PositionEditingHUD() protected static void PositionEditingHUD()
{ {
int maxHeight = 100; int maxHeight =
if (Screen.Selected == GameMain.SubEditorScreen) Screen.Selected == GameMain.SubEditorScreen ?
{ GameMain.GraphicsHeight - GameMain.SubEditorScreen.EntityMenu.Rect.Height - GameMain.SubEditorScreen.TopPanel.Rect.Bottom * 2 - 20 :
editingHUD.RectTransform.SetPosition(Anchor.TopRight); HUDLayoutSettings.InventoryAreaLower.Y - HUDLayoutSettings.CrewArea.Bottom - 10;
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;
}
var listBox = editingHUD.GetChild<GUIListBox>(); var listBox = editingHUD.GetChild<GUIListBox>();
if (listBox != null) if (listBox != null)
@@ -1087,6 +1079,17 @@ namespace Barotrauma
MathHelper.Clamp(contentHeight + padding * 2, 50, maxHeight)), resizeChildren: false); 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); 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) { } public virtual void DrawEditing(SpriteBatch spriteBatch, Camera cam) { }
@@ -6,9 +6,9 @@ namespace Barotrauma.Networking
{ {
partial class ChatMessage partial class ChatMessage
{ {
public virtual void ClientWrite(IWriteMessage msg) public virtual void ClientWrite(in SegmentTableWriter<ClientNetSegment> segmentTableWriter, IWriteMessage msg)
{ {
msg.WriteByte((byte)ClientNetObject.CHAT_MESSAGE); segmentTableWriter.StartNewSegment(ClientNetSegment.ChatMessage);
msg.WriteUInt16(NetStateID); msg.WriteUInt16(NetStateID);
msg.WriteRangedInteger((int)Type, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); msg.WriteRangedInteger((int)Type, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1);
msg.WriteRangedInteger((int)ChatMode, 0, Enum.GetValues(typeof(ChatMode)).Length - 1); msg.WriteRangedInteger((int)ChatMode, 0, Enum.GetValues(typeof(ChatMode)).Length - 1);
@@ -485,15 +485,11 @@ namespace Barotrauma.Networking
} }
catch (Exception e) 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."; } if (GameMain.Client == null) { errorMsg += "Client disposed."; }
errorMsg += "\n" + e.StackTrace.CleanupStackTrace(); AppendExceptionInfo(ref errorMsg, e);
if (e.InnerException != null)
{
errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace();
}
GameAnalyticsManager.AddErrorEventOnce("GameClient.Update:CheckServerMessagesException" + e.TargetSite.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); 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()))) new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariables("MessageReadError", ("[message]", e.Message), ("[targetsite]", e.TargetSite.ToString())))
{ {
DisplayInLoadingScreens = true DisplayInLoadingScreens = true
@@ -636,14 +632,8 @@ namespace Barotrauma.Networking
} }
catch (Exception e) catch (Exception e)
{ {
string errorMsg = "Error while reading an ingame update message from server. {" + e + "}\n" + e.StackTrace.CleanupStackTrace(); string errorMsg = "Error while reading an ingame update message from server.";
if (e.InnerException != null) AppendExceptionInfo(ref errorMsg, e);
{
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
GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadDataMessage:ReadIngameUpdate", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadDataMessage:ReadIngameUpdate", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw; throw;
} }
@@ -872,22 +862,31 @@ namespace Barotrauma.Networking
} }
byte missionCount = inc.ReadByte(); 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<Identifier> serverMissionIdentifiers = new List<Identifier>(); List<Identifier> serverMissionIdentifiers = new List<Identifier>();
for (int i = 0; i < missionCount; i++) for (int i = 0; i < missionCount; i++)
{ {
serverMissionIdentifiers.Add(inc.ReadIdentifier()); 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 (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); GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw new Exception(errorMsg); throw new Exception(errorMsg);
} }
@@ -1883,12 +1882,11 @@ namespace Barotrauma.Networking
private void ReadLobbyUpdate(IReadMessage inc) private void ReadLobbyUpdate(IReadMessage inc)
{ {
ServerNetObject objHeader; SegmentTableReader<ServerNetSegment>.Read(inc, (segment, inc) =>
while ((objHeader = (ServerNetObject)inc.ReadByte()) != ServerNetObject.END_OF_MESSAGE)
{ {
switch (objHeader) switch (segment)
{ {
case ServerNetObject.SYNC_IDS: case ServerNetSegment.SyncIds:
bool lobbyUpdated = inc.ReadBoolean(); bool lobbyUpdated = inc.ReadBoolean();
inc.ReadPadBits(); inc.ReadPadBits();
@@ -2020,17 +2018,19 @@ namespace Barotrauma.Networking
lastSentChatMsgID = inc.ReadUInt16(); lastSentChatMsgID = inc.ReadUInt16();
break; break;
case ServerNetObject.CLIENT_LIST: case ServerNetSegment.ClientList:
ReadClientList(inc); ReadClientList(inc);
break; break;
case ServerNetObject.CHAT_MESSAGE: case ServerNetSegment.ChatMessage:
ChatMessage.ClientRead(inc); ChatMessage.ClientRead(inc);
break; break;
case ServerNetObject.VOTE: case ServerNetSegment.Vote:
Voting.ClientRead(inc); Voting.ClientRead(inc);
break; break;
} }
}
return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.No;
});
} }
readonly List<IServerSerializable> debugEntityList = new List<IServerSerializable>(); readonly List<IServerSerializable> debugEntityList = new List<IServerSerializable>();
@@ -2040,117 +2040,106 @@ namespace Barotrauma.Networking
float sendingTime = inc.ReadSingle() - 0.0f;//TODO: reimplement inc.SenderConnection.RemoteTimeOffset; float sendingTime = inc.ReadSingle() - 0.0f;//TODO: reimplement inc.SenderConnection.RemoteTimeOffset;
ServerNetObject? prevObjHeader = null; SegmentTableReader<ServerNetSegment>.Read(inc,
long prevBitPos = 0; segmentDataReader: (segment, inc) =>
long prevBytePos = 0;
long prevBitLength = 0;
long prevByteLength = 0;
ServerNetObject? objHeader = null;
try
{ {
while ((objHeader = (ServerNetObject)inc.ReadByte()) != ServerNetObject.END_OF_MESSAGE) switch (segment)
{ {
switch (objHeader) case ServerNetSegment.SyncIds:
{ lastSentChatMsgID = inc.ReadUInt16();
case ServerNetObject.SYNC_IDS: LastSentEntityEventID = inc.ReadUInt16();
lastSentChatMsgID = inc.ReadUInt16();
LastSentEntityEventID = inc.ReadUInt16();
bool campaignUpdated = inc.ReadBoolean(); bool campaignUpdated = inc.ReadBoolean();
inc.ReadPadBits(); inc.ReadPadBits();
if (campaignUpdated) if (campaignUpdated)
{ {
MultiPlayerCampaign.ClientRead(inc); MultiPlayerCampaign.ClientRead(inc);
} }
else if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign) else if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign)
{ {
GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null); GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null);
} }
break; break;
case ServerNetObject.ENTITY_POSITION: case ServerNetSegment.EntityPosition:
inc.ReadPadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly inc.ReadPadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly
bool isItem = inc.ReadBoolean(); inc.ReadPadBits(); bool isItem = inc.ReadBoolean(); inc.ReadPadBits();
UInt32 incomingUintIdentifier = inc.ReadUInt32(); UInt32 incomingUintIdentifier = inc.ReadUInt32();
UInt16 id = inc.ReadUInt16(); UInt16 id = inc.ReadUInt16();
uint msgLength = inc.ReadVariableUInt32(); uint msgLength = inc.ReadVariableUInt32();
int msgEndPos = (int)(inc.BitPosition + msgLength * 8); int msgEndPos = (int)(inc.BitPosition + msgLength * 8);
var entity = Entity.FindEntityByID(id) as IServerPositionSync; var entity = Entity.FindEntityByID(id) as IServerPositionSync;
if (msgEndPos > inc.LengthBits) 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."); DebugConsole.ThrowError($"Error while reading a position update for the entity \"({entity?.ToString() ?? "null"})\". Message length exceeds the size of the buffer.");
return; return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.Yes;
} }
debugEntityList.Add(entity); debugEntityList.Add(entity);
if (entity != null) 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...");
{
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);
}
} }
else if (entity is MapEntity { Prefab: { UintIdentifier: { } uintIdentifier } } me &&
//force to the correct position in case the entity doesn't exist uintIdentifier != incomingUintIdentifier)
//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))
{ {
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; else
case ServerNetObject.CHAT_MESSAGE: {
ChatMessage.ClientRead(inc); entity.ClientReadPosition(inc, sendingTime);
break; }
default: }
throw new Exception($"Unknown object header \"{objHeader}\"!)");
}
prevBitLength = inc.BitPosition - prevBitPos;
prevByteLength = inc.BytePosition - prevBytePos;
prevObjHeader = objHeader; //force to the correct position in case the entity doesn't exist
prevBitPos = inc.BitPosition; //or the message wasn't read correctly for whatever reason
prevBytePos = inc.BytePosition; 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<ServerNetSegment>.BreakSegmentReading.Yes;
}
break;
case ServerNetSegment.ChatMessage:
ChatMessage.ClientRead(inc);
break;
default:
throw new Exception($"Unknown segment \"{segment}\"!)");
} }
}
catch (Exception ex) return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.No;
},
exceptionHandler: (segment, prevSegments, ex) =>
{ {
List<string> errorLines = new List<string> List<string> errorLines = new List<string>
{ {
ex.Message, ex.Message,
"Message length: " + inc.LengthBits + " (" + inc.LengthBytes + " bytes)", "Message length: " + inc.LengthBits + " (" + inc.LengthBytes + " bytes)",
"Read position: " + inc.BitPosition, "Read position: " + inc.BitPosition,
"Header: " + (objHeader != null ? objHeader.Value.ToString() : "Error occurred on the very first header!"), $"Segment with error: {segment}"
prevObjHeader != null ? "Previous header: " + prevObjHeader : "Error occurred on the very first header!",
"Previous object was " + (prevBitLength) + " bits long (" + (prevByteLength) + " bytes)",
" "
}; };
if (prevSegments.Any())
{
errorLines.Add("Prev segments: " + string.Join(", ", prevSegments));
errorLines.Add(" ");
}
errorLines.Add(ex.StackTrace.CleanupStackTrace()); errorLines.Add(ex.StackTrace.CleanupStackTrace());
errorLines.Add(" "); errorLines.Add(" ");
if (prevObjHeader == ServerNetObject.ENTITY_EVENT || prevObjHeader == ServerNetObject.ENTITY_EVENT_INITIAL || if (prevSegments.Concat(segment.ToEnumerable()).Any(s => s.Identifier
objHeader == ServerNetObject.ENTITY_EVENT || objHeader == ServerNetObject.ENTITY_EVENT_INITIAL || is ServerNetSegment.EntityPosition
objHeader == ServerNetObject.ENTITY_POSITION || prevObjHeader == ServerNetObject.ENTITY_POSITION) or ServerNetSegment.EntityEvent
or ServerNetSegment.EntityEventInitial))
{ {
foreach (IServerSerializable ent in debugEntityList) 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:"); errorLines.Add("Last console messages:");
for (int i = DebugConsole.Messages.Count - 1; i > Math.Max(0, DebugConsole.Messages.Count - 20); i--) 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); errorLines.Add("[" + DebugConsole.Messages[i].Time + "] " + DebugConsole.Messages[i].Text);
} }
GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadInGameUpdate", GameAnalyticsManager.ErrorSeverity.Critical, string.Join("\n", errorLines)); 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"); throw new Exception(
$"Exception thrown while reading segment {segment.Identifier} at position {segment.Pointer}." +
using (FileStream fl = File.Open("networkerror_data.log", System.IO.FileMode.Create)) (prevSegments.Any() ? $" Previous segments: {string.Join(", ", prevSegments)}" : ""),
{ ex);
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\"!");
}
} }
private void SendLobbyUpdate() private void SendLobbyUpdate()
@@ -2199,50 +2172,51 @@ namespace Barotrauma.Networking
IWriteMessage outmsg = new WriteOnlyMessage(); IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.WriteByte((byte)ClientPacketHeader.UPDATE_LOBBY); outmsg.WriteByte((byte)ClientPacketHeader.UPDATE_LOBBY);
outmsg.WriteByte((byte)ClientNetObject.SYNC_IDS); using (var segmentTable = SegmentTableWriter<ClientNetSegment>.StartWriting(outmsg))
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.WriteIdentifier(jobPreferences[0].Prefab.Identifier); segmentTable.StartNewSegment(ClientNetSegment.SyncIds);
} outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID);
else outmsg.WriteUInt16(ChatMessage.LastID);
{ outmsg.WriteUInt16(LastClientListUpdateID);
outmsg.WriteIdentifier(Identifier.Empty); outmsg.WriteUInt16(nameId);
} outmsg.WriteString(Name);
outmsg.WriteByte((byte)MultiplayerPreferences.Instance.TeamPreference); var jobPreferences = GameMain.NetLobbyScreen.JobPreferences;
if (jobPreferences.Count > 0)
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.WriteIdentifier(jobPreferences[0].Prefab.Identifier);
} }
outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); else
}
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 outmsg.WriteIdentifier(Identifier.Empty);
break;
} }
chatMsgQueue[i].ClientWrite(outmsg); outmsg.WriteByte((byte)MultiplayerPreferences.Instance.TeamPreference);
}
outmsg.WriteByte((byte)ClientNetObject.END_OF_MESSAGE);
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) if (outmsg.LengthBytes > MsgConstants.MTU)
{ {
DebugConsole.ThrowError($"Maximum packet size exceeded ({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.WriteBoolean(EntityEventManager.MidRoundSyncingDone);
outmsg.WritePadBits(); outmsg.WritePadBits();
outmsg.WriteByte((byte)ClientNetObject.SYNC_IDS); using (var segmentTable = SegmentTableWriter<ClientNetSegment>.StartWriting(outmsg))
//outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID); {
outmsg.WriteUInt16(ChatMessage.LastID); segmentTable.StartNewSegment(ClientNetSegment.SyncIds);
outmsg.WriteUInt16(EntityEventManager.LastReceivedID); //outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID);
outmsg.WriteUInt16(LastClientListUpdateID); outmsg.WriteUInt16(ChatMessage.LastID);
outmsg.WriteUInt16(EntityEventManager.LastReceivedID);
outmsg.WriteUInt16(LastClientListUpdateID);
if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) 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)))
{ {
outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(flag)); outmsg.WriteUInt16((UInt16)0);
} }
outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); else
}
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)
{ {
//not enough room in this packet outmsg.WriteUInt16(campaign.LastSaveID);
break; outmsg.WriteByte(campaign.CampaignID);
} foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
chatMsgQueue[i].ClientWrite(outmsg); {
} 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) if (outmsg.LengthBytes > MsgConstants.MTU)
{ {
@@ -2608,7 +2585,6 @@ namespace Barotrauma.Networking
IWriteMessage msg = new WriteOnlyMessage(); IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ClientPacketHeader.UPDATE_CHARACTERINFO); msg.WriteByte((byte)ClientPacketHeader.UPDATE_CHARACTERINFO);
WriteCharacterInfo(msg, newName); WriteCharacterInfo(msg, newName);
msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE);
ClientPeer?.Send(msg, DeliveryMethod.Reliable); ClientPeer?.Send(msg, DeliveryMethod.Reliable);
} }
@@ -2648,9 +2624,11 @@ namespace Barotrauma.Networking
IWriteMessage msg = new WriteOnlyMessage(); IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ClientPacketHeader.UPDATE_LOBBY); msg.WriteByte((byte)ClientPacketHeader.UPDATE_LOBBY);
msg.WriteByte((byte)ClientNetObject.VOTE); using (var segmentTable = SegmentTableWriter<ClientNetSegment>.StartWriting(msg))
Voting.ClientWrite(msg, voteType, data); {
msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); segmentTable.StartNewSegment(ClientNetSegment.Vote);
Voting.ClientWrite(msg, voteType, data);
}
ClientPeer.Send(msg, DeliveryMethod.Reliable); ClientPeer.Send(msg, DeliveryMethod.Reliable);
} }
@@ -2763,7 +2741,6 @@ namespace Barotrauma.Networking
msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND);
msg.WriteUInt16((UInt16)ClientPermissions.ManageCampaign); msg.WriteUInt16((UInt16)ClientPermissions.ManageCampaign);
campaign.ClientWrite(msg); campaign.ClientWrite(msg);
msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE);
ClientPeer.Send(msg, DeliveryMethod.Reliable); ClientPeer.Send(msg, DeliveryMethod.Reliable);
} }
@@ -2812,7 +2789,6 @@ namespace Barotrauma.Networking
msg.WriteUInt16((UInt16)ClientPermissions.SelectSub); msg.WriteUInt16((UInt16)ClientPermissions.SelectSub);
msg.WriteBoolean(isShuttle); msg.WritePadBits(); msg.WriteBoolean(isShuttle); msg.WritePadBits();
msg.WriteString(sub.MD5Hash.StringRepresentation); msg.WriteString(sub.MD5Hash.StringRepresentation);
msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE);
ClientPeer.Send(msg, DeliveryMethod.Reliable); ClientPeer.Send(msg, DeliveryMethod.Reliable);
} }
@@ -2832,7 +2808,6 @@ namespace Barotrauma.Networking
msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND);
msg.WriteUInt16((UInt16)ClientPermissions.SelectMode); msg.WriteUInt16((UInt16)ClientPermissions.SelectMode);
msg.WriteUInt16((UInt16)modeIndex); msg.WriteUInt16((UInt16)modeIndex);
msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE);
ClientPeer.Send(msg, DeliveryMethod.Reliable); ClientPeer.Send(msg, DeliveryMethod.Reliable);
} }
@@ -3538,6 +3513,23 @@ namespace Barotrauma.Networking
eventErrorWritten = true; 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 #if DEBUG
public void ForceTimeOut() public void ForceTimeOut()
{ {
@@ -66,7 +66,7 @@ namespace Barotrauma.Networking
events.Add(newEvent); events.Add(newEvent);
} }
public void Write(IWriteMessage msg, NetworkConnection serverConnection) public void Write(in SegmentTableWriter<ClientNetSegment> segmentTable, IWriteMessage msg, NetworkConnection serverConnection)
{ {
if (events.Count == 0 || serverConnection == null) return; if (events.Count == 0 || serverConnection == null) return;
@@ -103,7 +103,7 @@ namespace Barotrauma.Networking
eventLastSent[entityEvent.ID] = (float)Lidgren.Network.NetTime.Now; eventLastSent[entityEvent.ID] = (float)Lidgren.Network.NetTime.Now;
} }
msg.WriteByte((byte)ClientNetObject.ENTITY_STATE); segmentTable.StartNewSegment(ClientNetSegment.EntityState);
Write(msg, eventsToSync, out _); Write(msg, eventsToSync, out _);
} }
@@ -112,11 +112,11 @@ namespace Barotrauma.Networking
/// <summary> /// <summary>
/// Read the events from the message, ignoring ones we've already received. Returns false if reading the events fails. /// Read the events from the message, ignoring ones we've already received. Returns false if reading the events fails.
/// </summary> /// </summary>
public bool Read(ServerNetObject type, IReadMessage msg, float sendingTime, List<IServerSerializable> entities) public bool Read(ServerNetSegment type, IReadMessage msg, float sendingTime, List<IServerSerializable> entities)
{ {
UInt16 unreceivedEntityEventCount = 0; UInt16 unreceivedEntityEventCount = 0;
if (type == ServerNetObject.ENTITY_EVENT_INITIAL) if (type == ServerNetSegment.EntityEventInitial)
{ {
unreceivedEntityEventCount = msg.ReadUInt16(); unreceivedEntityEventCount = msg.ReadUInt16();
firstNewID = msg.ReadUInt16(); firstNewID = msg.ReadUInt16();
@@ -218,43 +218,20 @@ namespace Barotrauma.Networking
Microsoft.Xna.Framework.Color.Green); Microsoft.Xna.Framework.Color.Green);
} }
lastReceivedID++; lastReceivedID++;
try ReadEvent(msg, entity, sendingTime);
msg.ReadPadBits();
if (msg.BitPosition != msgPosition + msgLength * 8)
{ {
ReadEvent(msg, entity, sendingTime); var prevEntity = entities.Count >= 2 ? entities[entities.Count - 2] : null;
msg.ReadPadBits(); 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) GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:BitPosMismatch", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
{
var prevEntity = entities.Count >= 2 ? entities[entities.Count - 2] : null; throw new Exception(errorMsg);
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);
} }
} }
} }
@@ -4,9 +4,9 @@ namespace Barotrauma.Networking
{ {
partial class OrderChatMessage : ChatMessage partial class OrderChatMessage : ChatMessage
{ {
public override void ClientWrite(IWriteMessage msg) public override void ClientWrite(in SegmentTableWriter<ClientNetSegment> segmentTableWriter, IWriteMessage msg)
{ {
msg.WriteByte((byte)ClientNetObject.CHAT_MESSAGE); segmentTableWriter.StartNewSegment(ClientNetSegment.ChatMessage);
msg.WriteUInt16(NetStateID); msg.WriteUInt16(NetStateID);
msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1);
msg.WriteRangedInteger((int)ChatMode.None, 0, Enum.GetValues(typeof(ChatMode)).Length - 1); msg.WriteRangedInteger((int)ChatMode.None, 0, Enum.GetValues(typeof(ChatMode)).Length - 1);
@@ -1766,9 +1766,15 @@ namespace Barotrauma.CharacterEditor
var modProject = new ModProject(contentPackage); var modProject = new ModProject(contentPackage);
var newFile = ModProject.File.FromPath<CharacterFile>(configFilePath); var newFile = ModProject.File.FromPath<CharacterFile>(configFilePath);
modProject.AddFile(newFile); modProject.AddFile(newFile);
modProject.Save(contentPackage.Path); 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)); DebugConsole.NewMessage(GetCharacterEditorTranslation("ContentPackageSaved").Replace("[path]", contentPackage.Path));
@@ -956,16 +956,21 @@ namespace Barotrauma
backgroundSprite = new Sprite("Content/UnstableBackground.png", sourceRectangle: null); 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) if (vignette != null)
{ {
vignette.Draw(spriteBatch, Vector2.Zero, Color.White, Vector2.Zero, 0.0f, vignette.Draw(spriteBatch, Vector2.Zero, Color.White, Vector2.Zero, 0.0f, vignetteScale);
new Vector2(Math.Min(GameMain.GraphicsWidth / vignette.size.X, GameMain.GraphicsHeight / vignette.size.Y)));
} }
} }
@@ -1,6 +1,7 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Barotrauma.IO; 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() public override void Select()
{ {
base.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) LogAndThrowException("Error in ModDownloadScreen: the list of mods the server has enabled was empty. "
{ +$"Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}",
string errorMsg = $"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");
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);
}
} }
var missingPackages = GameMain.Client.ClientPeer.ServerContentPackages var missingPackages = GameMain.Client.ClientPeer.ServerContentPackages
@@ -96,11 +95,18 @@ namespace Barotrauma
{ {
if (!GameMain.Client.IsServerOwner) if (!GameMain.Client.IsServerOwner)
{ {
var corePackage = GameMain.Client.ClientPeer.ServerContentPackages
.Select(p => p.CorePackage)
.OfType<CorePackage>().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.BackUp();
ContentPackageManager.EnabledPackages.SetCore( ContentPackageManager.EnabledPackages.SetCore(corePackage);
GameMain.Client.ClientPeer.ServerContentPackages
.Select(p => p.CorePackage)
.OfType<CorePackage>().First());
List<RegularPackage> regularPackages = List<RegularPackage> regularPackages =
GameMain.Client.ClientPeer.ServerContentPackages GameMain.Client.ClientPeer.ServerContentPackages
.Select(p => p.RegularPackage) .Select(p => p.RegularPackage)
@@ -113,6 +119,15 @@ namespace Barotrauma
return; 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( GUIMessageBox msgBox = new GUIMessageBox(
TextManager.Get("ModDownloadTitle"), TextManager.Get("ModDownloadTitle"),
"", "",
@@ -291,14 +306,23 @@ namespace Barotrauma
var serverPackages = GameMain.Client.ClientPeer.ServerContentPackages; var serverPackages = GameMain.Client.ClientPeer.ServerContentPackages;
CorePackage corePackage CorePackage corePackage
= downloadedPackages.FirstOrDefault(p => p is CorePackage) as CorePackage = downloadedPackages.FirstOrDefault(p => p is CorePackage) as CorePackage
?? serverPackages.FirstOrDefault(p => p.CorePackage != null) ?? serverPackages.FirstOrDefault(p => p.CorePackage != null)?.CorePackage
?.CorePackage
?? throw new Exception($"Failed to find core package to enable"); ?? throw new Exception($"Failed to find core package to enable");
List<RegularPackage> regularPackages = new List<RegularPackage>(); List<RegularPackage> regularPackages = new List<RegularPackage>();
foreach (var p in serverPackages) 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 = RegularPackage? matchingPackage =
p.RegularPackage ?? downloadedPackages.FirstOrDefault(d => d is RegularPackage && d.Hash.Equals(p.Hash)) as RegularPackage; p.RegularPackage ?? downloadedPackages.FirstOrDefault(d => d is RegularPackage && d.Hash.Equals(p.Hash)) as RegularPackage;
if (matchingPackage is null) if (matchingPackage is null)
@@ -355,9 +379,13 @@ namespace Barotrauma
string dir = path.RemoveFromEnd(ModReceiver.Extension, StringComparison.OrdinalIgnoreCase); string dir = path.RemoveFromEnd(ModReceiver.Extension, StringComparison.OrdinalIgnoreCase);
SaveUtil.DecompressToDirectory(path, dir, file => { }); SaveUtil.DecompressToDirectory(path, dir, file => { });
ContentPackage newPackage var result = ContentPackage.TryLoad(Path.Combine(dir, ContentPackage.FileListFileName));
= ContentPackage.TryLoad($"{dir}/{ContentPackage.FileListFileName}")
?? throw new Exception($"Failed to load downloaded mod \"{currentDownload.Name}\""); 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)) if (!currentDownload.Hash.Equals(newPackage.Hash))
{ {
throw new Exception($"Hash mismatch for downloaded mod \"{currentDownload.Name}\" (expected {currentDownload.Hash}, got {newPackage.Hash})"); throw new Exception($"Hash mismatch for downloaded mod \"{currentDownload.Name}\" (expected {currentDownload.Hash}, got {newPackage.Hash})");
@@ -2740,6 +2740,7 @@ namespace Barotrauma
} }
public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch)
{ {
if (backgroundSprite?.Texture == null) { return; }
graphics.Clear(Color.Black); graphics.Clear(Color.Black);
spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable);
GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite, Color.White); GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite, Color.White);
@@ -1352,6 +1352,10 @@ namespace Barotrauma
private void AddToServerList(ServerInfo serverInfo, bool skipPing = false) 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.RefreshingServerList);
RemoveMsgFromServerList(MsgUserData.NoServers); RemoveMsgFromServerList(MsgUserData.NoServers);
var serverFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), serverList.Content.RectTransform) { MinSize = new Point(0, 35) }, var serverFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), serverList.Content.RectTransform) { MinSize = new Point(0, 35) },
@@ -165,6 +165,7 @@ namespace Barotrauma
void DrawOverlay(Sprite sprite, Color color) void DrawOverlay(Sprite sprite, Color color)
{ {
if (sprite.Texture == null) { return; }
GUI.DrawBackgroundSprite(spriteBatch, sprite, color); GUI.DrawBackgroundSprite(spriteBatch, sprite, color);
} }
} }
@@ -3086,11 +3086,17 @@ namespace Barotrauma
XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus)); XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus));
doc.SaveSafe(filePath); doc.SaveSafe(filePath);
var resultPackage = ContentPackageManager.ReloadContentPackage(existingContentPackage) as RegularPackage; var result = ContentPackageManager.ReloadContentPackage(existingContentPackage);
if (!ContentPackageManager.EnabledPackages.Regular.Contains(resultPackage)) 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(); GameSettings.SaveCurrentConfig();
} }
@@ -517,6 +517,11 @@ namespace Barotrauma
if (musicDisposed) { Thread.Sleep(60); } if (musicDisposed) { Thread.Sleep(60); }
} }
public static void ForceMusicUpdate()
{
updateMusicTimer = 0.0f;
}
private static void UpdateMusic(float deltaTime) private static void UpdateMusic(float deltaTime)
{ {
@@ -544,7 +549,7 @@ namespace Barotrauma
IEnumerable<BackgroundMusic> suitableMusic = GetSuitableMusicClips(currentMusicType, currentIntensity); IEnumerable<BackgroundMusic> suitableMusic = GetSuitableMusicClips(currentMusicType, currentIntensity);
int mainTrackIndex = 0; int mainTrackIndex = 0;
if (suitableMusic.Count() == 0) if (suitableMusic.None())
{ {
targetMusic[mainTrackIndex] = null; targetMusic[mainTrackIndex] = null;
} }
@@ -611,10 +616,17 @@ namespace Barotrauma
targetMusic[typeAmbienceTrackIndex] = suitableTypeAmbiences.GetRandomUnsynced(); targetMusic[typeAmbienceTrackIndex] = suitableTypeAmbiences.GetRandomUnsynced();
} }
IEnumerable<BackgroundMusic> suitableIntensityMusic = Enumerable.Empty<BackgroundMusic>();
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 //get the appropriate intensity layers for current situation
IEnumerable<BackgroundMusic> suitableIntensityMusic = Screen.Selected == GameMain.GameScreen ?
GetSuitableMusicClips("intensity".ToIdentifier(), currentIntensity) :
Enumerable.Empty<BackgroundMusic>();
int intensityTrackStartIndex = 3; int intensityTrackStartIndex = 3;
for (int i = intensityTrackStartIndex; i < MaxMusicChannels; i++) for (int i = intensityTrackStartIndex; i < MaxMusicChannels; i++)
{ {
@@ -744,6 +756,14 @@ namespace Barotrauma
firstTimeInMainMenu = false; 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) if (Character.Controlled != null)
{ {
@@ -833,7 +853,7 @@ namespace Barotrauma
{ {
return "levelend".ToIdentifier(); return "levelend".ToIdentifier();
} }
if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0 && if (GameMain.GameSession.RoundDuration > 120.0 &&
Level.Loaded?.Type == LevelData.LevelType.LocationConnection) Level.Loaded?.Type == LevelData.LevelType.LocationConnection)
{ {
return "start".ToIdentifier(); return "start".ToIdentifier();
@@ -238,17 +238,24 @@ namespace Barotrauma
public readonly float Volume; public readonly float Volume;
public readonly Vector2 IntensityRange; public readonly Vector2 IntensityRange;
public readonly bool MuteIntensityTracks;
public readonly float? ForceIntensityTrack;
public readonly bool ContinueFromPreviousTime; public readonly bool ContinueFromPreviousTime;
public int PreviousTime; public int PreviousTime;
public BackgroundMusic(ContentXElement element, SoundsFile file) : base(element, file, stream: true) public BackgroundMusic(ContentXElement element, SoundsFile file) : base(element, file, stream: true)
{ {
Type = element.GetAttributeIdentifier("type", ""); Type = element.GetAttributeIdentifier(nameof(Type), "");
IntensityRange = element.GetAttributeVector2("intensityrange", new Vector2(0.0f, 100.0f)); IntensityRange = element.GetAttributeVector2(nameof(IntensityRange), new Vector2(0.0f, 100.0f));
DuckVolume = element.GetAttributeBool("duckvolume", false); DuckVolume = element.GetAttributeBool(nameof(DuckVolume), false);
this.Volume = element.GetAttributeFloat("volume", 1.0f); MuteIntensityTracks = element.GetAttributeBool(nameof(MuteIntensityTracks), false);
ContinueFromPreviousTime = element.GetAttributeBool("continuefromprevioustime", 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);
} }
} }
@@ -73,7 +73,13 @@ namespace Barotrauma.Steam
//This callback seems to take place when the item has been downloaded recently and an update //This callback seems to take place when the item has been downloaded recently and an update
//or a redownload has taken place //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! //Maybe I'm completely wrong! All I know is that we need to handle both!
} }
@@ -177,7 +177,13 @@ namespace Barotrauma.Steam
await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, PublishStagingDir, ShouldCorrectPaths.No); await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, PublishStagingDir, ShouldCorrectPaths.No);
var stagingFileListPath = Path.Combine(PublishStagingDir, ContentPackage.FileListFileName); 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 //Load filelist.xml and write the hash into it so anyone downloading this mod knows what it should be
ModProject modProject = new ModProject(tempPkg) ModProject modProject = new ModProject(tempPkg)
@@ -40,7 +40,7 @@ namespace Barotrauma.Steam
CanBeFocused = false, CanBeFocused = false,
UserData = p UserData = p
}; };
if (p.Errors.Any()) if (p.FatalLoadErrors.Any())
{ {
CreateModErrorInfo(p, regularBox, regularBox); CreateModErrorInfo(p, regularBox, regularBox);
regularBox.CanBeFocused = true; regularBox.CanBeFocused = true;
@@ -49,21 +49,10 @@ namespace Barotrauma.Steam
SteamManager.Workshop.DownloadModThenEnqueueInstall(item); SteamManager.Workshop.DownloadModThenEnqueueInstall(item);
} }
} }
TaskPool.Add("RemoveUnsubscribedItems", SteamManager.Workshop.GetPublishedItems(), t => SteamManager.Workshop.DeleteUnsubscribedMods(removedPackages =>
{ {
if (!t.TryGetResult(out ISet<Steamworks.Ugc.Item> publishedItems)) { return; } if (removedPackages.Any())
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)
{ {
PopulateInstalledModLists(); PopulateInstalledModLists();
} }
@@ -487,8 +476,9 @@ namespace Barotrauma.Steam
{ {
string str = modsListFilter.Text; string str = modsListFilter.Text;
enabledRegularModsList.Content.Children.Concat(disabledRegularModsList.Content.Children) enabledRegularModsList.Content.Children.Concat(disabledRegularModsList.Content.Children)
.ForEach(c => c.Visible = c.UserData is not ContentPackage p .ForEach(c
|| ModNameMatches(p, str) && ModMatchesTickboxes(p, c)); => c.Visible = c.UserData is not ContentPackage p
|| ModNameMatches(p, str) && ModMatchesTickboxes(p, c));
} }
private bool ModMatchesTickboxes(ContentPackage p, GUIComponent guiItem) private bool ModMatchesTickboxes(ContentPackage p, GUIComponent guiItem)
@@ -550,7 +540,20 @@ namespace Barotrauma.Steam
(p) => p.Name, (p) => p.Name,
ContentPackageManager.CorePackages.ToArray(), ContentPackageManager.CorePackages.ToArray(),
ContentPackageManager.EnabledPackages.Core!, ContentPackageManager.EnabledPackages.Core!,
(p) => { }); (p) =>
{
enabledCoreDropdown.ButtonTextColor =
p.HasAnyErrors
? GUIStyle.Red
: GUIStyle.TextColorNormal;
});
enabledCoreDropdown.ListBox.Content.Children
.OfType<GUITextBlock>()
.ForEach(tb =>
CreateModErrorInfo(
(tb.UserData as ContentPackage)!,
tb,
tb));
void addRegularModToList(RegularPackage mod, GUIListBox list) void addRegularModToList(RegularPackage mod, GUIListBox list)
{ {
@@ -658,10 +661,7 @@ namespace Barotrauma.Steam
{ {
CanBeFocused = false CanBeFocused = false
}; };
if (mod.Errors.Any()) CreateModErrorInfo(mod, modFrame, modName);
{
CreateModErrorInfo(mod, modFrame, modName);
}
if (ContentPackageManager.LocalPackages.Contains(mod)) if (ContentPackageManager.LocalPackages.Contains(mod))
{ {
var editButton = new GUIButton(new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", var editButton = new GUIButton(new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "",
@@ -165,6 +165,10 @@ namespace Barotrauma.Steam
.Select(c => c.UserData as RegularPackage).OfType<RegularPackage>().ToArray()); .Select(c => c.UserData as RegularPackage).OfType<RegularPackage>().ToArray());
PopulateInstalledModLists(forceRefreshEnabled: true, refreshDisabled: true); PopulateInstalledModLists(forceRefreshEnabled: true, refreshDisabled: true);
ContentPackageManager.LogEnabledRegularPackageErrors(); ContentPackageManager.LogEnabledRegularPackageErrors();
enabledCoreDropdown.ButtonTextColor =
EnabledCorePackage.HasAnyErrors
? GUIStyle.Red
: GUIStyle.TextColorNormal;
} }
} }
} }
@@ -294,10 +294,11 @@ namespace Barotrauma.Steam
{ {
//Reload the package to force hash recalculation //Reload the package to force hash recalculation
string packageName = localPackage.Name; string packageName = localPackage.Name;
localPackage = ContentPackageManager.ReloadContentPackage(localPackage); var result = ContentPackageManager.ReloadContentPackage(localPackage);
if (localPackage is null) 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 //Set up the Ugc.Editor object that we'll need to publish
@@ -133,20 +133,29 @@ namespace Barotrauma.Steam
return searchBox; 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; const int maxErrorsToShow = 5;
nameText.TextColor = GUIStyle.Red; nameText.TextColor = GUIStyle.Red;
uiElement.ToolTip = uiElement.ToolTip =
TextManager.GetWithVariable("contentpackagehaserrors", "[packagename]", mod.Name) TextManager.GetWithVariable("ContentPackageHasFatalErrors", "[packagename]", mod.Name)
+ '\n' + string.Join('\n', mod.Errors.Take(maxErrorsToShow).Select(e => e.Message)); + '\n' + string.Join('\n', mod.FatalLoadErrors.Take(maxErrorsToShow).Select(e => e.Message));
if (mod.Errors.Count() > maxErrorsToShow) 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);
}
} }
} }
} }
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>100.5.0.0</Version> <Version>100.6.0.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>100.5.0.0</Version> <Version>100.6.0.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>100.5.0.0</Version> <Version>100.6.0.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>100.5.0.0</Version> <Version>100.6.0.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>100.5.0.0</Version> <Version>100.6.0.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -82,54 +82,50 @@ namespace Barotrauma
} }
//dequeue messages //dequeue messages
lock (queuedMessages) if (queuedMessages.Count > 0)
{ {
if (queuedMessages.Count > 0)
if (!Console.IsOutputRedirected)
{ {
Console.CursorLeft = 0;
if (!Console.IsOutputRedirected) }
while (queuedMessages.TryDequeue(out var msg))
{
Messages.Add(msg);
if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging)
{ {
Console.CursorLeft = 0; unsavedMessages.Add(msg);
} if (unsavedMessages.Count >= messagesPerFile)
while (queuedMessages.Count > 0)
{
ColoredText msg = queuedMessages.Dequeue();
Messages.Add(msg);
if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging)
{ {
unsavedMessages.Add(msg); SaveLogs();
if (unsavedMessages.Count >= messagesPerFile) 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) 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. // 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) if(!Console.IsOutputRedirected && !Console.IsInputRedirected)
@@ -272,26 +268,22 @@ namespace Barotrauma
public static void Clear() 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(); unsavedMessages.Add(msg);
Messages.Add(msg); if (unsavedMessages.Count >= messagesPerFile)
if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging)
{ {
unsavedMessages.Add(msg); SaveLogs();
if (unsavedMessages.Count >= messagesPerFile) unsavedMessages.Clear();
{
SaveLogs();
unsavedMessages.Clear();
}
} }
} }
if (Messages.Count > MaxMessages) }
{ if (Messages.Count > MaxMessages)
Messages.RemoveRange(0, Messages.Count - MaxMessages); {
} Messages.RemoveRange(0, Messages.Count - MaxMessages);
} }
} }
@@ -162,13 +162,13 @@ namespace Barotrauma
} }
else else
{ {
name = doc.Root.GetAttributeString("name", "Server"); name = doc.Root.GetAttributeString(nameof(ServerSettings.Name), "Server");
port = doc.Root.GetAttributeInt("port", NetConfig.DefaultPort); port = doc.Root.GetAttributeInt(nameof(ServerSettings.Port), NetConfig.DefaultPort);
queryPort = doc.Root.GetAttributeInt("queryport", NetConfig.DefaultQueryPort); queryPort = doc.Root.GetAttributeInt(nameof(ServerSettings.QueryPort), NetConfig.DefaultQueryPort);
publiclyVisible = doc.Root.GetAttributeBool("public", false); 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", ""); password = doc.Root.GetAttributeString("password", "");
enableUpnp = doc.Root.GetAttributeBool("enableupnp", false);
maxPlayers = doc.Root.GetAttributeInt("maxplayers", 10);
ownerKey = Option<int>.None(); ownerKey = Option<int>.None();
} }
@@ -361,7 +361,7 @@ namespace Barotrauma
if (prevUpdateRates.Count >= 10) if (prevUpdateRates.Count >= 10)
{ {
int avgUpdateRate = (int)prevUpdateRates.Average(); 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)!"); DebugConsole.AddWarning($"Running slowly ({avgUpdateRate} updates/s)!");
if (Server != null) if (Server != null)
@@ -335,7 +335,7 @@ namespace Barotrauma
break; 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); bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
if (success) if (success)
@@ -200,9 +200,9 @@ namespace Barotrauma.Networking
return length; return length;
} }
public virtual void ServerWrite(IWriteMessage msg, Client c) public virtual void ServerWrite(in SegmentTableWriter<ServerNetSegment> segmentTable, IWriteMessage msg, Client c)
{ {
msg.WriteByte((byte)ServerNetObject.CHAT_MESSAGE); segmentTable.StartNewSegment(ServerNetSegment.ChatMessage);
msg.WriteUInt16(NetStateID); msg.WriteUInt16(NetStateID);
msg.WriteRangedInteger((int)Type, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); msg.WriteRangedInteger((int)Type, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1);
msg.WriteByte((byte)ChangeType); msg.WriteByte((byte)ChangeType);
@@ -1028,12 +1028,11 @@ namespace Barotrauma.Networking
return; return;
} }
ClientNetObject objHeader; SegmentTableReader<ClientNetSegment>.Read(inc, (segment, inc) =>
while ((objHeader = (ClientNetObject)inc.ReadByte()) != ClientNetObject.END_OF_MESSAGE)
{ {
switch (objHeader) switch (segment)
{ {
case ClientNetObject.SYNC_IDS: case ClientNetSegment.SyncIds:
//TODO: might want to use a clever class for this //TODO: might want to use a clever class for this
c.LastRecvLobbyUpdate = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvLobbyUpdate, GameMain.NetLobbyScreen.LastUpdateID); c.LastRecvLobbyUpdate = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvLobbyUpdate, GameMain.NetLobbyScreen.LastUpdateID);
if (c.HasPermission(ClientPermissions.ManageSettings) && if (c.HasPermission(ClientPermissions.ManageSettings) &&
@@ -1072,19 +1071,21 @@ namespace Barotrauma.Networking
} }
} }
break; break;
case ClientNetObject.CHAT_MESSAGE: case ClientNetSegment.ChatMessage:
ChatMessage.ServerRead(inc, c); ChatMessage.ServerRead(inc, c);
break; break;
case ClientNetObject.VOTE: case ClientNetSegment.Vote:
Voting.ServerRead(inc, c); Voting.ServerRead(inc, c);
break; break;
default: default:
return; return SegmentTableReader<ClientNetSegment>.BreakSegmentReading.Yes;
} }
//don't read further messages if the client has been disconnected (kicked due to spam for example) //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<ClientNetSegment>.BreakSegmentReading.No
: SegmentTableReader<ClientNetSegment>.BreakSegmentReading.Yes;
});
} }
private void ClientReadIngame(IReadMessage inc) private void ClientReadIngame(IReadMessage inc)
@@ -1109,13 +1110,12 @@ namespace Barotrauma.Networking
} }
} }
ClientNetObject objHeader; SegmentTableReader<ClientNetSegment>.Read(inc, (segment, inc) =>
while ((objHeader = (ClientNetObject)inc.ReadByte()) != ClientNetObject.END_OF_MESSAGE)
{ {
switch (objHeader) switch (segment)
{ {
case ClientNetObject.SYNC_IDS: case ClientNetSegment.SyncIds:
//TODO: might want to use a clever class for this //TODO: switch this to INetSerializableStruct
UInt16 lastRecvChatMsgID = inc.ReadUInt16(); UInt16 lastRecvChatMsgID = inc.ReadUInt16();
UInt16 lastRecvEntityEventID = inc.ReadUInt16(); UInt16 lastRecvEntityEventID = inc.ReadUInt16();
@@ -1218,10 +1218,10 @@ namespace Barotrauma.Networking
} }
break; break;
case ClientNetObject.CHAT_MESSAGE: case ClientNetSegment.ChatMessage:
ChatMessage.ServerRead(inc, c); ChatMessage.ServerRead(inc, c);
break; break;
case ClientNetObject.CHARACTER_INPUT: case ClientNetSegment.CharacterInput:
if (c.Character != null) if (c.Character != null)
{ {
c.Character.ServerReadInput(inc, c); 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})."); DebugConsole.AddWarning($"Received character inputs from a client who's not controlling a character ({c.Name}).");
} }
break; break;
case ClientNetObject.ENTITY_STATE: case ClientNetSegment.EntityState:
entityEventManager.Read(inc, c); entityEventManager.Read(inc, c);
break; break;
case ClientNetObject.VOTE: case ClientNetSegment.Vote:
Voting.ServerRead(inc, c); Voting.ServerRead(inc, c);
break; break;
case ClientNetObject.SPECTATING_POS: case ClientNetSegment.SpectatingPos:
c.SpectatePos = new Vector2(inc.ReadSingle(), inc.ReadSingle()); c.SpectatePos = new Vector2(inc.ReadSingle(), inc.ReadSingle());
break; break;
default: default:
return; return SegmentTableReader<ClientNetSegment>.BreakSegmentReading.Yes;
} }
//don't read further messages if the client has been disconnected (kicked due to spam for example) //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<ClientNetSegment>.BreakSegmentReading.No
: SegmentTableReader<ClientNetSegment>.BreakSegmentReading.Yes;
});
} }
private void ReadCrewMessage(IReadMessage inc, Client sender) private void ReadCrewMessage(IReadMessage inc, Client sender)
@@ -1701,78 +1703,78 @@ namespace Barotrauma.Networking
IWriteMessage outmsg = new WriteOnlyMessage(); IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME); outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME);
outmsg.WriteSingle((float)NetTime.Now); outmsg.WriteSingle((float)NetTime.Now);
outmsg.WriteByte((byte)ServerNetObject.SYNC_IDS); using (var segmentTable = SegmentTableWriter<ServerNetSegment>.StartWriting(outmsg))
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)
{ {
outmsg.WriteBoolean(true); segmentTable.StartNewSegment(ServerNetSegment.SyncIds);
outmsg.WritePadBits(); outmsg.WriteUInt16(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server
campaign.ServerWrite(outmsg, c); outmsg.WriteUInt16(c.LastSentEntityEventID);
}
else
{
outmsg.WriteBoolean(false);
outmsg.WritePadBits();
}
int clientListBytes = outmsg.LengthBytes; if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode)
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)))
{ {
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(); c.PendingPositionUpdates.Dequeue();
continue;
} }
positionUpdateBytes = outmsg.LengthBytes - positionUpdateBytes;
IWriteMessage tempBuffer = new ReadWriteMessage(); if (outmsg.LengthBytes > MsgConstants.MTU)
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; 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); serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable);
@@ -1785,46 +1787,50 @@ namespace Barotrauma.Networking
outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME); outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME);
outmsg.WriteSingle((float)Lidgren.Network.NetTime.Now); outmsg.WriteSingle((float)Lidgren.Network.NetTime.Now);
int eventManagerBytes = outmsg.LengthBytes; using (var segmentTable = SegmentTableWriter<ServerNetSegment>.StartWriting(outmsg))
entityEventManager.Write(c, outmsg, out List<NetEntityEvent> sentEvents);
eventManagerBytes = outmsg.LengthBytes - eventManagerBytes;
if (sentEvents.Count == 0)
{ {
break; int eventManagerBytes = outmsg.LengthBytes;
} entityEventManager.Write(segmentTable, c, outmsg, out List<NetEntityEvent> sentEvents);
eventManagerBytes = outmsg.LengthBytes - eventManagerBytes;
outmsg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); if (sentEvents.Count == 0)
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"; break;
foreach (var entityEvent in sentEvents)
{
errorMsg += " - " + (entityEvent.Entity?.ToString() ?? "null") + "\n";
}
} }
DebugConsole.ThrowError(errorMsg); if (outmsg.LengthBytes > MsgConstants.MTU)
GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame2:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); {
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); serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable);
} }
} }
private void WriteClientList(Client c, IWriteMessage outmsg) private void WriteClientList(in SegmentTableWriter<ServerNetSegment> segmentTable, Client c, IWriteMessage outmsg)
{ {
bool hasChanged = NetIdUtils.IdMoreRecent(LastClientListUpdateID, c.LastRecvClientListUpdate); bool hasChanged = NetIdUtils.IdMoreRecent(LastClientListUpdateID, c.LastRecvClientListUpdate);
if (!hasChanged) { return; } if (!hasChanged) { return; }
outmsg.WriteByte((byte)ServerNetObject.CLIENT_LIST); segmentTable.StartNewSegment(ServerNetSegment.ClientList);
outmsg.WriteUInt16(LastClientListUpdateID); outmsg.WriteUInt16(LastClientListUpdateID);
outmsg.WriteByte((byte)connectedClients.Count); outmsg.WriteByte((byte)connectedClients.Count);
@@ -1861,133 +1867,135 @@ namespace Barotrauma.Networking
IWriteMessage outmsg = new WriteOnlyMessage(); IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_LOBBY); outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_LOBBY);
outmsg.WriteByte((byte)ServerNetObject.SYNC_IDS); bool messageTooLarge;
using (var segmentTable = SegmentTableWriter<ServerNetSegment>.StartWriting(outmsg))
int settingsBytes = outmsg.LengthBytes;
int initialUpdateBytes = 0;
if (ServerSettings.UnsentFlags() != ServerSettings.NetFlags.None)
{ {
GameMain.NetLobbyScreen.LastUpdateID++; segmentTable.StartNewSegment(ServerNetSegment.SyncIds);
}
IWriteMessage settingsBuf = null;
if (NetIdUtils.IdMoreRecent(GameMain.NetLobbyScreen.LastUpdateID, c.LastRecvLobbyUpdate))
{
outmsg.WriteBoolean(true);
outmsg.WritePadBits();
outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID); int settingsBytes = outmsg.LengthBytes;
int initialUpdateBytes = 0;
settingsBuf = new ReadWriteMessage(); if (ServerSettings.UnsentFlags() != ServerSettings.NetFlags.None)
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; GameMain.NetLobbyScreen.LastUpdateID++;
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); IWriteMessage settingsBuf = null;
outmsg.WriteBoolean(ServerSettings.AllowModeVoting); if (NetIdUtils.IdMoreRecent(GameMain.NetLobbyScreen.LastUpdateID, c.LastRecvLobbyUpdate))
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); 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
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)
{ {
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 += warningMsg +=
" Initial update size: " + settingsBuf.LengthBytes + " bytes\n"; " Client list size: " + clientListBytes + " bytes\n" +
} " Chat message size: " + chatMessageBytes + " bytes\n" +
if (settingsBuf != null) " Campaign size: " + campaignBytes + " bytes\n" +
{ " Settings size: " + settingsBytes + " bytes\n";
warningMsg += if (initialUpdateBytes > 0)
" Settings buffer size: " + settingsBuf.LengthBytes + " bytes\n"; {
} warningMsg +=
" Initial update size: " + initialUpdateBytes + " bytes\n";
}
if (settingsBuf != null)
{
warningMsg +=
" Settings buffer size: " + settingsBuf.LengthBytes + " bytes\n";
}
#if DEBUG || UNSTABLE #if DEBUG || UNSTABLE
DebugConsole.ThrowError(warningMsg); DebugConsole.ThrowError(warningMsg);
#else #else
if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.AddWarning(warningMsg); } if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.AddWarning(warningMsg); }
#endif #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) if (isInitialUpdate || messageTooLarge)
@@ -2014,7 +2022,7 @@ namespace Barotrauma.Networking
} }
} }
private void WriteChatMessages(IWriteMessage outmsg, Client c) private void WriteChatMessages(in SegmentTableWriter<ServerNetSegment> segmentTable, IWriteMessage outmsg, Client c)
{ {
c.ChatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, c.LastRecvChatMsgID)); c.ChatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, c.LastRecvChatMsgID));
for (int i = 0; i < c.ChatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) 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 //not enough room in this packet
return; 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.InventoryData = new XElement("inventory");
spawnedCharacter.Info.StartItemsGiven = true; spawnedCharacter.Info.StartItemsGiven = true;
spawnedCharacter.SaveInventory(); 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(); spawnedCharacter.LoadTalents();
} }
} }
@@ -3327,9 +3334,11 @@ namespace Barotrauma.Networking
IWriteMessage msg = new WriteOnlyMessage(); IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ServerPacketHeader.UPDATE_LOBBY); msg.WriteByte((byte)ServerPacketHeader.UPDATE_LOBBY);
msg.WriteByte((byte)ServerNetObject.VOTE); using (var segmentTable = SegmentTableWriter<ServerNetSegment>.StartWriting(msg))
Voting.ServerWrite(msg); {
msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE); segmentTable.StartNewSegment(ServerNetSegment.Vote);
Voting.ServerWrite(msg);
}
foreach (var c in recipients) foreach (var c in recipients)
{ {
@@ -123,7 +123,7 @@ namespace Barotrauma.Networking
//remove old events that have been sent to all clients, they are redundant now //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 // 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 // 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 => events.RemoveAll(e =>
(NetIdUtils.IdMoreRecent(lastSentToAll, e.ID) || !inGameClientsPresent) && (NetIdUtils.IdMoreRecent(lastSentToAll, e.ID) || !inGameClientsPresent) &&
@@ -217,7 +217,7 @@ namespace Barotrauma.Networking
if (Timing.TotalTime - lastWarningTime > 5.0 && if (Timing.TotalTime - lastWarningTime > 5.0 &&
Timing.TotalTime - lastSentToAnyoneTime > 10.0 && Timing.TotalTime - lastSentToAnyoneTime > 10.0 &&
Timing.TotalTime > GameMain.GameSession.RoundStartTime + NetConfig.RoundStartSyncDuration) GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration)
{ {
lastWarningTime = Timing.TotalTime; lastWarningTime = Timing.TotalTime;
GameServer.Log("WARNING: ServerEntityEventManager is lagging behind! Last sent id: " + lastSentToAnyone.ToString() + ", latest create id: " + ID.ToString(), ServerLog.MessageType.ServerMessage); 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)); ServerEntityEvent firstEventToResend = events.Find(e => e.ID == (ushort)(lastSentToAll + 1));
if (firstEventToResend != null && 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)) ((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, // This event is 10 seconds older than the last one we've successfully sent,
@@ -295,15 +295,15 @@ namespace Barotrauma.Networking
/// <summary> /// <summary>
/// Writes all the events that the client hasn't received yet into the outgoing message /// Writes all the events that the client hasn't received yet into the outgoing message
/// </summary> /// </summary>
public void Write(Client client, IWriteMessage msg) public void Write(in SegmentTableWriter<ServerNetSegment> segmentTable, Client client, IWriteMessage msg)
{ {
Write(client, msg, out _); Write(segmentTable, client, msg, out _);
} }
/// <summary> /// <summary>
/// Writes all the events that the client hasn't received yet into the outgoing message /// Writes all the events that the client hasn't received yet into the outgoing message
/// </summary> /// </summary>
public void Write(Client client, IWriteMessage msg, out List<NetEntityEvent> sentEvents) public void Write(in SegmentTableWriter<ServerNetSegment> segmentTable, Client client, IWriteMessage msg, out List<NetEntityEvent> sentEvents)
{ {
List<NetEntityEvent> eventsToSync = GetEventsToSync(client); List<NetEntityEvent> eventsToSync = GetEventsToSync(client);
@@ -315,7 +315,7 @@ namespace Barotrauma.Networking
//too many events for one packet //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) //(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) if (eventsToSync.Count > 200 && !client.NeedsMidRoundSync && Timing.TotalTime > lastEventCountHighWarning + 2.0)
{ {
@@ -345,7 +345,7 @@ namespace Barotrauma.Networking
if (client.NeedsMidRoundSync) if (client.NeedsMidRoundSync)
{ {
msg.WriteByte((byte)ServerNetObject.ENTITY_EVENT_INITIAL); segmentTable.StartNewSegment(ServerNetSegment.EntityEventInitial);
msg.WriteUInt16(client.UnreceivedEntityEventCount); msg.WriteUInt16(client.UnreceivedEntityEventCount);
msg.WriteUInt16(client.FirstNewEventID); msg.WriteUInt16(client.FirstNewEventID);
@@ -353,7 +353,7 @@ namespace Barotrauma.Networking
} }
else else
{ {
msg.WriteByte((byte)ServerNetObject.ENTITY_EVENT); segmentTable.StartNewSegment(ServerNetSegment.EntityEvent);
Write(msg, eventsToSync, out sentEvents, client); Write(msg, eventsToSync, out sentEvents, client);
} }
@@ -1,13 +1,13 @@
using Barotrauma.Steam; using Barotrauma.Steam;
using System; using System;
namespace Barotrauma.Networking namespace Barotrauma.Networking
{ {
partial class OrderChatMessage : ChatMessage partial class OrderChatMessage : ChatMessage
{ {
public override void ServerWrite(IWriteMessage msg, Client c) public override void ServerWrite(in SegmentTableWriter<ServerNetSegment> segmentTable, IWriteMessage msg, Client c)
{ {
msg.WriteByte((byte)ServerNetObject.CHAT_MESSAGE); segmentTable.StartNewSegment(ServerNetSegment.ChatMessage);
msg.WriteUInt16(NetStateID); msg.WriteUInt16(NetStateID);
msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1);
msg.WriteString(SenderName); msg.WriteString(SenderName);
@@ -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; } if (!UseRespawnPrompt || !(GameMain.GameSession.GameMode is MultiPlayerCampaign campaign)) { return false; }
@@ -67,7 +67,7 @@ namespace Barotrauma.Networking
return false; return false;
} }
private List<CharacterInfo> GetBotsToRespawn() private static List<CharacterInfo> GetBotsToRespawn()
{ {
if (GameMain.Server.ServerSettings.BotSpawnMode == BotSpawnMode.Normal) if (GameMain.Server.ServerSettings.BotSpawnMode == BotSpawnMode.Normal)
{ {
@@ -110,7 +110,7 @@ namespace Barotrauma.Networking
return ShouldStartRespawnCountdown(characterToRespawnCount); return ShouldStartRespawnCountdown(characterToRespawnCount);
} }
private int GetMinCharactersToRespawn() private static int GetMinCharactersToRespawn()
{ {
return Math.Max((int)(GameMain.Server.ConnectedClients.Count * GameMain.Server.ServerSettings.MinRespawnRatio), 1); 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) if (scooterPrefab != null)
{ {
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>100.5.0.0</Version> <Version>100.6.0.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -1625,7 +1625,7 @@ namespace Barotrauma
float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage); float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage);
maxAccumulatedDamage = Math.Max(accumulatedDamage, 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; var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage;
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss); GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss);
@@ -77,11 +77,11 @@ namespace Barotrauma
{ {
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection) 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) 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?.CurrentHull != null &&
(speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) && (speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) &&
Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull)) Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull))
@@ -212,7 +212,7 @@ namespace Barotrauma
public float Stun { get; private set; } public float Stun { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Can damage only Humans."), Editable] [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] [Serialize("", IsPropertySaveable.Yes), Editable]
public string ApplyForceOnLimbs public string ApplyForceOnLimbs
@@ -328,7 +328,7 @@ namespace Barotrauma
List<Affliction> multipliedAfflictions = new List<Affliction>(); List<Affliction> multipliedAfflictions = new List<Affliction>();
foreach (Affliction affliction in Afflictions.Keys) foreach (Affliction affliction in Afflictions.Keys)
{ {
multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier, affliction.Probability)); multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier, affliction));
} }
return multipliedAfflictions; return multipliedAfflictions;
} }
@@ -540,7 +540,7 @@ namespace Barotrauma
private Color speechBubbleColor; private Color speechBubbleColor;
private float speechBubbleTimer; private float speechBubbleTimer;
public bool ResetInteract; public bool DisableInteract { get; set; }
//text displayed when the character is highlighted if custom interact is set //text displayed when the character is highlighted if custom interact is set
public LocalizedString CustomInteractHUDText { get; private set; } public LocalizedString CustomInteractHUDText { get; private set; }
@@ -809,6 +809,7 @@ namespace Barotrauma
public float MaxHealth => MaxVitality; public float MaxHealth => MaxVitality;
public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle; 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 bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached;
public float EmpVulnerability => Params.Health.EmpVulnerability;
public float Bloodloss public float Bloodloss
{ {
@@ -856,6 +857,12 @@ namespace Barotrauma
set; set;
} }
public bool IgnoreMeleeWeapons
{
get;
set;
}
/// <summary> /// <summary>
/// Current speed of the character's collider. Can be used by status effects to check if the character is moving. /// Current speed of the character's collider. Can be used by status effects to check if the character is moving.
/// </summary> /// </summary>
@@ -906,6 +913,10 @@ namespace Barotrauma
{ {
itemSelectedTime = Timing.TotalTime; itemSelectedTime = Timing.TotalTime;
} }
if (prevSelectedItem != _selectedItem && prevSelectedItem?.OnDeselect != null)
{
prevSelectedItem.OnDeselect(this);
}
} }
} }
/// <summary> /// <summary>
@@ -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}\"."); 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; return;
} }
@@ -2681,9 +2692,9 @@ namespace Barotrauma
return; return;
} }
if (ResetInteract) if (DisableInteract)
{ {
ResetInteract = false; DisableInteract = false;
return; return;
} }
@@ -2704,25 +2715,31 @@ namespace Barotrauma
{ {
if (!IsMouseOnUI && (ViewTarget == null || ViewTarget == this)) 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 (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld)
if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; }
float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f);
if (HeldItems.Any(it => it?.GetComponent<Wire>()?.IsActive ?? false))
{ {
//disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null;
aimAssist = 0.0f; if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; }
} float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f);
if (HeldItems.Any(it => it?.GetComponent<Wire>()?.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 ? FindItemAtPosition(mouseSimPos, aimAssist) : null;
if (focusedItem != null && focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None)
focusedItem = CanInteract ? item : null; {
if (focusedItem != null && focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None) FocusedCharacter = null;
{ }
FocusedCharacter = null; findFocusedTimer = 0.05f;
}
else
{
if (focusedItem != null && !CanInteractWith(focusedItem)) { focusedItem = null; }
if (FocusedCharacter != null && !CanInteractWith(FocusedCharacter)) { FocusedCharacter = null; }
} }
findFocusedTimer = 0.05f;
} }
} }
else else
@@ -2953,6 +2970,15 @@ namespace Barotrauma
{ {
UpdateProjSpecific(deltaTime, cam); 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; KnockbackCooldownTimer -= deltaTime;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && this == Controlled && !isSynced) { return; } if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && this == Controlled && !isSynced) { return; }
@@ -2992,6 +3018,7 @@ namespace Barotrauma
} }
HideFace = false; HideFace = false;
IgnoreMeleeWeapons = false;
UpdateSightRange(deltaTime); UpdateSightRange(deltaTime);
UpdateSoundRange(deltaTime); UpdateSoundRange(deltaTime);
@@ -4096,7 +4123,13 @@ namespace Barotrauma
{ {
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; } if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; }
if (Screen.Selected != GameMain.GameScreen) { 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 ((newStun <= Stun && !allowStunDecrease) || !MathUtils.IsValid(newStun)) { return; }
if (Math.Sign(newStun) != Math.Sign(Stun)) if (Math.Sign(newStun) != Math.Sign(Stun))
{ {
@@ -4345,6 +4378,9 @@ namespace Barotrauma
{ {
foreach (Item heldItem in HeldItems.ToList()) 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<Wearable>();
if (wearable is { IsActive: true }) { continue; }
heldItem.Drop(this); heldItem.Drop(this);
} }
} }
@@ -1241,7 +1241,7 @@ namespace Barotrauma
partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel); 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; int prevAmount = ExperiencePoints;
@@ -1295,6 +1295,18 @@ namespace Barotrauma
return experienceRequired + ExperienceRequiredPerLevel(level); 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() public int GetCurrentLevel()
{ {
return GetCurrentLevel(out _); return GetCurrentLevel(out _);
@@ -1312,7 +1324,7 @@ namespace Barotrauma
return level; return level;
} }
private int ExperienceRequiredPerLevel(int level) private static int ExperienceRequiredPerLevel(int level)
{ {
return BaseExperienceRequired + AddedExperienceRequiredPerLevel * level; return BaseExperienceRequired + AddedExperienceRequiredPerLevel * level;
} }
@@ -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] [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; } 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 DamagePerSecond;
public float DamagePerSecondTimer; public float DamagePerSecondTimer;
public float PreviousVitalityDecrease; public float PreviousVitalityDecrease;
@@ -104,6 +107,15 @@ namespace Barotrauma
} }
} }
/// <summary>
/// Copy properties here instead of using SerializableProperties (with reflection).
/// </summary>
public void CopyProperties(Affliction source)
{
Probability = source.Probability;
DivideByLimbCount = source.DivideByLimbCount;
MultiplyByMaxVitality = source.MultiplyByMaxVitality;
}
public void Serialize(XElement element) public void Serialize(XElement element)
{ {
@@ -115,10 +127,10 @@ namespace Barotrauma
SerializableProperties = SerializableProperty.DeserializeProperties(this, element); 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); var instance = Prefab.Instantiate(NonClampedStrength * multiplier, Source);
instance.Probability = probability; instance.CopyProperties(affliction);
return instance; return instance;
} }
@@ -693,8 +693,15 @@ namespace Barotrauma
if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; } if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; }
if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { 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.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; } if (newAffliction.Prefab.TargetSpecies.Any() && newAffliction.Prefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; }
Affliction existingAffliction = null; Affliction existingAffliction = null;
@@ -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; } if (ItemSets == null || !ItemSets.Any()) { return false; }
var spawnItems = ToolBox.SelectWeightedRandom(ItemSets, it => it.commonness, randSync).element; var spawnItems = ToolBox.SelectWeightedRandom(ItemSets, it => it.commonness, randSync).element;
@@ -196,7 +196,7 @@ namespace Barotrauma
int amount = itemElement.GetAttributeInt("amount", 1); int amount = itemElement.GetAttributeInt("amount", 1);
for (int i = 0; i < amount; i++) 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; 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; ItemPrefab itemPrefab;
string itemIdentifier = itemElement.GetAttributeString("identifier", ""); string itemIdentifier = itemElement.GetAttributeString("identifier", "");
@@ -278,7 +278,7 @@ namespace Barotrauma
IdCard idCardComponent = item.GetComponent<IdCard>(); IdCard idCardComponent = item.GetComponent<IdCard>();
if (idCardComponent != null) if (idCardComponent != null)
{ {
idCardComponent.Initialize(null, character); idCardComponent.Initialize(spawnPoint, character);
if (submarine != null && (submarine.Info.IsWreck || submarine.Info.IsOutpost)) if (submarine != null && (submarine.Info.IsWreck || submarine.Info.IsOutpost))
{ {
idCardComponent.SubmarineSpecificID = submarine.SubmarineSpecificIDTag; idCardComponent.SubmarineSpecificID = submarine.SubmarineSpecificIDTag;
@@ -301,7 +301,7 @@ namespace Barotrauma
int amount = childItemElement.GetAttributeInt("amount", 1); int amount = childItemElement.GetAttributeInt("amount", 1);
for (int i = 0; i < amount; i++) for (int i = 0; i < amount; i++)
{ {
InitializeItem(character, childItemElement, submarine, humanPrefab, item, createNetworkEvents); InitializeItem(character, childItemElement, submarine, humanPrefab, spawnPoint, item, createNetworkEvents);
} }
} }
} }
@@ -757,6 +757,10 @@ namespace Barotrauma
} }
if (!foundMatchingModifier && random > affliction.Probability) { continue; } if (!foundMatchingModifier && random > affliction.Probability) { continue; }
float finalDamageModifier = damageMultiplier; float finalDamageModifier = damageMultiplier;
if (affliction.Prefab.AfflictionType == "emp" && character.EmpVulnerability > 0)
{
finalDamageModifier *= character.EmpVulnerability;
}
foreach (DamageModifier damageModifier in tempModifiers) foreach (DamageModifier damageModifier in tempModifiers)
{ {
float damageModifierValue = damageModifier.DamageMultiplier; float damageModifierValue = damageModifier.DamageMultiplier;
@@ -766,9 +770,13 @@ namespace Barotrauma
} }
finalDamageModifier *= damageModifierValue; finalDamageModifier *= damageModifierValue;
} }
if (affliction.MultiplyByMaxVitality)
{
finalDamageModifier *= character.MaxVitality / 100f;
}
if (!MathUtils.NearlyEqual(finalDamageModifier, 1.0f)) if (!MathUtils.NearlyEqual(finalDamageModifier, 1.0f))
{ {
newAffliction = affliction.CreateMultiplied(finalDamageModifier, affliction.Probability); newAffliction = affliction.CreateMultiplied(finalDamageModifier, affliction);
} }
else else
{ {
@@ -504,6 +504,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes), Editable] [Serialize(false, IsPropertySaveable.Yes), Editable]
public bool PoisonImmunity { get; set; } 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] [Serialize(false, IsPropertySaveable.Yes, description: "Can afflictions affect the face/body tint of the character."), Editable]
public bool ApplyAfflictionColors { get; private set; } public bool ApplyAfflictionColors { get; private set; }
@@ -39,8 +39,18 @@ namespace Barotrauma.Abilities
{ {
if (factions.Any()) if (factions.Any())
{ {
// FIXME there's probably a better way to check the faction affiliated with the mission later if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; }
return mission.ReputationRewards.Keys.Any(factionIdentifier => factions.Contains(factionIdentifier));
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); return missionType.Contains(mission.Prefab.Type);
@@ -23,7 +23,6 @@
protected override bool MatchesConditionSpecific() protected override bool MatchesConditionSpecific()
{ {
Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier); Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier);
return character.Info.GetSavedStatValue(statType, identifier) >= min; return character.Info.GetSavedStatValue(statType, identifier) >= min;
} }
} }
@@ -5,15 +5,33 @@ internal sealed class CharacterAbilityGiveExperience : CharacterAbility
public override bool AppliesEffectOnIntervalUpdate => true; public override bool AppliesEffectOnIntervalUpdate => true;
private readonly int amount; private readonly int amount;
private readonly int level;
public CharacterAbilityGiveExperience(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) public CharacterAbilityGiveExperience(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{ {
amount = abilityElement.GetAttributeInt("amount", 0); 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) 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) protected override void ApplyEffect(AbilityObject abilityObject)
@@ -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();
}
}
}
@@ -58,10 +58,8 @@ namespace Barotrauma.Abilities
foreach (Identifier identifier in selectedTalentTree) foreach (Identifier identifier in selectedTalentTree)
{ {
if (Character.HasTalent(identifier)) { continue; } if (Character.HasTalent(identifier)) { continue; }
if (Character.GiveTalent(identifier))
{ Character.GiveTalent(identifier);
Character.Info.AdditionalTalentPoints++;
}
} }
static bool IsShowCaseTalent(Identifier identifier, TalentOption option) static bool IsShowCaseTalent(Identifier identifier, TalentOption option)
@@ -141,23 +141,13 @@ namespace Barotrauma
{ {
if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier)) if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier))
{ {
return TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents); return !talentOptionStage.HasMaxTalents(selectedTalents) && TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents);
} }
bool optionStageCompleted = talentOptionStage.HasEnoughTalents(selectedTalents); bool optionStageCompleted = talentOptionStage.HasEnoughTalents(selectedTalents);
if (!optionStageCompleted) if (!optionStageCompleted)
{ {
break; break;
} }
/*bool hasTalentInThisTier = talentOptionStage.HasMaxTalents(selectedTalents);
if (!hasTalentInThisTier)
{
if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier))
{
return TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents);
}
break;
}*/
} }
} }
@@ -67,10 +67,10 @@ namespace Barotrauma
.ToImmutableHashSet(); .ToImmutableHashSet();
} }
public static Result<ContentFile, LoadError> CreateFromXElement(ContentPackage contentPackage, XElement element) public static Result<ContentFile, ContentPackage.LoadError> CreateFromXElement(ContentPackage contentPackage, XElement element)
{ {
static Result<ContentFile, LoadError> fail(string error, Exception? exception = null) static Result<ContentFile, ContentPackage.LoadError> fail(string error, Exception? exception = null)
=> Result<ContentFile, LoadError>.Failure(new LoadError(error, exception)); => Result<ContentFile, ContentPackage.LoadError>.Failure(new ContentPackage.LoadError(error, exception));
Identifier elemName = element.NameAsIdentifier(); Identifier elemName = element.NameAsIdentifier();
var type = Types.FirstOrDefault(t => t.Names.Contains(elemName)); 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}\""); return fail($"No content path defined for file of type \"{elemName}\"");
} }
using var errorCatcher = DebugConsole.ErrorCatcher.Create();
try try
{ {
filePath = type.MutateContentPath(filePath); filePath = type.MutateContentPath(filePath);
@@ -90,10 +92,16 @@ namespace Barotrauma
{ {
return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": file not found."); return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": file not found.");
} }
var file = type.CreateInstance(contentPackage, filePath); var file = type.CreateInstance(contentPackage, filePath);
return file is null if (file is null) { return fail($"Content type {type.Type.Name} is not implemented correctly"); }
? throw new Exception($"Content type is not implemented correctly")
: Result<ContentFile, LoadError>.Success(file); if (errorCatcher.Errors.Any())
{
return fail(
$"Errors were issued to the debug console when loading \"{filePath}\" of type \"{elemName}\"");
}
return Result<ContentFile, ContentPackage.LoadError>.Success(file);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -123,23 +131,5 @@ namespace Barotrauma
} }
public bool NotSyncedInMultiplayer => Types.Any(t => t.Type == GetType() && t.NotSyncedInMultiplayer); 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);
}
} }
} }
@@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml.Linq; using System.Xml.Linq;
@@ -14,6 +13,15 @@ namespace Barotrauma
{ {
public abstract class ContentPackage 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 static readonly Version MinimumHashCompatibleVersion = new Version(0, 18, 13, 0);
public const string LocalModsDir = "LocalMods"; public const string LocalModsDir = "LocalMods";
@@ -37,12 +45,30 @@ namespace Barotrauma
public readonly Option<DateTime> InstallTime; public readonly Option<DateTime> InstallTime;
public ImmutableArray<ContentFile> Files { get; private set; } public ImmutableArray<ContentFile> Files { get; private set; }
public ImmutableArray<ContentFile.LoadError> Errors { get; private set; }
/// <summary>
/// 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.
/// </summary>
public ImmutableArray<LoadError> FatalLoadErrors { get; private set; }
/// <summary>
/// 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.
/// </summary>
public Option<ContentPackageManager.LoadProgress.Error> EnableError { get; private set; }
= Option.None;
public bool HasAnyErrors => FatalLoadErrors.Length > 0 || EnableError.IsSome();
public async Task<bool> IsUpToDate() public async Task<bool> IsUpToDate()
{ {
if (!UgcId.TryUnwrap(out var ugcId)) { return true; } 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; } if (!InstallTime.TryUnwrap(out var installTime)) { return true; }
Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value); Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value);
@@ -55,20 +81,25 @@ namespace Barotrauma
/// <summary> /// <summary>
/// Does the content package include some content that needs to match between all players in multiplayer. /// Does the content package include some content that needs to match between all players in multiplayer.
/// </summary> /// </summary>
public bool HasMultiplayerSyncedContent { get; private set; } public bool HasMultiplayerSyncedContent { get; }
protected ContentPackage(XDocument doc, string path) protected ContentPackage(XDocument doc, string path)
{ {
using var errorCatcher = DebugConsole.ErrorCatcher.Create();
Path = path.CleanUpPathCrossPlatform(); Path = path.CleanUpPathCrossPlatform();
XElement rootElement = doc.Root ?? throw new NullReferenceException("XML document is invalid: root element is null."); XElement rootElement = doc.Root ?? throw new NullReferenceException("XML document is invalid: root element is null.");
Name = rootElement.GetAttributeString("name", "").Trim(); Name = rootElement.GetAttributeString("name", "").Trim();
AltNames = rootElement.GetAttributeStringArray("altnames", Array.Empty<string>()) AltNames = rootElement.GetAttributeStringArray("altnames", Array.Empty<string>())
.Select(n => n.Trim()).ToImmutableArray(); .Select(n => n.Trim()).ToImmutableArray();
AssertCondition(!string.IsNullOrEmpty(Name), "Name is null or empty");
UInt64 steamWorkshopId = rootElement.GetAttributeUInt64("steamworkshopid", 0); UInt64 steamWorkshopId = rootElement.GetAttributeUInt64("steamworkshopid", 0);
if (Name.IsNullOrWhiteSpace() && AltNames.Any())
{
Name = AltNames.First();
}
UgcId = steamWorkshopId != 0 UgcId = steamWorkshopId != 0
? Option<ContentPackageId>.Some(new SteamWorkshopId(steamWorkshopId)) ? Option<ContentPackageId>.Some(new SteamWorkshopId(steamWorkshopId))
: Option<ContentPackageId>.None(); : Option<ContentPackageId>.None();
@@ -85,23 +116,31 @@ namespace Barotrauma
.ToArray(); .ToArray();
Files = fileResults Files = fileResults
.OfType<Success<ContentFile, ContentFile.LoadError>>() .Successes()
.Select(f => f.Value)
.ToImmutableArray(); .ToImmutableArray();
Errors = fileResults FatalLoadErrors = fileResults
.OfType<Failure<ContentFile, ContentFile.LoadError>>() .Failures()
.Select(f => f.Error)
.ToImmutableArray(); .ToImmutableArray();
AssertCondition(!string.IsNullOrEmpty(Name), $"{nameof(Name)} is null or empty");
HasMultiplayerSyncedContent = Files.Any(f => !f.NotSyncedInMultiplayer); HasMultiplayerSyncedContent = Files.Any(f => !f.NotSyncedInMultiplayer);
Hash = CalculateHash(); Hash = CalculateHash();
var expectedHash = rootElement.GetAttributeString("expectedhash", ""); var expectedHash = rootElement.GetAttributeString("expectedhash", "");
if (HashMismatches(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) public bool HashMismatches(string expectedHash)
@@ -122,21 +161,21 @@ namespace Barotrauma
public bool NameMatches(string name) public bool NameMatches(string name)
=> NameMatches(name.ToIdentifier()); => NameMatches(name.ToIdentifier());
public static ContentPackage? TryLoad(string path) public static Result<ContentPackage, Exception> TryLoad(string path)
{ {
var (success, failure) = Result<ContentPackage, Exception>.GetFactoryMethods();
XDocument doc = XMLExtensions.TryLoadXml(path); XDocument doc = XMLExtensions.TryLoadXml(path);
try try
{ {
return doc.Root.GetAttributeBool("corepackage", false) return success(doc.Root.GetAttributeBool("corepackage", false)
? (ContentPackage)new CorePackage(doc, path) ? new CorePackage(doc, path)
: new RegularPackage(doc, path); : new RegularPackage(doc, path));
} }
catch (Exception e) catch (Exception e)
{ {
e = e.GetInnermost(); return failure(e.GetInnermost());
DebugConsole.ThrowError($"{e.Message}: {e.StackTrace}");
return null;
} }
} }
@@ -181,7 +220,7 @@ namespace Barotrauma
{ {
if (!condition) 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 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; return LoadResult.Success;
} }
public IEnumerable<ContentPackageManager.LoadProgress> LoadPackageEnumerable() public IEnumerable<ContentPackageManager.LoadProgress> LoadContentEnumerable()
{ {
using var errorCatcher = DebugConsole.ErrorCatcher.Create();
ContentFile[] getFilesToLoad(Predicate<ContentFile> predicate) ContentFile[] getFilesToLoad(Predicate<ContentFile> predicate)
=> Files.Where(predicate.Invoke).ToArray() => Files.Where(predicate.Invoke).ToArray()
#if DEBUG #if DEBUG
@@ -227,6 +268,7 @@ namespace Barotrauma
for (int i = 0; i < filesToLoad.Length; i++) for (int i = 0; i < filesToLoad.Length; i++)
{ {
Exception? exception = null; Exception? exception = null;
try try
{ {
//do not allow exceptions thrown here to crash the game //do not allow exceptions thrown here to crash the game
@@ -234,42 +276,53 @@ namespace Barotrauma
} }
catch (Exception e) catch (Exception e)
{ {
var innermost = e.GetInnermost();
DebugConsole.LogError($"Failed to load \"{filesToLoad[i].Path}\": {innermost.Message}\n{innermost.StackTrace}");
exception = e; exception = e;
} }
if (exception != null) if (exception != null)
{ {
yield return ContentPackageManager.LoadProgress.Failure(exception); 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 //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. //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 remainder = getFilesToLoad(f => !priorityFiles.Contains(f));
var loadEnumerable = var loadEnumerable =
loadFiles(priorityFiles, 0) loadFiles(priorityFiles, 0)
.Concat(loadFiles(remainder, priorityFiles.Length)); .Concat(loadFiles(remainder, priorityFiles.Length));
foreach (var p in loadEnumerable) 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; yield return p;
break; yield break;
} }
yield return p; yield return p;
} }
errorCatcher.Dispose();
} }
protected abstract void HandleLoadException(Exception e); public void UnloadContent()
public void UnloadPackage()
{ {
Files.ForEach(f => f.UnloadFile()); Files.ForEach(f => f.UnloadFile());
} }
@@ -284,21 +337,16 @@ namespace Barotrauma
.Select(e => ContentFile.CreateFromXElement(this, e)) .Select(e => ContentFile.CreateFromXElement(this, e))
.ToArray(); .ToArray();
foreach (var result in fileResults) foreach (var file in fileResults.Successes())
{ {
switch (result) if (file is BaseSubFile or ItemAssemblyFile)
{ {
case Success<ContentFile, ContentFile.LoadError> { Value: var file }: newFileList.Add(file);
if (file is BaseSubFile || file is ItemAssemblyFile) }
{ else
newFileList.Add(file); {
} var existingFile = Files.FirstOrDefault(f => f.Path == file.Path);
else newFileList.Add(existingFile ?? file);
{
var existingFile = Files.FirstOrDefault(f => f.Path == file.Path);
newFileList.Add(existingFile ?? file);
}
break;
} }
} }
@@ -331,16 +379,16 @@ namespace Barotrauma
public void LogErrors() public void LogErrors()
{ {
if (!Errors.Any()) if (!FatalLoadErrors.Any())
{ {
return; return;
} }
DebugConsole.AddWarning( DebugConsole.AddWarning(
$"The following errors occurred while loading the content package \"{Name}\". The package might not work correctly.\n" + $"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(); => error.ToString();
} }
} }
@@ -43,10 +43,5 @@ namespace Barotrauma
"Core package requires at least one of the following content types: " + "Core package requires at least one of the following content types: " +
string.Join(", ", missingFileTypes.Select(t => t.Type.Name))); 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);
}
} }
} }
@@ -1,4 +1,3 @@
using System;
using System.Xml.Linq; using System.Xml.Linq;
namespace Barotrauma namespace Barotrauma
@@ -9,11 +8,5 @@ namespace Barotrauma
{ {
AssertCondition(!doc.Root.GetAttributeBool("corepackage", false), "Expected a regular package, got a core package"); 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);
}
} }
} }
@@ -47,18 +47,19 @@ namespace Barotrauma
{ {
var oldCore = Core; var oldCore = Core;
if (newCore == oldCore) { yield break; } if (newCore == oldCore) { yield break; }
Core?.UnloadPackage(); if (newCore.FatalLoadErrors.Any()) { yield break; }
Core?.UnloadContent();
Core = newCore; Core = newCore;
foreach (var p in newCore.LoadPackageEnumerable()) { yield return p; } foreach (var p in newCore.LoadContentEnumerable()) { yield return p; }
SortContent(); SortContent();
yield return new LoadProgress(1.0f); yield return LoadProgress.Progress(1.0f);
} }
public static void ReloadCore() public static void ReloadCore()
{ {
if (Core == null) { return; } if (Core == null) { return; }
Core.UnloadPackage(); Core.UnloadContent();
Core.LoadPackage(); Core.LoadContent();
SortContent(); SortContent();
} }
@@ -79,10 +80,14 @@ namespace Barotrauma
if (ReferenceEquals(inNewRegular, regular)) { yield break; } if (ReferenceEquals(inNewRegular, regular)) { yield break; }
if (inNewRegular.SequenceEqual(regular)) { yield break; } if (inNewRegular.SequenceEqual(regular)) { yield break; }
ThrowIfDuplicates(inNewRegular); 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<RegularPackage> toUnload = regular.Where(r => !newRegular.Contains(r)); IEnumerable<RegularPackage> toUnload = regular.Where(r => !newRegular.Contains(r));
RegularPackage[] toLoad = newRegular.Where(r => !regular.Contains(r)).ToArray(); RegularPackage[] toLoad = newRegular.Where(r => !regular.Contains(r)).ToArray();
toUnload.ForEach(r => r.UnloadPackage()); toUnload.ForEach(r => r.UnloadContent());
Range<float> loadingRange = new Range<float>(0.0f, 1.0f); Range<float> loadingRange = new Range<float>(0.0f, 1.0f);
@@ -90,9 +95,9 @@ namespace Barotrauma
{ {
var package = toLoad[i]; var package = toLoad[i];
loadingRange = new Range<float>(i / (float)toLoad.Length, (i + 1) / (float)toLoad.Length); loadingRange = new Range<float>(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 //If an exception was thrown while loading this package, refuse to add it to the list of enabled packages
newRegular.Remove(package); newRegular.Remove(package);
@@ -103,7 +108,7 @@ namespace Barotrauma
} }
regular.Clear(); regular.AddRange(newRegular); regular.Clear(); regular.AddRange(newRegular);
SortContent(); SortContent();
yield return new LoadProgress(1.0f); yield return LoadProgress.Progress(1.0f);
} }
public static void ThrowIfDuplicates(IEnumerable<ContentPackage> pkgs) public static void ThrowIfDuplicates(IEnumerable<ContentPackage> pkgs)
@@ -231,10 +236,12 @@ namespace Barotrauma
public sealed partial class PackageSource : ICollection<ContentPackage> public sealed partial class PackageSource : ICollection<ContentPackage>
{ {
private readonly Predicate<string>? skipPredicate; private readonly Predicate<string>? skipPredicate;
private readonly Action<string, Exception>? onLoadFail;
public PackageSource(string dir, Predicate<string>? skipPredicate) public PackageSource(string dir, Predicate<string>? skipPredicate, Action<string, Exception>? onLoadFail)
{ {
this.skipPredicate = skipPredicate; this.skipPredicate = skipPredicate;
this.onLoadFail = onLoadFail;
directory = dir; directory = dir;
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
} }
@@ -278,25 +285,30 @@ namespace Barotrauma
{ {
var fileListPath = Path.Combine(subDir, ContentPackage.FileListFileName).CleanUpPathCrossPlatform(); var fileListPath = Path.Combine(subDir, ContentPackage.FileListFileName).CleanUpPathCrossPlatform();
if (this.Any(p => p.Path.Equals(fileListPath, StringComparison.OrdinalIgnoreCase))) { continue; } 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)) if (!File.Exists(fileListPath)) { continue; }
{ if (skipPredicate?.Invoke(fileListPath) is true) { continue; }
Debug.WriteLine($"Loaded \"{newPackage.Name}\"");
} 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 bool IsReadOnly => true;
} }
public static readonly PackageSource LocalPackages = new PackageSource(ContentPackage.LocalModsDir, skipPredicate: null); public static readonly PackageSource LocalPackages
public static readonly PackageSource WorkshopPackages = new PackageSource(ContentPackage.WorkshopModsDir, skipPredicate: SteamManager.Workshop.IsInstallingToPath); = 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; public static CorePackage? VanillaCorePackage { get; private set; } = null;
@@ -373,63 +397,77 @@ namespace Barotrauma
EnabledPackages.DisableRemovedMods(); EnabledPackages.DisableRemovedMods();
} }
public static ContentPackage? ReloadContentPackage(ContentPackage p) public static Result<ContentPackage, Exception> ReloadContentPackage(ContentPackage p)
{ {
ContentPackage? newPackage = ContentPackage.TryLoad(p.Path); var result = 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);
}
}
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); LocalPackages.SwapPackage(p, newPackage);
WorkshopPackages.SwapPackage(p, newPackage); WorkshopPackages.SwapPackage(p, newPackage);
} }
EnabledPackages.DisableRemovedMods(); EnabledPackages.DisableRemovedMods();
return newPackage; return result;
} }
public readonly struct LoadProgress public readonly record struct LoadProgress(Result<float, LoadProgress.Error> Result)
{ {
public readonly float Value; public readonly record struct Error(
public readonly Exception? Exception; Error.Reason ErrorReason,
Option<Exception> Exception)
public LoadProgress(float value)
{ {
Value = value; public enum Reason { Exception, ConsoleErrorsThrown }
Exception = null;
}
private LoadProgress(Exception exception) public Error(Reason reason) : this(reason, Option.None) { }
{ public Error(Exception exception) : this(Reason.Exception, Option.Some(exception)) { }
Value = -1f;
Exception = exception;
} }
public static LoadProgress Failure(Exception exception) public static LoadProgress Failure(Exception exception)
=> new LoadProgress(exception); => new LoadProgress(
Result<float, Error>.Failure(new Error(exception)));
public static LoadProgress Failure(Error.Reason reason)
=> new LoadProgress(
Result<float, Error>.Failure(new Error(reason)));
public static LoadProgress Progress(float value)
=> new LoadProgress(
Result<float, Error>.Success(value));
public LoadProgress Transform(Range<float> range) public LoadProgress Transform(Range<float> range)
=> Exception != null => Result.TryUnwrapSuccess(out var value)
? this ? new LoadProgress(
: new LoadProgress(MathHelper.Lerp(range.Start, range.End, Value)); Result<float, Error>.Success(
MathHelper.Lerp(range.Start, range.End, value)))
: this;
} }
public static void LoadVanillaFileList() public static void LoadVanillaFileList()
{ {
VanillaCorePackage = new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList); 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()); DebugConsole.ThrowError(error.ToString());
} }
@@ -444,6 +482,8 @@ namespace Barotrauma
if (VanillaCorePackage is null) { LoadVanillaFileList(); } if (VanillaCorePackage is null) { LoadVanillaFileList(); }
SteamManager.Workshop.DeleteUnsubscribedMods();
CorePackage enabledCorePackage = VanillaCorePackage!; CorePackage enabledCorePackage = VanillaCorePackage!;
List<RegularPackage> enabledRegularPackages = new List<RegularPackage>(); List<RegularPackage> enabledRegularPackages = new List<RegularPackage>();
@@ -512,7 +552,7 @@ namespace Barotrauma
yield return p.Transform(loadingRange); yield return p.Transform(loadingRange);
} }
yield return new LoadProgress(1.0f); yield return LoadProgress.Progress(1.0f);
} }
public static void LogEnabledRegularPackageErrors() public static void LogEnabledRegularPackageErrors()
@@ -5,6 +5,7 @@ using Barotrauma.Steam;
using FarseerPhysics; using FarseerPhysics;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization; using System.Globalization;
@@ -15,12 +16,12 @@ using Barotrauma.MapCreatures.Behavior;
namespace Barotrauma namespace Barotrauma
{ {
struct ColoredText readonly struct ColoredText
{ {
public string Text; public readonly string Text;
public Color Color; public readonly Color Color;
public bool IsCommand; public readonly bool IsCommand;
public bool IsError; public readonly bool IsError;
public readonly string Time; public readonly string Time;
@@ -31,7 +32,7 @@ namespace Barotrauma
this.IsCommand = isCommand; this.IsCommand = isCommand;
this.IsError = isError; this.IsError = isError;
Time = DateTime.Now.ToString(); Time = DateTime.Now.ToString(CultureInfo.InvariantCulture);
} }
} }
@@ -91,13 +92,57 @@ namespace Barotrauma
} }
} }
private static readonly Queue<ColoredText> queuedMessages = new Queue<ColoredText>(); private static readonly ConcurrentQueue<ColoredText> queuedMessages
= new ConcurrentQueue<ColoredText>();
public static readonly NamedEvent<ColoredText> MessageHandler = new NamedEvent<ColoredText>();
public struct ErrorCatcher : IDisposable
{
private readonly List<ColoredText> errors;
private readonly bool wasConsoleOpen;
private Identifier handlerId;
public IReadOnlyList<ColoredText> Errors => errors;
private ErrorCatcher(Identifier handlerId)
{
this.handlerId = handlerId;
#if CLIENT
this.wasConsoleOpen = IsOpen;
#else
this.wasConsoleOpen = false;
#endif
this.errors = new List<ColoredText>();
//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); static partial void ShowHelpMessage(Command command);
const int MaxMessages = 300; const int MaxMessages = 300;
public static List<ColoredText> Messages = new List<ColoredText>(); public static readonly List<ColoredText> Messages = new List<ColoredText>();
public delegate void QuestionCallback(string answer); public delegate void QuestionCallback(string answer);
private static QuestionCallback activeQuestionCallback; private static QuestionCallback activeQuestionCallback;
@@ -2290,11 +2335,10 @@ namespace Barotrauma
private static void NewMessage(string msg, Color color, bool isCommand, bool isError) private static void NewMessage(string msg, Color color, bool isCommand, bool isError)
{ {
if (string.IsNullOrEmpty(msg)) { return; } if (string.IsNullOrEmpty(msg)) { return; }
lock (queuedMessages) var newMsg = new ColoredText(msg, color, isCommand, isError);
{ queuedMessages.Enqueue(newMsg);
queuedMessages.Enqueue(new ColoredText(msg, color, isCommand, isError)); MessageHandler.Invoke(newMsg);
}
} }
public static void ShowQuestionPrompt(string question, QuestionCallback onAnswered, string[] args = null, int argCount = -1) public static void ShowQuestionPrompt(string question, QuestionCallback onAnswered, string[] args = null, int argCount = -1)
@@ -159,7 +159,7 @@ namespace Barotrauma
newCharacter.HumanPrefab = humanPrefab; newCharacter.HumanPrefab = humanPrefab;
newCharacter.TeamID = TeamID; newCharacter.TeamID = TeamID;
newCharacter.EnableDespawn = false; newCharacter.EnableDespawn = false;
humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); humanPrefab.GiveItems(newCharacter, newCharacter.Submarine, spawnPos as WayPoint);
if (LootingIsStealing) if (LootingIsStealing)
{ {
foreach (Item item in newCharacter.Inventory.FindAllItems(recursive: true)) foreach (Item item in newCharacter.Inventory.FindAllItems(recursive: true))
@@ -25,6 +25,8 @@ namespace Barotrauma
private readonly Identifier spawnPointTag; private readonly Identifier spawnPointTag;
private readonly Identifier destructibleItemTag; private readonly Identifier destructibleItemTag;
private readonly string endCinematicSound;
private ImmutableArray<Character> minions; private ImmutableArray<Character> minions;
private readonly int minionCount; private readonly int minionCount;
private readonly float minionScatter; private readonly float minionScatter;
@@ -134,7 +136,7 @@ namespace Barotrauma
spawnPointTag = prefab.ConfigElement.GetAttributeIdentifier(nameof(spawnPointTag), Identifier.Empty); spawnPointTag = prefab.ConfigElement.GetAttributeIdentifier(nameof(spawnPointTag), Identifier.Empty);
destructibleItemTag = prefab.ConfigElement.GetAttributeIdentifier(nameof(destructibleItemTag), 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); startCinematicDistance = prefab.ConfigElement.GetAttributeFloat(nameof(startCinematicDistance), 0);
} }
@@ -407,7 +407,7 @@ namespace Barotrauma
{ {
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f);
info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); 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 // 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); Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
spawnedCharacter.HumanPrefab = humanPrefab; spawnedCharacter.HumanPrefab = humanPrefab;
humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn); 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); characters.Add(spawnedCharacter);
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true)); characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
@@ -15,11 +15,17 @@ namespace Barotrauma
{ {
public Item Item; public Item Item;
/// <summary>
/// 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 <see href="RequiredRetrievalState">RequiredRetrievalState</see> 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)
/// </summary>
public enum RetrievalState public enum RetrievalState
{ {
None = 0, None = 0,
PickedUp = 1, Interact = 1,
RetrievedToSub = 2 PickedUp = 2,
RetrievedToSub = 3
} }
public readonly ItemPrefab ItemPrefab; public readonly ItemPrefab ItemPrefab;
@@ -41,10 +47,19 @@ namespace Barotrauma
public readonly bool HideLabelAfterRetrieved; public readonly bool HideLabelAfterRetrieved;
public bool Retrieved => public bool Retrieved
RequiredRetrievalState == RetrievalState.RetrievedToSub ? {
State == RetrievalState.RetrievedToSub : get
State != RetrievalState.None; {
return RequiredRetrievalState switch
{
RetrievalState.None => true,
RetrievalState.Interact or RetrievalState.PickedUp => State >= RequiredRetrievalState,
RetrievalState.RetrievedToSub => State == RetrievalState.RetrievedToSub,
_ => throw new NotImplementedException(),
};
}
}
private RetrievalState state; private RetrievalState state;
public RetrievalState State public RetrievalState State
@@ -60,6 +75,8 @@ namespace Barotrauma
} }
} }
public bool Interacted;
private readonly SalvageMission mission; private readonly SalvageMission mission;
/// <summary> /// <summary>
@@ -268,6 +285,13 @@ namespace Barotrauma
target.Item.body.SetTransformIgnoreContacts(target.Item.body.SimPosition, target.Item.body.Rotation); target.Item.body.SetTransformIgnoreContacts(target.Item.body.SimPosition, target.Item.body.Rotation);
target.Item.body.FarseerBody.BodyType = BodyType.Kinematic; 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++) for (int i = 0; i < target.StatusEffects.Count; i++)
{ {
List<StatusEffect> effectList = target.StatusEffects[i]; List<StatusEffect> effectList = target.StatusEffects[i];
@@ -352,22 +376,33 @@ namespace Barotrauma
switch (target.State) switch (target.State)
{ {
case Target.RetrievalState.None: case Target.RetrievalState.None:
if (target.Interacted)
{
TrySetRetrievalState(Target.RetrievalState.Interact);
}
var root = target.Item?.GetRootContainer() ?? target.Item; var root = target.Item?.GetRootContainer() ?? target.Item;
if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1) if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1)
{ {
target.State = Target.RetrievalState.PickedUp; TrySetRetrievalState(Target.RetrievalState.PickedUp);
if (target.Retrieved) { State = i + 1 ; }
} }
break; break;
case Target.RetrievalState.PickedUp: case Target.RetrievalState.PickedUp:
Submarine parentSub = target.Item.CurrentHull?.Submarine ?? target.Item.GetRootInventoryOwner()?.Submarine; Submarine parentSub = target.Item.CurrentHull?.Submarine ?? target.Item.GetRootInventoryOwner()?.Submarine;
if (parentSub != null && parentSub.Info.Type == SubmarineType.Player) if (parentSub != null && parentSub.Info.Type == SubmarineType.Player)
{ {
target.State = Target.RetrievalState.RetrievedToSub; TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
if (target.Retrieved) { State = i + 1; }
} }
break; 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)) if (targets.All(t => t.Retrieved))
{ {
@@ -383,7 +418,7 @@ namespace Barotrauma
protected override void EndMissionSpecific(bool completed) 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 //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) foreach (var target in targets)
{ {
if (target.RemoveItem) if (target.RemoveItem)
@@ -594,7 +594,7 @@ namespace Barotrauma
{ {
GameAnalyticsManager.AddDesignEvent( GameAnalyticsManager.AddDesignEvent(
$"MonsterSpawn:{GameMain.GameSession.GameMode?.Preset?.Identifier.Value ?? "none"}:{Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"}:{SpawnPosType}:{SpeciesName}", $"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); }, delayBetweenSpawns * i);
i++; i++;
@@ -316,5 +316,17 @@ namespace Barotrauma.Extensions
=> source => source
.OfType<Some<T>>() .OfType<Some<T>>()
.Select(some => some.Value); .Select(some => some.Value);
public static IEnumerable<TSuccess> Successes<TSuccess, TFailure>(
this IEnumerable<Result<TSuccess, TFailure>> source)
=> source
.OfType<Success<TSuccess, TFailure>>()
.Select(s => s.Value);
public static IEnumerable<TFailure> Failures<TSuccess, TFailure>(
this IEnumerable<Result<TSuccess, TFailure>> source)
=> source
.OfType<Failure<TSuccess, TFailure>>()
.Select(f => f.Error);
} }
} }
@@ -57,6 +57,7 @@ namespace Barotrauma
if (!data.ContainsKey(identifier)) if (!data.ContainsKey(identifier))
{ {
data.Add(identifier, value); data.Add(identifier, value);
SteamAchievementManager.OnCampaignMetadataSet(identifier, value);
return; return;
} }
@@ -65,7 +65,7 @@ namespace Barotrauma
float reputationGainMultiplier = 1f; float reputationGainMultiplier = 1f;
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) 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; reputationGainMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationGainMultiplier, Identifier) ?? 0;
} }
reputationChange *= reputationGainMultiplier; reputationChange *= reputationGainMultiplier;
@@ -75,7 +75,7 @@ namespace Barotrauma
float reputationLossMultiplier = 1f; float reputationLossMultiplier = 1f;
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) 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; reputationLossMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationLossMultiplier, Identifier) ?? 0;
} }
reputationChange *= reputationLossMultiplier; reputationChange *= reputationLossMultiplier;
@@ -79,7 +79,7 @@ namespace Barotrauma
public bool DisableEvents public bool DisableEvents
{ {
get { return IsFirstRound && Timing.TotalTime < GameMain.GameSession.RoundStartTime + FirstRoundEventDelay; } get { return IsFirstRound && GameMain.GameSession.RoundDuration > FirstRoundEventDelay; }
} }
public bool CheatsEnabled; public bool CheatsEnabled;
@@ -248,7 +248,7 @@ namespace Barotrauma
{ {
for (int i = 0; i < wall.SectionCount; i++) 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.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 Pickable p && p.IsAttached)) { continue; }
if (item.Components.Any(c => c is Wire w && w.Connections.Any(c => c != null))) { continue; } if (item.Components.Any(c => c is Wire w && w.Connections.Any(c => c != null))) { continue; }
if (item.Container?.GetComponent<ItemContainer>() is { DrawInventory: false }) { continue; }
itemsToTransfer.Add((item, item.Container)); itemsToTransfer.Add((item, item.Container));
item.Submarine = null; item.Submarine = null;
} }
@@ -29,7 +29,10 @@ namespace Barotrauma
private Location[]? dummyLocations; private Location[]? dummyLocations;
public CrewManager? CrewManager; public CrewManager? CrewManager;
public double RoundStartTime; public float RoundDuration
{
get; private set;
}
public double TimeSpentCleaning, TimeSpentPainting; public double TimeSpentCleaning, TimeSpentPainting;
@@ -392,6 +395,7 @@ namespace Barotrauma
#if DEBUG #if DEBUG
DateTime startTime = DateTime.Now; DateTime startTime = DateTime.Now;
#endif #endif
RoundDuration = 0.0f;
AfflictionPrefab.LoadAllEffects(); AfflictionPrefab.LoadAllEffects();
MirrorLevel = mirrorLevel; MirrorLevel = mirrorLevel;
@@ -543,7 +547,7 @@ namespace Barotrauma
RoundSummary = new RoundSummary(GameMode, Missions, StartLocation, EndLocation); 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); GUI.AddMessage("", Color.Transparent, 3.0f, playSound: false);
if (EndLocation != null && levelData != null) if (EndLocation != null && levelData != null)
@@ -610,9 +614,9 @@ namespace Barotrauma
GameMode.Start(); GameMode.Start();
foreach (Mission mission in missions) foreach (Mission mission in missions)
{ {
int prevEntityCount = Entity.GetEntities().Count(); int prevEntityCount = Entity.GetEntities().Count;
mission.Start(Level.Loaded); 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( DebugConsole.ThrowError(
$"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " + $"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " +
@@ -651,7 +655,7 @@ namespace Barotrauma
CreatureMetrics.Instance.RecentlyEncountered.Clear(); CreatureMetrics.Instance.RecentlyEncountered.Clear();
GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition; GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition;
RoundStartTime = Timing.TotalTime; RoundDuration = 0.0f;
GameMain.ResetFrameTime(); GameMain.ResetFrameTime();
IsRunning = true; IsRunning = true;
} }
@@ -762,6 +766,7 @@ namespace Barotrauma
public void Update(float deltaTime) public void Update(float deltaTime)
{ {
RoundDuration += deltaTime;
EventManager?.Update(deltaTime); EventManager?.Update(deltaTime);
GameMode?.Update(deltaTime); GameMode?.Update(deltaTime);
//backwards for loop because the missions may get completed and removed from the list in Update() //backwards for loop because the missions may get completed and removed from the list in Update()
@@ -914,17 +919,16 @@ namespace Barotrauma
#else #else
bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
#endif #endif
double roundDuration = Timing.TotalTime - RoundStartTime;
GameAnalyticsManager.AddProgressionEvent( GameAnalyticsManager.AddProgressionEvent(
success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail, success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail,
GameMode?.Preset.Identifier.Value ?? "none", GameMode?.Preset.Identifier.Value ?? "none",
roundDuration); RoundDuration);
string eventId = "EndRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":"; string eventId = "EndRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":";
LogEndRoundStats(eventId); LogEndRoundStats(eventId);
if (GameMode is CampaignMode campaignMode) if (GameMode is CampaignMode campaignMode)
{ {
GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", GetAmountOfMoney(crewCharacters) - prevMoney); GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", GetAmountOfMoney(crewCharacters) - prevMoney);
campaignMode.TotalPlayTime += roundDuration; campaignMode.TotalPlayTime += RoundDuration;
} }
#if CLIENT #if CLIENT
HintManager.OnRoundEnded(); HintManager.OnRoundEnded();
@@ -950,21 +954,20 @@ namespace Barotrauma
public void LogEndRoundStats(string eventId) public void LogEndRoundStats(string eventId)
{ {
double roundDuration = Timing.TotalTime - RoundStartTime; GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), RoundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), roundDuration); GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name.Value ?? "none"), RoundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name.Value ?? "none"), roundDuration); GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), RoundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), roundDuration);
foreach (Mission mission in missions) 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) if (Level.Loaded != null)
{ {
Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ? Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ?
Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier : Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier :
Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier(); Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier();
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + levelId), roundDuration); 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 + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"), RoundDuration);
} }
if (Submarine.MainSub != null) if (Submarine.MainSub != null)
@@ -315,6 +315,7 @@ namespace Barotrauma.Items.Components
if (f2.Body.UserData is Limb targetLimb) if (f2.Body.UserData is Limb targetLimb)
{ {
if (targetLimb.IsSevered || targetLimb.character == null || targetLimb.character == User) { return false; } if (targetLimb.IsSevered || targetLimb.character == null || targetLimb.character == User) { return false; }
if (targetLimb.character.IgnoreMeleeWeapons) { return false; }
var targetCharacter = targetLimb.character; var targetCharacter = targetLimb.character;
if (targetCharacter == picker) { return false; } if (targetCharacter == picker) { return false; }
if (AllowHitMultiple) if (AllowHitMultiple)
@@ -330,6 +331,7 @@ namespace Barotrauma.Items.Components
else if (f2.Body.UserData is Character targetCharacter) else if (f2.Body.UserData is Character targetCharacter)
{ {
if (targetCharacter == picker || targetCharacter == User) { return false; } 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 targetLimb = targetCharacter.AnimController.GetLimb(LimbType.Torso); //Otherwise armor can be bypassed in strange ways
if (AllowHitMultiple) if (AllowHitMultiple)
{ {
@@ -809,7 +809,14 @@ namespace Barotrauma.Items.Components
} }
else else
{ {
hasRequiredItems = itemList.Any(Predicate); if (itemList.Any(Predicate))
{
hasRequiredItems = !relatedItem.RequireEmpty;
}
else
{
hasRequiredItems = relatedItem.MatchOnEmpty || relatedItem.RequireEmpty;
}
if (!hasRequiredItems) if (!hasRequiredItems)
{ {
shouldBreak = true; shouldBreak = true;
@@ -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.")] [Serialize(100, IsPropertySaveable.No, description: "How many items are placed in a row before starting a new row.")]
public int ItemsPerRow { get; set; } 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 public bool DrawInventory
{ {
get; get;
@@ -135,9 +135,6 @@ namespace Barotrauma.Items.Components
set; set;
} }
[Serialize(true, IsPropertySaveable.No)]
public bool AllowAccess { get; set; }
[Serialize(false, IsPropertySaveable.No)] [Serialize(false, IsPropertySaveable.No)]
public bool AccessOnlyWhenBroken { get; set; } public bool AccessOnlyWhenBroken { get; set; }
@@ -530,12 +527,12 @@ namespace Barotrauma.Items.Components
public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) 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) public override bool Select(Character character)
{ {
if (!AllowAccess) { return false; } if (!DrawInventory) { return false; }
if (item.Container != null) { return false; } if (item.Container != null) { return false; }
if (AccessOnlyWhenBroken) if (AccessOnlyWhenBroken)
{ {
@@ -571,7 +568,7 @@ namespace Barotrauma.Items.Components
public override bool Pick(Character picker) public override bool Pick(Character picker)
{ {
if (!AllowAccess) { return false; } if (!DrawInventory) { return false; }
if (AccessOnlyWhenBroken) if (AccessOnlyWhenBroken)
{ {
if (item.Condition > 0) if (item.Condition > 0)
@@ -539,7 +539,7 @@ namespace Barotrauma.Items.Components
var prevUser = user; var prevUser = user;
CancelFabricating(); CancelFabricating();
amountRemaining--; amountRemaining--;
if (amountRemaining > 0 && CanBeFabricated(prevFabricatedItem, availableIngredients, prevUser)) if (amountRemaining > 0 && CanBeFabricated(prevFabricatedItem, availableIngredients, prevUser))
{ {
//keep fabricating if we can fabricate more //keep fabricating if we can fabricate more
@@ -745,7 +745,7 @@ namespace Barotrauma.Items.Components
itemList.AddRange(container.Inventory.AllItems); itemList.AddRange(container.Inventory.AllItems);
} }
} }
if (user?.Inventory != null) if (user?.Inventory != null && user.SelectedItem == item)
{ {
itemList.AddRange(user.Inventory.AllItems); itemList.AddRange(user.Inventory.AllItems);
linkedInventories.Add(user.Inventory); linkedInventories.Add(user.Inventory);
@@ -65,14 +65,7 @@ namespace Barotrauma.Items.Components
{ {
get get
{ {
if (GameMain.GameSession != null) return GameMain.GameSession?.RoundDuration ?? 0.0f;
{
return (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime);
}
else
{
return 0.0f;
}
} }
} }

Some files were not shown because too many files have changed in this diff Show More