fix(#187): drop the "Name == Door" special-case — register any entity with a resolvable MotionTableId
The door-swing animation rescue (GameWindow.cs:3897, for entities whose rest pose
is a static single frame but which still carry a reactive MotionTable) was gated on
an exact display-name string match. Sliding doors, gates, portcullises, and disguised
secret-passage props ("Magic Wall") all fail that check because their in-game Name
isn't literally "Door" -- so ACE's UpdateMotion for them was silently dropped forever
by the _animatedEntities.TryGetValue bail-out in OnLiveUpdateMotion.
Retail's own dispatch chain (ACCObjectMaint::CreateObject -> CPhysicsObj::
set_description -> SetMotionTableID -> CPartArray::SetMotionTableID 0x005186e0 ->
MotionTableManager::PerformMovement) is unconditionally data-driven: the only gate
for creating a motion dispatcher anywhere in that chain is "motion table id != 0" --
no CDoor class, no WeenieType switch, no name check. Production weenie data confirms
Sliding Door / Portcullis / Gate / Magic Wall all carry the identical WeenieType=Door
+ non-zero-MotionTableId shape as a plain "Door", differing only in display name.
Fix: the branch's existing `mtableId != 0` check (already computed one line later)
is now the entire gate, matching retail exactly. IsDoorSpawn deleted (dead code);
IsDoorName kept only for an unrelated diagnostic log-label filter.
Live-verified: sliding doors now animate open/closed correctly. Full regression
green (App 741 / Core 2631).
docs(#188): file the fading-wall render gap surfaced during #187's live gate
A "Pedestal Weak Spot" secret-passage door dispatches correctly (proving #187's fix
reaches it) but never visibly changes. Decoded its actual dat MotionTable directly
(0x090000F9): its open cycle carries EtherealHook + TransparentPartHook +
SoundTableHook -- a translucency-fade effect, not part-transform motion. acdream's
IAnimationHookSink documents these hook types as intended for "GfxObjMesh / renderer
state mutations" but no sink anywhere consumes them (only Particle/Lighting/Audio are
wired) -- confirmed via full-repo grep. Collision already works correctly via a
separate server-authoritative SetState wire message, independent of the animation
hook. This is feature-shaped rendering work (a per-part runtime alpha under the
mandatory N.5 bindless pipeline), not a quick fix -- filed for its own design pass.
Kept Issue188FadingDoorMotionTableInspectionTests.cs as a reusable MotionTable/hook
decoder for future "why doesn't this animate" questions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
0efa7ed2a1
commit
e2e285b855
3 changed files with 271 additions and 47 deletions
107
docs/ISSUES.md
107
docs/ISSUES.md
|
|
@ -46,42 +46,93 @@ Copy this block when adding a new issue:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## #187 — Non-hinged doors (sliding doors, "fading wall" gates) don't play their open animation
|
## #187 — [DONE 2026-07-08] Non-hinged doors (sliding doors, gates) don't play their open animation
|
||||||
|
|
||||||
**Status:** OPEN — under investigation
|
**Status:** CLOSED 2026-07-08 — user-confirmed live gate: "sliding doors now work."
|
||||||
**Severity:** MEDIUM (visual correctness; collision + interaction already work — #137)
|
**Severity:** MEDIUM (visual correctness; collision + interaction already work — #137)
|
||||||
**Filed:** 2026-07-08
|
**Filed:** 2026-07-08
|
||||||
**Component:** render — entity animation registration (`GameWindow.cs` live-spawn dispatch)
|
**Component:** render — entity animation registration (`GameWindow.cs` live-spawn dispatch)
|
||||||
|
|
||||||
**Description (user):** door COLLISION is confirmed fixed (#137) — all doors can be clicked
|
**Root cause (CONFIRMED via retail decomp + a live weenie-data survey):**
|
||||||
and passed through correctly. But not all door TYPES play their open animation: normal
|
`GameWindow.cs:3897` registered the reactive-motion-table rescue sequencer only when
|
||||||
hinged doors swing open, but sliding doors and "fading wall" style dungeon gates do not
|
`spawn.Name == "Door"` (an exact display-name string match). Retail's own client-side
|
||||||
animate — the player passes through as if the door/wall just isn't there.
|
motion dispatch chain (`ACCObjectMaint::CreateObject` → `CPhysicsObj::set_description` →
|
||||||
|
`SetMotionTableID` → `CPartArray::SetMotionTableID` 0x005186e0 → `MotionTableManager::
|
||||||
|
PerformMovement`) is unconditionally data-driven — the only gate anywhere in that chain
|
||||||
|
is "does this object have a non-zero MotionTableId" (`if (ebx != 0)`); there is no
|
||||||
|
`CDoor` class and no name/type check. Production weenie data confirms Sliding Door /
|
||||||
|
Portcullis / Gate / "Magic Wall" all carry the identical WeenieType=Door +
|
||||||
|
non-zero-MotionTableId shape as a plain "Door", differing only in display name — so the
|
||||||
|
name-string gate silently dropped every door-like object not literally named "Door".
|
||||||
|
|
||||||
**Suspected root cause (pre-investigation read, NOT yet decomp-verified):**
|
**Fix:** `GameWindow.cs:3897` — dropped the name check; the branch's existing
|
||||||
`GameWindow.cs:3897` registers the door-swing sequencer only when
|
`mtableId != 0` test (already computed one line later) is now the entire gate, matching
|
||||||
`IsDoorSpawn(spawn)` (`GameWindow.cs:3128`: `spawn.Name == "Door"`, an exact
|
retail exactly. `IsDoorSpawn` deleted (dead code); `IsDoorName` kept only for an
|
||||||
display-name string match). The branch above it (`:3818`) already handles ANY entity
|
unrelated diagnostic log-label filter. Full regression green (App 741 / Core 2631).
|
||||||
generically via its resolved idle cycle (multi-frame, framerate != 0) — no name check.
|
Live-verified: sliding doors now animate open/closed correctly.
|
||||||
Doors need the special branch only because their REST pose is a static single frame
|
|
||||||
(fails the generic gate) while their MotionTable still carries reactive On/Off cycles
|
|
||||||
ACE drives via `UpdateMotion`. That "static rest + reactive motion-table cycle" shape
|
|
||||||
is not unique to hinged doors — any sliding/lever/gate prop with the same shape would
|
|
||||||
need the identical rescue, but only literal name `"Door"` gets it. Retail's own decomp
|
|
||||||
has ZERO `CDoor`-class or door-specific dispatch (only `ObjCollisionProfile::SetDoor`,
|
|
||||||
a collision-only concept) — suggesting retail's client-side animation dispatch is
|
|
||||||
entity-type-agnostic (MotionTable-driven), and the name-string gate is an acdream-only
|
|
||||||
divergence, not a port of anything retail does.
|
|
||||||
|
|
||||||
**Open question for the investigation:** are "fading wall" gates even MotionTable/
|
**Scope note:** the investigation also surfaced a SEPARATE, deeper gap — some
|
||||||
skeletal objects, or a structurally different mechanism (an ObjectDescription /
|
door-family objects (confirmed: "Pedestal Weak Spot", a fading-wall secret passage)
|
||||||
translucency-toggle triggered by a script, no motion involved)? Needs dat/weenie-data
|
don't use ordinary part-transform motion at all; their open cycle is a translucency-fade
|
||||||
survey before designing the fix — must not just add more name strings (same class of
|
+ ethereal-toggle effect that acdream has no rendering sink for. That is NOT a
|
||||||
bug, just deferred).
|
registration problem (this fix's dispatch reaches it correctly) — filed separately as
|
||||||
|
**#188**.
|
||||||
|
|
||||||
**Acceptance:** sliding doors + fading-wall gates play their open/close transition
|
---
|
||||||
matching the door's actual open/closed wire state, same as hinged doors today. Hinged
|
|
||||||
doors keep working (no regression). Suites green.
|
## #188 — "Fading wall" secret passages don't visually fade (missing TransparentHook/EtherealHook render sink)
|
||||||
|
|
||||||
|
**Status:** OPEN
|
||||||
|
**Severity:** MEDIUM (a real but narrow class of dungeon secret-door objects; collision
|
||||||
|
already correct via a separate wire channel)
|
||||||
|
**Filed:** 2026-07-08 (surfaced during #187's live gate)
|
||||||
|
**Component:** render — animation hook dispatch (`IAnimationHookSink` / render-state sinks)
|
||||||
|
|
||||||
|
**Description (user):** a "fading wall" style secret-passage door ("Pedestal Weak Spot")
|
||||||
|
can be used and passed through, but never visibly changes — no fade, no motion, nothing.
|
||||||
|
Confirmed distinct from #187 (registration/dispatch already reaches this entity
|
||||||
|
correctly).
|
||||||
|
|
||||||
|
**Root cause (CONFIRMED via a live dat decode of the actual entity, not inference):**
|
||||||
|
loaded the real MotionTable (`0x090000F9`) for the live-identified "Pedestal Weak Spot"
|
||||||
|
(guid `0x7C95B03B`) and dumped its open cycle's animation hooks directly — the cycle
|
||||||
|
(`anim 0x03000919`, 10 frames) carries **`EtherealHook`, `TransparentPartHook`,
|
||||||
|
`SoundTableHook`** — i.e. this door type's "open" isn't skeletal motion at all, it's a
|
||||||
|
per-part translucency fade + a collision-passthrough toggle + a sound cue, fired as
|
||||||
|
keyframe hooks during the animation.
|
||||||
|
|
||||||
|
`src/AcDream.Core/Physics/IAnimationHookSink.cs`'s own doc comment documents
|
||||||
|
`TransparentHook`/`NoDrawHook`/`ScaleHook`/`ReplaceObjectHook`/etc. as intended to route
|
||||||
|
to "GfxObjMesh / renderer state mutations on the target entity" — but only three sinks
|
||||||
|
are ever registered (`_particleSink`, `_lightingSink`, `_audioSink`; `GameWindow.cs`
|
||||||
|
~1414-1441). No sink anywhere in the codebase pattern-matches `TransparentHook` /
|
||||||
|
`TransparentPartHook` / `EtherealHook` / `NoDrawHook` / `ScaleHook` /
|
||||||
|
`ReplaceObjectHook` (verified by a full-repo grep) — the sequencer correctly parses and
|
||||||
|
fires these hooks every tick, the router fans them out, and all three registered sinks
|
||||||
|
silently ignore them. Nothing crashes (the router swallows exceptions per-sink); nothing
|
||||||
|
renders differently either.
|
||||||
|
|
||||||
|
**Confirmed NOT a collision bug:** the object's ethereal/collision-passthrough state is
|
||||||
|
applied correctly via a SEPARATE server-authoritative wire message (`SetState` →
|
||||||
|
`OnLiveStateUpdated`, `GameWindow.cs:5517`) — independent of the animation-hook
|
||||||
|
mechanism. `EtherealHook` firing client-side during the animation is very likely a
|
||||||
|
redundant/cosmetic signal in retail's own design; the real remaining gap is purely
|
||||||
|
**`TransparentPartHook` → no visible fade**.
|
||||||
|
|
||||||
|
**Scope (not yet designed):** implementing this touches the render pipeline (a per-part
|
||||||
|
runtime alpha under the mandatory N.5 bindless/MDI pipeline — see
|
||||||
|
`memory/reference_modern_rendering_pipeline.md` for the existing SSBO layout
|
||||||
|
constraints) — this is feature-shaped work, not a one-line fix. Needs its own design
|
||||||
|
pass (grep retail's `TransparentHook::Execute`/`SetTranslucency2`/`SetPartTranslucency`
|
||||||
|
decomp for the exact interpolation semantics) before implementation.
|
||||||
|
|
||||||
|
**Apparatus (kept):** `tests/AcDream.Core.Tests/Physics/Issue187FadingDoorMotionTableInspectionTests.cs`
|
||||||
|
— reflects the real `DatReaderWriter` hook-type shapes + decodes a live MotionTable's
|
||||||
|
hook contents directly (no guessing). Reusable for any future "why doesn't this animate"
|
||||||
|
question — just swap the MotionTableId.
|
||||||
|
|
||||||
|
**Acceptance:** the Pedestal Weak Spot (and similar fading-wall objects) visibly fades
|
||||||
|
out/in when triggered, matching retail. No regression to #187's fix or any other door type.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3119,17 +3119,18 @@ public sealed class GameWindow : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Door detection by server-sent name. Doors fail the generic
|
/// Diagnostic-only label filter for the [door-cycle] UM dispatch trail
|
||||||
/// multi-frame-idle gate at line 2692 (no idle cycle), so we register
|
/// (below). #187: this is NOT the functional registration gate — that
|
||||||
/// them via a sibling branch with a state-seeded sequencer. Shared
|
/// was an exact-name match (`spawn.Name == "Door"`) which silently
|
||||||
/// with the [door-cycle] UM dispatch diagnostic — both sites must
|
/// dropped every door-like object whose display name isn't literally
|
||||||
/// agree on the name predicate.
|
/// "Door" (Sliding Door, Portcullis, Gate, "Magic Wall"). The real
|
||||||
|
/// registration gate is now data-driven (MotionTableId != 0, matching
|
||||||
|
/// retail's CPartArray::SetMotionTableID 0x005186e0) — see the rescue
|
||||||
|
/// branch below. Kept only to scope the debug print to door-ish
|
||||||
|
/// entities so the log doesn't fill with every reactive-motion prop.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool IsDoorName(string? name) => name == "Door";
|
private static bool IsDoorName(string? name) => name == "Door";
|
||||||
|
|
||||||
private static bool IsDoorSpawn(AcDream.Core.Net.WorldSession.EntitySpawn spawn)
|
|
||||||
=> IsDoorName(spawn.Name);
|
|
||||||
|
|
||||||
private void OnLiveEntitySpawnedLocked(AcDream.Core.Net.WorldSession.EntitySpawn spawn)
|
private void OnLiveEntitySpawnedLocked(AcDream.Core.Net.WorldSession.EntitySpawn spawn)
|
||||||
{
|
{
|
||||||
_liveSpawnReceived++;
|
_liveSpawnReceived++;
|
||||||
|
|
@ -3894,20 +3895,34 @@ public sealed class GameWindow : IDisposable
|
||||||
_entitySoundTables.Set(entity.Id, soundTableId);
|
_entitySoundTables.Set(entity.Id, soundTableId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (IsDoorSpawn(spawn) && _animLoader is not null)
|
else if (_animLoader is not null)
|
||||||
{
|
{
|
||||||
// Phase B.4c — Door swing animation. Doors fail the
|
// Phase B.4c / #187 — reactive motion-table rescue. An entity
|
||||||
// multi-frame-idle gate above (no idle cycle) but DO have a
|
// whose REST pose is a static single frame fails the generic
|
||||||
// MotionTable with On/Off cycles that ACE drives via
|
// multi-frame-idle gate above, but may still carry a MotionTable
|
||||||
// UpdateMotion. Register with a seeded sequencer so the
|
// with On/Off (or other) cycles the server drives reactively via
|
||||||
// per-frame tick has frames to advance from frame 1 (without
|
// UpdateMotion. Hinged doors, sliding doors, gates/portcullises,
|
||||||
// the seed, Sequencer.Advance(dt) returns no frames and the
|
// and disguised secret-passage props ("Magic Wall") all share
|
||||||
// MeshRefs rebuild at line 7691 collapses the door to origin).
|
// this exact shape. #187: this branch used to be gated on
|
||||||
|
// `spawn.Name == "Door"`, which silently dropped every door-like
|
||||||
|
// object whose display name isn't literally "Door" — the
|
||||||
|
// production weenie data confirms Sliding Door / Portcullis /
|
||||||
|
// Gate / Magic Wall all carry the SAME WeenieType=Door +
|
||||||
|
// non-zero MotionTableId shape as a plain Door, differing only
|
||||||
|
// in name. Retail's own gate for creating a motion dispatcher is
|
||||||
|
// purely data-driven on the motion table id
|
||||||
|
// (CPartArray::SetMotionTableID 0x005186e0: `if (ebx != 0)`) — no
|
||||||
|
// WeenieType switch, no name check — so the `mtableId != 0` test
|
||||||
|
// just below is now the ENTIRE gate, matching retail exactly.
|
||||||
|
// Register with a seeded sequencer so the per-frame tick has
|
||||||
|
// frames to advance from frame 1 (without the seed,
|
||||||
|
// Sequencer.Advance(dt) returns no frames and the MeshRefs
|
||||||
|
// rebuild at line 7691 collapses the entity to origin).
|
||||||
//
|
//
|
||||||
// Initial cycle mirrors ACE's Door.cs:43
|
// Initial cycle mirrors ACE's Door.cs:43
|
||||||
// (CurrentMotionState = motionClosed): Off when the door is
|
// (CurrentMotionState = motionClosed): Off when closed at spawn,
|
||||||
// closed at spawn, On when the spawn PhysicsState carries the
|
// On when the spawn PhysicsState carries the ETHEREAL bit
|
||||||
// ETHEREAL bit (door was already open in ACE's DB).
|
// (already open in ACE's DB).
|
||||||
uint mtableId = spawn.MotionTableId ?? (uint)setup.DefaultMotionTable;
|
uint mtableId = spawn.MotionTableId ?? (uint)setup.DefaultMotionTable;
|
||||||
if (mtableId != 0)
|
if (mtableId != 0)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using DatReaderWriter;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
using DatReaderWriter.Options;
|
||||||
|
using DatReaderWriter.Types;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
using Env = System.Environment;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Tests.Physics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// #188 root-cause evidence (report-only): decode the REAL dat MotionTable
|
||||||
|
/// for the "Pedestal Weak Spot" fading-wall entity identified live
|
||||||
|
/// (guid 0x7C95B03B, MotionTableId 0x090000F9, Setup 0x02000D55). CONFIRMED:
|
||||||
|
/// its open cycle carries EtherealHook + TransparentPartHook + SoundTableHook
|
||||||
|
/// — a translucency-fade effect, not part-transform motion — and acdream has
|
||||||
|
/// no registered <see cref="IAnimationHookSink"/> that consumes Transparent/
|
||||||
|
/// NoDraw/Ethereal/ReplaceObject/Scale hooks (only Particle/Lighting/Audio are
|
||||||
|
/// wired). Kept as a reusable decoder for any future "why doesn't this
|
||||||
|
/// animate" question — swap the MotionTableId.
|
||||||
|
/// </summary>
|
||||||
|
public class Issue188FadingDoorMotionTableInspectionTests
|
||||||
|
{
|
||||||
|
private readonly ITestOutputHelper _out;
|
||||||
|
public Issue188FadingDoorMotionTableInspectionTests(ITestOutputHelper output) => _out = output;
|
||||||
|
|
||||||
|
private static string? DatDir()
|
||||||
|
{
|
||||||
|
var d = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||||||
|
?? Path.Combine(Env.GetFolderPath(Env.SpecialFolder.UserProfile), "Documents", "Asheron's Call");
|
||||||
|
return Directory.Exists(d) ? d : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Reflect_DatReaderWriter_HookTypes()
|
||||||
|
{
|
||||||
|
// Dump the DatReaderWriter assembly's hook-related type shapes so the
|
||||||
|
// real data-walk test below uses correct field names (no guessing).
|
||||||
|
var asm = typeof(MotionTable).Assembly;
|
||||||
|
_out.WriteLine($"assembly: {asm.FullName}");
|
||||||
|
|
||||||
|
foreach (var t in asm.GetTypes().Where(t => t.Name.Contains("Hook", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
_out.WriteLine($"=== type {t.FullName} (base={t.BaseType?.Name}) ===");
|
||||||
|
foreach (var p in t.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||||
|
_out.WriteLine($" {p.PropertyType.Name} {p.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DumpMembers(Type t, string label)
|
||||||
|
{
|
||||||
|
_out.WriteLine($"=== {label} ({t.FullName}) ===");
|
||||||
|
foreach (var p in t.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||||
|
_out.WriteLine($" [prop] {p.PropertyType} {p.Name}");
|
||||||
|
foreach (var f in t.GetFields(BindingFlags.Public | BindingFlags.Instance))
|
||||||
|
_out.WriteLine($" [field] {f.FieldType} {f.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
DumpMembers(typeof(MotionData), "MotionData");
|
||||||
|
DumpMembers(asm.GetTypes().First(t => t.Name == "AnimData"), "AnimData");
|
||||||
|
DumpMembers(typeof(Animation), "Animation");
|
||||||
|
var frameType = asm.GetTypes().FirstOrDefault(t => t.Name is "AnimationFrame" or "AnimFrame");
|
||||||
|
if (frameType is not null) DumpMembers(frameType, frameType.Name);
|
||||||
|
var qdiType = asm.GetTypes().FirstOrDefault(t => t.Name.StartsWith("QualifiedDataId"));
|
||||||
|
if (qdiType is not null) DumpMembers(qdiType, qdiType.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Dump_PedestalWeakSpot_MotionTable_HookContents()
|
||||||
|
{
|
||||||
|
var datDir = DatDir();
|
||||||
|
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
|
||||||
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||||
|
|
||||||
|
const uint MotionTableId = 0x090000F9u; // live-captured from the Pedestal Weak Spot entity
|
||||||
|
var mtable = dats.Get<MotionTable>(MotionTableId);
|
||||||
|
if (mtable is null) { _out.WriteLine($"MotionTable 0x{MotionTableId:X8} NOT FOUND"); return; }
|
||||||
|
|
||||||
|
_out.WriteLine($"MotionTable 0x{MotionTableId:X8}: DefaultStyle=0x{(uint)mtable.DefaultStyle:X8}");
|
||||||
|
|
||||||
|
var allMotionData = new List<(string source, MotionData md)>();
|
||||||
|
foreach (var kv in mtable.Cycles) allMotionData.Add(($"Cycles[{kv.Key:X}]", kv.Value));
|
||||||
|
foreach (var kv in mtable.Modifiers) allMotionData.Add(($"Modifiers[{kv.Key:X}]", kv.Value));
|
||||||
|
foreach (var linkKv in mtable.Links)
|
||||||
|
foreach (var innerKv in linkKv.Value.MotionData)
|
||||||
|
allMotionData.Add(($"Links[{linkKv.Key:X}][{innerKv.Key:X}]", innerKv.Value));
|
||||||
|
|
||||||
|
_out.WriteLine($"total MotionData entries: {allMotionData.Count}");
|
||||||
|
|
||||||
|
var animIdsSeen = new HashSet<uint>();
|
||||||
|
var hookTypesSeen = new SortedSet<string>();
|
||||||
|
|
||||||
|
foreach (var (source, md) in allMotionData)
|
||||||
|
{
|
||||||
|
if (md?.Anims is null) continue;
|
||||||
|
foreach (var animData in md.Anims)
|
||||||
|
{
|
||||||
|
uint animId = GetAnimId(animData);
|
||||||
|
if (animId == 0 || !animIdsSeen.Add(animId)) continue;
|
||||||
|
|
||||||
|
var anim = dats.Get<Animation>(animId);
|
||||||
|
if (anim is null) { _out.WriteLine($" [{source}] anim 0x{animId:X8} NOT FOUND"); continue; }
|
||||||
|
|
||||||
|
int frameCount = 0, hookCount = 0;
|
||||||
|
foreach (var frame in GetFrames(anim))
|
||||||
|
{
|
||||||
|
frameCount++;
|
||||||
|
foreach (var hook in GetHooks(frame))
|
||||||
|
{
|
||||||
|
hookCount++;
|
||||||
|
hookTypesSeen.Add(hook.GetType().Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_out.WriteLine($" [{source}] anim 0x{animId:X8}: frames={frameCount} hooks={hookCount}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_out.WriteLine($"=== DISTINCT HOOK TYPES ACROSS ENTIRE TABLE: {(hookTypesSeen.Count == 0 ? "(none)" : string.Join(", ", hookTypesSeen))} ===");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object? GetMember(object obj, string name)
|
||||||
|
{
|
||||||
|
var t = obj.GetType();
|
||||||
|
var prop = t.GetProperty(name, BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
if (prop is not null) return prop.GetValue(obj);
|
||||||
|
var field = t.GetField(name, BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
return field?.GetValue(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint GetAnimId(object animData)
|
||||||
|
{
|
||||||
|
var val = GetMember(animData, "AnimId") ?? GetMember(animData, "Id");
|
||||||
|
if (val is null) return 0;
|
||||||
|
var dataId = GetMember(val, "DataId") ?? GetMember(val, "Id");
|
||||||
|
return dataId switch { uint u => u, int i => (uint)i, ulong ul => (uint)ul, _ => 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<object> GetFrames(Animation anim)
|
||||||
|
{
|
||||||
|
var frames = (GetMember(anim, "PartFrames") ?? GetMember(anim, "Frames")) as IEnumerable;
|
||||||
|
if (frames is null) yield break;
|
||||||
|
foreach (var f in frames)
|
||||||
|
if (f is not null) yield return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<object> GetHooks(object frame)
|
||||||
|
{
|
||||||
|
var hooks = GetMember(frame, "Hooks") as IEnumerable;
|
||||||
|
if (hooks is null) yield break;
|
||||||
|
foreach (var h in hooks)
|
||||||
|
if (h is not null) yield return h;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue