From 4b392079e9607804764f8ff4cbefabdda842b1f0 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:34:35 -0800 Subject: [PATCH] fix: Fixes exploits with bank box deposits. Makes stacking more efficient (#2337) ### Summary - Fix AccountGold gold duplication exploit: When AccountGold.Enabled was true, double-clicking a BankCheck deposited the full value to the account but then continued creating physical gold piles for the same amount, duplicating the value - Fix Deposit/DepositUpTo partial deposit exploit: Both methods created new max-size gold piles and checks without first filling existing partial stacks, wasting container slots and causing premature "bank full" failures that could be leveraged to manipulate gold distribution - Fix BagOfSending bank stacking: Gold and BankCheck items sent via BagOfSending now use Banker.Deposit for efficient stacking instead of naive TryDropItem, which could fail on a full bank even when existing piles had room - Improve gold/check deposit efficiency: Banker.Deposit and Banker.DepositUpTo now top off existing gold piles (up to 60k) and bank checks (up to 1M) before creating new items, maximizing use of available container slots --- Projects/UOContent/Items/Misc/BankCheck.cs | 89 ++++---- .../Items/Special/Solen Items/BagOfSending.cs | 34 ++- Projects/UOContent/Mobiles/Townfolk/Banker.cs | 211 +++++++++++++----- 3 files changed, 232 insertions(+), 102 deletions(-) diff --git a/Projects/UOContent/Items/Misc/BankCheck.cs b/Projects/UOContent/Items/Misc/BankCheck.cs index 1f42e6383..a3e38404c 100644 --- a/Projects/UOContent/Items/Misc/BankCheck.cs +++ b/Projects/UOContent/Items/Misc/BankCheck.cs @@ -137,68 +137,69 @@ public partial class BankCheck : Item return; } - var deposited = 0; - var toAdd = _worth; - - if (AccountGold.Enabled && from.Account?.DepositGold(toAdd) == true) + if (AccountGold.Enabled) { - deposited = toAdd; - } - - while (toAdd > 0) - { - var amount = Math.Min(toAdd, 60000); - - var gold = new Gold(amount); - - if (box.TryDropItem(from, gold, false)) + if (from.Account?.DepositGold(_worth) != true) { - toAdd -= amount; - deposited += amount; + return; } - else - { - gold.Delete(); - break; - } - } - if (deposited >= _worth) - { Delete(); + + // Gold was deposited in your account: + from.SendLocalizedMessage(1042672, true, $"{_worth:N0}"); } else { - Worth -= deposited; - } + // Internalize the check to free its slot for deposit. + // reserveSlots: 1 conditionally keeps a slot free for the check to bounce back + // only if the full amount won't fit — if it fits, no reservation is needed. + RecordBounce(); + Internalize(); + + var deposited = Banker.DepositUpTo(from, _worth, false, 1); + + if (deposited >= _worth) + { + Delete(); + } + else if (deposited > 0) + { + Worth -= deposited; + Bounce(from); + } + else + { + Bounce(from); + from.SendLocalizedMessage(500390); // Your bank box is full. + return; + } - if (deposited > 0) - { // Gold was deposited in your account: from.SendLocalizedMessage(1042672, true, $"{deposited:N0}"); + } - if (from is PlayerMobile pm) + if (from is PlayerMobile pm) + { + var qs = pm.Quest; + + if (qs is DarkTidesQuest) { - var qs = pm.Quest; + var obj = qs.FindObjective(); - if (qs is DarkTidesQuest) + if (obj?.Completed == false) { - var obj = qs.FindObjective(); - - if (obj?.Completed == false) - { - obj.Complete(); - } + obj.Complete(); } + } - if (qs is UzeraanTurmoilQuest) + if (qs is UzeraanTurmoilQuest) + { + var obj = qs.FindObjective(typeof(Engines.Quests.Haven.CashBankCheckObjective)); + + if (obj?.Completed == false) { - var obj = qs.FindObjective(typeof(Engines.Quests.Haven.CashBankCheckObjective)); - - if (obj?.Completed == false) - { - obj.Complete(); - } + obj.Complete(); } } } diff --git a/Projects/UOContent/Items/Special/Solen Items/BagOfSending.cs b/Projects/UOContent/Items/Special/Solen Items/BagOfSending.cs index d81470d5d..f2772b342 100644 --- a/Projects/UOContent/Items/Special/Solen Items/BagOfSending.cs +++ b/Projects/UOContent/Items/Special/Solen Items/BagOfSending.cs @@ -5,6 +5,7 @@ using Server.ContextMenus; using Server.Engines.Quests; using Server.Regions; using Server.Spells; +using Server.Mobiles; using Server.Targeting; namespace Server.Items; @@ -233,16 +234,35 @@ public partial class BagOfSending : Item, TranslocationItem { from.SendLocalizedMessage(1079932); // You don't have enough charges to send that much weight } - else if (!from.BankBox.TryDropItem(from, item, false)) - { - _bag.SendLocalizedMessageTo(from, 1054110, 0x59); // Your bank box is full. - } else { - _bag.Charges -= Core.ML ? reqCharges : 1; + bool sent; - // The item was placed in your bank box. - _bag.SendLocalizedMessageTo(from, 1054150, 0x59); + if (item is Gold or BankCheck) + { + var amount = (item as Gold)?.Amount ?? ((BankCheck)item).Worth; + sent = Banker.Deposit(from, amount); + if (sent) + { + item.Delete(); + } + } + else + { + sent = from.BankBox.TryDropItem(from, item, false); + } + + if (sent) + { + _bag.Charges -= Core.ML ? reqCharges : 1; + + // The item was placed in your bank box. + _bag.SendLocalizedMessageTo(from, 1054150, 0x59); + } + else + { + _bag.SendLocalizedMessageTo(from, 1054110, 0x59); // Your bank box is full. + } } } } diff --git a/Projects/UOContent/Mobiles/Townfolk/Banker.cs b/Projects/UOContent/Mobiles/Townfolk/Banker.cs index 7744c1918..e93ddcf4f 100644 --- a/Projects/UOContent/Mobiles/Townfolk/Banker.cs +++ b/Projects/UOContent/Mobiles/Townfolk/Banker.cs @@ -206,7 +206,7 @@ public partial class Banker : BaseVendor return true; } - public static bool Deposit(Mobile from, int amount) + public static bool Deposit(Mobile from, int amount, bool useChecks = true) { // If for whatever reason the TOL checks fail, we should still try old methods for depositing currency. if (amount <= 0 || AccountGold.Enabled && from.Account?.DepositGold(amount) == true) @@ -221,47 +221,99 @@ public partial class Banker : BaseVendor return false; } - using var items = PooledRefQueue.Create(); + using var items = box.ListItemsByType([typeof(Gold), typeof(BankCheck)], false); - while (amount > 0) + // Pre-calculate available capacity without modifying anything + var remaining = amount; + + for (var i = 0; i < items.Count && remaining > 0; i++) { - Item item; - if (amount < 5000) + var item = items[i]; + if (item is Gold g) { - item = new Gold(amount); - amount = 0; + remaining -= Math.Min(60000 - g.Amount, remaining); } - else if (amount <= 1000000) + else if (useChecks && item is BankCheck c) { - item = new BankCheck(amount); - amount = 0; - } - else - { - item = new BankCheck(1000000); - amount -= 1000000; + remaining -= Math.Min(1000000 - c.Worth, remaining); } + } - if (box.TryDropItem(from, item, false)) + // Calculate new item slots needed for the remainder + if (remaining > 0) + { + var slotsNeeded = 0; + var r = remaining; + while (r > 0) { - items.Enqueue(item); - } - else - { - item.Delete(); - while (items.Count > 0) + if (!useChecks || r < 5000) { - items.Dequeue().Delete(); + r -= Math.Min(r, 60000); + } + else if (r <= 1000000) + { + r = 0; + } + else + { + r -= 1000000; } + slotsNeeded++; + } + + var maxItems = box.MaxItems; + if (maxItems != 0 && slotsNeeded > maxItems - box.TotalItems) + { return false; } } + // Capacity verified — execute deposit + remaining = amount; + + for (var i = 0; i < items.Count && remaining > 0; i++) + { + var item = items[i]; + if (item is Gold g && g.Amount < 60000) + { + var add = Math.Min(60000 - g.Amount, remaining); + g.Amount += add; + remaining -= add; + } + else if (useChecks && item is BankCheck c && c.Worth < 1000000) + { + var add = Math.Min(1000000 - c.Worth, remaining); + c.Worth += add; + remaining -= add; + } + } + + // Create new items for the remainder + while (remaining > 0) + { + if (!useChecks || remaining < 5000) + { + var pile = Math.Min(remaining, 60000); + box.DropItem(new Gold(pile)); + remaining -= pile; + } + else if (remaining <= 1000000) + { + box.DropItem(new BankCheck(remaining)); + remaining = 0; + } + else + { + box.DropItem(new BankCheck(1000000)); + remaining -= 1000000; + } + } + return true; } - public static int DepositUpTo(Mobile from, int amount) + public static int DepositUpTo(Mobile from, int amount, bool useChecks = true, int reserveSlots = 0) { // If for whatever reason the TOL checks fail, we should still try old methods for depositing currency. if (AccountGold.Enabled && from.Account?.DepositGold(amount) == true) @@ -276,40 +328,97 @@ public partial class Banker : BaseVendor return 0; } - var amountLeft = amount; - while (amountLeft > 0) + var remaining = amount; + + // Top off existing gold piles and checks + using var items = box.ListItemsByType([typeof(Gold), typeof(BankCheck)], false); + + for (var i = 0; i < items.Count && remaining > 0; i++) { - Item item; - int amountGiven; - - if (amountLeft < 5000) + var item = items[i]; + if (item is Gold g && g.Amount < 60000) { - item = new Gold(amountLeft); - amountGiven = amountLeft; + var add = Math.Min(60000 - g.Amount, remaining); + g.Amount += add; + remaining -= add; } - else if (amountLeft <= 1000000) + else if (useChecks && item is BankCheck c && c.Worth < 1000000) { - item = new BankCheck(amountLeft); - amountGiven = amountLeft; - } - else - { - item = new BankCheck(1000000); - amountGiven = 1000000; - } - - if (box.TryDropItem(from, item, false)) - { - amountLeft -= amountGiven; - } - else - { - item.Delete(); - break; + var add = Math.Min(1000000 - c.Worth, remaining); + c.Worth += add; + remaining -= add; } } - return amount - amountLeft; + if (remaining == 0) + { + return amount; + } + + // Calculate how many new item slots can be filled + int slotsToFill; + var maxItems = box.MaxItems; + if (maxItems == 0 || from.AccessLevel >= AccessLevel.GameMaster) + { + slotsToFill = int.MaxValue; + } + else + { + slotsToFill = maxItems - box.TotalItems; + if (reserveSlots > 0) + { + // Count slots needed to determine if the full amount fits. + // Only reserve if it doesn't — the caller will delete the reserved item on full deposit. + var slots = slotsToFill; + var r = remaining; + while (r > 0) + { + if (!useChecks || r < 5000) + { + r -= Math.Min(r, 60000); + } + else if (r <= 1000000) + { + r = 0; + } + else + { + r -= 1000000; + } + + if (--slots < 0) + { + slotsToFill -= reserveSlots; + break; + } + } + } + } + + // Create new items for the remainder + while (remaining > 0 && slotsToFill > 0) + { + if (!useChecks || remaining < 5000) + { + var pile = Math.Min(remaining, 60000); + box.DropItem(new Gold(pile)); + remaining -= pile; + } + else if (remaining <= 1000000) + { + box.DropItem(new BankCheck(remaining)); + remaining = 0; + } + else + { + box.DropItem(new BankCheck(1000000)); + remaining -= 1000000; + } + + slotsToFill--; + } + + return amount - remaining; } public static void Deposit(Container cont, int amount)