ModernUO/Projects/UOContent/Items/Misc
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
..
Bedlam
Blighted Grove
Corpses
Labyrinth
Painted Caves
Palace of Paroxysmus
Prism of Light
The Citadel
Twisted Weald
Acid.cs
ArcaneGem.cs
BankCheck.cs
Beeswax.cs
Blocker.cs
Blood.cs
Bola.cs
BolaBall.cs
ClockworkAssembly.cs
CommunicationCrystals.cs
DeceitBrazier.cs
EffectController.cs
EffectItem.cs
ExecutionersCap.cs
Firebomb.cs
FlippableAddonAttribute.cs
FlippableAttribute.cs
GlassItems.cs
Gold.cs
Guillotine.cs
GumpToggleItems.cs
HairDye.cs
HoveringWisp.cs
IDurability.cs
InteriorDecorator.cs
Key.cs
KeyRing.cs
LOSBlocker.cs
Moonstone.cs
MoonstoneGate.cs
MorphItem.cs
OilCloth.cs
OilFlask.cs
Origami.cs
PlayerBulletinBoards.cs
PlayerVendorDeed.cs
PowerCrystal.cs
PowerGenerator.cs
ProjectedItem.cs
PromotionalToken.cs
PublicMoongate.cs
Rares.cs
Scales.cs
SerpentPillar.cs
SpecialBeardDye.cs
SpecialHairDye.cs
Static.cs
SwarmOfFlies.cs
Teleporter.cs
TrashBarrel.cs
TrashChest.cs
TribalBerry.cs
TribalPaint.cs
UnholyBone.cs
WarningItem.cs
WayPoint.cs
WindChimes.cs