GpuWorldState.RemoveLandblock rescued persistent entities (the player)
only from the _loaded list, silently dropping one sitting in the
_pendingByLandblock bucket. The player is re-injected via AppendLiveEntity
every frame; right after a teleport its destination landblock has not
streamed in yet, so the player lands in the pending bucket — and if that
landblock is then unloaded during the streaming churn, the persistent
entry was dropped, violating the "persistent therefore survives unload"
contract. Leading candidate for the #138 "own avatar vanishes after a
couple round-trips" symptom (cumulative; needs user visual confirm).
Fix: scan the pending bucket for persistent guids and rescue them too,
so DrainRescued re-parks them at the next valid landblock. Provable
correctness fix with a deterministic test (rescue-from-pending plus a
negative for non-persistent). Core tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Doors/NPCs/portals vanished after a portal OUT of the 0x0007 dungeon back
to Holtburg. Root cause confirmed via ACE + holtburger cross-reference:
the dungeon collapse drops a landblock's render entities for FPS, and ACE
will NOT re-broadcast objects whose guid is still in its per-player
KnownObjects set (never cleared on a normal teleport — ACE relies on the
client retaining its object table and culling stale objects itself). So
nothing restored them on the way back.
Retail-faithful fix: a real client keeps its weenie_object_table and
re-renders the world from it (holtburger keeps the table across a
teleport; only suspends physics bodies). acdream's _lastSpawnByGuid (the
parsed CreateObject records — position + Setup + appearance) IS that
table and survives the collapse (the collapse path never calls
RemoveLiveEntityByServerGuid, the only thing that prunes it). On landblock
(re)load, replay OnLiveEntitySpawnedLocked for retained spawns whose
render entity is absent — independent of any ACE re-send.
- LandblockEntityRehydrator: pure selection (landblock match; skip
already-present, the player, and mesh-less spawns), unit-tested (7).
- StreamingController: onLandblockLoaded callback after AddLandblock
(Loaded = dungeon-exit expand) and AddEntitiesToExistingLandblock
(Promoted = Far->Near).
- GameWindow.RehydrateServerEntitiesForLandblock: present-gate keys on
GpuWorldState (NOT _entitiesByServerGuid, which holds collapse
orphans), replay under _datLock; the replay's own
RemoveLiveEntityByServerGuid de-dup scrubs the orphan state.
Corrects the handoff: ClientObjectTable is inventory-only (no world
position/Setup) and cannot rebuild a render entity; _lastSpawnByGuid is
the world-object table. Register row AP-48 (no retail 25s visibility
cull). dotnet build + 1518 Core tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The factory mapped ZOrder from ReadOrder only, so the gmInventoryUI
full-window backdrop (0x100001D0, ReadOrder 4) painted over the nested
panels (ReadOrder 1-3). Wire ElementDesc.ZLevel through ElementInfo /
ToInfo / Merge and fold it into ZOrder = ReadOrder - ZLevel*10000 (higher
ZLevel = further back, ReadOrder the within-layer tiebreaker). Vitals
(all ZLevel 0) are unchanged; chat (ZLevel 900) + toolbar (1,2) shift to
their dat layering — verify visually.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LayoutImporter.Resolve now captures the resolved base element's children
and, for a pure-container leaf (no own children + no own media) inheriting
from a base WITH content, attaches that subtree. This pulls the nested
gmInventoryUI panels' content (paperdoll/backpack/3D-items) in through the
existing BaseElement+BaseLayoutId path. ShouldMountBaseChildren is a pure,
unit-tested predicate; it's inert for media-bearing inheritors and childless
style prototypes, so vitals/chat/toolbar are unaffected (existing importer
tests still green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Columns + CellWidth/CellHeight + a row-major CellOffset/LayoutCells pass.
CellWidth<=0 keeps the single-cell fill mode (toolbar slot sizes to the
list — unchanged); CellWidth>0 tiles cells in a grid. Layout runs on
AddItem and per-frame in OnDraw so a list resize reflows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Portals only worked once per session: teleporting OUT of a dungeon
mis-rooted the player into the SOURCE dungeon's coordinate frame, so every
move was sent dungeon-framed and ACE rejected it ("failed transition") —
the player couldn't move, never reached a portal, and the world wouldn't
re-render (only skybox).
Root cause: acdream's streaming-relative frame recenters on teleport, but
resident physics landblocks keep their load-time world-offset. After
recentering onto the outdoor destination, the collapsed source dungeon
(offset 0,0 as the prior center) and the destination (offset 0,0 as the
new center) overlap, and the Z-agnostic outdoor cell-snap returns the
dungeon for both the arrival placement and every per-frame resolve.
Fix (server-authoritative teleport placement):
- Drop the stale source center landblock from physics at the teleport
recenter (GameWindow.OnLivePositionUpdated) so the resolve falls through
to the server position (Resolve NO-LANDBLOCK verbatim) until the
destination streams in.
- Place outdoor teleports immediately (TeleportArrivalRules) — holding is
futile because streaming does not progress during a PortalSpace hold.
- Clear a dangling CellGraph.CurrCell when its landblock is removed
(PhysicsEngine.RemoveLandblock) — otherwise the dungeon-streaming gate
keeps streaming collapsed onto the gone dungeon (only skybox renders).
Keeps DungeonStreamingGate (gate suppression during the hold). Indoor
(dungeon-entry) placement is unchanged (cell-keyed, IsSpawnCellReady).
User-verified: in->out->re-enter works repeatedly, no ACE errors, world
renders. Remaining facets (server objects + own avatar not rendering after
a teleport-out) are entity render/lifecycle — split to #138.
Registers AP-36 + AD-2 updated. New: DungeonStreamingGate (+4 tests),
TeleportArrivalRules (+4 tests). Build + 2727 tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BringToFront sets a window's ZOrder one past the max among its peers.
ShowWindow now raises on open; OnMouseDown raises any pressed top-level
window (retail-faithful stacking). Existing drag/resize tests unaffected
(raise only touches ZOrder, not geometry).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A Dictionary<string,UiElement> registry on UiRoot with RegisterWindow +
Show/Hide/Toggle. Show/Hide flip UiElement.Visible (already gates
Draw/Tick/HitTest); Toggle returns the new visibility; unknown names are
no-ops. WindowNames.Inventory const shared by mount/registry/toggle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Spec §6 test 3 — the maxH clamp is the load-bearing path for the toolbar collapse; the implementer updated the existing ResizeRect tests' signatures but didn't add the clamp-verification cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
UiElement.MaxHeight + ResizableEdges mask; UiCollapsibleFrame snaps height to the nearer
of {collapsed,expanded} and toggles row-2 visibility; GameWindow computes the two heights
from the layout + top-anchors the content. Amends IA-17. UiNineSlicePanel unsealed to
allow subclassing.
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>
ToolbarController is now the LIVE drag handler:
- OnDragLift: removes the source slot from ShortcutStore, sends
RemoveShortcut (0x019D) wire immediately (retail remove-on-lift model).
- HandleDropRelease: evicts occupant from target (RemoveShortcut),
places dragged item (AddShortcut 0x019C), bumps evicted item into
the vacated source slot if empty (swap path); off-bar release leaves
the lift's removal standing.
- Populate() now lazy-loads ShortcutStore from the PD shortcut list on
first call; store is authoritative thereafter.
- IsShortcutGuid() uses the store (O(18)) when loaded, falls back to
scanning _shortcuts() in the pre-PD window.
- All 18 slot cells now get DragAcceptSprite=0x060011FA (green cross,
distinct from the inventory ring 0x060011F9).
- GameWindow.Bind wired: sendAddShortcut + sendRemoveShortcut lambdas
forwarded to _liveSession?.Send*().
- Divergence register: AP-47 Divergence+Risk cells updated (opacity
corrected, underlay-backing framing improved). TS-33 row deleted
(stopgap retired). Section header 32→31 rows.
- Tests: HandleDropRelease_isInertStub removed; 5 new B.2 tests added
(lift removes + sends, swap sequence, empty-target place, self-drop
re-adds, green-cross sprite). 24/24 ToolbarController tests pass.
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>
Found at visual verification: an occupied UiItemSlot sits inside the Draggable
toolbar frame (UiNineSlicePanel.Draggable=true), so UiRoot.OnMouseDown's FindWindow
returned the frame and the window-move branch won — press+drag on a slot moved the
whole bar instead of picking up the item. The slot wasn't CapturesPointerDrag (that
path is for self-driven text-selection and suppresses the BeginDrag promotion), and
UiRoot had no path for "a drag-source inside a draggable window."
Fix: add UiElement.IsDragSource (virtual, default false); UiItemSlot overrides it to
`ItemId != 0` (occupancy-gated). UiRoot.OnMouseDown now prioritizes IsDragSource over
window-move — an OCCUPIED slot starts the item drag (promotes to BeginDrag on >3px),
an EMPTY slot falls through to the IA-12 whole-window-drag so the bar stays movable
by its empty cells / chrome. UiRoot stays item-agnostic (reads only the bool). This
REDUCES divergence (occupied cells now drag like retail) within IA-12's umbrella — no
new register row.
Regression tests reproduce the LIVE topology (slot inside a Draggable frame); the
earlier RootWithBoundSlot tests put the slot directly under the root, so they could
not catch it. Full suite 493 pass / 0 fail / 2 skip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements Task 4 of the drag-drop spine: ToolbarController now satisfies
IItemListDragHandler. The ctor loop wires RegisterDragHandler(this) on every
slot list and stamps Cell.SlotIndex + Cell.SourceKind=ShortcutBar, matching
retail's gmToolbarUI::PostInit/RegisterItemListDragHandler pattern. OnDragOver
accepts any non-empty payload (TS-33 stub; eligibility gate is Stream B.2).
HandleDropRelease logs and returns (no AddShortcut 0x019C / RemoveShortcut
0x019D wire yet). Three new B.1 xUnit tests cover handler registration, accept,
and inert-stub semantics. All 490 app tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Standing inside (or looking into) a windowed building like the Agent of
Arcanum, interior objects (furniture, NPCs, the player) were lit by the
directional sun because acdream's sun gate was per-FRAME (keyed on the
player being in a sealed cell), not per-DRAW as retail does it.
Retail's PView::DrawCells (0x005a4840) runs two stages per frame:
outdoor stage → useSunlightSet(1) (0x005a485a): sun ON
interior stage → useSunlightSet(0) (0x005a49f3): sun OFF
DrawMeshInternal (0x0059f398) then calls minimize_object_lighting only
when useSunlight==0, so indoor objects ALWAYS skip the sun regardless of
whether the player's cell is windowed or sealed.
Fix: add a per-instance uint SSBO (binding=6 instanceIndoor[]) whose value
is IndoorObjectReceivesTorches(ParentCellId) — the same predicate AP-43
already uses for the torch gate. In mesh_modern.vert, nest the sun loop
inside an additional `if (instanceIndoor[instanceIndex] == 0u)` check
inside the existing `if (uLightingMode == 0)` block. Indoor objects get
torches (unchanged) but now skip the sun; outdoor objects keep the sun and
still get no torches. The ambient regime (UpdateSunFromSky: 0.2 sealed /
sky otherwise) is untouched — it was already correct.
Mechanically: _currentEntityIndoor set once per entity in
ComputeEntityLightSet; appended to InstanceGroup.IndoorFlags in
AppendCurrentLightSet; grown/packed/uploaded in the same cursor loop as
_clipSlotData and _lightSetData; deleted in Dispose. Mode-1 draws
(EnvCellRenderer) never read binding=6 — the sun loop is inside the
uLightingMode==0 uniform-control-flow branch.
AP-43 divergence register updated: the sun half is now per-draw (no
longer a residual). Residual narrowed to the unaudited ebp_2 test in
CellManager::ChangePosition (no observed impact).
Tests: WbDrawDispatcherIndoorFlagTests pins IndoorObjectReceivesTorches
for the spec §5 representative ids: 0xA9B40172 (Agent of Arcanum EnvCell)
→ 1; 0xA9B40031 (land sub-cell) → 0; 0xA9B4FFFF (landblock) → 0; null
(outdoor shell) → 0; plus the boundary cases 0x0100/0x00FF.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- UiElement: two new virtuals GetDragPayload()/GetDragGhost() (default null)
keep UiRoot item-agnostic; any leaf can opt into drag by overriding these.
- UiItemSlot: SlotIndex + SourceKind properties for payload identity; two
overrides return ItemDragPayload / icon ghost when the slot is occupied.
FindList() walks the parent chain to locate the owning UiItemList and its
registered IItemListDragHandler.
- UiItemSlot.OnEvent: MouseDown now just consumes the press; use-item fires
on Click (mouse-up) so a drag doesn't also trigger the use-item callback.
DragEnter → ask handler, set Accept/Reject overlay. DragOver → reset to
None (fires on leave). DropReleased → clear overlay + dispatch to handler
when Data0 == 1 (accepted). DragBegin consumed (source).
- OnDraw: accept/reject sprite overlay drawn last, guarded on id != 0 to
avoid the resolve(0)-→-magenta footgun.
- ToolbarControllerTests: Click_emitsUseForBoundItem changed from MouseDown
to Click to match the new dispatch.
- 12 new DragDropSpineTests pass; full suite 481/483 (2 pre-existing skips).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Integrates main's 19 commits (A7 outdoor/indoor torch lighting Fix A/B/C/D,
GlobalLightPacker, shader updates, UN-7) under the D.5 toolbar/item-model stack
(D.5.1/D.5.2/D.5.4/D.5.3a). Auto-merged cleanly except docs/ISSUES.md.
Conflict resolved: both lineages used #140 for different issues. Kept main's
#140 = "A7 Fix D" (resolved); renumbered the toolbar/selected-object issue to
#141 (note added; this branch's commits/spec still reference #140 — immutable).
The register auto-merged (AP-46 cites file:line, not #140; UN-7 keeps #140=Fix D).
Build + full suite green on the merged tree (2,713 passed / 4 skipped).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Visual gate against retail surfaced several fidelity gaps in the selected-object
strip; all fixed and user-confirmed. Faithful to gmToolbarUI::HandleSelectionChanged
(acclient_2013_pseudo_c.txt:198635) + RecvNotice_UpdateObjectHealth (:196213).
- UiMeter.DrawHBar: guard each slice on `id != 0` BEFORE resolve. resolve(0)
returns the 1x1 magenta placeholder with a non-zero GL handle, so the single-
image meter (caps id=0) was drawing 1px magenta caps at the bar's ends. The
3-slice vitals meter (all ids set) was unaffected. (the magenta-lines bug)
- SelectedObjectController: meter visibility is now UpdateHealth-driven (shown when
health is known for the selected guid — HasHealth at select or HealthChanged),
not shown-on-select; brief green selection flash via Tick revert; overlay floated
above the meter so the flash isn't hidden by the bar; name top-aligned into the
bar sprite's black band (NameBandHeight) with the bar below.
- GameWindow.IsHealthBarTarget: gate the health bar on the server PWD bits
BF_ATTACKABLE (0x10) | BF_PLAYER (0x8) — friendly/vendor NPCs and attackable
Doors (Misc type) are name-only; players/monsters get the bar. Replaces the
too-loose IsLiveCreatureTarget. Wired SelectedObjectController.Tick in OnUpdate.
- CombatState.HasHealth(guid): distinguishes a known health value from the 1.0
default, so a re-selected already-assessed target shows its bar immediately.
- TextureCache.GetOrUploadRenderSurface: resolve the surface's DefaultPaletteId
so paletted (P8/INDEX16) UI sprites decode instead of falling to magenta.
- ToolbarController.HiddenIds: also hide 0x100001A3 (stack-entry box) — retail
hides it in HandleSelectionChanged; it was rendering as a stray black box.
Divergence register: AP-47 (meter-visible timing) retired (now faithful); AP-46
rewritten to the BF_ATTACKABLE/BF_PLAYER gate approximation. Full suite green
(2,688 passed / 4 skipped). User-confirmed: name on top, NPC name-only, monster
bar on assess, green flash, no magenta.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves the divergence-register conflict: kept the accurate per-VERTEX AP-35
(Fix A shipped per-vertex; main's row was the stale pre-Fix-A per-pixel text),
kept main's UI rows AP-37..AP-42, and renumbered this branch's torch-gate row
AP-37 -> AP-43 (AP-37 was taken by main's LayoutDesc row). AP count 41 -> 42.
Retargeted the AP-37 references in WbDrawDispatcher + the CHECKPOINT to AP-43.
Marked ISSUES #140 RESOLVED (b7d655b) with the corrected root cause.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Holtburg meeting-hall facade washed out warm/bright vs retail. The round-1
checkpoint blamed torch REACH (acdream Falloff 6×1.3=7.8m vs a supposed retail
Falloff 4). That theory is WRONG, and this commit fixes the real cause.
Empirical (HoltburgTorchFalloffProbeTests, headless dat dump via the production
LightInfoLoader): the orange entrance torch (setup 0x020005D8) is raw dat
Falloff 6 and acdream reads it FAITHFULLY — there is no Falloff-4 torch anywhere
in Holtburg. Both clients read the same dat float, so reach was never inflated.
Decomp (read verbatim + corroborated by an independent adversarial workflow):
retail's per-object torch binder minimize_object_lighting (0x0054d480) is gated
in RenderDeviceD3D::DrawMeshInternal (0x0059f398) by `if (Render::useSunlight == 0)`.
The outdoor landscape stage runs useSunlightSet(1) (PView::DrawCells 0x005a485a,
before LScape::draw), so the building EXTERIOR shell — drawn via
DrawBlock→DrawSortCell→DrawBuilding→CPhysicsPart::Draw→DrawMeshInternal — is lit
by SUN + ambient ONLY; torches are SKIPPED. The static bake
(SetStaticLightingVertexColors 0x0059cfe0) is EnvCell-only. So retail NEVER
torch-lights outdoor objects. This exactly explains the isolation test (object
point lights OFF → building matches retail).
Fix: WbDrawDispatcher.ComputeEntityLightSet gates per-object torch selection on
the object being INDOOR (ParentCellId is an EnvCell, (id&0xFFFF)>=0x0100) via the
pure predicate IndoorObjectReceivesTorches. Outdoor objects (building shells with
null ParentCellId, outdoor scenery, outdoor creatures) keep the all-(-1) light
set ⇒ sun + ambient only = retail. The indoor "no sun" half is already handled by
the global sun-kill when the player is inside a cell (UpdateSunFromSky). No
dungeon regression: EnvCell statics get ParentCellId set (keep torches).
Divergence register: AP-37 (residual: acdream keys sun/torch on the object's own
cell + a per-frame player-inside sun-kill, vs retail's per-draw-stage useSunlight;
only matters for through-doorway look-ins). The round-1 CHECKPOINT got a RESOLVED
banner correcting the reach theory.
Tests: WbDrawDispatcherTorchGateTests (7), HoltburgTorchFalloffProbeTests (dat
dump). App 280/1skip, Core 1486/2skip green. Held at the visual gate — not merged.
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>
Now that the object table holds ALL entities (creatures, NPCs, world objects),
filtering ObjectAdded/Updated/Removed to the 18 shortcut guids prevents the bar
from thrashing on every creature spawn in a busy zone.
Also subscribes to ObjectRemoved so a despawned/traded-away item clears its slot
(matching retail gmToolbarUI::SetDelayedShortcutNum's deferred-bind contract).
Four new unit tests (iconIds spy pattern) verify: non-shortcut ObjectAdded/Removed
do NOT invoke Populate; shortcut ObjectAdded deferred-binds; shortcut ObjectRemoved
clears the slot. 2671 tests, 4 skipped, 0 failures.
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>
Brings Fix C (57c1135, sun-vector magnitude / ~32% over-bright) + the A7 lighting
handoff doc onto main. Auto-merged clean against the D.2b line. Merged tree builds
green; 18/18 sky tests pass. Fix A/B already on main (37911ed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Outdoor lighting was ~32% too bright (washed-out, weak shading). Live cdb on
retail (SmartBox::SetWorldAmbientLight + SkyDesc::GetLighting + LScape::sunlight,
binary matches refs/acclient.pdb) pinned it: at the SAME game time + DayGroup,
acdream's ambient COLOR matched retail exactly (the purple is correct, authored
per-time-of-day in the sky dat) but the LEVEL was 0.607 vs retail's 0.459.
level = AmbBright + 0.2·|sunVec|, both AmbBright=0.40, so acdream's |sunVec|≈1.06
vs retail's ≈0.30. Retail's LScape::sunlight read live = (0.2238, ~0, 0.00352),
magnitude 0.224 = DirBright, y≈0.
RetailSunVector had `y = cos(P)` (≈1) — the raw PRE-transform value SkyDesc::
GetLighting writes to arg5 (0x00500ac9), before LScape::set_sky_position's
world transform. acdream ported the un-transformed vector, so the y=cos(P)≈1
term inflated |sunVec| to ~1.06. That magnitude feeds BOTH the ambient boost
(SkyKeyframe.AmbientColor) AND the sun colour (SkyKeyframe.SunColor =
DirColor×|sunVec|), over-brightening the whole scene (terrain, objects, sky)
~30% and also pointing the sun the wrong way.
Fix: RetailSunVector = DirBright × (cos(P)·sin(H), cos(P)·cos(H), sin(P)) — the
world-space spherical form LScape::sunlight actually holds; |sunVec| == DirBright
for all H/P. After: acdream ambient (0.353,0.176,0.449) vs retail (0.360,0.180,
0.459) — within ~2%, user-confirmed "better outside". Sun direction also corrected
(was pointing ~North from the bad y term).
Tests updated to the cdb-verified values (the prior tests pinned the inflated
magnitude). 18/18 sky tests green. reference-retail-ambient-values memory updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Visual verification (Coldeve, Energy Crystal) showed acdream's Magical blue as a
flat tint vs retail's gradient. Root cause: RenderIcons calls the SURFACE overload
of SurfaceWindow::ReplaceColor (0x004415b0), which copies the textured effect tile
pixel-by-pixel into the icon's pure-white pixels — not the flat color->color overload
(0x00441530) I'd approximated with the tile's mean color. Port the surface overload
exactly (dst[x,y]=src[x,y] where dst==white); confirmed via clean Ghidra decompile +
named decomp. Retires AP-43 (mean-color approximation); IA-18 updated to the surface op.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Visual verification caught it: a no-mana scroll's icon edges are BLACK in retail
but rendered WHITE in acdream. Cause = the effects!=0 gate (registered AP-44) that
skipped retail's effects==0 recolor. Retail's effect tile is non-null even for
effects==0 (the 0x21 SOLID-BLACK fallback 0x060011C5), so RenderIcons recolors
pure-white pixels to black on mundane items and to the effect hue on magical ones.
Remove the gate (always recolor); retire AP-44 (now faithful). TryGetEffectColor
made internal + a golden test pins effects==0 -> ~black.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 'item with mana vs out of mana' core promise: a draining item whose
UiEffects clears to 0 returns to its base icon. Guards EnrichItem +
UpdateIntProperty unconditional-assign against a future != 0 regression.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Widen the cache key to (typeUnderlay, icon, underlay, overlay, effects).
GetIcon is now a 2-stage composite mirroring retail IconData::RenderIcons
(0x0058d180): Stage 1 builds the drag composite (base + overlay) and,
when effects != 0, ReplaceColorWhite tints it with the effect tile's
mean-opaque color (DR-1: tint SOURCE, not blit; DR-3: zero-effects
black path skipped). Stage 2 blits typeUnderlay + custom underlay +
drag into the final cached GL texture.
Both callers updated: ToolbarController Func arity widened to 6-arg
(passes item.Effects); GameWindow closure and OnLiveEntitySpawned
EnrichItem call pass spawn.UiEffects. Tree builds with 0 warnings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ReplaceColorWhite (retail SurfaceWindow::ReplaceColor 0x00441530):
replaces only pure-white-opaque (RGBA 255,255,255,255) pixels in place.
TryGetEffectColor: resolves the effect tile DID via ResolveEffectDid,
decodes the RenderSurface, and returns the mean-opaque RGB as the tint
color (divergence DR-2: exact retail color byte is decompiler-ambiguous).
TryDecode: shared RenderSurface decode helper for the effect path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add effect-overlay submap resolve: EnsureEffectSubMap walks the portal
MasterMap (0x25000000) → EnumIDMap 0x10000005 → submap 0x25000009;
ResolveEffectDid(effects) maps LowestSetBit(effects)+1 → RenderSurface
DID with fallback to index 0x21. Golden test validates all 6 cases
(Magical/Poisoned/BoostHealth/BoostStamina/Nether/zero) against the
live dat. Retail ref: IconData::RenderIcons 0x0058d180.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>