From 046452d7c7234fdfa679f6534acffa8df831d370 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 22 Aug 2026 12:52:07 -0700 Subject: [PATCH] fix: drop the decay reset stamp when an item loses eligibility A stamped item that was re-frozen, re-hidden, spawner-linked, or moved into a container programmatically kept its stamp - and the CompactInfo holding it - for as long as it stayed ineligible. UpdateDecayRegistration's ineligible branch now clears the stamp; every re-eligibility path re-anchors, so only the allocation was at stake. The guarded setter makes the hot-path cost a field read and two compares, with no allocation. Co-Authored-By: Claude Fable 5 --- .../Tests/Items/DecayRegistrationTests.cs | 63 +++++++++++++++++++ Projects/Server/Items/Item.cs | 6 ++ 2 files changed, 69 insertions(+) diff --git a/Projects/Server.Tests/Tests/Items/DecayRegistrationTests.cs b/Projects/Server.Tests/Tests/Items/DecayRegistrationTests.cs index 4159780d7..6ffb9a7a0 100644 --- a/Projects/Server.Tests/Tests/Items/DecayRegistrationTests.cs +++ b/Projects/Server.Tests/Tests/Items/DecayRegistrationTests.cs @@ -374,6 +374,69 @@ public class DecayRegistrationTests } } + // Losing decay eligibility makes the stamp meaningless; it must be dropped so the + // CompactInfo is not held for as long as the item stays ineligible. + [Fact] + public void ItemBecomingIneligible_DropsTheDecayResetStamp() + { + var start = Core._now; + + try + { + var item = new Item(0x1234); + item.MoveToWorld(new Point3D(115, 100, 0), Map.Felucca); + + item.Movable = false; + Core._now = start + TimeSpan.FromDays(30); + item.Movable = true; + + Assert.NotEqual(default, item.DecayResetTime); + + item.Movable = false; + + Assert.Equal(default, item.DecayResetTime); + + item.Delete(); + } + finally + { + Core._now = start; + } + } + + // Moving a stamped item into a container programmatically (no drop, no SetLastMoved) + // must also drop the stamp. + [Fact] + public void StampedItemAddedToContainer_DropsTheDecayResetStamp() + { + var start = Core._now; + + try + { + var pack = new Container(0xE75); + pack.MoveToWorld(new Point3D(116, 100, 0), Map.Felucca); + + var item = new Item(0x1234); + item.MoveToWorld(new Point3D(117, 100, 0), Map.Felucca); + + item.Movable = false; + Core._now = start + TimeSpan.FromDays(30); + item.Movable = true; + + Assert.NotEqual(default, item.DecayResetTime); + + pack.AddItem(item); + + Assert.Equal(default, item.DecayResetTime); + + pack.Delete(); + } + finally + { + Core._now = start; + } + } + // A raw Map assignment (e.g. a GM changing Map through props) is a move: it must enroll // an untracked item for decay instead of leaving it to linger forever. [Fact] diff --git a/Projects/Server/Items/Item.cs b/Projects/Server/Items/Item.cs index 113cbf913..6767f38aa 100644 --- a/Projects/Server/Items/Item.cs +++ b/Projects/Server/Items/Item.cs @@ -2429,6 +2429,12 @@ public partial class Item : IHued, IComparable, ISpawnable, IObjectPropert { DecayScheduler.Register(this); } + else + { + // No countdown to anchor while ineligible; drop the stamp so the CompactInfo + // can collapse. Re-eligibility always re-anchors. + DecayResetTime = default; + } } public void SetLastMoved()