feat(combat): port retail held weapon parenting
Select the default combat mode from ordered equipped objects so bows request missile stance. Parse CreateObject parent metadata and ParentEvent, then render held objects as separate children composed from setup holding locations and placement frames each animation tick.
This commit is contained in:
parent
564d39dfea
commit
ab6d96d113
18 changed files with 1152 additions and 17 deletions
|
|
@ -122,6 +122,12 @@ public static class CreateObject
|
|||
// L.2g S1 (DEV-6): ObjectMovement stamp (timestamp block index 1)
|
||||
// seeds MotionSequenceGate's MOVEMENT_TS at spawn.
|
||||
ushort MovementSequence = 0,
|
||||
// Parent/placement bootstrap for equipped child objects. These are
|
||||
// the CreateObject equivalents of ParentEvent 0xF749.
|
||||
ushort PositionSequence = 0,
|
||||
uint? ParentGuid = null,
|
||||
uint? ParentLocation = null,
|
||||
uint? PlacementId = null,
|
||||
uint? PhysicsState = null,
|
||||
uint? ObjectDescriptionFlags = null,
|
||||
// L.3b (2026-04-30): per-object friction + elasticity from the
|
||||
|
|
@ -522,6 +528,10 @@ public static class CreateObject
|
|||
physicsState = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
|
||||
uint? placementId = null;
|
||||
uint? parentGuid = null;
|
||||
uint? parentLocation = null;
|
||||
|
||||
if ((physicsFlags & PhysicsDescriptionFlag.Movement) != 0)
|
||||
{
|
||||
// u32 length, length bytes of serialized MovementData (no header
|
||||
|
|
@ -543,6 +553,7 @@ public static class CreateObject
|
|||
else if ((physicsFlags & PhysicsDescriptionFlag.AnimationFrame) != 0)
|
||||
{
|
||||
if (body.Length - pos < 4) return null;
|
||||
placementId = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
|
|
@ -593,7 +604,9 @@ public static class CreateObject
|
|||
if ((physicsFlags & PhysicsDescriptionFlag.Parent) != 0)
|
||||
{
|
||||
if (body.Length - pos < 8) return null;
|
||||
pos += 8; // wielderId u32 + parentLocation u32
|
||||
parentGuid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
parentLocation = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos + 4));
|
||||
pos += 8;
|
||||
}
|
||||
if ((physicsFlags & PhysicsDescriptionFlag.Children) != 0)
|
||||
{
|
||||
|
|
@ -643,6 +656,7 @@ public static class CreateObject
|
|||
if (body.Length - pos < 9 * 2) return PartialResult();
|
||||
var seqSpan = body.Slice(pos, 9 * 2);
|
||||
ushort instanceSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(8 * 2));
|
||||
ushort positionSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(0 * 2));
|
||||
ushort movementSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(1 * 2));
|
||||
ushort teleportSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(4 * 2));
|
||||
ushort serverControlSeq = BinaryPrimitives.ReadUInt16LittleEndian(seqSpan.Slice(5 * 2));
|
||||
|
|
@ -984,8 +998,14 @@ public static class CreateObject
|
|||
textureChanges, subPalettes, basePaletteId, objScale, name, itemType, motionState, motionTableId,
|
||||
instanceSeq, teleportSeq, serverControlSeq, forcePositionSeq,
|
||||
movementSeq,
|
||||
physicsState, objectDescriptionFlags,
|
||||
friction, elasticity,
|
||||
PositionSequence: positionSeq,
|
||||
ParentGuid: parentGuid,
|
||||
ParentLocation: parentLocation,
|
||||
PlacementId: placementId,
|
||||
PhysicsState: physicsState,
|
||||
ObjectDescriptionFlags: objectDescriptionFlags,
|
||||
Friction: friction,
|
||||
Elasticity: elasticity,
|
||||
IconId: iconId,
|
||||
Useability: useability, UseRadius: useRadius, TargetType: targetType,
|
||||
IconOverlayId: iconOverlayId, IconUnderlayId: iconUnderlayId,
|
||||
|
|
|
|||
42
src/AcDream.Core.Net/Messages/ParentEvent.cs
Normal file
42
src/AcDream.Core.Net/Messages/ParentEvent.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
using System.Buffers.Binary;
|
||||
|
||||
namespace AcDream.Core.Net.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Inbound retail <c>ParentEvent</c> (<c>0xF749</c>): attach a child physics
|
||||
/// object (weapon, shield, ammunition) to a named holding location on a
|
||||
/// parent creature and apply the child's placement frame.
|
||||
///
|
||||
/// Wire oracle: <c>CM_Physics::DispatchSB_ParentEvent @ 0x006ACAF0</c>.
|
||||
/// Runtime oracle: <c>SmartBox::HandleParentEvent @ 0x004535D0</c> and
|
||||
/// <c>SmartBox::DoParentEvent @ 0x00452290</c>.
|
||||
/// </summary>
|
||||
public static class ParentEvent
|
||||
{
|
||||
public const uint Opcode = 0xF749u;
|
||||
|
||||
public readonly record struct Parsed(
|
||||
uint ParentGuid,
|
||||
uint ChildGuid,
|
||||
uint ParentLocation,
|
||||
uint PlacementId,
|
||||
ushort ParentInstanceSequence,
|
||||
ushort ChildPositionSequence);
|
||||
|
||||
public static Parsed? TryParse(ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body.Length < 24)
|
||||
return null;
|
||||
|
||||
if (BinaryPrimitives.ReadUInt32LittleEndian(body) != Opcode)
|
||||
return null;
|
||||
|
||||
return new Parsed(
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(4, 4)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(8, 4)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(12, 4)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(16, 4)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(20, 2)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(22, 2)));
|
||||
}
|
||||
}
|
||||
|
|
@ -118,6 +118,10 @@ public sealed class WorldSession : IDisposable
|
|||
ushort InstanceSequence = 0,
|
||||
ushort MovementSequence = 0,
|
||||
ushort ServerControlSequence = 0,
|
||||
ushort PositionSequence = 0,
|
||||
uint? ParentGuid = null,
|
||||
uint? ParentLocation = null,
|
||||
uint? PlacementId = null,
|
||||
// PublicWeenieDesc optional-tail bytes. null means the corresponding
|
||||
// flag was absent; zero means the server explicitly sent the enum's
|
||||
// undefined/default value.
|
||||
|
|
@ -175,6 +179,10 @@ public sealed class WorldSession : IDisposable
|
|||
InstanceSequence: parsed.InstanceSequence,
|
||||
MovementSequence: parsed.MovementSequence,
|
||||
ServerControlSequence: parsed.ServerControlSequence,
|
||||
PositionSequence: parsed.PositionSequence,
|
||||
ParentGuid: parsed.ParentGuid,
|
||||
ParentLocation: parsed.ParentLocation,
|
||||
PlacementId: parsed.PlacementId,
|
||||
RadarBlipColor: parsed.RadarBlipColor,
|
||||
RadarBehavior: parsed.RadarBehavior,
|
||||
CombatUse: parsed.CombatUse,
|
||||
|
|
@ -243,6 +251,12 @@ public sealed class WorldSession : IDisposable
|
|||
/// </summary>
|
||||
public event Action<VectorUpdate.Parsed>? VectorUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Fires for retail <c>ParentEvent (0xF749)</c>, which attaches a separate
|
||||
/// child object to a creature holding location (weapons, shields, ammo).
|
||||
/// </summary>
|
||||
public event Action<ParentEvent.Parsed>? ParentUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Fires when the server broadcasts a <c>SetState (0xF74B)</c> game
|
||||
/// message — a previously-spawned entity's <c>PhysicsState</c>
|
||||
|
|
@ -886,6 +900,12 @@ public sealed class WorldSession : IDisposable
|
|||
new DeleteObject.Parsed(
|
||||
parsed.Value.Guid, parsed.Value.InstanceSequence, FromPickup: true));
|
||||
}
|
||||
else if (op == ParentEvent.Opcode)
|
||||
{
|
||||
var parsed = ParentEvent.TryParse(body);
|
||||
if (parsed is not null)
|
||||
ParentUpdated?.Invoke(parsed.Value);
|
||||
}
|
||||
else if (op == UpdateMotion.Opcode)
|
||||
{
|
||||
// Phase 6.6: the server sends UpdateMotion (0xF74C) whenever an
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue