fix(ui): restore radar, retail wield switching, and protection meshes

Late-bind radar snapshots to the canonical LiveEntityRuntime maps so the retained radar survives bootstrap and session replacement instead of capturing empty sentinels.

Route paperdoll drops through the retail AutoWield blocker transaction. Move conflicting held weapons, shields, explicit jewelry destinations, and mismatched ammo to the backpack one authoritative confirmation at a time; preserve compatible arrows when switching to melee.

Render mode-1 and no-degrade particle GfxObjs as their authored modern-pipeline meshes, retain Always2D billboards, interleave both paths back-to-front, balance emitter mesh ownership, and fail safely on corrupt DAT material metadata. This restores the closed apex on Armor Self/protection effects.

Retain Studio fixture controller lifetimes, add installed-DAT and adversarial regression coverage, synchronize retail research/divergence bookkeeping, and pass all three review tracks plus the full Release suite.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-14 20:52:45 +02:00
parent 8d63e5c28a
commit b26f84cc69
26 changed files with 1361 additions and 141 deletions

View file

@ -208,13 +208,15 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| AP-105 | **PARTIAL 2026-07-13** — inherited scrollbar media/roles now come from DAT (decrement/top `0x06004C69`, increment/bottom `0x06004C6C`), and both chat backends share typed client-command routing plus one retained `ChatVM` for reply state. Retained chat still lacks complete tab/filter/unread, social availability, incoming squelch enforcement, and focus-opacity behavior. | `src/AcDream.App/UI/Layout/DatWidgetFactory.cs`; `ChatWindowController.cs`; `src/AcDream.App/UI/ClientCommandController.cs`; chat mount in `GameWindow.cs` | Shared log/send path, wrapping, scrollbar roles, command ownership, and outer maximize geometry work; later chat work consolidates the remaining presentation/filter state | Tabs are no-ops, squelched lines can still render, contextual social actions are absent, and focus visuals diverge | `gmMainChatUI @ 0x004CCCC0..0x004CE2A0`; `UIElement_Scrollbar::OnSetAttribute @ 0x004714D0`; `ChatInterface` methods |
| AP-106 | The retained UI has no external/ground-container window lifecycle, while the original owned-side-bag `NoLongerViewingContents` premise was incorrect (#196) | `src/AcDream.App/UI`; window runtime; absent external-container controller | Owned inventory navigation remains usable and must not gain an unproven `0x0195` send; Wave 2/6 adds lifecycle ownership and the separate external surface | External container replacement/close cannot notify the server exactly once; adding the packet to owned bag close would itself diverge from the named retail call graph | `ClientUISystem.groundObject`; `CM_Inventory::Event_NoLongerViewingContents @ 0x006ABC50`; `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md` |
| ~~AP-107~~ | **RETIRED 2026-07-11 (Wave 3.3 / #197)** — typed `OfferPrimaryClick` returns `NotActive`, `ConsumedSuccess`, or `ConsumedRejected`; every retained item surface plus radar/world offers active target mode before local selection/open/use fallback. Rejections are consumed and cannot drift selection. | `src/AcDream.App/UI/ItemInteractionController.cs`; inventory/paperdoll/toolbar/radar/world call sites | — | — | `UIElement_ItemList::HandleTargetedUseLeftClick @ 0x004E24D0` |
| AP-108 | Paperdoll/AutoWield still omit full `AutoWieldIsLegal`/dual-wield rules, double-click examine/drag from the doll, and body-part selection lighting. **Aetheria retired from this row 2026-07-13:** all three sigil slots use exact backgrounds/equip masks and live `PropertyInt.AetheriaBitfield` visibility. | `src/AcDream.App/UI/Layout/PaperdollController.cs`; `src/AcDream.App/UI/AutoWieldController.cs` | Basic equip slots, Aetheria, and live doll work; inventory double-click faithfully sequences the primary weapon, incompatible shield, and mismatched ammo blockers through server-confirmed dequip→wield in both peace and war | Remaining illegal/off-hand cases, doll examine/drag, and selection lighting still differ functionally | `CPlayerSystem::AutoWield @ 0x00560A60`; `ACCWeenieObject::BlocksUseOfShield @ 0x0055D3E0`; `gmPaperDollUI @ 0x004A3590..0x004A5F90` |
| AP-108 | Paperdoll/AutoWield still omit full `AutoWieldIsLegal`/dual-wield rules, double-click examine/drag from the doll, body-part selection lighting, and retail's synchronous `" - cannot unwield the %s"` failure suffix (the current send seam reports rejection asynchronously). **Primary replacement retired from this row 2026-07-14:** inventory activation and paperdoll drops share the confirmed blocker transaction, preserve explicit slot intent, relocate an already-worn item to an explicitly selected compatible slot, and emit retail's successful move-to-backpack status. **Aetheria retired 2026-07-13.** | `src/AcDream.App/UI/Layout/PaperdollController.cs`; `src/AcDream.App/UI/AutoWieldController.cs` | Basic equip slots, Aetheria, and live doll work; primary weapon, incompatible shield, and mismatched ammo blockers sequence through server-confirmed dequip→wield in both peace and war | Remaining illegal/off-hand cases, asynchronous dequip rejection wording, doll examine/drag, and selection lighting still differ functionally | `CPlayerSystem::AutoWield @ 0x00560A60`; `ACCWeenieObject::BlocksUseOfShield @ 0x0055D3E0`; `gmPaperDollUI @ 0x004A3590..0x004A5F90` |
| 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: advanced-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 | Basic `gmCombatUI` now covers the active M2 melee/missile loop; Wave 10 lands each remaining surface against authoritative state | Large portions of retail gameplay still 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` |
| AP-112 | The basic combat bar ports visibility, height selection, desired-power slider, exact 1.0/0.8-second charge, ready-stance gating, request/release, server-response queueing, and auto-repeat, but omits `StartAttackRequest`'s `FinishJump`/`MaybeStopCompletely` command-interpreter calls and exact trained-Recklessness visibility semantics (IA-20 keeps the dark range as the accepted baseline) | `src/AcDream.App/Combat/CombatAttackController.cs`; `src/AcDream.App/UI/Layout/CombatUiController.cs` | The M2 attack contract and authored basic panel are live; the remaining seams require the jump/movement command owner and a distinct Recklessness treatment rather than UI-local guesses | Starting an attack while charging a jump or deliberately moving may not stop/cancel exactly when retail does; trained/untrained Recklessness presentation is identical | `ClientCombatSystem::StartAttackRequest @ 0x0056C040`; `gmCombatUI::ListenToElementMessage @ 0x004CC430` |
| AP-113 | Invalid lifestone-command arguments display the local text `Usage: /lifestone`; retail definitely emits a local usage/error line but Binary Ninja misidentifies the referenced wide-string address, so its exact wording is not yet recovered | `src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs`; `RetailClientCommandCatalog.cs` | The behavior boundary is exact (handled locally, no chat and no game action); only a low-impact diagnostic sentence differs | `/ls now` can show different wording/color from retail while still refusing the invalid request correctly | `ClientCommunicationSystem::DoLifestone @ 0x0056FC70` |
| ~~AP-114~~ | **RETIRED 2026-07-14 (protection-effect corrective gate)** — the particle renderer no longer replaces every authored GfxObj with one bounding-box quad. Retail `Always2D` classification preserves mode-1/no-degrade full meshes through the modern shared mesh buffer and leaves only other degrade modes on the billboard path; stable emitter handles balance mesh ownership. | `src/AcDream.App/Rendering/ParticleRenderer.cs`; `RetailParticleGeometryClassifier.cs`; `particle_mesh.vert/.frag` | — | — | `CPhysicsPart::Draw @ 0x0050D7A0`; `CPhysicsPart::Always2D @ 0x0050D8A0`; `ParticleEmitter::SetInfo @ 0x0051CE90`; `docs/research/2026-07-13-retail-projectile-vfx-pseudocode.md` |
## 4. Temporary stopgap (TS) — 34 active 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)
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |

View file

@ -559,8 +559,12 @@ include dungeons.
TS-26's
`UpdatePosition` freshness gap is retired by the nine-channel timestamp
gates; TS-49 remains the explicit
DetectionManager/`LeftDetection` gap. The final two-client visual gate is
still pending.
DetectionManager/`LeftDetection` gap. A 2026-07-14 corrective visual slice
ports `CPhysicsPart::Always2D`: mode-1/no-degrade particles now keep every
authored mesh polygon through the modern global buffer, while other degrade
modes retain the billboard path. This restores the four-piece apex of
Armor/protection shield effects. The final two-client visual gate is still
pending.
- **L.1c local attack receive path (implemented 2026-07-11; live gate pending)**
local non-autonomous mt-0 UpdateMotion now uses retail's wholesale interpreted
funnel and action-stamp gate, so ACE's server-selected melee/missile action

View file

@ -3,8 +3,9 @@
## Scope
Inventory double-click of a weapon while another primary weapon is equipped,
in both peace and combat modes. The UI request sequence is shared; combat-mode
animation remains server-authoritative.
or dragging an item onto a specific paperdoll slot, in both peace and combat
modes. The UI request sequence is shared; combat-mode animation remains
server-authoritative.
## Retail oracle
@ -21,6 +22,10 @@ animation remains server-authoritative.
`AutoWield` only after the server confirms the blocking item moved.
- `ACCWeenieObject::UIAttemptWield @ 0x0058D590` sends
`GetAndWieldItem(item, equipMask)`.
- `gmPaperDollUI::HandleDropRelease @ 0x004A4D80` routes an accepted physical
item through `AcceptDragObject @ 0x004A3B10`, which calls `AutoWield` (or
`AutoWear`) with the paperdoll's resolved target side. It does not send a
direct `GetAndWieldItem` while a blocker remains equipped.
- `ClientCombatSystem::OnQualityChanged @ 0x0056C1B0` reads player
PropertyInt 40 (`CombatMode`) and applies it through `SetCombatMode`.
@ -48,6 +53,10 @@ on inventory item double-click(item):
if decision is WieldRight or WieldLeft or AutoSort:
auto_wield(item)
on paperdoll drop(item, target slot):
resolvedMask = resolve item.ValidLocations against the authored slot side
auto_wield(item, resolvedMask)
auto_wield(item):
if another auto-wield transaction is waiting for the server:
consume the request without sending another inventory action
@ -60,7 +69,10 @@ auto_wield(item):
blocker = player's equipped item intersecting WEAPON_READY_MASK
if blocker exists:
pending = (requestedItem = item.id, blockingItem = blocker.id)
print system text "Moving <blocker name> to your backpack"
pending = (requestedItem = item.id,
blockingItem = blocker.id,
resolvedMask = desiredMask)
send PutItemInContainer(blocker.id, player.id, placement = 0)
return true
@ -82,7 +94,7 @@ on server-confirmed item move(blocker):
and blocker is now loose in the player's main pack:
requested = pending.requestedItem
clear pending before re-entry
auto_wield(requested)
auto_wield(requested, pending.resolvedMask)
on server inventory-request failure(item, error):
if item matches pending.blockingItem or pending.requestedItem:
@ -124,8 +136,14 @@ separate optimistic rollback counter.
## Known remaining retail surface
This port covers the primary weapon-ready group plus retail's secondary
incompatible-shield and mismatched-ammo blockers. `CreateObject` now retains
the exact `AmmoType` needed for those decisions. Full `AutoWieldIsLegal`,
Aetheria, and dual-wield/off-hand rules remain tracked by divergence row
AP-108.
This port covers inventory activation and paperdoll drops through the same
primary weapon-ready transaction, plus retail's secondary incompatible-shield
and mismatched-ammo blockers. `CreateObject` retains the exact `AmmoType`
needed for those decisions, and paperdoll target-side intent survives every
authoritative blocker retry. Full `AutoWieldIsLegal` and dual-wield/off-hand
rules remain tracked by divergence row AP-108. Retail also appends
`" - cannot unwield the %s"` when `AttemptToPlaceInContainer` rejects
synchronously. ACDream's current send seam is void and ACE reports that
rejection asynchronously, so the successful status line is exact while that
failure suffix remains tracked instead of being fabricated at the wrong
transaction boundary.

View file

@ -1169,3 +1169,39 @@ animation-hook/PES route plus Hidden/remote-placement/UnHide ownership. Installe
DAT colors, trail/cloud appearance, remote portal observation, and impact-origin
presentation remain visual acceptance items; an in-memory conformance fixture
cannot claim pixel appearance.
## Corrective visual gate — authored particle geometry (2026-07-14)
Oracle: `CPhysicsPart::Draw` at `0x0050D7A0`, `CPhysicsPart::Always2D` at
`0x0050D8A0`, and `ParticleEmitter::SetInfo` at `0x0051CE90`.
```text
particle presentation part = construct a complete CPhysicsPart from emitter GfxObj
Always2D(part):
if part has no degrade table or the table has no entries:
return false
return firstDegrade.DegradeMode != 1
draw particle:
if Always2D(part):
draw the camera-facing degrade presentation
else:
draw every authored mesh polygon and surface through CPhysicsPart::DrawMesh
```
The installed Armor Self chain proves why this distinction is visible:
`PS_ShieldUpGrey` resolves through table `0x34000004` to PES `0x3300068F`,
whose first four CreateParticle hooks use emitters `0x32000350`, `351`, `355`,
and `356`. Their hardware GfxObjs `0x01001666..0x01001669` are mode-1 meshes,
each with five vertices and multiple triangular polygons. Each piece contributes
an apex face meeting near local Z = 2.32 m; together they close the crystal top.
Emitter `0x32000379`/GfxObj `0x01001098` is the separate mode-2 additive
billboard. Flattening every GfxObj to one bounding-box quad discards the four
apex pieces and produces the observed open shield.
The App renderer now follows `Always2D`: full-mesh particles reuse the mandatory
modern `GlobalMeshBuffer` batches, bindless surface handles, cull modes, and
authored indices; only the 2D case enters the six-index quad path. Mesh ownership
is keyed by the stable particle-emitter handle and releases its WB reference on
the exact `EmitterDied` lifecycle edge (or renderer teardown before WB teardown).