Networking & access
Player identity and username
Durable player identifiers, session-scoped ids, display names, the rename flow, and what a game server can actually verify about a connecting player.
On this page5 sections
Each installation has a persistent cryptographic identity. The Master verifies the username used by chat, nameplates, presence events and server scripts.
Stable id and display name
Identifiers have different jobs:
| Value | Lifetime | Use |
|---|---|---|
license |
Permanent Open77 account | Characters, inventories and progression across linked devices |
userId / open77 |
Persistent installation identity | Existing identity-based APIs, including built-in access lists and ACLs |
steam / gog / epic |
Linked store account | Store identity supplied by the verified Master ticket; Epic needs v3 |
playerId / source |
One connection | Addressing the player during the current session |
displayName |
Editable profile field | Chat and presentation only |
For account-level persistence, store license as a string. Use userId where an existing API
requires the installation identity; the two values are not interchangeable. A player can rename
their profile and receives a new playerId after reconnecting. Existing data keyed by userId
needs an explicit migration before switching keys.
license, steam and gog are available to server Lua since 2.31.13+op77.101. See
Steam, GOG and permanent account identifiers
for a complete join handler, formats and missing-identifier behavior.
Server 2.31.15+op77.105 adds Epic identifiers, with the matching Master update
deployed. Older servers continue receiving v2 tickets: players can still join,
but epic is absent there. Updating a server to read Epic IDs is optional.
A missing store ID never removes the Open77 license.
Steam/GOG ownership is verified once and remains linked to the Open77 account. Since
2.31.13+op77.102, an account manually approved by an Open77 administrator can play without a
linked store: license is present, while steam and gog may both be absent. Use the Open77
account key for progression rather than requiring a store ID.
Linking is currently optional: players can skip it and still receive their permanent Open77
license. An absent Steam/GOG ID is a supported account state, not a failed connection.
Changing the username
The server browser shows the current username in its Identity field. Pressing Save sends a freshly signed enrollment request to the Master. The Master accepts the update only when the private key already attached to the userId signed it.
Names contain 1 through 32 UTF-8 bytes and cannot contain control characters. A username cannot be changed during an active game session.
The private key never leaves the Windows identity store. The request, response, and saved profile contain no reusable password.
What a game server verifies
The Master issues an Ed25519 certificate covering all three public profile values:
userId || P-256 public key || displayName
During connection, the game server verifies that certificate and a fresh P-256 session proof bound to its challenge. Changing only the name in a modified client invalidates the Master certificate, so the forged name is rejected before the session becomes active.
Server Lua
Server resources receive the verified values through the normal player API:
AddEventHandler("onPlayerConnected", function(playerId, playerName)
local player = tonumber(playerId) or 0
print(GetPlayerIdentifier(player)) -- installation userId
print(GetPlayerName(player)) -- Master-verified displayName
print(GetPlayerIdentifierByType(player, "license")) -- permanent Open77 account ID
end)
The connect event is onPlayerConnected, and it passes the player ID as its first
argument. source is not set here: it is populated only for handlers reached through
a network event, never for a plain TriggerEvent dispatch.
The username-editing Lua bridge is reserved for the trusted local server-browser package. Downloaded server resources cannot rewrite a player's identity.
Language
The game's language is a setting of the player's own game, not of their account: the
engine stores it as three variables of the /language settings group -- OnScreen (text),
VoiceOver (audio) and Subtitles -- whose values are CName codes such as en-us,
fr-fr, jp-jp, kr-kr, pt-br, zh-cn (the very values the vanilla settings screen
compares against; it is ru-ru that triggers the Russian voice-over disclaimer and ar-ar
that flips the layout right-to-left). Open77 reads them where they live and reports them on
both runtimes.
Client, permission session.locale:
local locale, reason = Open77.session.locale()
-- { code = "fr-FR", gameLanguage = "fr-fr", voiceLanguage = "en-us",
-- subtitles = "fr-fr", chromium = "en-US", revision = 1 }
code is a BCP-47 tag derived from OnScreen (the four languages the engine spells by
country are mapped: jpβja, krβko, czβcs, uaβuk; the region is upper-cased).
The three raw engine codes ride beside it, so a resource that wants the audio language, or
meets a code the mapping has not seen, always has the source. chromium is the operating
system's UI language -- what a WebUI page's navigator.language (the JS Open77.getLocale())
has always answered. It is not the game's language and is kept for contrast only.
nil, "locale_not_reported" until the script bridge has answered once: the settings
container is captured by the health-bar controller, so the first answer follows the first
gameplay frame by a moment. The read is asked again, rate-limited, on every call, and at
every world-ready.
Server, permission players.locale.read:
permissions { "players.locale.read" }
AddEventHandler("onPlayerReady", function(playerId)
CreateThread(function()
Wait(5000) -- the report follows world-ready by a few seconds
local locale = Open77.players.locale(playerId)
local language = locale and locale.code:sub(1, 2) or "en"
Open77.chat.send(tonumber(playerId), ({ fr = "Bienvenue !", de = "Willkommen!" })[language] or "Welcome!")
end)
end)
The server never asks the engine: the client host -- not a resource -- pushes the value
once per world-ready on the reserved open77:session:locale net event, which the server
consumes before any resource route and caches per session. TriggerServerEvent refuses the
name on the client and TriggerEvent refuses it on the server (reserved_event), so what
players.locale answers is what the player's own game said and nothing else. It is a
report, not a ledger: a modified client can lie about its language, which costs nobody
anything. The table adds ageMs (the age of the report) and revision (how many reports
this session has produced). GetPlayerLocale(playerId) is the FiveM spelling of the same
function.
Four refusals, on purpose distinct: invalid_player_id; player_not_found for a player who
is not connected; not_reported for one whose client has not pushed yet -- a client that
never reached world-ready, or an older build; locale_unavailable in an embedding with no
transport.
A language change re-reports. The engine only allows changing the language at the main
menu (the /language variables are pre-game only), so a player who changes it leaves the
world to do so; the next world-ready pushes the new value, revision moves, and ageMs
restarts. Nothing announces it: read the value when you need it rather than caching it for
the session.
// For agents & LLMsthis page as Markdownllms.txtllms-full.txt
