Observer nameplates
These 12 new server-side functions control overhead labels for an ordered pair of connected human players: observer → target. They are universal presentation primitives: the gamemode owns acquaintance rules, permissions, character/account identifiers and database persistence. They do not replace setPlayerName or change the player's character appearance.
Function reference
| Function | Purpose |
|---|---|
| setPlayerNameplateTextFor / getPlayerNameplateTextFor / resetPlayerNameplateTextFor | Set, read or remove an explicit text override for one pair. |
| setPlayerNameplateVisibleFor / isPlayerNameplateVisibleFor / resetPlayerNameplateVisibleFor | Control the complete overhead label for one pair, including numeric ID and speaking suffix. |
| setPlayerNameplateIdVisibleFor / isPlayerNameplateIdVisibleFor / resetPlayerNameplateIdVisibleFor | Control only the numeric session-ID suffix for one pair. |
| getPlayerNameplateStateFor | Read effective policy, explicit override flags and raw values together. |
| setPlayerNameplateIdVisible / isPlayerNameplateIdVisible | Set/read the global numeric-ID fallback, default true.
|
All twelve functions have PascalCase aliases, for example SetPlayerNameplateTextFor. The first ten take observerId, targetId; both must be different connected human-player session IDs. NPC IDs, persistent character IDs, invalid IDs and self-pairs are rejected.
Independent fields and current defaults
Each field resolves independently: pair override → current global field → native default. A text override does not force visibility or suppress the numeric ID. An ID override does not hide the name or mute voice. Whole-label visibility false hides text, ID and the speaking suffix; voice audio routing is unchanged.
-- A sees B as a stranger; C knows B. B's view of A is unchanged.
setPlayerNameplateTextFor(playerA, playerB, "Stranger")
setPlayerNameplateTextFor(playerC, playerB, "Known friend")
setPlayerNameplateIdVisibleFor(playerA, playerB, false)
-- Reset only A's text. A now uses B's CURRENT global text/name,
-- while A's explicit ID-hidden override remains.
resetPlayerNameplateTextFor(playerA, playerB)
Existing setPlayerNameplateText, resetPlayerNameplateText and setPlayerNameplateVisible remain global fallback controls; they do not clear pair overrides. getPlayerNameplateText returns global custom text or false when unset. isPlayerNameplateVisible reads the global visibility policy, not the observer-specific result. Global settings are not resource-owned and are not reverted automatically when the writing resource stops.
Reading explicit versus effective state
getPlayerNameplateTextFor returns only an explicit pair text string, otherwise false. It never returns inherited text. The two is...For getters return the effective boolean after inheritance; false can also mean an invalid pair.
getPlayerNameplateStateFor returns this table, or false for an invalid pair/unavailable service:
| Field | Meaning |
|---|---|
text, visible, idVisible |
Effective text string and boolean policies. Text excludes the system ID and speaking suffixes. |
textOverridden, visibilityOverridden, idVisibilityOverridden |
Whether the corresponding explicit pair field exists. |
textOverride |
Explicit string, or false when absent.
|
visibleOverride, idVisibleOverride |
Explicit boolean, or false when absent. Use the flag to distinguish absent from explicitly false.
|
local state = getPlayerNameplateStateFor(observerId, targetId)
if state then
outputDebugString(state.text)
if state.visibilityOverridden and state.visibleOverride == false then
outputDebugString("Explicitly hidden for this observer")
end
end
The table is a resource-global, read-only server view: it includes fields owned by other resources. It is not an AOI test or client-render acknowledgement. Effective text may be non-empty while visibility is false. Distance, native presentation rules and asynchronous delivery still determine whether pixels are rendered.
Validation and exclusive resource ownership
- New boolean setters require real Lua booleans;
0,1, strings andnilare not substitutes. IDs must be finite positive integers. Invalid argument counts returnfalse. - Text must be valid UTF-8, at most 48 Unicode code points and 96 bytes. It is rejected, not truncated, on invalid UTF-8, controls,
<,>,&, BOM, or the shared validator's disallowed zero-width/bidirectional formatting characters. Empty text means reset. - The resource that first writes a pair field owns that field exclusively. Another resource cannot overwrite or reset it until the owner releases it. Different fields of the same pair can have different owners.
- An identical owned write or reset of an absent field succeeds without a state change. A foreign write/reset fails. Always check the returned boolean; a failed mutation does not grant ownership.
- Reset removes only the selected owned field and follows the current fallback. It does not restore a captured old global value.
- The core does not impose an RP permission or acquaintance policy. Validate permissions and remote-event senders in your own trusted server resource.
Relevance, reconnect and resource stop
Accepted changes are sent reliably to the affected observer when relevant. Pair overrides remain stored outside synchronization range; the current resolved state is replayed on late join and AOI re-entry. Resource stop removes only that resource's fields. Disconnect of either endpoint clears affected pairs, and session-generation checks prevent stale state from applying to a reused numeric player ID.
These overrides are runtime state, not persistent database relations. After reconnect or resource restart the gamemode must restore its policy. Global nameplate state also ends when that player disconnects. A setter returning true means server acceptance, not proof that another client has rendered the label.
Configurable bounds
Default server configuration:
[nameplates]
maxPairs=250000
maxPairsPerResource=250000
mutationsPerSecond=4096
mutationBurst=8192
Capacity keys accept 1..1000000; mutation rate/burst accept 1..250000. A pair counts once globally and once per resource owning any of its fields. The per-resource token bucket charges changed, non-reset field writes. Identical writes, resets and cleanup do not consume tokens. A capacity/rate rejection returns false; batch large restores over ticks and retry with bounded scheduling, not a tight loop.
Defaults can hold all 500 × 499 directed pairs. A sparse global-default-plus-known-exceptions design usually uses fewer entries and less work.
Building a persistent acquaintance system
The server developer supplies the database schema and adapter. Store directional relationships by stable character IDs, not temporary playerId. Decide whether A knows B implies a reciprocal relationship; the engine deliberately does not assume this.
A possible policy:
- When a character becomes ready, establish its global label as
Stranger (<characterId>)and global numeric-ID policy as false. Choose one resource to own this global policy, and do so before exposing the character under your gamemode's login/spawn flow. - For each observer who knows that character, set a pair text override with the known name. Do not call setPlayerName to simulate a stranger: that changes the canonical name used by other systems.
- On new acquaintances or forgotten relations, update/reset only the relevant directional pair. Check returned booleans for resource conflicts or rate limits.
- On reconnect/resource restart, rebuild policy in bounded batches from your own database. Core pair cleanup does not revert global settings for you.
Guard asynchronous DB results against both endpoints changing. Capture both session objects/generations, both stable character IDs, and your resource's load epoch before starting a query. Before applying its result, verify every captured value is still current and both players are connected and character-ready. isPlayerConnected alone cannot detect ID reuse or a character switch.
-- Integration sketch: sessions, loadEpoch and queryAcquaintance are YOUR
-- gamemode's state/DB adapter, not extra G1R:MP API functions.
local observerSession = sessions[observerId]
local targetSession = sessions[targetId]
local requestEpoch = loadEpoch
local observerCharacter = observerSession.characterId
local targetCharacter = targetSession.characterId
queryAcquaintance(observerCharacter, targetCharacter, function(knownName)
if loadEpoch ~= requestEpoch
or sessions[observerId] ~= observerSession
or sessions[targetId] ~= targetSession
or observerSession.characterId ~= observerCharacter
or targetSession.characterId ~= targetCharacter
or not observerSession.characterReady or not targetSession.characterReady
or not isPlayerConnected(observerId) or not isPlayerConnected(targetId) then
return
end
-- Adapter returns a validated name or false; handle DB failures separately.
local accepted
if type(knownName) == "string" then
accepted = setPlayerNameplateTextFor(observerId, targetId, knownName)
elseif knownName == false then
accepted = resetPlayerNameplateTextFor(observerId, targetId)
end
if accepted == false then
outputDebugString("Acquaintance presentation rejected; check ownership or limits")
end
end)
Initialize and validate both session objects before entering this sketch; replace them on session/character changes and invalidate loadEpoch when abandoning a restore batch.
Privacy boundary
Hiding an overhead label is not network anonymity. Canonical names still exist in identity packets, getPlayerName, ordinary player-list and chat flows unless those systems independently become recipient-aware. This feature neither changes account credentials nor guarantees that a client cannot inspect canonical identity elsewhere. For consistent stranger RP, apply one server-side recipient-aware resolver to each relevant chat/roster/UI path as a separate gamemode concern.
Optional three-client test
The shipped nameplate_test resource is not autostarted. Start it explicitly in the server console:
start nameplate_test
The bundled GothicRP adapter requires an authenticated test operator with admin.players and RP.Config.testTools.enabled == true. All participants must be logged in and fully spawned. An authenticated test owner can still stop the test after permissions/settings change. Other gamemodes need their own trusted server-resource adapter; do not expose the test event as remotely callable.
/nametest start <A ID> <B ID> <C ID>
/nametest next
/nametest status
/nametest stop
Use three different connected players with no existing overrides on the six directed pairs. Only the starter controls the active sequence. English chat messages guide 10 steps: directional text, ID hiding, complete-label hiding, visibility restore, acquaintance text, reverse direction, text reset, visibility/ID reset, AOI re-entry and final cleanup.
Step 9 requires moving B outside the server synchronization range and back, then checking each client's intended label without a stale/canonical-name flash. Optional disconnect or resource-stop checks terminate the sequence and clear test-owned fields. The test does not change globals, database records, equipment or player positions; global controls have separate C++ tests. Chat readback verifies server state, so all three clients must still inspect actual visuals. Use matching protocol-34 server/client builds.