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

View File

@@ -73,7 +73,7 @@ body:
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.
options:
- v1.10.5.0 (Autumn Update 2025)
- v1.10.6.0 (Autumn Update 2025 Hotfix 1)
- Other
validations:
required: true

View File

@@ -2866,7 +2866,7 @@ namespace Barotrauma
}
contextualOrders.RemoveAll(o => !IsOrderAvailable(o));
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++)
{
var order = contextualOrders[i];

View File

@@ -238,11 +238,11 @@ namespace Barotrauma.Items.Components
public void Spray(Character user, float deltaTime, bool applyColors)
{
Item liquidItem = liquidContainer?.Inventory.FirstOrDefault();
Item liquidItem = LiquidContainer?.Inventory.FirstOrDefault();
if (liquidItem == null) { return; }
bool isCleaning = false;
liquidColors.TryGetValue(liquidItem.Prefab.Identifier, out color);
LiquidColors.TryGetValue(liquidItem.Prefab.Identifier, out color);
if (applyColors && targetSections.Any())
{

View File

@@ -183,7 +183,7 @@ namespace Barotrauma
networkUpdateTimer += deltaTime;
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
//they should be usable even when not controlling a character
@@ -194,11 +194,11 @@ namespace Barotrauma
GameMain.Client?.CreateEntityEvent(this, new DecalEventData(decal));
}
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;
networkUpdateTimer = 0.0f;
}

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.10.5.0</Version>
<Version>1.10.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.10.5.0</Version>
<Version>1.10.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.10.5.0</Version>
<Version>1.10.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>1.10.5.0</Version>
<Version>1.10.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>1.10.5.0</Version>
<Version>1.10.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -933,207 +933,235 @@ namespace Barotrauma
CheckTooManyMissions(Map.CurrentLocation, sender);
}
var prevBuyCrateItems = new Dictionary<Identifier, List<PurchasedItem>>();
foreach (var kvp in CargoManager.ItemsInBuyCrate)
if (HasCampaignInteractionAvailable(sender, InteractionType.Store))
{
prevBuyCrateItems.Add(kvp.Key, new List<PurchasedItem>(kvp.Value));
}
foreach (var store in prevBuyCrateItems)
{
foreach (var item in store.Value.ToList())
var prevBuyCrateItems = new Dictionary<Identifier, List<PurchasedItem>>();
foreach (var kvp in CargoManager.ItemsInBuyCrate)
{
CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, -item.Quantity, sender);
prevBuyCrateItems.Add(kvp.Key, new List<PurchasedItem>(kvp.Value));
}
}
foreach (var store in buyCrateItems)
{
foreach (var item in store.Value.ToList())
foreach (var store in prevBuyCrateItems)
{
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;
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())
foreach (var item in store.Value.ToList())
{
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})")));
CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, -item.Quantity, sender);
}
}
DebugConsole.Log($" New purchases:");
foreach (var purchasedItem in purchasedItems[storeId])
foreach (var store in buyCrateItems)
{
if (purchasedItem.Delivered) { continue; }
int quantity = purchasedItem.Quantity;
if (alreadyPurchased != null)
foreach (var item in store.Value.ToList())
{
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})");
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;
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);
}
}
}
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)
var prevPurchasedItems = new Dictionary<Identifier, List<PurchasedItem>>();
foreach (var kvp in CargoManager.PurchasedItems)
{
int desiredQuantity = purchasedItem.Quantity;
if (prevPurchasedItems.TryGetValue(storeId, out var alreadyPurchasedList) &&
alreadyPurchasedList.FirstOrDefault(p => p.ItemPrefab == purchasedItem.ItemPrefab && p.DeliverImmediately == purchasedItem.DeliverImmediately) is { } alreadyPurchased)
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))
{
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;
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;
}
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)
DebugConsole.Log($" New purchases:");
foreach (var purchasedItem in purchasedItems[storeId])
{
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;
}
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);
}
}
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())
var prevSubSellCrateItems = new Dictionary<Identifier, List<PurchasedItem>>(CargoManager.ItemsInSellFromSubCrate);
foreach (var store in prevSubSellCrateItems)
{
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);
if (allowedToSellInventoryItems && allowedToSellSubItems)
private bool HasCampaignInteractionAvailable(Client sender, InteractionType interactionType)
{
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
// 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)
if (otherCharacter.CampaignInteractionType != interactionType) { continue; }
//larger-than default maximum distance to give legit clients some leeway if their position is a bit off
if (sender.Character.CanInteractWith(otherCharacter, maxDist: 250.0f))
{
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);
}
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 true;
}
}
return false;
}
public void ServerReadMoney(IReadMessage msg, Client sender)
@@ -1265,7 +1293,7 @@ namespace Barotrauma
CharacterInfo firedCharacter = null;
(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))
{
@@ -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
if (renameCharacter && existingCrewMember)

View File

@@ -64,15 +64,15 @@ namespace Barotrauma
decalUpdateTimer = 0;
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;
pendingSectionUpdates.Clear();
pendingSectorUpdates.Clear();
}
}
@@ -177,6 +177,8 @@ namespace Barotrauma
}
break;
case EventType.BackgroundSections:
bool addPendingSectorUpdate = false;
SharedBackgroundSectionRead(
msg,
bsnu =>
@@ -185,17 +187,67 @@ namespace Barotrauma
Color color = bsnu.Color;
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 { AllowInput: true })) { return; }
if (c.Character.HeldItems.All(it => it.GetComponent<Sprayer>() == null)) { return; }
if (c.Character is not { AllowInput: true }) { 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].SetColor(color);
},
out int sectorToUpdate);
RefreshAveragePaintedColor();
//add to pending updates to notify other clients as well
pendingSectionUpdates.Add(sectorToUpdate);
if (addPendingSectorUpdate)
{
RefreshAveragePaintedColor();
//add to pending updates to notify other clients as well
pendingSectorUpdates.Add(sectorToUpdate);
}
break;
case EventType.Decal:
byte decalIndex = msg.ReadByte();
@@ -209,7 +261,7 @@ namespace Barotrauma
break;
default:
throw new Exception($"Malformed incoming hull event: {eventType} is not a supported event type");
}
}
}
}
}

