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
|
|
@ -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