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>
6.1 KiB
Retail default combat mode and held-item parenting
Sources
ClientCombatSystem::GetDefaultCombatMode @ 0x0056B310ACCWeenieObject::GetObjectAtLocation @ 0x0058CE00CM_Physics::DispatchSB_ParentEvent @ 0x006ACAF0SmartBox::HandleParentEvent @ 0x004535D0SmartBox::DoParentEvent @ 0x00452290CPhysicsObj::set_parent @ 0x00515A90CPhysicsObj::add_child @ 0x0050F870CPhysicsObj::UpdateChild @ 0x00512D50CPartArray::SetPlacementFrame @ 0x005193D0Frame::combine @ 0x005122E0
Cross-references:
- ACE
Creature_Equipment.GetPlacementLocationandTrySetChild - ACE
GameMessageParentEvent - ACE
WorldObject.SerializePhysicsData - DatReaderWriter
Setup.HoldingLocations,LocationType, andSetup.PlacementFrames
Default combat mode
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:
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
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
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:
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:
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.