# Wield ownership and targeted-action facing pseudocode Date: 2026-07-14 ## Sources - Named retail `ItemHolder::DetermineUseResult @ 0x00588460` - Named retail `ACCWeenieObject::UIAttemptWield @ 0x0058D590` - Named retail `ACCWeenieObject::ServerSaysMoveItem @ 0x0058DBB0` - Named retail `ACCWeenieObject::SetPlayerWieldLocation @ 0x0058D8C0` - Named retail `ACCObjectMaint::ViewObjectContents @ 0x00558A70` - Named retail `gmPaperDollUI::ServerSaysMoveItem @ 0x004A64A0` - Named retail `gmToolbarUI::RecvNotice_ServerSaysMoveItem @ 0x004BF2B0` - Named retail WieldObject dispatch `0x0055B27C..0x0055B2B5` - Named retail `ClientCombatSystem::ExecuteAttack @ 0x0056BB70` - Named retail `ClientCombatSystem::StartAttackRequest @ 0x0056C040` - Named retail `CameraSet::ToggleMouseLook @ 0x00457490` - Named retail `CameraSet::MouseLookHandler @ 0x00458D80` - Named retail `CameraSet::Rotate @ 0x00458310` - Named retail `CommandInterpreter::HandleMouseMovementCommand @ 0x006B3870` - Named retail `CommandInterpreter::StopListHeadMovement @ 0x006B35A0` - Named retail `CommandInterpreter::MaybeStopCompletely @ 0x006B3B90` - Named retail `MovementManager::unpack_movement @ 0x00524440` - Named retail `MoveToManager::TurnToObject @ 0x005297D0` - Named retail `CommandInterpreter::SendMovementEvent @ 0x006B4680` - Named retail `CommandInterpreter::SendPositionEvent @ 0x006B4770` - Named retail `CommandInterpreter::ShouldSendPositionEvent @ 0x006B45E0` - Named retail `CommandInterpreter::UseTime @ 0x006B3BF0` - Named retail `Frame::is_equal @ 0x00424C30` - Named retail `Frame::is_quaternion_equal @ 0x00424C70` - Named retail `Position::Pack @ 0x005A9640` - ACE `Player_Missile.HandleActionTargetedMissileAttack` and `Creature.Rotate/TurnToObject` (interpretation aid; retail remains the oracle) ## Wield request and authoritative ownership `UIAttemptWield` does not rewrite the item's public weenie fields. It sends the request and records the one outstanding inventory request: ```text UIAttemptWield(item, location): if player is not ready for an inventory request: return if item is a split stack and a partial split is selected: send StackableSplitToWield(item.id, location, splitSize) return send GetAndWieldItem(item.id, location) prevRequestObjectID = item.id prevRequest = Wield prevRequestTime = now ``` `DetermineUseResult` uses the authoritative `wielderID`, not container membership, to decide whether a player-owned weapon needs to be wielded: ```text if item is owned by the player and (item.combatUse != None or item is a caster or item has WieldOnUse) and item.wielderID != player.id: return WieldLeft when WieldLeft is set, otherwise WieldRight ``` The server move callback updates container, wielder, and location as one authoritative state transition: ```text ServerSaysMoveItem(item, newContainer, placement, newWielder, newLocation): oldContainer = item.containerID oldWielder = item.wielderID oldLocation = item.location remove item from oldContainer.contents, when present item.containerID = newContainer add item to newContainer.contents at placement, when present if newWielder == player.id: SetPlayerWieldLocation(item, newLocation) item.wielderID = player.id else if oldWielder == player.id: SetPlayerWieldLocation(item, None) item.wielderID = 0 item.location = newLocation clear the matching outstanding inventory request notify inventory UI with old/new container, slot, wielder, and location ``` Paperdoll and toolbar consumers test the complete old and new placement. A post-mutation item plus old/new container alone is insufficient: confirmed equipment has `ContainerId=0`, so an unwield into a side bag would otherwise lose the old player wielder/location and fail to clear the visible slot. Retail ViewContents is a different operation: ```text ViewObjectContents(container, contentProfile): clear container.itemsList and container.containersList rebuild those ordered lists from ContentProfile do not mutate any child containerID, wielderID, or location ``` PlayerDescription therefore initializes login inventory ownership explicitly; later ViewContents packets replace only the viewed list projection. A later authoritative move or delete evicts the GUID from every stale viewed-list projection before applying its new canonical membership. Only move, wield, and put-in-3D events publish placement transitions. PlayerDescription assigns its complete packed InventoryPlacement list directly, preserving wire order. Later live `SetPlayerWieldLocation` calls insert the new placement at the head, and `GetObjectAtLocation` scans head-to-tail. Combat mode and ammo selection must preserve this mixed lifecycle rather than sorting confirmed equipment by GUID. The retail 0x22 and 0x23 dispatchers publish `ServerSaysMoveItem_s` even when the item GUID has not resolved to a weenie yet. The notice carries the GUID and zero old placement plus the raw new placement; it must not be suppressed or manufacture a retained stub. This lets UI transaction owners observe the exact server boundary despite CreateObject/message reordering. The WieldObject event itself contains exactly two words. Its dispatch supplies the local player identity and an empty container to `ServerSaysMoveItem`: ```text WieldObject(itemGuid, equipLocation): item = FindObject(itemGuid) ServerSaysMoveItem( item, newContainer = 0, placement = 0, newWielder = GetPlayerID(), newLocation = equipLocation, isServerMove = true) ``` Conformance consequence: a bow moved from the paper doll into a backpack must have `WielderId == 0` after the authoritative PutObjectInContainer event. If it retains the player's ID, the next double click is classified as already wielded and no GetAndWieldItem request is sent. ## Targeted missile facing ownership Retail's client combat UI does not invent a local pre-attack rotation: ```text ClientCombatSystem::ExecuteAttack(target, height, power): validate target and player readiness send Event_TargetedMissileAttack(target.id, height, power) ``` The authoritative movement response is consumed through the ordinary movement funnel: ```text MovementManager::unpack_movement(server movement): interrupt current movement apply the wire stance when it changed case TurnToObject: unpack object id, heading, and turn parameters physicsObject.TurnToObject(object id, parameters) ``` ACE independently confirms the expected server sequence for the local test environment: ```text HandleActionTargetedMissileAttack(target): rotateTime = Rotate(target) wait rotateTime LaunchMissile(target) Rotate(target): broadcast server-controlled TurnToObject(target) calculate the retail motion-table turn duration ``` Therefore missile facing belongs in the shared inbound TurnToObject execution path. Delaying the outbound missile attack request or synthesizing a bow-specific turn would be a retail divergence. Targeted spell facing needs its own retail casting-path proof before making the same claim. The inbound turn also depends on the server knowing the player's current heading. ACE calculates its turn duration from the server-side `Location` orientation. If a stationary client turn never publishes its quaternion, ACE can believe the player already faces the target, schedule a zero-duration turn, and launch before the ordinary inbound TurnToObject visibly completes. Retail keeps that authority synchronized through the full position-event cadence: ```text CommandInterpreter.UseTime(): if ShouldSendPositionEvent(): SendPositionEvent() ShouldSendPositionEvent(): if lastSentTime + 1 second >= now: return cell changed OR contact plane changed return cell changed OR NOT Frame.is_equal(lastFrame, player.frame) Frame.is_equal(a, b): return origin components differ by <= 0.0002 world units AND quaternion W/X/Y/Z components differ by < 0.0002 SendMovementEvent(): send MoveToState(player.m_position) lastSentTime = now // do not replace last frame or contact plane SendPositionEvent(): send AutonomousPosition(player.m_position) lastSentTime = now lastSentPosition = player.m_position // cell + origin + orientation lastSentContactPlane = player.contact_plane ``` Quaternion comparison is component-wise; retail does not canonicalize the equivalent stored `q` and `-q` representations during this comparison. Plane comparison uses `<= 0.0002` for the three normal components but `< 0.0002` for the distance component. `UseTime` proves the internal Should→AP order, while MTS originates in separate input handlers; their relative same-tick callback and wire order is not established by these functions and remains the narrow TS-33 trace item. This is the shared correction for melee, missile, use, and spell targeting; there is no weapon-specific pre-turn or artificial attack delay. ## Instant mouse-look is player movement, not a render-only yaw Retail's middle-mouse instant look does not mutate the visible character frame behind the movement system. The camera owner translates the mouse into ordinary turn motions and the command interpreter publishes those motions to the server: ```text CameraSet.ToggleMouseLook(active): mouseLookActive = active commandInterpreter.SetMouseLookActive(active) commandInterpreter.MovePlayer(CameraInstantMouseLook, active, 1, mouse=true, holdRun=true) if !active: commandInterpreter.ApplyCurrentMovement() commandInterpreter.SendMovementEvent() // Toggle does not update CameraSet.m_LastServerMessage. CInputManager_WIN32.GetInput(): accumulate all raw device records into the frame's cursor displacement if the cursor position changed: lastInputEvent = now CallMouseLookHandler(totalDx, totalDy) once else if mouseLookActive && now > lastInputEvent + 0.2 seconds: CallMouseLookHandler(0, 0) CameraSet.MouseLookHandler(mouseX, mouseY): if raw mouseX == 0 && raw mouseY == 0: StopDrift() if now > m_LastServerMessage + 0.5 seconds: m_LastServerMessage = now SendMovementEvent() low-pass filter the one accumulated raw sample multiply by configured input sensitivity and 0.0666666701 on horizontal zero: reset mouselook_x_extent on horizontal nonzero: increment mouselook_x_extent only after five consecutive nonzero samples: CameraSet.Rotate(filtered horizontal input) Rotate publishes only when its own invocation crosses the same strict m_LastServerMessage + 0.5-second boundary CameraSet.Rotate(horizontal input): direction = horizontal input selects TurnLeft or TurnRight speed = 2 * filtered camera adjustment for this frame speed = min(speed, 1.5) commandInterpreter.MovePlayer(direction, start=true, speed, mouse=true, holdRun=true) if last mouse movement event + 0.5 seconds < now: commandInterpreter.SendMovementEvent() CommandInterpreter.HandleMouseMovementCommand(command): TakeControlFromServer() remember command as mouse-origin movement MovePlayer(command.motion, start=true, command.speed, mouse=true, holdRun=true) SendMovementEvent() CommandInterpreter.StopListHeadMovement(): if the active command came from the mouse: MovePlayer(command.motion, start=false, command.speed, mouse=true, holdRun=true) CommandInterpreter.TakeControlFromServer(): if controlled_by_server and autonomy_level != 0 and player is alive: controlled_by_server = false player.last_move_was_autonomous = true player.StopCompletely() StopInterpolating() SetHoldRun() ApplyCurrentMovement() ``` `ApplyCurrentMovement` means the complete device snapshot: global run state, forward/back, sidestep, and turn. `CameraInstantMouseLook` remaps a held keyboard turn into the sidestep channel with that axis's `HoldKey.Run`, even when the global movement mode is walk. Both the synchronous toggle packet and later movement-edge packets must serialize the canonical remapped raw channel; reconstructing sidestep from only the physical strafe keys loses A/D after MMB is already active. `RawMotionState.CurrentHoldKey` is serialized independently of active axes, so an idle run/walk change cannot be inferred from “is moving.” Attack entry also clears client movement before the request: ```text ClientCombatSystem.StartAttackRequest(...): attack_request_in_progress = true requested_attack_power = 1 FinishJump() commandInterpreter.MaybeStopCompletely() current_build_is_automatic = false build and send the attack request CommandInterpreter.MaybeStopCompletely(): if player movement is not currently controlled by the server: StopCompletely() ``` The architectural invariant is therefore one heading with one owner: the character seen on screen, the local physics orientation, the raw outbound turn state, and ACE's authoritative player orientation all advance through the same movement command. A mouse callback that writes `PlayerMovementController.Yaw` directly violates that invariant. It creates the exact failure mode observed in the bow test: the client can appear 180 degrees away while ACE retains the old heading, computes a zero-duration `Rotate(target)`, and launches immediately. ACE 1.76.4751 commit `7233b0e57db9a8566954d8dfbc2128c84e64c4ff` cross-checks the consequence: `HandleActionTargetedMissileAttack` calls `Rotate(target)` and delays by the returned turn time; `Creature.Rotate` computes that delay from the server-side heading and broadcasts `TurnToObject`. ACE is an interpretation aid here; the client mechanism and ordering above come from named retail. ## Outbound position frame Retail never reconstructs protocol coordinates from a renderer origin: ```text SendMovementEvent(): MoveToStatePack(rawMotionState, &player.m_position, ...) SendMoveToStateEvent(pack) SendPositionEvent(): if Position.IsValid(player.m_position): AutonomousPositionPack(&player.m_position, ...) SendAutonomousPositionEvent(pack) Position.Pack(): write objcell_id Frame.Pack(frame) // landblock-local origin + orientation ``` `CPhysicsObj::m_position` is the canonical carried pair `(cell id, local Frame.Origin)`. In acdream the equivalent is `PhysicsBody.CellPosition`. `_liveCenterX/Y` is a render/streaming translation and must not participate in outbound messages. The captured regression was exact: the player crossed from A9B1 to A9B2 and the logical entity's projection was recovered. That recovery re-ran the login origin initialization, moving `_liveCenterY` by one block while the existing world position remained in the A9B1 render frame. Reconstructing local Y then sent `214.56` instead of the carried `22.56`, a 192 m displacement. The fix has two invariants: ```text first accepted canonical player Position in CreateObject or Position update: initialize render/streaming origin once, before world translation non-player projection before that Position: retain canonical live record, defer render projection spatial projection recovery, rebucketing, or appearance mutation afterward: never initialize or move the session origin every outbound movement/position/jump packet: serialize PlayerMovementController.CellPosition directly ```