Project PlayerDescription equipment through the same contained-by-wielder ownership and ordered contents index as live WieldObject updates. Preserve equip masks and priorities so retail GetObjectAtLocation selects Missile for an already-equipped crossbow instead of sending a server-rejected Melee request. Co-Authored-By: Codex <codex@openai.com>
183 lines
6.1 KiB
Markdown
183 lines
6.1 KiB
Markdown
# 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).
|
|
|
|
### Login projection invariant
|
|
|
|
PlayerDescription carries loose inventory and equipped objects as two ordered
|
|
lists. They must feed one player-owned location lookup after parsing:
|
|
|
|
```text
|
|
OnPlayerDescription(player, inventory, equipped):
|
|
replace player-owned loose contents with inventory, preserving wire order
|
|
for each equipped entry in wire order:
|
|
record item as owned by player
|
|
record equipped location and layering priority
|
|
append it to the same ordered player-owned lookup
|
|
```
|
|
|
|
This is the login equivalent of a live `WieldObject`, which also models the
|
|
item as contained by its wielder with a non-zero equipped location. Keeping
|
|
login equipment outside that lookup makes `GetDefaultCombatMode` miss an
|
|
already-equipped bow/crossbow and request Melee; ACE then correctly rejects the
|
|
mode because a missile weapon is equipped. ACE's
|
|
`Player.HandleActionChangeCombatMode_Inner` performs that rejection, while
|
|
holtburger independently retains PlayerDescription's `equipped_objects` list
|
|
for its suggested-combat-mode decision.
|
|
|
|
## 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.
|