fix: drop a superseded DecayResetTime so CompactInfo can collapse
The reset stamp is only meaningful while it is ahead of LastMoved. Clearing it from the LastMoved setter covers every path that records a real move (SetLastMoved, MoveToWorld, direct writes) and lets VerifyCompactInfo free the CompactInfo instead of holding it forever for any item that was ever unfrozen. Deserialization applies the same guard, since minute rounding can land the stamp on LastMoved. The setter deliberately does not touch scheduler registration: MoveToWorld assigns LastMoved while parent/map are mid-transition and defers registration until its state is final. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
107ca3c1c0
commit
382956a941
2 changed files with 62 additions and 2 deletions
|
|
@ -340,6 +340,40 @@ public class DecayRegistrationTests
|
|||
}
|
||||
}
|
||||
|
||||
// Once a real move supersedes the reset stamp, the stamp must be dropped so the item's
|
||||
// CompactInfo can collapse instead of holding ~40 bytes forever.
|
||||
[Fact]
|
||||
public void MovingAnItem_ClearsASupersededDecayResetStamp()
|
||||
{
|
||||
var start = Core._now;
|
||||
|
||||
try
|
||||
{
|
||||
var item = new Item(0x1234);
|
||||
item.MoveToWorld(new Point3D(112, 100, 0), Map.Felucca);
|
||||
|
||||
item.Movable = false;
|
||||
Core._now = start + TimeSpan.FromDays(30);
|
||||
item.Movable = true;
|
||||
|
||||
Assert.NotEqual(default, item.DecayResetTime);
|
||||
|
||||
// A real move supersedes the stamp.
|
||||
Core._now += TimeSpan.FromMinutes(1);
|
||||
item.MoveToWorld(new Point3D(113, 100, 0), Map.Felucca);
|
||||
|
||||
Assert.Equal(default, item.DecayResetTime);
|
||||
Assert.Equal(item.LastMoved + item.DecayTime, item.ScheduledDecayTime);
|
||||
Assert.True(DecayScheduler.IsRegistered(item));
|
||||
|
||||
item.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]
|
||||
|
|
|
|||
|
|
@ -335,7 +335,26 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public virtual bool Decays => Movable && Visible && Spawner == null;
|
||||
|
||||
public DateTime LastMoved { get; set; }
|
||||
private DateTime _lastMoved;
|
||||
|
||||
public DateTime LastMoved
|
||||
{
|
||||
get => _lastMoved;
|
||||
set
|
||||
{
|
||||
_lastMoved = value;
|
||||
|
||||
// A move at or past the reset stamp supersedes it; drop the stamp so the
|
||||
// CompactInfo it lives in can collapse instead of being held forever.
|
||||
var info = LookupCompactInfo();
|
||||
|
||||
if (info != null && info.m_DecayReset != default && info.m_DecayReset <= value)
|
||||
{
|
||||
info.m_DecayReset = default;
|
||||
VerifyCompactInfo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public bool Stackable
|
||||
|
|
@ -2761,7 +2780,14 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
|
||||
if (version >= 10 && GetSaveFlag(flags, SaveFlag.DecayReset))
|
||||
{
|
||||
DecayResetTime = Core.Now - TimeSpan.FromMinutes(reader.ReadEncodedInt());
|
||||
var reset = Core.Now - TimeSpan.FromMinutes(reader.ReadEncodedInt());
|
||||
|
||||
// Minute rounding can land the stamp on LastMoved; only keep it while
|
||||
// it still extends the deadline, so CompactInfo is not held for nothing.
|
||||
if (reset > LastMoved)
|
||||
{
|
||||
DecayResetTime = reset;
|
||||
}
|
||||
}
|
||||
|
||||
if (GetSaveFlag(flags, SaveFlag.Direction))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue