Build 0.20.8.0

This commit is contained in:
Markus Isberg
2022-11-25 19:56:30 +02:00
parent ecb6d40b4b
commit df805574c4
111 changed files with 2347 additions and 1283 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);
} }
@@ -653,15 +653,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;
@@ -101,6 +101,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 &&
@@ -174,7 +175,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;
} }
@@ -242,6 +244,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)
@@ -491,7 +502,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);
}
} }
} }
} }
@@ -327,7 +327,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),
@@ -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);
@@ -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;
} }
@@ -859,18 +860,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();
@@ -879,15 +879,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)
@@ -945,11 +947,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();
@@ -964,14 +966,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)
@@ -1029,11 +1034,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();
@@ -1048,14 +1053,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)
@@ -1142,6 +1150,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)
{ {
@@ -1172,6 +1181,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)
{ {
@@ -1205,52 +1215,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()}");
}
} }
} }
@@ -1287,7 +1300,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)
{ {
@@ -1662,6 +1675,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)
{ {
@@ -2062,7 +2077,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;
@@ -2082,7 +2102,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");
@@ -2110,6 +2134,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
@@ -2119,6 +2144,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()
@@ -2192,29 +2218,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)
{ {
@@ -444,7 +444,7 @@ namespace Barotrauma
break; break;
} }
Map.ProgressWorld(transitionType, (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime)); Map.ProgressWorld(transitionType, GameMain.GameSession.RoundDuration);
var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null,
transitionType == TransitionType.LeaveLocation ? Alignment.BottomCenter : Alignment.Center, transitionType == TransitionType.LeaveLocation ? Alignment.BottomCenter : Alignment.Center,
@@ -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;
@@ -67,6 +71,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)
{ {
CreateGUI(); CreateGUI();
@@ -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)
@@ -761,6 +787,95 @@ namespace Barotrauma.Items.Components
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();
@@ -812,7 +812,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)
{ {
@@ -44,7 +51,7 @@ namespace Barotrauma.Items.Components
//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;
@@ -1282,7 +1282,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)
{ {
@@ -1042,19 +1042,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)
@@ -1074,6 +1066,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);
}
else if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign)
{
GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null);
}
break;
case ServerNetSegment.EntityPosition:
inc.ReadPadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly
bool isItem = inc.ReadBoolean(); inc.ReadPadBits();
UInt32 incomingUintIdentifier = inc.ReadUInt32();
UInt16 id = inc.ReadUInt16();
uint msgLength = inc.ReadVariableUInt32();
int msgEndPos = (int)(inc.BitPosition + msgLength * 8);
var entity = Entity.FindEntityByID(id) as IServerPositionSync;
if (msgEndPos > inc.LengthBits)
{
DebugConsole.ThrowError($"Error while reading a position update for the entity \"({entity?.ToString() ?? "null"})\". Message length exceeds the size of the buffer.");
return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.Yes;
}
debugEntityList.Add(entity);
if (entity != null)
{
if (entity is Item != isItem)
{ {
MultiPlayerCampaign.ClientRead(inc); 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 (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign) else if (entity is MapEntity { Prefab: { UintIdentifier: { } uintIdentifier } } me &&
uintIdentifier != incomingUintIdentifier)
{ {
GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null); 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.ENTITY_POSITION:
inc.ReadPadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly
bool isItem = inc.ReadBoolean(); inc.ReadPadBits();
UInt32 incomingUintIdentifier = inc.ReadUInt32();
UInt16 id = inc.ReadUInt16();
uint msgLength = inc.ReadVariableUInt32();
int msgEndPos = (int)(inc.BitPosition + msgLength * 8);
var entity = Entity.FindEntityByID(id) as IServerPositionSync;
if (msgEndPos > inc.LengthBits)
{ {
DebugConsole.ThrowError($"Error while reading a position update for the entity \"({entity?.ToString() ?? "null"})\". Message length exceeds the size of the buffer."); entity.ClientReadPosition(inc, sendingTime);
return;
} }
}
debugEntityList.Add(entity); //force to the correct position in case the entity doesn't exist
if (entity != null) //or the message wasn't read correctly for whatever reason
{ inc.BitPosition = msgEndPos;
if (entity is Item != isItem) inc.ReadPadBits();
{ break;
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..."); case ServerNetSegment.ClientList:
} ReadClientList(inc);
else if (entity is MapEntity { Prefab: { UintIdentifier: { } uintIdentifier } } me && break;
uintIdentifier != incomingUintIdentifier) case ServerNetSegment.EntityEvent:
{ case ServerNetSegment.EntityEventInitial:
DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message." if (!EntityEventManager.Read(segment, inc, sendingTime, debugEntityList))
+$"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..."); return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.Yes;
} }
else break;
{ case ServerNetSegment.ChatMessage:
entity.ClientReadPosition(inc, sendingTime); ChatMessage.ClientRead(inc);
} break;
} default:
throw new Exception($"Unknown segment \"{segment}\"!)");
//force to the correct position in case the entity doesn't exist
//or the message wasn't read correctly for whatever reason
inc.BitPosition = msgEndPos;
inc.ReadPadBits();
break;
case ServerNetObject.CLIENT_LIST:
ReadClientList(inc);
break;
case ServerNetObject.ENTITY_EVENT:
case ServerNetObject.ENTITY_EVENT_INITIAL:
if (!EntityEventManager.Read(objHeader.Value, inc, sendingTime, debugEntityList))
{
return;
}
break;
case ServerNetObject.CHAT_MESSAGE:
ChatMessage.ClientRead(inc);
break;
default:
throw new Exception($"Unknown object header \"{objHeader}\"!)");
}
prevBitLength = inc.BitPosition - prevBitPos;
prevByteLength = inc.BytePosition - prevBytePos;
prevObjHeader = objHeader;
prevBitPos = inc.BitPosition;
prevBytePos = inc.BytePosition;
} }
}
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,10 +2153,6 @@ 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--)
{ {
@@ -2175,23 +2160,11 @@ namespace Barotrauma.Networking
} }
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;
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); throw new Exception(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));
@@ -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})");
@@ -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) },
@@ -3076,10 +3076,16 @@ 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();
} }
@@ -814,7 +814,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();
@@ -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;
@@ -50,20 +50,9 @@ namespace Barotrauma.Steam
} }
} }
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>0.20.7.0</Version> <Version>0.20.8.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>0.20.7.0</Version> <Version>0.20.8.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>0.20.7.0</Version> <Version>0.20.8.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>0.20.7.0</Version> <Version>0.20.8.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>0.20.7.0</Version> <Version>0.20.8.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(transitionType, (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime)); Map.ProgressWorld(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; int settingsBytes = outmsg.LengthBytes;
if (NetIdUtils.IdMoreRecent(GameMain.NetLobbyScreen.LastUpdateID, c.LastRecvLobbyUpdate)) int initialUpdateBytes = 0;
{
outmsg.WriteBoolean(true);
outmsg.WritePadBits();
outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID); if (ServerSettings.UnsentFlags() != ServerSettings.NetFlags.None)
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; 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);
} }
} }
@@ -3327,9 +3335,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>0.20.7.0</Version> <Version>0.20.8.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>
@@ -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))
@@ -206,7 +206,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
@@ -322,7 +322,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;
} }
@@ -529,7 +529,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; }
@@ -798,6 +798,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
{ {
@@ -845,6 +846,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>
@@ -895,6 +902,10 @@ namespace Barotrauma
{ {
itemSelectedTime = Timing.TotalTime; itemSelectedTime = Timing.TotalTime;
} }
if (prevSelectedItem != _selectedItem && prevSelectedItem?.OnDeselect != null)
{
prevSelectedItem.OnDeselect(this);
}
} }
} }
/// <summary> /// <summary>
@@ -2687,9 +2698,9 @@ namespace Barotrauma
return; return;
} }
if (ResetInteract) if (DisableInteract)
{ {
ResetInteract = false; DisableInteract = false;
return; return;
} }
@@ -2710,25 +2721,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;
}
focusedItem = CanInteract ? FindItemAtPosition(mouseSimPos, aimAssist) : null;
if (focusedItem != null && focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None)
{
FocusedCharacter = null;
}
findFocusedTimer = 0.05f;
} }
else
var item = FindItemAtPosition(mouseSimPos, aimAssist);
focusedItem = CanInteract ? item : null;
if (focusedItem != null && focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None)
{ {
FocusedCharacter = null; if (focusedItem != null && !CanInteractWith(focusedItem)) { focusedItem = null; }
if (FocusedCharacter != null && !CanInteractWith(FocusedCharacter)) { FocusedCharacter = null; }
} }
findFocusedTimer = 0.05f;
} }
} }
else else
@@ -2957,6 +2974,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; }
@@ -2996,6 +3022,7 @@ namespace Barotrauma
} }
HideFace = false; HideFace = false;
IgnoreMeleeWeapons = false;
UpdateSightRange(deltaTime); UpdateSightRange(deltaTime);
UpdateSoundRange(deltaTime); UpdateSoundRange(deltaTime);
@@ -4102,7 +4129,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))
{ {
@@ -1238,7 +1238,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;
@@ -1292,6 +1292,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 _);
@@ -1309,7 +1321,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 is AfflictionPrefabHusk huskPrefab) if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab)
{ {
if (huskPrefab.TargetSpecies.None(s => s == Character.SpeciesName)) if (huskPrefab.TargetSpecies.None(s => s == Character.SpeciesName))
@@ -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
{ {
@@ -501,6 +501,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; }
@@ -37,14 +37,19 @@ namespace Barotrauma.Abilities
{ {
if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; } if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; }
// FIXME there's probably a better way to check the faction affiliated with the mission later foreach (var (factionIdentifier, amount) in mission.ReputationRewards)
foreach (Identifier factionIdentifier in mission.ReputationRewards.Keys)
{ {
if (factions.Where(faction => factionIdentifier == faction.Prefab.Identifier).Any(static faction => faction.GetPlayerAffiliationStatus() != FactionAffiliation.Affiliated)) if (amount <= 0) { continue; }
Faction faction = factions.FirstOrDefault(faction => factionIdentifier == faction.Prefab.Identifier);
if (faction?.GetPlayerAffiliationStatus() is FactionAffiliation.Affiliated)
{ {
return false; return true;
} }
} }
return false;
} }
return missionType.Contains(mission.Prefab.Type); return missionType.Contains(mission.Prefab.Type);
@@ -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();
}
}
}
@@ -35,10 +35,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++;
}
} }
} }
@@ -139,23 +139,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,20 +276,30 @@ 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));
@@ -257,19 +309,20 @@ namespace Barotrauma
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 (!File.Exists(fileListPath)) { continue; }
if (skipPredicate?.Invoke(fileListPath) is true) { continue; }
var result = ContentPackage.TryLoad(fileListPath);
if (!result.TryUnwrapSuccess(out var newPackage))
{ {
if (skipPredicate?.Invoke(fileListPath) is true) { continue; } onLoadFail?.Invoke(
fileListPath,
ContentPackage? newPackage = ContentPackage.TryLoad(fileListPath); result.TryUnwrapFailure(out var exception) ? exception : throw new Exception("unreachable"));
if (newPackage is CorePackage corePackage) continue;
{
corePackages.Add(corePackage);
}
else if (newPackage is RegularPackage regularPackage)
{
regularPackages.Add(regularPackage);
}
if (!(newPackage is null))
{
Debug.WriteLine($"Loaded \"{newPackage.Name}\"");
}
} }
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;
@@ -2288,10 +2333,9 @@ namespace Barotrauma
{ {
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)
@@ -404,7 +404,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
@@ -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++;
@@ -310,5 +310,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);
} }
} }
@@ -62,6 +62,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;
} }
@@ -80,7 +80,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;
@@ -249,7 +249,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);
} }
} }
} }
@@ -1094,6 +1094,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;
} }
@@ -28,7 +28,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;
@@ -353,6 +356,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;
@@ -503,7 +507,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)
@@ -571,9 +575,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. " +
@@ -612,7 +616,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;
} }
@@ -713,6 +717,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()
@@ -865,17 +870,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();
@@ -901,21 +905,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)
{ {
@@ -816,7 +816,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)
@@ -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;
}
} }
} }
@@ -639,7 +639,13 @@ namespace Barotrauma.Items.Components
ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent<ItemContainer>(); ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent<ItemContainer>();
if (projectileContainer != null && projectileContainer.Item != item) if (projectileContainer != null && projectileContainer.Item != item)
{ {
projectileContainer?.Item.Use(deltaTime, null); projectileContainer.Item.Use(deltaTime, null);
//Use root container (e.g. loader) too in case it needs to react to firing somehow
var rootContainer = projectileContainer.Item.GetRootContainer();
if (rootContainer != projectileContainer.Item)
{
rootContainer.Use(deltaTime, null);
}
} }
} }
else else
@@ -649,7 +655,7 @@ namespace Barotrauma.Items.Components
var e = item.linkedTo[(j + currentLoaderIndex) % item.linkedTo.Count]; var e = item.linkedTo[(j + currentLoaderIndex) % item.linkedTo.Count];
//use linked projectile containers in case they have to react to the turret being launched somehow //use linked projectile containers in case they have to react to the turret being launched somehow
//(play a sound, spawn more projectiles) //(play a sound, spawn more projectiles)
if (!(e is Item linkedItem)) { continue; } if (e is not Item linkedItem) { continue; }
if (!item.Prefab.IsLinkAllowed(e.Prefab)) { continue; } if (!item.Prefab.IsLinkAllowed(e.Prefab)) { continue; }
if (linkedItem.Condition <= 0.0f) if (linkedItem.Condition <= 0.0f)
{ {
@@ -692,7 +698,7 @@ namespace Barotrauma.Items.Components
foreach (MapEntity e in item.linkedTo) foreach (MapEntity e in item.linkedTo)
{ {
if (!(e is Item linkedItem)) { continue; } if (e is not Item linkedItem) { continue; }
if (!((MapEntity)item).Prefab.IsLinkAllowed(e.Prefab)) { continue; } if (!((MapEntity)item).Prefab.IsLinkAllowed(e.Prefab)) { continue; }
if (linkedItem.GetComponent<Repairable>() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag("turretammosource")) if (linkedItem.GetComponent<Repairable>() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag("turretammosource"))
{ {
@@ -872,7 +878,7 @@ namespace Barotrauma.Items.Components
partial void LaunchProjSpecific(); partial void LaunchProjSpecific();
private void ShiftItemsInProjectileContainer(ItemContainer container) private static void ShiftItemsInProjectileContainer(ItemContainer container)
{ {
if (container == null) { return; } if (container == null) { return; }
bool moved; bool moved;
@@ -1063,8 +1069,8 @@ namespace Barotrauma.Items.Components
character.AIController.SelectTarget(null); character.AIController.SelectTarget(null);
} }
bool canShoot = true; bool canShoot = HasPowerToShoot();
if (!HasPowerToShoot()) if (!canShoot)
{ {
List<PowerContainer> batteries = GetDirectlyConnectedBatteries(); List<PowerContainer> batteries = GetDirectlyConnectedBatteries();
float lowestCharge = 0.0f; float lowestCharge = 0.0f;
@@ -1089,7 +1095,6 @@ namespace Barotrauma.Items.Components
character.Speak(TextManager.Get("DialogSupercapacitorIsBroken").Value, character.Speak(TextManager.Get("DialogSupercapacitorIsBroken").Value,
identifier: "supercapacitorisbroken".ToIdentifier(), identifier: "supercapacitorisbroken".ToIdentifier(),
minDurationBetweenSimilar: 30.0f); minDurationBetweenSimilar: 30.0f);
canShoot = false;
} }
} }
} }
@@ -1104,7 +1109,6 @@ namespace Barotrauma.Items.Components
character.Speak(TextManager.Get("DialogTurretHasNoPower").Value, character.Speak(TextManager.Get("DialogTurretHasNoPower").Value,
identifier: "turrethasnopower".ToIdentifier(), identifier: "turrethasnopower".ToIdentifier(),
minDurationBetweenSimilar: 30.0f); minDurationBetweenSimilar: 30.0f);
canShoot = false;
} }
} }
@@ -1283,7 +1287,7 @@ namespace Barotrauma.Items.Components
closestDistance = shootDistance; closestDistance = shootDistance;
foreach (var wall in Level.Loaded.ExtraWalls) foreach (var wall in Level.Loaded.ExtraWalls)
{ {
if (!(wall is DestructibleLevelWall destructibleWall) || destructibleWall.Destroyed) { continue; } if (wall is not DestructibleLevelWall destructibleWall || destructibleWall.Destroyed) { continue; }
foreach (var cell in wall.Cells) foreach (var cell in wall.Cells)
{ {
if (cell.DoesDamage) if (cell.DoesDamage)
@@ -1405,19 +1409,18 @@ namespace Barotrauma.Items.Components
Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value); Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value);
// Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target. // Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target.
Body worldTarget = CheckLineOfSight(start, end); Body worldTarget = CheckLineOfSight(start, end);
bool shoot;
if (closestEnemy != null && closestEnemy.Submarine != null) if (closestEnemy != null && closestEnemy.Submarine != null)
{ {
start -= closestEnemy.Submarine.SimPosition; start -= closestEnemy.Submarine.SimPosition;
end -= closestEnemy.Submarine.SimPosition; end -= closestEnemy.Submarine.SimPosition;
Body transformedTarget = CheckLineOfSight(start, end); Body transformedTarget = CheckLineOfSight(start, end);
shoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character)); canShoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character));
} }
else else
{ {
shoot = CanShoot(worldTarget, character); canShoot = CanShoot(worldTarget, character);
} }
if (!shoot) { return false; } if (!canShoot) { return false; }
if (character.IsOnPlayerTeam) if (character.IsOnPlayerTeam)
{ {
character.Speak(TextManager.Get("DialogFireTurret").Value, character.Speak(TextManager.Get("DialogFireTurret").Value,
@@ -1471,6 +1474,7 @@ namespace Barotrauma.Items.Components
{ {
if (targetBody.UserData is ISpatialEntity e) if (targetBody.UserData is ISpatialEntity e)
{ {
if (e is Structure s && s.Indestructible) { return false; }
Submarine sub = e.Submarine ?? e as Submarine; Submarine sub = e.Submarine ?? e as Submarine;
if (!targetSubmarines && e is Submarine) { return false; } if (!targetSubmarines && e is Submarine) { return false; }
if (sub == null) { return false; } if (sub == null) { return false; }
@@ -1559,7 +1563,7 @@ namespace Barotrauma.Items.Components
return projectiles; return projectiles;
} }
private void CheckProjectileContainer(Item projectileContainer, List<Projectile> projectiles, out bool stopSearching) private static void CheckProjectileContainer(Item projectileContainer, List<Projectile> projectiles, out bool stopSearching)
{ {
stopSearching = false; stopSearching = false;
if (projectileContainer.Condition <= 0.0f) { return; } if (projectileContainer.Condition <= 0.0f) { return; }
@@ -866,6 +866,8 @@ namespace Barotrauma
} }
} }
public Action<Character> OnDeselect;
public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true) public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true)
: this(new Rectangle( : this(new Rectangle(
(int)(position.X - itemPrefab.Sprite.size.X / 2 * itemPrefab.Scale), (int)(position.X - itemPrefab.Sprite.size.X / 2 * itemPrefab.Scale),
@@ -2155,12 +2157,15 @@ namespace Barotrauma
if (projectile.ShouldIgnoreSubmarineCollision(f2, contact)) { return false; } if (projectile.ShouldIgnoreSubmarineCollision(f2, contact)) { return false; }
} }
contact.GetWorldManifold(out Vector2 normal, out _); if (GameMain.GameSession == null || GameMain.GameSession.RoundDuration > 1.0f)
if (contact.FixtureA.Body == f1.Body) { normal = -normal; } {
float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal); contact.GetWorldManifold(out Vector2 normal, out _);
if (contact.FixtureA.Body == f1.Body) { normal = -normal; }
float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal);
impactQueue ??= new ConcurrentQueue<float>();
impactQueue.Enqueue(impact);
}
impactQueue ??= new ConcurrentQueue<float>();
impactQueue.Enqueue(impact);
isActive = true; isActive = true;
return true; return true;
@@ -2774,6 +2779,7 @@ namespace Barotrauma
if (GameMain.NetworkMember is { IsServer: true }) if (GameMain.NetworkMember is { IsServer: true })
{ {
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(conditionalActionType, ic, character, targetLimb)); GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(conditionalActionType, ic, character, targetLimb));
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnUse, ic, character, targetLimb));
} }
if (ic.DeleteOnUse) { remove = true; } if (ic.DeleteOnUse) { remove = true; }
@@ -1156,7 +1156,7 @@ namespace Barotrauma
public bool CanBeBoughtFrom(Location.StoreInfo store, out PriceInfo priceInfo) public bool CanBeBoughtFrom(Location.StoreInfo store, out PriceInfo priceInfo)
{ {
priceInfo = GetPriceInfo(store); priceInfo = GetPriceInfo(store);
return priceInfo is { CanBeBought: true } && (store.Location?.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty; return priceInfo is { CanBeBought: true } && (store?.Location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty;
} }
public bool CanBeBoughtFrom(Location location) public bool CanBeBoughtFrom(Location location)
@@ -1167,7 +1167,7 @@ namespace Barotrauma
var priceInfo = GetPriceInfo(store.Value); var priceInfo = GetPriceInfo(store.Value);
if (priceInfo == null) { continue; } if (priceInfo == null) { continue; }
if (!priceInfo.CanBeBought) { continue; } if (!priceInfo.CanBeBought) { continue; }
if ((location.LevelData?.Difficulty ?? 0) < priceInfo.MinLevelDifficulty) { continue; } if (location.LevelData.Difficulty < priceInfo.MinLevelDifficulty) { continue; }
return true; return true;
} }
return false; return false;
@@ -27,13 +27,14 @@ namespace Barotrauma
private readonly bool applyFireEffects; private readonly bool applyFireEffects;
private readonly string[] ignoreFireEffectsForTags; private readonly string[] ignoreFireEffectsForTags;
private readonly bool ignoreCover; private readonly bool ignoreCover;
private readonly bool onlyInside,onlyOutside;
private readonly float flashDuration; private readonly float flashDuration;
private readonly float? flashRange; private readonly float? flashRange;
private readonly string decal; private readonly string decal;
private readonly float decalSize; private readonly float decalSize;
private readonly bool applyToSelf; private readonly bool applyToSelf;
public bool OnlyInside, OnlyOutside;
private readonly float itemRepairStrength; private readonly float itemRepairStrength;
public readonly HashSet<Submarine> IgnoredSubmarines = new HashSet<Submarine>(); public readonly HashSet<Submarine> IgnoredSubmarines = new HashSet<Submarine>();
@@ -81,8 +82,8 @@ namespace Barotrauma
ignoreFireEffectsForTags = element.GetAttributeStringArray("ignorefireeffectsfortags", Array.Empty<string>(), convertToLowerInvariant: true); ignoreFireEffectsForTags = element.GetAttributeStringArray("ignorefireeffectsfortags", Array.Empty<string>(), convertToLowerInvariant: true);
ignoreCover = element.GetAttributeBool("ignorecover", false); ignoreCover = element.GetAttributeBool("ignorecover", false);
onlyInside = element.GetAttributeBool("onlyinside", false); OnlyInside = element.GetAttributeBool("onlyinside", false);
onlyOutside = element.GetAttributeBool("onlyoutside", false); OnlyOutside = element.GetAttributeBool("onlyoutside", false);
flash = element.GetAttributeBool("flash", showEffects); flash = element.GetAttributeBool("flash", showEffects);
flashDuration = element.GetAttributeFloat("flashduration", 0.05f); flashDuration = element.GetAttributeFloat("flashduration", 0.05f);
@@ -176,13 +177,12 @@ namespace Barotrauma
foreach (Item item in Item.ItemList) foreach (Item item in Item.ItemList)
{ {
float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition); float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition);
if (distSqr > displayRangeSqr) continue; if (distSqr > displayRangeSqr) { continue; }
float distFactor = CalculateDistanceFactor(distSqr, displayRange);
float distFactor = 1.0f - (float)Math.Sqrt(distSqr) / displayRange;
//damage repairable power-consuming items //damage repairable power-consuming items
var powered = item.GetComponent<Powered>(); var powered = item.GetComponent<Powered>();
if (powered == null || !powered.VulnerableToEMP) continue; if (powered == null || !powered.VulnerableToEMP) { continue; }
if (item.Repairables.Any()) if (item.Repairables.Any())
{ {
item.Condition -= item.MaxCondition * EmpStrength * distFactor; item.Condition -= item.MaxCondition * EmpStrength * distFactor;
@@ -195,6 +195,7 @@ namespace Barotrauma
powerContainer.Charge -= powerContainer.GetCapacity() * EmpStrength * distFactor; powerContainer.Charge -= powerContainer.GetCapacity() * EmpStrength * distFactor;
} }
} }
static float CalculateDistanceFactor(float distSqr, float displayRange) => 1.0f - (float)Math.Sqrt(distSqr) / displayRange;
} }
if (itemRepairStrength > 0.0f) if (itemRepairStrength > 0.0f)
@@ -288,10 +289,16 @@ namespace Barotrauma
{ {
continue; continue;
} }
if (c == attacker && !applyToSelf) { continue; } //if (c == attacker && !applyToSelf) { continue; }
if (onlyInside && c.Submarine == null) { continue; } if (OnlyInside && c.Submarine == null)
else if (onlyOutside && c.Submarine != null) { continue; } {
continue;
}
else if (OnlyOutside && c.Submarine != null)
{
continue;
}
Vector2 explosionPos = worldPosition; Vector2 explosionPos = worldPosition;
if (c.Submarine != null) { explosionPos -= c.Submarine.Position; } if (c.Submarine != null) { explosionPos -= c.Submarine.Position; }
@@ -337,15 +344,19 @@ namespace Barotrauma
modifiedAfflictions.Clear(); modifiedAfflictions.Clear();
foreach (Affliction affliction in attack.Afflictions.Keys) foreach (Affliction affliction in attack.Afflictions.Keys)
{ {
// Shouldn't go above 15, or the damage can be unexpectedly low -> doesn't break armor
// Effectively this makes large explosions more effective against large creatures (because more limbs are affected), but I don't think that's necessarily a bad thing.
float limbCountFactor = Math.Min(distFactors.Count, 15);
float dmgMultiplier = distFactor; float dmgMultiplier = distFactor;
if (affliction.DivideByLimbCount) if (affliction.DivideByLimbCount)
{ {
float limbCountFactor = distFactors.Count;
if (affliction.Prefab.LimbSpecific && affliction.Prefab.AfflictionType == "damage")
{
// Shouldn't go above 15, or the damage can be unexpectedly low -> doesn't break armor
// Effectively this makes large explosions more effective against large creatures (because more limbs are affected), but I don't think that's necessarily a bad thing.
limbCountFactor = Math.Min(distFactors.Count, 15);
}
dmgMultiplier /= limbCountFactor; dmgMultiplier /= limbCountFactor;
} }
modifiedAfflictions.Add(affliction.CreateMultiplied(dmgMultiplier, affliction.Probability)); modifiedAfflictions.Add(affliction.CreateMultiplied(dmgMultiplier, affliction));
} }
c.LastDamageSource = damageSource; c.LastDamageSource = damageSource;
if (attacker == null) if (attacker == null)
@@ -353,27 +364,27 @@ namespace Barotrauma
if (damageSource is Item item) if (damageSource is Item item)
{ {
attacker = item.GetComponent<Projectile>()?.User; attacker = item.GetComponent<Projectile>()?.User;
if (attacker == null) attacker ??= item.GetComponent<MeleeWeapon>()?.User;
{
attacker = item.GetComponent<MeleeWeapon>()?.User;
}
} }
} }
if (attack.Afflictions.Any() || attack.Stun > 0.0f) if (attack.Afflictions.Any() || attack.Stun > 0.0f)
{ {
AbilityAttackData attackData = new AbilityAttackData(Attack, c, attacker); if (!attack.OnlyHumans || c.IsHuman)
if (attackData.Afflictions != null)
{ {
modifiedAfflictions.AddRange(attackData.Afflictions); AbilityAttackData attackData = new AbilityAttackData(Attack, c, attacker);
} if (attackData.Afflictions != null)
{
modifiedAfflictions.AddRange(attackData.Afflictions);
}
//use a position slightly from the limb's position towards the explosion //use a position slightly from the limb's position towards the explosion
//ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods //ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods
Vector2 dir = worldPosition - limb.WorldPosition; Vector2 dir = worldPosition - limb.WorldPosition;
Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f; Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f;
AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier); AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier);
damages.Add(limb, attackResult.Damage); damages.Add(limb, attackResult.Damage);
}
} }
if (attack.StatusEffects != null && attack.StatusEffects.Any()) if (attack.StatusEffects != null && attack.StatusEffects.Any())
@@ -438,7 +449,7 @@ namespace Barotrauma
damagedStructureList.Clear(); damagedStructureList.Clear();
foreach (MapEntity entity in MapEntity.mapEntityList) foreach (MapEntity entity in MapEntity.mapEntityList)
{ {
if (!(entity is Structure structure)) { continue; } if (entity is not Structure structure) { continue; }
if (ignoredSubmarines != null && entity.Submarine != null && ignoredSubmarines.Contains(entity.Submarine)) { continue; } if (ignoredSubmarines != null && entity.Submarine != null && ignoredSubmarines.Contains(entity.Submarine)) { continue; }
if (structure.HasBody && if (structure.HasBody &&
@@ -487,7 +498,7 @@ namespace Barotrauma
for (int i = Level.Loaded.ExtraWalls.Count - 1; i >= 0; i--) for (int i = Level.Loaded.ExtraWalls.Count - 1; i >= 0; i--)
{ {
if (!(Level.Loaded.ExtraWalls[i] is DestructibleLevelWall destructibleWall)) { continue; } if (Level.Loaded.ExtraWalls[i] is not DestructibleLevelWall destructibleWall) { continue; }
foreach (var cell in destructibleWall.Cells) foreach (var cell in destructibleWall.Cells)
{ {
if (cell.IsPointInside(worldPosition)) if (cell.IsPointInside(worldPosition))
@@ -510,7 +521,7 @@ namespace Barotrauma
return damagedStructures; return damagedStructures;
} }
public void RangedBallastFloraDamage(Vector2 worldPosition, float worldRange, float damage, Character attacker = null) public static void RangedBallastFloraDamage(Vector2 worldPosition, float worldRange, float damage, Character attacker = null)
{ {
List<BallastFloraBehavior> ballastFlorae = new List<BallastFloraBehavior>(); List<BallastFloraBehavior> ballastFlorae = new List<BallastFloraBehavior>();
@@ -549,21 +549,24 @@ namespace Barotrauma
if (hull1.WaterVolume < hull1.Volume / Hull.MaxCompress && if (hull1.WaterVolume < hull1.Volume / Hull.MaxCompress &&
hull1.Surface < rect.Y) hull1.Surface < rect.Y)
{ {
//create a wave from the side of the hull the water is leaking from
if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f) if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f)
{ {
float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1])) * 6.0f; CreateWave(rect, hull1, hull1.WaveY.Length - 1, hull1.WaveY.Length - 2, flowForce, deltaTime);
vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f);
hull1.WaveVel[hull1.WaveY.Length - 1] += vel * deltaTime;
hull1.WaveVel[hull1.WaveY.Length - 2] += vel * deltaTime;
} }
else else
{ {
float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[0])) * 6.0f; CreateWave(rect, hull1, 0, 1, flowForce, deltaTime);
}
static void CreateWave(Rectangle rect, Hull hull1, int index1, int index2, Vector2 flowForce, float deltaTime)
{
float vel = (rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[index1]);
vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f); vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f);
if (vel > 0.0f)
hull1.WaveVel[0] += vel * deltaTime; {
hull1.WaveVel[1] += vel * deltaTime; hull1.WaveVel[index1] += vel * deltaTime;
hull1.WaveVel[index2] += vel * deltaTime;
}
} }
} }
else else
@@ -405,7 +405,7 @@ namespace Barotrauma
if (wall.Submarine != sub) { continue; } if (wall.Submarine != sub) { continue; }
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);
} }
} }
foreach (Hull hull in Hull.HullList) foreach (Hull hull in Hull.HullList)
@@ -318,7 +318,7 @@ namespace Barotrauma
var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
if (characters.Any()) if (characters.Any())
{ {
price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier)); price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier, includeSaved: false));
price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, tag))); price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, tag)));
} }
@@ -659,7 +659,7 @@ namespace Barotrauma
return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations); return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations);
} }
public void ChangeType(LocationType newType) public void ChangeType(LocationType newType, bool createStores = true)
{ {
if (newType == Type) { return; } if (newType == Type) { return; }
@@ -683,7 +683,10 @@ namespace Barotrauma
UnlockMissionByTag(Type.MissionTags.GetRandomUnsynced()); UnlockMissionByTag(Type.MissionTags.GetRandomUnsynced());
} }
CreateStores(force: true); if (createStores)
{
CreateStores(force: true);
}
} }
public void UnlockInitialMissions() public void UnlockInitialMissions()
@@ -552,7 +552,8 @@ namespace Barotrauma
Connections[i].Locations[1]; Connections[i].Locations[1];
if (!leftMostLocation.Type.HasOutpost || leftMostLocation.Type.Identifier == "abandoned") if (!leftMostLocation.Type.HasOutpost || leftMostLocation.Type.Identifier == "abandoned")
{ {
leftMostLocation.ChangeType(LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned")); leftMostLocation.ChangeType(LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned"),
createStores: false);
} }
leftMostLocation.IsGateBetweenBiomes = true; leftMostLocation.IsGateBetweenBiomes = true;
Connections[i].Locked = true; Connections[i].Locked = true;
@@ -628,6 +629,7 @@ namespace Barotrauma
foreach (Location location in Locations) foreach (Location location in Locations)
{ {
location.LevelData = new LevelData(location, CalculateDifficulty(location.MapPosition.X, location.Biome)); location.LevelData = new LevelData(location, CalculateDifficulty(location.MapPosition.X, location.Biome));
location.CreateStores(force: true);
} }
foreach (LocationConnection connection in Connections) foreach (LocationConnection connection in Connections)
{ {
@@ -734,7 +736,7 @@ namespace Barotrauma
if (LocationType.Prefabs.TryGet("none", out LocationType locationType)) if (LocationType.Prefabs.TryGet("none", out LocationType locationType))
{ {
previousToEndLocation.ChangeType(locationType); previousToEndLocation.ChangeType(locationType, createStores: false);
} }
//remove all locations from the end biome except the end location //remove all locations from the end biome except the end location
@@ -56,6 +56,8 @@ namespace Barotrauma
//dimensions of the wall sections' physics bodies (only used for debug rendering) //dimensions of the wall sections' physics bodies (only used for debug rendering)
private readonly List<Vector2> bodyDebugDimensions = new List<Vector2>(); private readonly List<Vector2> bodyDebugDimensions = new List<Vector2>();
private static Explosion explosionOnBroken;
#if DEBUG #if DEBUG
[Serialize(false, IsPropertySaveable.Yes), Editable] [Serialize(false, IsPropertySaveable.Yes), Editable]
#else #else
@@ -1083,7 +1085,7 @@ namespace Barotrauma
return new AttackResult(damageAmount, null); return new AttackResult(damageAmount, null);
} }
public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true) public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true, bool createExplosionEffect = true)
{ {
if (Submarine != null && Submarine.GodMode || Indestructible) { return; } if (Submarine != null && Submarine.GodMode || Indestructible) { return; }
if (!Prefab.Body) { return; } if (!Prefab.Body) { return; }
@@ -1128,6 +1130,7 @@ namespace Barotrauma
} }
else else
{ {
float prevGapOpenState = Sections[sectionIndex].gap?.Open ?? 0.0f;
if (Sections[sectionIndex].gap == null) if (Sections[sectionIndex].gap == null)
{ {
Rectangle gapRect = Sections[sectionIndex].rect; Rectangle gapRect = Sections[sectionIndex].rect;
@@ -1204,8 +1207,15 @@ namespace Barotrauma
#endif #endif
} }
var gap = Sections[sectionIndex].gap;
float gapOpen = MaxHealth <= 0.0f ? 0.0f : (damage / MaxHealth - LeakThreshold) * (1.0f / (1.0f - LeakThreshold)); float gapOpen = MaxHealth <= 0.0f ? 0.0f : (damage / MaxHealth - LeakThreshold) * (1.0f / (1.0f - LeakThreshold));
Sections[sectionIndex].gap.Open = gapOpen; gap.Open = gapOpen;
//gap appeared or became much larger -> explosion effect
if (gapOpen - prevGapOpenState > 0.25f && createExplosionEffect && !gap.IsRoomToRoom)
{
CreateWallDamageExplosion(gap, attacker);
}
} }
float damageDiff = damage - Sections[sectionIndex].damage; float damageDiff = damage - Sections[sectionIndex].damage;
@@ -1234,6 +1244,59 @@ namespace Barotrauma
UpdateSections(); UpdateSections();
} }
private void CreateWallDamageExplosion(Gap gap, Character attacker)
{
const float explosionRange = 750.0f;
float explosionStrength = gap.Open;
var linkedHull = gap.linkedTo.FirstOrDefault() as Hull;
if (linkedHull != null)
{
//existing, nearby gaps leading to the same hull reduce the strength of the explosion
// -> the first breached section does most (or all) of the damage, making it more consistent
// (otherwise the damage would depend on how many structures and sections happen to be breached)
foreach (var otherGap in linkedHull.ConnectedGaps)
{
if (otherGap == gap || otherGap.IsRoomToRoom || otherGap.Open < 0.25f) { continue; }
explosionStrength -= Math.Max(0, explosionRange - Vector2.Distance(otherGap.WorldPosition, gap.WorldPosition)) / explosionRange;
if (explosionStrength <= 0.0f) { return; }
}
}
if (explosionOnBroken == null)
{
explosionOnBroken = new Explosion(explosionRange * gap.Open, force: 10.0f, damage: 0.0f, structureDamage: 0.0f, itemDamage: 0.0f);
if (AfflictionPrefab.Prefabs.TryGet("lacerations".ToIdentifier(), out AfflictionPrefab lacerations))
{
explosionOnBroken.Attack.Afflictions.Add(lacerations.Instantiate(50.0f), null);
}
else
{
explosionOnBroken.Attack.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(5.0f), null);
}
explosionOnBroken.OnlyInside = true;
explosionOnBroken.DisableParticles();
}
explosionOnBroken.Attack.DamageMultiplier = explosionStrength;
explosionOnBroken?.Explode(gap.WorldPosition, damageSource: null, attacker: attacker);
#if CLIENT
if (linkedHull != null)
{
for (int i = 0; i <= 50; i++)
{
Vector2 particlePos = new Vector2(Rand.Range(gap.WorldRect.X, gap.WorldRect.Right), Rand.Range(gap.WorldRect.Y - gap.WorldRect.Height, gap.WorldRect.Y));
var velocity = gap.IsHorizontal ?
gap.linkedTo[0].WorldPosition.X < gap.WorldPosition.X ? -Vector2.UnitX : Vector2.UnitX :
gap.linkedTo[0].WorldPosition.Y < gap.WorldPosition.Y ? -Vector2.UnitY : Vector2.UnitY;
velocity = new Vector2(velocity.X + Rand.Range(-0.2f, 0.2f), velocity.Y + Rand.Range(-0.2f, 0.2f));
var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, velocity * Rand.Range(100.0f, 3000.0f), collisionIgnoreTimer: 0.1f);
if (particle == null) { break; }
}
}
#endif
}
partial void OnHealthChangedProjSpecific(Character attacker, float damageAmount); partial void OnHealthChangedProjSpecific(Character attacker, float damageAmount);
public void SetCollisionCategory(Category collisionCategory) public void SetCollisionCategory(Category collisionCategory)
@@ -1570,7 +1633,7 @@ namespace Barotrauma
{ {
for (int i = 0; i < Sections.Length; i++) for (int i = 0; i < Sections.Length; i++)
{ {
SetDamage(i, Sections[i].damage, createNetworkEvent: false); SetDamage(i, Sections[i].damage, createNetworkEvent: false, createExplosionEffect: false);
} }
} }
@@ -720,7 +720,7 @@ namespace Barotrauma
private void HandleLevelCollision(Impact impact, VoronoiCell cell = null) private void HandleLevelCollision(Impact impact, VoronoiCell cell = null)
{ {
if (GameMain.GameSession != null && Timing.TotalTime < GameMain.GameSession.RoundStartTime + 10) if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 10)
{ {
//ignore level collisions for the first 10 seconds of the round in case the sub spawns in a way that causes it to hit a wall //ignore level collisions for the first 10 seconds of the round in case the sub spawns in a way that causes it to hit a wall
//(e.g. level without outposts to dock to and an incorrectly configured ballast that makes the sub go up) //(e.g. level without outposts to dock to and an incorrectly configured ballast that makes the sub go up)
@@ -38,15 +38,15 @@ namespace Barotrauma.Networking
READY_CHECK, READY_CHECK,
READY_TO_SPAWN READY_TO_SPAWN
} }
enum ClientNetObject
enum ClientNetSegment
{ {
END_OF_MESSAGE, //self-explanatory SyncIds, //ids of the last changes the client knows about
SYNC_IDS, //ids of the last changes the client knows about ChatMessage, //also self-explanatory
CHAT_MESSAGE, //also self-explanatory Vote, //you get the idea
VOTE, //you get the idea CharacterInput,
CHARACTER_INPUT, EntityState,
ENTITY_STATE, SpectatingPos
SPECTATING_POS
} }
enum ClientNetError enum ClientNetError
@@ -88,16 +88,15 @@ namespace Barotrauma.Networking
MONEY, MONEY,
READY_CHECK //start, end and update a ready check READY_CHECK //start, end and update a ready check
} }
enum ServerNetObject enum ServerNetSegment
{ {
END_OF_MESSAGE, SyncIds,
SYNC_IDS, ChatMessage,
CHAT_MESSAGE, Vote,
VOTE, ClientList,
CLIENT_LIST, EntityPosition,
ENTITY_POSITION, EntityEvent,
ENTITY_EVENT, EntityEventInitial
ENTITY_EVENT_INITIAL
} }
enum TraitorMessageType enum TraitorMessageType
@@ -272,6 +272,9 @@ namespace Barotrauma.Networking
[NetworkSerialize] [NetworkSerialize]
public bool IsMandatory; public bool IsMandatory;
[NetworkSerialize]
public bool IsVanilla;
private Md5Hash? cachedHash; private Md5Hash? cachedHash;
private DateTime? cachedDateTime; private DateTime? cachedDateTime;
@@ -305,6 +308,7 @@ namespace Barotrauma.Networking
? ugcId.StringRepresentation ? ugcId.StringRepresentation
: ""; : "";
IsMandatory = !contentPackage.Files.All(f => f is SubmarineFile); IsMandatory = !contentPackage.Files.All(f => f is SubmarineFile);
IsVanilla = contentPackage == ContentPackageManager.VanillaCorePackage;
InstallTimeDiffInSeconds = InstallTimeDiffInSeconds =
contentPackage.InstallTime.TryUnwrap(out var installTime) contentPackage.InstallTime.TryUnwrap(out var installTime)
? (uint)(installTime - referenceTime).TotalSeconds ? (uint)(installTime - referenceTime).TotalSeconds
@@ -65,7 +65,7 @@ namespace Barotrauma.Networking
public State CurrentState { get; private set; } public State CurrentState { get; private set; }
public bool UseRespawnPrompt public static bool UseRespawnPrompt
{ {
get get
{ {
@@ -191,6 +191,7 @@ namespace Barotrauma.Networking
public void ForceRespawn() public void ForceRespawn()
{ {
ResetShuttle(); ResetShuttle();
RespawnCountdownStarted = true;
RespawnTime = DateTime.Now; RespawnTime = DateTime.Now;
CurrentState = State.Waiting; CurrentState = State.Waiting;
} }
@@ -246,11 +246,13 @@ namespace Barotrauma
{ {
public readonly Identifier SkillIdentifier; public readonly Identifier SkillIdentifier;
public readonly float Amount; public readonly float Amount;
public readonly bool TriggerTalents;
public GiveSkill(XElement element, string parentDebugName) public GiveSkill(XElement element, string parentDebugName)
{ {
SkillIdentifier = element.GetAttributeIdentifier("skillidentifier", Identifier.Empty); SkillIdentifier = element.GetAttributeIdentifier(nameof(SkillIdentifier), Identifier.Empty);
Amount = element.GetAttributeFloat("amount", 0); Amount = element.GetAttributeFloat(nameof(Amount), 0);
TriggerTalents = element.GetAttributeBool(nameof(TriggerTalents), true);
if (SkillIdentifier == Identifier.Empty) if (SkillIdentifier == Identifier.Empty)
{ {
@@ -424,7 +426,7 @@ namespace Barotrauma
private set; private set;
} }
private readonly bool multiplyAfflictionsByMaxVitality; private readonly bool? multiplyAfflictionsByMaxVitality;
public IEnumerable<CharacterSpawnInfo> SpawnCharacters public IEnumerable<CharacterSpawnInfo> SpawnCharacters
{ {
@@ -490,7 +492,11 @@ namespace Barotrauma
talentTriggers = new List<Identifier>(); talentTriggers = new List<Identifier>();
giveExperiences = new List<int>(); giveExperiences = new List<int>();
giveSkills = new List<GiveSkill>(); giveSkills = new List<GiveSkill>();
multiplyAfflictionsByMaxVitality = element.GetAttributeBool("multiplyafflictionsbymaxvitality", false); var multiplyAfflictionsElement = element.GetAttribute(nameof(multiplyAfflictionsByMaxVitality));
if (multiplyAfflictionsElement != null)
{
multiplyAfflictionsByMaxVitality = multiplyAfflictionsElement.GetAttributeBool(false);
}
tags = new HashSet<string>(element.GetAttributeString("tags", "").Split(',')); tags = new HashSet<string>(element.GetAttributeString("tags", "").Split(','));
OnlyInside = element.GetAttributeBool("onlyinside", false); OnlyInside = element.GetAttributeBool("onlyinside", false);
@@ -1294,7 +1300,7 @@ namespace Barotrauma
} }
for (int i = 0; i < targets.Count; i++) for (int i = 0; i < targets.Count; i++)
{ {
if (!(targets[i] is Item item)) { continue; } if (targets[i] is not Item item) { continue; }
for (int j = 0; j < useItemCount; j++) for (int j = 0; j < useItemCount; j++)
{ {
if (item.Removed) { continue; } if (item.Removed) { continue; }
@@ -1327,6 +1333,13 @@ namespace Barotrauma
} }
} }
} }
else if (targets[i] is Character character && character.Inventory != null)
{
foreach (var containedItem in character.Inventory.AllItemsMod)
{
containedItem.Drop(dropper: null);
}
}
} }
} }
if (removeItem) if (removeItem)
@@ -1566,9 +1579,7 @@ namespace Barotrauma
if (targetCharacter != null && !targetCharacter.Removed) if (targetCharacter != null && !targetCharacter.Removed)
{ {
Identifier skillIdentifier = giveSkill.SkillIdentifier == "randomskill" ? GetRandomSkill() : giveSkill.SkillIdentifier; Identifier skillIdentifier = giveSkill.SkillIdentifier == "randomskill" ? GetRandomSkill() : giveSkill.SkillIdentifier;
targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, giveSkill.Amount, !giveSkill.TriggerTalents);
targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, giveSkill.Amount);
Identifier GetRandomSkill() Identifier GetRandomSkill()
{ {
return targetCharacter.Info?.Job?.GetSkills().GetRandomUnsynced()?.Identifier ?? Identifier.Empty; return targetCharacter.Info?.Job?.GetSkills().GetRandomUnsynced()?.Identifier ?? Identifier.Empty;
@@ -2140,10 +2151,10 @@ namespace Barotrauma
return multiplier * AfflictionMultiplier; return multiplier * AfflictionMultiplier;
} }
private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool modifyByMaxVitality) private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool? multiplyByMaxVitality)
{ {
float afflictionMultiplier = GetAfflictionMultiplier(entity, targetCharacter, deltaTime); float afflictionMultiplier = GetAfflictionMultiplier(entity, targetCharacter, deltaTime);
if (modifyByMaxVitality) if (multiplyByMaxVitality ?? affliction.MultiplyByMaxVitality)
{ {
afflictionMultiplier *= targetCharacter.MaxVitality / 100f; afflictionMultiplier *= targetCharacter.MaxVitality / 100f;
} }
@@ -2162,7 +2173,7 @@ namespace Barotrauma
if (!MathUtils.NearlyEqual(afflictionMultiplier, 1.0f)) if (!MathUtils.NearlyEqual(afflictionMultiplier, 1.0f))
{ {
return affliction.CreateMultiplied(afflictionMultiplier, affliction.Probability); return affliction.CreateMultiplied(afflictionMultiplier, affliction);
} }
return affliction; return affliction;
} }
@@ -89,7 +89,7 @@ namespace Barotrauma.Steam
{ {
if (!IsInitialized || !Steamworks.SteamClient.IsValid) if (!IsInitialized || !Steamworks.SteamClient.IsValid)
{ {
return new PublishedFileId[0]; return Array.Empty<PublishedFileId>();
} }
return Steamworks.SteamUGC.GetSubscribedItems(); return Steamworks.SteamUGC.GetSubscribedItems();
} }
@@ -61,6 +61,12 @@ namespace Barotrauma.Steam
if (set.Count == prevSize) { break; } if (set.Count == prevSize) { break; }
prevSize = set.Count; prevSize = set.Count;
} }
// Remove items that do not have the correct consumer app ID,
// which can happen on items that are not visible to the currently
// logged in player (i.e. private & friends-only items)
set.RemoveWhere(it => it.ConsumerApp != AppID);
return set; return set;
} }
@@ -277,6 +283,62 @@ namespace Barotrauma.Steam
} }
} }
public static ISet<ulong> GetInstalledItems()
=> ContentPackageManager.WorkshopPackages
.Select(p => p.UgcId)
.NotNone()
.OfType<SteamWorkshopId>()
.Select(id => id.Value)
.ToHashSet();
public static async Task<ISet<Steamworks.Ugc.Item>> GetPublishedAndSubscribedItems()
{
var allItems = (await GetAllSubscribedItems()).ToHashSet();
allItems.UnionWith(await GetPublishedItems());
// This is a hack that eliminates subscribed mods that have been
// made private. Players cannot download updates for these, so
// we treat them as if they were deleted.
allItems = (await Task.WhenAll(allItems.Select(it => GetItem(it.Id.Value))))
.NotNull()
.Where(it => it.ConsumerApp == AppID)
.ToHashSet();
return allItems;
}
public static void DeleteUnsubscribedMods(Action<ContentPackage[]>? callback = null)
{
#if SERVER
// Servers do not run this because they can't subscribe to anything
return;
#endif
//If Steamworks isn't initialized then we can't know what the user has unsubscribed from
if (!IsInitialized) { return; }
if (!Steamworks.SteamClient.IsValid) { return; }
if (!Steamworks.SteamClient.IsLoggedOn) { return; }
TaskPool.Add("DeleteUnsubscribedMods", GetPublishedAndSubscribedItems().WaitForLoadingScreen(), t =>
{
if (!t.TryGetResult(out ISet<Steamworks.Ugc.Item> items)) { return; }
var ids = items.Select(it => it.Id.Value).ToHashSet();
var toUninstall = ContentPackageManager.WorkshopPackages
.Where(pkg
=> !pkg.UgcId.TryUnwrap(out SteamWorkshopId workshopId)
|| !ids.Contains(workshopId.Value))
.ToArray();
if (toUninstall.Any())
{
foreach (var pkg in toUninstall)
{
Directory.TryDelete(pkg.Dir, recursive: true);
}
ContentPackageManager.UpdateContentPackageList();
}
callback?.Invoke(toUninstall);
});
}
public static bool IsInstallingToPath(string path) public static bool IsInstallingToPath(string path)
=> File.Exists(Path.Combine(Path.GetDirectoryName(path)!, ContentPackageManager.CopyIndicatorFileName)); => File.Exists(Path.Combine(Path.GetDirectoryName(path)!, ContentPackageManager.CopyIndicatorFileName));
@@ -457,7 +519,7 @@ namespace Barotrauma.Steam
} }
catch (Exception e) catch (Exception e)
{ {
DebugConsole.ThrowError( DebugConsole.AddWarning(
$"An exception was thrown when attempting to copy \"{from}\" to \"{to}\": {e.Message}\n{e.StackTrace}"); $"An exception was thrown when attempting to copy \"{from}\" to \"{to}\": {e.Message}\n{e.StackTrace}");
} }
} }

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