using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using DatReaderWriter.DBObjs; namespace AcDream.App.Rendering; /// /// Builds the initial motion-table state for a server CreateObject using the /// same description-then-enter-world lifecycle as retail. /// /// /// Retail oracle: ACCObjectMaint::CreateObject (0x00558870) calls /// CPhysicsObj::set_description (0x00514F40), which applies the wire /// MovementData. SmartBox::HandleCreateObject (0x00454C80) then calls /// CPhysicsObj::enter_world (0x00516170), reaching /// CPartArray::HandleEnterWorld (0x00517D70) and /// MotionTableManager::HandleEnterWorld (0x0051BDD0). /// 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; } /// /// 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. /// 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); } }