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>
11 KiB
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
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
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
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
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 @ 0x005397E0returns the drawing BSP root sphere exactly; the DAT source isGfxObj.DrawingBSP.Root.BoundingSphere.
Vivid target indicator
VividTargetIndicator::SetSelected @ 0x004F5CE0
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
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
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
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
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
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
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:
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
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
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
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:
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.