docs(B.4b): design spec for outbound Use handler wiring
Phase B.4b closes the M1-blocker discovered during the L.2g slice 1 visual test: the input dispatcher fires SelectDblLeft on click but GameWindow.OnInputAction has no case for any Select* / UseSelected action, so clicks silently die. Spec creates the minimum new structure to close the gap: - New static helper WorldPicker (BuildRay + Pick over WorldEntities) - Rename _selectedTargetGuid -> _selectedGuid on GameWindow (unifies combat + interaction selection per retail's single-target model) - Three switch cases (SelectLeft, SelectDblLeft, UseSelected) Two further L.2g handoff inaccuracies surfaced during exploration: WorldPicker and SelectionState do NOT exist in src/ (handoff and ISSUES #57 both claimed they did). BuildPickUp also doesn't exist; only BuildUse / BuildUseWithTarget / BuildTeleToLifestone are present. Spec accounts for the actual state and defers BuildPickUp + SelectionState class extraction. Visual verification scenario reuses the L.2g slice 1 reproducibility recipe: one Holtburg inn doorway log captures both L.2g + B.4b. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
eea9b4d99a
commit
4a1c594887
1 changed files with 454 additions and 0 deletions
454
docs/superpowers/specs/2026-05-13-phase-b4b-design.md
Normal file
454
docs/superpowers/specs/2026-05-13-phase-b4b-design.md
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
# Phase B.4b — Outbound Use Handler Wiring
|
||||
|
||||
**Status:** Design spec, created 2026-05-13 after L.2g slice 1 ship handoff.
|
||||
**Branch:** `claude/compassionate-wilson-23ff99` (worktree `compassionate-wilson-23ff99`).
|
||||
**Predecessors:**
|
||||
- [docs/research/2026-05-12-l2g-slice1-shipped-handoff.md](../../research/2026-05-12-l2g-slice1-shipped-handoff.md)
|
||||
— L.2g slice 1 shipped the inbound `SetState (0xF74B)` pipeline; visual
|
||||
test was deferred when investigation uncovered that the outbound Use
|
||||
handler had never been wired.
|
||||
- [docs/ISSUES.md](../../ISSUES.md) #57 — B.4 interaction-handler gap
|
||||
filed 2026-05-12, promoted to Phase B.4b.
|
||||
- Phase B.4 (`InteractRequests` wire builders + `InputAction` enum +
|
||||
`KeyBindings`, shipped 2026-04-28 per memory; commit history confirms
|
||||
the wire builders + bindings but not the handler).
|
||||
|
||||
**Milestone:** M1 — Walkable + clickable world. Demo scenario *"open
|
||||
the inn door"* depends on this slice landing. Once B.4b lands,
|
||||
L.2g slice 1's deferred visual test verifies in the same scenario.
|
||||
|
||||
**Estimate:** ~80 LOC, 1-2 subagent dispatches, ~30 minutes implementation.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
Phase B.4 (2026-04-28) shipped half of itself: the wire-message
|
||||
builders (`InteractRequests.BuildUse` / `BuildUseWithTarget` /
|
||||
`BuildTeleToLifestone`), the `InputAction` enum entries
|
||||
(`SelectLeft` / `SelectDblLeft` / `UseSelected` / etc.), and the
|
||||
default keybindings. What was never landed: a handler that picks an
|
||||
entity at the mouse position when the user clicks, stores the
|
||||
selection, and sends a `BuildUse` packet.
|
||||
|
||||
Two further gaps surfaced during this session's exploration that the
|
||||
L.2g handoff and ISSUES.md #57 both miss-claim as "exists":
|
||||
- `WorldPicker` — does NOT exist in `src/`. Doc-only.
|
||||
- `SelectionState` — does NOT exist in `src/`. Doc-only.
|
||||
- `InteractRequests.BuildPickUp` — does NOT exist; only `BuildUse`,
|
||||
`BuildUseWithTarget`, and `BuildTeleToLifestone` are present.
|
||||
|
||||
B.4b creates the minimum new structure to close the gap: one new
|
||||
file (`WorldPicker.cs` as a stateless static helper), one rename
|
||||
(`_selectedTargetGuid` → `_selectedGuid` on `GameWindow`, unifying
|
||||
combat + interaction selection), and three switch cases in
|
||||
`GameWindow.OnInputAction` (for `SelectLeft`, `SelectDblLeft`,
|
||||
`UseSelected`). `SelectionState` as a class extraction is deferred
|
||||
to whenever the M2 HUD wants a `SelectionChanged` event; per CLAUDE.md
|
||||
"don't add abstractions beyond what the task requires."
|
||||
|
||||
`BuildPickUp` (F-key) is out of scope — not on the Holtburg inn-door
|
||||
critical path. Filed as a follow-up.
|
||||
|
||||
---
|
||||
|
||||
## Why B.4b (and not "fix B.4" or "Phase B.5")
|
||||
|
||||
| Option | Verdict |
|
||||
|---|---|
|
||||
| **Reopen "Phase B.4" and amend** | Rejected. B.4 is in memory + commit history as shipped 2026-04-28; reopening creates retroactive confusion. The gap is real; promote it to its own short-lived sub-phase per the L.2/L.2g precedent. |
|
||||
| **Roll into M2 interaction work** | Rejected. M2 is creatures + combat + a real selection HUD — weeks of work. The doors-open scenario is M1. Need a small slice that unblocks the M1 visual test without dragging M2 forward. |
|
||||
| **New phase "B.4b — Outbound Use handler wiring"** | **Selected.** Mirrors the L.2d → L.2g lettered-sub-phase pattern. Phase-sized (30-50 LOC was the initial estimate; final is closer to ~80 with the picker file), commit-trackable, closeable as soon as the visual test passes. |
|
||||
|
||||
---
|
||||
|
||||
## Problem evidence
|
||||
|
||||
Discovered 2026-05-12 while running the L.2g slice 1 visual test. The
|
||||
input dispatcher correctly fires `SelectDblLeft` on every double-left-
|
||||
click — the diagnostic `[input] SelectDblLeft Press` line shows in the
|
||||
log — but `GameWindow.OnInputAction`'s switch has zero `case
|
||||
InputAction.SelectLeft / SelectDblLeft / UseSelected` branches.
|
||||
Nothing downstream listens. The click silently dies.
|
||||
|
||||
From `GameWindow.cs:8546-8646` (the full `OnInputAction` switch as of
|
||||
this morning's L.2g merge):
|
||||
|
||||
```
|
||||
switch (action)
|
||||
{
|
||||
case InputAction.AcdreamToggleDebugPanel: ...
|
||||
case InputAction.AcdreamToggleCollisionWires: ...
|
||||
case InputAction.AcdreamDumpNearby: ...
|
||||
case InputAction.AcdreamCycleTimeOfDay: ...
|
||||
// ... 12 other Acdream*/Combat*/Toggle* cases ...
|
||||
case InputAction.SelectionClosestMonster:
|
||||
SelectClosestCombatTarget(showToast: true);
|
||||
break;
|
||||
case InputAction.EscapeKey: ...
|
||||
}
|
||||
```
|
||||
|
||||
`SelectionClosestMonster` (Q-cycle combat target) is the *only*
|
||||
selection-related case. `SelectLeft` / `SelectDblLeft` / `SelectRight`
|
||||
/ `UseSelected` / `SelectionPickUp` / all the other Select-family
|
||||
actions have no cases at all.
|
||||
|
||||
Inbound side (L.2g slice 1) is wired and ready to receive the
|
||||
server's reply. Outbound is the only block.
|
||||
|
||||
---
|
||||
|
||||
## Current acdream state
|
||||
|
||||
| Component | State |
|
||||
|---|---|
|
||||
| `InteractRequests.BuildUse(seq, guid)` wire builder | shipped at `src/AcDream.Core.Net/Messages/InteractRequests.cs:37` |
|
||||
| `InteractRequests.BuildUseWithTarget` | shipped at same file:51 |
|
||||
| `InteractRequests.BuildPickUp` | DOES NOT EXIST (handoff was wrong) |
|
||||
| `InputAction.SelectLeft` / `SelectDblLeft` / `SelectRight` / `UseSelected` / `SelectionPickUp` | defined in `InputAction` enum |
|
||||
| KeyBindings: LMB → `SelectLeft`, LMB-dblclick → `SelectDblLeft`, RMB → `SelectRight`, R → `UseSelected`, F → `SelectionPickUp` | wired in `src/AcDream.UI.Abstractions/Input/KeyBindings.cs:303-320, 172, 210` |
|
||||
| `WorldPicker` class | DOES NOT EXIST (handoff was wrong) |
|
||||
| `SelectionState` class | DOES NOT EXIST (handoff was wrong) |
|
||||
| Selection field on GameWindow | exists as `_selectedTargetGuid` but combat-only (used by `SelectClosestCombatTarget` and `ToggleLiveCombatMode`) |
|
||||
| `WorldSession.NextGameActionSequence()` | shipped; outbound chat/move already uses it |
|
||||
| `WorldSession.SendGameAction(byte[])` | shipped; outbound chat/move already uses it |
|
||||
| `OnInputAction` switch case for `Select*` / `UseSelected` | MISSING — **the gap** |
|
||||
|
||||
---
|
||||
|
||||
## Design
|
||||
|
||||
### Architecture
|
||||
|
||||
One new file + edits to `GameWindow.cs`.
|
||||
|
||||
**New:** `src/AcDream.App/Rendering/WorldPicker.cs` — static helper
|
||||
class in `AcDream.App.Rendering` namespace. Two pure methods, no
|
||||
state, no DI. Lives next to `GameWindow.cs` because the picker
|
||||
shares `WorldEntity` shape knowledge with the rendering layer (uses
|
||||
`.ServerGuid` + `.Position`).
|
||||
|
||||
**Edited:** `src/AcDream.App/Rendering/GameWindow.cs`:
|
||||
1. Rename field `_selectedTargetGuid` → `_selectedGuid` (project-wide
|
||||
find/replace; ~5 call sites all inside `GameWindow.cs`). Unifies
|
||||
combat + interaction selection on one field. Retail-faithful: AC
|
||||
has one "current target" not two.
|
||||
2. Add three switch cases to `OnInputAction`: `SelectLeft`,
|
||||
`SelectDblLeft`, `UseSelected`.
|
||||
|
||||
`SelectionState` as a separate class is deferred. Reason: only two
|
||||
consumers today (combat Q-cycle, click handler). The class earns its
|
||||
keep when consumer #3 (HUD widget that subscribes to
|
||||
`SelectionChanged`) lands in M2. Premature otherwise.
|
||||
|
||||
### Components
|
||||
|
||||
#### `WorldPicker.BuildRay`
|
||||
|
||||
Standard mouse-to-world unprojection. Convert pixel `(mouseX, mouseY)`
|
||||
to NDC `(2*mouseX/vpW - 1, 1 - 2*mouseY/vpH)`, unproject the near
|
||||
point (`ndc.z = -1`) and far point (`ndc.z = +1`) through
|
||||
`inverse(projection) → inverse(view)`, return `(origin = near,
|
||||
direction = normalize(far - near))`.
|
||||
|
||||
Signature:
|
||||
```csharp
|
||||
public static (Vector3 Origin, Vector3 Direction) BuildRay(
|
||||
float mouseX, float mouseY,
|
||||
float viewportW, float viewportH,
|
||||
Matrix4x4 view, Matrix4x4 projection);
|
||||
```
|
||||
|
||||
~20 LOC. Pure math. No exception paths — the OpenGL view/proj matrices
|
||||
we hand it are always invertible.
|
||||
|
||||
#### `WorldPicker.Pick`
|
||||
|
||||
Ray-sphere intersection against each candidate entity's `Position`
|
||||
with radius 5.0f (matches `WorldEntity.DefaultAabbRadius`). Skip the
|
||||
self-guid (player). Track the closest hit with `t < maxDistance` (50m
|
||||
default). Return the picked entity's `ServerGuid`, or `null` for miss.
|
||||
|
||||
Signature:
|
||||
```csharp
|
||||
public static uint? Pick(
|
||||
Vector3 origin, Vector3 direction,
|
||||
IEnumerable<WorldEntity> candidates,
|
||||
uint skipServerGuid,
|
||||
float maxDistance = 50f);
|
||||
```
|
||||
|
||||
~30 LOC. Excludes entities with `ServerGuid == 0` (atlas-tier scenery
|
||||
+ dat-hydrated statics) — those have no server-side identity, so a
|
||||
`BuildUse` against them would carry guid=0 and be rejected.
|
||||
|
||||
Sphere intersection math (geometric form): for each candidate, compute
|
||||
`oc = origin - entity.Position`, `b = dot(oc, direction)`, `c =
|
||||
dot(oc, oc) - r²`, discriminant `d = b² - c`. If `d < 0` no hit.
|
||||
Otherwise `t = -b - sqrt(d)` is the near intersection; track smallest
|
||||
positive `t < maxDistance`.
|
||||
|
||||
#### `OnInputAction` switch cases
|
||||
|
||||
Three new cases right before the `EscapeKey` case (preserve the
|
||||
existing case ordering by feature group):
|
||||
|
||||
```csharp
|
||||
case InputAction.SelectLeft:
|
||||
PickAndStoreSelection(useImmediately: false);
|
||||
break;
|
||||
|
||||
case InputAction.SelectDblLeft:
|
||||
PickAndStoreSelection(useImmediately: true);
|
||||
break;
|
||||
|
||||
case InputAction.UseSelected:
|
||||
UseCurrentSelection();
|
||||
break;
|
||||
```
|
||||
|
||||
Plus three private helper methods on `GameWindow`:
|
||||
|
||||
- `PickAndStoreSelection(bool useImmediately)`: pull `_lastMouseX/Y`,
|
||||
`_cameraController.Active.View/Projection`, `_window.Size`; call
|
||||
`WorldPicker.BuildRay` → `WorldPicker.Pick`; on hit, set
|
||||
`_selectedGuid = picked`, toast "Selected: {name}", emit diagnostic
|
||||
`[B.4b] pick guid=0x{picked:X8} name={DescribeLiveEntity(picked)}`. If
|
||||
`useImmediately`, also call `SendUse(picked)`. On miss, toast
|
||||
"Nothing to select" (no diagnostic line, no state change).
|
||||
- `UseCurrentSelection()`: if `_selectedGuid is uint sel`, call
|
||||
`SendUse(sel)`. Otherwise toast "Nothing selected".
|
||||
- `SendUse(uint guid)`: gate on `_liveSession?.CurrentState ==
|
||||
InWorld`; `seq = _liveSession.NextGameActionSequence()`; `body =
|
||||
InteractRequests.BuildUse(seq, guid)`;
|
||||
`_liveSession.SendGameAction(body)`; diagnostic
|
||||
`[B.4b] use guid=0x{guid:X8} seq={seq}`.
|
||||
|
||||
All three switch branches honor the existing `if (activation !=
|
||||
ActivationType.Press) return;` filter above the switch.
|
||||
|
||||
### Data flow (happy path)
|
||||
|
||||
```
|
||||
mouse double-click on door at pixel (540, 320)
|
||||
-> Silk.NET window event
|
||||
InputDispatcher.OnMouseDown -> DoubleClick chord match
|
||||
->
|
||||
InputDispatcher.Fired(SelectDblLeft, Press)
|
||||
->
|
||||
GameWindow.OnInputAction(SelectDblLeft, Press)
|
||||
->
|
||||
PickAndStoreSelection(useImmediately: true)
|
||||
-> pulls _lastMouseX/Y + _cameraController.Active.View/Projection
|
||||
WorldPicker.BuildRay(540, 320, vpW, vpH, view, proj) -> (origin, dir)
|
||||
->
|
||||
WorldPicker.Pick(origin, dir, _entitiesByServerGuid.Values,
|
||||
_playerServerGuid, 50f) -> 0xDoorGuid
|
||||
->
|
||||
_selectedGuid = 0xDoorGuid
|
||||
toast "Selected: Door"
|
||||
log "[B.4b] pick guid=0x000F4244 name=Door"
|
||||
->
|
||||
SendUse(0xDoorGuid)
|
||||
->
|
||||
seq = _liveSession.NextGameActionSequence()
|
||||
body = InteractRequests.BuildUse(seq, 0xDoorGuid)
|
||||
_liveSession.SendGameAction(body)
|
||||
log "[B.4b] use guid=0x000F4244 seq=N"
|
||||
-> ACE processes Use, calls Door.Open()
|
||||
ACE broadcasts UpdateMotion(NonCombat,On) -> swing animation
|
||||
ACE broadcasts SetState(guid=0xDoor, state=0x14)
|
||||
->
|
||||
WorldSession.StateUpdated event fires (L.2g slice 1 path)
|
||||
->
|
||||
ShadowObjectRegistry.UpdatePhysicsState(doorGuid, 0x14)
|
||||
-> next physics tick
|
||||
CollisionExemption.ShouldSkip returns true -> door no longer blocks
|
||||
->
|
||||
player walks through doorway
|
||||
```
|
||||
|
||||
### Error handling / edge cases
|
||||
|
||||
- **No entity hit** (clicked on terrain, sky, empty space): `Pick`
|
||||
returns `null`. No `_selectedGuid` change. Toast "Nothing to
|
||||
select". No network send.
|
||||
- **ImGui consuming the click**: `InputDispatcher` already filters
|
||||
via `wantCaptureMouse`. `OnInputAction` only fires when the click
|
||||
was outside ImGui panels. No new guard needed.
|
||||
- **No live session / not in world**: `SendUse` short-circuits early.
|
||||
Toast "Not in world" for debug visibility. Picker still runs (cheap;
|
||||
fine to leave selection state updated even offline).
|
||||
- **`UseSelected` with no current selection**: toast "Nothing
|
||||
selected". No network send.
|
||||
- **Selected entity despawns between select and use**: `BuildUse`
|
||||
still sends the cached guid. ACE replies with `UseDone` carrying
|
||||
`WeenieError.InvalidObject` (a non-zero error code). That error
|
||||
already flows into the chat-log channel via the existing
|
||||
`GameEventType.UseDone` handler; no new code needed.
|
||||
- **Ray construction degenerate**: if `direction.LengthSquared() < eps`,
|
||||
treat as no-hit and return null from `Pick`. Defensive — should
|
||||
never trigger for sane view/proj matrices.
|
||||
- **Entity at exactly `_selectedGuid` despawns silently while
|
||||
selected** (e.g. NPC walks out of streaming range): `_selectedGuid`
|
||||
becomes a stale reference. Acceptable for B.4b — the next
|
||||
`UseSelected` either sends a guid the server now doesn't recognize
|
||||
(server replies with an error, harmless) or the player picks a new
|
||||
target before pressing R. Stale-selection cleanup is M2 HUD work.
|
||||
|
||||
### Testing
|
||||
|
||||
**Unit tests** — `tests/AcDream.App.Tests/Rendering/WorldPickerTests.cs`
|
||||
(new file):
|
||||
|
||||
| Test | Scenario | Asserts |
|
||||
|---|---|---|
|
||||
| `BuildRay_CenterOfViewport_ReturnsForwardRay` | mouse at (vpW/2, vpH/2), identity view, simple perspective proj | direction approx -Z (camera-forward) within eps |
|
||||
| `BuildRay_OffsetMouse_DeflectsRay` | mouse right-of-center, same camera | direction.X > 0 (deflects toward camera-right) |
|
||||
| `Pick_RayThroughEntity_ReturnsServerGuid` | synthetic entity at (0,0,-10) with ServerGuid=0xABCD, ray from origin along -Z | returns 0xABCD |
|
||||
| `Pick_RayMisses_ReturnsNull` | same entity, ray aimed at +X | returns null |
|
||||
| `Pick_TwoEntitiesInLine_ReturnsCloser` | entities at -5 and -10, ray along -Z | returns the -5 one |
|
||||
| `Pick_SkipsSkipGuid` | one entity at -10 with guid=0xABCD, skipServerGuid=0xABCD | returns null |
|
||||
| `Pick_SkipsZeroServerGuid` | entity with ServerGuid=0 (dat-hydrated scenery) in path | returns null |
|
||||
| `Pick_BeyondMaxDistance_ReturnsNull` | entity at -100, default maxDist=50 | returns null |
|
||||
|
||||
**Switch-case behavior** — not unit-tested. Would require mocking
|
||||
`GameWindow` + `WorldSession` + `InputDispatcher` + `CameraController`,
|
||||
high cost low value for a 3-case wiring change. Verified at runtime via
|
||||
visual test.
|
||||
|
||||
**Runtime verification** — Holtburg inn doorway scenario (per the L.2g
|
||||
slice 1 handoff reproducibility recipe):
|
||||
|
||||
```powershell
|
||||
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
|
||||
$env:ACDREAM_LIVE = "1"
|
||||
$env:ACDREAM_TEST_HOST = "127.0.0.1"
|
||||
$env:ACDREAM_TEST_PORT = "9000"
|
||||
$env:ACDREAM_TEST_USER = "testaccount"
|
||||
$env:ACDREAM_TEST_PASS = "testpassword"
|
||||
$env:ACDREAM_DEVTOOLS = "1"
|
||||
$env:ACDREAM_PROBE_BUILDING = "1"
|
||||
$env:ACDREAM_PROBE_RESOLVE = "1"
|
||||
dotnet run --project src\AcDream.App\AcDream.App.csproj -c Debug 2>&1 |
|
||||
Tee-Object -FilePath "launch-b4b.log"
|
||||
```
|
||||
|
||||
Then in-client: walk to the Holtburg inn doorway, double-left-click
|
||||
the closed door, wait for swing animation, walk through. After 30s,
|
||||
watch auto-close.
|
||||
|
||||
Expected log grep:
|
||||
|
||||
```powershell
|
||||
Select-String -Path launch-b4b.log -Pattern `
|
||||
"B.4b|setstate-hex|setstate.*guid|input.*SelectDblLeft|entity-source.*Door"
|
||||
```
|
||||
|
||||
Expected matches:
|
||||
- `[input] SelectDblLeft Press` (dispatcher fires — already worked pre-B.4b)
|
||||
- **NEW:** `[B.4b] use guid=0x000F4244 seq=N` (B.4b send fires)
|
||||
- `[setstate-hex] body.len=16 ...` (server replied — L.2g hex probe)
|
||||
- `[setstate] guid=0x000F4244 state=0x00000014` (door opens — L.2g
|
||||
per-tick probe) — **NB:** if state is `0x4` only (not `0x14`),
|
||||
follow the L.2g slice-1 review's "Important note" → file a tiny
|
||||
L.2g slice 1b to widen `CollisionExemption.ShouldSkip`.
|
||||
- `[setstate] guid=0x000F4244 state=0x00000000` ~30s later (auto-close).
|
||||
- Player visibly walks through doorway during the open window.
|
||||
|
||||
### Slice plan
|
||||
|
||||
This is one slice. No further sub-slicing.
|
||||
|
||||
| Step | Files | LOC | Subagent? |
|
||||
|---|---|---|---|
|
||||
| 1. Write `WorldPickerTests.cs` (TDD: tests first) | `tests/AcDream.App.Tests/Rendering/WorldPickerTests.cs` (new) | ~80 | Yes (Sonnet) — bounded TDD task |
|
||||
| 2. Create `WorldPicker.cs` static helper | `src/AcDream.App/Rendering/WorldPicker.cs` (new) | ~50 | Same agent as step 1 |
|
||||
| 3. Rename `_selectedTargetGuid` → `_selectedGuid` in `GameWindow.cs` | 1 file edit | ~5 sites | Manual or Sonnet |
|
||||
| 4. Add 3 switch cases + 3 helper methods in `GameWindow.OnInputAction` | 1 file edit | ~40 | Manual or Sonnet |
|
||||
| 5. `dotnet build` + `dotnet test` green | — | — | Manual |
|
||||
| 6. Visual test at Holtburg inn doorway + log grep | — | — | Manual (user) |
|
||||
| 7. Commit + close #57 + update roadmap + update memory | — | — | Manual |
|
||||
|
||||
Total: ~80 LOC new code + ~80 LOC tests + ~50 LOC edits. One commit
|
||||
(or two: picker + test as one, handler wiring + rename as another).
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
- [ ] `dotnet build` green
|
||||
- [ ] `dotnet test` green; 8 new `WorldPickerTests` pass
|
||||
- [ ] Double-left-click on closed door in Holtburg inn doorway:
|
||||
- [ ] Log shows `[B.4b] pick guid=0x... name=Door`
|
||||
- [ ] Log shows `[B.4b] use guid=0x... seq=N`
|
||||
- [ ] Log shows `[setstate] guid=0x... state=0x14` (or `0x4`) shortly after
|
||||
- [ ] Door swings open visually (animation plays)
|
||||
- [ ] Player can walk through threshold (no `RESOLVE`-line wall hits)
|
||||
- [ ] R hotkey with no selection: toast "Nothing selected", no send.
|
||||
- [ ] R hotkey after selecting a door (single click) but not using it
|
||||
(no double-click): sends `BuildUse` for the same guid.
|
||||
- [ ] Single left-click on terrain (or sky): toast "Nothing to select",
|
||||
no send.
|
||||
- [ ] Q-cycle (combat closest-target) still works after the
|
||||
`_selectedTargetGuid` → `_selectedGuid` rename.
|
||||
- [ ] ISSUES.md #57 moved to "Recently closed" with this commit's SHA.
|
||||
- [ ] Roadmap "shipped" table updated.
|
||||
- [ ] CLAUDE.md "Currently in Phase L.2" paragraph updated to reflect
|
||||
L.2g slice 1 + B.4b verified, next phase candidate is the next
|
||||
preference-order item from the candidate list.
|
||||
|
||||
### Non-goals / explicitly deferred
|
||||
|
||||
- **`BuildPickUp` (F-key pickup)** — `InteractRequests` doesn't have
|
||||
this builder yet. Out of M1 critical path; file as a follow-up note.
|
||||
- **`UseWithTarget`** — wire builder exists but no client-side UX yet
|
||||
(cursor-on-item then click-on-target). M2 work.
|
||||
- **`SelectionState` as a class with `SelectionChanged` event** — wait
|
||||
for HUD consumer in M2.
|
||||
- **Hover-highlight / cursor change on hover** — UX polish, M2/M3.
|
||||
- **Right-click `SelectRight` radial menu** — M3.
|
||||
- **Selected-entity HUD widget (name, vitals)** — M2.
|
||||
- **Stale-selection auto-clear when target despawns** — M2 HUD work.
|
||||
- **Mesh-accurate picking (vs. 5m sphere)** — optimization for later;
|
||||
the 5m sphere is the retail "fast bbox" first pass, which retail
|
||||
followed with a per-triangle test on the candidate. Add only if a
|
||||
visual-test session reports a wrong-entity pick.
|
||||
|
||||
### Risks / open questions
|
||||
|
||||
| Risk | Mitigation |
|
||||
|---|---|
|
||||
| **5m sphere too generous at doorways** — picks the wall or NPC inside the inn instead of the door | First visual test pass settles it. If it picks the wrong entity, tighten the radius to 3m or add a closer-than-furthest tiebreak by entity type. |
|
||||
| **Camera-mode mismatch** — in fly/orbit mode the ray origin should be the camera position, not the player. | Resolved by using `_cameraController.Active.View` which is the camera's view matrix regardless of mode. The picker doesn't care about player position. |
|
||||
| **State value `0x4` vs `0x14`** — L.2g slice-1 review flagged that `CollisionExemption.ShouldSkip` requires both `ETHEREAL (0x4)` AND `IGNORE_COLLISIONS (0x10)`. If ACE sends only `0x4`, the exemption won't fire. | Settled by the same visual test's `[setstate]` log line. If `0x4` only, file a tiny L.2g slice 1b to widen the check; that's a one-line edit and out of B.4b scope. |
|
||||
| **`_lastMouseX/Y` at click time vs. dispatcher-fire time** — if there's a frame of latency between Silk's mouse-down event and the dispatcher fire, the mouse may have moved. | Silk fires mouse-down synchronously; `_lastMouseX/Y` are updated on every move, so they hold the click position at the moment the dispatcher fires. Verified by reading the existing `OnMouseDown` path. Low risk. |
|
||||
| **Entity not in `_entitiesByServerGuid` despite being visible** — e.g. dat-hydrated EnvCell statics have `ServerGuid=0` and won't be pickable | Acceptable for B.4b. Doors and NPCs in Holtburg are server-spawned with non-zero `ServerGuid`. Dat-hydrated statics (fireplaces, decorations) aren't meant to be Use-able. |
|
||||
|
||||
### Open question after slice ships (L.2g slice 1b)
|
||||
|
||||
The L.2g slice-1 final-review "Important note" — does ACE's
|
||||
`PhysicsObj.cs:787-791` set both `ETHEREAL_PS (0x4)` AND
|
||||
`IGNORE_COLLISIONS_PS (0x10)` simultaneously when doors open, or only
|
||||
ETHEREAL? B.4b's visual test settles this. If the hex shows `0x4`
|
||||
alone, file L.2g slice 1b to either widen `CollisionExemption.ShouldSkip`
|
||||
to `((state & ETHEREAL_PS) != 0)` alone, or set both bits in
|
||||
`UpdatePhysicsState`. Decision deferred until evidence lands.
|
||||
|
||||
---
|
||||
|
||||
## Reproducibility
|
||||
|
||||
Same launch recipe as L.2g slice 1 (above). Visual verification is the
|
||||
same scenario — both L.2g slice 1 and B.4b verified together. No
|
||||
separate L.2g visual test session needed.
|
||||
|
||||
---
|
||||
|
||||
## Worktree
|
||||
|
||||
Branch: `claude/compassionate-wilson-23ff99`, worktree
|
||||
`compassionate-wilson-23ff99`. Clean off main (commit `eea9b4d` =
|
||||
the L.2g slice 1 merge from the previous session).
|
||||
|
||||
After ship: merge to main, close #57, update CLAUDE.md + roadmap +
|
||||
memory, archive this spec + the impl plan.
|
||||
Loading…
Add table
Add a link
Reference in a new issue