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;
}
public void ClientWrite(IWriteMessage msg)
public void ClientWrite(in SegmentTableWriter<ClientNetSegment> segmentTableWriter, IWriteMessage msg)
{
if (Character.Controlled != null && !Character.Controlled.IsDead) { return; }
msg.WriteByte((byte)ClientNetObject.SPECTATING_POS);
segmentTableWriter.StartNewSegment(ClientNetSegment.SpectatingPos);
msg.WriteSingle(position.X);
msg.WriteSingle(position.Y);
}
@@ -653,15 +653,6 @@ namespace Barotrauma
partial void UpdateProjSpecific(float deltaTime, Camera cam)
{
if (InvisibleTimer > 0.0f)
{
if (Controlled == null || Controlled == this || (Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f)
{
InvisibleTimer = Math.Min(InvisibleTimer, 1.0f);
}
InvisibleTimer -= deltaTime;
}
foreach (GUIMessage message in guiMessages)
{
bool wasPending = message.Timer < 0.0f;
@@ -101,6 +101,7 @@ namespace Barotrauma
public static bool ShouldRecreateHudTexts { get; set; } = true;
private static bool heldDownShiftWhenGotHudTexts;
private static float timeHealthWindowClosed;
public static bool IsCampaignInterfaceOpen =>
GameMain.GameSession?.Campaign != null &&
@@ -174,7 +175,8 @@ namespace Barotrauma
if (character.Info != null && !character.ShouldLockHud() && character.SelectedCharacter == null && Screen.Selected != GameMain.SubEditorScreen)
{
bool mouseOnPortrait = MouseOnCharacterPortrait() && GUI.MouseOn == null;
if (mouseOnPortrait && PlayerInput.PrimaryMouseButtonClicked() && Inventory.DraggingItems.None())
bool healthWindowOpen = CharacterHealth.OpenHealthWindow != null || timeHealthWindowClosed < 0.2f;
if (mouseOnPortrait && !healthWindowOpen && PlayerInput.PrimaryMouseButtonClicked() && Inventory.DraggingItems.None())
{
CharacterHealth.OpenHealthWindow = character.CharacterHealth;
}
@@ -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)
@@ -491,7 +502,17 @@ namespace Barotrauma
{
var item = character.Inventory.GetItemAt(i);
if (item == null || character.Inventory.SlotTypes[i] == InvSlotType.Any) { continue; }
//if the item is also equipped in another slot we already went through, don't draw the hud again
bool duplicateFound = false;
for (int j = 0; j < i; j++)
{
if (character.Inventory.SlotTypes[j] != InvSlotType.Any && character.Inventory.GetItemAt(j) == item)
{
duplicateFound = true;
break;
}
}
if (duplicateFound) { continue; }
foreach (ItemComponent ic in item.Components)
{
if (ic.DrawHudWhenEquipped) { ic.DrawHUD(spriteBatch, character); }
@@ -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)
{
@@ -156,7 +156,7 @@ namespace Barotrauma
Character.Controlled.DeselectCharacter();
}
Character.Controlled.ResetInteract = true;
Character.Controlled.DisableInteract = true;
if (openHealthWindow != null)
{
if (value.Character.Info == null || value.Character == Character.Controlled || Character.Controlled.HasEquippedItem("healthscanner".ToIdentifier()))
@@ -51,11 +51,10 @@ namespace Barotrauma
&& p.InstallTime.TryUnwrap(out var installTime)
&& item.LatestUpdateTime <= installTime))
.ToArray();
if (needInstalling.Any())
{
await Task.WhenAll(
needInstalling.Select(SteamManager.Workshop.DownloadModThenEnqueueInstall));
}
if (!needInstalling.Any()) { return Enumerable.Empty<Steamworks.Ugc.Item>(); }
await Task.WhenAll(
needInstalling.Select(SteamManager.Workshop.DownloadModThenEnqueueInstall));
return needInstalling;
}
@@ -128,21 +128,17 @@ namespace Barotrauma
public static void Update(float deltaTime)
{
lock (queuedMessages)
while (queuedMessages.TryDequeue(out var newMsg))
{
while (queuedMessages.Count > 0)
{
var newMsg = queuedMessages.Dequeue();
AddMessage(newMsg);
AddMessage(newMsg);
if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging)
if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging)
{
unsavedMessages.Add(newMsg);
if (unsavedMessages.Count >= messagesPerFile)
{
unsavedMessages.Add(newMsg);
if (unsavedMessages.Count >= messagesPerFile)
{
SaveLogs();
unsavedMessages.Clear();
}
SaveLogs();
unsavedMessages.Clear();
}
}
}
@@ -258,25 +254,21 @@ namespace Barotrauma
public static void DequeueMessages()
{
lock (queuedMessages)
while (queuedMessages.TryDequeue(out var newMsg))
{
while (queuedMessages.Count > 0)
if (listBox == null)
{
var newMsg = queuedMessages.Dequeue();
if (listBox == null)
{
//don't attempt to add to the listbox if it hasn't been created yet
Messages.Add(newMsg);
}
else
{
AddMessage(newMsg);
}
//don't attempt to add to the listbox if it hasn't been created yet
Messages.Add(newMsg);
}
else
{
AddMessage(newMsg);
}
if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging)
{
unsavedMessages.Add(newMsg);
}
if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging)
{
unsavedMessages.Add(newMsg);
}
}
}
@@ -327,7 +327,7 @@ namespace Barotrauma
DrawString(spriteBatch, new Vector2(10, y),
"FPS: " + Math.Round(GameMain.PerformanceCounter.AverageFramesPerSecond),
Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
if (GameMain.GameSession != null && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 1.0)
if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 1.0)
{
y += yStep;
DrawString(spriteBatch, new Vector2(10, y),
@@ -758,7 +758,7 @@ namespace Barotrauma
toolTipBlock.DrawManually(spriteBatch);
}
public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle targetElement)
public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle targetElement, Anchor anchor = Anchor.BottomCenter, Pivot pivot = Pivot.TopLeft)
{
if (ObjectiveManager.ContentRunning) { return; }
@@ -775,7 +775,10 @@ namespace Barotrauma
toolTipBlock.UserData = toolTip;
}
toolTipBlock.RectTransform.AbsoluteOffset = new Point(targetElement.Center.X, targetElement.Bottom);
toolTipBlock.RectTransform.AbsoluteOffset =
RectTransform.CalculateAnchorPoint(anchor, targetElement) +
RectTransform.CalculatePivotOffset(pivot, toolTipBlock.RectTransform.NonScaledSize);
if (toolTipBlock.Rect.Right > GameMain.GraphicsWidth - 10)
{
toolTipBlock.RectTransform.AbsoluteOffset -= new Point(toolTipBlock.Rect.Width, 0);
@@ -134,7 +134,7 @@ namespace Barotrauma
set => hadSellSubPermissions = value;
}
private bool HasPermissionToUseTab(StoreTab tab)
private static bool HasPermissionToUseTab(StoreTab tab)
{
return tab switch
{
@@ -278,6 +278,7 @@ namespace Barotrauma
RefreshBuying(updateOwned: false);
RefreshSelling(updateOwned: false);
RefreshSellingFromSub(updateOwned: false);
SetConfirmButtonBehavior();
needsRefresh = false;
}
@@ -859,18 +860,17 @@ namespace Barotrauma
float prevBuyListScroll = storeBuyList.BarScroll;
float prevShoppingCrateScroll = shoppingCrateBuyList.BarScroll;
int dailySpecialCount = ActiveStore.DailySpecials.Count;
if ((storeDailySpecialsGroup != null) != ActiveStore.DailySpecials.Any() || dailySpecialCount != prevDailySpecialCount)
int dailySpecialCount = ActiveStore?.DailySpecials.Count(s => s.CanCharacterBuy()) ?? 0;
if ((ActiveStore == null && storeDailySpecialsGroup != null) || (storeDailySpecialsGroup != null) != ActiveStore.DailySpecials.Any() || dailySpecialCount != prevDailySpecialCount)
{
if (storeDailySpecialsGroup == null || dailySpecialCount != prevDailySpecialCount)
storeBuyList.RemoveChild(storeDailySpecialsGroup?.Parent);
if (ActiveStore != null && (storeDailySpecialsGroup == null || dailySpecialCount != prevDailySpecialCount))
{
storeBuyList.RemoveChild(storeDailySpecialsGroup?.Parent);
storeDailySpecialsGroup = CreateDealsGroup(storeBuyList, dailySpecialCount);
storeDailySpecialsGroup.Parent.SetAsFirstChild();
}
else
{
storeBuyList.RemoveChild(storeDailySpecialsGroup.Parent);
storeDailySpecialsGroup = null;
}
storeBuyList.RecalculateChildren();
@@ -879,15 +879,17 @@ namespace Barotrauma
bool hasPermissions = HasTabPermissions(StoreTab.Buy);
var existingItemFrames = new HashSet<GUIComponent>();
foreach (PurchasedItem item in ActiveStore.Stock)
if (ActiveStore != null)
{
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
}
foreach (ItemPrefab itemPrefab in ActiveStore.DailySpecials)
{
if (ActiveStore.Stock.Any(pi => pi.ItemPrefab == itemPrefab)) { continue; }
CreateOrUpdateItemFrame(itemPrefab, 0);
foreach (PurchasedItem item in ActiveStore.Stock)
{
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
}
foreach (ItemPrefab itemPrefab in ActiveStore.DailySpecials)
{
if (ActiveStore.Stock.Any(pi => pi.ItemPrefab == itemPrefab)) { continue; }
CreateOrUpdateItemFrame(itemPrefab, 0);
}
}
void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int quantity)
@@ -945,11 +947,11 @@ namespace Barotrauma
float prevSellListScroll = storeSellList.BarScroll;
float prevShoppingCrateScroll = shoppingCrateSellList.BarScroll;
int requestedGoodsCount = ActiveStore.RequestedGoods.Count;
if ((storeRequestedGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevRequestedGoodsCount)
int requestedGoodsCount = ActiveStore?.RequestedGoods.Count ?? 0;
if ((ActiveStore == null && storeRequestedGoodGroup != null) || (storeRequestedGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevRequestedGoodsCount)
{
storeSellList.RemoveChild(storeRequestedGoodGroup?.Parent);
if (storeRequestedGoodGroup == null || requestedGoodsCount != prevRequestedGoodsCount)
if (ActiveStore != null && (storeRequestedGoodGroup == null || requestedGoodsCount != prevRequestedGoodsCount))
{
storeRequestedGoodGroup = CreateDealsGroup(storeSellList, requestedGoodsCount);
storeRequestedGoodGroup.Parent.SetAsFirstChild();
@@ -964,14 +966,17 @@ namespace Barotrauma
bool hasPermissions = HasTabPermissions(StoreTab.Sell);
var existingItemFrames = new HashSet<GUIComponent>();
foreach (PurchasedItem item in itemsToSell)
if (ActiveStore != null)
{
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
}
foreach (var requestedGood in ActiveStore.RequestedGoods)
{
if (itemsToSell.Any(pi => pi.ItemPrefab == requestedGood)) { continue; }
CreateOrUpdateItemFrame(requestedGood, 0);
foreach (PurchasedItem item in itemsToSell)
{
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
}
foreach (var requestedGood in ActiveStore.RequestedGoods)
{
if (itemsToSell.Any(pi => pi.ItemPrefab == requestedGood)) { continue; }
CreateOrUpdateItemFrame(requestedGood, 0);
}
}
void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity)
@@ -1029,11 +1034,11 @@ namespace Barotrauma
float prevSellListScroll = storeSellFromSubList.BarScroll;
float prevShoppingCrateScroll = shoppingCrateSellFromSubList.BarScroll;
int requestedGoodsCount = ActiveStore.RequestedGoods.Count;
if ((storeRequestedSubGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevSubRequestedGoodsCount)
int requestedGoodsCount = ActiveStore?.RequestedGoods.Count ?? 0;
if ((ActiveStore == null && storeRequestedSubGoodGroup != null) || (storeRequestedSubGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevSubRequestedGoodsCount)
{
storeSellFromSubList.RemoveChild(storeRequestedSubGoodGroup?.Parent);
if (storeRequestedSubGoodGroup == null || requestedGoodsCount != prevSubRequestedGoodsCount)
if (ActiveStore != null && (storeRequestedSubGoodGroup == null || requestedGoodsCount != prevSubRequestedGoodsCount))
{
storeRequestedSubGoodGroup = CreateDealsGroup(storeSellFromSubList, requestedGoodsCount);
storeRequestedSubGoodGroup.Parent.SetAsFirstChild();
@@ -1048,14 +1053,17 @@ namespace Barotrauma
bool hasPermissions = HasSellSubPermissions;
var existingItemFrames = new HashSet<GUIComponent>();
foreach (PurchasedItem item in itemsToSellFromSub)
if (ActiveStore != null)
{
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
}
foreach (var requestedGood in ActiveStore.RequestedGoods)
{
if (itemsToSellFromSub.Any(pi => pi.ItemPrefab == requestedGood)) { continue; }
CreateOrUpdateItemFrame(requestedGood, 0);
foreach (PurchasedItem item in itemsToSellFromSub)
{
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
}
foreach (var requestedGood in ActiveStore.RequestedGoods)
{
if (itemsToSellFromSub.Any(pi => pi.ItemPrefab == requestedGood)) { continue; }
CreateOrUpdateItemFrame(requestedGood, 0);
}
}
void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity)
@@ -1142,6 +1150,7 @@ namespace Barotrauma
public void RefreshItemsToSell()
{
itemsToSell.Clear();
if (ActiveStore == null) { return; }
var playerItems = CargoManager.GetSellableItems(Character.Controlled);
foreach (Item playerItem in playerItems)
{
@@ -1172,6 +1181,7 @@ namespace Barotrauma
public void RefreshItemsToSellFromSub()
{
itemsToSellFromSub.Clear();
if (ActiveStore == null) { return; }
var subItems = CargoManager.GetSellableItemsFromSub();
foreach (Item subItem in subItems)
{
@@ -1205,52 +1215,55 @@ namespace Barotrauma
bool hasPermissions = HasTabPermissions(tab);
HashSet<GUIComponent> existingItemFrames = new HashSet<GUIComponent>();
int totalPrice = 0;
foreach (PurchasedItem item in items)
if (ActiveStore != null)
{
if (!(item.ItemPrefab.GetPriceInfo(ActiveStore) is { } priceInfo)) { continue; }
GUINumberInput numInput = null;
if (!(listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier) is { } itemFrame))
foreach (PurchasedItem item in items)
{
itemFrame = CreateItemFrame(item, listBox, tab, forceDisable: !hasPermissions);
numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput;
}
else
{
itemFrame.UserData = item;
numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput;
if (!(item.ItemPrefab.GetPriceInfo(ActiveStore) is { } priceInfo)) { continue; }
GUINumberInput numInput = null;
if (!(listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier) is { } itemFrame))
{
itemFrame = CreateItemFrame(item, listBox, tab, forceDisable: !hasPermissions);
numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput;
}
else
{
itemFrame.UserData = item;
numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput;
if (numInput != null)
{
numInput.UserData = item;
numInput.Enabled = hasPermissions;
numInput.MaxValueInt = GetMaxAvailable(item.ItemPrefab, tab);
}
SetOwnedText(itemFrame);
SetItemFrameStatus(itemFrame, hasPermissions);
}
existingItemFrames.Add(itemFrame);
suppressBuySell = true;
if (numInput != null)
{
numInput.UserData = item;
numInput.Enabled = hasPermissions;
numInput.MaxValueInt = GetMaxAvailable(item.ItemPrefab, tab);
if (numInput.IntValue != item.Quantity) { itemFrame.Flash(GUIStyle.Green); }
numInput.IntValue = item.Quantity;
}
SetOwnedText(itemFrame);
SetItemFrameStatus(itemFrame, hasPermissions);
}
existingItemFrames.Add(itemFrame);
suppressBuySell = false;
suppressBuySell = true;
if (numInput != null)
{
if (numInput.IntValue != item.Quantity) { itemFrame.Flash(GUIStyle.Green); }
numInput.IntValue = item.Quantity;
}
suppressBuySell = false;
try
{
int price = tab switch
try
{
StoreTab.Buy => ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo),
StoreTab.Sell => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo),
StoreTab.SellSub => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo),
_ => throw new NotImplementedException()
};
totalPrice += item.Quantity * price;
}
catch (NotImplementedException e)
{
DebugConsole.LogError($"Error getting item price: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}");
int price = tab switch
{
StoreTab.Buy => ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo),
StoreTab.Sell => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo),
StoreTab.SellSub => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo),
_ => throw new NotImplementedException()
};
totalPrice += item.Quantity * price;
}
catch (NotImplementedException e)
{
DebugConsole.LogError($"Error getting item price: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}");
}
}
}
@@ -1287,7 +1300,7 @@ namespace Barotrauma
private void SortItems(GUIListBox list, SortingMethod sortingMethod)
{
if (CurrentLocation == null) { return; }
if (CurrentLocation == null || ActiveStore == null) { return; }
if (sortingMethod == SortingMethod.AlphabeticalAsc || sortingMethod == SortingMethod.AlphabeticalDesc)
{
@@ -1662,6 +1675,8 @@ namespace Barotrauma
{
OwnedItems.Clear();
if (ActiveStore == null) { return; }
// Add items on the sub(s)
if (Submarine.MainSub?.GetItems(true) is List<Item> subItems)
{
@@ -2062,7 +2077,12 @@ namespace Barotrauma
private void SetShoppingCrateTotalText()
{
if (IsBuying)
if (ActiveStore == null)
{
shoppingCrateTotal.Text = TextManager.FormatCurrency(0);
shoppingCrateTotal.TextColor = Color.White;
}
else if (IsBuying)
{
shoppingCrateTotal.Text = TextManager.FormatCurrency(buyTotal);
shoppingCrateTotal.TextColor = Balance < buyTotal ? Color.Red : Color.White;
@@ -2082,7 +2102,11 @@ namespace Barotrauma
private void SetConfirmButtonBehavior()
{
if (IsBuying)
if (ActiveStore == null)
{
confirmButton.OnClicked = null;
}
else if (IsBuying)
{
confirmButton.ClickSound = GUISoundType.ConfirmTransaction;
confirmButton.Text = TextManager.Get("CampaignStore.Purchase");
@@ -2110,6 +2134,7 @@ namespace Barotrauma
private void SetConfirmButtonStatus()
{
confirmButton.Enabled =
ActiveStore != null &&
HasActiveTabPermissions() &&
ActiveShoppingCrateList.Content.RectTransform.Children.Any() &&
activeTab switch
@@ -2119,6 +2144,7 @@ namespace Barotrauma
StoreTab.SellSub => CurrentLocation != null && sellFromSubTotal <= ActiveStore.Balance,
_ => false
};
confirmButton.Visible = ActiveStore != null;
}
private void SetClearAllButtonStatus()
@@ -2192,29 +2218,32 @@ namespace Barotrauma
prevBalance = currBalance;
}
}
if (needsItemsToSellRefresh)
if (ActiveStore != null)
{
RefreshItemsToSell();
}
if (needsItemsToSellFromSubRefresh)
{
RefreshItemsToSellFromSub();
}
if (needsRefresh)
{
Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f);
}
if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy))
{
RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f);
}
if (needsSellingRefresh || HavePermissionsChanged(StoreTab.Sell))
{
RefreshSelling(updateOwned: ownedItemsUpdateTimer > 0.0f);
}
if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub))
{
RefreshSellingFromSub(updateOwned: ownedItemsUpdateTimer > 0.0f, updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f);
if (needsItemsToSellRefresh)
{
RefreshItemsToSell();
}
if (needsItemsToSellFromSubRefresh)
{
RefreshItemsToSellFromSub();
}
if (needsRefresh)
{
Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f);
}
if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy))
{
RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f);
}
if (needsSellingRefresh || HavePermissionsChanged(StoreTab.Sell))
{
RefreshSelling(updateOwned: ownedItemsUpdateTimer > 0.0f);
}
if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub))
{
RefreshSellingFromSub(updateOwned: ownedItemsUpdateTimer > 0.0f, updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f);
}
}
updateStopwatch.Stop();
@@ -1491,7 +1491,11 @@ namespace Barotrauma
GUIFrame missionFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox");
int padding = (int)(0.0245f * missionFrame.Rect.Height);
GUIFrame missionFrameContent = new GUIFrame(new RectTransform(new Point(missionFrame.Rect.Width - padding * 2, missionFrame.Rect.Height - padding * 2), infoFrame.RectTransform, Anchor.Center), style: null);
Location location = GameMain.GameSession.EndLocation ?? GameMain.GameSession.StartLocation;
Location location = GameMain.GameSession.StartLocation;
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
{
location ??= GameMain.GameSession.EndLocation;
}
GUILayoutGroup locationInfoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), missionFrameContent.RectTransform))
{
@@ -283,7 +283,7 @@ namespace Barotrauma
break;
case TalentTreeType.Specialization:
talentList = GetSpecializationList();
treeSize = new Vector2(0.333f, 1f);
treeSize = new Vector2(Math.Max(0.333f, 1.0f / tree.TalentSubTrees.Count(t => t.Type == TalentTreeType.Specialization)), 1f);
break;
default:
throw new ArgumentOutOfRangeException($"Invalid TalentTreeType \"{subTree.Type}\"");
@@ -325,7 +325,9 @@ namespace Barotrauma
}
var specializationList = GetSpecializationList();
GetSpecializationList().Content.RectTransform.Resize(new Point(specializationList.Content.Children.Sum(static c => c.Rect.Width), specializationList.Rect.Height), resizeChildren: false);
//resize (scale up) children if there's less than 3 of them to make them cover the whole width of the menu
specializationList.Content.RectTransform.Resize(new Point(specializationList.Content.Children.Sum(static c => c.Rect.Width), specializationList.Rect.Height),
resizeChildren: specializationList.Content.Children.Count() < 3);
GUITextBlock.AutoScaleAndNormalize(subTreeNames);
@@ -16,6 +16,7 @@ using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -465,10 +466,11 @@ namespace Barotrauma
LegacySteamUgcTransition.Prepare();
var contentPackageLoadRoutine = ContentPackageManager.Init();
foreach (var progress in contentPackageLoadRoutine)
foreach (var progress in contentPackageLoadRoutine
.Select(p => p.Result).Successes())
{
const float min = 1f, max = 70f;
TitleScreen.LoadState = MathHelper.Lerp(min, max, progress.Value);
TitleScreen.LoadState = MathHelper.Lerp(min, max, progress);
yield return CoroutineStatus.Running;
}
@@ -1078,10 +1080,9 @@ namespace Barotrauma
if (GameSession != null)
{
double roundDuration = Timing.TotalTime - GameSession.RoundStartTime;
GameAnalyticsManager.AddProgressionEvent(GameAnalyticsManager.ProgressionStatus.Fail,
GameSession.GameMode?.Preset.Identifier.Value ?? "none",
roundDuration);
GameSession.RoundDuration);
string eventId = "QuitRound:" + (GameSession.GameMode?.Preset.Identifier.Value ?? "none") + ":";
GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:CurrentIntensity", GameSession.EventManager.CurrentIntensity);
foreach (var activeEvent in GameSession.EventManager.ActiveEvents)
@@ -11,7 +11,15 @@ namespace Barotrauma
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
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)
{
@@ -444,7 +444,7 @@ namespace Barotrauma
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,
transitionType == TransitionType.LeaveLocation ? Alignment.BottomCenter : Alignment.Center,
@@ -296,7 +296,7 @@ static class ObjectiveManager
};
for (int i = 0; i < activeObjectives.Count; i++)
{
AddToObjectiveList(activeObjectives[i]);
AddToObjectiveList(activeObjectives[i], useExistingIndex: true);
}
screenSettings = new ScreenSettings(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Scale, GameSettings.CurrentConfig.Graphics.DisplayMode);
}
@@ -318,7 +318,7 @@ static class ObjectiveManager
/// <summary>
/// Adds the segment to the objective list
/// </summary>
private static void AddToObjectiveList(Segment segment, bool connectExisting = false)
private static void AddToObjectiveList(Segment segment, bool connectExisting = false, bool useExistingIndex = false)
{
if (connectExisting)
{
@@ -338,8 +338,8 @@ static class ObjectiveManager
if (parentSegment is not null)
{
// Add this child as the last child in case there are other existing children already
int totalChildren = activeObjectives.Count(s => s.ParentId == segment.ParentId);
int childIndex = activeObjectives.IndexOf(parentSegment) + totalChildren;
int childIndex = useExistingIndex ? activeObjectives.IndexOf(segment) :
activeObjectives.IndexOf(parentSegment) + activeObjectives.Count(s => s.ParentId == segment.ParentId);
if (objectiveGroup.RectTransform.GetChildIndex(frameRt) != childIndex)
{
frameRt.RepositionChildInHierarchy(childIndex);
@@ -971,7 +971,7 @@ namespace Barotrauma
//don't allow swapping if we're moving items into an item with 1 slot holding a stack of items
//(in that case, the quick action should just fill up the stack)
bool disallowSwapping =
heldItem.OwnInventory.Capacity == 1 &&
(heldItem.OwnInventory.Capacity == 1 || heldItem.OwnInventory.Container.HasSubContainers) &&
heldItem.OwnInventory.GetItemAt(0)?.Prefab == item.Prefab &&
heldItem.OwnInventory.GetItemsAt(0).Count() > 1;
if (heldItem.OwnInventory.TryPutItem(item, Character.Controlled) ||
@@ -1131,7 +1131,7 @@ namespace Barotrauma
GUI.DrawRectangle(spriteBatch, inventoryArea, new Color(30,30,30,100), isFilled: true);
var lockIcon = GUIStyle.GetComponentStyle("LockIcon")?.GetDefaultSprite();
lockIcon?.Draw(spriteBatch, inventoryArea.Center.ToVector2(), scale: Math.Min(inventoryArea.Height / lockIcon.size.Y * 0.7f, 1.0f));
if (inventoryArea.Contains(PlayerInput.MousePosition))
if (inventoryArea.Contains(PlayerInput.MousePosition) && character.LockHands)
{
GUIComponent.DrawToolTip(spriteBatch, TextManager.Get("handcuffed"), new Rectangle(inventoryArea.Center - new Point(inventoryArea.Height / 2), new Point(inventoryArea.Height)));
}
@@ -408,6 +408,8 @@ namespace Barotrauma.Items.Components
var wire = it.GetComponent<Wire>();
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; }
return true;
@@ -29,7 +29,11 @@ namespace Barotrauma.Items.Components
private Sprite tempRangeIndicator;
private Sprite graphLine;
//private GUIFrame graph;
private GUICustomComponent graph;
private GUIFrame inventoryWindow;
private GUILayoutGroup buttonArea;
private GUIFrame infographic;
private Color optimalRangeColor = new Color(74,238,104,255);
private Color offRangeColor = Color.Orange;
@@ -66,6 +70,8 @@ namespace Barotrauma.Items.Components
};
public override bool RecreateGUIOnResolutionChange => true;
public bool TriggerInfographic { get; set; }
partial void InitProjSpecific(ContentXElement element)
{
@@ -122,7 +128,7 @@ namespace Barotrauma.Items.Components
//left column
//----------------------------------------------------------
GUIFrame inventoryWindow = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.75f), GuiFrame.RectTransform, Anchor.TopLeft, Pivot.TopRight)
inventoryWindow = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.75f), GuiFrame.RectTransform, Anchor.TopLeft, Pivot.TopRight)
{
MinSize = new Point(85, 220),
RelativeOffset = new Vector2(-0.02f, 0)
@@ -255,7 +261,7 @@ namespace Barotrauma.Items.Components
};
TurbineOutputScrollBar.Frame.UserData = UIHighlightAction.ElementId.TurbineOutputSlider;
var buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1, 0.2f), columnLeft.RectTransform))
buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1, 0.2f), columnLeft.RectTransform))
{
Stretch = true,
RelativeSpacing = 0.02f
@@ -436,8 +442,8 @@ namespace Barotrauma.Items.Components
LocalizedString kW = TextManager.Get("kilowatt");
loadText.TextGetter += () => $"{loadStr.Replace("[kw]", ((int)Load).ToString())} {kW}";
var graph = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), graphArea.RectTransform), style: "InnerFrameRed");
new GUICustomComponent(new RectTransform(new Vector2(0.9f, 0.98f), graph.RectTransform, Anchor.Center), DrawGraph, null);
var graphFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), graphArea.RectTransform), style: "InnerFrameRed");
graph = new GUICustomComponent(new RectTransform(new Vector2(0.9f, 0.98f), graphFrame.RectTransform, Anchor.Center), DrawGraph, null);
var outputText = new GUITextBlock(new RectTransform(relativeTextSize, graphArea.RectTransform),
"Output", textColor: outputColor, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft)
@@ -448,6 +454,22 @@ namespace Barotrauma.Items.Components
outputText.TextGetter += () => $"{outputStr.Replace("[kw]", ((int)-currPowerConsumption).ToString())} {kW}";
InitInventoryUI();
// Infographic overlay ---------------------
int buttonHeight = (int)(GUIStyle.ItemFrameMargin.Y * 0.4f);
var helpButtonRt = new RectTransform(new Point(buttonHeight), parent: GuiFrame.RectTransform, anchor: Anchor.TopRight)
{
AbsoluteOffset = new Point(buttonHeight / 4),
MinSize = new Point(buttonHeight)
};
new GUIButton(helpButtonRt, "", style: "HelpIcon")
{
OnClicked = (_, _) =>
{
CreateInfrographic();
return true;
}
};
}
private void InitInventoryUI()
@@ -469,7 +491,6 @@ namespace Barotrauma.Items.Components
InitInventoryUI();
}
private void DrawTempMeter(SpriteBatch spriteBatch, GUICustomComponent container)
{
Vector2 meterPos = new Vector2(container.Rect.X, container.Rect.Y);
@@ -518,7 +539,6 @@ namespace Barotrauma.Items.Components
DrawGraph(loadGraph, spriteBatch, graphRect, Math.Max(10000.0f, maxLoad), xOffset, loadColor);
}
private void UpdateGraph(float deltaTime)
{
graphTimer += deltaTime * 1000.0f;
@@ -645,6 +665,12 @@ namespace Barotrauma.Items.Components
}
}
}
if (TriggerInfographic)
{
CreateInfrographic();
TriggerInfographic = false;
}
}
private void DrawMeter(SpriteBatch spriteBatch, Rectangle rect, Sprite meterSprite, float value, Vector2 range, Vector2 optimalRange, Vector2 allowedRange)
@@ -760,7 +786,96 @@ namespace Barotrauma.Items.Components
spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect;
spriteBatch.Begin(SpriteSortMode.Deferred);
}
private enum InfographicArrowStyle { Straight, Curved };
private void CreateInfrographic()
{
if (infographic != null) { return; }
var dimColor = Color.Lerp(Color.Black, Color.TransparentBlack, 0.25f);
// Dim reactor interface
infographic = new GUIFrame(new RectTransform(Vector2.One, GuiFrame.RectTransform))
{
CanBeFocused = false,
Color = dimColor
};
// Dim inventory window
new GUIFrame(new RectTransform(inventoryWindow.Rect.Size, infographic.RectTransform) { AbsoluteOffset = inventoryWindow.Rect.Location - GuiFrame.Rect.Location}, color: dimColor)
{
CanBeFocused = false
};
int arrowSize = (int)(70 * GUI.Scale);
var arrows = new Dictionary<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()
{
base.RemoveComponentSpecific();
@@ -812,7 +812,7 @@ namespace Barotrauma.Items.Components
if (distSqr > t.SoundRange * t.SoundRange * 2) { continue; }
float dist = (float)Math.Sqrt(distSqr);
if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500)
if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500 && t.IsWithinSector(transducerCenter))
{
int prevBlipCount = sonarBlips.Count;
Ping(t.WorldPosition, transducerCenter,
@@ -35,6 +35,13 @@ namespace Barotrauma.Items.Components
if (GuiFrame == null) { return; }
originalMaxSize = GuiFrame.RectTransform.MaxSize;
originalRelativeSize = GuiFrame.RectTransform.RelativeSize;
CreateGUI();
}
protected override void CreateGUI()
{
if (GuiFrame == null) { return; }
CheckForLabelOverlap();
var content = new GUICustomComponent(new RectTransform(Vector2.One, GuiFrame.RectTransform), DrawConnections, null)
{
@@ -43,8 +50,8 @@ namespace Barotrauma.Items.Components
content.RectTransform.SetAsFirstChild();
//prevents inputs from going through the GUICustomComponent to the drag handle
dragArea = new GUIFrame(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center)
{ AbsoluteOffset = GUIStyle.ItemFrameOffset }, style: null);
dragArea = new GUIFrame(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center)
{ AbsoluteOffset = GUIStyle.ItemFrameOffset }, style: null);
}
public void TriggerRewiringSound()
@@ -121,12 +128,6 @@ namespace Barotrauma.Items.Components
}
}
protected override void OnResolutionChanged()
{
if (GuiFrame == null) { return; }
CheckForLabelOverlap();
}
private void CheckForLabelOverlap()
{
GuiFrame.RectTransform.MaxSize = originalMaxSize;
@@ -324,7 +324,7 @@ namespace Barotrauma.Items.Components
if (Character.Controlled != null)
{
Character.Controlled.FocusedItem = null;
Character.Controlled.ResetInteract = true;
Character.Controlled.DisableInteract = true;
Character.Controlled.ClearInputs();
}
//cancel dragging
@@ -401,7 +401,7 @@ namespace Barotrauma.Items.Components
{
if (Character.Controlled != null)
{
Character.Controlled.ResetInteract = true;
Character.Controlled.DisableInteract = true;
Character.Controlled.ClearInputs();
}
int closestSectionIndex = selectedWire.GetClosestSectionIndex(mousePos, sectionSelectDist, out _);
@@ -431,7 +431,7 @@ namespace Barotrauma.Items.Components
{
if (Character.Controlled != null)
{
Character.Controlled.ResetInteract = true;
Character.Controlled.DisableInteract = true;
Character.Controlled.ClearInputs();
}
draggingWire = selectedWire;
@@ -1282,7 +1282,8 @@ namespace Barotrauma
{
foreach (ItemComponent ic in activeHUDs)
{
if (ic.GuiFrame == null || !ic.CanBeSelected) { continue; }
if (ic.GuiFrame == null) { continue; }
if (!ic.CanBeSelected && !ic.DrawHudWhenEquipped) { continue; }
ic.GuiFrame.RectTransform.ScreenSpaceOffset = Point.Zero;
if (ic.UseAlternativeLayout)
{
@@ -1042,19 +1042,11 @@ namespace Barotrauma
protected static void PositionEditingHUD()
{
int maxHeight = 100;
if (Screen.Selected == GameMain.SubEditorScreen)
{
editingHUD.RectTransform.SetPosition(Anchor.TopRight);
editingHUD.RectTransform.AbsoluteOffset = new Point(0, GameMain.SubEditorScreen.TopPanel.Rect.Bottom);
maxHeight = (GameMain.GraphicsHeight - GameMain.SubEditorScreen.EntityMenu.Rect.Height) - GameMain.SubEditorScreen.TopPanel.Rect.Bottom * 2 - 20;
}
else
{
editingHUD.RectTransform.SetPosition(Anchor.TopRight);
editingHUD.RectTransform.RelativeOffset = new Vector2(0.0f, (HUDLayoutSettings.CrewArea.Bottom + 10.0f) / (editingHUD.RectTransform.Parent ?? GUI.Canvas).Rect.Height);
maxHeight = HUDLayoutSettings.InventoryAreaLower.Y - HUDLayoutSettings.CrewArea.Bottom - 10;
}
int maxHeight =
Screen.Selected == GameMain.SubEditorScreen ?
GameMain.GraphicsHeight - GameMain.SubEditorScreen.EntityMenu.Rect.Height - GameMain.SubEditorScreen.TopPanel.Rect.Bottom * 2 - 20 :
HUDLayoutSettings.InventoryAreaLower.Y - HUDLayoutSettings.CrewArea.Bottom - 10;
var listBox = editingHUD.GetChild<GUIListBox>();
if (listBox != null)
@@ -1074,6 +1066,17 @@ namespace Barotrauma
MathHelper.Clamp(contentHeight + padding * 2, 50, maxHeight)), resizeChildren: false);
listBox.RectTransform.Resize(new Point(listBox.RectTransform.NonScaledSize.X, editingHUD.RectTransform.NonScaledSize.Y - padding * 2), resizeChildren: false);
}
editingHUD.RectTransform.SetPosition(Anchor.TopRight);
if (Screen.Selected == GameMain.SubEditorScreen)
{
editingHUD.RectTransform.AbsoluteOffset = new Point(0, GameMain.SubEditorScreen.TopPanel.Rect.Bottom);
}
else
{
editingHUD.RectTransform.AbsoluteOffset = new Point(
0,
HUDLayoutSettings.HealthBarAfflictionArea.Y - editingHUD.Rect.Height - GUI.IntScale(10));
}
}
public virtual void DrawEditing(SpriteBatch spriteBatch, Camera cam) { }
@@ -6,9 +6,9 @@ namespace Barotrauma.Networking
{
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.WriteRangedInteger((int)Type, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1);
msg.WriteRangedInteger((int)ChatMode, 0, Enum.GetValues(typeof(ChatMode)).Length - 1);
@@ -485,15 +485,11 @@ namespace Barotrauma.Networking
}
catch (Exception e)
{
string errorMsg = "Error while reading a message from server. {" + e + "}. ";
string errorMsg = "Error while reading a message from server. ";
if (GameMain.Client == null) { errorMsg += "Client disposed."; }
errorMsg += "\n" + e.StackTrace.CleanupStackTrace();
if (e.InnerException != null)
{
errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace();
}
AppendExceptionInfo(ref errorMsg, e);
GameAnalyticsManager.AddErrorEventOnce("GameClient.Update:CheckServerMessagesException" + e.TargetSite.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
DebugConsole.ThrowError("Error while reading a message from server.", e);
DebugConsole.ThrowError(errorMsg);
new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariables("MessageReadError", ("[message]", e.Message), ("[targetsite]", e.TargetSite.ToString())))
{
DisplayInLoadingScreens = true
@@ -636,14 +632,8 @@ namespace Barotrauma.Networking
}
catch (Exception e)
{
string errorMsg = "Error while reading an ingame update message from server. {" + e + "}\n" + e.StackTrace.CleanupStackTrace();
if (e.InnerException != null)
{
errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace();
}
#if DEBUG
DebugConsole.ThrowError("Error while reading an ingame update message from server.", e);
#endif
string errorMsg = "Error while reading an ingame update message from server.";
AppendExceptionInfo(ref errorMsg, e);
GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadDataMessage:ReadIngameUpdate", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw;
}
@@ -872,22 +862,31 @@ namespace Barotrauma.Networking
}
byte missionCount = inc.ReadByte();
if (missionCount != GameMain.GameSession.Missions.Count())
{
string errorMsg = $"Mission equality check failed. Mission count doesn't match the server (server: {missionCount}, client: {GameMain.GameSession.Missions.Count()})";
throw new Exception(errorMsg);
}
List<Identifier> serverMissionIdentifiers = new List<Identifier>();
for (int i = 0; i < missionCount; i++)
{
serverMissionIdentifiers.Add(inc.ReadIdentifier());
}
if (missionCount != GameMain.GameSession.GameMode.Missions.Count())
{
string errorMsg =
$"Mission equality check failed. Mission count doesn't match the server. " +
$"Server: {string.Join(", ", serverMissionIdentifiers)}, " +
$"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " +
$"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})";
GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsCountMismatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw new Exception(errorMsg);
}
if (missionCount > 0)
{
if (!GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier).OrderBy(id => id).SequenceEqual(serverMissionIdentifiers.OrderBy(id => id)))
if (!GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier).OrderBy(id => id).SequenceEqual(serverMissionIdentifiers.OrderBy(id => id)))
{
string errorMsg = $"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server (server: {string.Join(", ", serverMissionIdentifiers)}, client: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})";
string errorMsg =
$"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server " +
$"Server: {string.Join(", ", serverMissionIdentifiers)}, " +
$"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " +
$"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})";
GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw new Exception(errorMsg);
}
@@ -1883,12 +1882,11 @@ namespace Barotrauma.Networking
private void ReadLobbyUpdate(IReadMessage inc)
{
ServerNetObject objHeader;
while ((objHeader = (ServerNetObject)inc.ReadByte()) != ServerNetObject.END_OF_MESSAGE)
SegmentTableReader<ServerNetSegment>.Read(inc, (segment, inc) =>
{
switch (objHeader)
switch (segment)
{
case ServerNetObject.SYNC_IDS:
case ServerNetSegment.SyncIds:
bool lobbyUpdated = inc.ReadBoolean();
inc.ReadPadBits();
@@ -2020,17 +2018,19 @@ namespace Barotrauma.Networking
lastSentChatMsgID = inc.ReadUInt16();
break;
case ServerNetObject.CLIENT_LIST:
case ServerNetSegment.ClientList:
ReadClientList(inc);
break;
case ServerNetObject.CHAT_MESSAGE:
case ServerNetSegment.ChatMessage:
ChatMessage.ClientRead(inc);
break;
case ServerNetObject.VOTE:
case ServerNetSegment.Vote:
Voting.ClientRead(inc);
break;
}
}
return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.No;
});
}
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;
ServerNetObject? prevObjHeader = null;
long prevBitPos = 0;
long prevBytePos = 0;
long prevBitLength = 0;
long prevByteLength = 0;
ServerNetObject? objHeader = null;
try
SegmentTableReader<ServerNetSegment>.Read(inc,
segmentDataReader: (segment, inc) =>
{
while ((objHeader = (ServerNetObject)inc.ReadByte()) != ServerNetObject.END_OF_MESSAGE)
switch (segment)
{
switch (objHeader)
{
case ServerNetObject.SYNC_IDS:
lastSentChatMsgID = inc.ReadUInt16();
LastSentEntityEventID = inc.ReadUInt16();
case ServerNetSegment.SyncIds:
lastSentChatMsgID = inc.ReadUInt16();
LastSentEntityEventID = inc.ReadUInt16();
bool campaignUpdated = inc.ReadBoolean();
inc.ReadPadBits();
if (campaignUpdated)
{
MultiPlayerCampaign.ClientRead(inc);
}
else if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign)
{
GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null);
}
break;
case ServerNetObject.ENTITY_POSITION:
inc.ReadPadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly
bool isItem = inc.ReadBoolean(); inc.ReadPadBits();
UInt32 incomingUintIdentifier = inc.ReadUInt32();
UInt16 id = inc.ReadUInt16();
uint msgLength = inc.ReadVariableUInt32();
int msgEndPos = (int)(inc.BitPosition + msgLength * 8);
bool campaignUpdated = inc.ReadBoolean();
inc.ReadPadBits();
if (campaignUpdated)
{
MultiPlayerCampaign.ClientRead(inc);
}
else if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign)
{
GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null);
}
break;
case ServerNetSegment.EntityPosition:
inc.ReadPadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly
bool isItem = inc.ReadBoolean(); inc.ReadPadBits();
UInt32 incomingUintIdentifier = inc.ReadUInt32();
UInt16 id = inc.ReadUInt16();
uint msgLength = inc.ReadVariableUInt32();
int msgEndPos = (int)(inc.BitPosition + msgLength * 8);
var entity = Entity.FindEntityByID(id) as IServerPositionSync;
if (msgEndPos > inc.LengthBits)
{
DebugConsole.ThrowError($"Error while reading a position update for the entity \"({entity?.ToString() ?? "null"})\". Message length exceeds the size of the buffer.");
return;
}
var entity = Entity.FindEntityByID(id) as IServerPositionSync;
if (msgEndPos > inc.LengthBits)
{
DebugConsole.ThrowError($"Error while reading a position update for the entity \"({entity?.ToString() ?? "null"})\". Message length exceeds the size of the buffer.");
return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.Yes;
}
debugEntityList.Add(entity);
if (entity != null)
debugEntityList.Add(entity);
if (entity != null)
{
if (entity is Item != isItem)
{
if (entity is Item != isItem)
{
DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message. Entity type does not match (server entity is {(isItem ? "an item" : "not an item")}, client entity is {(entity?.GetType().ToString() ?? "null")}). Ignoring the message...");
}
else if (entity is MapEntity { Prefab: { UintIdentifier: { } uintIdentifier } } me &&
uintIdentifier != incomingUintIdentifier)
{
DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message."
+$"Entity identifier does not match (server entity is {MapEntityPrefab.List.FirstOrDefault(p => p.UintIdentifier == incomingUintIdentifier)?.Identifier.Value ?? "[not found]"}, "
+$"client entity is {me.Prefab.Identifier}). Ignoring the message...");
}
else
{
entity.ClientReadPosition(inc, sendingTime);
}
DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message. Entity type does not match (server entity is {(isItem ? "an item" : "not an item")}, client entity is {(entity?.GetType().ToString() ?? "null")}). Ignoring the message...");
}
//force to the correct position in case the entity doesn't exist
//or the message wasn't read correctly for whatever reason
inc.BitPosition = msgEndPos;
inc.ReadPadBits();
break;
case ServerNetObject.CLIENT_LIST:
ReadClientList(inc);
break;
case ServerNetObject.ENTITY_EVENT:
case ServerNetObject.ENTITY_EVENT_INITIAL:
if (!EntityEventManager.Read(objHeader.Value, inc, sendingTime, debugEntityList))
else if (entity is MapEntity { Prefab: { UintIdentifier: { } uintIdentifier } } me &&
uintIdentifier != incomingUintIdentifier)
{
return;
DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message."
+$"Entity identifier does not match (server entity is {MapEntityPrefab.List.FirstOrDefault(p => p.UintIdentifier == incomingUintIdentifier)?.Identifier.Value ?? "[not found]"}, "
+$"client entity is {me.Prefab.Identifier}). Ignoring the message...");
}
break;
case ServerNetObject.CHAT_MESSAGE:
ChatMessage.ClientRead(inc);
break;
default:
throw new Exception($"Unknown object header \"{objHeader}\"!)");
}
prevBitLength = inc.BitPosition - prevBitPos;
prevByteLength = inc.BytePosition - prevBytePos;
else
{
entity.ClientReadPosition(inc, sendingTime);
}
}
prevObjHeader = objHeader;
prevBitPos = inc.BitPosition;
prevBytePos = inc.BytePosition;
//force to the correct position in case the entity doesn't exist
//or the message wasn't read correctly for whatever reason
inc.BitPosition = msgEndPos;
inc.ReadPadBits();
break;
case ServerNetSegment.ClientList:
ReadClientList(inc);
break;
case ServerNetSegment.EntityEvent:
case ServerNetSegment.EntityEventInitial:
if (!EntityEventManager.Read(segment, inc, sendingTime, debugEntityList))
{
return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.Yes;
}
break;
case ServerNetSegment.ChatMessage:
ChatMessage.ClientRead(inc);
break;
default:
throw new Exception($"Unknown segment \"{segment}\"!)");
}
}
catch (Exception ex)
return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.No;
},
exceptionHandler: (segment, prevSegments, ex) =>
{
List<string> errorLines = new List<string>
{
ex.Message,
"Message length: " + inc.LengthBits + " (" + inc.LengthBytes + " bytes)",
"Read position: " + inc.BitPosition,
"Header: " + (objHeader != null ? objHeader.Value.ToString() : "Error occurred on the very first header!"),
prevObjHeader != null ? "Previous header: " + prevObjHeader : "Error occurred on the very first header!",
"Previous object was " + (prevBitLength) + " bits long (" + (prevByteLength) + " bytes)",
" "
$"Segment with error: {segment}"
};
if (prevSegments.Any())
{
errorLines.Add("Prev segments: " + string.Join(", ", prevSegments));
errorLines.Add(" ");
}
errorLines.Add(ex.StackTrace.CleanupStackTrace());
errorLines.Add(" ");
if (prevObjHeader == ServerNetObject.ENTITY_EVENT || prevObjHeader == ServerNetObject.ENTITY_EVENT_INITIAL ||
objHeader == ServerNetObject.ENTITY_EVENT || objHeader == ServerNetObject.ENTITY_EVENT_INITIAL ||
objHeader == ServerNetObject.ENTITY_POSITION || prevObjHeader == ServerNetObject.ENTITY_POSITION)
if (prevSegments.Concat(segment.ToEnumerable()).Any(s => s.Identifier
is ServerNetSegment.EntityPosition
or ServerNetSegment.EntityEvent
or ServerNetSegment.EntityEventInitial))
{
foreach (IServerSerializable ent in debugEntityList)
{
@@ -2164,34 +2153,18 @@ namespace Barotrauma.Networking
}
}
foreach (string line in errorLines)
{
DebugConsole.ThrowError(line);
}
errorLines.Add("Last console messages:");
for (int i = DebugConsole.Messages.Count - 1; i > Math.Max(0, DebugConsole.Messages.Count - 20); i--)
{
errorLines.Add("[" + DebugConsole.Messages[i].Time + "] " + DebugConsole.Messages[i].Text);
}
GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadInGameUpdate", GameAnalyticsManager.ErrorSeverity.Critical, string.Join("\n", errorLines));
DebugConsole.ThrowError("Writing object data to \"networkerror_data.log\", please send this file to us at http://github.com/Regalis11/Barotrauma/issues");
using (FileStream fl = File.Open("networkerror_data.log", System.IO.FileMode.Create))
{
using (System.IO.BinaryWriter bw = new System.IO.BinaryWriter(fl))
using (System.IO.StreamWriter sw = new System.IO.StreamWriter(fl))
{
bw.Write(inc.Buffer, (int)(prevBytePos - prevByteLength), (int)(prevByteLength));
sw.WriteLine("");
foreach (string line in errorLines)
{
sw.WriteLine(line);
}
}
}
throw new Exception("Read error: please send us \"networkerror_data.log\"!");
}
throw new Exception(
$"Exception thrown while reading segment {segment.Identifier} at position {segment.Pointer}." +
(prevSegments.Any() ? $" Previous segments: {string.Join(", ", prevSegments)}" : ""),
ex);
});
}
private void SendLobbyUpdate()
@@ -2199,50 +2172,51 @@ namespace Barotrauma.Networking
IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.WriteByte((byte)ClientPacketHeader.UPDATE_LOBBY);
outmsg.WriteByte((byte)ClientNetObject.SYNC_IDS);
outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID);
outmsg.WriteUInt16(ChatMessage.LastID);
outmsg.WriteUInt16(LastClientListUpdateID);
outmsg.WriteUInt16(nameId);
outmsg.WriteString(Name);
var jobPreferences = GameMain.NetLobbyScreen.JobPreferences;
if (jobPreferences.Count > 0)
using (var segmentTable = SegmentTableWriter<ClientNetSegment>.StartWriting(outmsg))
{
outmsg.WriteIdentifier(jobPreferences[0].Prefab.Identifier);
}
else
{
outmsg.WriteIdentifier(Identifier.Empty);
}
outmsg.WriteByte((byte)MultiplayerPreferences.Instance.TeamPreference);
if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0)
{
outmsg.WriteUInt16((UInt16)0);
}
else
{
outmsg.WriteUInt16(campaign.LastSaveID);
outmsg.WriteByte(campaign.CampaignID);
foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
segmentTable.StartNewSegment(ClientNetSegment.SyncIds);
outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID);
outmsg.WriteUInt16(ChatMessage.LastID);
outmsg.WriteUInt16(LastClientListUpdateID);
outmsg.WriteUInt16(nameId);
outmsg.WriteString(Name);
var jobPreferences = GameMain.NetLobbyScreen.JobPreferences;
if (jobPreferences.Count > 0)
{
outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(netFlag));
outmsg.WriteIdentifier(jobPreferences[0].Prefab.Identifier);
}
outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded);
}
chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID));
for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++)
{
if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5)
else
{
//no more room in this packet
break;
outmsg.WriteIdentifier(Identifier.Empty);
}
chatMsgQueue[i].ClientWrite(outmsg);
}
outmsg.WriteByte((byte)ClientNetObject.END_OF_MESSAGE);
outmsg.WriteByte((byte)MultiplayerPreferences.Instance.TeamPreference);
if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0)
{
outmsg.WriteUInt16((UInt16)0);
}
else
{
outmsg.WriteUInt16(campaign.LastSaveID);
outmsg.WriteByte(campaign.CampaignID);
foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
{
outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(netFlag));
}
outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded);
}
chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID));
for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++)
{
if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5)
{
//no more room in this packet
break;
}
chatMsgQueue[i].ClientWrite(segmentTable, outmsg);
}
}
if (outmsg.LengthBytes > MsgConstants.MTU)
{
DebugConsole.ThrowError($"Maximum packet size exceeded ({outmsg.LengthBytes} > {MsgConstants.MTU})");
@@ -2258,44 +2232,47 @@ namespace Barotrauma.Networking
outmsg.WriteBoolean(EntityEventManager.MidRoundSyncingDone);
outmsg.WritePadBits();
outmsg.WriteByte((byte)ClientNetObject.SYNC_IDS);
//outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID);
outmsg.WriteUInt16(ChatMessage.LastID);
outmsg.WriteUInt16(EntityEventManager.LastReceivedID);
outmsg.WriteUInt16(LastClientListUpdateID);
using (var segmentTable = SegmentTableWriter<ClientNetSegment>.StartWriting(outmsg))
{
segmentTable.StartNewSegment(ClientNetSegment.SyncIds);
//outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID);
outmsg.WriteUInt16(ChatMessage.LastID);
outmsg.WriteUInt16(EntityEventManager.LastReceivedID);
outmsg.WriteUInt16(LastClientListUpdateID);
if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0)
{
outmsg.WriteUInt16((UInt16)0);
}
else
{
outmsg.WriteUInt16(campaign.LastSaveID);
outmsg.WriteByte(campaign.CampaignID);
foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0)
{
outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(flag));
outmsg.WriteUInt16((UInt16)0);
}
outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded);
}
Character.Controlled?.ClientWriteInput(outmsg);
GameMain.GameScreen.Cam?.ClientWrite(outmsg);
EntityEventManager.Write(outmsg, ClientPeer?.ServerConnection);
chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID));
for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++)
{
if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5)
else
{
//not enough room in this packet
break;
}
chatMsgQueue[i].ClientWrite(outmsg);
}
outmsg.WriteUInt16(campaign.LastSaveID);
outmsg.WriteByte(campaign.CampaignID);
foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags)))
{
outmsg.WriteUInt16(campaign.GetLastUpdateIdForFlag(flag));
}
outmsg.WriteByte((byte)ClientNetObject.END_OF_MESSAGE);
outmsg.WriteBoolean(GameMain.NetLobbyScreen.CampaignCharacterDiscarded);
}
Character.Controlled?.ClientWriteInput(segmentTable, outmsg);
GameMain.GameScreen.Cam?.ClientWrite(segmentTable, outmsg);
EntityEventManager.Write(segmentTable, outmsg, ClientPeer?.ServerConnection);
chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID));
for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++)
{
if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5)
{
//not enough room in this packet
break;
}
chatMsgQueue[i].ClientWrite(segmentTable, outmsg);
}
}
if (outmsg.LengthBytes > MsgConstants.MTU)
{
@@ -2608,7 +2585,6 @@ namespace Barotrauma.Networking
IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ClientPacketHeader.UPDATE_CHARACTERINFO);
WriteCharacterInfo(msg, newName);
msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE);
ClientPeer?.Send(msg, DeliveryMethod.Reliable);
}
@@ -2648,9 +2624,11 @@ namespace Barotrauma.Networking
IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ClientPacketHeader.UPDATE_LOBBY);
msg.WriteByte((byte)ClientNetObject.VOTE);
Voting.ClientWrite(msg, voteType, data);
msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE);
using (var segmentTable = SegmentTableWriter<ClientNetSegment>.StartWriting(msg))
{
segmentTable.StartNewSegment(ClientNetSegment.Vote);
Voting.ClientWrite(msg, voteType, data);
}
ClientPeer.Send(msg, DeliveryMethod.Reliable);
}
@@ -2763,7 +2741,6 @@ namespace Barotrauma.Networking
msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND);
msg.WriteUInt16((UInt16)ClientPermissions.ManageCampaign);
campaign.ClientWrite(msg);
msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE);
ClientPeer.Send(msg, DeliveryMethod.Reliable);
}
@@ -2812,7 +2789,6 @@ namespace Barotrauma.Networking
msg.WriteUInt16((UInt16)ClientPermissions.SelectSub);
msg.WriteBoolean(isShuttle); msg.WritePadBits();
msg.WriteString(sub.MD5Hash.StringRepresentation);
msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE);
ClientPeer.Send(msg, DeliveryMethod.Reliable);
}
@@ -2832,7 +2808,6 @@ namespace Barotrauma.Networking
msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND);
msg.WriteUInt16((UInt16)ClientPermissions.SelectMode);
msg.WriteUInt16((UInt16)modeIndex);
msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE);
ClientPeer.Send(msg, DeliveryMethod.Reliable);
}
@@ -3538,6 +3513,23 @@ namespace Barotrauma.Networking
eventErrorWritten = true;
}
private static void AppendExceptionInfo(ref string errorMsg, Exception e)
{
if (!errorMsg.EndsWith("\n")) { errorMsg += "\n"; }
errorMsg += e.Message + "\n";
var innermostException = e.GetInnermost();
if (innermostException != e)
{
// If available, only append the stacktrace of the innermost exception,
// because that's the most important one to fix
errorMsg += "Inner exception: " + innermostException.Message + "\n" + innermostException.StackTrace.CleanupStackTrace();
}
else
{
errorMsg += e.StackTrace.CleanupStackTrace();
}
}
#if DEBUG
public void ForceTimeOut()
{
@@ -66,7 +66,7 @@ namespace Barotrauma.Networking
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;
@@ -103,7 +103,7 @@ namespace Barotrauma.Networking
eventLastSent[entityEvent.ID] = (float)Lidgren.Network.NetTime.Now;
}
msg.WriteByte((byte)ClientNetObject.ENTITY_STATE);
segmentTable.StartNewSegment(ClientNetSegment.EntityState);
Write(msg, eventsToSync, out _);
}
@@ -112,11 +112,11 @@ namespace Barotrauma.Networking
/// <summary>
/// Read the events from the message, ignoring ones we've already received. Returns false if reading the events fails.
/// </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;
if (type == ServerNetObject.ENTITY_EVENT_INITIAL)
if (type == ServerNetSegment.EntityEventInitial)
{
unreceivedEntityEventCount = msg.ReadUInt16();
firstNewID = msg.ReadUInt16();
@@ -218,43 +218,20 @@ namespace Barotrauma.Networking
Microsoft.Xna.Framework.Color.Green);
}
lastReceivedID++;
try
ReadEvent(msg, entity, sendingTime);
msg.ReadPadBits();
if (msg.BitPosition != msgPosition + msgLength * 8)
{
ReadEvent(msg, entity, sendingTime);
msg.ReadPadBits();
var prevEntity = entities.Count >= 2 ? entities[entities.Count - 2] : null;
ushort prevId = prevEntity is Entity p ? p.ID : (ushort)0;
string errorMsg = $"Message byte position incorrect after reading an event for the entity \"{entity}\" (ID {(entity is Entity e ? e.ID : 0)}). "
+$"The previous entity was \"{prevEntity}\" (ID {prevId}) "
+$"Read {msg.BitPosition - msgPosition} bits, expected message length was {msgLength * 8} bits.";
if (msg.BitPosition != msgPosition + msgLength * 8)
{
var prevEntity = entities.Count >= 2 ? entities[entities.Count - 2] : null;
ushort prevId = prevEntity is Entity p ? p.ID : (ushort)0;
string errorMsg = $"Message byte position incorrect after reading an event for the entity \"{entity}\" (ID {(entity is Entity e ? e.ID : 0)}). "
+$"The previous entity was \"{prevEntity}\" (ID {prevId}) "
+$"Read {msg.BitPosition - msgPosition} bits, expected message length was {msgLength * 8} bits.";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:BitPosMismatch", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
//TODO: force the BitPosition to correct place? Having some entity in a potentially incorrect state is not as bad as a desync kick
//msg.BitPosition = (int)(msgPosition + msgLength * 8);
}
}
catch (Exception e)
{
string errorMsg = $"Failed to read event {thisEventID} for entity \"{entity}\"" +
$"{(entity is Entity { ID: var entityId } ? $", id {entityId}" : "")} ";
DebugConsole.ThrowError(errorMsg, e);
errorMsg += $"({e.Message})! (MidRoundSyncing: {thisClient.MidRoundSyncing})\n{e.StackTrace.CleanupStackTrace()}";
errorMsg += "\nPrevious entities:";
for (int j = entities.Count - 2; j >= 0; j--)
{
errorMsg += "\n" + (entities[j] == null ? "NULL" : entities[j].ToString());
}
GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:ReadFailed" + entity.ToString(),
GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
msg.BitPosition = (int)(msgPosition + msgLength * 8);
GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:BitPosMismatch", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw new Exception(errorMsg);
}
}
}
@@ -4,9 +4,9 @@ namespace Barotrauma.Networking
{
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.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).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 newFile = ModProject.File.FromPath<CharacterFile>(configFilePath);
modProject.AddFile(newFile);
modProject.Save(contentPackage.Path);
contentPackage = ContentPackageManager.ReloadContentPackage(contentPackage);
var reloadResult = ContentPackageManager.ReloadContentPackage(contentPackage);
if (!reloadResult.TryUnwrapSuccess(out var newPackage))
{
throw new Exception($"Failed to reload package",
reloadResult.TryUnwrapFailure(out var exception) ? exception : null);
}
contentPackage = newPackage;
DebugConsole.NewMessage(GetCharacterEditorTranslation("ContentPackageSaved").Replace("[path]", contentPackage.Path));
@@ -1,6 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.IO;
@@ -40,6 +41,13 @@ namespace Barotrauma
}
}
[DoesNotReturn]
private static void LogAndThrowException(string errorMsg, string analyticsId)
{
GameAnalyticsManager.AddErrorEventOnce(analyticsId, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw new InvalidOperationException(errorMsg);
}
public override void Select()
{
base.Select();
@@ -74,20 +82,11 @@ namespace Barotrauma
}
};
if (!GameMain.Client.IsServerOwner)
if (!GameMain.Client.IsServerOwner && GameMain.Client.ClientPeer.ServerContentPackages.Length == 0)
{
if (GameMain.Client.ClientPeer.ServerContentPackages.Length == 0)
{
string errorMsg = $"Error in ModDownloadScreen: the list of mods the server has enabled was empty. Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}";
GameAnalyticsManager.AddErrorEventOnce("ModDownloadScreen.Select:NoContentPackages", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw new InvalidOperationException(errorMsg);
}
if (GameMain.Client.ClientPeer.ServerContentPackages.None(p => p.CorePackage != null))
{
string errorMsg = $"Error in ModDownloadScreen: no core packages in the list of mods the server has enabled. Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}";
GameAnalyticsManager.AddErrorEventOnce("ModDownloadScreen.Select:NoCorePackage", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw new InvalidOperationException(errorMsg);
}
LogAndThrowException("Error in ModDownloadScreen: the list of mods the server has enabled was empty. "
+$"Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}",
analyticsId: "ModDownloadScreen.Select:NoContentPackages");
}
var missingPackages = GameMain.Client.ClientPeer.ServerContentPackages
@@ -96,11 +95,18 @@ namespace Barotrauma
{
if (!GameMain.Client.IsServerOwner)
{
var corePackage = GameMain.Client.ClientPeer.ServerContentPackages
.Select(p => p.CorePackage)
.OfType<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.SetCore(
GameMain.Client.ClientPeer.ServerContentPackages
.Select(p => p.CorePackage)
.OfType<CorePackage>().First());
ContentPackageManager.EnabledPackages.SetCore(corePackage);
List<RegularPackage> regularPackages =
GameMain.Client.ClientPeer.ServerContentPackages
.Select(p => p.RegularPackage)
@@ -113,6 +119,15 @@ namespace Barotrauma
return;
}
if (missingPackages.FirstOrDefault(p => p.IsVanilla) is { } mismatchedVanilla)
{
LogAndThrowException("Error in ModDownloadScreen: mismatched Vanilla package: "
+$"local hash is {ContentPackageManager.VanillaCorePackage?.Hash.StringRepresentation ?? "[NULL]"}, "
+$"remote hash is {mismatchedVanilla.Hash.StringRepresentation}. "
+$"Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}",
analyticsId: "ModDownloadScreen.Select:MismatchedVanilla");
}
GUIMessageBox msgBox = new GUIMessageBox(
TextManager.Get("ModDownloadTitle"),
"",
@@ -291,14 +306,23 @@ namespace Barotrauma
var serverPackages = GameMain.Client.ClientPeer.ServerContentPackages;
CorePackage corePackage
= downloadedPackages.FirstOrDefault(p => p is CorePackage) as CorePackage
?? serverPackages.FirstOrDefault(p => p.CorePackage != null)
?.CorePackage
?? serverPackages.FirstOrDefault(p => p.CorePackage != null)?.CorePackage
?? throw new Exception($"Failed to find core package to enable");
List<RegularPackage> regularPackages = new List<RegularPackage>();
foreach (var p in serverPackages)
{
if (p.CorePackage != null) { continue; }
if (p.CorePackage != null)
{
// This package is one of our installed core packages
continue;
}
if (corePackage.Hash.Equals(p.Hash))
{
// This package is the core package we downloaded from the server
continue;
}
RegularPackage? matchingPackage =
p.RegularPackage ?? downloadedPackages.FirstOrDefault(d => d is RegularPackage && d.Hash.Equals(p.Hash)) as RegularPackage;
if (matchingPackage is null)
@@ -355,9 +379,13 @@ namespace Barotrauma
string dir = path.RemoveFromEnd(ModReceiver.Extension, StringComparison.OrdinalIgnoreCase);
SaveUtil.DecompressToDirectory(path, dir, file => { });
ContentPackage newPackage
= ContentPackage.TryLoad($"{dir}/{ContentPackage.FileListFileName}")
?? throw new Exception($"Failed to load downloaded mod \"{currentDownload.Name}\"");
var result = ContentPackage.TryLoad(Path.Combine(dir, ContentPackage.FileListFileName));
if (!result.TryUnwrapSuccess(out var newPackage))
{
throw new Exception($"Failed to load downloaded mod \"{currentDownload.Name}\"",
result.TryUnwrapFailure(out var exception) ? exception : null);
}
if (!currentDownload.Hash.Equals(newPackage.Hash))
{
throw new Exception($"Hash mismatch for downloaded mod \"{currentDownload.Name}\" (expected {currentDownload.Hash}, got {newPackage.Hash})");
@@ -1352,6 +1352,10 @@ namespace Barotrauma
private void AddToServerList(ServerInfo serverInfo, bool skipPing = false)
{
if (serverInfo.PlayerCount > serverInfo.MaxPlayers) { return; }
if (serverInfo.PlayerCount < 0) { return; }
if (serverInfo.MaxPlayers <= 0) { return; }
RemoveMsgFromServerList(MsgUserData.RefreshingServerList);
RemoveMsgFromServerList(MsgUserData.NoServers);
var serverFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), serverList.Content.RectTransform) { MinSize = new Point(0, 35) },
@@ -3075,11 +3075,17 @@ namespace Barotrauma
XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus));
doc.SaveSafe(filePath);
var resultPackage = ContentPackageManager.ReloadContentPackage(existingContentPackage) as RegularPackage;
if (!ContentPackageManager.EnabledPackages.Regular.Contains(resultPackage))
var result = ContentPackageManager.ReloadContentPackage(existingContentPackage);
if (!result.TryUnwrapSuccess(out var resultPackage))
{
ContentPackageManager.EnabledPackages.EnableRegular(resultPackage);
throw new Exception($"Failed to reload content package \"{existingContentPackage.Name}\"",
result.TryUnwrapFailure(out var exception) ? exception : null);
}
if (resultPackage is RegularPackage regularPackage
&& !ContentPackageManager.EnabledPackages.Regular.Contains(regularPackage))
{
ContentPackageManager.EnabledPackages.EnableRegular(regularPackage);
GameSettings.SaveCurrentConfig();
}
@@ -814,7 +814,7 @@ namespace Barotrauma
{
return "levelend".ToIdentifier();
}
if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0 &&
if (GameMain.GameSession.RoundDuration > 120.0 &&
Level.Loaded?.Type == LevelData.LevelType.LocationConnection)
{
return "start".ToIdentifier();
@@ -73,7 +73,13 @@ namespace Barotrauma.Steam
//This callback seems to take place when the item has been downloaded recently and an update
//or a redownload has taken place
Steamworks.SteamUGC.OnDownloadItemResult += (result, id) => Workshop.OnItemDownloadComplete(id);
Steamworks.SteamUGC.OnDownloadItemResult += (result, id) =>
{
if (result == Steamworks.Result.OK)
{
Workshop.OnItemDownloadComplete(id);
}
};
//Maybe I'm completely wrong! All I know is that we need to handle both!
}
@@ -177,7 +177,13 @@ namespace Barotrauma.Steam
await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, PublishStagingDir, ShouldCorrectPaths.No);
var stagingFileListPath = Path.Combine(PublishStagingDir, ContentPackage.FileListFileName);
ContentPackage tempPkg = ContentPackage.TryLoad(stagingFileListPath) ?? throw new Exception("Staging copy could not be loaded");
var result = ContentPackage.TryLoad(stagingFileListPath);
if (!result.TryUnwrapSuccess(out var tempPkg))
{
throw new Exception("Staging copy could not be loaded",
result.TryUnwrapFailure(out var exception) ? exception : null);
}
//Load filelist.xml and write the hash into it so anyone downloading this mod knows what it should be
ModProject modProject = new ModProject(tempPkg)
@@ -40,7 +40,7 @@ namespace Barotrauma.Steam
CanBeFocused = false,
UserData = p
};
if (p.Errors.Any())
if (p.FatalLoadErrors.Any())
{
CreateModErrorInfo(p, regularBox, regularBox);
regularBox.CanBeFocused = true;
@@ -49,21 +49,10 @@ namespace Barotrauma.Steam
SteamManager.Workshop.DownloadModThenEnqueueInstall(item);
}
}
TaskPool.Add("RemoveUnsubscribedItems", SteamManager.Workshop.GetPublishedItems(), t =>
SteamManager.Workshop.DeleteUnsubscribedMods(removedPackages =>
{
if (!t.TryGetResult(out ISet<Steamworks.Ugc.Item> publishedItems)) { return; }
var allRequiredInstalled = subscribedIds.Union(publishedItems.Select(it => it.Id)).ToHashSet();
bool needsRefresh = false;
foreach (var id in installedIds.Where(id2 => !allRequiredInstalled.Contains(id2)))
{
Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id);
SteamManager.Workshop.Uninstall(item);
needsRefresh = true;
}
if (needsRefresh)
if (removedPackages.Any())
{
PopulateInstalledModLists();
}
@@ -487,8 +476,9 @@ namespace Barotrauma.Steam
{
string str = modsListFilter.Text;
enabledRegularModsList.Content.Children.Concat(disabledRegularModsList.Content.Children)
.ForEach(c => c.Visible = c.UserData is not ContentPackage p
|| ModNameMatches(p, str) && ModMatchesTickboxes(p, c));
.ForEach(c
=> c.Visible = c.UserData is not ContentPackage p
|| ModNameMatches(p, str) && ModMatchesTickboxes(p, c));
}
private bool ModMatchesTickboxes(ContentPackage p, GUIComponent guiItem)
@@ -550,7 +540,20 @@ namespace Barotrauma.Steam
(p) => p.Name,
ContentPackageManager.CorePackages.ToArray(),
ContentPackageManager.EnabledPackages.Core!,
(p) => { });
(p) =>
{
enabledCoreDropdown.ButtonTextColor =
p.HasAnyErrors
? GUIStyle.Red
: GUIStyle.TextColorNormal;
});
enabledCoreDropdown.ListBox.Content.Children
.OfType<GUITextBlock>()
.ForEach(tb =>
CreateModErrorInfo(
(tb.UserData as ContentPackage)!,
tb,
tb));
void addRegularModToList(RegularPackage mod, GUIListBox list)
{
@@ -658,10 +661,7 @@ namespace Barotrauma.Steam
{
CanBeFocused = false
};
if (mod.Errors.Any())
{
CreateModErrorInfo(mod, modFrame, modName);
}
CreateModErrorInfo(mod, modFrame, modName);
if (ContentPackageManager.LocalPackages.Contains(mod))
{
var editButton = new GUIButton(new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "",
@@ -165,6 +165,10 @@ namespace Barotrauma.Steam
.Select(c => c.UserData as RegularPackage).OfType<RegularPackage>().ToArray());
PopulateInstalledModLists(forceRefreshEnabled: true, refreshDisabled: true);
ContentPackageManager.LogEnabledRegularPackageErrors();
enabledCoreDropdown.ButtonTextColor =
EnabledCorePackage.HasAnyErrors
? GUIStyle.Red
: GUIStyle.TextColorNormal;
}
}
}
@@ -294,10 +294,11 @@ namespace Barotrauma.Steam
{
//Reload the package to force hash recalculation
string packageName = localPackage.Name;
localPackage = ContentPackageManager.ReloadContentPackage(localPackage);
if (localPackage is null)
var result = ContentPackageManager.ReloadContentPackage(localPackage);
if (!result.TryUnwrapSuccess(out localPackage))
{
throw new Exception($"\"{packageName}\" was removed upon reload");
throw new Exception($"\"{packageName}\" was removed upon reload",
result.TryUnwrapFailure(out var exception) ? exception : null);
}
//Set up the Ugc.Editor object that we'll need to publish
@@ -133,20 +133,29 @@ namespace Barotrauma.Steam
return searchBox;
}
protected void CreateModErrorInfo(ContentPackage mod, GUIComponent uiElement, GUITextBlock nameText)
protected static void CreateModErrorInfo(ContentPackage mod, GUIComponent uiElement, GUITextBlock nameText)
{
if (mod.Errors.Any())
uiElement.ToolTip = "";
if (mod.FatalLoadErrors.Any())
{
const int maxErrorsToShow = 5;
nameText.TextColor = GUIStyle.Red;
uiElement.ToolTip =
TextManager.GetWithVariable("contentpackagehaserrors", "[packagename]", mod.Name)
+ '\n' + string.Join('\n', mod.Errors.Take(maxErrorsToShow).Select(e => e.Message));
if (mod.Errors.Count() > maxErrorsToShow)
TextManager.GetWithVariable("ContentPackageHasFatalErrors", "[packagename]", mod.Name)
+ '\n' + string.Join('\n', mod.FatalLoadErrors.Take(maxErrorsToShow).Select(e => e.Message));
if (mod.FatalLoadErrors.Length > maxErrorsToShow)
{
uiElement.ToolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (mod.Errors.Count() - maxErrorsToShow).ToString());
uiElement.ToolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (mod.FatalLoadErrors.Count() - maxErrorsToShow).ToString());
}
}
if (mod.EnableError.IsSome())
{
nameText.TextColor = GUIStyle.Red;
if (!uiElement.ToolTip.IsNullOrWhiteSpace()) { uiElement.ToolTip += "\n"; }
uiElement.ToolTip += TextManager.GetWithVariable(
"ContentPackageEnableError", "[packagename]", mod.Name);
}
}
}
}
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>0.20.7.0</Version>
<Version>0.20.8.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>0.20.7.0</Version>
<Version>0.20.8.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>0.20.7.0</Version>
<Version>0.20.8.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>0.20.7.0</Version>
<Version>0.20.8.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>0.20.7.0</Version>
<Version>0.20.8.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>
@@ -82,54 +82,50 @@ namespace Barotrauma
}
//dequeue messages
lock (queuedMessages)
if (queuedMessages.Count > 0)
{
if (queuedMessages.Count > 0)
if (!Console.IsOutputRedirected)
{
if (!Console.IsOutputRedirected)
Console.CursorLeft = 0;
}
while (queuedMessages.TryDequeue(out var msg))
{
Messages.Add(msg);
if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging)
{
Console.CursorLeft = 0;
}
while (queuedMessages.Count > 0)
{
ColoredText msg = queuedMessages.Dequeue();
Messages.Add(msg);
if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging)
unsavedMessages.Add(msg);
if (unsavedMessages.Count >= messagesPerFile)
{
unsavedMessages.Add(msg);
if (unsavedMessages.Count >= messagesPerFile)
{
SaveLogs();
unsavedMessages.Clear();
}
SaveLogs();
unsavedMessages.Clear();
}
string msgTxt = msg.Text;
if (msg.IsCommand) commandMemory.Add(msgTxt);
if(!Console.IsOutputRedirected)
{
int paddingLen = consoleWidth - (msg.Text.Length % consoleWidth) - 1;
msgTxt += new string(' ', paddingLen > 0 ? paddingLen : 0);
Console.ForegroundColor = XnaToConsoleColor.Convert(msg.Color);
}
Console.WriteLine(msgTxt);
if (sw.ElapsedMilliseconds >= maxTime) { break; }
}
string msgTxt = msg.Text;
if (msg.IsCommand) commandMemory.Add(msgTxt);
if(!Console.IsOutputRedirected)
{
RewriteInputToCommandLine(input);
int paddingLen = consoleWidth - (msg.Text.Length % consoleWidth) - 1;
msgTxt += new string(' ', paddingLen > 0 ? paddingLen : 0);
Console.ForegroundColor = XnaToConsoleColor.Convert(msg.Color);
}
Console.WriteLine(msgTxt);
if (sw.ElapsedMilliseconds >= maxTime) { break; }
}
if (Messages.Count > MaxMessages)
if (!Console.IsOutputRedirected)
{
Messages.RemoveRange(0, Messages.Count - MaxMessages);
RewriteInputToCommandLine(input);
}
}
if (Messages.Count > MaxMessages)
{
Messages.RemoveRange(0, Messages.Count - MaxMessages);
}
// No good way to display input when console output is redirected, and can't read from redirected input using KeyAvailable.
if(!Console.IsOutputRedirected && !Console.IsInputRedirected)
@@ -272,26 +268,22 @@ namespace Barotrauma
public static void Clear()
{
lock (queuedMessages)
while (queuedMessages.TryDequeue(out var msg))
{
while (queuedMessages.Count > 0)
Messages.Add(msg);
if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging)
{
var msg = queuedMessages.Dequeue();
Messages.Add(msg);
if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging)
unsavedMessages.Add(msg);
if (unsavedMessages.Count >= messagesPerFile)
{
unsavedMessages.Add(msg);
if (unsavedMessages.Count >= messagesPerFile)
{
SaveLogs();
unsavedMessages.Clear();
}
SaveLogs();
unsavedMessages.Clear();
}
}
if (Messages.Count > MaxMessages)
{
Messages.RemoveRange(0, Messages.Count - MaxMessages);
}
}
if (Messages.Count > MaxMessages)
{
Messages.RemoveRange(0, Messages.Count - MaxMessages);
}
}
@@ -162,13 +162,13 @@ namespace Barotrauma
}
else
{
name = doc.Root.GetAttributeString("name", "Server");
port = doc.Root.GetAttributeInt("port", NetConfig.DefaultPort);
queryPort = doc.Root.GetAttributeInt("queryport", NetConfig.DefaultQueryPort);
publiclyVisible = doc.Root.GetAttributeBool("public", false);
name = doc.Root.GetAttributeString(nameof(ServerSettings.Name), "Server");
port = doc.Root.GetAttributeInt(nameof(ServerSettings.Port), NetConfig.DefaultPort);
queryPort = doc.Root.GetAttributeInt(nameof(ServerSettings.QueryPort), NetConfig.DefaultQueryPort);
publiclyVisible = doc.Root.GetAttributeBool(nameof(ServerSettings.IsPublic), false);
enableUpnp = doc.Root.GetAttributeBool(nameof(ServerSettings.EnableUPnP), false);
maxPlayers = doc.Root.GetAttributeInt(nameof(ServerSettings.MaxPlayers), 10);
password = doc.Root.GetAttributeString("password", "");
enableUpnp = doc.Root.GetAttributeBool("enableupnp", false);
maxPlayers = doc.Root.GetAttributeInt("maxplayers", 10);
ownerKey = Option<int>.None();
}
@@ -361,7 +361,7 @@ namespace Barotrauma
if (prevUpdateRates.Count >= 10)
{
int avgUpdateRate = (int)prevUpdateRates.Average();
if (avgUpdateRate < Timing.FixedUpdateRate * 0.98 && GameSession != null && Timing.TotalTime > GameSession.RoundStartTime + 1.0)
if (avgUpdateRate < Timing.FixedUpdateRate * 0.98 && GameSession != null && GameSession.RoundDuration > 1.0)
{
DebugConsole.AddWarning($"Running slowly ({avgUpdateRate} updates/s)!");
if (Server != null)
@@ -335,7 +335,7 @@ namespace Barotrauma
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);
if (success)
@@ -200,9 +200,9 @@ namespace Barotrauma.Networking
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.WriteRangedInteger((int)Type, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1);
msg.WriteByte((byte)ChangeType);
@@ -1028,12 +1028,11 @@ namespace Barotrauma.Networking
return;
}
ClientNetObject objHeader;
while ((objHeader = (ClientNetObject)inc.ReadByte()) != ClientNetObject.END_OF_MESSAGE)
SegmentTableReader<ClientNetSegment>.Read(inc, (segment, inc) =>
{
switch (objHeader)
switch (segment)
{
case ClientNetObject.SYNC_IDS:
case ClientNetSegment.SyncIds:
//TODO: might want to use a clever class for this
c.LastRecvLobbyUpdate = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvLobbyUpdate, GameMain.NetLobbyScreen.LastUpdateID);
if (c.HasPermission(ClientPermissions.ManageSettings) &&
@@ -1072,19 +1071,21 @@ namespace Barotrauma.Networking
}
}
break;
case ClientNetObject.CHAT_MESSAGE:
case ClientNetSegment.ChatMessage:
ChatMessage.ServerRead(inc, c);
break;
case ClientNetObject.VOTE:
case ClientNetSegment.Vote:
Voting.ServerRead(inc, c);
break;
default:
return;
return SegmentTableReader<ClientNetSegment>.BreakSegmentReading.Yes;
}
//don't read further messages if the client has been disconnected (kicked due to spam for example)
if (!connectedClients.Contains(c)) break;
}
return connectedClients.Contains(c)
? SegmentTableReader<ClientNetSegment>.BreakSegmentReading.No
: SegmentTableReader<ClientNetSegment>.BreakSegmentReading.Yes;
});
}
private void ClientReadIngame(IReadMessage inc)
@@ -1109,13 +1110,12 @@ namespace Barotrauma.Networking
}
}
ClientNetObject objHeader;
while ((objHeader = (ClientNetObject)inc.ReadByte()) != ClientNetObject.END_OF_MESSAGE)
SegmentTableReader<ClientNetSegment>.Read(inc, (segment, inc) =>
{
switch (objHeader)
switch (segment)
{
case ClientNetObject.SYNC_IDS:
//TODO: might want to use a clever class for this
case ClientNetSegment.SyncIds:
//TODO: switch this to INetSerializableStruct
UInt16 lastRecvChatMsgID = inc.ReadUInt16();
UInt16 lastRecvEntityEventID = inc.ReadUInt16();
@@ -1218,10 +1218,10 @@ namespace Barotrauma.Networking
}
break;
case ClientNetObject.CHAT_MESSAGE:
case ClientNetSegment.ChatMessage:
ChatMessage.ServerRead(inc, c);
break;
case ClientNetObject.CHARACTER_INPUT:
case ClientNetSegment.CharacterInput:
if (c.Character != null)
{
c.Character.ServerReadInput(inc, c);
@@ -1231,22 +1231,24 @@ namespace Barotrauma.Networking
DebugConsole.AddWarning($"Received character inputs from a client who's not controlling a character ({c.Name}).");
}
break;
case ClientNetObject.ENTITY_STATE:
case ClientNetSegment.EntityState:
entityEventManager.Read(inc, c);
break;
case ClientNetObject.VOTE:
case ClientNetSegment.Vote:
Voting.ServerRead(inc, c);
break;
case ClientNetObject.SPECTATING_POS:
case ClientNetSegment.SpectatingPos:
c.SpectatePos = new Vector2(inc.ReadSingle(), inc.ReadSingle());
break;
default:
return;
return SegmentTableReader<ClientNetSegment>.BreakSegmentReading.Yes;
}
//don't read further messages if the client has been disconnected (kicked due to spam for example)
if (!connectedClients.Contains(c)) { break; }
}
return connectedClients.Contains(c)
? SegmentTableReader<ClientNetSegment>.BreakSegmentReading.No
: SegmentTableReader<ClientNetSegment>.BreakSegmentReading.Yes;
});
}
private void ReadCrewMessage(IReadMessage inc, Client sender)
@@ -1701,78 +1703,78 @@ namespace Barotrauma.Networking
IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME);
outmsg.WriteSingle((float)NetTime.Now);
outmsg.WriteByte((byte)ServerNetObject.SYNC_IDS);
outmsg.WriteUInt16(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server
outmsg.WriteUInt16(c.LastSentEntityEventID);
if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode)
using (var segmentTable = SegmentTableWriter<ServerNetSegment>.StartWriting(outmsg))
{
outmsg.WriteBoolean(true);
outmsg.WritePadBits();
campaign.ServerWrite(outmsg, c);
}
else
{
outmsg.WriteBoolean(false);
outmsg.WritePadBits();
}
segmentTable.StartNewSegment(ServerNetSegment.SyncIds);
outmsg.WriteUInt16(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server
outmsg.WriteUInt16(c.LastSentEntityEventID);
int clientListBytes = outmsg.LengthBytes;
WriteClientList(c, outmsg);
clientListBytes = outmsg.LengthBytes - clientListBytes;
int chatMessageBytes = outmsg.LengthBytes;
WriteChatMessages(outmsg, c);
chatMessageBytes = outmsg.LengthBytes - chatMessageBytes;
//write as many position updates as the message can fit (only after midround syncing is done)
int positionUpdateBytes = outmsg.LengthBytes;
while (!c.NeedsMidRoundSync && c.PendingPositionUpdates.Count > 0)
{
var entity = c.PendingPositionUpdates.Peek();
if (!(entity is IServerPositionSync entityPositionSync) ||
entity.Removed ||
(entity is Item item && float.IsInfinity(item.PositionUpdateInterval)))
if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode)
{
outmsg.WriteBoolean(true);
outmsg.WritePadBits();
campaign.ServerWrite(outmsg, c);
}
else
{
outmsg.WriteBoolean(false);
outmsg.WritePadBits();
}
int clientListBytes = outmsg.LengthBytes;
WriteClientList(segmentTable, c, outmsg);
clientListBytes = outmsg.LengthBytes - clientListBytes;
int chatMessageBytes = outmsg.LengthBytes;
WriteChatMessages(segmentTable, outmsg, c);
chatMessageBytes = outmsg.LengthBytes - chatMessageBytes;
//write as many position updates as the message can fit (only after midround syncing is done)
int positionUpdateBytes = outmsg.LengthBytes;
while (!c.NeedsMidRoundSync && c.PendingPositionUpdates.Count > 0)
{
var entity = c.PendingPositionUpdates.Peek();
if (!(entity is IServerPositionSync entityPositionSync) ||
entity.Removed ||
(entity is Item item && float.IsInfinity(item.PositionUpdateInterval)))
{
c.PendingPositionUpdates.Dequeue();
continue;
}
IWriteMessage tempBuffer = new ReadWriteMessage();
tempBuffer.WriteBoolean(entity is Item); tempBuffer.WritePadBits();
tempBuffer.WriteUInt32(entity is MapEntity me ? me.Prefab.UintIdentifier : (UInt32)0);
entityPositionSync.ServerWritePosition(tempBuffer, c);
//no more room in this packet
if (outmsg.LengthBytes + tempBuffer.LengthBytes > MsgConstants.MTU - 100)
{
break;
}
segmentTable.StartNewSegment(ServerNetSegment.EntityPosition);
outmsg.WritePadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly
outmsg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes);
outmsg.WritePadBits();
c.PositionUpdateLastSent[entity] = (float)NetTime.Now;
c.PendingPositionUpdates.Dequeue();
continue;
}
positionUpdateBytes = outmsg.LengthBytes - positionUpdateBytes;
IWriteMessage tempBuffer = new ReadWriteMessage();
tempBuffer.WriteBoolean(entity is Item); tempBuffer.WritePadBits();
tempBuffer.WriteUInt32(entity is MapEntity me ? me.Prefab.UintIdentifier : (UInt32)0);
entityPositionSync.ServerWritePosition(tempBuffer, c);
//no more room in this packet
if (outmsg.LengthBytes + tempBuffer.LengthBytes > MsgConstants.MTU - 100)
if (outmsg.LengthBytes > MsgConstants.MTU)
{
break;
string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n";
errorMsg +=
" Client list size: " + clientListBytes + " bytes\n" +
" Chat message size: " + chatMessageBytes + " bytes\n" +
" Position update size: " + positionUpdateBytes + " bytes\n\n";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
}
outmsg.WriteByte((byte)ServerNetObject.ENTITY_POSITION);
outmsg.WritePadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly
outmsg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes);
outmsg.WritePadBits();
c.PositionUpdateLastSent[entity] = (float)NetTime.Now;
c.PendingPositionUpdates.Dequeue();
}
positionUpdateBytes = outmsg.LengthBytes - positionUpdateBytes;
outmsg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE);
if (outmsg.LengthBytes > MsgConstants.MTU)
{
string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n";
errorMsg +=
" Client list size: " + clientListBytes + " bytes\n" +
" Chat message size: " + chatMessageBytes + " bytes\n" +
" Position update size: " + positionUpdateBytes + " bytes\n\n";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
}
serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable);
@@ -1785,46 +1787,50 @@ namespace Barotrauma.Networking
outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME);
outmsg.WriteSingle((float)Lidgren.Network.NetTime.Now);
int eventManagerBytes = outmsg.LengthBytes;
entityEventManager.Write(c, outmsg, out List<NetEntityEvent> sentEvents);
eventManagerBytes = outmsg.LengthBytes - eventManagerBytes;
if (sentEvents.Count == 0)
using (var segmentTable = SegmentTableWriter<ServerNetSegment>.StartWriting(outmsg))
{
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 (outmsg.LengthBytes > MsgConstants.MTU)
{
string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n";
errorMsg +=
" Event size: " + eventManagerBytes + " bytes\n";
if (sentEvents != null && sentEvents.Count > 0)
if (sentEvents.Count == 0)
{
errorMsg += "Sent events: \n";
foreach (var entityEvent in sentEvents)
{
errorMsg += " - " + (entityEvent.Entity?.ToString() ?? "null") + "\n";
}
break;
}
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame2:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
if (outmsg.LengthBytes > MsgConstants.MTU)
{
string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " +
MsgConstants.MTU + ")\n";
errorMsg +=
" Event size: " + eventManagerBytes + " bytes\n";
if (sentEvents != null && sentEvents.Count > 0)
{
errorMsg += "Sent events: \n";
foreach (var entityEvent in sentEvents)
{
errorMsg += " - " + (entityEvent.Entity?.ToString() ?? "null") + "\n";
}
}
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"GameServer.ClientWriteIngame2:PacketSizeExceeded" + outmsg.LengthBytes,
GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
}
}
serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable);
}
}
private void WriteClientList(Client c, IWriteMessage outmsg)
private void WriteClientList(in SegmentTableWriter<ServerNetSegment> segmentTable, Client c, IWriteMessage outmsg)
{
bool hasChanged = NetIdUtils.IdMoreRecent(LastClientListUpdateID, c.LastRecvClientListUpdate);
if (!hasChanged) { return; }
outmsg.WriteByte((byte)ServerNetObject.CLIENT_LIST);
segmentTable.StartNewSegment(ServerNetSegment.ClientList);
outmsg.WriteUInt16(LastClientListUpdateID);
outmsg.WriteByte((byte)connectedClients.Count);
@@ -1861,133 +1867,135 @@ namespace Barotrauma.Networking
IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_LOBBY);
outmsg.WriteByte((byte)ServerNetObject.SYNC_IDS);
int settingsBytes = outmsg.LengthBytes;
int initialUpdateBytes = 0;
if (ServerSettings.UnsentFlags() != ServerSettings.NetFlags.None)
bool messageTooLarge;
using (var segmentTable = SegmentTableWriter<ServerNetSegment>.StartWriting(outmsg))
{
GameMain.NetLobbyScreen.LastUpdateID++;
}
IWriteMessage settingsBuf = null;
if (NetIdUtils.IdMoreRecent(GameMain.NetLobbyScreen.LastUpdateID, c.LastRecvLobbyUpdate))
{
outmsg.WriteBoolean(true);
outmsg.WritePadBits();
segmentTable.StartNewSegment(ServerNetSegment.SyncIds);
outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID);
int settingsBytes = outmsg.LengthBytes;
int initialUpdateBytes = 0;
settingsBuf = new ReadWriteMessage();
ServerSettings.ServerWrite(settingsBuf, c);
outmsg.WriteUInt16((UInt16)settingsBuf.LengthBytes);
outmsg.WriteBytes(settingsBuf.Buffer, 0, settingsBuf.LengthBytes);
outmsg.WriteBoolean(c.LastRecvLobbyUpdate < 1);
if (c.LastRecvLobbyUpdate < 1)
if (ServerSettings.UnsentFlags() != ServerSettings.NetFlags.None)
{
isInitialUpdate = true;
initialUpdateBytes = outmsg.LengthBytes;
ClientWriteInitial(c, outmsg);
initialUpdateBytes = outmsg.LengthBytes - initialUpdateBytes;
GameMain.NetLobbyScreen.LastUpdateID++;
}
outmsg.WriteString(GameMain.NetLobbyScreen.SelectedSub.Name);
outmsg.WriteString(GameMain.NetLobbyScreen.SelectedSub.MD5Hash.ToString());
outmsg.WriteBoolean(IsUsingRespawnShuttle());
var selectedShuttle = GameStarted && RespawnManager != null && RespawnManager.UsingShuttle ?
RespawnManager.RespawnShuttle.Info :
GameMain.NetLobbyScreen.SelectedShuttle;
outmsg.WriteString(selectedShuttle.Name);
outmsg.WriteString(selectedShuttle.MD5Hash.ToString());
outmsg.WriteBoolean(ServerSettings.AllowSubVoting);
outmsg.WriteBoolean(ServerSettings.AllowModeVoting);
outmsg.WriteBoolean(ServerSettings.VoiceChatEnabled);
outmsg.WriteBoolean(ServerSettings.AllowSpectating);
outmsg.WriteRangedInteger((int)ServerSettings.TraitorsEnabled, 0, 2);
outmsg.WriteRangedInteger((int)GameMain.NetLobbyScreen.MissionType, 0, (int)MissionType.All);
outmsg.WriteByte((byte)GameMain.NetLobbyScreen.SelectedModeIndex);
outmsg.WriteString(GameMain.NetLobbyScreen.LevelSeed);
outmsg.WriteSingle(ServerSettings.SelectedLevelDifficulty);
outmsg.WriteByte((byte)ServerSettings.BotCount);
outmsg.WriteBoolean(ServerSettings.BotSpawnMode == BotSpawnMode.Fill);
outmsg.WriteBoolean(ServerSettings.AutoRestart);
if (ServerSettings.AutoRestart)
IWriteMessage settingsBuf = null;
if (NetIdUtils.IdMoreRecent(GameMain.NetLobbyScreen.LastUpdateID, c.LastRecvLobbyUpdate))
{
outmsg.WriteSingle(autoRestartTimerRunning ? ServerSettings.AutoRestartTimer : 0.0f);
outmsg.WriteBoolean(true);
outmsg.WritePadBits();
outmsg.WriteUInt16(GameMain.NetLobbyScreen.LastUpdateID);
settingsBuf = new ReadWriteMessage();
ServerSettings.ServerWrite(settingsBuf, c);
outmsg.WriteUInt16((UInt16)settingsBuf.LengthBytes);
outmsg.WriteBytes(settingsBuf.Buffer, 0, settingsBuf.LengthBytes);
outmsg.WriteBoolean(c.LastRecvLobbyUpdate < 1);
if (c.LastRecvLobbyUpdate < 1)
{
isInitialUpdate = true;
initialUpdateBytes = outmsg.LengthBytes;
ClientWriteInitial(c, outmsg);
initialUpdateBytes = outmsg.LengthBytes - initialUpdateBytes;
}
outmsg.WriteString(GameMain.NetLobbyScreen.SelectedSub.Name);
outmsg.WriteString(GameMain.NetLobbyScreen.SelectedSub.MD5Hash.ToString());
outmsg.WriteBoolean(IsUsingRespawnShuttle());
var selectedShuttle = GameStarted && RespawnManager != null && RespawnManager.UsingShuttle ?
RespawnManager.RespawnShuttle.Info :
GameMain.NetLobbyScreen.SelectedShuttle;
outmsg.WriteString(selectedShuttle.Name);
outmsg.WriteString(selectedShuttle.MD5Hash.ToString());
outmsg.WriteBoolean(ServerSettings.AllowSubVoting);
outmsg.WriteBoolean(ServerSettings.AllowModeVoting);
outmsg.WriteBoolean(ServerSettings.VoiceChatEnabled);
outmsg.WriteBoolean(ServerSettings.AllowSpectating);
outmsg.WriteRangedInteger((int)ServerSettings.TraitorsEnabled, 0, 2);
outmsg.WriteRangedInteger((int)GameMain.NetLobbyScreen.MissionType, 0, (int)MissionType.All);
outmsg.WriteByte((byte)GameMain.NetLobbyScreen.SelectedModeIndex);
outmsg.WriteString(GameMain.NetLobbyScreen.LevelSeed);
outmsg.WriteSingle(ServerSettings.SelectedLevelDifficulty);
outmsg.WriteByte((byte)ServerSettings.BotCount);
outmsg.WriteBoolean(ServerSettings.BotSpawnMode == BotSpawnMode.Fill);
outmsg.WriteBoolean(ServerSettings.AutoRestart);
if (ServerSettings.AutoRestart)
{
outmsg.WriteSingle(autoRestartTimerRunning ? ServerSettings.AutoRestartTimer : 0.0f);
}
}
}
else
{
outmsg.WriteBoolean(false);
outmsg.WritePadBits();
}
settingsBytes = outmsg.LengthBytes - settingsBytes;
int campaignBytes = outmsg.LengthBytes;
var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign;
if (outmsg.LengthBytes < MsgConstants.MTU - 500 &&
campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode)
{
outmsg.WriteBoolean(true);
outmsg.WritePadBits();
campaign.ServerWrite(outmsg, c);
}
else
{
outmsg.WriteBoolean(false);
outmsg.WritePadBits();
}
campaignBytes = outmsg.LengthBytes - campaignBytes;
outmsg.WriteUInt16(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server
int clientListBytes = outmsg.LengthBytes;
if (outmsg.LengthBytes < MsgConstants.MTU - 500)
{
WriteClientList(c, outmsg);
}
clientListBytes = outmsg.LengthBytes - clientListBytes;
int chatMessageBytes = outmsg.LengthBytes;
WriteChatMessages(outmsg, c);
chatMessageBytes = outmsg.LengthBytes - chatMessageBytes;
outmsg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE);
bool messageTooLarge = outmsg.LengthBytes > MsgConstants.MTU;
if (messageTooLarge && !isInitialUpdate)
{
string warningMsg = "Maximum packet size exceeded, will send using reliable mode (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n";
warningMsg +=
" Client list size: " + clientListBytes + " bytes\n" +
" Chat message size: " + chatMessageBytes + " bytes\n" +
" Campaign size: " + campaignBytes + " bytes\n" +
" Settings size: " + settingsBytes + " bytes\n";
if (initialUpdateBytes > 0)
else
{
outmsg.WriteBoolean(false);
outmsg.WritePadBits();
}
settingsBytes = outmsg.LengthBytes - settingsBytes;
int campaignBytes = outmsg.LengthBytes;
var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign;
if (outmsg.LengthBytes < MsgConstants.MTU - 500 &&
campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode)
{
outmsg.WriteBoolean(true);
outmsg.WritePadBits();
campaign.ServerWrite(outmsg, c);
}
else
{
outmsg.WriteBoolean(false);
outmsg.WritePadBits();
}
campaignBytes = outmsg.LengthBytes - campaignBytes;
outmsg.WriteUInt16(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server
int clientListBytes = outmsg.LengthBytes;
if (outmsg.LengthBytes < MsgConstants.MTU - 500)
{
WriteClientList(segmentTable, c, outmsg);
}
clientListBytes = outmsg.LengthBytes - clientListBytes;
int chatMessageBytes = outmsg.LengthBytes;
WriteChatMessages(segmentTable, outmsg, c);
chatMessageBytes = outmsg.LengthBytes - chatMessageBytes;
messageTooLarge = outmsg.LengthBytes > MsgConstants.MTU;
if (messageTooLarge && !isInitialUpdate)
{
string warningMsg = "Maximum packet size exceeded, will send using reliable mode (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n";
warningMsg +=
" Initial update size: " + settingsBuf.LengthBytes + " bytes\n";
}
if (settingsBuf != null)
{
warningMsg +=
" Settings buffer size: " + settingsBuf.LengthBytes + " bytes\n";
}
" Client list size: " + clientListBytes + " bytes\n" +
" Chat message size: " + chatMessageBytes + " bytes\n" +
" Campaign size: " + campaignBytes + " bytes\n" +
" Settings size: " + settingsBytes + " bytes\n";
if (initialUpdateBytes > 0)
{
warningMsg +=
" Initial update size: " + initialUpdateBytes + " bytes\n";
}
if (settingsBuf != null)
{
warningMsg +=
" Settings buffer size: " + settingsBuf.LengthBytes + " bytes\n";
}
#if DEBUG || UNSTABLE
DebugConsole.ThrowError(warningMsg);
DebugConsole.ThrowError(warningMsg);
#else
if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.AddWarning(warningMsg); }
if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.AddWarning(warningMsg); }
#endif
GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:ClientWriteLobby" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg);
GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:ClientWriteLobby" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg);
}
}
if (isInitialUpdate || messageTooLarge)
@@ -2014,7 +2022,7 @@ namespace Barotrauma.Networking
}
}
private void WriteChatMessages(IWriteMessage outmsg, Client c)
private void WriteChatMessages(in SegmentTableWriter<ServerNetSegment> segmentTable, IWriteMessage outmsg, Client c)
{
c.ChatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, c.LastRecvChatMsgID));
for (int i = 0; i < c.ChatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++)
@@ -2024,7 +2032,7 @@ namespace Barotrauma.Networking
//not enough room in this packet
return;
}
c.ChatMsgQueue[i].ServerWrite(outmsg, c);
c.ChatMsgQueue[i].ServerWrite(segmentTable, outmsg, c);
}
}
@@ -3327,9 +3335,11 @@ namespace Barotrauma.Networking
IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ServerPacketHeader.UPDATE_LOBBY);
msg.WriteByte((byte)ServerNetObject.VOTE);
Voting.ServerWrite(msg);
msg.WriteByte((byte)ServerNetObject.END_OF_MESSAGE);
using (var segmentTable = SegmentTableWriter<ServerNetSegment>.StartWriting(msg))
{
segmentTable.StartNewSegment(ServerNetSegment.Vote);
Voting.ServerWrite(msg);
}
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
// keep at least one event in the list (lastSentToAll == e.ID) so we can use it to keep track of the latest ID
// and events less than 15 seconds old to give disconnected clients a bit of time to reconnect without getting desynced
if (Timing.TotalTime > GameMain.GameSession.RoundStartTime + NetConfig.RoundStartSyncDuration)
if (GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration)
{
events.RemoveAll(e =>
(NetIdUtils.IdMoreRecent(lastSentToAll, e.ID) || !inGameClientsPresent) &&
@@ -217,7 +217,7 @@ namespace Barotrauma.Networking
if (Timing.TotalTime - lastWarningTime > 5.0 &&
Timing.TotalTime - lastSentToAnyoneTime > 10.0 &&
Timing.TotalTime > GameMain.GameSession.RoundStartTime + NetConfig.RoundStartSyncDuration)
GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration)
{
lastWarningTime = Timing.TotalTime;
GameServer.Log("WARNING: ServerEntityEventManager is lagging behind! Last sent id: " + lastSentToAnyone.ToString() + ", latest create id: " + ID.ToString(), ServerLog.MessageType.ServerMessage);
@@ -229,7 +229,7 @@ namespace Barotrauma.Networking
ServerEntityEvent firstEventToResend = events.Find(e => e.ID == (ushort)(lastSentToAll + 1));
if (firstEventToResend != null &&
Timing.TotalTime > GameMain.GameSession.RoundStartTime + NetConfig.RoundStartSyncDuration &&
GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration &&
((lastSentToAnyoneTime - firstEventToResend.CreateTime) > NetConfig.OldReceivedEventKickTime || (Timing.TotalTime - firstEventToResend.CreateTime) > NetConfig.OldEventKickTime))
{
// This event is 10 seconds older than the last one we've successfully sent,
@@ -295,15 +295,15 @@ namespace Barotrauma.Networking
/// <summary>
/// Writes all the events that the client hasn't received yet into the outgoing message
/// </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>
/// Writes all the events that the client hasn't received yet into the outgoing message
/// </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);
@@ -315,7 +315,7 @@ namespace Barotrauma.Networking
//too many events for one packet
//(normal right after a round has just started, don't show a warning if it's been less than 10 seconds)
if (eventsToSync.Count > 200 && GameMain.GameSession != null && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 10.0)
if (eventsToSync.Count > 200 && GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 10.0)
{
if (eventsToSync.Count > 200 && !client.NeedsMidRoundSync && Timing.TotalTime > lastEventCountHighWarning + 2.0)
{
@@ -345,7 +345,7 @@ namespace Barotrauma.Networking
if (client.NeedsMidRoundSync)
{
msg.WriteByte((byte)ServerNetObject.ENTITY_EVENT_INITIAL);
segmentTable.StartNewSegment(ServerNetSegment.EntityEventInitial);
msg.WriteUInt16(client.UnreceivedEntityEventCount);
msg.WriteUInt16(client.FirstNewEventID);
@@ -353,7 +353,7 @@ namespace Barotrauma.Networking
}
else
{
msg.WriteByte((byte)ServerNetObject.ENTITY_EVENT);
segmentTable.StartNewSegment(ServerNetSegment.EntityEvent);
Write(msg, eventsToSync, out sentEvents, client);
}
@@ -1,13 +1,13 @@
using Barotrauma.Steam;
using Barotrauma.Steam;
using System;
namespace Barotrauma.Networking
{
partial class OrderChatMessage : ChatMessage
{
public override void ServerWrite(IWriteMessage msg, Client c)
public override void ServerWrite(in SegmentTableWriter<ServerNetSegment> segmentTable, IWriteMessage msg, Client c)
{
msg.WriteByte((byte)ServerNetObject.CHAT_MESSAGE);
segmentTable.StartNewSegment(ServerNetSegment.ChatMessage);
msg.WriteUInt16(NetStateID);
msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1);
msg.WriteString(SenderName);
@@ -44,7 +44,7 @@ namespace Barotrauma.Networking
}
}
private bool IsRespawnPromptPendingForClient(Client c)
private static bool IsRespawnPromptPendingForClient(Client c)
{
if (!UseRespawnPrompt || !(GameMain.GameSession.GameMode is MultiPlayerCampaign campaign)) { return false; }
@@ -67,7 +67,7 @@ namespace Barotrauma.Networking
return false;
}
private List<CharacterInfo> GetBotsToRespawn()
private static List<CharacterInfo> GetBotsToRespawn()
{
if (GameMain.Server.ServerSettings.BotSpawnMode == BotSpawnMode.Normal)
{
@@ -110,7 +110,7 @@ namespace Barotrauma.Networking
return ShouldStartRespawnCountdown(characterToRespawnCount);
}
private int GetMinCharactersToRespawn()
private static int GetMinCharactersToRespawn()
{
return Math.Max((int)(GameMain.Server.ConnectedClients.Count * GameMain.Server.ServerSettings.MinRespawnRatio), 1);
}
@@ -464,7 +464,7 @@ namespace Barotrauma.Networking
}
}
if (!(GameMain.GameSession.GameMode is CampaignMode))
if (GameMain.GameSession.GameMode is not CampaignMode)
{
if (scooterPrefab != null)
{
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>0.20.7.0</Version>
<Version>0.20.8.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>
@@ -77,11 +77,11 @@ namespace Barotrauma
{
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
{
if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 30.0f) { currentFlags.Add("Initial".ToIdentifier()); }
if (GameMain.GameSession.RoundDuration > 30.0f) { currentFlags.Add("Initial".ToIdentifier()); }
}
else if (Level.Loaded.Type == LevelData.LevelType.Outpost)
{
if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0f &&
if (GameMain.GameSession.RoundDuration > 120.0f &&
speaker?.CurrentHull != null &&
(speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) &&
Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull))
@@ -206,7 +206,7 @@ namespace Barotrauma
public float Stun { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Can damage only Humans."), Editable]
public bool OnlyHumans { get; private set; }
public bool OnlyHumans { get; set; }
[Serialize("", IsPropertySaveable.Yes), Editable]
public string ApplyForceOnLimbs
@@ -322,7 +322,7 @@ namespace Barotrauma
List<Affliction> multipliedAfflictions = new List<Affliction>();
foreach (Affliction affliction in Afflictions.Keys)
{
multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier, affliction.Probability));
multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier, affliction));
}
return multipliedAfflictions;
}
@@ -529,7 +529,7 @@ namespace Barotrauma
private Color speechBubbleColor;
private float speechBubbleTimer;
public bool ResetInteract;
public bool DisableInteract { get; set; }
//text displayed when the character is highlighted if custom interact is set
public LocalizedString CustomInteractHUDText { get; private set; }
@@ -798,6 +798,7 @@ namespace Barotrauma
public float MaxHealth => MaxVitality;
public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle;
public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached;
public float EmpVulnerability => Params.Health.EmpVulnerability;
public float Bloodloss
{
@@ -845,6 +846,12 @@ namespace Barotrauma
set;
}
public bool IgnoreMeleeWeapons
{
get;
set;
}
/// <summary>
/// Current speed of the character's collider. Can be used by status effects to check if the character is moving.
/// </summary>
@@ -895,6 +902,10 @@ namespace Barotrauma
{
itemSelectedTime = Timing.TotalTime;
}
if (prevSelectedItem != _selectedItem && prevSelectedItem?.OnDeselect != null)
{
prevSelectedItem.OnDeselect(this);
}
}
}
/// <summary>
@@ -2687,9 +2698,9 @@ namespace Barotrauma
return;
}
if (ResetInteract)
if (DisableInteract)
{
ResetInteract = false;
DisableInteract = false;
return;
}
@@ -2710,25 +2721,31 @@ namespace Barotrauma
{
if (!IsMouseOnUI && (ViewTarget == null || ViewTarget == this))
{
if ((findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen) && (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld))
if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen)
{
FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null;
if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; }
float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f);
if (HeldItems.Any(it => it?.GetComponent<Wire>()?.IsActive ?? false))
if (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld)
{
//disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes
aimAssist = 0.0f;
}
FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null;
if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; }
float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f);
if (HeldItems.Any(it => it?.GetComponent<Wire>()?.IsActive ?? false))
{
//disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes
aimAssist = 0.0f;
}
var item = FindItemAtPosition(mouseSimPos, aimAssist);
focusedItem = CanInteract ? item : null;
if (focusedItem != null && focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None)
{
FocusedCharacter = null;
focusedItem = CanInteract ? FindItemAtPosition(mouseSimPos, aimAssist) : null;
if (focusedItem != null && focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None)
{
FocusedCharacter = null;
}
findFocusedTimer = 0.05f;
}
else
{
if (focusedItem != null && !CanInteractWith(focusedItem)) { focusedItem = null; }
if (FocusedCharacter != null && !CanInteractWith(FocusedCharacter)) { FocusedCharacter = null; }
}
findFocusedTimer = 0.05f;
}
}
else
@@ -2957,6 +2974,15 @@ namespace Barotrauma
{
UpdateProjSpecific(deltaTime, cam);
if (InvisibleTimer > 0.0f)
{
if (Controlled == null || Controlled == this || (Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f)
{
InvisibleTimer = Math.Min(InvisibleTimer, 1.0f);
}
InvisibleTimer -= deltaTime;
}
KnockbackCooldownTimer -= deltaTime;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && this == Controlled && !isSynced) { return; }
@@ -2996,6 +3022,7 @@ namespace Barotrauma
}
HideFace = false;
IgnoreMeleeWeapons = false;
UpdateSightRange(deltaTime);
UpdateSoundRange(deltaTime);
@@ -4102,7 +4129,13 @@ namespace Barotrauma
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; }
if (Screen.Selected != GameMain.GameScreen) { return; }
if (newStun > 0 && Params.Health.StunImmunity) { return; }
if (newStun > 0 && Params.Health.StunImmunity)
{
if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0)
{
return;
}
}
if ((newStun <= Stun && !allowStunDecrease) || !MathUtils.IsValid(newStun)) { return; }
if (Math.Sign(newStun) != Math.Sign(Stun))
{
@@ -1238,7 +1238,7 @@ namespace Barotrauma
partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel);
public void GiveExperience(int amount, bool isMissionExperience = false)
public void GiveExperience(int amount)
{
int prevAmount = ExperiencePoints;
@@ -1292,6 +1292,18 @@ namespace Barotrauma
return experienceRequired + ExperienceRequiredPerLevel(level);
}
public int GetExperienceRequiredForLevel(int level)
{
int currentLevel = GetCurrentLevel(out int experienceRequired);
if (currentLevel >= level) { return 0; }
int required = experienceRequired;
for (int i = currentLevel + 1; i <= level; i++)
{
required += ExperienceRequiredPerLevel(i);
}
return required;
}
public int GetCurrentLevel()
{
return GetCurrentLevel(out _);
@@ -1309,7 +1321,7 @@ namespace Barotrauma
return level;
}
private int ExperienceRequiredPerLevel(int level)
private static int ExperienceRequiredPerLevel(int 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]
public bool DivideByLimbCount { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Is the damage relative to the max vitality (percentage) or absolute (normal)"), Editable]
public bool MultiplyByMaxVitality { get; private set; }
public float DamagePerSecond;
public float DamagePerSecondTimer;
public float PreviousVitalityDecrease;
@@ -104,6 +107,15 @@ namespace Barotrauma
}
}
/// <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)
{
@@ -115,10 +127,10 @@ namespace Barotrauma
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public Affliction CreateMultiplied(float multiplier, float probability)
public Affliction CreateMultiplied(float multiplier, Affliction affliction)
{
var instance = Prefab.Instantiate(NonClampedStrength * multiplier, Source);
instance.Probability = probability;
instance.CopyProperties(affliction);
return instance;
}
@@ -693,8 +693,15 @@ namespace Barotrauma
if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; }
if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") { return; }
if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun")
{
if (Character.EmpVulnerability <= 0 || GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0)
{
return;
}
}
if (Character.Params.Health.PoisonImmunity && newAffliction.Prefab.AfflictionType == "poison") { return; }
if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == "emp") { return; }
if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab)
{
if (huskPrefab.TargetSpecies.None(s => s == Character.SpeciesName))
@@ -757,6 +757,10 @@ namespace Barotrauma
}
if (!foundMatchingModifier && random > affliction.Probability) { continue; }
float finalDamageModifier = damageMultiplier;
if (affliction.Prefab.AfflictionType == "emp" && character.EmpVulnerability > 0)
{
finalDamageModifier *= character.EmpVulnerability;
}
foreach (DamageModifier damageModifier in tempModifiers)
{
float damageModifierValue = damageModifier.DamageMultiplier;
@@ -766,9 +770,13 @@ namespace Barotrauma
}
finalDamageModifier *= damageModifierValue;
}
if (affliction.MultiplyByMaxVitality)
{
finalDamageModifier *= character.MaxVitality / 100f;
}
if (!MathUtils.NearlyEqual(finalDamageModifier, 1.0f))
{
newAffliction = affliction.CreateMultiplied(finalDamageModifier, affliction.Probability);
newAffliction = affliction.CreateMultiplied(finalDamageModifier, affliction);
}
else
{
@@ -501,6 +501,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool PoisonImmunity { get; set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable]
public float EmpVulnerability { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Can afflictions affect the face/body tint of the character."), Editable]
public bool ApplyAfflictionColors { get; private set; }
@@ -37,14 +37,19 @@ namespace Barotrauma.Abilities
{
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 (Identifier factionIdentifier in mission.ReputationRewards.Keys)
foreach (var (factionIdentifier, amount) in mission.ReputationRewards)
{
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);
@@ -5,15 +5,33 @@ internal sealed class CharacterAbilityGiveExperience : CharacterAbility
public override bool AppliesEffectOnIntervalUpdate => true;
private readonly int amount;
private readonly int level;
public CharacterAbilityGiveExperience(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
amount = abilityElement.GetAttributeInt("amount", 0);
level = abilityElement.GetAttributeInt("level", 0);
if (amount == 0 && level == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - no exp amount or level defined in {nameof(CharacterAbilityGiveExperience)}.");
}
if (amount > 0 && level > 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - {nameof(CharacterAbilityGiveExperience)} defines both an exp amount and a level.");
}
}
private void ApplyEffectSpecific(Character targetCharacter)
{
targetCharacter.Info?.GiveExperience(amount);
if (amount != 0)
{
targetCharacter.Info?.GiveExperience(amount);
}
if (level > 0)
{
targetCharacter.Info?.GiveExperience(targetCharacter.Info.GetExperienceRequiredForLevel(level));
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
@@ -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)
{
if (Character.HasTalent(identifier)) { continue; }
if (Character.GiveTalent(identifier))
{
Character.Info.AdditionalTalentPoints++;
}
Character.GiveTalent(identifier);
}
}
@@ -139,23 +139,13 @@ namespace Barotrauma
{
if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier))
{
return TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents);
return !talentOptionStage.HasMaxTalents(selectedTalents) && TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents);
}
bool optionStageCompleted = talentOptionStage.HasEnoughTalents(selectedTalents);
if (!optionStageCompleted)
{
break;
}
/*bool hasTalentInThisTier = talentOptionStage.HasMaxTalents(selectedTalents);
if (!hasTalentInThisTier)
{
if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier))
{
return TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents);
}
break;
}*/
}
}
@@ -67,10 +67,10 @@ namespace Barotrauma
.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)
=> Result<ContentFile, LoadError>.Failure(new LoadError(error, exception));
static Result<ContentFile, ContentPackage.LoadError> fail(string error, Exception? exception = null)
=> Result<ContentFile, ContentPackage.LoadError>.Failure(new ContentPackage.LoadError(error, exception));
Identifier elemName = element.NameAsIdentifier();
var type = Types.FirstOrDefault(t => t.Names.Contains(elemName));
@@ -83,6 +83,8 @@ namespace Barotrauma
{
return fail($"No content path defined for file of type \"{elemName}\"");
}
using var errorCatcher = DebugConsole.ErrorCatcher.Create();
try
{
filePath = type.MutateContentPath(filePath);
@@ -90,10 +92,16 @@ namespace Barotrauma
{
return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": file not found.");
}
var file = type.CreateInstance(contentPackage, filePath);
return file is null
? throw new Exception($"Content type is not implemented correctly")
: Result<ContentFile, LoadError>.Success(file);
if (file is null) { return fail($"Content type {type.Type.Name} is not implemented correctly"); }
if (errorCatcher.Errors.Any())
{
return fail(
$"Errors were issued to the debug console when loading \"{filePath}\" of type \"{elemName}\"");
}
return Result<ContentFile, ContentPackage.LoadError>.Success(file);
}
catch (Exception e)
{
@@ -123,23 +131,5 @@ namespace Barotrauma
}
public bool NotSyncedInMultiplayer => Types.Any(t => t.Type == GetType() && t.NotSyncedInMultiplayer);
public readonly struct LoadError
{
public readonly string Message;
public readonly Exception? Exception;
public LoadError(string message, Exception? exception)
{
Message = message;
Exception = exception;
}
public override string ToString()
=> Message
+ (Exception is { StackTrace: var stackTrace }
? '\n' + stackTrace.CleanupStackTrace()
: string.Empty);
}
}
}
@@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
@@ -14,6 +13,15 @@ namespace Barotrauma
{
public abstract class ContentPackage
{
public readonly record struct LoadError(string Message, Exception? Exception)
{
public override string ToString()
=> Message
+ (Exception is { StackTrace: var stackTrace }
? '\n' + stackTrace.CleanupStackTrace()
: string.Empty);
}
public static readonly Version MinimumHashCompatibleVersion = new Version(0, 18, 13, 0);
public const string LocalModsDir = "LocalMods";
@@ -37,12 +45,30 @@ namespace Barotrauma
public readonly Option<DateTime> InstallTime;
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()
{
if (!UgcId.TryUnwrap(out var ugcId)) { return true; }
if (!(ugcId is SteamWorkshopId steamWorkshopId)) { return true; }
if (ugcId is not SteamWorkshopId steamWorkshopId) { return true; }
if (!InstallTime.TryUnwrap(out var installTime)) { return true; }
Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value);
@@ -55,20 +81,25 @@ namespace Barotrauma
/// <summary>
/// Does the content package include some content that needs to match between all players in multiplayer.
/// </summary>
public bool HasMultiplayerSyncedContent { get; private set; }
public bool HasMultiplayerSyncedContent { get; }
protected ContentPackage(XDocument doc, string path)
{
using var errorCatcher = DebugConsole.ErrorCatcher.Create();
Path = path.CleanUpPathCrossPlatform();
XElement rootElement = doc.Root ?? throw new NullReferenceException("XML document is invalid: root element is null.");
Name = rootElement.GetAttributeString("name", "").Trim();
AltNames = rootElement.GetAttributeStringArray("altnames", Array.Empty<string>())
.Select(n => n.Trim()).ToImmutableArray();
AssertCondition(!string.IsNullOrEmpty(Name), "Name is null or empty");
UInt64 steamWorkshopId = rootElement.GetAttributeUInt64("steamworkshopid", 0);
if (Name.IsNullOrWhiteSpace() && AltNames.Any())
{
Name = AltNames.First();
}
UgcId = steamWorkshopId != 0
? Option<ContentPackageId>.Some(new SteamWorkshopId(steamWorkshopId))
: Option<ContentPackageId>.None();
@@ -85,23 +116,31 @@ namespace Barotrauma
.ToArray();
Files = fileResults
.OfType<Success<ContentFile, ContentFile.LoadError>>()
.Select(f => f.Value)
.Successes()
.ToImmutableArray();
Errors = fileResults
.OfType<Failure<ContentFile, ContentFile.LoadError>>()
.Select(f => f.Error)
FatalLoadErrors = fileResults
.Failures()
.ToImmutableArray();
AssertCondition(!string.IsNullOrEmpty(Name), $"{nameof(Name)} is null or empty");
HasMultiplayerSyncedContent = Files.Any(f => !f.NotSyncedInMultiplayer);
Hash = CalculateHash();
var expectedHash = rootElement.GetAttributeString("expectedhash", "");
if (HashMismatches(expectedHash))
{
DebugConsole.ThrowError($"Hash calculation for content package \"{Name}\" didn't match expected hash ({Hash.StringRepresentation} != {expectedHash})");
FatalLoadErrors = FatalLoadErrors.Add(
new LoadError(
Message: $"Hash calculation returned {Hash.StringRepresentation}, expected {expectedHash}",
Exception: null
));
}
FatalLoadErrors = FatalLoadErrors
.Concat(errorCatcher.Errors.Select(err => new LoadError(err.Text, null)))
.ToImmutableArray();
}
public bool HashMismatches(string expectedHash)
@@ -122,21 +161,21 @@ namespace Barotrauma
public bool NameMatches(string name)
=> NameMatches(name.ToIdentifier());
public static ContentPackage? TryLoad(string path)
public static Result<ContentPackage, Exception> TryLoad(string path)
{
var (success, failure) = Result<ContentPackage, Exception>.GetFactoryMethods();
XDocument doc = XMLExtensions.TryLoadXml(path);
try
{
return doc.Root.GetAttributeBool("corepackage", false)
? (ContentPackage)new CorePackage(doc, path)
: new RegularPackage(doc, path);
return success(doc.Root.GetAttributeBool("corepackage", false)
? new CorePackage(doc, path)
: new RegularPackage(doc, path));
}
catch (Exception e)
{
e = e.GetInnermost();
DebugConsole.ThrowError($"{e.Message}: {e.StackTrace}");
return null;
return failure(e.GetInnermost());
}
}
@@ -181,7 +220,7 @@ namespace Barotrauma
{
if (!condition)
{
throw new InvalidOperationException($"Failed to load \"{Name}\" at {Path}: {errorMsg}");
FatalLoadErrors = FatalLoadErrors.Add(new LoadError(errorMsg, null));
}
}
@@ -201,17 +240,19 @@ namespace Barotrauma
Failure
}
public LoadResult LoadPackage()
public LoadResult LoadContent()
{
foreach (var p in LoadPackageEnumerable())
foreach (var p in LoadContentEnumerable())
{
if (p.Exception != null) { return LoadResult.Failure; }
if (p.Result.IsFailure) { return LoadResult.Failure; }
}
return LoadResult.Success;
}
public IEnumerable<ContentPackageManager.LoadProgress> LoadPackageEnumerable()
public IEnumerable<ContentPackageManager.LoadProgress> LoadContentEnumerable()
{
using var errorCatcher = DebugConsole.ErrorCatcher.Create();
ContentFile[] getFilesToLoad(Predicate<ContentFile> predicate)
=> Files.Where(predicate.Invoke).ToArray()
#if DEBUG
@@ -227,6 +268,7 @@ namespace Barotrauma
for (int i = 0; i < filesToLoad.Length; i++)
{
Exception? exception = null;
try
{
//do not allow exceptions thrown here to crash the game
@@ -234,42 +276,53 @@ namespace Barotrauma
}
catch (Exception e)
{
var innermost = e.GetInnermost();
DebugConsole.LogError($"Failed to load \"{filesToLoad[i].Path}\": {innermost.Message}\n{innermost.StackTrace}");
exception = e;
}
if (exception != null)
{
yield return ContentPackageManager.LoadProgress.Failure(exception);
break;
yield break;
}
yield return new ContentPackageManager.LoadProgress((i + indexOffset) / (float)Files.Length);
if (errorCatcher.Errors.Any())
{
yield return ContentPackageManager.LoadProgress.Failure(
ContentPackageManager.LoadProgress.Error
.Reason.ConsoleErrorsThrown);
yield break;
}
yield return ContentPackageManager.LoadProgress.Progress((i + indexOffset) / (float)Files.Length);
}
}
//Load the UI and text files first. This is to allow the game
//to render the text in the loading screen as soon as possible.
var priorityFiles = getFilesToLoad(f => f is UIStyleFile || f is TextFile);
var priorityFiles = getFilesToLoad(f => f is UIStyleFile or TextFile);
var remainder = getFilesToLoad(f => !priorityFiles.Contains(f));
var loadEnumerable =
loadFiles(priorityFiles, 0)
.Concat(loadFiles(remainder, priorityFiles.Length));
foreach (var p in loadEnumerable)
{
if (p.Exception != null)
if (p.Result.TryUnwrapFailure(out var failure))
{
HandleLoadException(p.Exception);
errorCatcher.Dispose();
UnloadContent();
EnableError = Option.Some(failure);
yield return p;
break;
yield break;
}
yield return p;
}
errorCatcher.Dispose();
}
protected abstract void HandleLoadException(Exception e);
public void UnloadPackage()
public void UnloadContent()
{
Files.ForEach(f => f.UnloadFile());
}
@@ -284,21 +337,16 @@ namespace Barotrauma
.Select(e => ContentFile.CreateFromXElement(this, e))
.ToArray();
foreach (var result in fileResults)
foreach (var file in fileResults.Successes())
{
switch (result)
if (file is BaseSubFile or ItemAssemblyFile)
{
case Success<ContentFile, ContentFile.LoadError> { Value: var file }:
if (file is BaseSubFile || file is ItemAssemblyFile)
{
newFileList.Add(file);
}
else
{
var existingFile = Files.FirstOrDefault(f => f.Path == file.Path);
newFileList.Add(existingFile ?? file);
}
break;
newFileList.Add(file);
}
else
{
var existingFile = Files.FirstOrDefault(f => f.Path == file.Path);
newFileList.Add(existingFile ?? file);
}
}
@@ -331,16 +379,16 @@ namespace Barotrauma
public void LogErrors()
{
if (!Errors.Any())
if (!FatalLoadErrors.Any())
{
return;
}
DebugConsole.AddWarning(
$"The following errors occurred while loading the content package \"{Name}\". The package might not work correctly.\n" +
string.Join('\n', Errors.Select(errorToStr)));
string.Join('\n', FatalLoadErrors.Select(errorToStr)));
static string errorToStr(ContentFile.LoadError error)
static string errorToStr(LoadError error)
=> error.ToString();
}
}
@@ -43,10 +43,5 @@ namespace Barotrauma
"Core package requires at least one of the following content types: " +
string.Join(", ", missingFileTypes.Select(t => t.Type.Name)));
}
protected override void HandleLoadException(Exception e)
{
throw new Exception($"An exception was thrown while loading \"{Name}\"", e);
}
}
}
@@ -1,4 +1,3 @@
using System;
using System.Xml.Linq;
namespace Barotrauma
@@ -9,11 +8,5 @@ namespace Barotrauma
{
AssertCondition(!doc.Root.GetAttributeBool("corepackage", false), "Expected a regular package, got a core package");
}
protected override void HandleLoadException(Exception e)
{
UnloadPackage();
DebugConsole.ThrowError($"Failed to load package \"{Name}\"", e);
}
}
}
@@ -47,18 +47,19 @@ namespace Barotrauma
{
var oldCore = Core;
if (newCore == oldCore) { yield break; }
Core?.UnloadPackage();
if (newCore.FatalLoadErrors.Any()) { yield break; }
Core?.UnloadContent();
Core = newCore;
foreach (var p in newCore.LoadPackageEnumerable()) { yield return p; }
foreach (var p in newCore.LoadContentEnumerable()) { yield return p; }
SortContent();
yield return new LoadProgress(1.0f);
yield return LoadProgress.Progress(1.0f);
}
public static void ReloadCore()
{
if (Core == null) { return; }
Core.UnloadPackage();
Core.LoadPackage();
Core.UnloadContent();
Core.LoadContent();
SortContent();
}
@@ -79,10 +80,14 @@ namespace Barotrauma
if (ReferenceEquals(inNewRegular, regular)) { yield break; }
if (inNewRegular.SequenceEqual(regular)) { yield break; }
ThrowIfDuplicates(inNewRegular);
var newRegular = inNewRegular.ToList();
var newRegular = inNewRegular
// Refuse to enable packages with load errors
// so people are forced away from broken mods
.Where(r => !r.FatalLoadErrors.Any())
.ToList();
IEnumerable<RegularPackage> toUnload = regular.Where(r => !newRegular.Contains(r));
RegularPackage[] toLoad = newRegular.Where(r => !regular.Contains(r)).ToArray();
toUnload.ForEach(r => r.UnloadPackage());
toUnload.ForEach(r => r.UnloadContent());
Range<float> loadingRange = new Range<float>(0.0f, 1.0f);
@@ -90,9 +95,9 @@ namespace Barotrauma
{
var package = toLoad[i];
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
newRegular.Remove(package);
@@ -103,7 +108,7 @@ namespace Barotrauma
}
regular.Clear(); regular.AddRange(newRegular);
SortContent();
yield return new LoadProgress(1.0f);
yield return LoadProgress.Progress(1.0f);
}
public static void ThrowIfDuplicates(IEnumerable<ContentPackage> pkgs)
@@ -231,10 +236,12 @@ namespace Barotrauma
public sealed partial class PackageSource : ICollection<ContentPackage>
{
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.onLoadFail = onLoadFail;
directory = dir;
Directory.CreateDirectory(directory);
}
@@ -278,25 +285,30 @@ namespace Barotrauma
{
var fileListPath = Path.Combine(subDir, ContentPackage.FileListFileName).CleanUpPathCrossPlatform();
if (this.Any(p => p.Path.Equals(fileListPath, StringComparison.OrdinalIgnoreCase))) { continue; }
if (File.Exists(fileListPath))
{
if (skipPredicate?.Invoke(fileListPath) is true) { continue; }
ContentPackage? newPackage = ContentPackage.TryLoad(fileListPath);
if (newPackage is CorePackage corePackage)
{
corePackages.Add(corePackage);
}
else if (newPackage is RegularPackage regularPackage)
{
regularPackages.Add(regularPackage);
}
if (!(newPackage is null))
{
Debug.WriteLine($"Loaded \"{newPackage.Name}\"");
}
if (!File.Exists(fileListPath)) { continue; }
if (skipPredicate?.Invoke(fileListPath) is true) { continue; }
var result = ContentPackage.TryLoad(fileListPath);
if (!result.TryUnwrapSuccess(out var newPackage))
{
onLoadFail?.Invoke(
fileListPath,
result.TryUnwrapFailure(out var exception) ? exception : throw new Exception("unreachable"));
continue;
}
switch (newPackage)
{
case CorePackage corePackage:
corePackages.Add(corePackage);
break;
case RegularPackage regularPackage:
regularPackages.Add(regularPackage);
break;
}
Debug.WriteLine($"Loaded \"{newPackage.Name}\"");
}
}
@@ -348,8 +360,20 @@ namespace Barotrauma
public bool IsReadOnly => true;
}
public static readonly PackageSource LocalPackages = new PackageSource(ContentPackage.LocalModsDir, skipPredicate: null);
public static readonly PackageSource WorkshopPackages = new PackageSource(ContentPackage.WorkshopModsDir, skipPredicate: SteamManager.Workshop.IsInstallingToPath);
public static readonly PackageSource LocalPackages
= new PackageSource(
ContentPackage.LocalModsDir,
skipPredicate: null,
onLoadFail: null);
public static readonly PackageSource WorkshopPackages = new PackageSource(
ContentPackage.WorkshopModsDir,
skipPredicate: SteamManager.Workshop.IsInstallingToPath,
onLoadFail: (fileListPath, exception) =>
{
// Delete Workshop mods that fail to load to
// force a reinstall on next launch if necessary
Directory.TryDelete(Path.GetDirectoryName(fileListPath)!);
});
public static CorePackage? VanillaCorePackage { get; private set; } = null;
@@ -373,63 +397,77 @@ namespace Barotrauma
EnabledPackages.DisableRemovedMods();
}
public static ContentPackage? ReloadContentPackage(ContentPackage p)
public static Result<ContentPackage, Exception> ReloadContentPackage(ContentPackage p)
{
ContentPackage? newPackage = ContentPackage.TryLoad(p.Path);
if (newPackage is CorePackage core)
{
if (EnabledPackages.Core == p) { EnabledPackages.SetCore(core); }
}
else if (newPackage is RegularPackage regular)
{
int index = EnabledPackages.Regular.IndexOf(p);
if (index >= 0)
{
var newRegular = EnabledPackages.Regular.ToArray();
newRegular[index] = regular;
EnabledPackages.SetRegular(newRegular);
}
}
var result = ContentPackage.TryLoad(p.Path);
if (newPackage != null)
if (result.TryUnwrapSuccess(out var newPackage))
{
switch (newPackage)
{
case CorePackage core:
{
if (EnabledPackages.Core == p) { EnabledPackages.SetCore(core); }
break;
}
case RegularPackage regular:
{
int index = EnabledPackages.Regular.IndexOf(p);
if (index >= 0)
{
var newRegular = EnabledPackages.Regular.ToArray();
newRegular[index] = regular;
EnabledPackages.SetRegular(newRegular);
}
break;
}
}
LocalPackages.SwapPackage(p, newPackage);
WorkshopPackages.SwapPackage(p, newPackage);
}
EnabledPackages.DisableRemovedMods();
return newPackage;
return result;
}
public readonly struct LoadProgress
public readonly record struct LoadProgress(Result<float, LoadProgress.Error> Result)
{
public readonly float Value;
public readonly Exception? Exception;
public LoadProgress(float value)
public readonly record struct Error(
Error.Reason ErrorReason,
Option<Exception> Exception)
{
Value = value;
Exception = null;
}
public enum Reason { Exception, ConsoleErrorsThrown }
private LoadProgress(Exception exception)
{
Value = -1f;
Exception = exception;
public Error(Reason reason) : this(reason, Option.None) { }
public Error(Exception exception) : this(Reason.Exception, Option.Some(exception)) { }
}
public static LoadProgress Failure(Exception exception)
=> new LoadProgress(exception);
=> new LoadProgress(
Result<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)
=> Exception != null
? this
: new LoadProgress(MathHelper.Lerp(range.Start, range.End, Value));
=> Result.TryUnwrapSuccess(out var value)
? new LoadProgress(
Result<float, Error>.Success(
MathHelper.Lerp(range.Start, range.End, value)))
: this;
}
public static void LoadVanillaFileList()
{
VanillaCorePackage = new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList);
foreach (ContentFile.LoadError error in VanillaCorePackage.Errors)
foreach (ContentPackage.LoadError error in VanillaCorePackage.FatalLoadErrors)
{
DebugConsole.ThrowError(error.ToString());
}
@@ -444,6 +482,8 @@ namespace Barotrauma
if (VanillaCorePackage is null) { LoadVanillaFileList(); }
SteamManager.Workshop.DeleteUnsubscribedMods();
CorePackage enabledCorePackage = VanillaCorePackage!;
List<RegularPackage> enabledRegularPackages = new List<RegularPackage>();
@@ -512,7 +552,7 @@ namespace Barotrauma
yield return p.Transform(loadingRange);
}
yield return new LoadProgress(1.0f);
yield return LoadProgress.Progress(1.0f);
}
public static void LogEnabledRegularPackageErrors()
@@ -5,6 +5,7 @@ using Barotrauma.Steam;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
@@ -15,12 +16,12 @@ using Barotrauma.MapCreatures.Behavior;
namespace Barotrauma
{
struct ColoredText
readonly struct ColoredText
{
public string Text;
public Color Color;
public bool IsCommand;
public bool IsError;
public readonly string Text;
public readonly Color Color;
public readonly bool IsCommand;
public readonly bool IsError;
public readonly string Time;
@@ -31,7 +32,7 @@ namespace Barotrauma
this.IsCommand = isCommand;
this.IsError = isError;
Time = DateTime.Now.ToString();
Time = DateTime.Now.ToString(CultureInfo.InvariantCulture);
}
}
@@ -91,13 +92,57 @@ namespace Barotrauma
}
}
private static readonly Queue<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);
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);
private static QuestionCallback activeQuestionCallback;
@@ -2287,11 +2332,10 @@ namespace Barotrauma
private static void NewMessage(string msg, Color color, bool isCommand, bool isError)
{
if (string.IsNullOrEmpty(msg)) { return; }
lock (queuedMessages)
{
queuedMessages.Enqueue(new ColoredText(msg, color, isCommand, isError));
}
var newMsg = new ColoredText(msg, color, isCommand, isError);
queuedMessages.Enqueue(newMsg);
MessageHandler.Invoke(newMsg);
}
public static void ShowQuestionPrompt(string question, QuestionCallback onAnswered, string[] args = null, int argCount = -1)
@@ -404,7 +404,7 @@ namespace Barotrauma
{
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f);
info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual);
info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value), isMissionExperience: true);
info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value));
}
// apply money gains afterwards to prevent them from affecting XP gains
@@ -594,7 +594,7 @@ namespace Barotrauma
{
GameAnalyticsManager.AddDesignEvent(
$"MonsterSpawn:{GameMain.GameSession.GameMode?.Preset?.Identifier.Value ?? "none"}:{Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"}:{SpawnPosType}:{SpeciesName}",
value: Timing.TotalTime - GameMain.GameSession.RoundStartTime);
value: GameMain.GameSession.RoundDuration);
}
}, delayBetweenSpawns * i);
i++;
@@ -310,5 +310,17 @@ namespace Barotrauma.Extensions
=> source
.OfType<Some<T>>()
.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))
{
data.Add(identifier, value);
SteamAchievementManager.OnCampaignMetadataSet(identifier, value);
return;
}
@@ -80,7 +80,7 @@ namespace Barotrauma
public bool DisableEvents
{
get { return IsFirstRound && Timing.TotalTime < GameMain.GameSession.RoundStartTime + FirstRoundEventDelay; }
get { return IsFirstRound && GameMain.GameSession.RoundDuration > FirstRoundEventDelay; }
}
public bool CheatsEnabled;
@@ -249,7 +249,7 @@ namespace Barotrauma
{
for (int i = 0; i < wall.SectionCount; i++)
{
wall.SetDamage(i, 0, createNetworkEvent: false);
wall.SetDamage(i, 0, createNetworkEvent: false, createExplosionEffect: false);
}
}
}
@@ -1094,6 +1094,7 @@ namespace Barotrauma
if (item.Components.None(c => c is Pickable)) { continue; }
if (item.Components.Any(c => c is Pickable p && p.IsAttached)) { continue; }
if (item.Components.Any(c => c is Wire w && w.Connections.Any(c => c != null))) { continue; }
if (item.Container?.GetComponent<ItemContainer>() is { DrawInventory: false }) { continue; }
itemsToTransfer.Add((item, item.Container));
item.Submarine = null;
}
@@ -28,7 +28,10 @@ namespace Barotrauma
private Location[]? dummyLocations;
public CrewManager? CrewManager;
public double RoundStartTime;
public float RoundDuration
{
get; private set;
}
public double TimeSpentCleaning, TimeSpentPainting;
@@ -353,6 +356,7 @@ namespace Barotrauma
#if DEBUG
DateTime startTime = DateTime.Now;
#endif
RoundDuration = 0.0f;
AfflictionPrefab.LoadAllEffects();
MirrorLevel = mirrorLevel;
@@ -503,7 +507,7 @@ namespace Barotrauma
RoundSummary = new RoundSummary(GameMode, Missions, StartLocation, EndLocation);
if (!(GameMode is TutorialMode) && !(GameMode is TestGameMode))
if (GameMode is not TutorialMode && GameMode is not TestGameMode)
{
GUI.AddMessage("", Color.Transparent, 3.0f, playSound: false);
if (EndLocation != null && levelData != null)
@@ -571,9 +575,9 @@ namespace Barotrauma
GameMode.Start();
foreach (Mission mission in missions)
{
int prevEntityCount = Entity.GetEntities().Count();
int prevEntityCount = Entity.GetEntities().Count;
mission.Start(Level.Loaded);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count() != prevEntityCount)
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count != prevEntityCount)
{
DebugConsole.ThrowError(
$"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " +
@@ -612,7 +616,7 @@ namespace Barotrauma
CreatureMetrics.Instance.RecentlyEncountered.Clear();
GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition;
RoundStartTime = Timing.TotalTime;
RoundDuration = 0.0f;
GameMain.ResetFrameTime();
IsRunning = true;
}
@@ -713,6 +717,7 @@ namespace Barotrauma
public void Update(float deltaTime)
{
RoundDuration += deltaTime;
EventManager?.Update(deltaTime);
GameMode?.Update(deltaTime);
//backwards for loop because the missions may get completed and removed from the list in Update()
@@ -865,17 +870,16 @@ namespace Barotrauma
#else
bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
#endif
double roundDuration = Timing.TotalTime - RoundStartTime;
GameAnalyticsManager.AddProgressionEvent(
success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail,
GameMode?.Preset.Identifier.Value ?? "none",
roundDuration);
RoundDuration);
string eventId = "EndRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":";
LogEndRoundStats(eventId);
if (GameMode is CampaignMode campaignMode)
{
GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", GetAmountOfMoney(crewCharacters) - prevMoney);
campaignMode.TotalPlayTime += roundDuration;
campaignMode.TotalPlayTime += RoundDuration;
}
#if CLIENT
HintManager.OnRoundEnded();
@@ -901,21 +905,20 @@ namespace Barotrauma
public void LogEndRoundStats(string eventId)
{
double roundDuration = Timing.TotalTime - RoundStartTime;
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name.Value ?? "none"), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), RoundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name.Value ?? "none"), RoundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), RoundDuration);
foreach (Mission mission in missions)
{
GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), RoundDuration);
}
if (Level.Loaded != null)
{
Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ?
Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier :
Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier();
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + levelId), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + levelId), RoundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"), RoundDuration);
}
if (Submarine.MainSub != null)
@@ -315,6 +315,7 @@ namespace Barotrauma.Items.Components
if (f2.Body.UserData is Limb targetLimb)
{
if (targetLimb.IsSevered || targetLimb.character == null || targetLimb.character == User) { return false; }
if (targetLimb.character.IgnoreMeleeWeapons) { return false; }
var targetCharacter = targetLimb.character;
if (targetCharacter == picker) { return false; }
if (AllowHitMultiple)
@@ -330,6 +331,7 @@ namespace Barotrauma.Items.Components
else if (f2.Body.UserData is Character targetCharacter)
{
if (targetCharacter == picker || targetCharacter == User) { return false; }
if (targetCharacter.IgnoreMeleeWeapons) { return false; }
targetLimb = targetCharacter.AnimController.GetLimb(LimbType.Torso); //Otherwise armor can be bypassed in strange ways
if (AllowHitMultiple)
{
@@ -816,7 +816,14 @@ namespace Barotrauma.Items.Components
}
else
{
hasRequiredItems = itemList.Any(Predicate);
if (itemList.Any(Predicate))
{
hasRequiredItems = !relatedItem.RequireEmpty;
}
else
{
hasRequiredItems = relatedItem.MatchOnEmpty || relatedItem.RequireEmpty;
}
if (!hasRequiredItems)
{
shouldBreak = true;
@@ -107,7 +107,7 @@ namespace Barotrauma.Items.Components
[Serialize(100, IsPropertySaveable.No, description: "How many items are placed in a row before starting a new row.")]
public int ItemsPerRow { get; set; }
[Serialize(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected.")]
[Serialize(true, IsPropertySaveable.No, description: "Should the contents in the item's inventory be visible? Disabled on items like magazines that spawn the contents as needed.")]
public bool DrawInventory
{
get;
@@ -135,9 +135,6 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.No)]
public bool AllowAccess { get; set; }
[Serialize(false, IsPropertySaveable.No)]
public bool AccessOnlyWhenBroken { get; set; }
@@ -530,12 +527,12 @@ namespace Barotrauma.Items.Components
public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
{
return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
return DrawInventory && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
}
public override bool Select(Character character)
{
if (!AllowAccess) { return false; }
if (!DrawInventory) { return false; }
if (item.Container != null) { return false; }
if (AccessOnlyWhenBroken)
{
@@ -571,7 +568,7 @@ namespace Barotrauma.Items.Components
public override bool Pick(Character picker)
{
if (!AllowAccess) { return false; }
if (!DrawInventory) { return false; }
if (AccessOnlyWhenBroken)
{
if (item.Condition > 0)
@@ -539,7 +539,7 @@ namespace Barotrauma.Items.Components
var prevUser = user;
CancelFabricating();
amountRemaining--;
amountRemaining--;
if (amountRemaining > 0 && CanBeFabricated(prevFabricatedItem, availableIngredients, prevUser))
{
//keep fabricating if we can fabricate more
@@ -745,7 +745,7 @@ namespace Barotrauma.Items.Components
itemList.AddRange(container.Inventory.AllItems);
}
}
if (user?.Inventory != null)
if (user?.Inventory != null && user.SelectedItem == item)
{
itemList.AddRange(user.Inventory.AllItems);
linkedInventories.Add(user.Inventory);
@@ -65,14 +65,7 @@ namespace Barotrauma.Items.Components
{
get
{
if (GameMain.GameSession != null)
{
return (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime);
}
else
{
return 0.0f;
}
return GameMain.GameSession?.RoundDuration ?? 0.0f;
}
}
@@ -639,7 +639,13 @@ namespace Barotrauma.Items.Components
ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent<ItemContainer>();
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
@@ -649,7 +655,7 @@ namespace Barotrauma.Items.Components
var e = item.linkedTo[(j + currentLoaderIndex) % item.linkedTo.Count];
//use linked projectile containers in case they have to react to the turret being launched somehow
//(play a sound, spawn more projectiles)
if (!(e is Item linkedItem)) { continue; }
if (e is not Item linkedItem) { continue; }
if (!item.Prefab.IsLinkAllowed(e.Prefab)) { continue; }
if (linkedItem.Condition <= 0.0f)
{
@@ -692,7 +698,7 @@ namespace Barotrauma.Items.Components
foreach (MapEntity e in item.linkedTo)
{
if (!(e is Item linkedItem)) { continue; }
if (e is not Item linkedItem) { continue; }
if (!((MapEntity)item).Prefab.IsLinkAllowed(e.Prefab)) { continue; }
if (linkedItem.GetComponent<Repairable>() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag("turretammosource"))
{
@@ -872,7 +878,7 @@ namespace Barotrauma.Items.Components
partial void LaunchProjSpecific();
private void ShiftItemsInProjectileContainer(ItemContainer container)
private static void ShiftItemsInProjectileContainer(ItemContainer container)
{
if (container == null) { return; }
bool moved;
@@ -1063,8 +1069,8 @@ namespace Barotrauma.Items.Components
character.AIController.SelectTarget(null);
}
bool canShoot = true;
if (!HasPowerToShoot())
bool canShoot = HasPowerToShoot();
if (!canShoot)
{
List<PowerContainer> batteries = GetDirectlyConnectedBatteries();
float lowestCharge = 0.0f;
@@ -1089,7 +1095,6 @@ namespace Barotrauma.Items.Components
character.Speak(TextManager.Get("DialogSupercapacitorIsBroken").Value,
identifier: "supercapacitorisbroken".ToIdentifier(),
minDurationBetweenSimilar: 30.0f);
canShoot = false;
}
}
}
@@ -1104,7 +1109,6 @@ namespace Barotrauma.Items.Components
character.Speak(TextManager.Get("DialogTurretHasNoPower").Value,
identifier: "turrethasnopower".ToIdentifier(),
minDurationBetweenSimilar: 30.0f);
canShoot = false;
}
}
@@ -1283,7 +1287,7 @@ namespace Barotrauma.Items.Components
closestDistance = shootDistance;
foreach (var wall in Level.Loaded.ExtraWalls)
{
if (!(wall is DestructibleLevelWall destructibleWall) || destructibleWall.Destroyed) { continue; }
if (wall is not DestructibleLevelWall destructibleWall || destructibleWall.Destroyed) { continue; }
foreach (var cell in wall.Cells)
{
if (cell.DoesDamage)
@@ -1405,19 +1409,18 @@ namespace Barotrauma.Items.Components
Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value);
// Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target.
Body worldTarget = CheckLineOfSight(start, end);
bool shoot;
if (closestEnemy != null && closestEnemy.Submarine != null)
{
start -= closestEnemy.Submarine.SimPosition;
end -= closestEnemy.Submarine.SimPosition;
Body transformedTarget = CheckLineOfSight(start, end);
shoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character));
canShoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character));
}
else
{
shoot = CanShoot(worldTarget, character);
canShoot = CanShoot(worldTarget, character);
}
if (!shoot) { return false; }
if (!canShoot) { return false; }
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogFireTurret").Value,
@@ -1471,6 +1474,7 @@ namespace Barotrauma.Items.Components
{
if (targetBody.UserData is ISpatialEntity e)
{
if (e is Structure s && s.Indestructible) { return false; }
Submarine sub = e.Submarine ?? e as Submarine;
if (!targetSubmarines && e is Submarine) { return false; }
if (sub == null) { return false; }
@@ -1559,7 +1563,7 @@ namespace Barotrauma.Items.Components
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;
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)
: this(new Rectangle(
(int)(position.X - itemPrefab.Sprite.size.X / 2 * itemPrefab.Scale),
@@ -2155,12 +2157,15 @@ namespace Barotrauma
if (projectile.ShouldIgnoreSubmarineCollision(f2, contact)) { return false; }
}
contact.GetWorldManifold(out Vector2 normal, out _);
if (contact.FixtureA.Body == f1.Body) { normal = -normal; }
float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal);
if (GameMain.GameSession == null || GameMain.GameSession.RoundDuration > 1.0f)
{
contact.GetWorldManifold(out Vector2 normal, out _);
if (contact.FixtureA.Body == f1.Body) { normal = -normal; }
float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal);
impactQueue ??= new ConcurrentQueue<float>();
impactQueue.Enqueue(impact);
}
impactQueue ??= new ConcurrentQueue<float>();
impactQueue.Enqueue(impact);
isActive = true;
return true;
@@ -2774,6 +2779,7 @@ namespace Barotrauma
if (GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(conditionalActionType, ic, character, targetLimb));
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnUse, ic, character, targetLimb));
}
if (ic.DeleteOnUse) { remove = true; }
@@ -1156,7 +1156,7 @@ namespace Barotrauma
public bool CanBeBoughtFrom(Location.StoreInfo store, out PriceInfo priceInfo)
{
priceInfo = GetPriceInfo(store);
return priceInfo is { CanBeBought: true } && (store.Location?.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty;
return priceInfo is { CanBeBought: true } && (store?.Location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty;
}
public bool CanBeBoughtFrom(Location location)
@@ -1167,7 +1167,7 @@ namespace Barotrauma
var priceInfo = GetPriceInfo(store.Value);
if (priceInfo == null) { continue; }
if (!priceInfo.CanBeBought) { continue; }
if ((location.LevelData?.Difficulty ?? 0) < priceInfo.MinLevelDifficulty) { continue; }
if (location.LevelData.Difficulty < priceInfo.MinLevelDifficulty) { continue; }
return true;
}
return false;
@@ -27,13 +27,14 @@ namespace Barotrauma
private readonly bool applyFireEffects;
private readonly string[] ignoreFireEffectsForTags;
private readonly bool ignoreCover;
private readonly bool onlyInside,onlyOutside;
private readonly float flashDuration;
private readonly float? flashRange;
private readonly string decal;
private readonly float decalSize;
private readonly bool applyToSelf;
public bool OnlyInside, OnlyOutside;
private readonly float itemRepairStrength;
public readonly HashSet<Submarine> IgnoredSubmarines = new HashSet<Submarine>();
@@ -81,8 +82,8 @@ namespace Barotrauma
ignoreFireEffectsForTags = element.GetAttributeStringArray("ignorefireeffectsfortags", Array.Empty<string>(), convertToLowerInvariant: true);
ignoreCover = element.GetAttributeBool("ignorecover", false);
onlyInside = element.GetAttributeBool("onlyinside", false);
onlyOutside = element.GetAttributeBool("onlyoutside", false);
OnlyInside = element.GetAttributeBool("onlyinside", false);
OnlyOutside = element.GetAttributeBool("onlyoutside", false);
flash = element.GetAttributeBool("flash", showEffects);
flashDuration = element.GetAttributeFloat("flashduration", 0.05f);
@@ -176,13 +177,12 @@ namespace Barotrauma
foreach (Item item in Item.ItemList)
{
float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition);
if (distSqr > displayRangeSqr) continue;
float distFactor = 1.0f - (float)Math.Sqrt(distSqr) / displayRange;
if (distSqr > displayRangeSqr) { continue; }
float distFactor = CalculateDistanceFactor(distSqr, displayRange);
//damage repairable power-consuming items
var powered = item.GetComponent<Powered>();
if (powered == null || !powered.VulnerableToEMP) continue;
if (powered == null || !powered.VulnerableToEMP) { continue; }
if (item.Repairables.Any())
{
item.Condition -= item.MaxCondition * EmpStrength * distFactor;
@@ -195,6 +195,7 @@ namespace Barotrauma
powerContainer.Charge -= powerContainer.GetCapacity() * EmpStrength * distFactor;
}
}
static float CalculateDistanceFactor(float distSqr, float displayRange) => 1.0f - (float)Math.Sqrt(distSqr) / displayRange;
}
if (itemRepairStrength > 0.0f)
@@ -288,10 +289,16 @@ namespace Barotrauma
{
continue;
}
if (c == attacker && !applyToSelf) { continue; }
//if (c == attacker && !applyToSelf) { continue; }
if (onlyInside && c.Submarine == null) { continue; }
else if (onlyOutside && c.Submarine != null) { continue; }
if (OnlyInside && c.Submarine == null)
{
continue;
}
else if (OnlyOutside && c.Submarine != null)
{
continue;
}
Vector2 explosionPos = worldPosition;
if (c.Submarine != null) { explosionPos -= c.Submarine.Position; }
@@ -337,15 +344,19 @@ namespace Barotrauma
modifiedAfflictions.Clear();
foreach (Affliction affliction in attack.Afflictions.Keys)
{
// Shouldn't go above 15, or the damage can be unexpectedly low -> doesn't break armor
// Effectively this makes large explosions more effective against large creatures (because more limbs are affected), but I don't think that's necessarily a bad thing.
float limbCountFactor = Math.Min(distFactors.Count, 15);
float dmgMultiplier = distFactor;
if (affliction.DivideByLimbCount)
{
float limbCountFactor = distFactors.Count;
if (affliction.Prefab.LimbSpecific && affliction.Prefab.AfflictionType == "damage")
{
// Shouldn't go above 15, or the damage can be unexpectedly low -> doesn't break armor
// Effectively this makes large explosions more effective against large creatures (because more limbs are affected), but I don't think that's necessarily a bad thing.
limbCountFactor = Math.Min(distFactors.Count, 15);
}
dmgMultiplier /= limbCountFactor;
}
modifiedAfflictions.Add(affliction.CreateMultiplied(dmgMultiplier, affliction.Probability));
modifiedAfflictions.Add(affliction.CreateMultiplied(dmgMultiplier, affliction));
}
c.LastDamageSource = damageSource;
if (attacker == null)
@@ -353,29 +364,29 @@ namespace Barotrauma
if (damageSource is Item item)
{
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)
{
AbilityAttackData attackData = new AbilityAttackData(Attack, c, attacker);
if (attackData.Afflictions != null)
if (!attack.OnlyHumans || c.IsHuman)
{
modifiedAfflictions.AddRange(attackData.Afflictions);
}
AbilityAttackData attackData = new AbilityAttackData(Attack, c, attacker);
if (attackData.Afflictions != null)
{
modifiedAfflictions.AddRange(attackData.Afflictions);
}
//use a position slightly from the limb's position towards the explosion
//ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods
Vector2 dir = worldPosition - limb.WorldPosition;
Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f;
AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier);
damages.Add(limb, attackResult.Damage);
//use a position slightly from the limb's position towards the explosion
//ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods
Vector2 dir = worldPosition - limb.WorldPosition;
Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f;
AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier);
damages.Add(limb, attackResult.Damage);
}
}
if (attack.StatusEffects != null && attack.StatusEffects.Any())
{
attack.SetUser(attacker);
@@ -438,7 +449,7 @@ namespace Barotrauma
damagedStructureList.Clear();
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 (structure.HasBody &&
@@ -487,7 +498,7 @@ namespace Barotrauma
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)
{
if (cell.IsPointInside(worldPosition))
@@ -510,7 +521,7 @@ namespace Barotrauma
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>();
@@ -549,21 +549,24 @@ namespace Barotrauma
if (hull1.WaterVolume < hull1.Volume / Hull.MaxCompress &&
hull1.Surface < rect.Y)
{
//create a wave from the side of the hull the water is leaking from
if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f)
{
float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1])) * 6.0f;
vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f);
hull1.WaveVel[hull1.WaveY.Length - 1] += vel * deltaTime;
hull1.WaveVel[hull1.WaveY.Length - 2] += vel * deltaTime;
CreateWave(rect, hull1, hull1.WaveY.Length - 1, hull1.WaveY.Length - 2, flowForce, deltaTime);
}
else
{
float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[0])) * 6.0f;
CreateWave(rect, hull1, 0, 1, flowForce, deltaTime);
}
static void CreateWave(Rectangle rect, Hull hull1, int index1, int index2, Vector2 flowForce, float deltaTime)
{
float vel = (rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[index1]);
vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f);
hull1.WaveVel[0] += vel * deltaTime;
hull1.WaveVel[1] += vel * deltaTime;
if (vel > 0.0f)
{
hull1.WaveVel[index1] += vel * deltaTime;
hull1.WaveVel[index2] += vel * deltaTime;
}
}
}
else
@@ -405,7 +405,7 @@ namespace Barotrauma
if (wall.Submarine != sub) { continue; }
for (int i = 0; i < wall.SectionCount; i++)
{
wall.SetDamage(i, 0, createNetworkEvent: false);
wall.SetDamage(i, 0, createNetworkEvent: false, createExplosionEffect: false);
}
}
foreach (Hull hull in Hull.HullList)
@@ -318,7 +318,7 @@ namespace Barotrauma
var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
if (characters.Any())
{
price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier));
price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier, includeSaved: false));
price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, tag)));
}
@@ -659,7 +659,7 @@ namespace Barotrauma
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; }
@@ -683,7 +683,10 @@ namespace Barotrauma
UnlockMissionByTag(Type.MissionTags.GetRandomUnsynced());
}
CreateStores(force: true);
if (createStores)
{
CreateStores(force: true);
}
}
public void UnlockInitialMissions()
@@ -552,7 +552,8 @@ namespace Barotrauma
Connections[i].Locations[1];
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;
Connections[i].Locked = true;
@@ -628,6 +629,7 @@ namespace Barotrauma
foreach (Location location in Locations)
{
location.LevelData = new LevelData(location, CalculateDifficulty(location.MapPosition.X, location.Biome));
location.CreateStores(force: true);
}
foreach (LocationConnection connection in Connections)
{
@@ -734,7 +736,7 @@ namespace Barotrauma
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
@@ -56,6 +56,8 @@ namespace Barotrauma
//dimensions of the wall sections' physics bodies (only used for debug rendering)
private readonly List<Vector2> bodyDebugDimensions = new List<Vector2>();
private static Explosion explosionOnBroken;
#if DEBUG
[Serialize(false, IsPropertySaveable.Yes), Editable]
#else
@@ -1083,7 +1085,7 @@ namespace Barotrauma
return new AttackResult(damageAmount, null);
}
public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true)
public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true, bool createExplosionEffect = true)
{
if (Submarine != null && Submarine.GodMode || Indestructible) { return; }
if (!Prefab.Body) { return; }
@@ -1128,6 +1130,7 @@ namespace Barotrauma
}
else
{
float prevGapOpenState = Sections[sectionIndex].gap?.Open ?? 0.0f;
if (Sections[sectionIndex].gap == null)
{
Rectangle gapRect = Sections[sectionIndex].rect;
@@ -1204,8 +1207,15 @@ namespace Barotrauma
#endif
}
var gap = Sections[sectionIndex].gap;
float gapOpen = MaxHealth <= 0.0f ? 0.0f : (damage / MaxHealth - LeakThreshold) * (1.0f / (1.0f - LeakThreshold));
Sections[sectionIndex].gap.Open = gapOpen;
gap.Open = gapOpen;
//gap appeared or became much larger -> explosion effect
if (gapOpen - prevGapOpenState > 0.25f && createExplosionEffect && !gap.IsRoomToRoom)
{
CreateWallDamageExplosion(gap, attacker);
}
}
float damageDiff = damage - Sections[sectionIndex].damage;
@@ -1234,6 +1244,59 @@ namespace Barotrauma
UpdateSections();
}
private void CreateWallDamageExplosion(Gap gap, Character attacker)
{
const float explosionRange = 750.0f;
float explosionStrength = gap.Open;
var linkedHull = gap.linkedTo.FirstOrDefault() as Hull;
if (linkedHull != null)
{
//existing, nearby gaps leading to the same hull reduce the strength of the explosion
// -> the first breached section does most (or all) of the damage, making it more consistent
// (otherwise the damage would depend on how many structures and sections happen to be breached)
foreach (var otherGap in linkedHull.ConnectedGaps)
{
if (otherGap == gap || otherGap.IsRoomToRoom || otherGap.Open < 0.25f) { continue; }
explosionStrength -= Math.Max(0, explosionRange - Vector2.Distance(otherGap.WorldPosition, gap.WorldPosition)) / explosionRange;
if (explosionStrength <= 0.0f) { return; }
}
}
if (explosionOnBroken == null)
{
explosionOnBroken = new Explosion(explosionRange * gap.Open, force: 10.0f, damage: 0.0f, structureDamage: 0.0f, itemDamage: 0.0f);
if (AfflictionPrefab.Prefabs.TryGet("lacerations".ToIdentifier(), out AfflictionPrefab lacerations))
{
explosionOnBroken.Attack.Afflictions.Add(lacerations.Instantiate(50.0f), null);
}
else
{
explosionOnBroken.Attack.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(5.0f), null);
}
explosionOnBroken.OnlyInside = true;
explosionOnBroken.DisableParticles();
}
explosionOnBroken.Attack.DamageMultiplier = explosionStrength;
explosionOnBroken?.Explode(gap.WorldPosition, damageSource: null, attacker: attacker);
#if CLIENT
if (linkedHull != null)
{
for (int i = 0; i <= 50; i++)
{
Vector2 particlePos = new Vector2(Rand.Range(gap.WorldRect.X, gap.WorldRect.Right), Rand.Range(gap.WorldRect.Y - gap.WorldRect.Height, gap.WorldRect.Y));
var velocity = gap.IsHorizontal ?
gap.linkedTo[0].WorldPosition.X < gap.WorldPosition.X ? -Vector2.UnitX : Vector2.UnitX :
gap.linkedTo[0].WorldPosition.Y < gap.WorldPosition.Y ? -Vector2.UnitY : Vector2.UnitY;
velocity = new Vector2(velocity.X + Rand.Range(-0.2f, 0.2f), velocity.Y + Rand.Range(-0.2f, 0.2f));
var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, velocity * Rand.Range(100.0f, 3000.0f), collisionIgnoreTimer: 0.1f);
if (particle == null) { break; }
}
}
#endif
}
partial void OnHealthChangedProjSpecific(Character attacker, float damageAmount);
public void SetCollisionCategory(Category collisionCategory)
@@ -1570,7 +1633,7 @@ namespace Barotrauma
{
for (int i = 0; i < Sections.Length; i++)
{
SetDamage(i, Sections[i].damage, createNetworkEvent: false);
SetDamage(i, Sections[i].damage, createNetworkEvent: false, createExplosionEffect: false);
}
}
@@ -720,7 +720,7 @@ namespace Barotrauma
private void HandleLevelCollision(Impact impact, VoronoiCell cell = null)
{
if (GameMain.GameSession != null && Timing.TotalTime < GameMain.GameSession.RoundStartTime + 10)
if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 10)
{
//ignore level collisions for the first 10 seconds of the round in case the sub spawns in a way that causes it to hit a wall
//(e.g. level without outposts to dock to and an incorrectly configured ballast that makes the sub go up)
@@ -38,15 +38,15 @@ namespace Barotrauma.Networking
READY_CHECK,
READY_TO_SPAWN
}
enum ClientNetObject
enum ClientNetSegment
{
END_OF_MESSAGE, //self-explanatory
SYNC_IDS, //ids of the last changes the client knows about
CHAT_MESSAGE, //also self-explanatory
VOTE, //you get the idea
CHARACTER_INPUT,
ENTITY_STATE,
SPECTATING_POS
SyncIds, //ids of the last changes the client knows about
ChatMessage, //also self-explanatory
Vote, //you get the idea
CharacterInput,
EntityState,
SpectatingPos
}
enum ClientNetError
@@ -88,16 +88,15 @@ namespace Barotrauma.Networking
MONEY,
READY_CHECK //start, end and update a ready check
}
enum ServerNetObject
enum ServerNetSegment
{
END_OF_MESSAGE,
SYNC_IDS,
CHAT_MESSAGE,
VOTE,
CLIENT_LIST,
ENTITY_POSITION,
ENTITY_EVENT,
ENTITY_EVENT_INITIAL
SyncIds,
ChatMessage,
Vote,
ClientList,
EntityPosition,
EntityEvent,
EntityEventInitial
}
enum TraitorMessageType
@@ -272,6 +272,9 @@ namespace Barotrauma.Networking
[NetworkSerialize]
public bool IsMandatory;
[NetworkSerialize]
public bool IsVanilla;
private Md5Hash? cachedHash;
private DateTime? cachedDateTime;
@@ -305,6 +308,7 @@ namespace Barotrauma.Networking
? ugcId.StringRepresentation
: "";
IsMandatory = !contentPackage.Files.All(f => f is SubmarineFile);
IsVanilla = contentPackage == ContentPackageManager.VanillaCorePackage;
InstallTimeDiffInSeconds =
contentPackage.InstallTime.TryUnwrap(out var installTime)
? (uint)(installTime - referenceTime).TotalSeconds
@@ -65,7 +65,7 @@ namespace Barotrauma.Networking
public State CurrentState { get; private set; }
public bool UseRespawnPrompt
public static bool UseRespawnPrompt
{
get
{
@@ -191,6 +191,7 @@ namespace Barotrauma.Networking
public void ForceRespawn()
{
ResetShuttle();
RespawnCountdownStarted = true;
RespawnTime = DateTime.Now;
CurrentState = State.Waiting;
}
@@ -246,11 +246,13 @@ namespace Barotrauma
{
public readonly Identifier SkillIdentifier;
public readonly float Amount;
public readonly bool TriggerTalents;
public GiveSkill(XElement element, string parentDebugName)
{
SkillIdentifier = element.GetAttributeIdentifier("skillidentifier", Identifier.Empty);
Amount = element.GetAttributeFloat("amount", 0);
SkillIdentifier = element.GetAttributeIdentifier(nameof(SkillIdentifier), Identifier.Empty);
Amount = element.GetAttributeFloat(nameof(Amount), 0);
TriggerTalents = element.GetAttributeBool(nameof(TriggerTalents), true);
if (SkillIdentifier == Identifier.Empty)
{
@@ -424,7 +426,7 @@ namespace Barotrauma
private set;
}
private readonly bool multiplyAfflictionsByMaxVitality;
private readonly bool? multiplyAfflictionsByMaxVitality;
public IEnumerable<CharacterSpawnInfo> SpawnCharacters
{
@@ -490,7 +492,11 @@ namespace Barotrauma
talentTriggers = new List<Identifier>();
giveExperiences = new List<int>();
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(','));
OnlyInside = element.GetAttributeBool("onlyinside", false);
@@ -1294,7 +1300,7 @@ namespace Barotrauma
}
for (int i = 0; i < targets.Count; i++)
{
if (!(targets[i] is Item item)) { continue; }
if (targets[i] is not Item item) { continue; }
for (int j = 0; j < useItemCount; j++)
{
if (item.Removed) { continue; }
@@ -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)
@@ -1566,9 +1579,7 @@ namespace Barotrauma
if (targetCharacter != null && !targetCharacter.Removed)
{
Identifier skillIdentifier = giveSkill.SkillIdentifier == "randomskill" ? GetRandomSkill() : giveSkill.SkillIdentifier;
targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, giveSkill.Amount);
targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, giveSkill.Amount, !giveSkill.TriggerTalents);
Identifier GetRandomSkill()
{
return targetCharacter.Info?.Job?.GetSkills().GetRandomUnsynced()?.Identifier ?? Identifier.Empty;
@@ -2140,10 +2151,10 @@ namespace Barotrauma
return multiplier * AfflictionMultiplier;
}
private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool modifyByMaxVitality)
private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool? multiplyByMaxVitality)
{
float afflictionMultiplier = GetAfflictionMultiplier(entity, targetCharacter, deltaTime);
if (modifyByMaxVitality)
if (multiplyByMaxVitality ?? affliction.MultiplyByMaxVitality)
{
afflictionMultiplier *= targetCharacter.MaxVitality / 100f;
}
@@ -2162,7 +2173,7 @@ namespace Barotrauma
if (!MathUtils.NearlyEqual(afflictionMultiplier, 1.0f))
{
return affliction.CreateMultiplied(afflictionMultiplier, affliction.Probability);
return affliction.CreateMultiplied(afflictionMultiplier, affliction);
}
return affliction;
}
@@ -89,7 +89,7 @@ namespace Barotrauma.Steam
{
if (!IsInitialized || !Steamworks.SteamClient.IsValid)
{
return new PublishedFileId[0];
return Array.Empty<PublishedFileId>();
}
return Steamworks.SteamUGC.GetSubscribedItems();
}
@@ -61,6 +61,12 @@ namespace Barotrauma.Steam
if (set.Count == prevSize) { break; }
prevSize = set.Count;
}
// Remove items that do not have the correct consumer app ID,
// which can happen on items that are not visible to the currently
// logged in player (i.e. private & friends-only items)
set.RemoveWhere(it => it.ConsumerApp != AppID);
return set;
}
@@ -276,6 +282,62 @@ namespace Barotrauma.Steam
}
}
}
public static ISet<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)
=> File.Exists(Path.Combine(Path.GetDirectoryName(path)!, ContentPackageManager.CopyIndicatorFileName));
@@ -457,7 +519,7 @@ namespace Barotrauma.Steam
}
catch (Exception e)
{
DebugConsole.ThrowError(
DebugConsole.AddWarning(
$"An exception was thrown when attempting to copy \"{from}\" to \"{to}\": {e.Message}\n{e.StackTrace}");
}
}

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