Release 1.10.6.0 - Autumn Update 2025 Hotfix 1

This commit is contained in:
Regalis11
2025-09-25 11:11:35 +03:00
parent bad999d5fc
commit b2d91cde7c
25 changed files with 435 additions and 226 deletions
+1 -1
View File
@@ -73,7 +73,7 @@ body:
label: Version label: Version
description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu.
options: options:
- v1.10.5.0 (Autumn Update 2025) - v1.10.6.0 (Autumn Update 2025 Hotfix 1)
- Other - Other
validations: validations:
required: true required: true
@@ -2866,7 +2866,7 @@ namespace Barotrauma
} }
contextualOrders.RemoveAll(o => !IsOrderAvailable(o)); contextualOrders.RemoveAll(o => !IsOrderAvailable(o));
var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, contextualOrders.Count, MathHelper.ToRadians(90f + 180f / contextualOrders.Count)); var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, contextualOrders.Count, MathHelper.ToRadians(90f + 180f / contextualOrders.Count));
bool canCharacterBeHeard = !CanCharacterBeHeard(); bool canCharacterBeHeard = CanCharacterBeHeard();
for (int i = 0; i < contextualOrders.Count; i++) for (int i = 0; i < contextualOrders.Count; i++)
{ {
var order = contextualOrders[i]; var order = contextualOrders[i];
@@ -238,11 +238,11 @@ namespace Barotrauma.Items.Components
public void Spray(Character user, float deltaTime, bool applyColors) public void Spray(Character user, float deltaTime, bool applyColors)
{ {
Item liquidItem = liquidContainer?.Inventory.FirstOrDefault(); Item liquidItem = LiquidContainer?.Inventory.FirstOrDefault();
if (liquidItem == null) { return; } if (liquidItem == null) { return; }
bool isCleaning = false; bool isCleaning = false;
liquidColors.TryGetValue(liquidItem.Prefab.Identifier, out color); LiquidColors.TryGetValue(liquidItem.Prefab.Identifier, out color);
if (applyColors && targetSections.Any()) if (applyColors && targetSections.Any())
{ {
@@ -183,7 +183,7 @@ namespace Barotrauma
networkUpdateTimer += deltaTime; networkUpdateTimer += deltaTime;
if (networkUpdateTimer > 0.2f) if (networkUpdateTimer > 0.2f)
{ {
if (!pendingSectionUpdates.Any() && !pendingDecalUpdates.Any()) if (!pendingSectorUpdates.Any() && !pendingDecalUpdates.Any())
{ {
//these are used to modify the amount water/fire in the hull with console commands //these are used to modify the amount water/fire in the hull with console commands
//they should be usable even when not controlling a character //they should be usable even when not controlling a character
@@ -194,11 +194,11 @@ namespace Barotrauma
GameMain.Client?.CreateEntityEvent(this, new DecalEventData(decal)); GameMain.Client?.CreateEntityEvent(this, new DecalEventData(decal));
} }
pendingDecalUpdates.Clear(); pendingDecalUpdates.Clear();
foreach (int pendingSectionUpdate in pendingSectionUpdates) foreach (int pendingSectorUpdate in pendingSectorUpdates)
{ {
GameMain.Client?.CreateEntityEvent(this, new BackgroundSectionsEventData(pendingSectionUpdate)); GameMain.Client?.CreateEntityEvent(this, new BackgroundSectionsEventData(pendingSectorUpdate));
} }
pendingSectionUpdates.Clear(); pendingSectorUpdates.Clear();
networkUpdatePending = false; networkUpdatePending = false;
networkUpdateTimer = 0.0f; networkUpdateTimer = 0.0f;
} }
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.10.5.0</Version> <Version>1.10.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright> <Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.10.5.0</Version> <Version>1.10.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright> <Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.10.5.0</Version> <Version>1.10.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright> <Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>1.10.5.0</Version> <Version>1.10.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>1.10.5.0</Version> <Version>1.10.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -933,207 +933,235 @@ namespace Barotrauma
CheckTooManyMissions(Map.CurrentLocation, sender); CheckTooManyMissions(Map.CurrentLocation, sender);
} }
var prevBuyCrateItems = new Dictionary<Identifier, List<PurchasedItem>>(); if (HasCampaignInteractionAvailable(sender, InteractionType.Store))
foreach (var kvp in CargoManager.ItemsInBuyCrate)
{ {
prevBuyCrateItems.Add(kvp.Key, new List<PurchasedItem>(kvp.Value)); var prevBuyCrateItems = new Dictionary<Identifier, List<PurchasedItem>>();
} foreach (var kvp in CargoManager.ItemsInBuyCrate)
foreach (var store in prevBuyCrateItems)
{
foreach (var item in store.Value.ToList())
{ {
CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, -item.Quantity, sender); prevBuyCrateItems.Add(kvp.Key, new List<PurchasedItem>(kvp.Value));
} }
} foreach (var store in prevBuyCrateItems)
foreach (var store in buyCrateItems)
{
foreach (var item in store.Value.ToList())
{ {
if (map?.CurrentLocation?.Stores == null || !map.CurrentLocation.Stores.ContainsKey(store.Key)) { continue; } foreach (var item in store.Value.ToList())
int availableQuantity = map.CurrentLocation.Stores[store.Key].Stock.Find(s => s.ItemPrefab == item.ItemPrefab)?.Quantity ?? 0;
int alreadyPurchasedQuantity =
CargoManager.GetBuyCrateItem(store.Key, item.ItemPrefab)?.Quantity ?? 0 +
CargoManager.GetPurchasedItemCount(store.Key, item.ItemPrefab);
item.Quantity = MathHelper.Clamp(item.Quantity, 0, availableQuantity - alreadyPurchasedQuantity);
CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, item.Quantity, sender);
}
}
var prevPurchasedItems = new Dictionary<Identifier, List<PurchasedItem>>();
foreach (var kvp in CargoManager.PurchasedItems)
{
prevPurchasedItems.Add(kvp.Key, new List<PurchasedItem>(kvp.Value));
}
foreach (var storeId in purchasedItems.Keys)
{
DebugConsole.Log($"Purchased items ({storeId}):\n");
if (prevPurchasedItems.TryGetValue(storeId, out var alreadyPurchased))
{
var delivered = alreadyPurchased.Where(it => it.Delivered);
var notDelivered = alreadyPurchased.Where(it => !it.Delivered);
if (delivered.Any())
{ {
DebugConsole.Log($" Already delivered:\n" + string.Concat(delivered.Select(it => $" - {it.ItemPrefab.Name} (x{it.Quantity})"))); CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, -item.Quantity, sender);
}
if (notDelivered.Any())
{
DebugConsole.Log($" Already purchased:\n" + string.Concat(notDelivered.Where(it => !it.Delivered).Select(it => $" - {it.ItemPrefab.Name} (x{it.Quantity})")));
} }
} }
DebugConsole.Log($" New purchases:"); foreach (var store in buyCrateItems)
foreach (var purchasedItem in purchasedItems[storeId])
{ {
if (purchasedItem.Delivered) { continue; } foreach (var item in store.Value.ToList())
int quantity = purchasedItem.Quantity;
if (alreadyPurchased != null)
{ {
quantity -= alreadyPurchased.Where(it => it.DeliverImmediately == purchasedItem.DeliverImmediately && it.ItemPrefab == purchasedItem.ItemPrefab).Sum(it => it.Quantity); if (map?.CurrentLocation?.Stores == null || !map.CurrentLocation.Stores.ContainsKey(store.Key)) { continue; }
} int availableQuantity = map.CurrentLocation.Stores[store.Key].Stock.Find(s => s.ItemPrefab == item.ItemPrefab)?.Quantity ?? 0;
if (quantity > 0) int alreadyPurchasedQuantity =
{ CargoManager.GetBuyCrateItem(store.Key, item.ItemPrefab)?.Quantity ?? 0 +
DebugConsole.Log($" - {purchasedItem.ItemPrefab.Name} (x{quantity})"); CargoManager.GetPurchasedItemCount(store.Key, item.ItemPrefab);
item.Quantity = MathHelper.Clamp(item.Quantity, 0, availableQuantity - alreadyPurchasedQuantity);
CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, item.Quantity, sender);
} }
} }
}
foreach (var storeId in soldItems.Keys)
{
DebugConsole.Log($"Sold items:\n" + string.Concat(soldItems[storeId].Select(it => $" - {it.ItemPrefab.Name}")));
}
foreach (var kvp in purchasedItems) var prevPurchasedItems = new Dictionary<Identifier, List<PurchasedItem>>();
{ foreach (var kvp in CargoManager.PurchasedItems)
var storeId = kvp.Key;
var purchasedItemList = kvp.Value;
foreach (var purchasedItem in purchasedItemList)
{ {
int desiredQuantity = purchasedItem.Quantity; prevPurchasedItems.Add(kvp.Key, new List<PurchasedItem>(kvp.Value));
if (prevPurchasedItems.TryGetValue(storeId, out var alreadyPurchasedList) && }
alreadyPurchasedList.FirstOrDefault(p => p.ItemPrefab == purchasedItem.ItemPrefab && p.DeliverImmediately == purchasedItem.DeliverImmediately) is { } alreadyPurchased)
foreach (var storeId in purchasedItems.Keys)
{
DebugConsole.Log($"Purchased items ({storeId}):\n");
if (prevPurchasedItems.TryGetValue(storeId, out var alreadyPurchased))
{ {
desiredQuantity -= alreadyPurchased.Quantity; var delivered = alreadyPurchased.Where(it => it.Delivered);
var notDelivered = alreadyPurchased.Where(it => !it.Delivered);
if (delivered.Any())
{
DebugConsole.Log($" Already delivered:\n" + string.Concat(delivered.Select(it => $" - {it.ItemPrefab.Name} (x{it.Quantity})")));
}
if (notDelivered.Any())
{
DebugConsole.Log($" Already purchased:\n" + string.Concat(notDelivered.Where(it => !it.Delivered).Select(it => $" - {it.ItemPrefab.Name} (x{it.Quantity})")));
}
} }
int availableQuantity = map.CurrentLocation.Stores[storeId].Stock.Find(s => s.ItemPrefab == purchasedItem.ItemPrefab)?.Quantity ?? 0; DebugConsole.Log($" New purchases:");
purchasedItem.Quantity = Math.Min(desiredQuantity, availableQuantity); foreach (var purchasedItem in purchasedItems[storeId])
}
CargoManager.PurchaseItems(storeId, purchasedItemList, removeFromCrate: false, client: sender);
}
foreach (var (storeIdentifier, items) in CargoManager.PurchasedItems)
{
if (!prevPurchasedItems.ContainsKey(storeIdentifier))
{
CargoManager.LogNewItemPurchases(storeIdentifier, items, sender);
continue;
}
List<PurchasedItem> newItems = new List<PurchasedItem>();
List<PurchasedItem> prevItems = prevPurchasedItems[storeIdentifier];
foreach (PurchasedItem item in items)
{
PurchasedItem matching = prevItems.FirstOrDefault(ppi => ppi.ItemPrefab == item.ItemPrefab);
if (matching is null)
{ {
newItems.Add(item); if (purchasedItem.Delivered) { continue; }
int quantity = purchasedItem.Quantity;
if (alreadyPurchased != null)
{
quantity -= alreadyPurchased.Where(it => it.DeliverImmediately == purchasedItem.DeliverImmediately && it.ItemPrefab == purchasedItem.ItemPrefab).Sum(it => it.Quantity);
}
if (quantity > 0)
{
DebugConsole.Log($" - {purchasedItem.ItemPrefab.Name} (x{quantity})");
}
}
}
foreach (var storeId in soldItems.Keys)
{
DebugConsole.Log($"Sold items:\n" + string.Concat(soldItems[storeId].Select(it => $" - {it.ItemPrefab.Name}")));
}
foreach (var kvp in purchasedItems)
{
var storeId = kvp.Key;
var purchasedItemList = kvp.Value;
foreach (var purchasedItem in purchasedItemList)
{
int desiredQuantity = purchasedItem.Quantity;
if (prevPurchasedItems.TryGetValue(storeId, out var alreadyPurchasedList) &&
alreadyPurchasedList.FirstOrDefault(p => p.ItemPrefab == purchasedItem.ItemPrefab && p.DeliverImmediately == purchasedItem.DeliverImmediately) is { } alreadyPurchased)
{
desiredQuantity -= alreadyPurchased.Quantity;
}
int availableQuantity = map.CurrentLocation.Stores[storeId].Stock.Find(s => s.ItemPrefab == purchasedItem.ItemPrefab)?.Quantity ?? 0;
purchasedItem.Quantity = Math.Min(desiredQuantity, availableQuantity);
}
CargoManager.PurchaseItems(storeId, purchasedItemList, removeFromCrate: false, client: sender);
}
foreach (var (storeIdentifier, items) in CargoManager.PurchasedItems)
{
if (!prevPurchasedItems.ContainsKey(storeIdentifier))
{
CargoManager.LogNewItemPurchases(storeIdentifier, items, sender);
continue; continue;
} }
if (matching.Quantity < item.Quantity)
List<PurchasedItem> newItems = new List<PurchasedItem>();
List<PurchasedItem> prevItems = prevPurchasedItems[storeIdentifier];
foreach (PurchasedItem item in items)
{ {
newItems.Add(new PurchasedItem(item.ItemPrefab, item.Quantity - matching.Quantity, sender)); PurchasedItem matching = prevItems.FirstOrDefault(ppi => ppi.ItemPrefab == item.ItemPrefab);
if (matching is null)
{
newItems.Add(item);
continue;
}
if (matching.Quantity < item.Quantity)
{
newItems.Add(new PurchasedItem(item.ItemPrefab, item.Quantity - matching.Quantity, sender));
}
}
if (newItems.Any())
{
CargoManager.LogNewItemPurchases(storeIdentifier, newItems, sender);
} }
} }
if (newItems.Any()) bool allowedToSellSubItems = AllowedToManageCampaign(sender, ClientPermissions.SellSubItems);
if (allowedToSellSubItems)
{ {
CargoManager.LogNewItemPurchases(storeIdentifier, newItems, sender); var prevSubSellCrateItems = new Dictionary<Identifier, List<PurchasedItem>>(CargoManager.ItemsInSellFromSubCrate);
} foreach (var store in prevSubSellCrateItems)
}
bool allowedToSellSubItems = AllowedToManageCampaign(sender, ClientPermissions.SellSubItems);
if (allowedToSellSubItems)
{
var prevSubSellCrateItems = new Dictionary<Identifier, List<PurchasedItem>>(CargoManager.ItemsInSellFromSubCrate);
foreach (var store in prevSubSellCrateItems)
{
foreach (var item in store.Value.ToList())
{ {
CargoManager.ModifyItemQuantityInSubSellCrate(store.Key, item.ItemPrefab, -item.Quantity, sender); foreach (var item in store.Value.ToList())
{
CargoManager.ModifyItemQuantityInSubSellCrate(store.Key, item.ItemPrefab, -item.Quantity, sender);
}
}
foreach (var store in subSellCrateItems)
{
foreach (var item in store.Value.ToList())
{
CargoManager.ModifyItemQuantityInSubSellCrate(store.Key, item.ItemPrefab, item.Quantity, sender);
}
} }
} }
foreach (var store in subSellCrateItems)
bool allowedToSellInventoryItems = AllowedToManageCampaign(sender, ClientPermissions.SellInventoryItems);
if (allowedToSellInventoryItems && allowedToSellSubItems)
{ {
foreach (var item in store.Value.ToList()) // for some reason CargoManager.SoldItem is never cleared by the server, I've added a check to SellItems that ignores all
// sold items that are removed so they should be discarded on the next message
var prevSoldItems = new Dictionary<Identifier, List<SoldItem>>(CargoManager.SoldItems);
foreach (var store in prevSoldItems)
{ {
CargoManager.ModifyItemQuantityInSubSellCrate(store.Key, item.ItemPrefab, item.Quantity, sender); CargoManager.BuyBackSoldItems(store.Key, store.Value.ToList(), sender);
}
foreach (var store in soldItems)
{
CargoManager.SellItems(store.Key, store.Value.ToList(), sender);
}
}
else if (allowedToSellInventoryItems || allowedToSellSubItems)
{
var prevSoldItems = new Dictionary<Identifier, List<SoldItem>>(CargoManager.SoldItems);
foreach (var store in prevSoldItems)
{
store.Value.RemoveAll(predicate);
CargoManager.BuyBackSoldItems(store.Key, store.Value.ToList(), sender);
}
foreach (var store in soldItems)
{
store.Value.RemoveAll(predicate);
}
foreach (var store in soldItems)
{
CargoManager.SellItems(store.Key, store.Value.ToList(), sender);
}
bool predicate(SoldItem i) => allowedToSellInventoryItems != (i.Origin == SoldItem.SellOrigin.Character);
}
}
else
{
GameServer.Log($"{sender.Name} attempted to buy or sell items without having access to a store NPC.", ServerLog.MessageType.Error);
}
if ((purchasedUpgrades.Any() || purchasedItemSwaps.Any()) &&
HasCampaignInteractionAvailable(sender, InteractionType.Upgrade))
{
var characterList = GameSession.GetSessionCrewCharacters(CharacterType.Both);
foreach (var (prefab, category, _) in purchasedUpgrades)
{
UpgradeManager.TryPurchaseUpgrade(prefab, category, client: sender);
// unstable logging
int price = prefab.Price.GetBuyPrice(prefab, UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation, characterList);
int level = UpgradeManager.GetUpgradeLevel(prefab, category);
GameServer.Log($"SERVER: Purchased level {level} {category.Identifier}.{prefab.Identifier} for {price}", ServerLog.MessageType.ServerMessage);
}
foreach (var purchasedItemSwap in purchasedItemSwaps)
{
if (purchasedItemSwap.ItemToInstall == null)
{
UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove, client: sender);
}
else
{
UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, client: sender);
}
}
foreach (Item item in Item.ItemList)
{
if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item))
{
UpgradeManager.CancelItemSwap(item);
item.PendingItemSwap = null;
} }
} }
} }
else
{
GameServer.Log($"{sender.Name} attempted to buy upgrades without having access to an NPC offering upgrades.", ServerLog.MessageType.Error);
}
}
bool allowedToSellInventoryItems = AllowedToManageCampaign(sender, ClientPermissions.SellInventoryItems); private bool HasCampaignInteractionAvailable(Client sender, InteractionType interactionType)
if (allowedToSellInventoryItems && allowedToSellSubItems) {
if (sender.Character == null || sender.Character.IsIncapacitated) { return false; }
if (GameMain.Server?.ServerSettings is { AllowRemoteCampaignInteractions: true }) { return true; }
foreach (var otherCharacter in Character.CharacterList)
{ {
// for some reason CargoManager.SoldItem is never cleared by the server, I've added a check to SellItems that ignores all if (otherCharacter.CampaignInteractionType != interactionType) { continue; }
// sold items that are removed so they should be discarded on the next message //larger-than default maximum distance to give legit clients some leeway if their position is a bit off
var prevSoldItems = new Dictionary<Identifier, List<SoldItem>>(CargoManager.SoldItems); if (sender.Character.CanInteractWith(otherCharacter, maxDist: 250.0f))
foreach (var store in prevSoldItems)
{ {
CargoManager.BuyBackSoldItems(store.Key, store.Value.ToList(), sender); return true;
}
foreach (var store in soldItems)
{
CargoManager.SellItems(store.Key, store.Value.ToList(), sender);
}
}
else if (allowedToSellInventoryItems || allowedToSellSubItems)
{
var prevSoldItems = new Dictionary<Identifier, List<SoldItem>>(CargoManager.SoldItems);
foreach (var store in prevSoldItems)
{
store.Value.RemoveAll(predicate);
CargoManager.BuyBackSoldItems(store.Key, store.Value.ToList(), sender);
}
foreach (var store in soldItems)
{
store.Value.RemoveAll(predicate);
}
foreach (var store in soldItems)
{
CargoManager.SellItems(store.Key, store.Value.ToList(), sender);
}
bool predicate(SoldItem i) => allowedToSellInventoryItems != (i.Origin == SoldItem.SellOrigin.Character);
}
var characterList = GameSession.GetSessionCrewCharacters(CharacterType.Both);
foreach (var (prefab, category, _) in purchasedUpgrades)
{
UpgradeManager.TryPurchaseUpgrade(prefab, category, client: sender);
// unstable logging
int price = prefab.Price.GetBuyPrice(prefab, UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation, characterList);
int level = UpgradeManager.GetUpgradeLevel(prefab, category);
GameServer.Log($"SERVER: Purchased level {level} {category.Identifier}.{prefab.Identifier} for {price}", ServerLog.MessageType.ServerMessage);
}
foreach (var purchasedItemSwap in purchasedItemSwaps)
{
if (purchasedItemSwap.ItemToInstall == null)
{
UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove, client: sender);
}
else
{
UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, client: sender);
}
}
foreach (Item item in Item.ItemList)
{
if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item))
{
UpgradeManager.CancelItemSwap(item);
item.PendingItemSwap = null;
} }
} }
return false;
} }
public void ServerReadMoney(IReadMessage msg, Client sender) public void ServerReadMoney(IReadMessage msg, Client sender)
@@ -1265,7 +1293,7 @@ namespace Barotrauma
CharacterInfo firedCharacter = null; CharacterInfo firedCharacter = null;
(ushort id, string newName) appliedRename = (Entity.NullEntityID, string.Empty); (ushort id, string newName) appliedRename = (Entity.NullEntityID, string.Empty);
if (location != null) if (location != null && HasCampaignInteractionAvailable(sender, InteractionType.Crew))
{ {
if (fireCharacter && AllowedToManageCampaign(sender, ClientPermissions.ManageHires)) if (fireCharacter && AllowedToManageCampaign(sender, ClientPermissions.ManageHires))
{ {
@@ -1369,6 +1397,10 @@ namespace Barotrauma
}); });
} }
} }
else
{
GameServer.Log($"{sender.Name} attempted to manage hires without having access to an appropriate NPC.", ServerLog.MessageType.Error);
}
// bounce back // bounce back
if (renameCharacter && existingCrewMember) if (renameCharacter && existingCrewMember)
@@ -64,15 +64,15 @@ namespace Barotrauma
decalUpdateTimer = 0; decalUpdateTimer = 0;
decalUpdatePending = false; decalUpdatePending = false;
} }
if (pendingSectionUpdates.Count > 0 && backgroundSectionUpdateTimer > NetConfig.HullUpdateInterval) if (pendingSectorUpdates.Count > 0 && backgroundSectionUpdateTimer > NetConfig.HullUpdateInterval)
{ {
foreach (int pendingSectionUpdate in pendingSectionUpdates) foreach (int pendingSectorUpdate in pendingSectorUpdates)
{ {
GameMain.NetworkMember.CreateEntityEvent(this, new BackgroundSectionsEventData(pendingSectionUpdate)); GameMain.NetworkMember.CreateEntityEvent(this, new BackgroundSectionsEventData(pendingSectorUpdate));
} }
backgroundSectionUpdateTimer = 0; backgroundSectionUpdateTimer = 0;
pendingSectionUpdates.Clear(); pendingSectorUpdates.Clear();
} }
} }
@@ -177,6 +177,8 @@ namespace Barotrauma
} }
break; break;
case EventType.BackgroundSections: case EventType.BackgroundSections:
bool addPendingSectorUpdate = false;
SharedBackgroundSectionRead( SharedBackgroundSectionRead(
msg, msg,
bsnu => bsnu =>
@@ -185,17 +187,67 @@ namespace Barotrauma
Color color = bsnu.Color; Color color = bsnu.Color;
float colorStrength = bsnu.ColorStrength; float colorStrength = bsnu.ColorStrength;
#warning TODO: verify the client is close enough to this hull to paint it, that the sprayer is functional and that the color matches if (c.Character is not { AllowInput: true }) { return; }
if (!(c.Character is { AllowInput: true })) { return; }
if (c.Character.HeldItems.All(it => it.GetComponent<Sprayer>() == null)) { return; }
//ideally the server would just run the painting logic the same way as clients instead of relying on the clients setting colors on the hull,
//but that's non-trivial because the server doesn't know the client's exact cursor position, just the direction they're aiming at
//and we want the painting to be precise, lag shouldn't cause the paint to end up in the wrong place, etc.
//but now that clients set the colors themselves, we need to do some sanity checks:
var sprayer = c.Character.HeldItems
.Select(it => it.GetComponent<Sprayer>())
.FirstOrDefault(component => component != null);
if (sprayer == null) { return; }
Item liquidItem = sprayer.LiquidContainer?.Inventory?.FirstOrDefault();
if (liquidItem == null) { return; }
if (!sprayer.LiquidColors.TryGetValue(liquidItem.Prefab.Identifier, out Color paintColor)) { return; }
bool isCleaning = paintColor.A == 0;
var backgroundSectionPos = GetBackgroundSectionWorldPos(BackgroundSections[i]);
//rough distance check to disallow painting from very far away
//(slightly longer range than the normal range of the sprayer to give the client some leeway)
if (Vector2.Distance(backgroundSectionPos, sprayer.Item.WorldPosition) > sprayer.Range * 1.1f)
{
return;
}
//if we get to this point (client can paint this section), let's sync the changes
//the color change below may fail if the color is out of sync client-side, even if the client isn't doing anything malicious,
//in which case we want to get the client back in sync
addPendingSectorUpdate = true;
if (isCleaning)
{
//if we're cleaning, strength of the color must go down
if (colorStrength >= BackgroundSections[i].ColorStrength) { return; }
}
else
{
Vector3 colorChange = color.ToVector3() - BackgroundSections[i].Color.ToVector3();
Vector3 expectedColorChange = paintColor.ToVector3() - BackgroundSections[i].Color.ToVector3();
//color should be going towards the color of the paint, if it's not, don't allow changing it
if (Math.Sign(colorChange.X) != Math.Sign(expectedColorChange.X) ||
Math.Sign(colorChange.Y) != Math.Sign(expectedColorChange.Y) ||
Math.Sign(colorChange.Z) != Math.Sign(expectedColorChange.Z))
{
return;
}
BackgroundSections[i].SetColor(color);
}
BackgroundSections[i].SetColorStrength(colorStrength); BackgroundSections[i].SetColorStrength(colorStrength);
BackgroundSections[i].SetColor(color);
}, },
out int sectorToUpdate); out int sectorToUpdate);
RefreshAveragePaintedColor();
//add to pending updates to notify other clients as well if (addPendingSectorUpdate)
pendingSectionUpdates.Add(sectorToUpdate); {
RefreshAveragePaintedColor();
//add to pending updates to notify other clients as well
pendingSectorUpdates.Add(sectorToUpdate);
}
break; break;
case EventType.Decal: case EventType.Decal:
byte decalIndex = msg.ReadByte(); byte decalIndex = msg.ReadByte();
@@ -4384,9 +4384,22 @@ namespace Barotrauma.Networking
moustacheIndex: netInfo.MoustacheIndex, moustacheIndex: netInfo.MoustacheIndex,
faceAttachmentIndex: netInfo.FaceAttachmentIndex); faceAttachmentIndex: netInfo.FaceAttachmentIndex);
sender.CharacterInfo.Head.SkinColor = netInfo.SkinColor; sender.CharacterInfo.Head.SkinColor = validateColor(netInfo.SkinColor, "skin color", sender.CharacterInfo.SkinColors.Select(kvp => kvp.Color));
sender.CharacterInfo.Head.HairColor = netInfo.HairColor; sender.CharacterInfo.Head.HairColor = validateColor(netInfo.HairColor, "hair color", sender.CharacterInfo.HairColors.Select(kvp => kvp.Color));
sender.CharacterInfo.Head.FacialHairColor = netInfo.FacialHairColor; sender.CharacterInfo.Head.FacialHairColor = validateColor(netInfo.FacialHairColor, "facial hair color", sender.CharacterInfo.FacialHairColors.Select(kvp => kvp.Color));
Color validateColor(Color newColor, string colorName, IEnumerable<Color> supportedColors)
{
if (!supportedColors.Contains(newColor))
{
DebugConsole.AddWarning($"Client {sender.Name} attempted to set their {colorName} to an unsupported value ({newColor}).");
return supportedColors.First();
}
else
{
return newColor;
}
}
if (netInfo.JobVariants.Length > 0) if (netInfo.JobVariants.Length > 0)
{ {
@@ -269,8 +269,27 @@ namespace Barotrauma.Networking
return; return;
} }
var packet = INetSerializableStruct.Read<PeerPacketMessage>(inc); try
callbacks.OnMessageReceived.Invoke(conn, packet.GetReadMessage(packetHeader.IsCompressed(), conn)); {
var packet = INetSerializableStruct.Read<PeerPacketMessage>(inc);
callbacks.OnMessageReceived.Invoke(conn, packet.GetReadMessage(packetHeader.IsCompressed(), conn));
}
catch (NetStructReadException)
{
//kick the client if we fail to parse their message
if (conn != OwnerConnection)
{
if (connectedClients.Find(c => c.Connection == conn) is { } connectedClient)
{
Disconnect(connectedClient.Connection, PeerDisconnectPacket.WithReason(DisconnectReason.MalformedData));
}
}
else
{
throw;
}
}
} }
LidgrenConnection? FindConnection(NetConnection ligdrenConn) LidgrenConnection? FindConnection(NetConnection ligdrenConn)
@@ -73,11 +73,31 @@ namespace Barotrauma.Networking
try try
{ {
foreach (var incBuf in ChildServerRelay.Read()) foreach (byte[] incBuf in ChildServerRelay.Read())
{ {
IReadMessage inc = new ReadOnlyMessage(incBuf, false, 0, incBuf.Length, OwnerConnection); P2PEndpoint? senderEndpoint = null;
try
HandleDataMessage(inc); {
IReadMessage inc = new ReadOnlyMessage(incBuf, false, 0, incBuf.Length, OwnerConnection);
HandleDataMessage(inc, out senderEndpoint);
}
catch (NetStructReadException)
{
//kick the client if we fail to parse their message
if (senderEndpoint != null && senderEndpoint != ownerEndpoint)
{
if (pendingClients.Find(c => c.Connection.Endpoint == senderEndpoint) is { } pendingClient)
{
RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.MalformedData));
}
if (connectedClients.Find(c => c.Connection.Endpoint == senderEndpoint) is { } connectedClient)
{
Disconnect(connectedClient.Connection, PeerDisconnectPacket.WithReason(DisconnectReason.MalformedData));
}
break;
}
throw;
}
} }
} }
@@ -100,8 +120,9 @@ namespace Barotrauma.Networking
} }
} }
private void HandleDataMessage(IReadMessage inc) private void HandleDataMessage(IReadMessage inc, out P2PEndpoint? senderEndPoint)
{ {
senderEndPoint = null;
if (!started) { return; } if (!started) { return; }
var senderInfo = INetSerializableStruct.Read<P2POwnerToServerHeader>(inc); var senderInfo = INetSerializableStruct.Read<P2POwnerToServerHeader>(inc);
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>1.10.5.0</Version> <Version>1.10.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<contentpackage name="[DebugOnlyTest]testGapSub" modversion="1.0.3" corepackage="False" gameversion="1.9.5.0"> <contentpackage name="[DebugOnlyTest]testGapSub" modversion="1.0.4" corepackage="False" gameversion="1.10.4.0">
<Submarine file="%ModDir%/testGapSub.sub" /> <Submarine file="%ModDir%/testGapSub.sub" />
</contentpackage> </contentpackage>
@@ -1,5 +1,6 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Xml.Linq; using System.Xml.Linq;
namespace Barotrauma.Items.Components namespace Barotrauma.Items.Components
@@ -12,8 +13,8 @@ namespace Barotrauma.Items.Components
[Serialize(1.0f, IsPropertySaveable.No, description: "How fast the item changes the color of the walls.")] [Serialize(1.0f, IsPropertySaveable.No, description: "How fast the item changes the color of the walls.")]
public float SprayStrength { get; set; } public float SprayStrength { get; set; }
private readonly Dictionary<Identifier, Color> liquidColors; public readonly ImmutableDictionary<Identifier, Color> LiquidColors;
private ItemContainer liquidContainer; public ItemContainer LiquidContainer { get; private set; }
public Sprayer(Item item, ContentXElement element) : base(item, element) public Sprayer(Item item, ContentXElement element) : base(item, element)
{ {
@@ -26,7 +27,7 @@ namespace Barotrauma.Items.Components
{ {
case "paintcolors": case "paintcolors":
{ {
liquidColors = new Dictionary<Identifier, Color>(); var liquidColors = new Dictionary<Identifier, Color>();
foreach (XElement paintElement in subElement.Elements()) foreach (XElement paintElement in subElement.Elements())
{ {
Identifier paintName = paintElement.GetAttributeIdentifier("paintitem", Identifier.Empty); Identifier paintName = paintElement.GetAttributeIdentifier("paintitem", Identifier.Empty);
@@ -37,6 +38,7 @@ namespace Barotrauma.Items.Components
liquidColors.Add(paintName, paintColor); liquidColors.Add(paintName, paintColor);
} }
} }
LiquidColors = liquidColors.ToImmutableDictionary();
} }
break; break;
} }
@@ -46,7 +48,7 @@ namespace Barotrauma.Items.Components
public override void OnItemLoaded() public override void OnItemLoaded()
{ {
liquidContainer = item.GetComponent<ItemContainer>(); LiquidContainer = item.GetComponent<ItemContainer>();
} }
partial void InitProjSpecific(ContentXElement element); partial void InitProjSpecific(ContentXElement element);
@@ -59,6 +59,8 @@ namespace Barotrauma
private float higherSurface; private float higherSurface;
private float lowerSurface; private float lowerSurface;
private float waterFlowThisFrame;
private Vector2 lerpedFlowForce; private Vector2 lerpedFlowForce;
//if set to true, hull connections of this gap won't be updated when changes are being done to hulls //if set to true, hull connections of this gap won't be updated when changes are being done to hulls
@@ -503,6 +505,7 @@ namespace Barotrauma
delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - hull1.WaterVolume); delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - hull1.WaterVolume);
hull1.WaterVolume += delta; hull1.WaterVolume += delta;
hull2.WaterVolume -= delta; hull2.WaterVolume -= delta;
waterFlowThisFrame += delta;
if (hull1.WaterVolume > hull1.Volume) if (hull1.WaterVolume > hull1.Volume)
{ {
hull1.Pressure = Math.Max(hull1.Pressure, (hull1.Pressure + hull2.Pressure+subOffset.Y) / 2); hull1.Pressure = Math.Max(hull1.Pressure, (hull1.Pressure + hull2.Pressure+subOffset.Y) / 2);
@@ -529,7 +532,7 @@ namespace Barotrauma
{ {
hull2.Pressure = Math.Max(hull2.Pressure, ((hull1.Pressure-subOffset.Y) + hull2.Pressure) / 2); hull2.Pressure = Math.Max(hull2.Pressure, ((hull1.Pressure-subOffset.Y) + hull2.Pressure) / 2);
} }
waterFlowThisFrame += delta;
flowForce = new Vector2(delta * (float)(Timing.Step / deltaTime), 0.0f); flowForce = new Vector2(delta * (float)(Timing.Step / deltaTime), 0.0f);
} }
@@ -569,6 +572,7 @@ namespace Barotrauma
delta = Math.Max(delta, 0.0f); delta = Math.Max(delta, 0.0f);
hull1.WaterVolume += delta; hull1.WaterVolume += delta;
hull2.WaterVolume -= delta; hull2.WaterVolume -= delta;
waterFlowThisFrame += delta;
flowForce = new Vector2( flowForce = new Vector2(
0.0f, 0.0f,
@@ -597,6 +601,7 @@ namespace Barotrauma
} }
hull1.WaterVolume -= delta; hull1.WaterVolume -= delta;
hull2.WaterVolume += delta; hull2.WaterVolume += delta;
waterFlowThisFrame += delta;
flowForce = new Vector2( flowForce = new Vector2(
hull1.WaveY[hull1.GetWaveIndex(rect.X)] - hull1.WaveY[hull1.GetWaveIndex(rect.Right)], hull1.WaveY[hull1.GetWaveIndex(rect.X)] - hull1.WaveY[hull1.GetWaveIndex(rect.Right)],
@@ -734,6 +739,11 @@ namespace Barotrauma
return (linkedTo[0] == hull1 ? linkedTo[1] : linkedTo[0]) as Hull; return (linkedTo[0] == hull1 ? linkedTo[1] : linkedTo[0]) as Hull;
} }
public void ResetWaterFlowThisFrame()
{
waterFlowThisFrame = 0.0f;
}
private static readonly HashSet<Hull> checkedHulls = new HashSet<Hull>(); private static readonly HashSet<Hull> checkedHulls = new HashSet<Hull>();
/// <summary> /// <summary>
@@ -758,10 +768,24 @@ namespace Barotrauma
const float decay = 0.95f; const float decay = 0.95f;
maxFlow = Math.Min(maxFlow, gap.GetWaterFlowFromOutside(targetHull, deltaTime, ignoreCurrentWater: true)) * decay; maxFlow = Math.Min(maxFlow, gap.GetWaterFlowFromOutside(targetHull, deltaTime, ignoreCurrentWater: true)) * decay;
//if the hulls are not linked (i.e. not parts of the same room), limit the flow a bit
var sourceHull = gap.GetOtherLinkedHull(targetHull);
if (sourceHull != null && !sourceHull.linkedTo.Contains(targetHull))
{
maxFlow *= 0.5f;
}
//take the amount of water that has already passed through this gap into account
//(if there's multiple leaks to the outside recursively passing water through the same gap, the flow should not go above the maximum flow through this gap)
maxFlow -= gap.waterFlowThisFrame;
if (maxFlow <= 0.001f) { return; } if (maxFlow <= 0.001f) { return; }
checkedHulls.Add(targetHull); checkedHulls.Add(targetHull);
gap.waterFlowThisFrame += maxFlow;
//don't multiply by deltatime here, we already did that in GetWaterFlowFromOutside //don't multiply by deltatime here, we already did that in GetWaterFlowFromOutside
targetHull.WaterVolume += maxFlow; targetHull.WaterVolume += maxFlow;
//lerp lethal pressure up very fast //lerp lethal pressure up very fast
@@ -401,7 +401,11 @@ namespace Barotrauma
private set; private set;
} }
private readonly HashSet<int> pendingSectionUpdates = new HashSet<int>();
/// <summary>
/// Note that sector != section: a sector is a group of <see cref="BackgroundSectionsPerNetworkEvent"/> sections that are updated together in a single network event.
/// </summary>
private readonly HashSet<int> pendingSectorUpdates = new HashSet<int>();
public int xBackgroundMax, yBackgroundMax; public int xBackgroundMax, yBackgroundMax;
@@ -413,8 +417,8 @@ namespace Barotrauma
} }
} }
private const int sectorWidth = 4; private const int SectionWidth = 4;
private const int sectorHeight = 4; private const int SectionHeight = 4;
private const float minColorStrength = 0.0f; private const float minColorStrength = 0.0f;
private const float maxColorStrength = 0.7f; private const float maxColorStrength = 0.7f;
@@ -808,7 +812,7 @@ namespace Barotrauma
int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent; int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent;
int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1); int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1);
msg.WriteRangedInteger(sectorToUpdate, 0, BackgroundSections.Count - 1); msg.WriteRangedInteger(sectorToUpdate, 0, BackgroundSections.Count - 1);
for (int i = start; i < end; i++) for (int i = start; i <= end; i++)
{ {
msg.WriteRangedSingle(BackgroundSections[i].ColorStrength, 0.0f, 1.0f, 8); msg.WriteRangedSingle(BackgroundSections[i].ColorStrength, 0.0f, 1.0f, 8);
msg.WriteUInt32(BackgroundSections[i].Color.PackedValue); msg.WriteUInt32(BackgroundSections[i].Color.PackedValue);
@@ -859,12 +863,12 @@ namespace Barotrauma
} }
} }
private void SharedBackgroundSectionRead(IReadMessage msg, Action<BackgroundSectionNetworkUpdate> action, out int sectorToUpdate) private void SharedBackgroundSectionRead(IReadMessage msg, Action<BackgroundSectionNetworkUpdate> action, out int sectionToUpdate)
{ {
sectorToUpdate = msg.ReadRangedInteger(0, BackgroundSections.Count - 1); sectionToUpdate = msg.ReadRangedInteger(0, BackgroundSections.Count - 1);
int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent; int start = sectionToUpdate * BackgroundSectionsPerNetworkEvent;
int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1); int end = Math.Min((sectionToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1);
for (int i = start; i < end; i++) for (int i = start; i <= end; i++)
{ {
float colorStrength = msg.ReadRangedSingle(0.0f, 1.0f, 8); float colorStrength = msg.ReadRangedSingle(0.0f, 1.0f, 8);
Color color = new Color(msg.ReadUInt32()); Color color = new Color(msg.ReadUInt32());
@@ -1460,14 +1464,14 @@ namespace Barotrauma
BackgroundSections = new List<BackgroundSection>(xBackgroundMax * yBackgroundMax); BackgroundSections = new List<BackgroundSection>(xBackgroundMax * yBackgroundMax);
int sections = xBackgroundMax * yBackgroundMax; int sections = xBackgroundMax * yBackgroundMax;
float xSectors = xBackgroundMax / (float)sectorWidth; float xSectors = xBackgroundMax / (float)SectionWidth;
for (int y = 0; y < yBackgroundMax; y++) for (int y = 0; y < yBackgroundMax; y++)
{ {
for (int x = 0; x < xBackgroundMax; x++) for (int x = 0; x < xBackgroundMax; x++)
{ {
ushort index = (ushort)BackgroundSections.Count; ushort index = (ushort)BackgroundSections.Count;
int sector = (int)Math.Floor(index / (float)sectorWidth - xSectors * y) + y / sectorHeight * (int)Math.Ceiling(xSectors); int sector = (int)Math.Floor(index / (float)SectionWidth - xSectors * y) + y / SectionHeight * (int)Math.Ceiling(xSectors);
BackgroundSections.Add(new BackgroundSection(new Rectangle(x * sectionWidth, y * -sectionHeight, sectionWidth, sectionHeight), index, (ushort)y)); BackgroundSections.Add(new BackgroundSection(new Rectangle(x * sectionWidth, y * -sectionHeight, sectionWidth, sectionHeight), index, (ushort)y));
} }
} }
@@ -1504,6 +1508,12 @@ namespace Barotrauma
return BackgroundSections[xIndex + yIndex * xBackgroundMax]; return BackgroundSections[xIndex + yIndex * xBackgroundMax];
} }
public Vector2 GetBackgroundSectionWorldPos(BackgroundSection backgroundSection)
{
Vector2 subOffset = Submarine == null ? Vector2.Zero : Submarine.Position;
return Rect.Location.ToVector2() + subOffset + new Vector2(backgroundSection.Rect.X, backgroundSection.Rect.Y);
}
public IEnumerable<BackgroundSection> GetBackgroundSectionsViaContaining(Rectangle rectArea) public IEnumerable<BackgroundSection> GetBackgroundSectionsViaContaining(Rectangle rectArea)
{ {
if (BackgroundSections == null || BackgroundSections.Count == 0) if (BackgroundSections == null || BackgroundSections.Count == 0)
@@ -1573,7 +1583,7 @@ namespace Barotrauma
if (sectionUpdated && GameMain.NetworkMember != null && requiresUpdate) if (sectionUpdated && GameMain.NetworkMember != null && requiresUpdate)
{ {
networkUpdatePending = true; networkUpdatePending = true;
pendingSectionUpdates.Add((int)Math.Floor(section.Index / (float)BackgroundSectionsPerNetworkEvent)); pendingSectorUpdates.Add((int)Math.Floor(section.Index / (float)BackgroundSectionsPerNetworkEvent));
#if CLIENT #if CLIENT
serverUpdateDelay = 0.5f; serverUpdateDelay = 0.5f;
#endif #endif
@@ -654,6 +654,10 @@ namespace Barotrauma
structure.Update(deltaTime, cam); structure.Update(deltaTime, cam);
} }
foreach (Gap gap in Gap.GapList)
{
gap.ResetWaterFlowThisFrame();
}
//update gaps in random order, because otherwise in rooms with multiple gaps //update gaps in random order, because otherwise in rooms with multiple gaps
//the water/air will always tend to flow through the first gap in the list, //the water/air will always tend to flow through the first gap in the list,
//which may lead to weird behavior like water draining down only through //which may lead to weird behavior like water draining down only through
@@ -1,4 +1,4 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -99,7 +99,7 @@ namespace Barotrauma
{ {
if (inc.BitPosition >= inc.LengthBits) if (inc.BitPosition >= inc.LengthBits)
{ {
throw new Exception("Failed to find the end of the bit field: end of the message reached."); throw new NetStructReadException("Failed to find the end of the bit field: end of the message reached.");
} }
currentByte = inc.ReadByte(); currentByte = inc.ReadByte();
bytes.Add(currentByte); bytes.Add(currentByte);
@@ -1,5 +1,4 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
@@ -13,6 +12,11 @@ using Microsoft.Xna.Framework;
namespace Barotrauma namespace Barotrauma
{ {
public class NetStructReadException : Exception
{
public NetStructReadException(string message, Exception? innerException = null) : base(message, innerException) { }
}
/// <summary> /// <summary>
/// Marks fields and properties as to be serialized and deserialized by <see cref="INetSerializableStruct"/>. /// Marks fields and properties as to be serialized and deserialized by <see cref="INetSerializableStruct"/>.
/// Also contains settings for some types like maximum and minimum values for numbers to reduce bits used. /// Also contains settings for some types like maximum and minimum values for numbers to reduce bits used.
@@ -730,8 +734,15 @@ namespace Barotrauma
/// <returns>A new struct of type T with fields and properties deserialized</returns> /// <returns>A new struct of type T with fields and properties deserialized</returns>
public static T Read<T>(IReadMessage inc) where T : INetSerializableStruct public static T Read<T>(IReadMessage inc) where T : INetSerializableStruct
{ {
ReadOnlyBitField bitField = new ReadOnlyBitField(inc); try
return ReadInternal<T>(inc, bitField); {
ReadOnlyBitField bitField = new ReadOnlyBitField(inc);
return ReadInternal<T>(inc, bitField);
}
catch (Exception e)
{
throw new NetStructReadException($"Failed to read {nameof(INetSerializableStruct)}", e);
}
} }
public static T ReadInternal<T>(IReadMessage inc, ReadOnlyBitField bitField) where T : INetSerializableStruct public static T ReadInternal<T>(IReadMessage inc, ReadOnlyBitField bitField) where T : INetSerializableStruct
@@ -749,7 +760,7 @@ namespace Barotrauma
} }
catch (Exception exception) catch (Exception exception)
{ {
throw new Exception($"Failed to assign" + throw new NetStructReadException($"Failed to assign" +
$" {value ?? "[NULL]"} ({value?.GetType().Name ?? "[NULL]"})" + $" {value ?? "[NULL]"} ({value?.GetType().Name ?? "[NULL]"})" +
$" to {typeof(T).Name}.{property.Name} ({property.Type.Name})", exception); $" to {typeof(T).Name}.{property.Name} ({property.Type.Name})", exception);
} }
@@ -648,6 +648,17 @@ namespace Barotrauma.Networking
private set; private set;
} }
/// <summary>
/// Does the server allow interacting with NPCs that offer services (e.g. stores) remotely?
/// Can be enabled if you're using mods that allow remote interactions - disabled by default to prevent modified clients from cheating.
/// </summary>
[Serialize(false, IsPropertySaveable.Yes)]
public bool AllowRemoteCampaignInteractions
{
get;
private set;
} = false;
private bool voiceChatEnabled; private bool voiceChatEnabled;
[Serialize(true, IsPropertySaveable.Yes)] [Serialize(true, IsPropertySaveable.Yes)]
public bool VoiceChatEnabled public bool VoiceChatEnabled
+10
View File
@@ -1,4 +1,14 @@
------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------
v1.10.6.0
-------------------------------------------------------------------------------------------------------------------------------------------------
- Fixed contextual orders no longer working even if you were within radio/speaking range of a bot.
- Fixes to several multiplayer exploits, including one that allowed clients to cause server-side lag by sending specifically crafted messages to the server, even if the server didn't allow them to join.
- Fixed a bug caused by the water flow changes in the previous update: water flow through gaps in inside the submarine was much faster than intended when there were multiple breaches on the submarine's outer hull.
- Reduced the water flow through gaps when the hulls on are not linked. Linked hulls are intended to behave as if they were one room, and should allow unrestricted water flow.
- Railgun shells now disappear shortly after the first impact: the change in the previous update (which allowed them to penetrate multiple limbs/targets) had the side-effect that a shell that went through a single limb could fall back on the submarine and still do full damage.
-------------------------------------------------------------------------------------------------------------------------------------------------
v1.10.5.0 v1.10.5.0
------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------