Quick navigation

Go to

Players & movement

Players around you (client)

Enumerate the other players a client can already see β€” ids, bodies, distances and the nearest one β€” with no permission at all, and why a player id does not outlive a body.

5 min read1,174 wordsMarkdown
On this page5 sections

Open77.players reads the identities, positions and distances of players rendered by the client. These client-side queries require no permission.

resource "proximity_demo"
version "1.0.0"
client_script "client/main.lua"
-- no permissions block: the enumeration below is ungated

Player IDs and entity handles

This is a streaming-scoped view, not the server roster. It holds the players this client has been told about β€” the same roster that draws the nameplates β€” and nothing more. A player the server counts as connected can be missing from it for a long time: a different routing bucket, too far to stream, a proxy body still spawning, a world transition in progress. The list is also per-client, so two players standing together will not necessarily see the same one.

So:

Question Ask
Who is standing near me, and where is his body? Open77.players on the client
How many players are on this server? Is this id on the whitelist? Who gets the payout? the server, over a net callback

A resource that treats #Open77.players.all() as a head count writes a race that a quiet test server will never reproduce and a busy one will hit within the hour. The client cannot be made authoritative about a roster it only receives a slice of; the fix is always to ask the server.

The API

Function Answers
Open77.players.all(options?) { playerId, ... }, ascending, the local player included
Open77.players.localId() your own player id
Open77.players.entity(playerId) the entity id of that player's body
Open77.players.fromEntity(entityId) which player wears that body
Open77.players.nearby(radius, options?) everyone with a body within radius, nearest first
Open77.players.closest(options?) just the nearest one

Failures follow the repository convention: nil, reason, with stable snake_case tokens. The reason matters here more than usual, because "he has no body yet" and "I have never heard of him" are different facts and a proximity resource has to act differently on each.

localId() β€” and why it is not local()

local is a reserved word in Lua, so Open77.players.local() is a syntax error: the parser refuses the name before anything runs. The function is registered under both spellings, so

Open77.players.localId()      -- write this
Open77.players["local"]()     -- identical: the same function value

Both return your player id, or nil, "no_session" when you are not connected to a server.

all(options?)

for _, id in ipairs(Open77.players.all()) do
    print(id, id == Open77.players.localId() and "(me)" or "")
end

-- only the ones that actually have a body right now
local visible = Open77.players.all({ streamed = true })

Ascending by player id, so two calls in the same frame compare cleanly. Your own id is in the list, matching FiveM's GetActivePlayers; filter it out yourself if you do not want it.

A player who is connected but has no body β€” his proxy has not spawned, or it was retired for a death, a re-dress or a world transition β€” stays in the list with no entity. Dropping him would make "not streamed yet" indistinguishable from "left the server", which is exactly the distinction this API exists to preserve. { streamed = true } is how you ask for only the bodied ones; it is the equivalent of FiveM's NetworkIsPlayerActive filter.

A player who leaves is gone from the list immediately, and so is the id of a body that was retired: see Ids do not outlive bodies.

entity(playerId) and fromEntity(entityId)

The two directions of the mapping. Feed the entity into any Open77 entity API β€” Open77.character.state(entity) first among them β€” for the transform, stance, weapon and vehicle. For the same body's own axes β€” a point two metres in front of it, or where its head is β€” see an entity's own axes.

local id = Open77.players.localId()
local entity = Open77.players.entity(id)
local state = Open77.character.state(entity)
print(("%s is at %.1f, %.1f"):format(id, state.position.x, state.position.y))

entity() accepts the player id as a number or as a decimal string. It returns:

Result Meaning
an entity id that player has a body, and this addresses it
nil, "player_not_streamed" he is here, he has no body β€” do not retry in a loop, wait for him
nil, "unknown_player" this client has never heard of that id
nil, "invalid_player" the argument was not an id at all

Your own body is the entity id 0. That is not a failure value: 0 is the reserved id that every Open77 entity API reads as "the local player", which is why Open77.character.state() with no argument and Open77.character.state(Open77.players.entity(Open77.players.localId())) are the same call. fromEntity(0) gives your player id back.

fromEntity() takes the entity as a number or a decimal string β€” the string form is what a raycast hit or the older Open77.animations._context() map hands out β€” and answers nil, "unknown_entity" for anything that is not a live player body right now.

nearby(radius, options?) and closest(options?)

Centred on your own body, sorted by ascending distance, ties broken by player id so that "closest" does not flicker between two bodies standing on the same spot.

for _, player in ipairs(Open77.players.nearby(15.0)) do
    print(("%s at %.1f m"):format(player.name, player.distance))
end

local target = Open77.players.closest({ radius = 3.0 })
if target then
    print(("you are next to %s"):format(target.name))
end

Each entry is:

Field
playerId the network id
entity the body, ready for Open77.character.state
distance metres from the origin
position world position β€” a plain { x, y, z } table, which is a vector3
name display name
isLocal true only for your own entry, and only if you asked for it

Options, shared by both:

Option Default
origin your own body measure from somewhere else β€” a raycast hit, a door, a marker
includeSelf false put your own entry in the result
limit unlimited nearby only, 1..256, applied after sorting
radius unlimited closest only; nearby takes it as its first argument

Because position is a plain x/y/z table, it is already a vector everywhere a vector is accepted:

local a, b = Open77.players.nearby(50)[1], Open77.players.nearby(50)[2]
print(#(vector3(a.position) - vector3(b.position)))   -- metres between them

Failure reasons:

Reason Meaning
no_session not connected to a server
no_local_position you are connected but have no body β€” the "continue" screen, or a world transition β€” so there is no centre to measure from. Pass origin to ask anyway
no_player_in_range closest only: nobody qualified. nearby returns an empty table instead
invalid_radius / invalid_origin / invalid_limit the argument was malformed

Players without a body are skipped by both: they have no position to sort by. Use all() if you need to know they exist.

Ids do not outlive bodies

A proxy body is destroyed and recreated more often than players expect β€” on death, on a clothing change, on a world transition. When it goes, the entity id goes with it:

local entity = Open77.players.entity(otherPlayer)   -- 12884901889
-- ... he dies and respawns ...
Open77.players.fromEntity(entity)                   --> nil, "unknown_entity"
Open77.players.entity(otherPlayer)                  --> a different id

Cache the player id, never the entity id. The player id is stable for the whole session; the entity id is a handle on one particular body. Resolving it again is one table lookup, so there is nothing to gain by holding it.

Permission

None. The roster is what the client is already rendering.

The two things a roster could leak that a screenshot cannot β€” life state and stats β€” are not here: Open77.players.getLifeState, isDead, allLifeStates, getHealthState and allHealthStates stay behind players.life.read in the same table, and the stats reads behind players.stats.read. Asking who is standing in front of you does not grant you their health bar.

Relation to the server API

The dedicated server has its own Open77.players with the same names and different authority β€” its roster is the real one, and it can act on it. See the server API. A resource that needs a truth the client cannot have asks for it over a net callback:

-- server: the authoritative answer
Open77.net.register("population", function(source)
    return #Open77.players.all()
end)

-- client, inside a CreateThread, with `network.events` in the manifest
local count = Open77.net.callAwait("population")

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

// Guides Β· Lua API

Search the docs

1,905 guides and functions indexed

Try