feat(net): wire 0xF625 ObjDescEvent for live appearance updates
Retail-driven players observed from acdream rendered with stale appearance — wrong skin/hair palettes, missing clothing — because ACE's mid-session appearance broadcasts (equip/unequip/tailoring/ recipe/option-toggle) ride opcode 0xF625 ObjDescEvent and acdream silently dropped them. Initial CreateObject carries the appearance at spawn time, but every later equip change only updates via 0xF625 (per Skunkwors protocol docs in ACE/.../GameMessageObjDescEvent.cs). Retail handles via SmartBox::HandleObjDescEvent (named-retail 0x453340). Why: the retail observer sees the *server-relayed* view of remotes, not retail's local build, so dropping ObjDescEvent freezes appearance at the partial state in the first CreateObject. How: - Extract CreateObject's ModelData parsing into reusable CreateObject.ReadModelData(span, ref pos) returning (BasePaletteId, SubPalettes, TextureChanges, AnimPartChanges). - Add ObjDescEvent.cs (parser for 0xF625): body = u32 opcode | u32 guid | ModelData | u32 instanceSeq | u32 visualDescSeq. - WorldSession.AppearanceUpdated event + dispatcher branch. - GameWindow.OnLiveAppearanceUpdated splices new ModelData onto the cached spawn and replays via OnLiveEntitySpawned. The dedup at the start of OnLiveEntitySpawnedLocked tears down the old GPU/animated/ collision state cleanly before rebuild. - _lastSpawnByGuid cache populated at spawn-end and tracked through UpdatePosition so re-applies use current position (no pop-back to login spot on equip toggle). - ACDREAM_DUMP_APPEARANCE=1 env var prints structured SP/TC/APC decode for every 0xF625 — replaces the earlier raw-hex preview. - ACDREAM_DUMP_CLOTHING extended with setup.Parts.Count, flatten.Count, and per-part triangle counts for offline polygon-budget audit. Tests: 4 new ObjDescEvent tests (round-trip + parser drift guard); 269 net tests green. User-verified live: skin/hair colors match retail's character data; equip/unequip no longer pops position. Note: a separate "puffy arms / bulky body" geometry issue remains where base body parts visibly overlap clothing meshes — different root cause, tracked separately.
This commit is contained in:
parent
24407fec3c
commit
e471527924
5 changed files with 441 additions and 62 deletions
|
|
@ -139,6 +139,20 @@ public sealed class WorldSession : IDisposable
|
|||
/// </summary>
|
||||
public event Action<uint>? TeleportStarted;
|
||||
|
||||
/// <summary>
|
||||
/// Fires when the server broadcasts an <c>ObjDescEvent (0xF625)</c> —
|
||||
/// a creature/player's appearance changed after the initial CreateObject
|
||||
/// (equip / unequip / tailoring / recipe result / character option toggle).
|
||||
/// Subscribers re-apply the new <c>ModelData</c> to the existing entity:
|
||||
/// AnimPartChanges replace mesh refs, TextureChanges update per-part
|
||||
/// surface texture overrides, and SubPalettes rebuild the palette
|
||||
/// override (the channel that carries skin/hair tone). Without this,
|
||||
/// retail-driven characters observed from acdream end up "stuck" at
|
||||
/// whatever appearance was in their first CreateObject — see issue
|
||||
/// notes in commit history around 2026-05-06.
|
||||
/// </summary>
|
||||
public event Action<ObjDescEvent.Parsed>? AppearanceUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Phase H.1: fires when a local or ranged speech message (0x02BB /
|
||||
/// 0x02BC) is received. Subscribers typically feed these into a
|
||||
|
|
@ -314,12 +328,18 @@ public sealed class WorldSession : IDisposable
|
|||
private readonly FragmentAssembler _assembler = new();
|
||||
|
||||
// Issue #5 diagnostics (env-var-gated):
|
||||
// ACDREAM_DUMP_OPCODES=1 → log first occurrence of each unhandled opcode
|
||||
// ACDREAM_DUMP_VITALS=1 → log every PrivateUpdateVital(Current) parse
|
||||
// ACDREAM_DUMP_OPCODES=1 → log first occurrence of each unhandled opcode
|
||||
// ACDREAM_DUMP_VITALS=1 → log every PrivateUpdateVital(Current) parse
|
||||
// ACDREAM_DUMP_APPEARANCE=1 → log every 0xF625 ObjDescEvent + 0xF7DB UpdateObject
|
||||
// with body len, target guid, hex preview. Used to
|
||||
// debug remote-player appearance asymmetry (retail
|
||||
// observer in acdream renders wrong skin/hair).
|
||||
private static readonly bool DumpOpcodesEnabled =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_DUMP_OPCODES") == "1";
|
||||
private static readonly bool DumpVitalsEnabled =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_DUMP_VITALS") == "1";
|
||||
private static readonly bool DumpAppearanceEnabled =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_DUMP_APPEARANCE") == "1";
|
||||
private readonly System.Collections.Generic.HashSet<uint> _seenUnhandledOpcodes = new();
|
||||
|
||||
private IsaacRandom? _inboundIsaac;
|
||||
|
|
@ -861,6 +881,36 @@ public sealed class WorldSession : IDisposable
|
|||
_teleportSequence = sequence; // track for outbound movement messages
|
||||
TeleportStarted?.Invoke(sequence);
|
||||
}
|
||||
else if (op == ObjDescEvent.Opcode)
|
||||
{
|
||||
// 0xF625 ObjDescEvent — per-entity appearance update. ACE
|
||||
// broadcasts on equip/unequip/tailoring/recipe/option-change
|
||||
// (Creature_Equipment.cs:365, Tailoring.cs:504,
|
||||
// RecipeManager.cs:403, GameActionSetSingleCharacterOption.cs:27).
|
||||
// Retail handler: SmartBox::HandleObjDescEvent (named-retail
|
||||
// 0x453340). Body layout: u32 opcode | u32 guid | ModelData |
|
||||
// u32 instanceSeq | u32 visualDescSeq.
|
||||
var parsed = ObjDescEvent.TryParse(body);
|
||||
if (parsed is not null)
|
||||
{
|
||||
if (DumpAppearanceEnabled)
|
||||
{
|
||||
var md = parsed.Value.ModelData;
|
||||
Console.WriteLine($"appearance: 0xF625 guid=0x{parsed.Value.Guid:X8} basePal=0x{(md.BasePaletteId ?? 0):X8} subPals={md.SubPalettes.Count} texChanges={md.TextureChanges.Count} animParts={md.AnimPartChanges.Count}");
|
||||
foreach (var sp in md.SubPalettes)
|
||||
Console.WriteLine($" SP id=0x{sp.SubPaletteId:X8} offset={sp.Offset} length={sp.Length}");
|
||||
foreach (var tc in md.TextureChanges)
|
||||
Console.WriteLine($" TC part={tc.PartIndex:D2} oldTex=0x{tc.OldTexture:X8} -> newTex=0x{tc.NewTexture:X8}");
|
||||
foreach (var apc in md.AnimPartChanges)
|
||||
Console.WriteLine($" APC part={apc.PartIndex:D2} -> gfx=0x{apc.NewModelId:X8}");
|
||||
}
|
||||
AppearanceUpdated?.Invoke(parsed.Value);
|
||||
}
|
||||
else if (DumpAppearanceEnabled)
|
||||
{
|
||||
Console.WriteLine($"appearance: 0xF625 PARSE FAILED body.len={body.Length}");
|
||||
}
|
||||
}
|
||||
else if (DumpOpcodesEnabled)
|
||||
{
|
||||
// ACDREAM_DUMP_OPCODES=1 — emit a one-line trace per
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue