diff --git a/.github/DISCUSSION_TEMPLATE/bug-reports.yml b/.github/DISCUSSION_TEMPLATE/bug-reports.yml index e9c69188b..86b2fa804 100644 --- a/.github/DISCUSSION_TEMPLATE/bug-reports.yml +++ b/.github/DISCUSSION_TEMPLATE/bug-reports.yml @@ -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 diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index e3a8efb27..70b843e89 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -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]; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs index cb72f3474..c878a6495 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs @@ -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()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 4c6eee08a..09e467505 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -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; } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index dfc77450d..5934083da 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.10.5.0 + 1.10.6.0 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 011b3992c..87f35ddb7 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.10.5.0 + 1.10.6.0 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 0ef2232a4..5eca98d1c 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.10.5.0 + 1.10.6.0 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 68d267482..118b45e61 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.10.5.0 + 1.10.6.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index fd8876d56..11e1f7329 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.10.5.0 + 1.10.6.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 7adb32f32..c55f018f1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -933,207 +933,235 @@ namespace Barotrauma CheckTooManyMissions(Map.CurrentLocation, sender); } - var prevBuyCrateItems = new Dictionary>(); - foreach (var kvp in CargoManager.ItemsInBuyCrate) + if (HasCampaignInteractionAvailable(sender, InteractionType.Store)) { - prevBuyCrateItems.Add(kvp.Key, new List(kvp.Value)); - } - foreach (var store in prevBuyCrateItems) - { - foreach (var item in store.Value.ToList()) + var prevBuyCrateItems = new Dictionary>(); + foreach (var kvp in CargoManager.ItemsInBuyCrate) { - CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, -item.Quantity, sender); + prevBuyCrateItems.Add(kvp.Key, new List(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>(); - foreach (var kvp in CargoManager.PurchasedItems) - { - prevPurchasedItems.Add(kvp.Key, new List(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>(); + 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(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 newItems = new List(); - List 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 newItems = new List(); + List 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>(CargoManager.ItemsInSellFromSubCrate); - foreach (var store in prevSubSellCrateItems) - { - foreach (var item in store.Value.ToList()) + var prevSubSellCrateItems = new Dictionary>(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>(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>(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>(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>(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) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index 86a40a127..cfb0592cd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -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() == 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()) + .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"); - } + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 4ce36cbcf..61482754a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -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 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) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index 81083c420..79bfedd93 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -269,8 +269,27 @@ namespace Barotrauma.Networking return; } - var packet = INetSerializableStruct.Read(inc); - callbacks.OnMessageReceived.Invoke(conn, packet.GetReadMessage(packetHeader.IsCompressed(), conn)); + try + { + var packet = INetSerializableStruct.Read(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) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/P2PServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/P2PServerPeer.cs index c170aef88..16e309491 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/P2PServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/P2PServerPeer.cs @@ -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(inc); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 808dc379b..3e24cc9c7 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.10.5.0 + 1.10.6.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]testGapSub/filelist.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]testGapSub/filelist.xml index bb59ee8d3..5caadb4a3 100644 --- a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]testGapSub/filelist.xml +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]testGapSub/filelist.xml @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]testGapSub/testGapSub.sub b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]testGapSub/testGapSub.sub index 9f730ea4c..77747cf3d 100644 Binary files a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]testGapSub/testGapSub.sub and b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]testGapSub/testGapSub.sub differ diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Sprayer.cs index cbf773a34..5ccc6a433 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Sprayer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Sprayer.cs @@ -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 liquidColors; - private ItemContainer liquidContainer; + public readonly ImmutableDictionary 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(); + var liquidColors = new Dictionary(); 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(); + LiquidContainer = item.GetComponent(); } partial void InitProjSpecific(ContentXElement element); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 07e187866..bcd4a8a37 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -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 checkedHulls = new HashSet(); /// @@ -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 diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 2a79f9482..1bba2e53f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -401,7 +401,11 @@ namespace Barotrauma private set; } - private readonly HashSet pendingSectionUpdates = new HashSet(); + + /// + /// Note that sector != section: a sector is a group of sections that are updated together in a single network event. + /// + private readonly HashSet pendingSectorUpdates = new HashSet(); 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 action, out int sectorToUpdate) + private void SharedBackgroundSectionRead(IReadMessage msg, Action 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(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 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 diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index f0d5d35d7..43157965c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -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 diff --git a/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs b/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs index 7398a7f21..73cd53d8b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs @@ -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); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs index 69d121f72..ebb9572ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs @@ -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) { } + } + /// /// Marks fields and properties as to be serialized and deserialized by . /// Also contains settings for some types like maximum and minimum values for numbers to reduce bits used. @@ -730,8 +734,15 @@ namespace Barotrauma /// A new struct of type T with fields and properties deserialized public static T Read(IReadMessage inc) where T : INetSerializableStruct { - ReadOnlyBitField bitField = new ReadOnlyBitField(inc); - return ReadInternal(inc, bitField); + try + { + ReadOnlyBitField bitField = new ReadOnlyBitField(inc); + return ReadInternal(inc, bitField); + } + catch (Exception e) + { + throw new NetStructReadException($"Failed to read {nameof(INetSerializableStruct)}", e); + } } public static T ReadInternal(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); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index a4ef0b132..09573c126 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -648,6 +648,17 @@ namespace Barotrauma.Networking private set; } + /// + /// 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. + /// + [Serialize(false, IsPropertySaveable.Yes)] + public bool AllowRemoteCampaignInteractions + { + get; + private set; + } = false; + private bool voiceChatEnabled; [Serialize(true, IsPropertySaveable.Yes)] public bool VoiceChatEnabled diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index de28279f8..105a78eed 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -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 -------------------------------------------------------------------------------------------------------------------------------------------------