9.2 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.
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.