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
|
|
@ -0,0 +1,160 @@
|
|||
# Retail default combat mode and held-item parenting
|
||||
|
||||
## Sources
|
||||
|
||||
- `ClientCombatSystem::GetDefaultCombatMode @ 0x0056B310`
|
||||
- `ACCWeenieObject::GetObjectAtLocation @ 0x0058CE00`
|
||||
- `CM_Physics::DispatchSB_ParentEvent @ 0x006ACAF0`
|
||||
- `SmartBox::HandleParentEvent @ 0x004535D0`
|
||||
- `SmartBox::DoParentEvent @ 0x00452290`
|
||||
- `CPhysicsObj::set_parent @ 0x00515A90`
|
||||
- `CPhysicsObj::add_child @ 0x0050F870`
|
||||
- `CPhysicsObj::UpdateChild @ 0x00512D50`
|
||||
- `CPartArray::SetPlacementFrame @ 0x005193D0`
|
||||
- `Frame::combine @ 0x005122E0`
|
||||
|
||||
Cross-references:
|
||||
|
||||
- ACE `Creature_Equipment.GetPlacementLocation` and `TrySetChild`
|
||||
- ACE `GameMessageParentEvent`
|
||||
- ACE `WorldObject.SerializePhysicsData`
|
||||
- DatReaderWriter `Setup.HoldingLocations`, `LocationType`, and
|
||||
`Setup.PlacementFrames`
|
||||
|
||||
## Default combat mode
|
||||
|
||||
```text
|
||||
GetDefaultCombatMode(showError):
|
||||
player = get local player weenie
|
||||
if player is missing:
|
||||
return NonCombat
|
||||
|
||||
weaponId = player.GetObjectAtLocation(
|
||||
MeleeWeapon | MissileWeapon | TwoHanded,
|
||||
priority = 0)
|
||||
|
||||
if weaponId != 0:
|
||||
weapon = get weenie weaponId
|
||||
if weapon is missing:
|
||||
return NonCombat
|
||||
if weapon.PublicWeenieDesc.CombatUse == Missile:
|
||||
return Missile
|
||||
return Melee
|
||||
|
||||
heldId = player.GetObjectAtLocation(Held, priority = 0)
|
||||
if heldId == 0:
|
||||
return Melee
|
||||
|
||||
held = get weenie heldId
|
||||
if held is missing:
|
||||
return NonCombat
|
||||
if held.InqType has the Caster bit (0x00008000):
|
||||
return Magic
|
||||
|
||||
if showError == false:
|
||||
print the retail cannot-use-held-item notice
|
||||
return NonCombat
|
||||
```
|
||||
|
||||
`GetObjectAtLocation` walks the player's ordered inventory-placement list and
|
||||
returns the first entry whose location intersects the requested mask (and whose
|
||||
priority intersects the requested priority when that priority is non-zero).
|
||||
|
||||
## ParentEvent wire and freshness
|
||||
|
||||
```text
|
||||
DispatchParentEvent(blob):
|
||||
require opcode 0xF749
|
||||
parentGuid = read u32
|
||||
childGuid = read u32
|
||||
parentLocation = read u32
|
||||
placementId = read u32
|
||||
parentInstanceSequence = read u16
|
||||
childPositionSequence = read u16
|
||||
return HandleParentEvent(...)
|
||||
|
||||
HandleParentEvent(...):
|
||||
parent = GetObjectA(parentGuid)
|
||||
if parent is missing OR event parent-instance is newer than parent:
|
||||
queue blob for parentGuid
|
||||
return Queued
|
||||
if event parent-instance is older than parent:
|
||||
return Stale
|
||||
|
||||
child = GetObjectA(childGuid)
|
||||
if child is missing:
|
||||
queue blob for childGuid
|
||||
return Queued
|
||||
|
||||
DoParentEvent(child, parent, parentLocation, placementId,
|
||||
childPositionSequence)
|
||||
```
|
||||
|
||||
## Applying the attachment
|
||||
|
||||
```text
|
||||
DoParentEvent(child, parent, location, placement, positionSequence):
|
||||
if positionSequence is not strictly newer than child.positionTimestamp:
|
||||
return
|
||||
|
||||
child.positionTimestamp = positionSequence
|
||||
parentWasMissing = child.parent is null
|
||||
if parentWasMissing and parent is not the local player:
|
||||
mark the parent's weenie as having parented state
|
||||
|
||||
child.set_parent(parent, location)
|
||||
child.SetPlacementFrame(placement)
|
||||
|
||||
set_parent(child, parent, location):
|
||||
holding = parent.setup.HoldingLocations[location]
|
||||
if holding is missing:
|
||||
fail
|
||||
|
||||
store child with holding.PartId and holding.Frame in parent's child list
|
||||
detach child from its former parent and world cell
|
||||
child.parent = parent
|
||||
child.cell = parent.cell
|
||||
UpdateChild(parent, child, holding.PartId, holding.Frame)
|
||||
|
||||
UpdateChild(parent, child, parentPartId, holdingFrame):
|
||||
if parentPartId is a real part:
|
||||
parentFrame = parent.parts[parentPartId].worldFrame
|
||||
else:
|
||||
parentFrame = parent.objectWorldFrame
|
||||
child.objectWorldFrame = Combine(parentFrame, holdingFrame)
|
||||
|
||||
SetPlacementFrame(child, placement):
|
||||
frame = child.setup.PlacementFrames[placement]
|
||||
if frame is missing:
|
||||
frame = child.setup.PlacementFrames[Default]
|
||||
install frame (or null when Default is also absent)
|
||||
update every child part from child.objectWorldFrame
|
||||
```
|
||||
|
||||
With `System.Numerics` row-vector matrices, the render equivalent is:
|
||||
|
||||
```text
|
||||
childRootRelativeToParent = holdingFrame * parentPartRelativeToParent
|
||||
childPartRelativeToParent = childPlacementPart * childRootRelativeToParent
|
||||
childWorldRoot = parent's WorldEntity position + rotation
|
||||
```
|
||||
|
||||
The child root and each child part must be recomputed after the parent's
|
||||
animation advances, because the referenced hand part moves every frame.
|
||||
|
||||
## CreateObject bootstrap
|
||||
|
||||
An equipped child's `CreateObject` PhysicsData carries the same relationship
|
||||
without requiring a later ParentEvent:
|
||||
|
||||
```text
|
||||
AnimationFrame flag -> child Placement id
|
||||
Parent flag -> parent/wielder guid + ParentLocation
|
||||
timestamp[0] -> child position sequence
|
||||
timestamp[8] -> child instance sequence
|
||||
```
|
||||
|
||||
ACE uses this form for already-equipped children sent while an observer begins
|
||||
tracking a creature and at login. ParentEvent remains necessary for live
|
||||
equip/re-parent changes and may arrive before the child's CreateObject, so the
|
||||
client must retain it until both objects exist.
|
||||
Loading…
Add table
Add a link
Reference in a new issue