acdream/docs/research/2026-07-17-retail-world-selection-pseudocode.md

9.2 KiB
Raw Blame History

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 @ 0x005397E0 returns the drawing BSP root sphere exactly; the DAT source is GfxObj.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.