fix(combat): apply server attack motions locally

Route accepted non-autonomous local UpdateMotion state through retail's wholesale interpreted-motion funnel, so ACE-selected melee and missile actions use the normal motion queue and action-stamp gate. Share nullable wire conversion with remotes and remove the local direct command replay.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 13:34:55 +02:00
parent 8ec0944a1c
commit 9958458318
6 changed files with 275 additions and 135 deletions

View file

@ -0,0 +1,89 @@
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
namespace AcDream.App.Physics;
/// <summary>
/// Converts the nullable wire fields from an mt-0 UpdateMotion payload into
/// retail's wholesale <see cref="InboundInterpretedState"/> value.
/// </summary>
/// <remarks>
/// <para>
/// Retail constructs an <c>InterpretedMotionState</c>, whose constructor
/// supplies NonCombat/Ready/1.0 defaults, then calls <c>UnPack</c> over it
/// before <c>MovementManager::move_to_interpreted_state</c> copies every
/// field. See <c>MovementManager::unpack_movement</c> at 0x00524440 and
/// <c>CMotionInterp::move_to_interpreted_state</c> at 0x005289c0.
/// </para>
/// <para>
/// Keeping this conversion outside <c>GameWindow</c> gives local and remote
/// actors one definition of absent-field semantics and one command-list
/// reconstruction path.
/// </para>
/// </remarks>
internal static class InboundInterpretedMotionFactory
{
private const uint NonCombatStyle = 0x8000003Du;
private const uint Ready = 0x41000003u;
public static InboundInterpretedState Create(
in CreateObject.ServerMotionState wire,
uint fallbackForwardClass = 0x40000000u)
{
var result = new InboundInterpretedState
{
CurrentStyle = wire.Stance != 0
? 0x80000000u | wire.Stance
: NonCombatStyle,
ForwardCommand = ResolveForward(wire.ForwardCommand, fallbackForwardClass),
ForwardSpeed = wire.ForwardSpeed ?? 1f,
SideStepCommand = ResolveAxis(wire.SideStepCommand, 0x65000000u),
SideStepSpeed = wire.SideStepSpeed ?? 1f,
TurnCommand = ResolveAxis(wire.TurnCommand, 0x65000000u),
TurnSpeed = wire.TurnSpeed ?? 1f,
};
if (wire.Commands is { Count: > 0 } commands)
{
var actions = new List<InboundMotionAction>(commands.Count);
foreach (var item in commands)
{
uint command = MotionCommandResolver.ReconstructFullCommand(item.Command);
if (command == 0)
command = 0x10000000u | item.Command;
actions.Add(new InboundMotionAction(
command,
Stamp: item.PackedSequence & 0x7FFF,
Autonomous: (item.PackedSequence & 0x8000) != 0,
Speed: item.Speed));
}
result.Actions = actions;
}
return result;
}
private static uint ResolveForward(ushort? wireCommand, uint fallbackClass)
{
if (wireCommand is not { } command || command == 0)
return Ready;
uint resolved = MotionCommandResolver.ReconstructFullCommand(command);
if (resolved != 0)
return resolved;
uint commandClass = fallbackClass & 0xFF000000u;
return (commandClass != 0 ? commandClass : 0x40000000u) | command;
}
private static uint ResolveAxis(ushort? wireCommand, uint fallbackClass)
{
if (wireCommand is not { } command || command == 0)
return 0;
uint resolved = MotionCommandResolver.ReconstructFullCommand(command);
return resolved != 0 ? resolved : fallbackClass | command;
}
}