feat(combat): port retail held weapon parenting
Select the default combat mode from ordered equipped objects so bows request missile stance. Parse CreateObject parent metadata and ParentEvent, then render held objects as separate children composed from setup holding locations and placement frames each animation tick.
This commit is contained in:
parent
564d39dfea
commit
ab6d96d113
18 changed files with 1152 additions and 17 deletions
|
|
@ -2754,18 +2754,19 @@ Retail oracle for cell-id hysteresis: `acclient_2013_pseudo_c.txt:308742-308783`
|
|||
|
||||
## #94 — Held items project spotlight on walls
|
||||
|
||||
**Status:** OPEN — **BLOCKED, not an active A7 investigation target.** acdream does not
|
||||
yet support equipping hand-held items (weapons, orbs, light sources) at all
|
||||
(user-confirmed 2026-07-09), so this can't be reproduced or verified either way right
|
||||
now. Re-open for real investigation once item-equipping ships; until then this is a
|
||||
placeholder, not a live bug.
|
||||
**Status:** OPEN — **UNBLOCKED 2026-07-11.** Retail ParentEvent/CreateObject child
|
||||
parenting now renders hand-held objects and follows the animated attachment part. The
|
||||
original lighting symptom can now be reproduced and investigated after the held-item
|
||||
visual gate; no lighting conclusion has been drawn yet.
|
||||
**Severity:** MEDIUM (visual fidelity; doesn't block gameplay)
|
||||
**Filed:** 2026-05-20
|
||||
**Component:** lighting, rendering
|
||||
|
||||
**Description:** Items the player is holding (torches, light-source items) project a spotlight effect onto nearby walls. The spotlight direction is wrong — should be omnidirectional from the item, but appears to project specifically toward wall surfaces.
|
||||
|
||||
**Root cause / status:** Per-entity light direction transform. `LightingHookSink` owner-tracking applies an entity-rotation transform that's probably wrong for held-light items — likely passing the entity's facing-direction as the spotlight cone direction when retail's behavior is omnidirectional point-light. UNTESTABLE until item-equipping exists — this is a hypothesis from the original 2026-05-20 filing, not re-confirmed.
|
||||
**Root cause / status:** Unknown. The original per-entity light-direction theory is only
|
||||
a hypothesis and must be re-tested now that held-item parenting exists. Do not treat it
|
||||
as established root cause.
|
||||
|
||||
**Files:**
|
||||
- `src/AcDream.App/Rendering/Vfx/LightingHookSink.cs` (suspected — verify during A7.L1)
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@ AP-94..AP-110 for the confirmed retail-UI completion gaps.
|
|||
| AP-108 | Paperdoll omits Aetheria, full `AutoWieldIsLegal`/dual-wield rules, double-click examine/drag from the doll, and body-part selection lighting | `src/AcDream.App/UI/Layout/PaperdollController.cs` | Basic equip slots, live doll, toggle, optimistic wield, exact DAT body hit map, authoritative upper-item priority, normal selection, and target-self dispatch work | Special slots, invalid drops, doll examine/drag, and selection lighting still differ functionally | `gmPaperDollUI @ 0x004A3590..0x004A5F90`; `InventoryPlacement::DetermineHigherPriority` |
|
||||
| AP-109 | Character Titles page is inert and live displayed-title/luminance state is absent | `src/AcDream.App/UI/Layout/CharacterStatController.cs`; `CharacterSheetProvider.cs` | Attributes/skills core output is user-accepted | Titles cannot be selected/displayed and level-200 luminance fields are missing | `gmCharacterTitleUI @ 0x0049A610`; `gmStatManagementUI::UpdateExperience @ 0x004F0A70` |
|
||||
| AP-110 | Remaining retained gameplay panels and world HUD are absent: combat/powerbar, spellbook/effects/favorite spell bars, residual social/examine/floating chat, quests/map/vitae/options/smartbox, vendor/trade/salvage/tinkering, and D.6 nameplates/floaters | D.5/D.6 roadmap; retained layout registration set | Existing panels cover the current M2 loop; Wave 10 lands each missing surface against authoritative state | Large portions of retail gameplay have no production UI | Named `gm*UI::PostInit` methods and LayoutDesc catalog |
|
||||
| ~~AP-111~~ | **RETIRED 2026-07-11 (M2 held-object parenting)** — equipped hand items are no longer omitted from the render world. CreateObject now preserves Placement/Parent/position timestamp bootstrap; live `0xF749` ParentEvent is parsed with retail sequence freshness; a focused render controller resolves `Setup.HoldingLocations`, applies the child's placement frame, and recomposes the separate child entity after every parent animation tick. Pickup retains the weenie's visual metadata for a later wield. | `src/AcDream.Core.Net/Messages/{CreateObject,ParentEvent}.cs`; `src/AcDream.Core/Meshing/EquippedChildAttachment.cs`; `src/AcDream.App/Rendering/EquippedChildRenderController.cs` | — | — | `ClientCombatSystem::GetDefaultCombatMode @ 0x0056B310`; `SmartBox::HandleParentEvent @ 0x004535D0`; `CPhysicsObj::set_parent @ 0x00515A90`; `CPhysicsObj::UpdateChild @ 0x00512D50` |
|
||||
|
||||
## 4. Temporary stopgap (TS) — 38 rows (TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-44 narrowed same day — NPC UP unified onto the interp queue, gate retained for orientation)
|
||||
|
||||
|
|
|
|||
|
|
@ -495,6 +495,7 @@ behavior. Estimated 17–26 days focused work, 3–5 weeks calendar.
|
|||
- **✓ SHIPPED — Wave 4.4c exact selected-health policy.** Core ports `ObjectIsAttackable @ 0x0056A600`; CreateObject preserves second-header PetOwner; toolbar player/pet composition is exact. Self, pets, and Free-PK creatures now query health while friendly NPCs and attackable non-creatures remain name-only. AP-46 retired. The world/radar policy matrix and exact DAT paperdoll body-map + upper-item-priority route passed live 2026-07-11.
|
||||
- **✓ SHIPPED — Wave 4.4d toolbar launchers + Use/Examine.** All seven authored launchers are discovered through their DAT panel-id attribute; mounted Inventory/Character panels toggle through the registry and the other five buttons are ghosted. Typed window-visibility events own Highlight/Normal state. Use and Examine act on selection or enter their one-shot retail target modes, with exact Appraise wire dispatch and retail cursors. Warning-free App build and 4,747-pass / 5-skip Release suite are green; the connected live gate passed 2026-07-11. AP-101 is narrowed to the remaining ammo-number display.
|
||||
- **Wave 4.4e implemented — exact missile ammo number (live gate pending).** Pure Core reproduces retail's thrown-weapon-vs-separate-ammo resolution over the ordered player equipment list and its zero-to-one count normalization. Relevant equipment/stack events update authored missile indicator `0x10000194` with the DAT font. Warning-free Release build and 4,754-pass / 5-skip suite are green; AP-101 is retired.
|
||||
- **M2 held-object parenting implemented 2026-07-11; live gate pending.** The combat toggle now ports `GetDefaultCombatMode` over ordered equipped contents, so a bow requests Missile instead of the old hardcoded Melee. CreateObject preserves parent/placement/timestamp fields, `0xF749` ParentEvent is handled, and `EquippedChildRenderController` renders the weapon as a separate child composed from the animated hand part + holding frame + child placement frame. App Release builds with zero warnings; the full 4,765-pass / 5-skip suite is green. AP-111 is retired; research: `docs/research/2026-07-11-combat-default-and-parent-event-pseudocode.md`.
|
||||
- **✓ SHIPPED — Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`, 2026-06-26, same branch). **Visually user-confirmed 2026-06-26 — Attributes tab reads as retail.** Three tabs, header (name/heritage/PK), large-gold level number (dat font, `largeDatFont` 18px), "Total Experience (XP):" + "XP for next level:" captions, 9-row attribute list (icons + right-aligned values + Health/Stamina/Mana vitals), click-to-select (top/bottom selection bars + footer State-B "{Attr}: {value}" / "Experience To Raise: Infinity!" + affordability-gated raise triangles), centered footer. User noted "still needs some polish for later" — deferred to Issue #158.
|
||||
- **✓ SHIPPED — D.5.4 — Client object/item data model (foundation).** Shipped 2026-06-18 (`b506f53`..`a33e897`, 11 commits). Renamed `ItemRepository`→`ClientObjectTable` / `ItemInstance`→`ClientObject`; broadened the table to hold EVERY server object (retail `weenie_object_table` shape). `CreateObject` is now the canonical merge-upsert (`ClientObjectTable.Ingest`, retail `SetWeenieDesc` semantics) via a new Core.Net `ObjectTableWiring` (off GameWindow); `DeleteObject` evicts; `PlayerDescription` is a membership manifest (`RecordMembership`); live container-membership index (`GetContents`, retail `object_inventory_table`). `_liveEntityInfoByGuid` retired (selection/describe resolve from the one table). Root fix: the old enrich-existing-only `EnrichItem` dropped `CreateObject`s for items with no `PlayerDescription` stub — live-Coldeve 4/6 hotbar slots blank; items are now created, not dropped. **Crux resolved:** retail is TWO tables (`object_table` + `weenie_object_table`), NOT one — acdream's `WorldEntity` (3D system) + `ClientObjectTable` (data/UI) split was already architecturally faithful; the fix was the ingestion path, not a table unification. 2671 tests green.
|
||||
- **Roadmap correction (2026-07-10):** the completion order is now the architecture-first campaign in `docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md`. Retail `gmToolbarUI` is object-only: preserve `ShortCutData.index_`, `objectID_`, and `spellID_`, but do not invent spell glyphs on this bar. `PlayerModule::favorite_spells_[8]` feeds separate spell bars.
|
||||
|
|
|
|||
|
|
@ -451,6 +451,9 @@ include dungeons.
|
|||
Inventory list) — minimal ImGui surfaces, retail-skin deferred to M5.
|
||||
- **L.1c** — Combat animation wiring (draw/sheath, attack swings by
|
||||
stance/power/height, hit reactions, evades).
|
||||
- **L.1c held-object foundation (implemented 2026-07-11; live gate pending)** — exact
|
||||
equipment-aware default combat mode (bow→Missile, caster→Magic) plus retail
|
||||
CreateObject/ParentEvent child attachment and per-frame hand-follow rendering.
|
||||
- **L.1b** — Command router + motion-state cleanup (prereq for L.1c).
|
||||
|
||||
**Freeze on landing:**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,160 @@
|
|||
# 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).
|
||||
|
||||
## 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue