PickupEvent (0xF74A) and DeleteObject (0xF747) are semantically distinct:
- 0xF747 = weenie DESTROYED → evict from ClientObjectTable (weenie_object_table)
- 0xF74A = object LEFT THE 3D WORLD VIEW (moved into a container) → remove
the 3D WorldEntity, but the weenie persists in ClientObjectTable
Before this fix, both paths fired EntityDeleted identically, causing
ObjectTableWiring to evict the weenie from ClientObjectTable. The follow-up
InventoryPutObjInContainer (0x0022) then tried MoveItem on an unknown guid
and no-op'd, so the unwielded item simply vanished.
Fix: add `bool FromPickup` (default false) to DeleteObject.Parsed. WorldSession
sets it true on the PickupEvent path and false on the DeleteObject path.
ObjectTableWiring.Wire's EntityDeleted handler skips table.Remove when
FromPickup is true, preserving the weenie for the container-move echo.
GameWindow.OnLiveEntityDeleted (3D entity removal) is untouched — it fires
for both pickups and destroys, as intended.
Divergence register: AP-65 added (data ghosts for other-player pickups until
teleport/relog clear; harmless — no UI queries ContainerId 0).
Tests: +5 (DeleteObject FromPickup parser regression; wiring retain/evict
semantics; Parsed default/explicit FromPickup). 343/343 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors the InventoryPutObjInContainer 0x0022 handler which already does
MoveItem + ConfirmMove. Without this, WieldItemOptimistic's pending snapshot
would linger until the session ended (or incorrectly roll back on 0x00A0).
ConfirmMove is a no-op when nothing is pending, so safe for server-initiated
login wields.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Final-review response. Adds the spec §5 / Task-16 burden end-to-end coverage
the per-task plan missed: (1) burden reads wire EncumbranceVal over the carried
sum (asserts 50% from wire 7500, not 20% from sum 3000 — retires AP-48's
fallback as the primary), (2) a live player-int update repaints the bar (60%),
which only happens via the C1d Concerns `o.ObjectId == p` branch. Both are
discriminating (fail if the respective branch is reverted).
Plus two clarifying comments from the review: the 0x02CD player route depends on
the login PD upsert having created the player object (no-ops, no phantom, if
not); and a TODO that the container-open phase must treat ViewContents as a full
replace, not the additive merge used here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Wire signature gains optional 3rd param `Func<uint>? playerGuid` (existing
GameWindow caller `Wire(session, Objects)` still compiles — default null).
- ObjectIntPropertyUpdated gate loosened: was UiEffects-only, now applies ALL
PropertyInt updates on visible objects (server is the authority on object props).
- PlayerIntPropertyUpdated → UpdateIntProperty(playerGuid(), ...) so live
EncumbranceVal (0x02CD) updates the player's burden bar.
- StackSizeUpdated → UpdateStackSize(guid, stackSize, value).
- InventoryObjectRemoved → Remove(guid).
- Updated class doc-comment to list all 5 wired opcodes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add 3 new events (PlayerIntPropertyUpdated, StackSizeUpdated, InventoryObjectRemoved)
and their payload record types near the existing ObjectIntPropertyUpdated event.
Add 3 switch cases in the GameMessage dispatcher immediately after the
PublicUpdatePropertyInt (0x02CE) branch for:
- PrivateUpdatePropertyInt (0x02CD) — no-guid player-own property
- SetStackSize (0x0197) — stack count + value update
- InventoryRemoveObject (0x0024) — remove from inventory view
No test seam on the switch; verified by build + parser tests (Tasks 3/4/5)
+ live run per codebase convention (see ObjectTableWiringTests comment).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Register four previously-unwired GameEvent handlers after InventoryPutObjInContainer:
- ViewContents (0x0196): iterates ParseViewContents entries and calls
items.RecordMembership(entry.Guid, containerId) for each — so the
object table is correct before the container-open UI mounts.
- InventoryPutObjectIn3D (0x019A): calls items.MoveItem(guid, 0u) to
unparent a dropped item from its container (it's now a ground object).
- InventoryServerSaveFailed (0x00A0): parse + log; rollback behavior
deferred to B-Drag (acdream has no speculative moves yet).
- CloseGroundContainer (0x0052): parse + log; no table change needed
(the container-open view is UI-only).
Two dispatcher tests cover ViewContents membership and Put3D unparenting.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add optional Func<uint>? playerGuid parameter (last in WireAll signature so all
existing callers compile unchanged). When provided, the PD handler calls
items.UpsertProperties(playerGuid(), p.Value.Properties) immediately after the
null-guard, landing EncumbranceVal (PropertyInt 5) and other player stats into the
player ClientObject. Upsert (create-if-absent) handles PD arriving before the
player's CreateObject. Retires AP-48/AP-49 divergence rows (wired in Task 16).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 9 of the B-Wire inventory-wire plan. Adds three outbound C→S GameAction
builders to InventoryActions.cs (DropItem 0x001B, GetAndWieldItem 0x001A,
NoLongerViewingContents 0x0195) plus matching opcode constants after TeleToPoiOpcode.
Adds SendDropItem/SendGetAndWieldItem/SendNoLongerViewingContents wrappers to
WorldSession.cs (after SendRemoveShortcut, ~line 1157), following the exact same
NextGameActionSequence() + SendGameAction() pattern as the existing Send* family.
Three byte-layout tests added to InventoryActionsTests.cs (12 total, all green).
Core.Net build clean. Unblocks B-Drag (drop) and container-open (NoLongerViewingContents
on close) call sites in the inventory controller.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0x00A0 InventoryServerSaveFailed carries (itemGuid, weenieError) per ACE
GameEventInventoryServerSaveFailed.cs and holtburger events.rs:147. The
old parser returned only the itemGuid as uint?, silently dropping the
error code. Replaced with a typed InventoryServerSaveFailed record that
reads both u32s (8-byte guard). Parser was unwired (no callers in
GameEvents.cs or GameEventWiring.cs) so the signature change is safe.
1 new test in GameEventsInventoryTests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0x0022 InventoryPutObjInContainer carries 4 u32s per ACE
GameEventItemServerSaysContainId.cs: itemGuid, containerGuid, placement,
containerType. The parser was reading only 3 (12 bytes) and silently
dropping containerType. Fixed the record struct to add ContainerType and
raised the length guard to 16. GameEventWiring caller uses only
.ItemGuid/.ContainerGuid/.Placement — adding a positional field is
source-compatible. 1 new test in GameEventsInventoryTests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GameEvents.ParseViewContents parses the ViewContents GameEvent payload:
containerGuid + count + [guid, containerType]×count. Records
ViewContentsEntry and ViewContents added. 3 unit tests added in
GameEventsInventoryTests.cs (two-entry, zero-count, truncated).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror of PublicUpdatePropertyInt (0x02CE) but with no guid field —
targets the local player's own object. Burden (EncumbranceVal, PropertyInt 5)
changes ride this opcode on every pick-up / drop. Layout: opcode(4) + seq(1)
+ property(4) + value(4) = 13 bytes. 3 tests: happy path, wrong opcode, truncated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Matches the ClientObjectTable model-placement convention; Load now takes (slot,objGuid) pairs so
the store has no Core.Net dependency. + self-drop wire-count assert + comment fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Move ShortcutStoreTests from AcDream.Core.Tests to AcDream.Core.Net.Tests (Rule 6:
tests live in the project matching the layer under test; ShortcutStore is Core.Net)
- Replace fully-qualified System.Buffers.Binary.BinaryPrimitives. with BinaryPrimitives.
in the two new BuildAddShortcut tests (file already has `using System.Buffers.Binary`)
- Add `using System;` to ShortcutStore.cs; change System.Array.Clear → Array.Clear
(matches sibling file style, no behavior change)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ShortcutStore lands in AcDream.Core.Net.Items (not AcDream.Core.Items) because
it depends on PlayerDescriptionParser.ShortcutEntry; placing it in AcDream.Core
would create a circular dependency (Core.Net already references Core). The test
lives in AcDream.Core.Tests which gets Core.Net transitively via App.
BuildAddShortcut signature corrected from the old (seq, slotIndex, objectType,
targetId) 4×u32 layout to the retail ShortCutData wire format confirmed in the
action-bar deep-dive: Index(u32), ObjectId(u32), SpellId(u16), Layer(u16).
The old BuildAddShortcut_ThreeFields test is replaced by two new tests that
verify both item and spell shortcut packing.
WorldSession gains SendAddShortcut / SendRemoveShortcut following the
SendChangeCombatMode sender pattern.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port of gmToolbarUI::HandleSelectionChanged (acclient_2013_pseudo_c.txt:198635).
When the player selects a world object the action bar's bottom strip shows the
object name + (for player/pet/attackable targets) a live Health meter; deselect
clears it. Mana (#140) + stack slider deferred.
- SelectedObjectController (new): clear-then-populate on selection change; sets
name (UiText child, VitalsController pattern), overlay state (ObjectSelected /
StackedItemSelected via UiDatElement.ActiveState), shows the health meter and
sends QueryHealth for health targets. Subscribes via a delegate seam (no
GameWindow coupling).
- GameWindow: _selectedGuid field -> SelectedGuid property + SelectionChanged
event (fires on actual change only); 3 write sites converted, reads untouched.
All selection-write paths (LMB pick, Tab/Q, despawn-clear via Tick()) run on
the render thread, so the event-driven UI mutation is single-threaded.
- WorldSession.SendQueryHealth (0x01BF) — wraps SocialActions.BuildQueryHealth.
- DatWidgetFactory.BuildMeter: handle the single-image toolbar meter shape
(back-track on the element's own DirectState, fill on one Type-3 child). The
sprites go in the TILE slot (DrawMode=Normal tiles to full bar geometry per
UIElement_Meter::DrawChildren) — a left-cap assignment would gap/clamp a
sub-140px sprite. Vitals 3-slice path unchanged.
- ToolbarController.HiddenIds: A1 (health) now owned by SelectedObjectController;
A2 (mana) + A4 (stack) stay hidden (deferred) so their dat back-tracks don't
render as stray empty bars.
Adversarial Opus review found + fixed: the mana-meter orphan (A2 left unhidden)
and the meter tile-vs-cap render bug (C1). Divergence rows AP-46 (health gate
approximation: IsLiveCreatureTarget vs IsPlayer||pet||attackable) + AP-47
(meter shown on select vs on UpdateHealth reply). Spec §5 corrected.
Build + full test suite green (2,684 passed / 4 skipped). Health meter render
fidelity (full-width fill + fraction mapping) pending the user's visual gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The old seeding block set WeenieClassId = inv.ContainerType (a 0/1/2
container-kind discriminator, not a weenie class id) and used MoveItem
for the equipped block. Replace both loops with RecordMembership calls:
inventory guids get a bare stub (WeenieClassId stays 0); equipped guids
get the equip slot set directly. Weenie data arrives via CreateObject /
ObjectTableWiring, not PlayerDescription.
New test PlayerDescription_SeedsMembership_NotWeenieClassIdMisuse proves:
(a) inv guid is registered, (b) WeenieClassId==0 not ContainerType, and
(c) equipped guid CurrentlyEquippedLocation is set to MeleeWeapon.
No existing tests pinned the old behavior; all 15 GameEventWiringTests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CreateObject ingestion moves to Core.Net; GameWindow drops the EnrichItem call +
inline 0x02CE handler. Fixes the Coldeve blank-icon root cause: items with no PD
stub are now created, not dropped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WeenieClassId + Value/StackSize/MaxStackSize/Burden/capacities/Container/Wielder/
ValidLocations/CurrentWieldedLocation/Priority/Structure/Workmanship. Nullable =
flag absent (don't clobber on merge). Cursor walk unchanged; +cursor-integrity test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Broaden naming to the data side of every server object (retail weenie_object_table
shape). Pure rename; no behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New standalone parser for the server's live PropertyInt update targeting
a VISIBLE object (carries guid). Wire layout: u32 opcode + u8 sequence +
u32 guid + u32 property + i32 value (17 bytes total).
The sequence byte is parsed-past but not honored (latest-wins; DR-4).
The companion PrivateUpdatePropertyInt (0x02CD) targets the player's own
object (no guid) and is not parsed here.
Three tests: uiEffectsUpdate (round-trip guid/prop/value), wrongOpcode
(returns null), truncated (returns null on 16-byte input).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously, weenieFlags bit 0x80 (UiEffects) was read + discarded with
`pos += 4`. Now it is captured into `uiEffects` and surfaced as
`Parsed.UiEffects` — the sole wire path for the effect bitfield since
PropertyInt.UiEffects (18) has no [AssessmentProperty] and never appears
in appraise responses.
Test builder gains `uint uiEffects = 0` param; write line updated to use
it. Three new parse tests: UiEffects_Captured, UiEffectsThenIconOverlay
(cursor-arithmetic regression), and NoUiEffectsBit_LeavesUiEffectsZero.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CreateObject optional-tail walker previously stopped at UseRadius (~20 fields
before IconOverlay). This left ItemInstance.IconOverlayId/IconUnderlayId always 0,
so IconComposer's underlay/overlay layers were never drawn on toolbar icons.
Exact field order verified against ACE WorldObject_Networking.cs:87-219 (the
serializer is the authority; acdream connects to a local ACE server):
UseRadius → TargetType(u32) → UiEffects(u32) → CombatUse(sbyte) →
Structure(u16) → MaxStructure(u16) → StackSize(u16) → MaxStackSize(u16) →
Container(u32) → Wielder(u32) → ValidLocations(u32) →
CurrentlyWieldedLocation(u32) → Priority(u32) → RadarBlipColor(u8) →
RadarBehavior(u8) → PScript(u16) → Workmanship(f32) → Burden(u16) →
Spell(u16) → HouseOwner(u32) → HouseRestrictions(variable RestrictionDB) →
HookItemTypes(u32) → Monarch(u32) → HookType(u16) →
IconOverlay(PackedDwordKnownType) ← CAPTURE →
IconUnderlay from weenieFlags2 bit 0x01 ← CAPTURE
RestrictionDB handled correctly: Version(u32) + OpenStatus(u32) + MonarchId(u32)
+ count(u16) + numBuckets(u16) + count×8 bytes entries. Length-aware skip, not a
fixed constant.
weenieFlags2 is now CAPTURED (not skipped) when IncludesSecondHeader
(objDescFlags bit 0x04000000) is set, so the IconUnderlay bit can be tested.
The entire extended walk is inside try/catch: truncated packets degrade to
IconOverlayId=0 / IconUnderlayId=0 (no overlay drawn), never corrupting.
Threading: CreateObject.Parsed → WorldSession.EntitySpawn → GameWindow
OnLiveEntitySpawned → Items.EnrichItem — both ids thread through all three
seams. EnrichItem extended with optional iconOverlayId + iconUnderlayId params
(defaulted 0, backward-compatible).
No change to IconComposer or ToolbarController (they already consume the ids).
Tests: 4 new CreateObject tests (IconOverlay only, overlay+underlay, no-overlay
regression, intermediate-fields cursor arithmetic). Full suite: 0 failures,
2636 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add optional `onShortcuts` callback to `GameEventWiring.WireAll`; invoke
it with `parsed.Shortcuts` after the inventory/equipped loops in the
PlayerDescription handler. `GameWindow` holds the list in a new
`Shortcuts` property (initialized to empty) so the toolbar (D.5.1 Task 5)
can read hotbar slots without keeping a parser reference. Existing callers
compile unchanged — the parameter defaults to null.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Added uint IconId = 0 (defaulted, last positional param) to the EntitySpawn
record so existing call sites outside WorldSession compile unchanged. The
WorldSession invoke now passes parsed.Value.IconId as the final arg.
OnLiveEntitySpawned calls Items.EnrichItem unconditionally — it's a no-op
for non-item spawns (players/NPCs/furniture aren't in the repo), so the call
is safe for every incoming CreateObject.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ReadPackedDwordOfKnownType at the old line 516 was throwing the icon dat
id away. Declare iconId before the try-block, assign it there, and pass
IconId: iconId in the Parsed initializer so downstream UI (action bar /
equipment panels) can read the 0x06xxxxxx dat id without a separate lookup.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two related close-range bugs reported in #77 share a root in
PlayerMovementController.DriveServerAutoWalk + BeginServerAutoWalk:
1. **Walk-vs-run misclassification.** BeginServerAutoWalk decided
`_autoWalkInitiallyRunning = (initialDist - distanceToObject) >= 1.0f`,
forcing run at any chase past ~1.6 m. ACE's wire-level walk-vs-run
answer is the MovementParameters CanCharge bit (0x10), which
Creature.SetWalkRunThreshold sets when server-side player→target
distance >= WalkRunThreshold/2 (= 7.5 m default). Retail's
MovementParameters::get_command (decomp 0x0052aa00) gates the run
path on CanCharge first; the inner walk_run_threshold check
practically always walks given ACE's 15 m default. The hardcoded
1.0 m threshold pushed run into the 3-5 m walk-range the user
reported should walk.
2. **Velocity leak in turn-in-place phase.** When the auto-walked body
crossed the destination, desiredYaw flipped ~180°, walkAligned
dropped to false, and the `if (!moveForward) return true;` branch
returned without zeroing body velocity. The body kept the prior
frame's running velocity (RunAnimSpeed × runRate ≈ 11 m/s) and
slid 4-5 m past the target before the turn-around rotation
completed — the "runs and slides away, runs back, picks up"
symptom in #77 bug B.
Changes:
- `CreateObject.ServerMotionState.CanCharge`: new bool prop reading
bit 0x10 of MoveToParameters. Cross-ref ACE
MovementParams.CanCharge = 0x10.
- `PlayerMovementController.BeginServerAutoWalk`: replaces the unused
`walkRunThreshold` parameter with `bool canCharge`; sets
`_autoWalkInitiallyRunning = canCharge`.
- `PlayerMovementController.DriveServerAutoWalk` turn-in-place branch:
calls `_motion.DoMotion(Ready, 1.0)` and zeros body horizontal
velocity (preserving Z for gravity). No-op for case (a) initial-turn
with stationary body; fixes (b) overshoot recovery and (c) settling
cases.
- `GameWindow.OnLiveMotionUpdated`: passes
`update.MotionState.CanCharge` through; [autowalk-begin] trace
shows `canCharge=` instead of `walkRunThresh=`.
- `GameWindow.InstallSpeculativeTurnToTarget`: predicts ACE's
CanCharge from local distance using ACE's exact 7.5 m rule, so the
speculative install agrees with the wire-triggered overwrite that
arrives moments later.
Visual-verified at Holtburg 2026-05-18: walk-range NPC click walks +
fires Use, walk-range F-key pickup walks + no overshoot, far-range
(8-10 m) pickup still runs. Test baseline unchanged (8 Core pre-existing
failures, 0 net-new failures across Core/Net/UI/App suites).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the mesh-AABB approximation with retail's actual selection
mechanism. The user observed the indicator was too small and didn't
scale with the object the way retail does — root cause was using the
wrong source data.
Retail trace (decomp anchors in named-retail/acclient_2013_pseudo_c.txt):
- VividTargetIndicator::Draw at 0x004f6c30 is registered as the
SmartBox targetting callback (0x004f6df6).
- SmartBox::DoTargettingChecks at 0x00453bb4 calls
SmartBox::GetObjectBoundingBox (0x00452e20) to compute the rect.
- GetObjectBoundingBox uses CPhysicsObj::GetSelectionSphere
(0x0050ea40) → CPartArray::GetSelectionSphere (0x00518b80) which
reads setup->selection_sphere from the DAT, applies part-array
scale (component-wise on center, Z-scale on radius), then calls
Render::GetViewerBBox (0x0054b400) to project the sphere as a
screen-space camera-aligned BBox.
- VividTargetIndicator::OnDraw at 0x004f62b0 inflates that rect by
one triangle width/height on every side before drawing (eax_21 /
eax_23 in 0x004f6a0b–0x004f6a99), so the corner triangles sit
outside the projected sphere with a small gap.
Implementation:
- GameWindow.TryGetEntitySelectionSphere reads setup.SelectionSphere
from the DAT (Setup type already exposes Origin + Radius),
applies entity scale, rotates center via entity orientation, and
produces a world-space sphere.
- TargetIndicatorPanel.TryComputeScreenRectFromSphere projects the
sphere center via the view-projection matrix and computes
screenRadius = worldRadius * projection.M22 * viewport.Y /
(2 * clip.W). M22 = cot(fovY/2) for a standard right-handed
perspective. Mathematically equivalent to retail's
Render::GetViewerBBox followed by 2-corner xformPointInternal,
faster (no double projection).
- TargetInfo carries WorldSphereCenter + WorldSphereRadius (replaces
the previous WorldAabbMin/Max). Fallback to per-type height
heuristic still in place if Setup has no baked selection_sphere
(rare; Radius <= 1e-4f short-circuits).
- Inflate by TriangleSize on every side matches retail's eax_21 +
eax_23 offsets exactly.
- Triangle right-angle apex flipped to point INWARD toward the
target (per user feedback) — apex at corner + (±t, ±t),
hypotenuse along the outer diagonal of the corner.
- TriangleSize 10 → 14 → 8 (retail sprite is small).
Also fixes a parser bug in CreateObject.cs introduced in 58e1556:
BF_INCLUDES_SECOND_HEADER is 0x04000000 per acclient.h:6458 (ACE
ObjectDescriptionFlag.IncludesSecondHeader matches), NOT 0x80000000.
The wrong bit meant the weenieFlags2 4-byte skip never fired for
entities that had the bit set, potentially shifting Useability /
UseRadius reads by 4 bytes. Now correct.
Visual verification (2026-05-16):
- Holtburg town sign — indicator traces the visible sign + pole at
the right size (matches retail screenshot proportions).
- Sign R-key still silent no-op (B.8 useability gate intact).
- NPCs / doors / items still get correctly-sized indicators.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two retail divergences fixed end-to-end:
1. R-key Use on non-useable entities (signs, banners, decorative
scenery) was silently sending Use/PickUp to ACE, triggering
auto-walk + NPC-style chat fallback. Retail's client checks
ITEM_USEABLE (acclient.h:6478) and silently ignores Use when
the USEABLE_REMOTE (0x20) bit isn't set. Now ports that gate.
2. Holtburg town sign indicator + click sphere only covered the
base of the pole because the "everything else" default in
EntityHeightFor was 1.5 m and the picker's vertical offset
for default class was 0.2 m. A 3 m sign on a pole was almost
entirely outside both shapes.
Wire change:
- CreateObject parser now walks the WeenieHeader optional tail
(per ACE WorldObject_Networking.cs:87-114) up through Useability
+ UseRadius. Captures weenieFlags upfront, then conditionally
skips PluralName, ItemCapacity, ContainerCapacity, AmmoType,
Value before reading Useability (u32) and UseRadius (f32).
- CreateObject.Parsed + WorldSession.EntitySpawn record append two
new optional fields (Useability uint?, UseRadius float?), both
defaulting to null. Existing call sites unchanged.
- 3 new tests cover: no weenieFlags → null, weenieFlags=0x10 alone
→ useability read, weenieFlags=0x8|0x10|0x20 → walker skips Value
then reads Useability + UseRadius in correct order.
Behaviour change:
- GameWindow.IsUseableTarget(guid) — authoritative path uses spawn
.Useability when present (REMOTE bit gate); fallback when null
permits Use on creatures + BF_DOOR/LIFESTONE/PORTAL/CORPSE for
M1 flow continuity.
- UseCurrentSelection (R-key dispatcher) and SendUse + SendPickUp
(double-click + F-key direct paths) gate on IsUseableTarget,
silent early-return matching retail. isRetryAfterArrival skips
the gate (re-fires only previously-gated actions).
- TargetIndicatorPanel.EntityHeightFor default branch 1.5 m → 3 m
for non-creature non-flat non-small-item entities (sign-class).
Scale > 1 still grows proportionally.
- WorldPicker callbacks: new IsTallSceneryGuid branch lifts sphere
centre to 1.5 m with 1.6 m radius for sign-class entities,
mirroring the indicator's 3 m default so click sphere matches
the visible box.
Tests: 293/293 pass in AcDream.Core.Net.Tests (+3 new walker
tests). dotnet build clean.
Retail anchors:
- acclient.h:6478 — ITEM_USEABLE enum (USEABLE_REMOTE = 0x20)
- acclient.h:6431-6463 — PWD bitfield (BF_DOOR etc.)
- ACE WorldObject_Networking.cs:87-114 — wire field order
- ACE WeenieHeaderFlag — Usable = 0x10, UseRadius = 0x20
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After B.5 shipped, the actual pickup was invisible feedback-wise: the
item left the ground, ACE despawned it via PickupEvent (0xF74A), and
the ItemRepository got updated — but the player had no visual
acknowledgement that anything happened. The M1 demo's "pick up an
item" target visually felt like the item just vanished into the void.
Add a new EntityPickedUp event to WorldSession that fires from the
PickupEvent (0xF74A) dispatch branch BEFORE EntityDeleted, so the
subscriber can still read the entity's display name from
_entitiesByServerGuid before the despawn handler clears it.
GameWindow subscribes during the live-session wiring block and emits
a retail-style system chat line plus a debug toast on every successful
pickup, mirroring retail behavior (retail synthesized this line
client-side; ACE doesn't echo it).
Closes the M1 demo "pick up" target's visible-payoff gap.
ACE sends GameMessagePickupEvent (opcode 0xF74A) instead of
GameMessageDeleteObject (0xF747) for items removed via player pickup
(Player_Tracking.RemoveTrackedObject with fromPickup=true).
Without this handler, BuildPickUp succeeded server-side (item moved
into the player's container, retail observers saw it disappear), but
our local client kept rendering it on the ground because the despawn
message went to the unhandled-opcode bucket.
PickupEvent's wire body adds an objectPositionSequence field on top
of DeleteObject's layout, so the parser is its own type. The
downstream view-removal semantics are identical to DeleteObject, so
the dispatcher routes both opcodes into the same EntityDeleted event
via a small adapter.
Three changes folded into one commit:
1. New public StateUpdated event on WorldSession + dispatcher branch
for op == SetState.Opcode. Mirrors the VectorUpdated /
MotionUpdated event pattern. GameWindow will subscribe in the next
commit and feed the parsed (guid, newState) pair to
ShadowObjectRegistry.UpdatePhysicsState.
2. One-shot probe-gated hex-dump (ACDREAM_PROBE_BUILDING) emits the
first inbound SetState message's body bytes. Originally planned
as a separate slice 1.5 confidence-check on holtburger's claimed
12-byte payload vs ACE's GameMessageSetState.cs. Folded into the
dispatcher to avoid re-touching the same branch. The new
_setStateHexDumped guard keeps the log clean — auto-close every
30s would otherwise produce noise.
3. Doc-comment polish on SetState.cs requested by Task 1's code
review: remove false uncertainty about ACE's sequence-field width
(ACE's UShortSequence.CurrentBytes provably writes 2 bytes via
BitConverter), and align the 'total body size' phrasing with
VectorUpdate.cs's convention. Folded here to avoid churning the
file twice this slice.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DTO + TryParse for the GameMessageSetState wire message. The server
broadcasts this when an already-spawned entity's PhysicsState changes
post-CreateObject — chiefly when a door's Ethereal bit toggles on Use.
Wire format per holtburger SetStateData (validated against retail-format
servers): u32 opcode + u32 guid + u32 state + u16 instanceSequence + u16
stateSequence = 16 bytes total. Mirrors the existing VectorUpdate.cs
template.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After PlayerDescription is dispatched, the Inventory and Equipped lists
produced by the parser are now fed into ItemRepository via AddOrUpdate +
MoveItem so inventory/paperdoll panels see items after login.
Acceptance test PlayerDescription_RegistersInventoryEntries_InItemRepository
confirms ItemCount goes 0→2 for a synthetic PD with two inventory entries.
282 Net.Tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>