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:
parent
8ec0944a1c
commit
9958458318
6 changed files with 275 additions and 135 deletions
89
src/AcDream.App/Physics/InboundInterpretedMotionFactory.cs
Normal file
89
src/AcDream.App/Physics/InboundInterpretedMotionFactory.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue