acdream/src/AcDream.App/Rendering/SpawnMotionInitializer.cs

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);
}
}