Targeting & interactions
Screen picking and cursor utilities
`Open77.camera.screenRay`, `screenRaycast` and `Open77.input.cursor`: resolve the entity under a screen point through the native physics owner, with the identity kinds a client can and cannot promise.
On this page6 sections
Use client-side screen-picking and cursor APIs to select a world point or entity from a 2D interface. Check API availability when supporting clients without these helpers.
For a ready-made ALT/click interface, use open77_contextmenu.
Coordinates and permissions
Coordinates are normalized viewport coordinates: (0,0) is top-left,
(1,1) bottom-right and (.5,.5) the centre. Do not pass desktop pixels,
CEF backing-texture pixels or engine NDC coordinates. Browser clientX/innerWidth
and clientY/innerHeight follow this convention on a fullscreen surface.
Open77.camera.screenRay and screenRaycast require world.query.
Open77.input.cursor requires input.actions. These queries do not exist on
the dedicated server. They return nil, reason when unavailable or invalid.
Screen2DPointTo3DSpace / screenRay
local ray, reason = Open77.camera.screenRay(0.5, 0.5, 25)
-- Identical global alias:
local sameRay = Screen2DPointTo3DSpace(0.5, 0.5, 25)
Returns { origin={x,y,z}, direction={x,y,z}, position={x,y,z}, maxDistance }.
The origin is the active camera, direction is a unit world-space vector, and
position is the point origin + direction * maxDistance. This is not a hit:
unprojecting a pixel defines a ray, not a unique surface position.
Distance defaults to 100 metres and must be finite, within [0.1,1000].
Both screen coordinates must be finite numbers within [0,1]. Strings are not
coerced. The native projection is checked against the unprojected ray; inconsistent
camera state returns camera_projection_mismatch rather than a guessed direction.
screenRaycast
local hit, reason = Open77.camera.screenRaycast(0.5, 0.5, 12, {
static = true, dynamic = true, self = true -- Optional visible F7 body picking.
})
if not hit then print(reason); return end
if not hit.hit then return end
print(hit.position.x, hit.distance, hit.material)
if hit.target then
print(hit.target.kind, hit.target.engineEntity, hit.target.vehicleId)
end
Uses the same arguments as screenRay, plus optional boolean collision filters.
Both filters default to true; at least one must remain enabled. It selects the
nearest blocking physical hit, not the nearest object to the cursor. Static,
dynamic, terrain, collider, destructible, vehicle and NPC-hitbox categories are
included; the two options restrict actor mobility, not the target's Lua kind.
Query-only player sensors are excluded.
self is a third strict boolean, false by default. With self=true and
dynamic=true, the active visible F7 body is also tested using capsules around
its native animated body slots. This presentation object intentionally has no
physical colliders. A closer physical hit wins; hidden/suspended proxies are
excluded. No colliders, actor movement, animation or gameplay state are changed.
This is a local UI picking approximation, not mesh-perfect selection, combat
collision or a hidden first-person self button. It requires the client with
context-menu 1.1.0 support, beyond the first screen-query implementation.
All ray fields remain present. A miss has hit=false and position at the
maximum distance. A hit additionally returns:
| Field | Meaning |
|---|---|
position, normal |
World-space impact and normal. |
distance |
Camera-to-impact distance in metres. |
material |
Engine material name, possibly empty. |
entityLookupAvailable |
Whether the native physics-owner resolver is available. |
target |
Entity descriptor, or nil for geometry without an entity owner. |
hitSource |
self_presentation for an opt-in F7 visual hit; absent for ordinary physics. |
An F7 visual hit has hit=true, distance, position and target, but no physical
normal or material. Its target.kind is player, isLocalPlayer=true, and
playerId is the current local network player. Use the canonical player ID for
resource requests, not the F7 proxy's ephemeral engine handle.
The descriptor contains engineEntity as a decimal string, className,
kind and networked. Supported classifications include player, vehicle, NPC,
prop, door, device, item and object. Recognized network entities additionally
carry the corresponding playerId, vehicleId, npcId or propId.
propId is a decimal string, matching the props API; the other canonical IDs
are Lua integers. An NPC's record, when available, is its actual Character.*
record, not the virtual template label.
entity, when present, is an Open77 local registry handle encoded as a string.
Names/records are optional; isLocalPlayer=true identifies the local body,
including its F7 presentation proxy when hit.
Do not interchange these identifiers. Engine and registry IDs refer to this
client's streamed incarnation, not a portable network target. Keep their strings
intact through JavaScript; converting a 64-bit ID to Number can corrupt it.
Static world geometry can legitimately have no entity. Visual effects and props without physics collision (apart from the explicit F7 option) are not promised as pickable targets. There is no nearest-neighbour fallback, no through-wall selection and no authority granted by a successful client raycast.
Cursor and WebUI focus
local cursor, reason = Open77.input.cursor()
-- { x, y, pixelX, pixelY, width, height, inBounds, captured }
if cursor and cursor.inBounds then
local hit = Open77.camera.screenRaycast(cursor.x, cursor.y, 12)
end
page:show()
page:setFocus(true, true) -- Keyboard + cursor; game input blocked by default.
local ownsFocus = page:hasFocus()
page:setFocus(false, false)
page:hide()
cursor() samples the actual foreground game window, not an arbitrary desktop
window. It returns nil, game_not_focused after Alt+Tab; coordinates can be outside
[0,1] if the pointer is outside the client area (inBounds=false). It does not
show or capture the cursor itself. Use a resource-owned WebUI surface for that.
captured is true if a WebUI already owns keyboard or cursor input; check
it before opening a new modal so cursor-only interfaces are not interrupted.
WebUI has a virtual pointer: for an exact click on its fullscreen page, pass
event.clientX / innerWidth and event.clientY / innerHeight from that click,
instead of sampling the OS cursor after the message arrives.
page:hasFocus() reads current keyboard/cursor ownership and presentation state.
It does not steal focus, and is false when another surface owns input. Releasing
focus on your page must not release another page's focus. Surface teardown
automatically releases its capture.
Disable the native weapon wheel
-- Client, permission: input.actions. Reserve before ALT is pressed.
local ok, reason = Open77.input.setNativeActionBlocked("WeaponWheel", true)
-- Release only this resource's claim when your feature is disabled:
Open77.input.setNativeActionBlocked("WeaponWheel", false)
The native weapon-wheel state cannot enter while blocked. An already-open wheel
takes its normal cancellation/cleanup path, without selecting an item. Claims
from several resources compose: releasing yours never overrides another owner.
The host releases claims on stop, reload and disconnect. OpenMapMenu is also
supported; unknown names return false, "unsupported_native_action".
This blocks the native action, including rebound keys and gamepad, not a physical
ALT key or every weapon command. SwitchItem and other radial menus are unchanged.
Combine it with a focused WebUI to consume gameplay input during your own menu.
Cost and trust
Trace on a click or at a bounded rate while interacting. Do not scan the entire world each frame. These are synchronous game-thread queries, not a server physics engine. Server effects must validate authenticated source, canonical IDs, bucket, distance, life state, cooldown and gameplay permissions independently.
Common errors: permission_denied:world.query, permission_denied:input.actions,
screen_query_unavailable_on_this_host, invalid_screen_coordinates_or_distance,
invalid_collision_options, camera_unavailable, camera_projection_mismatch,
game_not_focused, cursor_unavailable, viewport_unavailable.
// For agents & LLMsthis page as Markdownllms.txtllms-full.txt
