# 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.