83 lines
3.2 KiB
C#
83 lines
3.2 KiB
C#
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using DatReaderWriter.DBObjs;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>
|
|
/// Builds the initial motion-table state for a server CreateObject using the
|
|
/// same description-then-enter-world lifecycle as retail.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Retail oracle: <c>ACCObjectMaint::CreateObject</c> (0x00558870) calls
|
|
/// <c>CPhysicsObj::set_description</c> (0x00514F40), which applies the wire
|
|
/// MovementData. <c>SmartBox::HandleCreateObject</c> (0x00454C80) then calls
|
|
/// <c>CPhysicsObj::enter_world</c> (0x00516170), reaching
|
|
/// <c>CPartArray::HandleEnterWorld</c> (0x00517D70) and
|
|
/// <c>MotionTableManager::HandleEnterWorld</c> (0x0051BDD0).
|
|
/// </remarks>
|
|
internal static class SpawnMotionInitializer
|
|
{
|
|
internal readonly record struct Plan(uint Style, uint Motion);
|
|
|
|
public static AnimationSequencer Create(
|
|
Setup setup,
|
|
MotionTable motionTable,
|
|
IAnimationLoader loader,
|
|
CreateObject.ServerMotionState? wireState)
|
|
{
|
|
var sequencer = new AnimationSequencer(setup, motionTable, loader);
|
|
Plan plan = ResolvePlan(motionTable, wireState);
|
|
|
|
// set_description: install the table default, then apply MovementData.
|
|
sequencer.InitializeState();
|
|
sequencer.SetCycle(plan.Style, plan.Motion);
|
|
|
|
// enter_world: discard description-time transition links so the first
|
|
// rendered pose is the server-authored persistent cycle. A corpse's
|
|
// NonCombat+Dead therefore starts fallen; a door's NonCombat+On/Off
|
|
// comes from the same wire path with no object-type special case.
|
|
sequencer.Manager.HandleEnterWorld();
|
|
return sequencer;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Replays description-time MovementData into the same retained PartArray
|
|
/// sequencer. This is used only when a fresher same-incarnation CreateObject
|
|
/// interrupted initial hydration before the object crossed its ready edge;
|
|
/// completed objects consume the packet through SetObjectMovement instead.
|
|
/// </summary>
|
|
public static void Reinitialize(
|
|
AnimationSequencer sequencer,
|
|
MotionTable motionTable,
|
|
CreateObject.ServerMotionState? wireState)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(sequencer);
|
|
ArgumentNullException.ThrowIfNull(motionTable);
|
|
Plan plan = ResolvePlan(motionTable, wireState);
|
|
sequencer.Reset();
|
|
sequencer.InitializeState();
|
|
sequencer.SetCycle(plan.Style, plan.Motion);
|
|
sequencer.Manager.HandleEnterWorld();
|
|
}
|
|
|
|
internal static Plan ResolvePlan(
|
|
MotionTable motionTable,
|
|
CreateObject.ServerMotionState? wireState)
|
|
{
|
|
uint style = wireState is { Stance: > 0 } state
|
|
? 0x80000000u | state.Stance
|
|
: (uint)motionTable.DefaultStyle;
|
|
|
|
uint motion = MotionCommand.Ready;
|
|
if (wireState?.ForwardCommand is ushort command && command > 0)
|
|
{
|
|
uint resolved = MotionCommandResolver.ReconstructFullCommand(command);
|
|
motion = resolved != 0
|
|
? resolved
|
|
: 0x40000000u | command;
|
|
}
|
|
|
|
return new Plan(style, motion);
|
|
}
|
|
}
|