ModernUO/Projects/UOContent/Items
Kamron Batman a8acfa31f8
refactor: decompose mobile/corpse hair, delete VirtualHairInfo, fix removal serial (#2462) (#2463)
Fixes #2462

## Summary
Removes the per-object `VirtualHairInfo` heap wrapper for mobile/corpse hair. Hair is now stored **inline** on `Mobile` and `Corpse` as `int _hairItemId` / `int _hairHue` plus a lazily-allocated, **non-serialized** ephemeral `Serial _hairSerial` (in the high virtual-serial range) — and likewise for facial hair. The `VirtualHairInfo` class is deleted, with a **lossless** save migration.

This delivers three things:
1. **Fixes a hair-removal bug.** `Delta(MobileDelta.Hair)` is deferred (it enqueues; `ProcessDeltaQueue` runs later in the tick). The old `HairItemID = 0` setter nulled `_hair` *immediately*, so by the time `ProcessDelta` built the remove packet the equipped virtual serial was already gone — the old `??=` code then re-materialized a **fresh** serial (≠ the equipped one), so clients never removed the right entity, and it left a phantom ItemId-0 object behind. The serial now lives on the entity and **persists across removal**, so remove packets carry the correct serial.
2. **Lightens the entity.** No heap hair object; bald mobiles allocate nothing (the serial is minted lazily only when hair is present). This was the original reason `HairItemID`/`HairHue` exist.
3. **Removes `VirtualHairInfo` entirely**, keeping the high-range virtual serial behavior.

## How
- **Mobile** (manual serialization): inline `_hairItemId/_hairHue/_hairSerial` (+facial); lazy `HairSerial`/`FacialHairSerial`; `ProcessDelta` reads those. Serialization **v36 → v37** — the v30-v37 deserialize is unified, reading the legacy per-hair `VirtualHairInfo` version int only when `version < 37`. Setting item id to 0 clears the hue (matching the old object-nulling) while retaining the serial.
- **Corpse** (codegen serialization): decomposed to `[SerializableField] int _hairItemId/_hairHue` (+facial) + ephemeral serial; **v16 → v17** with `MigrateFrom(V16Content)`.
- **Lossless migration:** the loader validates exact byte length, and the old corpse hair is a presence-bool-gated block, so a tiny **migration-only** `LegacyHairInfo` reader (no runtime role) consumes the legacy `[bool][int ver][int itemId][int hue]` bytes. Frozen `Corpse.v14/v15/v16.json` are retyped to it; `v17.json` describes the new int fields.
- All consumers updated to discrete accessors: `OutgoingMobilePackets`, `CorpsePackets`, corpse subclasses (`MilitiaFighterCorpse`, `SchmendrickApprenticeCorpse`), and the packet test mirrors.
- `VirtualHair.cs` renamed to `OutgoingVirtualHairPackets.cs` (the only type left in it after `VirtualHairInfo` was removed).

## Test Plan
- [x] Full solution build: **0 warnings, 0 errors** (`TreatWarningsAsErrors`).
- [x] `Server.Tests`: **708 passed** (incl. new `RemoveHairUsesEquippedSerial` / `RemoveFacialHairUsesEquippedSerial` proving the serial survives removal + hue clears).
- [x] `UOContent.Tests` corpse/hair: **6 passed** (incl. `CorpseHairMigrationTests` asserting the legacy hair bytes are consumed exactly — the loader's length invariant).
- [x] Generated migration code inspected: V14/V15/V16 readers consume the legacy block byte-for-byte; serial never written to disk.

## Upgrade notes
- Old Mobile (v30–v36) and Corpse (v13–v16) saves load losslessly.
- Minor cosmetic-only change: `SchmendrickApprenticeCorpse` hair/facial-hair RNG draws shift order within each pair (same draw count); irrelevant for a quest NPC corpse.
2026-06-06 14:33:28 -07:00
..
Addons perf: Migrate Holiday and Decorative gumps to DynamicGump/StaticGump (#2413) 2026-04-25 17:22:41 -07:00
Aquarium perf: Migrate Holiday and Decorative gumps to DynamicGump/StaticGump (#2413) 2026-04-25 17:22:41 -07:00
Armor perf: Migrate Item Interaction gumps to DynamicGump/StaticGump (#2421) 2026-04-26 11:31:49 -07:00
Body Parts feat: Adds pre-T2A-pub15 bounty system (#2377) 2026-03-22 12:35:16 -07:00
Books fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Bulletin Boards feat: Adds pre-T2A-pub15 bounty system (#2377) 2026-03-22 12:35:16 -07:00
Champion Artifacts fix: Optimizes items to use default weights. (Part 2) (#2241) 2025-07-24 15:41:44 -07:00
Clothing feat: Adds SerializableFieldChanged option (#2324) 2026-02-01 21:28:26 -08:00
Construction feat: Adjusts fame and karma system with era gates for OSI accuracy (#2389) 2026-04-25 11:19:57 -07:00
Containers perf: Migrate SoulStone and TMap chest gumps to DynamicGump (#2417) 2026-04-25 20:38:24 -07:00
Decoration Artifacts fix: Optimizes items to use default weights. (Part 2) (#2241) 2025-07-24 15:41:44 -07:00
Deeds perf: Migrate vendor management gumps to DynamicGump/StaticGump (#2431) 2026-05-03 10:19:12 -07:00
Facial Formats UO Content (#201) 2020-08-27 18:30:38 -07:00
Farming fix: fixes the id for farmable cabbage (#2227) 2025-07-02 15:04:48 -07:00
Food feat: Adds Endless Decanter of Water and Water Elemental acquisition (#2371) 2026-03-14 16:45:06 -07:00
Games fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Gems fix: Codegens gems (#1059) 2022-06-13 17:38:46 -07:00
Guilds perf: Migrate Old Guild System gumps to DynamicGump (#2420) 2026-04-26 10:44:11 -07:00
Jewels fix: Optimizes items to use default weights. (Part 2) (#2241) 2025-07-24 15:41:44 -07:00
Lights fix: Optimizes items to use default weights. (Part 3) (#2242) 2025-07-24 16:21:27 -07:00
Maps fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Minor Artifacts fix: Optimizes items to use default weights. (Part 3) (#2242) 2025-07-24 16:21:27 -07:00
Misc refactor: decompose mobile/corpse hair, delete VirtualHairInfo, fix removal serial (#2462) (#2463) 2026-06-06 14:33:28 -07:00
New Haven Quest Rewards fix: Codegen new haven quest rewards (#1269) 2022-11-23 13:00:28 -08:00
PlantsFlowers/Potted fix: Optimizes items to use default weights. (Part 2) (#2241) 2025-07-24 15:41:44 -07:00
Quivers chore: Use var everywhere (#2294) 2025-12-27 16:47:28 -08:00
Resources fix: Optimizes items to use default weights. (Part 3) (#2242) 2025-07-24 16:21:27 -07:00
Shields fix: Optimizes items to use default weights. (Part 3) (#2242) 2025-07-24 16:21:27 -07:00
Skill Items perf: Migrate Bulletin/Poll/SOS gumps to DynamicGump (#2418) 2026-04-25 20:49:12 -07:00
Special fix: HouseRaffleStone timer cleanup (#2456) 2026-05-23 10:39:58 -07:00
Suits fix: Optimizes items to use default weights. (Part 4) (#2243) 2025-07-24 23:29:29 -07:00
Talismans fix: Moves containers/bods/traps/etc to serialization generator (#2310) 2026-01-07 21:55:55 -08:00
Traps fix: Moves containers/bods/traps/etc to serialization generator (#2310) 2026-01-07 21:55:55 -08:00
TreasureChests fix: Updates serialization to use v2.0 (#998) 2022-04-17 08:02:15 -07:00
Wands fix: Optimizes items to use default weights. (Part 4) (#2243) 2025-07-24 23:29:29 -07:00
Weapons feat: Add zero-alloc interpolation handler to ValueStringBuilder, replace all StringBuilder usage (#2387) 2026-03-22 14:23:44 -07:00