feat(ui): port retail selected-stack quantity

Bind the authored stack count entry and horizontal slider to one Core split-quantity owner, preserve retail count-first naming and exact 1000-step rounding, refresh on stack changes, and consume the selected amount during merges. Conformance covers the production DAT fixture and retained pointer/focus paths.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 10:07:25 +02:00
parent dc1649c493
commit a20e5c68c7
19 changed files with 595 additions and 51 deletions

View file

@ -463,10 +463,10 @@ AttemptMerge(sourceId, targetId, quiet): // 0x005878F0
return true
```
acdream's selected-object stack entry/slider is still absent under AP-101, so the
current inventory merge path requests the whole source stack and lets the same
retail capacity clamp limit the transfer. Once the shared split-quantity owner
lands, it supplies `requested` without changing legality or wire ordering.
acdream now has one Core-owned split-quantity state shared by the selected-object
entry/slider and inventory merge. A selected source supplies that quantity to
`AttemptMerge`; an unselected source retains retail's full-source default. Legality,
capacity clamping, and wire ordering are unchanged.
---
@ -507,7 +507,7 @@ HandleSelectionChanged(): // 0x004BF380
if object missing: return
set selected name:
normal object: name, or formatted name + count when stacked
normal object: name, or `"%d %hs"` (count then name) when stacked
owned coinstack: formatted coin total/name path
splitSize = 1
@ -567,13 +567,22 @@ RecvNotice_UpdateItemMana(id, fraction, valid): // 0x004BD0C0
- On entry deactivation, parse unsigned text, clamp to `1..maxSplitSize`, rewrite
the text if clamped, update `GenItemHolder::splitSize`, update slider attribute
`0x86 = splitSize/maxSplitSize`, and broadcast the stack-slider notice.
- Slider message `0xA` maps its normalized value to an integer in
`1..maxSplitSize`, stores it, rewrites the entry, and broadcasts the notice.
- `UIElement_Scrollbar::SetScrollbarPosition @ 0x00470EC0` clamps the normalized
position to `[0,1]`, multiplies by `1000.0f`, truncates through `_ftol2`, and
broadcasts that integer as message `0xA` parameter 1.
- `gmToolbarUI::ListenToElementMessage @ 0x004BEFE0..0x004BF041` converts the
message exactly as follows, stores it, rewrites the entry, and broadcasts the
stack-slider notice:
**Unresolved:** Binary Ninja lost the exact floating expression immediately before
the slider's `1 - _ftol2()` conversion at `0x004BF02D`. The clamp, endpoints, and
text update are exact, but the intermediate rounding direction must be recovered
from Ghidra/disassembly before writing conformance vectors.
```text
positionMillis = truncate(clamp01(sliderRatio) * 1000)
splitSize = clamp(1 + floor(positionMillis * maxSplitSize / 1000),
1, maxSplitSize)
```
The apparently odd extra one is verbatim retail behavior. Static cdb disassembly
of the matching v11.4186 binary also pins the stack-name format string at
`0x007B4738` to `"%d %hs"` and the numeric-entry format at `0x007A0184` to `"%d"`.
---