Discord Rich Presence - server Lua API
Available in 0.1.9 / BUILD264 / Protocol 42.

SERVER-OWNER STARTER
server/resources/discord_presence contains config.lua, server.lua, scripts.xml
and README.txt. Disabled by default, public Application ID intentionally empty.
Use this on a server, not the unrestricted discord_presence_test QA resource.
Choose one owning resource; integrate the same files into a gamemode only with
distinct file names and manifest entries, never overwrite its existing scripts.
English public API and complete copyable owner setup (since 0.1.9):
https://g1r-mp.com/wiki/index.php?title=Discord_Rich_Presence
https://g1r-mp.com/wiki/index.php?title=Discord_Presence_configuration

SETUP
1. Create an application at https://discord.com/developers/applications.
   Its application name is the main game title displayed by Discord. Copy
   the public Application ID as a STRING. Do not use a bot token/Client Secret.
2. Optional: upload Rich Presence art assets in that application's settings.
   Use their asset keys (logo, old_camp). This API does not download images
   or accept arbitrary image URLs. Existing images need no custom client PAK.
3. Enable Discord Rich Presence in the G1R:MP launcher's Settings, save, rejoin.
   It is OFF by default. Server Lua cannot override this local privacy choice.
   The Discord desktop client must be running and activity sharing permitted.
4. Use the server Lua API below. Both Windows/Linux dedicated servers work;
   neither needs Discord installed. The supported game client is Windows.
   Discord inside a Linux/Wine host is not tested/supported by this transport.

FUNCTIONS (server-side only, available in every server resource)
setDiscordApplicationId(applicationIdString) -> boolean, errorString
getDiscordApplicationId() -> string (empty if not configured)
getDiscordTimestamp() -> current Unix timestamp in SECONDS, UTC
setPlayerDiscordPresence(playerId, fields) -> boolean, errorString
updatePlayerDiscordPresence(playerId, patch) -> boolean, errorString
clearPlayerDiscordPresence(playerId) -> boolean, errorString
getPlayerDiscordPresence(playerId) -> fields table or nil
getPlayerDiscordPresenceStatus(playerId) -> status string

The application ID is per resource. Changing it affects subsequent set/update
calls, not existing profiles until you update them. Never pass a snowflake ID
as a Lua number: the number can lose precision. Player IDs must be integers.
set replaces the whole profile; update merges it atomically, false removes
a field (e.g. {largeImage=false, largeText=false}). No partial writes on errors.
set/update returning true means QUEUED ON THE SERVER, not visible on Discord.
get returns a copy, with applicationId and owner metadata; those two read-only
keys must not be passed back as writable fields. No persistent storage is
performed by this feature: reapply desired presence when players join.

FIELDS (flat Lua table, all optional)
details, state: text, max 128 UTF-8 bytes each. Example server/location/job.
largeImage, smallImage: registered asset key, max 128 ASCII bytes [A-Za-z0-9_-].
largeText, smallText: asset tooltip, max 128 UTF-8 bytes.
startTimestamp, endTimestamp: Unix seconds, integer 0..4102444800; 0 disables.
  End requires a nonzero start and must be >= start. Use getDiscordTimestamp,
  NOT getTickCount (which is monotonic milliseconds, not a calendar clock).
partyId: opaque non-secret text max 128 UTF-8 bytes.
partySize, partyMax: integers 0..1000000; size <= max; nonzero max requires ID.
  Set partyId/partySize/partyMax together. This only displays a player count;
  it does not create a Discord party, invite, voice channel or server slot.
button1Label, button1Url, button2Label, button2Url: at most two HTTPS links.
  Label max 32 UTF-8 bytes, URL max 512 ASCII bytes. Label + URL together.
  Button2 requires button1. No file:, javascript:, custom schemes, credentials
  or port syntax in links. Links are displayed in Discord; not opened by Lua.
Empty text omits a field. Text must be valid UTF-8 without control characters.
Unknown fields are errors. Type is always Playing; the application name comes
from Discord, not from a freely spoofable Lua title field.

CALLBACK
addEventHandler("onPlayerDiscordPresenceStatus", root, function(playerId, status)
    outputDebugString("Discord status: " .. status)
end)
Emitted only to the resource owning the active profile, on status changes.
Status may be unsupported (no compatible client report / expired capability),
disabled (local opt-out), idle, connecting, unavailable (Discord not running /
pipe lost), applied (matching SET_ACTIVITY acknowledgement), error, cleared.
The getter also returns disconnected for an invalid/offline player.
After clear there is no resource owner, so cleanup does not emit an owner's
callback. The getter can still show the last report until the next client poll.
Reports are CLIENT-SUPPLIED DIAGNOSTICS, never evidence of a Discord account,
membership, permissions, rewards or proof that anyone else can see the status.
No Discord account ID, username, avatar, friends list or token is sent upstream.

LIFECYCLE / SAFETY
One resource owns a player's active presence. Another gets
presence-owned-by-another-resource until the owner clears/stops. No silent
overwrites. Resource stop/fault clears its profiles; disconnect drops server
state and client clears immediately on native session reset. A 45-second
heartbeat expiry is the fallback for an unexpected lost endpoint.
No protocol bump: separate versioned reserved events, negotiated by client
reports. Older clients ignore the feature and do not need gamemode changes.
Latest values are coalesced, normally at most one update per 15 seconds.
Clears bypass the update delay. Identical activity is not sent again to Discord;
server heartbeats maintain the session lease. Discord reconnect retries are
bounded (15 seconds), handshake/ACK timeouts 10 seconds. All pipe I/O is on
one background worker, created only if enabled AND a nonempty profile exists.
No-profile worker sleeps; opt-out never starts it. No bots, OAuth, webhooks,
Discord server plugins, browser automation, SDK DLL installs or extra launchers.
Do not put passwords, login names, IPs, tokens or private character information
in presence fields. Only explicitly configured gamemode text is published.
Application assets/Discord privacy/settings and availability affect visibility.

EXAMPLE (replace APPLICATION_ID; do not enable QA commands publicly)
assert(setDiscordApplicationId("YOUR_APPLICATION_ID_AS_A_STRING"))
addEventHandler("onPlayerJoin", root, function(playerId)
    setPlayerDiscordPresence(playerId, {
        details = "My Gothic RP server",
        state = "Creating a character",
        startTimestamp = getDiscordTimestamp(),
        largeImage = "server_logo",
        largeText = "My Gothic RP",
        button1Label = "Server website",
        button1Url = "https://example.com/"
    })
end)
-- Later, when the server's gameplay logic changes their location:
-- updatePlayerDiscordPresence(playerId, {state = "Exploring the Old Camp"})
-- clearPlayerDiscordPresence(playerId)

NOT INCLUDED
Discord account linking, bots, guild-role checks, voice, automatic Join/Spectate
and game invitations. Buttons can point to the website/Discord invite HTTPS
page, but do not bypass the existing launcher/master-list authentication.

PRIMARY REFERENCES (checked 2026-10-20)
https://docs.discord.com/developers/topics/rpc
https://docs.discord.com/developers/discord-social-sdk/development-guides/setting-rich-presence
https://github.com/discord/discord-rpc/blob/master/documentation/hard-mode.md
Uses documented direct desktop IPC SET_ACTIVITY without account authentication.
