acdream/docs/research/2026-07-11-combat-default-and-parent-event-pseudocode.md
Erik ab98cda26b fix #211: index login-equipped items under player
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>
2026-07-13 10:40:29 +02:00

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.