## 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
Players could not place **any door** while customizing a house, and placing other pieces could disconnect them outright. Staff saw neither problem: `HouseFoundation.Designer_Build` only enforces `ValidPiece` below `GameMaster`.
Original report and diagnosis by @SynPDX.
## Root cause 1 — no door is ever registered
The retail client's `doors.txt` separates its header rows with lines of **bare tabs** (it is the only sheet that does):
```
int<TAB>int<TAB>...<TAB>string
<TAB><TAB><TAB><TAB><TAB><TAB><TAB><TAB><TAB><TAB> <-- 10 tabs, not an empty line
Category<TAB>Piece1<TAB>...<TAB>FeatureMask<TAB>Comment
<TAB><TAB><TAB><TAB><TAB><TAB><TAB><TAB><TAB><TAB>
0<TAB>1657<TAB>1659<TAB>...
```
`Spreadsheet.ReadLine` skipped a line only when `line.Length > 0`. A 10-tab line has length 10, so it was returned as the **names row** — every column ended up named `""`, `GetColumnID("Piece1")` and friends returned `-1`, and not one of the 230 door graphics was registered. Unregistered item IDs keep the `-1` sentinel, and `CheckValidity` rejects those, so `ValidPiece` refused every door.
ClassicUO skips these lines (`string.IsNullOrWhiteSpace` in `HouseCustomizationManager.ParseFile`), which is why the client happily offers doors the server then rejects.
Measured against a retail 7.0.x `doors.txt` using the shipped `Spreadsheet`:
| | `FeatureMask` column | door graphics registered |
|---|---|---|
| before | `-1` | **0** |
| after | `9` | **230** |
## Root cause 2 — `IndexOutOfRangeException` out of the packet handler
Every sheet ends in a cosmetic `Comment` column that ModernUO never reads, and client sheets write an empty comment as a plain newline with no trailing tab. `Split('\t')` then returns one field fewer than the header declares, and the parser indexed past the end:
```
System.IndexOutOfRangeException: Index was outside the bounds of the array.
at Server.Multis.Spreadsheet..ctor(String path)
at Server.Multis.ComponentVerification.LoadSpreadsheet(...)
at Server.Multis.ComponentVerification.IsItemValid(Int32 itemID)
at Server.Multis.HouseFoundation.ValidPiece(Int32 itemID, Boolean roof)
at Server.Multis.HouseFoundation.Designer_Build(NetState state, ...)
```
The client's own parser only requires the columns up to `FeatureMask` — ClassicUO's `CustomHouseMisc.Parse` guards on `scanf.Length >= 12` for a 13-column `misc.txt` — so such a row is valid data listing real pieces. Missing trailing fields are now treated as empty rather than dropping the row, which would unregister every piece the row lists and reproduce the door symptom.
`EnsureLoaded` also set `_loaded` before loading, so once the throw escaped, an all `-1` table stayed cached and rejected everything for players from then on — the same player-visible symptom as #2500.
## Also made explicit rather than accidental
- **Named the table sentinels.** `NotAComponent` (-1) is the anti-cheat guard and the initial state; `NoFeatureRequired` (0) is a piece with no expansion gate — how `walls.txt` encodes pre-AOS base pieces and what `housing.bin` collapses to under `HousingTierMask` (#2500).
- **A sheet with no `FeatureMask` column is refused and logged.** `GetInt32` on a missing column returns 0 = `NoFeatureRequired`, which would have silently marked every piece in that sheet unconditionally placeable regardless of expansion. This was previously only harmless by accident.
- **A sheet matching none of its expected tile columns is refused and logged** — that is what `doors.txt` was doing silently. Individual missing columns stay tolerated, since older sheets predate columns such as `walls.txt`'s `SecondAltWindowS`/`E`.
- **Catch per sheet**, so one unreadable file no longer costs the other six.
- **Header guards**: an empty file or a types-only file raised a `NullReferenceException`; a names row shorter than the types row indexed past the end.
- **Fall back to the component sheets when `housing.bin` cannot be read**, instead of passing `null` into a `SpanReader`.
`_loaded` is still set before loading, deliberately: this runs from the design packet handler, and retrying would re-read every sheet on each subsequent placement attempt.
Sheet precedence is **unchanged** — the client's copies stay authoritative and `Data/Components` remains the fallback.
## Verification
- Retail 7.0.x client `doors.txt` through the shipped `Spreadsheet`: 0 door graphics before, 230 after.
- 5 new tests in `SpreadsheetTests` covering the tab separators, the omitted trailing field, per-row recovery, and both header guards. All 5 fail against `main` and pass here.
- `dotnet build` clean (0 warnings, 0 errors); `UOContent.Tests` 642/642.
## Summary
House customization rejected ~40% of components — all classic/base tiles such as **sandstone** — for non-staff players. The pieces appeared briefly in the editor, then vanished before commit; only staff (GM+) could add them.
## Root cause
A data-convention mismatch introduced when housing.bin support was added (#2329).
- The OSI `housing.bin` encodes pre-AOS base pieces with the client **T2A** feature bit (`0x1`).
- `walls.txt` (and RunUO) encode the same pieces as `FeatureMask = 0` (always valid).
- `ComponentVerification.CheckValidity` validates against `ExpansionInfo.HousingFlags`, whose enum has no `0x1` bit, so base pieces failed `(HousingFlags & 0x1) != 0`.
- `HouseFoundation.Designer_Build` only enforces `ValidPiece` for `AccessLevel < GameMaster`, so staff bypassed validation while players had placements rejected — the server re-sends the design state and the client rebuilds the house from it, erasing the just-placed piece.
This only affects servers loading `housing.bin` from a UOP client; the old txt-only path (pre-#2329, like RunUO) was unaffected because base pieces are `0` there.
## Fix
Normalize the `housing.bin` feature mask to the housing-tier bits on load (`& HousingFlags.HousingEJ`). Base pieces collapse to `0` (always valid, exactly as `walls.txt` encodes them); `AOS`/`SE`/`ML`/... pass through unchanged. Both data sources now produce an identical validity table, matching RunUO behavior. `CheckValidity` and the `val != -1` anti-cheat guard are unchanged.
## Verification
- Parsed the real `housing.bin` from a 7.0.x client: sandstone (`0x345`) loads as `0x1` → `& HousingEJ` → `0` → valid; tier pieces (`0x40` SE, etc.) pass through; unregistered tiles stay `-1` → rejected.
- Confirmed OSI's own files disagree for the same pieces: `walls.txt` base `FeatureMask = 0` vs `housing.bin` `0x1`.
- `dotnet build` clean (0 warnings, 0 errors).
## Problem
Houses and boats (multis) were pathed correctly only by **delegation to the slow path**: `StepCache.TryGetMask` returns `Fallthrough_Multi` for any multi-covered cell, and `GetSuccessors` ran `CheckMovement` **8× per cell** (each re-resolving the tile stack via `GetStaticAndMultiTiles`) — a sustained per-step cost near every house/boat. There was also no automated test pinning multi pathfinding.
This branch is the full multi-pathfinding effort in phases on one branch.
## Phase 1 — characterization tests (the oracle)
Implementation-agnostic invariants: a cache-on≡cache-off whole-path invariant, a per-cell sweep vs `CheckMovement` over footprint+halo (incl. destination Z), hand-verified routing (around walls, demolish-reopens, foundation-redesign-honored), classic-house / foundation / boat fixtures, non-vacuity guards. These gate every later phase byte-for-byte.
## Phase 2 — live single-pass synthesizer
`StepProbe.ComputeMultiMaskAt` synthesizes a covered cell's full 8-direction `StepMask` in one pass (the existing surface/step logic over `GetStaticAndMultiTiles` instead of 8× `CheckMovement`). `GetSuccessors` routes `Fallthrough_Multi` cells through it. No new cache, no `.swb` change. **~1.5×**, zero added allocations.
## Phase 3 / 3.1 — warm per-`multiID` interior cache (airtight)
`MultiMaskCache` caches each fixed multi's local-frame `StepMask` for **interior** cells (cell + all 8 neighbours covered → terrain-neighbour-free → position-invariant), keyed by `multiID & 0x3FFF`, built lazily from the MCL. Interior cells become ~20 ns lookups.
The cache is gated on a **per-instance footprint-clean flag** (`BaseMulti.PathInteriorCacheState`): an instance whose whole footprint terrain is below its floor (`maxTerrain < minFloor`) serves from the cache; a **dirty** instance (terrain intrudes — a contrived/GM placement) **degrades to live-synth, never a wrong mask**. This closes a cross-instance soundness gap (the cached mask depends on neighbour terrain too) found in a holistic review. The gate resets whenever the footprint's world-terrain relationship can change — **location, map, or ItemID** (a boat's heading swaps the MCL).
**Boats are cached too.** Their per-`multiID` deck masks are movement-invariant (built once per heading), so a sailing boat never rebuilds them; only the cheap clean-flag rescan repeats per move (and only when pathed near). Narrow existing boats have little interior; wide galleons (`multi.mul`) would gain Castle-class. `HouseFoundation` (per-instance runtime `DesignState`) is the one type that stays on the live path.
## Verification
- `UOContent.Tests` **454/454**, `Server.Tests` **708/708**, 0 failures.
- The Phase-1 oracle (`MultiPathInvariantTests`, cache-on ≡ cache-off) stays **byte-identical** with the synthesizer + interior cache active.
- Tests pin: footprint-cleanliness (clean vs sunk), dirty/cluttered placement degrades to live-synth while still pathing, clean placement serves, and the gate resets on move/ItemID change.
## Performance (modernuo/ModernUO-Benchmarks#8, full-fixture)
Houses at **Green Acres** (flat staff region → clean footprints, the legit-placement case):
| Route | Slow path | Phase 3.1 (interior cache) | Speedup |
|-------|----------:|---------------------------:|--------:|
| `around_a` (29 steps) | 238.3 µs | **49.1 µs** | **4.85×** |
| `around_b` (29 steps) | 224.3 µs | **49.5 µs** | **4.53×** |
~130 of ~167 multi cells/route serve from the cache (~20 ns) vs 37 live-synth. Per-cell, the slow path's 8× `CheckMovement` grows with multi complexity (GuildHouse ~857 ns → Castle ~1,194 ns), the synthesizer is a flat ~780 ns, and the cache serve is ~20 ns — so big/tall multis (and wide galleons) gain most. Identical allocations throughout.
## Summary
- Converts `HouseGumpAOS` from legacy `Gump` to `DynamicGump` with a private constructor and the static `DisplayTo` entry-point pattern.
- `AddPageButton`, `AddButtonLabeled`, and `AddList` helpers now write directly to the `DynamicGumpBuilder` via `ref` parameters.
- All internal re-display calls and the `HouseSign` / `ConfirmResizeHouseGump` callers route through `HouseGumpAOS.DisplayTo`.
- Field naming updated to underscore-prefix convention (`_house`, `_page`, `_from`, `_list`, `_hangerNumbers`, `_foundationNumbers`, `_postNumbers`, `_houseSigns`).
## Summary
- Converts the legacy pre-AOS `HouseGump` to `DynamicGump` with a private constructor and the static `DisplayTo` entry-point pattern.
- `HouseListGump` and `HouseRemoveGump` now route back through `HouseGump.DisplayTo` instead of constructing the gump directly.
- Updates the `HouseSign` caller accordingly.
### Summary
Updates all calls to container.EnumerateItems() to properly dispose of the underlying PooledRefQueue so that we are properly recycling pooled arrays.
### Summary
Changes component verification so the order is now housing.bin, then txt files in client, then txt files in Data/Components folder on the server.
> [!Important]
> **Developer Note**
> This code change will **completely move gumps out of the core**
### Summary
- Adds `GetGumps()` convenience which exposes methods to Find/Close/Send multiple gumps. This helper is a performance improvement by eliminating the Dictionary<Player, List> lookup for gumps.
## 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
### Changes
- Adds `[IgnoreDupe]` and `[SerializedIgnoreDupe]`
- Updates all _known_ classes that need the attribute. Some might be missing, please helps us find them!
- Adds `Item.Dupe()` command and encapsulates `CopyProperties` and `OnAfterDuped`. This is also overridable.
- Updates Dupe command to use the new logic.
- Fixes duping multiple kinds of objects that used to be outright broken.
### Bug Fixes
- Fixes issue with durability after duping
- Fixes issue with hue after duping
> [!Note]
> **Developer Note**
> Customizing how duping an item works now requires two steps:
> 1. Add `[IgnoreDupe]` or `[SerializedIgnoreDupe]` to the property/field
> 2. Add custom logic in an `OnAfterDuped` override
>
> When do you need to do this?
> *When the property being copied is not a primitive, and you need to manually deep-clone the contents of the property such as with Lists, Dictionaries, or sub classes.*
> [!Warning]
> Users on Linux/OSX will need to follow the Readme
> and make sure `libdeflate` is properly installed
> [!Note]
> **Developer Note**
> The API for compression has changed. Use `Deflate.Standard` for the same functionality.
### Summary
* Replaces Zlib with LibDeflate for a 50% performance improvement!
* Adds MacOS 14 to properly test Arm64
### Summary
* Adds `StaticNoticeGump<T>` and `StaticWarningGump<T>`
* Converts NoticeGump/WarningGump to use `DynamicGump`
* Changes various uses of notice gump and warning gump to their static counterpart.