fix: stop stack merges and splits from laundering PlayerConstructed
PlayerConstructed is per-instance provenance, and #2574 put it on every crafted item -- including potions, arrows and other stackables. Stack operations were written when no item carried provenance of any kind, so they treated two piles of the same graphic as interchangeable. Merging keeps the receiving stack's value. Dropping bought potions onto a crafted stack made the whole pile count as crafted; the reverse order erased it. Which happened was decided by drag direction alone. CanStackWith now compares PlayerConstructed, so the two never merge into one indistinguishable pile. Splitting rebuilds one half in Mobile.LiftItemDupe, which copies a fixed list of fields rather than going through Dupe/CopyProperties -- PlayerConstructed was not on that list, so dragging part of a pile off stripped the new half. Note that [IgnoreDupe] does not govern this path; it only applies to Dupe(). LiftItemDupe now copies the flag, so a split cannot produce halves that disagree about what they are. Refusing to merge is the whole fix: a stack has nowhere to record provenance, so the only coherent behaviour is to keep the two piles apart. Paths that genuinely virtualize an item -- pouring a potion keg, for one -- rebuild it without the flag, and the result is simply treated as not crafted. Adds 7 tests: both merge directions, the matching case, split copying, and the split/re-merge round trip.
This commit is contained in:
parent
bd79cb7759
commit
2b845b0d8d
3 changed files with 146 additions and 0 deletions
|
|
@ -2344,12 +2344,16 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
UpdateDecayRegistration();
|
||||
}
|
||||
|
||||
// PlayerConstructed is part of stack identity. A merge keeps the receiving stack's value, so
|
||||
// without this term the outcome depends on which side was dropped onto which: one order
|
||||
// launders the flag onto items that never earned it, the other erases it from items that did.
|
||||
public virtual bool CanStackWith(Item dropped) =>
|
||||
dropped.Stackable && Stackable &&
|
||||
dropped.GetType() == GetType() &&
|
||||
dropped.ItemID == ItemID &&
|
||||
dropped.Hue == Hue &&
|
||||
dropped.Name == Name &&
|
||||
dropped.PlayerConstructed == PlayerConstructed &&
|
||||
dropped.Amount + Amount <= 60000 &&
|
||||
dropped != this;
|
||||
|
||||
|
|
|
|||
|
|
@ -5248,6 +5248,12 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
item.Name = oldItem.Name;
|
||||
item.Weight = oldItem.Weight;
|
||||
|
||||
// Splitting a stack must not change what either half is. This copies a fixed list of
|
||||
// fields rather than going through Dupe/CopyProperties, so anything omitted here is
|
||||
// silently dropped on the new half -- for PlayerConstructed that would mean dragging
|
||||
// part of a pile off is enough to strip its provenance.
|
||||
item.PlayerConstructed = oldItem.PlayerConstructed;
|
||||
|
||||
item.Amount = oldAmount - amount;
|
||||
item.Map = oldItem.Map;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue