fix(vendor): re-gate residuals — MaxStackSize is the stack operand, wire-authored use radius, purse summaries
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
R1 the split bar's operand is the item's authored MaxStackSize —
three retail sites read pwd._maxStackSize directly (InqListSlotCount
pc:200052, buy-button cases pc:203996/204086) where ACE never fills
the desc stack and standard stock is unlimited. Threaded StackSizeMax
end to end with one shared resolver; the two literal _maxStackSize
sites are now byte-exact; AP-165 retired, AP-169 corrected.
R2 walk-to-vendor never opened because GetUseRadius used an UNCITED
3m Creature heuristic as the local stop distance while ACE's poll
demands the authored radius (default 0.6 m) — the walk stopped and
the Use fired far outside acceptance. Now reads the wire-authored
spawn UseRadius with ACE's exact fallback; heuristic constants
deleted. A first sabotage attempt was non-discriminating
(coincidental 0.6) and was corrected — the discriminating version is
what landed.
R3 the Buying/Selling purse summaries ("Buying %d %s worth %hsp" /
"You have %hsp") recovered from the binary data segment where BN
mis-attributes the Buy-side literal; wired to staging and money
changes on the four authored text elements; AP-166 narrowed to the
pending-sell highlight.
Clean-room complete solution: 11,528 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
68568a3a59
commit
d003449bb4
11 changed files with 680 additions and 103 deletions
|
|
@ -98,6 +98,16 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag
|
|||
public const uint SellingListId = 0x100000CEu;
|
||||
public const uint SellingScrollbarId = 0x100000CFu;
|
||||
|
||||
// R3 (user-requested retail presentation, closing AP-166's text half):
|
||||
// the Buying/Selling tabs' own staged-count/total-value and player-
|
||||
// purse text elements — retail m_buyListText/m_buyPurseText
|
||||
// (VendorBuyUI::VendorBuyUI, pc:199733-199738) and m_sellListText/
|
||||
// m_sellPurseText (VendorSellUI::VendorSellUI, pc:199777-199782).
|
||||
public const uint BuyingListTextId = 0x100000C7u;
|
||||
public const uint BuyingPurseTextId = 0x100000C8u;
|
||||
public const uint SellingListTextId = 0x100000D0u;
|
||||
public const uint SellingPurseTextId = 0x100000D1u;
|
||||
|
||||
// Slice 6b: the "Buying" tab's staging-review buttons
|
||||
// (docs/research/2026-08-08-slice5-vendor-browse-research.md §B.4 D0
|
||||
// tree). All four are optional (nullable) the same way BuyButtonId/
|
||||
|
|
@ -333,6 +343,12 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag
|
|||
private readonly UiMenu _typeMenu;
|
||||
private readonly UiText _itemNameText;
|
||||
private readonly UiText _itemCostText;
|
||||
// R3: the Buying/Selling tabs' own staged summary text — optional, the
|
||||
// same nullable degrade-gracefully convention as the staging buttons.
|
||||
private readonly UiText? _buyListText;
|
||||
private readonly UiText? _buyPurseText;
|
||||
private readonly UiText? _sellListText;
|
||||
private readonly UiText? _sellPurseText;
|
||||
private readonly UiButton? _close;
|
||||
private readonly UiButton? _buyButton;
|
||||
private readonly UiButton? _addButton;
|
||||
|
|
@ -391,6 +407,10 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag
|
|||
UiMenu typeMenu,
|
||||
UiText itemNameText,
|
||||
UiText itemCostText,
|
||||
UiText? buyListText,
|
||||
UiText? buyPurseText,
|
||||
UiText? sellListText,
|
||||
UiText? sellPurseText,
|
||||
UiButton? close,
|
||||
UiButton? buyButton,
|
||||
UiButton? addButton,
|
||||
|
|
@ -431,6 +451,10 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag
|
|||
_typeMenu = typeMenu;
|
||||
_itemNameText = itemNameText;
|
||||
_itemCostText = itemCostText;
|
||||
_buyListText = buyListText;
|
||||
_buyPurseText = buyPurseText;
|
||||
_sellListText = sellListText;
|
||||
_sellPurseText = sellPurseText;
|
||||
_close = close;
|
||||
_buyButton = buyButton;
|
||||
_addButton = addButton;
|
||||
|
|
@ -598,6 +622,20 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag
|
|||
// track staging too — see RefreshItemsTabAvailability's doc.
|
||||
_buyStaging.Changed += RefreshItemsTabAvailability;
|
||||
_sellStaging.Changed += RebuildSellingList;
|
||||
// R3: the tabs' own staged-count/total-value/purse text must track
|
||||
// every staging change, matching retail's Update() -> ...
|
||||
// UpdateTransactionValue()/UpdateTotalValue() chain (VendorBuyUI::Update
|
||||
// pc:202996-203005, VendorSellUI::Update pc:203009-203018) — both
|
||||
// called unconditionally on EVERY staging mutation, not just Add/Remove.
|
||||
_buyStaging.Changed += UpdateBuyTransactionText;
|
||||
_sellStaging.Changed += UpdateSellTransactionText;
|
||||
// R3: a player money change (a purchase/sale elsewhere, a pickup, a
|
||||
// drop) must repaint the purse line even with staging unchanged —
|
||||
// retail's own m_totalValue/m_last_sale-driven purse text has no
|
||||
// separate "staging changed" gate from "holdings changed" (both
|
||||
// UpdateTotalValue calls read the LIVE holding fresh, same as
|
||||
// BuildCostText's own PropertyInt.CoinValue read).
|
||||
_objects.ObjectUpdated += OnObjectMoneyChanged;
|
||||
|
||||
ShowTab(VendorPanelTab.Items);
|
||||
ClearContent();
|
||||
|
|
@ -726,6 +764,13 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag
|
|||
return null;
|
||||
}
|
||||
|
||||
// R3: the Buying/Selling tabs' own summary text — optional, same
|
||||
// degrade-gracefully convention as the staging buttons.
|
||||
UiText? buyListText = layout.FindElement(BuyingListTextId) as UiText;
|
||||
UiText? buyPurseText = layout.FindElement(BuyingPurseTextId) as UiText;
|
||||
UiText? sellListText = layout.FindElement(SellingListTextId) as UiText;
|
||||
UiText? sellPurseText = layout.FindElement(SellingPurseTextId) as UiText;
|
||||
|
||||
UiButton? close = layout.FindElement(CloseId) as UiButton;
|
||||
UiScrollbar? itemScrollbar = layout.FindElement(ItemScrollbarId) as UiScrollbar;
|
||||
UiButton? buyButton = layout.FindElement(BuyButtonId) as UiButton;
|
||||
|
|
@ -770,6 +815,10 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag
|
|||
typeMenu,
|
||||
itemNameText,
|
||||
itemCostText,
|
||||
buyListText,
|
||||
buyPurseText,
|
||||
sellListText,
|
||||
sellPurseText,
|
||||
close,
|
||||
buyButton,
|
||||
addButton,
|
||||
|
|
@ -1351,10 +1400,21 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag
|
|||
/// PREVIOUSLY-selected DIFFERENT stackable item must never leak into
|
||||
/// this one — otherwise the CURRENT slider value via
|
||||
/// <c>ItemHolder::GetObjectSplitSize</c> (<c>0x00586F00</c>).
|
||||
/// <para>
|
||||
/// R1 gate-finding fix (2026-08-08, register AP-169 correction): the
|
||||
/// ceiling this gates on is <see cref="VendorSplitPolicy.ResolveAuthoredStackSize"/>
|
||||
/// — <see cref="VendorShopItem.DescStackSize"/> when the wire carries
|
||||
/// it, else <see cref="VendorShopItem.MaxStackSize"/> — the SAME
|
||||
/// resolution <see cref="VendorShopItemMaterializer"/> uses to seed
|
||||
/// <see cref="ClientObject.StackSize"/> (and therefore the toolbar
|
||||
/// slider's own ceiling, <c>SelectedObjectController</c>). Using a
|
||||
/// narrower source here than the visible slider would let the slider
|
||||
/// show a ceiling of 1000 while every Buy still sent quantity 1.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private uint ResolveBuyQuantity(VendorShopItem item)
|
||||
{
|
||||
uint stackSize = (uint)Math.Max(item.DescStackSize ?? 1, 1);
|
||||
uint stackSize = (uint)VendorSplitPolicy.ResolveAuthoredStackSize(item.DescStackSize, item.MaxStackSize);
|
||||
if (stackSize <= 1u)
|
||||
return 1u;
|
||||
|
||||
|
|
@ -1541,19 +1601,15 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag
|
|||
/// Retail's <c>Buy Item</c>/<c>Clear Item</c> (Buying tab) shared
|
||||
/// removal-amount rule (<c>pc:203996</c>/<c>204086</c>): stackable -> -1
|
||||
/// (full removal), else 1. Retail tests the item's own
|
||||
/// <c>pwd._maxStackSize</c> (the item TYPE's stack ceiling) — a field
|
||||
/// <see cref="VendorShopItem"/> does not carry on the wire today. This
|
||||
/// substitutes <see cref="VendorShopItem.DescStackSize"/> (the item's
|
||||
/// CURRENT authored stack depth, already threaded through for pricing)
|
||||
/// as the stackability test instead; the two agree for every case that
|
||||
/// matters in practice (<c>DescStackSize <= 1</c> implies
|
||||
/// <c>MaxStackSize <= 1</c>) and disagree only for a vendor stocking a
|
||||
/// single unit of an otherwise-stackable item TYPE, where the worst case
|
||||
/// is a staged entry decrementing by one instead of clearing outright —
|
||||
/// a minor UI residue, not a money/wire-safety issue. See the register.
|
||||
/// <c>pwd._maxStackSize</c> (the item TYPE's stack ceiling) — now a
|
||||
/// byte-exact port (register AP-165 RETIRED 2026-08-08, R1 gate
|
||||
/// finding): <see cref="VendorShopItem.MaxStackSize"/> threads the wire
|
||||
/// field through directly, so this no longer needs the
|
||||
/// <see cref="VendorShopItem.DescStackSize"/> substitute the row
|
||||
/// originally filed.
|
||||
/// </summary>
|
||||
private static int BuyStagingRemovalAmount(VendorShopItem item) =>
|
||||
(item.DescStackSize ?? 1) > 1 ? -1 : 1;
|
||||
(item.MaxStackSize ?? 1) > 1 ? -1 : 1;
|
||||
|
||||
/// <summary>
|
||||
/// Slice 6b: "Buying" tab's "Buy Item" — retail case <c>0x100000c9</c>
|
||||
|
|
@ -1718,10 +1774,192 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag
|
|||
return total;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3: the Selling tab's total-proceeds counterpart to
|
||||
/// <see cref="ComputeBuyTransactionValue"/> — retail
|
||||
/// <c>VendorSellUI::UpdateTransactionValue</c> (<c>pc:202380-202468</c>),
|
||||
/// which prices each staged row via <c>VendorProfile::VendorBuyPrice</c>
|
||||
/// (the rate the VENDOR pays when IT buys FROM the player — see
|
||||
/// <see cref="VendorPricing"/>'s naming-inversion warning). Unlike the
|
||||
/// Buying side (whose priced item lives in <see cref="_vendor"/>'s shop
|
||||
/// list), a staged SELL entry's item is the PLAYER's OWN pack item —
|
||||
/// <see cref="_objects"/>, the same source <see cref="RebuildSellingList"/>
|
||||
/// already reads for icon/type data.
|
||||
/// </summary>
|
||||
private int ComputeSellTransactionValue()
|
||||
{
|
||||
VendorShopProfile profile = _vendor.Profile;
|
||||
int total = 0;
|
||||
foreach (VendorStagingEntry entry in _sellStaging.Entries)
|
||||
{
|
||||
if (_objects.Get(entry.ItemGuid) is not { } item)
|
||||
continue;
|
||||
int perUnit = VendorPricing.PerUnitValue(item.Value, item.StackSize);
|
||||
total += VendorPricing.BuyPrice(perUnit, (uint)item.Type, profile.BuyPrice, entry.Quantity);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3 (user-requested retail presentation, closing AP-166's text half —
|
||||
/// grand-gate live evidence: a selected vendor taper shows the Buying
|
||||
/// tab's right-side summary "Buying 2 items worth 422p" / "You have
|
||||
/// 23p"). Retail's exact literal, recovered BYTE-VERBATIM: the Sell side
|
||||
/// is directly legible in the decompiled body of
|
||||
/// <c>VendorSellUI::UpdateTransactionValue</c> (<c>pc:202458</c>,
|
||||
/// <c>u"Selling %d %s worth %hsp"</c>); the Buy side's IDENTICALLY-SHAPED
|
||||
/// literal is mis-attributed by the decompiler to a bogus vtable-slot
|
||||
/// symbol at its own call site (<c>VendorBuyUI::UpdateTransactionValue</c>,
|
||||
/// <c>pc:202290</c>) — recovered instead by reading the binary's own
|
||||
/// data segment directly (<c>C:\Users\erikn\Downloads\acclient.exe</c>,
|
||||
/// the Sept 2013 EoR build paired with <c>refs/acclient.pdb</c>), which
|
||||
/// carries the wide string <c>"Buying %d %s worth %hsp"</c> at VA
|
||||
/// <c>0x007b58bc</c> (Sell's own literal likewise resolves at
|
||||
/// <c>0x007b5930</c>, confirming both strings byte-for-byte). The "p"
|
||||
/// after <c>%hs</c> is a LITERAL pyreal-currency suffix character —
|
||||
/// same convention as <see cref="BuildCostText"/>'s own
|
||||
/// <c>"{0} {1}p (you have {2}p)"</c> — not part of the specifier.
|
||||
/// Singular/plural ("item"/"items") gates on the STAGED COUNT (the sum
|
||||
/// of every entry's quantity), matching retail's own per-row
|
||||
/// <c>_stackSize</c> accumulator feeding the same singular/plural test.
|
||||
/// <para>
|
||||
/// Alt-currency (a rare trade-note vendor): the Buy side's exact literal
|
||||
/// IS confirmed directly legible in the decompiled body
|
||||
/// (<c>VendorBuyUI::UpdateTotalValue</c>, <c>pc:202344</c>,
|
||||
/// <c>"You have %d %s."</c>) for the PURSE line; this method's alt-
|
||||
/// currency LIST-line construction is a faithful EXTRAPOLATION of the
|
||||
/// confirmed pyreal shape (dropping the "p" suffix, substituting the
|
||||
/// currency's plural name for the value) — the exact alt-currency LIST
|
||||
/// format string was not independently recovered byte-verbatim. See the
|
||||
/// register, AP-166, for this narrow residual.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private static string BuildTransactionListText(
|
||||
string verb, int count, int totalValue, VendorShopProfile profile)
|
||||
{
|
||||
string noun = count == 1 ? "item" : "items";
|
||||
if (profile.AlternateCurrencyWcid != 0u)
|
||||
{
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0} {1} {2} worth {3} {4}",
|
||||
verb,
|
||||
count,
|
||||
noun,
|
||||
totalValue,
|
||||
profile.AlternateCurrencyPluralName);
|
||||
}
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0} {1} {2} worth {3}p",
|
||||
verb,
|
||||
count,
|
||||
noun,
|
||||
totalValue.ToString("N0", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3: shared "You have %hsp" purse-text builder — retail
|
||||
/// <c>VendorBuyUI::UpdateTotalValue</c> (<c>pc:202366</c>,
|
||||
/// <c>u"You have %hsp"</c>) and <c>VendorSellUI::UpdateTotalValue</c>
|
||||
/// (<c>pc:202495</c>, the SAME literal <c>u"You have %hsp"</c>) — both
|
||||
/// directly legible in the decompiled body, byte-identical. Comma
|
||||
/// grouping matches retail's own <c>InsertCommas</c> call immediately
|
||||
/// before this format runs. The alt-currency branch's literal
|
||||
/// <c>"You have %d %s."</c> is directly legible at
|
||||
/// <c>VendorBuyUI::UpdateTotalValue</c> (<c>pc:202344</c>); the Sell
|
||||
/// side's alt-currency purse text was not independently traced but is
|
||||
/// presumed identical by symmetry — both read the SAME
|
||||
/// <c>shopVendorProfile->trade_num - m_last_sale</c> holding
|
||||
/// <see cref="VendorShopProfile.AlternateCurrencyAmount"/> already
|
||||
/// substitutes for elsewhere (see the register, AP-161's <c>m_last_sale</c>
|
||||
/// residual, and AP-166 for this untraced half).
|
||||
/// </summary>
|
||||
private string BuildPurseText(VendorShopProfile profile)
|
||||
{
|
||||
if (profile.AlternateCurrencyWcid != 0u)
|
||||
{
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"You have {0} {1}.",
|
||||
(int)profile.AlternateCurrencyAmount,
|
||||
profile.AlternateCurrencyPluralName);
|
||||
}
|
||||
int playerTotal = _objects.Get(_playerGuid())?.Properties.GetInt((uint)PropertyInt.CoinValue) ?? 0;
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"You have {0}p",
|
||||
playerTotal.ToString("N0", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3: repaints the Buying tab's own summary text — retail
|
||||
/// <c>VendorBuyUI::Update</c> (<c>pc:202996-203005</c>) calls
|
||||
/// <c>UpdateTransactionValue</c> then <c>UpdateTotalValue</c>
|
||||
/// unconditionally on every staging mutation.
|
||||
/// </summary>
|
||||
private void UpdateBuyTransactionText()
|
||||
{
|
||||
if (_buyListText is null && _buyPurseText is null)
|
||||
return;
|
||||
|
||||
VendorShopProfile profile = _vendor.Profile;
|
||||
int count = _buyStaging.Entries.Sum(e => e.Quantity);
|
||||
int totalValue = ComputeBuyTransactionValue();
|
||||
if (_buyListText is not null)
|
||||
SetPlainText(_buyListText, BuildTransactionListText("Buying", count, totalValue, profile));
|
||||
if (_buyPurseText is not null)
|
||||
SetPlainText(_buyPurseText, BuildPurseText(profile));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3: the Selling tab's counterpart to <see cref="UpdateBuyTransactionText"/> —
|
||||
/// retail <c>VendorSellUI::Update</c> (<c>pc:203009-203018</c>), same shape.
|
||||
/// </summary>
|
||||
private void UpdateSellTransactionText()
|
||||
{
|
||||
if (_sellListText is null && _sellPurseText is null)
|
||||
return;
|
||||
|
||||
VendorShopProfile profile = _vendor.Profile;
|
||||
int count = _sellStaging.Entries.Sum(e => e.Quantity);
|
||||
int totalValue = ComputeSellTransactionValue();
|
||||
if (_sellListText is not null)
|
||||
SetPlainText(_sellListText, BuildTransactionListText("Selling", count, totalValue, profile));
|
||||
if (_sellPurseText is not null)
|
||||
SetPlainText(_sellPurseText, BuildPurseText(profile));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3: a player money change (purchase/sale/pickup/drop elsewhere) must
|
||||
/// repaint BOTH tabs' purse line even when staging itself is unchanged —
|
||||
/// retail's own purse text always reads the LIVE holding fresh (same as
|
||||
/// <see cref="BuildCostText"/>'s own <c>PropertyInt.CoinValue</c> read),
|
||||
/// with no separate "did staging change" gate.
|
||||
/// </summary>
|
||||
private void OnObjectMoneyChanged(ClientObject updated)
|
||||
{
|
||||
if (updated.ObjectId != _playerGuid())
|
||||
return;
|
||||
UpdateBuyTransactionText();
|
||||
UpdateSellTransactionText();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F1: port of <c>gmVendorUI::InqListSlotCount</c> (<c>pc:200038-200065</c>,
|
||||
/// <c>0x004c0c10</c>) — see <see cref="BuyAllButtonPressed"/>'s own doc
|
||||
/// comment for the container-classification approximation.
|
||||
/// <para>
|
||||
/// R1 gate-finding fix (2026-08-08): retail's stackable test at this
|
||||
/// exact call site is <c>eax->pwd._maxStackSize <= 1</c>
|
||||
/// (<c>pc:200052</c>) — literally <c>MaxStackSize</c>, never
|
||||
/// <c>_stackSize</c>. This now reads <see cref="VendorShopItem.MaxStackSize"/>
|
||||
/// directly, a byte-exact port now that the wire field is threaded
|
||||
/// through (previously approximated with <c>DescStackSize</c>, which
|
||||
/// ACE never populates for a browse-list row, so a stackable item was
|
||||
/// always misclassified as non-stackable — see AP-169's sibling
|
||||
/// finding).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private (int ItemSlots, int ContainerSlots) ComputeBuySlotsNeeded(
|
||||
IReadOnlyList<(int Amount, uint ItemGuid)> items)
|
||||
|
|
@ -1732,7 +1970,7 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag
|
|||
if (!TryFindShopItem(guid, out VendorShopItem item))
|
||||
continue;
|
||||
bool isContainer = ((item.ItemType ?? 0u) & (uint)ItemType.Container) != 0u;
|
||||
bool stackable = (item.DescStackSize ?? 1) > 1;
|
||||
bool stackable = (item.MaxStackSize ?? 1) > 1;
|
||||
if (stackable)
|
||||
{
|
||||
if (isContainer) containerSlots += 1; else itemSlots += 1;
|
||||
|
|
@ -2242,6 +2480,12 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag
|
|||
// close clears a vendor-owned selection" behavior is OnObjectRemoved
|
||||
// reacting to VendorShopItemMaterializer's removal, not this method.
|
||||
ClearSelectionDisplay();
|
||||
// R3: reset the Buying/Selling summary text to its empty-staging
|
||||
// shape (retail's own text is never simply blanked — UpdateTotalValue
|
||||
// still renders "You have Np" etc. with a zero transaction) on both
|
||||
// constructor-time setup and session close/reset.
|
||||
UpdateBuyTransactionText();
|
||||
UpdateSellTransactionText();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -2292,11 +2536,14 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag
|
|||
_vendor.Changed -= OnVendorChanged;
|
||||
_selection.Changed -= OnSelectionTransition;
|
||||
_objects.ObjectRemoved -= OnObjectRemoved;
|
||||
_objects.ObjectUpdated -= OnObjectMoneyChanged;
|
||||
_itemInteraction.StateChanged -= OnInteractionStateChanged;
|
||||
_splitQuantity.Changed -= OnSplitQuantityChanged;
|
||||
_buyStaging.Changed -= RebuildBuyingList;
|
||||
_buyStaging.Changed -= RefreshItemsTabAvailability;
|
||||
_buyStaging.Changed -= UpdateBuyTransactionText;
|
||||
_sellStaging.Changed -= RebuildSellingList;
|
||||
_sellStaging.Changed -= UpdateSellTransactionText;
|
||||
DismissCloseConfirmationIfOpen();
|
||||
_dragOverSink.Parent?.RemoveChild(_dragOverSink);
|
||||
RetailTabBinding.SetClick(_itemsTab, null);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue