Quick navigation

Go to

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.

4 min read970 wordsMarkdown
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

// Guides Β· Lua API

Search the docs

1,905 guides and functions indexed

Try