## Summary
Phase 3 of the anchored-time work: **every actively-written delta-time value in the engine now stores an anchored timestamp** — absolute on the wire, shifted forward by the downtime at load. Remaining time survives restarts (as delta did), and unlike delta, the bytes do not change on every save, so an idle world serializes identically save after save.
The answer to "is it possible everywhere": **yes** — including the one case that looked impossible.
## The GenericPersistence problem, solved
`GenericPersistence` bins (`Virtues.bin`, `StealableArtifacts.bin`, …) are raw payloads with no idx header, so they have no anchor of their own — anchored reads there would silently apply zero shift. But the anchor is a property of the **save**, not the file: every file in one save shares one `World.SaveStartTime`, and `Persistence.Load` reads **all** entity indexes (phase 1) before **any** persistence payload (phase 2). So the idx v5 header stamps a save-wide `World.LoadTimeShift`, and generic persistence readers inherit it. No file-format change, no per-bin header, old bins unaffected.
## Converted
- **Item v10 → v11**: `LastMoved` — previously whole-minute delta, rewritten every save for every item, the single largest source of idle-save churn — and `DecayResetTime` (retiring the TODO from #2583). **Mobile v37 → v38**: the three stat-gain stamps. **BaseCreature v20 → v21**: `SummonEnd`.
- **17 code-generated classes** (`[DeltaDateTime]` → `[AnchoredDateTime]`, version bump + `MigrateFrom` each): the five field spells, TransientItem, VirtueContext (×7 fields), PuzzleChestSolutionAndTime, BaseCamp, BaseBoat, RentedVendor, PlayerVendor, Ethics Player, Sheep, StarRoomGate, ChampionSpawn (×3), Corpse (`TimeOfDeath`, v19). The `MigrateFrom` bodies were generated from each class's current migration schema and are compiler-verified; VirtueContext's save-flagged nullables fall back to the same defaults the old deserialize left in place. Corpse's six migrations moved to a new `Corpse.Migrations.cs`.
- **Hand-written sites**: StealableArtifacts (v2), VendorInventory (v1), ML quest objectives (persistence v3) — each gated on its own version.
**Not converted, deliberately**: the ~25 read-only `ReadDeltaTime` sites in legacy version fallbacks and migration replays — they decode existing old bytes and must never change. `[DeltaDateTime]`/`WriteDeltaTime` remain available for them.
## Verification
- Build 0 errors / 0 warnings; **837 + 708 tests green**.
- Schema regeneration produced exactly the 17 expected new `vN.json` files (all `AnchoredTime` rule args), nothing else touched.
- **New acceptance tests** pin the point of the whole effort: serializing the same item at two save times **5 hours apart produces byte-identical output**, and `LastMoved`/`DecayResetTime` round-trip **exactly** at sub-minute precision (the old minutes encoding destroyed both properties).
## Notes for review
- `LastMoved` grows from a 1–3 byte encoded minutes value to 8-byte ticks per item — the price of byte-stability; it repays itself in incremental-save behavior since unchanged items now produce unchanged bytes.
- BaseEscortable-style semantics are unchanged: anchored shift preserves *remaining* time exactly, the same contract delta provided, so no gameplay-visible behavior changes — deadlines simply stop being consumed by downtime that delta already protected against, now with stable bytes.
## Enforcement
`WriteDeltaTime` is now `[Obsolete]` (interface + implementation). With the repo's warnings-as-errors, any new delta-time write — hand-written or emitted by a still-unconverted `[DeltaDateTime]` field — fails the build, with the migration instructions in the message. That the full solution still builds with **zero warnings** is itself the proof no active delta writer survived the conversion. `ReadDeltaTime` deliberately stays un-attributed: its remaining callers decode existing old bytes and are correct forever; its XML docs now state the legacy-decode-only contract.
## Summary
Folds **113** hand-written `[SerializableProperty]` members into plain `[SerializableField]` declarations using the v4 setter hooks — value coercion/vetoes via `allowFieldChange`, post-change side effects via `fieldChanged` (whose `oldValue` parameter covers the old-house/old-sender unsubscribe patterns). Net **-450 lines** of setter boilerplate.
```cs
// before
[SerializableProperty(1)]
[CommandProperty(AccessLevel.GameMaster)]
public int Charges
{
get => _charges;
set
{
_charges = Math.Clamp(value, 0, MaxCharges);
InvalidateProperties();
this.MarkDirty();
}
}
// after
[SerializableField(1, allowFieldChange: nameof(AllowChargesChange))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
[InvalidateProperties]
private int _charges;
private bool AllowChargesChange(ref int value)
{
value = Math.Clamp(value, 0, MaxCharges);
return true;
}
```
## How sites were selected
A classifier parsed all 204 `[SerializableProperty]` sites and converted only those matching strict shapes: getter is exactly `get => _field;`, the assignment comes first (after at most an equality guard), and relocated side effects contain no `return`, no `value` mutation, and no field re-assignment. Everything else was left alone deliberately:
- **~34 custom getters** (fallback defaults like `_x == -1 ? Default : _x`, self-healing refs) — no setter hook can express these.
- **~35 pre-assignment logic** (durability Unscale/Scale sandwiches, old-state captures like PotionKeg's pile weight).
- **virtual/override members, name-mismatched backing fields (`m_`), exotic semantics** (guards' `Focus` does work on *equal* assignment; `ChampionSpawn.Active` never assigns its field).
Five sites the classifier refused were converted by hand where the hooks fit cleanly: `ReceiverCrystal.Sender`, `PlayerVendor.House`, `PlayerBarkeeper.House` (old-value unsubscribe via `oldValue`), `BaseSuit.AccessLevel` (its existing virtual `OnAccessLevelChanged` already had the exact callback shape), and `DyeTub.DyedHue` (a true veto: `AllowDyedHueChange(ref int value) => _redyable`).
## Verification
- Build: **0 errors, 0 warnings**.
- **Schema regeneration produces zero Migrations changes** — the conversion is wire- and schema-neutral by construction (same orders, types, and property names), and CI's schema diff check enforces it.
- **835 + 708 tests green.**
## Behavioral notes (all strict improvements, called out for review)
- Generated setters skip everything when the incoming value equals the current one; a few converted setters previously re-ran side effects on equal assignment (redundant `Update()`-style refreshes).
- Generated setters always `MarkDirty()` on change; several converted setters never did (e.g. `DyeTub.DyedHue`, `MorphItem` ranges) — their changes only persisted if something else dirtied the entity. Those latent persistence bugs are fixed by construction.
## Summary
Adopts ModernUO.Serialization 4.0.0 across the engine. Three commits, reviewable independently:
1. **Package + tool bump to 4.0.0** (`Server.csproj`, `UOContent.csproj`, `dotnet-tools.json`).
2. **Timers → `[DeserializeTimer]`** — the 8 drifting timers (BaseLight, TreasureMapChest, MarkContainer, FillableContainer, DeathRobe, DecayedCorpse, Corpse, BaseEscortable) now store their next tick as **anchored time**: server downtime no longer consumes the remaining delay, and idle-world saves are byte-stable. This changes their wire format, so each class bumps its serialization version with a `MigrateFrom` that replays the old delta-time read through the migration schema (the new `vN.json` files carry `@AnchoredTimer`; the old ones keep `@TimerDrift`, which the generator reads forever). The 2 wall-clock timers (Aquarium, FountainOfLife) keep their exact format via `wallClock: true` — no bump. Restart methods drop their `TimeSpan.MinValue` sentinel checks: v4 invokes them **only when a timer was actually running at save**.
3. **Linkage → field-side declarations** — 175 conversions across 25 files: `[SerializableFieldSaveFlag(order)]`/`[SerializableFieldDefault(order)]` become `[SaveFlag(nameof(...), nameof(...))]` on the field, and `[SerializableFieldChanged(order)]` becomes the `fieldChanged:` argument of `[SerializableField]`. **Wire-neutral: zero migration schemas changed.**
## Verification
- Solution builds with **0 errors, 0 warnings**; all three 4.0.0 packages verified indexed on nuget.org (no local feed needed).
- **835 + 708 tests green.**
- Generated output inspected: old-version content structs replay `ReadDeltaTime` (e.g. `V3Content.DecayTimerNext = reader.ReadDeltaTime()`), current versions write/read anchored time with the gated restart, and the wall-clock classes emit byte-identical `Write`/`ReadDateTime` framing.
- Schema tool run is committed (CI's `git diff --exit-code` schema check passes): exactly the 8 expected new `vN.json` files, nothing else touched.
- The conversion was scripted with a class-scoped resolver (order → same-class `[SerializableField(order)]`/`[SerializableProperty(order)]`); it planned 175/175 with zero ambiguities before applying.
## Notes
- New `MigrateFrom`s use the content structs' provided `XxxDelay` property, matching the pre-existing idiom in Corpse's and TreasureMapChest's older migrations.
- Follow-up candidate (separate PR, wire-neutral, any time): fold the ~150 eligible hand-written `[SerializableProperty]` setters (clamps, post-change side effects) down to `[SerializableField]` with `allowFieldChange`/`fieldChanged` hooks.
## Summary
- Removes allocation of a `List<ContextMenuEntry>` every time a context menu is created.
- Moves packet/context menu creation logic out of the core
- Fixes tame entry
## BREAKING CHANGE
> [!Important]
> **Developer Note**
> ```cs
> public virtual void GetContextMenuEntries(Mobile from, List<ContextMenuEntry> list)
> ```
> and similar functions changed to
> ```cs
> public virtual void GetContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list)
> ```
### Summary
* Makes mount stamina significantly faster.
* Reduces absolute reset from 24 hours to 4 hours.
* Adds Pub 46+ Ethereal stamina where the stamina is global to all mounts for the player
* Removes serialization of stamina entirely because it's ok if it gets reset during shard restarts.
### Summary
- Fixes the name `Vegies` to `Veggies`
- Adds feeding leather to goats
- Adds metal for Lava Lizards
- Fixes Bone Helm being classified as plate instead of bone.
- Fixes wooden shields not classified as wood.
### Note
On OSI, the preferred foods on the animal lore gump doesn't contain all possible favorite food categories. Furthermore, the one listed is not necessarily always the "first" in the list of possible categories. We aren't going to try and fix that since it isn't important.
## Overhaul to Stamina (Overweight) System
### Added configurations
```json
{
"settings": {
"stamina.enableMountStamina": "True",
"stamina.cannotMoveWhenFatigued": "True",
"stamina.stonesPerOverweightLoss": "25",
"stamina.stonesOverweightAllowance": "4",
"stamina.baseOverweightLoss": "5",
"stamina.additionalLossWhenBelow": "0.1",
"stamina.mountLastMoveStepsReset": "01:00:00:00",
"stamina.enableMountStamina": "True",
"stamina.useMountStaminaOnlyWhenOverloaded": "False",
},
}
```
- `stamina.cannotMoveWhenFatigued` - By default, Pre-AOS expansions will outright block a player if they are fatigued. A player is fatigued when they run out of stamina by any mechanism.
- `stamina.stonesPerOverweightLoss` - The amount of stamina lost for every X stones above overweight. Example, if a person is overweight 28 stones overweight, then there there is 1 additional stamina. 28 / 25 = 1 (no decimals, no rounding)
- `stamina.stonesOverweightAllowance` - The number of stones allowed before overweight penalties take affect. This _does not_ substract from the overweight stones calculation.
- `stamina.baseOverweightLoss` - The base amount of stamina loss for being overweight. While running, the final amount is multiplied by 2. If mount stamina is turned off, then the final amount is divided by 3 while mounted.
### Mount stamina
Mounts have a new property `StepsMax` to determine the maximum steps they can take before being fatigued. To regain steps, the player _must stay on the mount and not move_ (per OSI). The gain rates are configurable. If a mount is dismounted, or the player logs off, the mount is considered inactive and mounts regain all of their steps after _24 hours_.
### Changes to player stamina
Players now have a proper inactivity time reset for their steps. If a player is idle for 16 seconds (including logging off, or the server being offline), then the steps counter is rest.
### Developer Notes
- `StepsTaken` - This field has been removed. It was not serialized and could not be used reliably. If a developer was using it, then the recommendation is to build a mechanism to track total steps another way.
- `IHasStamina` - This new interface was added and currently `IMount` and `PlayerMobile` are valid types.
Important: Entities that are sent to the StaminaSystem for tracking (mounts, players, or something else), must be an `ISerializable` to be serialized properly. If they are not, then the serialization system will record a null, and nothing will be deserialized upon world load. No errors will be given.
* Movie ignore mobiles to Mobile class
* Allow necromancer familiars to ignore mobiles
* ChampionSpawn should not quietly fail when creating new spawn
* Fix client party crash bug: 2 people partied, the leader logs out, other client is hung
* Fix spellbooks creating with magery/meditation only and creating magery multiple times
* Fix BaseRunicTool.GetRandomSlayer() creating more undead slayers than intended
* Do not allow players to dismount each other whilst mounted
* Fix AOS onwards damage increase tooltip and wrong formula in AOS
* Fix monsters killing other monster revenants + animate dead should not attack player pets + other animates
* Fix fire steed having loot pack added twice
* Fix oaks spawn not able to create Unicorns + Kirins via Activator.CreateInstace() due to constructor having name as parametr
* Fix ignoreMobiles flag not propagated to client for non-players.
* Fix harrower tents not leeching from players
* Fix boats speedhacking around the map
* Fix bless, agility etc. not renewing duration
* Fix reveal always worked
* Fix empty constructor for steeds so they don't throw now.
- [X] Fixes NPE from account tags.
- [X] Fixes bad skill check due to missing cast to double.
- [X] Fixes water elemental duration.
- [X] Standardizes spell summon duration by expansion.
## Breaking Changes (New API)
ObjectPropertyList supports the following API:
```cs
list.Add(500000);
list.Add(500001, stringArgument);
list.Add("Some text");
list.Add($"Some text with {argument}");
list.Add(500002, $"{arg1}\t{arg2}");
```
## Notes
1. All API uses that require a formatter like this:
```cs
list.Add(500002, "{0}\t{1}", arg1, arg2);
```
Should be changed to use string interpolation, for example:
```cs
list.Add(500002, $"{arg1}\t{arg2}");
```
2. The following paradigm should no longer be used:
```cs
list.Add(1061170, prop.ToString()); // strength requirement ~1_val~
```
The new string interpolation API will avoid having to convert the argument to a string before writing it to the packet. Instead use the following:
```cs
list.Add(1061170, $"{prop}"); // strength requirement ~1_val~
```
### Benchmarks
```cs
| Method | Mean | Error | StdDev | Gen 0 | Allocated |
|------------------------------- |---------:|--------:|--------:|-------:|----------:|
| BenchmarkOldOPL | 241.0 ns | 0.56 ns | 0.47 ns | 0.0105 | 88 B |
| BenchmarkStringInterpolatedOPL | 199.9 ns | 2.44 ns | 2.39 ns | - | - |
```
### Changes
- [X] Removes crash in STArray.Return when array is null.
- [X] Fixes NPE in OPL when entity is null. Serial in packet will be 0 when entity is null.
- [X] Fixes NPE in AosAttributes when Parent is null.
- [X] Changes OPL to use string interpolation.
- [X] Introduces `IPropertyList` to allow extending PropertyList for other uses.
* Removes scaled speed by dex for monsters
* Changes scaled speed by dex for pets (HS+ expansion) to be 400ms -> 100ms between 50 -> 200 dex
* Removes speed changes that were broken for various mobs
* Combines "Legacy" speed and renames the class to `NPCSpeeds`
* Creates speed "classes" (slow, medium, fast, very fast)
* Introduces a new overridable property `SpeedClass`. Register a new speed class using `NPCSpeeds.Register()` and then set the speed class to bulk/mass apply speeds.
Order of speed determination:
1. SpeedMod
2. SpeedClass property (not null)
3. SpeedClass entry in Data\npc-speeds.json for that type
4. "Fast" (200ms Active, 400ms Passive)
* Adds the following configurations:
* `movement.delay.npcMinDelay` - 0.1 - Pets or Non-monster NPCs
* `movement.delay.npcMaxDelay` - 0.4 - Pets or Non-monster NPCs
* `movement.delay.monsterMinDelay` - 0.4 - Non-pet Monsters or NPC vs Player Combat
* `movement.delay.monsterMaxDelay` - 0.8 - Non-pet Monsters or NPC vs Player Combat
* `movement.delay.monsterMinDex` - 150 - Dex maximum for delay by dex
* `movement.delay.MinDex` - 190 - Dex maximum for delay by dex
* Removes BodyValue
* Removes BodyPicker gump
* Fixes setting Body and BodyMod in props gump
* Props will now look for an `implicit cast` in order to find a way to allow you to modify values with the props gump.
* Fixed `[self set body 0x190` so it works too.
### Changes/Fixes:
* Adds timer pooling.
* Allows pool to be configurable in ModernUO.json
* Pool replenishes itself asynchronously if depleted.
* Fixes an issue with barkeeps and town criers
* Fixes an issue with incognito buff icons not being removed
* Fixes an issue with polymorph name mod not being removed
* Fixes several places where timers go on forever even after an object is deleted, keeping a reference (memory leak)
* Eliminates the timer for MiningCart altogether.
* Deletes `AcidSlime` since it is a duplicate of `PoolOfAcid`
* Fixes HonorableExecution and standardizes the code for other Bushido moves.
## Changes to the Timer API:
```cs
public class Timer
{
// Creates a timer that will be returned to the pool once execution stops.
public static void StartTimer(Action callback);
public static void StartTimer(TimeSpan delay, Action callback);
public static void StartTimer(TimeSpan delay, TimeSpan interval, Action callback);
public static void StartTimer(TimeSpan interval, int count, Action callback);
public static void StartTimer(TimeSpan delay, TimeSpan interval, int count, Action callback);
// Creates a timer and returns a token for more control. Requires manual cancellation in order for the timer to be returned to the pool.
// If the token is dereferenced, the timer will be dereferenced too. While not returning a timer to the pool is not considered hazardous, it does defeat the purpose of pooled timers.
public static void StartTimer(Action callback, out TimerExecutionToken token);
public static void StartTimer(TimeSpan delay, Action callback, out TimerExecutionToken token);
public static void StartTimer(TimeSpan delay, TimeSpan interval, Action callback, out TimerExecutionToken token);
public static void StartTimer(TimeSpan interval, int count, Action callback, out TimerExecutionToken token);
public static void StartTimer(TimeSpan delay, TimeSpan interval, int count, Action callback, out TimerExecutionToken token);
// If you aren't sure how to use the API above, or you don't care about performance, then you can use the old RunUO Timer.DelayCall
public static DelayCallTimer DelayCall(Action callback);
public static DelayCallTimer DelayCall(TimeSpan delay, Action callback);
public static DelayCallTimer DelayCall(TimeSpan delay, TimeSpan interval, Action callback);
public static DelayCallTimer DelayCall(TimeSpan interval, int count, Action callback);
public static DelayCallTimer DelayCall(TimeSpan delay, TimeSpan interval, int count, Action callback);
}
public struct TimerExecutionToken
{
public bool Running { get; }
public int Index { get; }
public int RemainingCount { get; }
public DateTime Next { get; }
}
```
## When to use `TimerExecutionToken`?
Use tokens when you want to gain the performance benefit of using a pooled timer, but you need one of the following:
* Access to the next time the timer will tick:`token.Next`
* Access to which interval, how many intervals there are, or how many are remaining: `token.Index`, `token.Count`, and `token.RemainingCount`
* Stop a timer manually.
* Determine if the timer is running: `timer.Running`
* See notes below about requirements for using tokens!
## Notes about using the TimerExecutionToken:
When you opt-in to receive a token, you must call `Cancel()` to return the timer. This can be done inside of the callback, or outside of the callback at any time.
If this is not called and your timer is an infinite interval, then you will create a potential memory leak, or null pointer exception in your callback.
If the timer ends and is stopped, but cancel is not called, then the timer will never return to the pool and stay referenced until the token is deleted or cancel is called. (Memory leak)
## Is this thread safe?
No. The ModernUO timer system is not thread safe at all. If you require a thread safe timer system, contact me and I'll help adapt this system. Keep in mind that there is a massive performance hit to make this thread safe when there are literally no use cases for it.
If you need to synchronize execution, meaning you want to execute code from another thread on the core thread. Let's say you have a discord bot that is pushing commands to the game server. Then use `EventLoopContext.Post(SendOrPostCallback callback, object state);`.
- [X] Caches DateTime.NowUtc on the game loop (not other threads)
- [X] Replaces all locations where it makes sense
- [X] Adds Min/Max for `IComparable` (TimeSpan, DateTimes, etc)
Closes#261
- [X] Fixes an issue where a buffer smaller than 8 bytes would not double with enough space in some cases.
- [X] Fixes an issue with dupe copying the savebuffer reference (ugh).
- [X] Streamlines the IGenericWriter API to use better generics.
- [X] Streamlines the IGenericReader API to use better generics.
- [X] Forces `tidying` of a List/HashSet to be done externally since Writers/Readers should not have side effects.
- [X] Fixes an issue where Tidying a list didn't TrimExcess, causing memory leaks.
- [X] Reverted the meaning of `World.Running` to specifically refer to any world state post world loading.
- NOTE: Do not use this if you want to block on world saves. Instead use checks against `WorldState.Saving` states.
- [X] Fixes an issue with serializing negative DateTime deltas.
- [X] Fixes a potential issue with serializing non-UTC DateTime.
Bumps release version
- [X] Fixes an edge case where the cycle index goes negative and crashes
- [X] Fixes average CPS calculation
- [X] Fixes issue with logout delay
- [X] Bumps release version
- [X] Fixes several bugs
- [X] Updates more ordinal issues
- [X] Cleans up the code a bit
- [X] Turns classes static that should have been
- [X] Changes TcpServer.Instances to a HashSet
Bumps release version