View File

@@ -4384,9 +4384,22 @@ namespace Barotrauma.Networking
moustacheIndex: netInfo.MoustacheIndex,
faceAttachmentIndex: netInfo.FaceAttachmentIndex);
sender.CharacterInfo.Head.SkinColor = netInfo.SkinColor;
sender.CharacterInfo.Head.HairColor = netInfo.HairColor;
sender.CharacterInfo.Head.FacialHairColor = netInfo.FacialHairColor;
sender.CharacterInfo.Head.SkinColor = validateColor(netInfo.SkinColor, "skin color", sender.CharacterInfo.SkinColors.Select(kvp => kvp.Color));
sender.CharacterInfo.Head.HairColor = validateColor(netInfo.HairColor, "hair color", sender.CharacterInfo.HairColors.Select(kvp => kvp.Color));
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)
{

View File

@@ -269,8 +269,27 @@ namespace Barotrauma.Networking
return;
}
var packet = INetSerializableStruct.Read<PeerPacketMessage>(inc);
callbacks.OnMessageReceived.Invoke(conn, packet.GetReadMessage(packetHeader.IsCompressed(), conn));
try
{
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)

View File

@@ -73,11 +73,31 @@ namespace Barotrauma.Networking
try
{
foreach (var incBuf in ChildServerRelay.Read())
foreach (byte[] incBuf in ChildServerRelay.Read())
{
IReadMessage inc = new ReadOnlyMessage(incBuf, false, 0, incBuf.Length, OwnerConnection);
HandleDataMessage(inc);
P2PEndpoint? senderEndpoint = null;
try
{
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; }
var senderInfo = INetSerializableStruct.Read<P2POwnerToServerHeader>(inc);

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>1.10.5.0</Version>
<Version>1.10.6.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -1,4 +1,4 @@
<?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" />
</contentpackage>

View File

@@ -1,5 +1,6 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Xml.Linq;
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.")]
public float SprayStrength { get; set; }
private readonly Dictionary<Identifier, Color> liquidColors;
private ItemContainer liquidContainer;
public readonly ImmutableDictionary<Identifier, Color> LiquidColors;
public ItemContainer LiquidContainer { get; private set; }
public Sprayer(Item item, ContentXElement element) : base(item, element)
{
@@ -26,7 +27,7 @@ namespace Barotrauma.Items.Components
{
case "paintcolors":
{
liquidColors = new Dictionary<Identifier, Color>();
var liquidColors = new Dictionary<Identifier, Color>();
foreach (XElement paintElement in subElement.Elements())
{
Identifier paintName = paintElement.GetAttributeIdentifier("paintitem", Identifier.Empty);
@@ -37,6 +38,7 @@ namespace Barotrauma.Items.Components
liquidColors.Add(paintName, paintColor);
}
}
LiquidColors = liquidColors.ToImmutableDictionary();
}
break;
}
@@ -46,7 +48,7 @@ namespace Barotrauma.Items.Components
public override void OnItemLoaded()
{
liquidContainer = item.GetComponent<ItemContainer>();
LiquidContainer = item.GetComponent<ItemContainer>();
}
partial void InitProjSpecific(ContentXElement element);

View File

@@ -59,6 +59,8 @@ namespace Barotrauma
private float higherSurface;
private float lowerSurface;
private float waterFlowThisFrame;
private Vector2 lerpedFlowForce;
//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);
hull1.WaterVolume += delta;
hull2.WaterVolume -= delta;
waterFlowThisFrame += delta;
if (hull1.WaterVolume > hull1.Volume)
{
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);
}
waterFlowThisFrame += delta;
flowForce = new Vector2(delta * (float)(Timing.Step / deltaTime), 0.0f);
}
@@ -569,6 +572,7 @@ namespace Barotrauma
delta = Math.Max(delta, 0.0f);
hull1.WaterVolume += delta;
hull2.WaterVolume -= delta;
waterFlowThisFrame += delta;
flowForce = new Vector2(
0.0f,
@@ -597,6 +601,7 @@ namespace Barotrauma
}
hull1.WaterVolume -= delta;
hull2.WaterVolume += delta;
waterFlowThisFrame += delta;
flowForce = new Vector2(
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;
}
public void ResetWaterFlowThisFrame()
{
waterFlowThisFrame = 0.0f;
}
private static readonly HashSet<Hull> checkedHulls = new HashSet<Hull>();
/// <summary>
@@ -758,10 +768,24 @@ namespace Barotrauma
const float decay = 0.95f;
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; }
checkedHulls.Add(targetHull);
gap.waterFlowThisFrame += maxFlow;
//don't multiply by deltatime here, we already did that in GetWaterFlowFromOutside
targetHull.WaterVolume += maxFlow;
//lerp lethal pressure up very fast

View File

@@ -401,7 +401,11 @@ namespace Barotrauma
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;
@@ -413,8 +417,8 @@ namespace Barotrauma
}
}
private const int sectorWidth = 4;
private const int sectorHeight = 4;
private const int SectionWidth = 4;
private const int SectionHeight = 4;
private const float minColorStrength = 0.0f;
private const float maxColorStrength = 0.7f;
@@ -808,7 +812,7 @@ namespace Barotrauma
int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent;
int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, 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.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);
int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent;
int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1);
for (int i = start; i < end; i++)
sectionToUpdate = msg.ReadRangedInteger(0, BackgroundSections.Count - 1);
int start = sectionToUpdate * BackgroundSectionsPerNetworkEvent;
int end = Math.Min((sectionToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1);
for (int i = start; i <= end; i++)
{
float colorStrength = msg.ReadRangedSingle(0.0f, 1.0f, 8);
Color color = new Color(msg.ReadUInt32());
@@ -1460,14 +1464,14 @@ namespace Barotrauma
BackgroundSections = new List<BackgroundSection>(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 x = 0; x < xBackgroundMax; x++)
{
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));
}
}
@@ -1504,6 +1508,12 @@ namespace Barotrauma
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)
{
if (BackgroundSections == null || BackgroundSections.Count == 0)
@@ -1573,7 +1583,7 @@ namespace Barotrauma
if (sectionUpdated && GameMain.NetworkMember != null && requiresUpdate)
{
networkUpdatePending = true;
pendingSectionUpdates.Add((int)Math.Floor(section.Index / (float)BackgroundSectionsPerNetworkEvent));
pendingSectorUpdates.Add((int)Math.Floor(section.Index / (float)BackgroundSectionsPerNetworkEvent));
#if CLIENT
serverUpdateDelay = 0.5f;
#endif

View File

@@ -654,6 +654,10 @@ namespace Barotrauma
structure.Update(deltaTime, cam);
}
foreach (Gap gap in Gap.GapList)
{
gap.ResetWaterFlowThisFrame();
}
//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,
//which may lead to weird behavior like water draining down only through

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System;
using System.Collections.Generic;
@@ -99,7 +99,7 @@ namespace Barotrauma
{
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();
bytes.Add(currentByte);

View File

@@ -1,5 +1,4 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -13,6 +12,11 @@ using Microsoft.Xna.Framework;
namespace Barotrauma
{
public class NetStructReadException : Exception
{
public NetStructReadException(string message, Exception? innerException = null) : base(message, innerException) { }
}
/// <summary>
/// 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.
@@ -730,8 +734,15 @@ namespace Barotrauma
/// <returns>A new struct of type T with fields and properties deserialized</returns>
public static T Read<T>(IReadMessage inc) where T : INetSerializableStruct
{
ReadOnlyBitField bitField = new ReadOnlyBitField(inc);
return ReadInternal<T>(inc, bitField);
try
{
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
@@ -749,7 +760,7 @@ namespace Barotrauma
}
catch (Exception exception)
{
throw new Exception($"Failed to assign" +
throw new NetStructReadException($"Failed to assign" +
$" {value ?? "[NULL]"} ({value?.GetType().Name ?? "[NULL]"})" +
$" to {typeof(T).Name}.{property.Name} ({property.Type.Name})", exception);
}

View File

@@ -648,6 +648,17 @@ namespace Barotrauma.Networking
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;
[Serialize(true, IsPropertySaveable.Yes)]
public bool VoiceChatEnabled

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
-------------------------------------------------------------------------------------------------------------------------------------------------