fix(animation): preserve wire motion on every spawn path
Replace the door-specific static-animation seed with one retail description-then-enter-world initializer shared by normal and reactive spawns. This preserves authoritative Dead and On/Off states, prevents replacement corpses from returning to Ready, and pins the lifecycle with Core and App tests. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
9b97102c67
commit
8be933fc94
9 changed files with 335 additions and 64 deletions
|
|
@ -3648,35 +3648,8 @@ public sealed class GameWindow : IDisposable
|
|||
var mtable = _dats.Get<DatReaderWriter.DBObjs.MotionTable>(mtableId);
|
||||
if (mtable is not null)
|
||||
{
|
||||
sequencer = new AcDream.Core.Physics.AnimationSequencer(setup, mtable, _animLoader);
|
||||
uint seqStyle = stanceOverride is > 0
|
||||
? (0x80000000u | (uint)stanceOverride.Value)
|
||||
: (uint)mtable.DefaultStyle;
|
||||
uint seqMotion;
|
||||
if (commandOverride is > 0)
|
||||
{
|
||||
uint resolved = AcDream.Core.Physics.MotionCommandResolver
|
||||
.ReconstructFullCommand(commandOverride.Value);
|
||||
seqMotion = resolved != 0
|
||||
? resolved
|
||||
: (0x40000000u | (uint)commandOverride.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
seqMotion = AcDream.Core.Physics.MotionCommand.Ready;
|
||||
}
|
||||
|
||||
// R2-Q5: retail's enter-world sequence — initialize_
|
||||
// state installs the table default (so the entity is
|
||||
// NEVER without a cycle: the L.1c "torso on the
|
||||
// ground" hazard is structurally gone), then the
|
||||
// wire's initial motion dispatches through the
|
||||
// verbatim GetObjectSequence (a missing cycle leaves
|
||||
// the default playing — retail's own miss behavior;
|
||||
// the Run→Walk→Ready fallback chain is deleted with
|
||||
// RemoteMotionSink).
|
||||
sequencer.InitializeState();
|
||||
sequencer.SetCycle(seqStyle, seqMotion);
|
||||
sequencer = SpawnMotionInitializer.Create(
|
||||
setup, mtable, _animLoader, spawn.MotionState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3730,44 +3703,20 @@ public sealed class GameWindow : IDisposable
|
|||
// 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
|
||||
// (CurrentMotionState = motionClosed): Off when closed at spawn,
|
||||
// On when the spawn PhysicsState carries the ETHEREAL bit
|
||||
// (already open in ACE's DB).
|
||||
// #204: this path is not door-specific. Corpses also arrive here
|
||||
// because Dead is a persistent static rest pose. The old rescue
|
||||
// hard-coded Door On/Off from PhysicsState, discarded the wire's
|
||||
// authoritative Dead command, and left the corpse on Ready. Both
|
||||
// spawn paths must seed the same wire MovementData through retail's
|
||||
// description-then-enter-world lifecycle.
|
||||
uint mtableId = spawn.MotionTableId ?? (uint)setup.DefaultMotionTable;
|
||||
if (mtableId != 0)
|
||||
{
|
||||
var mtable = _dats.Get<DatReaderWriter.DBObjs.MotionTable>(mtableId);
|
||||
if (mtable is not null)
|
||||
{
|
||||
var sequencer = new AcDream.Core.Physics.AnimationSequencer(setup, mtable, _animLoader);
|
||||
|
||||
// Style key is `0x80000000 | stance`. ACE's MotionStance.NonCombat
|
||||
// is 0x3D (61 decimal), NOT 0x01. Verified live: ACE broadcasts
|
||||
// UpdateMotion with stance=0x003D and the sequencer keys cycles
|
||||
// by style=0x8000003D. An earlier B.4c seed used the wrong
|
||||
// 0x80000001 value, which made HasCycle always return false ->
|
||||
// SetCycle never fired -> sequencer empty -> Advance returned
|
||||
// no frames -> per-frame tick collapsed all door parts to the
|
||||
// entity origin (visible as "door halfway in the ground").
|
||||
const uint NonCombatStyle = 0x8000003Du;
|
||||
const uint MotionOn = 0x4000000Bu; // ACE MotionCommand.On (door open)
|
||||
const uint MotionOff = 0x4000000Cu; // ACE MotionCommand.Off (door closed)
|
||||
const uint EtherealPs = 0x4u;
|
||||
// Prefer the spawn's wire-level stance if provided; else default
|
||||
// to NonCombat. (Doors normally don't carry an initial MotionState
|
||||
// on spawn — falling back to NonCombat matches ACE Door.cs:43.)
|
||||
ushort spawnStance = spawn.MotionState?.Stance ?? 0;
|
||||
uint initialStyle = spawnStance != 0
|
||||
? (0x80000000u | (uint)spawnStance)
|
||||
: NonCombatStyle;
|
||||
uint spawnState = spawn.PhysicsState ?? 0u;
|
||||
uint initialCycle = (spawnState & EtherealPs) != 0 ? MotionOn : MotionOff;
|
||||
// R2-Q5: initialize_state guarantees a playing default;
|
||||
// the On/Off dispatch no-ops harmlessly if the door's
|
||||
// table lacks the cycle (HasCycle guard deleted).
|
||||
sequencer.InitializeState();
|
||||
sequencer.SetCycle(initialStyle, initialCycle);
|
||||
var sequencer = SpawnMotionInitializer.Create(
|
||||
setup, mtable, _animLoader, spawn.MotionState);
|
||||
|
||||
var template = new (uint, IReadOnlyDictionary<uint, uint>?)[meshRefs.Count];
|
||||
for (int i = 0; i < meshRefs.Count; i++)
|
||||
|
|
@ -3788,8 +3737,11 @@ public sealed class GameWindow : IDisposable
|
|||
};
|
||||
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeBuildingEnabled)
|
||||
{
|
||||
var initial = SpawnMotionInitializer.ResolvePlan(mtable, spawn.MotionState);
|
||||
Console.WriteLine(System.FormattableString.Invariant(
|
||||
$"[door-anim] registered guid=0x{spawn.Guid:X8} entityId=0x{entity.Id:X8} mtable=0x{mtableId:X8} initialStyle=0x{initialStyle:X8} initialCycle=0x{initialCycle:X8}"));
|
||||
$"[reactive-anim] registered guid=0x{spawn.Guid:X8} entityId=0x{entity.Id:X8} mtable=0x{mtableId:X8} initialStyle=0x{initial.Style:X8} initialCycle=0x{initial.Motion:X8}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue