## 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
The save-stability infrastructure consumed by generator v3's `[AnchoredDateTime]`: anchored timestamps are written as **absolute values** and re-based once at load by the elapsed time since the save started — so downtime doesn't age them, and an unchanged entity serializes to identical bytes (the prerequisite for replacing delta-time encodings, which rewrite every entity on every save).
## Design
- **`WriteAnchoredTime` / `ReadAnchoredTime`** on `IGenericWriter`/`IGenericReader`. The read side applies the reader's `AnchoredTimeShift`; `Min/MaxValue` sentinels pass through unshifted, and shifts saturate instead of overflowing.
- **`World.SaveStartTime`** is stamped the moment the world freezes for a snapshot — one anchor for the entire save, no per-persistence skew.
- **idx v5**: the anchor ticks sit in the header right after the version. The anchor travels with the file it re-anchors, so a single idx+bin pair restored from a backup is self-describing, and anchor presence is guaranteed by the same version gate as the record format — there is no separate anchor file to lose.
- **The shift rides the reader instance** (`BufferReader`, `UnmanagedDataReader`, `BinaryFileReader` delegating), not a static — parallel per-persistence loads and ad-hoc restores each see their own file's anchor. idx v4 and older read with a zero shift.
## Scope
Behavior-neutral: nothing serializes anchored values yet (`Item.DecayResetTime` and the `[DeltaDateTime]` field migrations come separately, with their own version bumps). Saves written from this branch are idx v5; loading v4/v3 saves is unchanged and remains pinned by the existing hand-written-header tests.
## Testing
- Unit round-trips: exact with zero shift, shifted read, sentinel passthrough, saturation, Local→UTC normalization.
- End-to-end through the real worker/segment-log pipeline: an anchored timestamp re-bases across a simulated two-hour downtime via the idx v5 header.
- Full suites green: Server.Tests 835/835, UOContent.Tests 708/708 (including the existing v4/v3 idx loading tests).
Fixes serialization/deserialization edge cases with BitArray. If you use BitArray, you will need to migrate.
1. Change the type in the migration JSON file (if there is one) from `BitArray` to `byte[]` for all the versions you need to migrate.
2. Then in the `MigrateFrom`, use the following function to convert the field from a byte[] back to the BitArray.
Example Migration JSON:
```json
{
"name": "RestrictedSpells",
"type": "byte[]",
"rule": "ArrayMigrationRule",
"ruleArguments": [
"byte",
"PrimitiveTypeMigrationRule",
""
]
},
```
Migration function to use in MigrateFrom:
```cs
public static BitArray MigrateBitArray(byte[] data, int bitLength) => new(data) { Length = bitLength };
```
Example use:
```cs
private void MigrateFrom(V0Content content)
{
// ... deserialize
_restrictedSpells = content.RestrictedSpells.MigrateBitArray(SpellRegistry.Types.Length);
_restrictedSkills = content.RestrictedSkills.MigrateBitArray(SkillInfo.Table.Length);
// ... rest of deserialize
}
```
### Summary
Updates the serialization strategy to use `MemoryMappedFile` instead of thick buffers. This has the benefit of being on-par with the current implementation (based on hardware/OS), however won't incur the double-memory issue.
> [!Important]
> **Developer Note**
> The `BinaryFileWriter` and `BinaryFileReader` has been removed in favor of `MemoryMapFileWriter` and `UnmanagedDataReader`
### Summary
The BitArray class will be optimized over the next several years for various platforms/hardware and maintaining a duplicate for serialization is not practical. Removing the custom implementation. Recommend against using BitArray for serialization unless it is absolutely necessary.
### Summary
- Fixes usernames not being `Intern`ed
- Reverts methods related to getting accounts from returning `Account` to `IAccount`.
- Makes `IAccount` also `ISerializable`
- Adds `IGenericReader.ReadAccount()` and `IGenericWriter.Write(IAccount)` -> The read method supports the original serialization of username, and using `IAccount.Serial`. The write method only serializes the `Serial`.
- Exposes `ReadStringRaw()` to allow some advanced scenarios.
### BREAKING CHANGE ###
The constructor for `TextDefinition` has been removed. Instead use `TextDefinition.Of()` or cast the integer/string to TextDefinition.
## Changes
* Improves type hashing by introducing xxHash3 (64bit)
* Removes individual `tdb` files in favor of a single `SerializedTypes.db` file. This file is only used to identify a type that is being deserialized, which doesn't exist.
* Adds duplicate type alias detection
* Adds `AssemblyHandler.FindTypeByHash`
View changed files whitespaces: https://github.com/modernuo/ModernUO/pull/1172/files?diff=split&w=1
## SerializedTypes.db
The serialized types file is used to get back the original name of a type in case it no longer exists in code. This can easily be necessary if a class is renamed in code and no `TypeAlias` is provided.
### Format
byte[4] - version
byte[4] - count
--array--
byte[8] - xxHash
byte[1] - flag, 0 - null, 1 - not null
byte[n] - Full class name in UTF8
### Example
<img width="472" alt="SerializedTypes_Example" src="https://user-images.githubusercontent.com/3953314/195255429-31d24293-6bd1-419e-811b-07874dd0f78d.png">
## Benchmarks
Serialized 500 Type fields. The 8192bytes comes from the _ConcurrentQueue_ that would later be used for SerializedTypes.
Note that the queue is never cleared, so it's size grew considerably.
```cs
| Method | Mean | Error | StdDev | Allocated |
|--------------------- |---------:|---------:|---------:|----------:|
| BenchmarkXXHash | 18.44 us | 0.278 us | 0.260 us | 8192 B |
| BenchmarkTypeStrings | 25.09 us | 0.292 us | 0.259 us | - |
```
TODO:
* Add support in the Serialization Generator for `ReadType()` and `Write(Type)`
* Remove `SetTypeRef` from Serialization Generator
**Only one functional change**
* Fixes a bug in LogFactory where `Warning` is being logged as `Information`
Non-functional changes:
* Updates/Fixes copyright headers
* Removes namespace scopes for core files.
View with [whitespace off](https://github.com/modernuo/ModernUO/pull/1187/files?w=1).
* Adds a custom BitArray class with the following added features:
* ctor for creating BitArray against read only span
* ctor for creating BitArray against BinaryReader
* CopyTo to copy a BitArray to a Span
* Adds BitArray to UO Primitive serialization so it can be codegenned.
* Adds `BeforeSerialized` for entities to handle cleanup.
* Adds `Created` and `LastSerialized` fields to all entities. While this is a big bloat, this will be necessary for identifying dangling references to other invalid entities.
* Changes formula for determining a valid reference to be _not null, not deleted, and reference's created date must be at or before the entities last serialized date_.
* Adds versioning to idx file and serializes `Created` and `LastSerialized`.
* Fixes Save Stats and also disables it by default.
- [X] Fixes bad `ReadEnum` by size
- [X] Fixes array, list, and set not handling null values properly.
- It will be up to the user (for now) to null out empty lists using `[AfterDeserialization]`. Convenience may be added later.
- [X] Fixes errors with `dotnet clean` and non-empty generation folder
- [X] Fixes bad field indexes on `Account.cs` causing `tags` to not be serialized/deserialized.
- This was caused by a duplicate entry. Don't have protection against this _yet_.
- [X] Adds `AdhocPersistence` which replaces RunUO `Persistence`
- [X] Fixes a few minor bugs with EntityPersistence deserialization
- [X] Reverts/updates some serialization changes
- [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] Speeds up world loading
- [X] Removes PeekChar cause it wasn't used
- [X] Adds a BufferReader
- [X] Removes BinaryFileReader
- [X] Removes reading/writing `char` since it is not consistent and will cause a deserialize issue if used.
Bumps version release
- [X] Fixes bugs in the buffer writer
- [X] Removes background save commands
- [X] Avoids calling Serialize() on deserialization
- In a future optimization I will copy the entire object to a buffer, deserialize using a SpanReader, then use that buffer for the SaveBuffer
- [X] Fixes issue with decay queue.
Bumps release version