249 lines
9.2 KiB
Markdown
249 lines
9.2 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`.
|
||
|
||
## 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.
|