Quick navigation

Go to

Map, markers & zones

World-anchored POIs

The open77_worldui facade: a ground marker and its interaction prompt created and destroyed as one owned handle, the definition reference, and the invisible-ring trap it exists to avoid.

4 min read853 wordsMarkdown
On this page7 sections

open77_worldui combines a native ground marker and an interaction prompt under one resource-owned handle. Creation and cleanup cover both elements.

It is mode-agnostic: nothing in its API names a role, a match, a team, or any other gamemode-shaped concept, and both official consumers (resources/gamemodes/pursuit and resources/gamemodes/race) call it through the same three functions below with no special-casing on either side.

Source: open77_worldui.

Add the dependency

resource "hideseek"
version "1.0.0"
dependency "open77_worldui >=0.1.0"

client_script "client/main.lua"

world.markers is not required by the caller -- open77_worldui holds that permission itself and is the presentation boundary precisely so a gamemode resource does not need it (see docs/writing-a-gamemode.md section 1, point 3, "least privilege"). A caller that also wants open77_interactions directly, bypassing this facade, needs its own dependency and permission for that -- this service does not re-export open77_interactions' surface.

Create a POI

local handle

CreateThread(function()
    local promise, callError = Open77.exports.call("open77_worldui", "create", {
        id = "start_line",
        position = { x = -1460.2, y = 99.9, z = 24.8 },
        radius = 2.5,
        style = "objective",
        maxDistance = 90.0,
        label = "Join the race",
        description = "Hold to line up at the start.",
        key = "E",
        holdSeconds = 1.0,
        color = "#00E5FF",
        event = "hideseek:startLineSelected",
    })
    assert(promise, callError)
    local result, awaitError = promise:await()
    assert(result and result.ok, awaitError or (result and result.error))
    handle = result.handle
end)

AddEventHandler("hideseek:startLineSelected", function()
    TriggerServerEvent("hideseek:joinIntent")
end)

label is optional. Omitting it creates a marker with no interaction prompt -- a pure ground/waypoint ring with nothing to press E on. This is the right shape for a checkpoint or a waypoint a player only needs to see, not act on; resources/gamemodes/race's course markers use exactly this form.

Definition reference

Field Meaning
id Owner-local identifier, 1-64 characters. Required.
position Static { x, y, z } ground point. Required.
style One of the four fixed native presets: interaction, objective, spawn, danger. Defaults to objective.
shape ring (default) or cylinder.
radius Marker radius, clamped to 0.1-50.0 m; defaults to 1.5.
maxDistance Marker visibility distance, clamped to 1.0-500.0 m; defaults to 80.0.
groundOffset Metres the marker is lifted off position.z, 0.0-2.0; defaults to 0.06. A ring flattened to the ground plane at exactly floor height is co-planar with the floor and renders nothing visible even though the handle and the native registry both report success -- see "A trap this service exists to avoid" below.
label, description Prompt copy. Presence of label is what creates the prompt half at all.
key, holdSeconds, icon, color Prompt affordance, passed straight through to open77_interactions.
marker, animated, focusRadius, requireLookAt, promptDistance, labelHeight Prompt presentation fields; see Contextual interactions for their meaning and ranges.
event Local event fired when the prompt's choice is selected. Required if label is set.

Radius and maxDistance are clamped here rather than left to fail against the native marker's own tighter bounds, so a caller gets a POI instead of an opaque native refusal.

The prompt half's markerScale and markerNearScale are not configurable through this facade -- they are fixed at 1.0 and 1.6 regardless of what definition carries. A caller that needs different prompt-marker scaling has to create the interaction directly through open77_interactions instead of through this service.

Update, list, remove

local function call(name, ...)
    local promise, reason = Open77.exports.call("open77_worldui", name, ...)
    assert(promise, reason)
    return promise:await()
end

local owned = call("list").pois       -- { { handle, id, position }, ... }
assert(call("remove", handle).ok)

There is no update export. Remove and recreate under the same id to change a POI's geometry or copy -- the same convention as open77_zones.

dump() is a diagnostic, not part of the steady-state API: it reads the native marker registry through this resource's world.markers permission (a caller without that permission cannot see the registry at all) and logs one line per marker, including the native rendered flag. Use it when a POI is not visually appearing to separate "the Lua handle exists" from "the REDengine effect is actually alive" -- the two failure modes look identical from Lua otherwise, and rendered=true has been measured to mislead on its own (see docs/gamemode-pursuit-plan.md section 7d).

Ownership and cleanup

Ownership comes from GetInvokingResource(), never from an argument. remove and list are scoped to the caller; a handle belonging to another resource is refused with not_owner. A caller's POIs (both the marker and the prompt half) are removed automatically when its resource generation stops or reloads. A limit of 128 POIs applies across every resource combined.

Surviving open77_interactions restarting underneath you

open77_interactions declares reload_policy "reconnect" (Contextual interactions), so any change to the session's resource set restarts it, and on restart it drops every registered entry. A POI's marker half survives (it belongs to open77_worldui's own generation), but the prompt half silently vanishes, leaving a visible ring with nothing to interact with.

open77_worldui watches open77_interactions' generation once a second and re-registers every prompt it is holding a promptSpec for when that generation changes. A caller does not need to do anything for this -- it is part of why the facade exists rather than every gamemode composing the two services itself.

A trap this service exists to avoid

A marker's ring mesh is flattened to ~0.04 m. Placed at exactly position.z with no lift, it sits co-planar with the ground mesh: the native entity spawns, the effect scales, the registry reports rendered = true -- and nothing is visible, because the ring is inside the floor geometry. groundOffset (default 0.06 m) exists specifically to keep a caller from rediscovering this. If a POI is invisible despite dump() reporting rendered = true, suspect a groundOffset of 0 before anything else.

// For agents & LLMsthis page as Markdownllms.txtllms-full.txt

// Guides Β· Lua API

Search the docs

1,905 guides and functions indexed

Try