Port UIElement_ItemList's physical-item right-click branch through the shared retained list. Select and appraise backpack, loot, paperdoll, and shortcut items through their canonical owners, while preventing RMB movement from lifting items or issuing appraisal requests. Co-authored-by: Codex <codex@openai.com>
314 lines
11 KiB
Markdown
314 lines
11 KiB
Markdown
# Retail world selection and vivid target indicator
|
||
|
||
Oracle: September 2013 EoR named retail client. Cross-checks: the extracted
|
||
WorldBuilder mesh path (DAT geometry and current part transforms) and ACE object
|
||
identity/qualities. Neither reference implementation supplies the client picker;
|
||
the named retail functions below are authoritative.
|
||
|
||
## Mouse selection
|
||
|
||
`SmartBox::find_object @ 0x00451C60`
|
||
|
||
```text
|
||
remember the clicked viewport pixel
|
||
clear the previous per-frame selection accumulator
|
||
arm mouse selection for the next world draw
|
||
```
|
||
|
||
`CPhysicsPart::Draw @ 0x0050D7A0`, `RenderDeviceD3D::DrawMesh @ 0x005A0860`,
|
||
and `Render::GfxObjUnderSelectionRay @ 0x0054C740`
|
||
|
||
```text
|
||
for each physics part which reaches the normal visible-mesh draw path:
|
||
reject parts which do not belong to a server object
|
||
transform the one world selection ray into this part's local coordinates
|
||
divide by the part's GfxObj scale (do not change the ray parameter units)
|
||
|
||
sphere = gfxObj.drawing_sphere
|
||
if sphere does not intersect the local ray:
|
||
continue
|
||
|
||
if a polygon has already won globally and sphereDistance is farther:
|
||
continue
|
||
|
||
if sphereDistance is nearer than the current sphere fallback:
|
||
remember this object's id, part index, and sphereDistance
|
||
|
||
for polygons in the GfxObj's stored flat DAT order:
|
||
if polygon_hits_ray(polygon, localRay, out t):
|
||
remember only this FIRST polygon hit for this part
|
||
if t is nearer than the current global polygon winner:
|
||
remember this object's id, part index, and t
|
||
break
|
||
```
|
||
|
||
`CPolygon::polygon_hits_ray @ 0x005395E0`
|
||
|
||
```text
|
||
if polygon.sides_type == ST_SINGLE (raw value 0)
|
||
and dot(ray.direction, polygon.plane.normal) > 0:
|
||
miss // back face
|
||
|
||
denominator = dot(ray.direction, polygon.plane.normal)
|
||
if abs(denominator) < 0.0002:
|
||
miss // parallel to the plane
|
||
|
||
t = plane/ray intersection time
|
||
if t < 0:
|
||
miss // behind the viewer
|
||
|
||
point = ray.origin + t * ray.direction
|
||
normal = normalize(sum(cross(vertex[i] - vertex[0],
|
||
vertex[i+1] - vertex[0])))
|
||
for each ordered edge (previous -> current):
|
||
inward = cross(normal, current - previous)
|
||
if dot(point - previous, inward) < 0:
|
||
miss
|
||
return hit
|
||
```
|
||
|
||
The broad-phase helper is retail `CSphere::sphere_intersects_ray @
|
||
0x005377A0`: it rejects rays beginning in/on the sphere, requires squared ray
|
||
direction length at least `0.0002`, solves the quadratic in double precision,
|
||
and chooses the near root when non-negative or the far root otherwise.
|
||
The ray origin is `Render::viewpoint` (the camera eye), not the unprojected
|
||
near-plane point; `Render::pick_ray @ 0x0054B610` supplies only its direction.
|
||
|
||
`Render::GetMouseSelectionObjectID @ 0x0054C950`
|
||
|
||
```text
|
||
if any polygon hit exists anywhere in the visible draw:
|
||
return the closest polygon winner
|
||
if any drawing-sphere hit exists:
|
||
return the closest sphere fallback
|
||
return no object
|
||
```
|
||
|
||
Important consequences:
|
||
|
||
- The broad phase is each visible **GfxObj part's drawing sphere**, not a Setup
|
||
selection sphere, physics collision shape, fixed creature hit box, or expanded
|
||
screen rectangle.
|
||
- A polygon hit on any visible part beats every sphere-only fallback.
|
||
- Retail does not ray-test world-cell walls as a second, independent occluder.
|
||
Occlusion is inherited from the same portal/viewcone draw traversal that decides
|
||
which parts may call `GfxObjUnderSelectionRay`.
|
||
- `BSPTREE::GetSphere @ 0x005397E0` returns the drawing BSP root sphere exactly;
|
||
the DAT source is `GfxObj.DrawingBSP.Root.BoundingSphere`.
|
||
|
||
## Vivid target indicator
|
||
|
||
`VividTargetIndicator::SetSelected @ 0x004F5CE0`
|
||
|
||
```text
|
||
if selected id is the player: clear it
|
||
if object is player-owned or IN_CONTAINER: clear it
|
||
publish the remaining id to SmartBox
|
||
if enabled and an id remains:
|
||
color = gmRadarUI::GetBlipColor(selected id)
|
||
else:
|
||
hide both indicator roots
|
||
```
|
||
|
||
`VividTargetIndicator::Initialized @ 0x004F6C60`
|
||
|
||
```text
|
||
onscreenRoot = child 0x10000038
|
||
corners = children 0x10000039, 0x1000003A, 0x1000003B, 0x1000003C
|
||
offscreenRoot = child 0x10000045
|
||
```
|
||
|
||
The constructor resolves twelve source surfaces by calling `DBObj::GetByEnum`
|
||
with RenderSurface DBO type `0x0C`, client-enum category `0x10000009`, and
|
||
enum values `1..12`. Values `1..4` are the four on-screen corners.
|
||
`CopyImage @ 0x004F5DD0`
|
||
uses the active retail blit mode (`s_BlitMethod == 4`) to colorize the source
|
||
art with the same color as the object's radar blip.
|
||
|
||
`SmartBox::GetObjectBoundingBox @ 0x00452E20`
|
||
|
||
```text
|
||
resolve the selected id directly through CObjectMaint::GetObjectA
|
||
if the object is absent:
|
||
return ObjectNotFound
|
||
|
||
reset to Render::set_default_view
|
||
push the object's position
|
||
obtain CPhysicsObj::GetSelectionSphere
|
||
if no sphere exists:
|
||
use an origin-centered sphere with radius 0.1 world units
|
||
|
||
if Render::viewconeCheck says the sphere intersects the full viewport cone:
|
||
project Render::GetViewerBBox's two corners
|
||
return ObjectOnscreen and the resulting screen rectangle
|
||
|
||
target = normalized selection-sphere center in viewer-local coordinates
|
||
if target is in front of the viewer:
|
||
angle = (450 - degrees(atan2(target.screenRight, target.screenUp))) mod 360
|
||
else:
|
||
angle = (450 - degrees(atan2(target.screenRight, 0))) mod 360
|
||
return ObjectOffscreen and angle
|
||
```
|
||
|
||
This path has no object-distance test and does not ask whether the selected
|
||
object participated in the visible world draw. `set_default_view` also removes
|
||
the per-portal view cone before the test. Consequently, a live selected object
|
||
keeps its four corners through walls and at any distance for which it remains in
|
||
the client object table.
|
||
|
||
`VividTargetIndicator::OnDraw @ 0x004F62B0`
|
||
|
||
```text
|
||
if disabled, display-off, or selected id is zero:
|
||
hide both roots
|
||
else if status is ObjectOnscreen:
|
||
colorize source images 1..4 with the selected radar-blip color
|
||
use the actual DAT corner width and height
|
||
place the corners just outside the SmartBox screen rectangle
|
||
clamp the assembled indicator to an 8-pixel viewport margin
|
||
show the on-screen root and hide the off-screen root
|
||
else if status is ObjectOffscreen:
|
||
choose one authored source image by angle:
|
||
[338,360) or [0,23) -> 6
|
||
[23,68) -> 7
|
||
[68,113) -> 9
|
||
[113,158) -> 12
|
||
[158,203) -> 11
|
||
[203,248) -> 10
|
||
[248,293) -> 8
|
||
[293,338) -> 5
|
||
colorize it with the selected radar-blip color
|
||
intersect the angle ray from viewport center with the screen edge
|
||
center the image on that intersection
|
||
clamp its top-left to the 8-pixel viewport margin
|
||
hide the on-screen root and show the off-screen image
|
||
```
|
||
|
||
Installed retail DAT verification pins images 1..4 at 12×12 pixels and images
|
||
5..12 at 24×24 pixels. The larger off-screen marker is authored art, not a
|
||
procedural arrow. The indicator is persistent selection presentation. Retail
|
||
also has a separate, short-lived click confirmation owned by SmartBox; it is not
|
||
implemented in `CPhysicsPart::Draw` or `ACCWeenieObject::SetSelected`.
|
||
|
||
## Right-click selection and examination
|
||
|
||
`UIElement_SmartBoxWrapper::MouseUp @ 0x004E5820`
|
||
|
||
```text
|
||
on right mouse up:
|
||
if mouse movement owns the gesture:
|
||
finish mouse movement and do not examine
|
||
else if instant mouse-look owns the gesture:
|
||
leave mouse-look and do not examine
|
||
else if no higher-priority SmartBox search is pending:
|
||
searchReason = sr_Examine
|
||
FindObject(releaseX, releaseY)
|
||
```
|
||
|
||
`UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound @ 0x004E5AD0`
|
||
|
||
```text
|
||
if a valid object was found for sr_Examine:
|
||
begin the normal SmartBox lighting pulse
|
||
SetSelectedObject(foundObjectId)
|
||
ClientUISystem.ExamineObject(foundObjectId)
|
||
else:
|
||
do nothing
|
||
```
|
||
|
||
The action is release-completed rather than mouse-down-completed so an RMB
|
||
camera drag does not appraise the object under the release point. Acdream maps
|
||
the configurable `SelectRight` action to the same complete-click gesture and
|
||
keeps `SelectionExamine` independently configurable (retail default E).
|
||
|
||
### Retained physical-item lists
|
||
|
||
`UIElement_ItemList::ListenToElementMessage @ 0x004E4D50`
|
||
|
||
```text
|
||
on ItemList right-click:
|
||
item = UIItem under the mouse
|
||
if item.itemID != 0:
|
||
if this list uses single selection:
|
||
update the list selection
|
||
SetSelectedObject(item.itemID)
|
||
ClientUISystem.ExamineObject(item.itemID)
|
||
else if item.spellID != 0:
|
||
ClientUISystem.ExamineSpell(item.spellID)
|
||
```
|
||
|
||
`gmPaperDollUI::ListenToElementMessage @ 0x004A5C30` repeats the physical-item
|
||
branch for a worn item resolved through the authored paperdoll click map:
|
||
|
||
```text
|
||
on paperdoll right-click:
|
||
itemID = GetPaperDollItemUnderMouse(mouse)
|
||
if itemID != 0:
|
||
SetSelectedObject(itemID)
|
||
ClientUISystem.ExamineObject(itemID)
|
||
```
|
||
|
||
Acdream's shared retained `UiItemList` owns the corresponding physical-item
|
||
notice. Backpack, side-bag, external-container, paperdoll-slot, and physical
|
||
toolbar cells provide their selection source and route the request through the
|
||
same `ItemInteractionController` appraisal owner used by world SmartBox and
|
||
the toolbar magnifying glass. RMB motion beyond the shared three-pixel click
|
||
threshold neither appraises nor begins an item drag.
|
||
|
||
## SmartBox click lighting pulse
|
||
|
||
`UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound @ 0x004E5AD0`
|
||
|
||
```text
|
||
when FindObject succeeds for Select, Examine, Use, Drop, Drag, or TargetedUse:
|
||
if another object is currently pulsing:
|
||
RestoreLighting(previous object)
|
||
SetLighting(found object, luminosity=0.99, diffuse=1.0) // HIGH
|
||
flipCount = 1
|
||
nextFlip = Timer::cur_time + 0.2 seconds
|
||
|
||
continue with the reason-specific select/examine/use/drop behavior
|
||
```
|
||
|
||
`UIElement_SmartBoxWrapper::Global_Loop @ 0x004E5620`
|
||
|
||
```text
|
||
if flipCount != 0 and Timer::cur_time >= nextFlip:
|
||
flipCount += 1
|
||
if flipCount < 5:
|
||
lightingMode = ((flipCount & 1) != 0) + 1
|
||
nextFlip = Timer::cur_time + 0.2 seconds
|
||
else:
|
||
flipCount = 0
|
||
lightingMode = RESTORE
|
||
ApplyLighting(pulsing object, lightingMode)
|
||
```
|
||
|
||
`UIElement_SmartBoxWrapper::ApplyLighting @ 0x004E5320`
|
||
|
||
```text
|
||
RESTORE: CPhysicsObj::RestoreLighting(object)
|
||
LOW: CPhysicsObj::SetLighting(object, luminosity=0.0, diffuse=0.35)
|
||
HIGH: CPhysicsObj::SetLighting(object, luminosity=0.99, diffuse=1.0)
|
||
```
|
||
|
||
The complete cadence is therefore HIGH immediately, LOW at 0.2 seconds, HIGH
|
||
at 0.4, LOW at 0.6, and authored material lighting restored at 0.8. A delayed
|
||
frame advances only one step because retail schedules the next flip from the
|
||
current `Timer::cur_time`; it does not drain missed intervals.
|
||
|
||
`CPhysicsObj::SetLighting @ 0x00511A80` forwards to every physics part through
|
||
`CPartArray::SetLightingInternal @ 0x00518490`. `CPhysicsPart::SetLighting @
|
||
0x0050E400` copies the material when needed, then calls
|
||
`CMaterial::SetLuminositySimple` followed by `CMaterial::SetDiffuseSimple`.
|
||
The modern renderer's equivalent is an instance-scoped material replacement:
|
||
|
||
```text
|
||
lit = luminosity + diffuse * sceneLighting
|
||
rgb = sampledTexture.rgb * clamp(lit, 0, 1)
|
||
```
|
||
|
||
Normal/restored rendering uses `(luminosity=0, diffuse=1)` in the current object
|
||
shader. The pulse is distinct from the persistent four-corner indicator and
|
||
must restart on every successfully resolved SmartBox click, even when the click
|
||
is consumed by targeted use instead of changing the selected object.
|