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 <noreply@anthropic.com>
This commit is contained in:
Kamron Batman 2026-08-22 12:52:07 -07:00
parent 8067a7d0aa
commit 046452d7c7
No known key found for this signature in database
GPG key ID: 7D81DF26D9A5D94A
2 changed files with 69 additions and 0 deletions

View file

@ -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]

View file

@ -2429,6 +2429,12 @@ public partial class Item : IHued, IComparable<Item>, 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()