Skip to main content

Definitive Edition server scripting

SanVerse server scripting targets GTA San Andreas Definitive Edition only. Scripts interact with the SanVerse server API, never game executables, game assets or launcher credentials.

Start here

This page is the introduction and compatibility boundary. Use the focused reference pages when you need a specific callback, command or object:

Lifecycle

The native scripting boundary exposes server start, authenticated player join/leave and state events, server-side commands, moderated chat and retained multiplayer-world state. A script receives an account identity only after the game plane has consumed the join ticket; it never receives the raw ticket. Gameplay commands and server-owned state are future versioned additions to this same DE-specific contract.

TypeScript resource shape

The public server-resource contract is TypeScript. The native C++ game-plane host owns the authoritative game state and exposes only the typed @sanverse/server-sdk capability surface; TypeScript does not receive game memory, launcher credentials, sockets, filesystem paths or operating-system APIs.

The developer-preview resource host can now load compiled TypeScript modules and exchange validated lifecycle, command and world operations over a line-delimited local bridge. Native Windows server process management now starts that host as a child process when a resource is configured. The bridge is local anonymous-pipe IPC, not a network listener. Player-state snapshots and chat moderation are available through this same bounded bridge.

Run a TypeScript resource

Build the SDK and resource first. The game server runs compiled JavaScript, not raw .ts files.

cd sdk/server
npm ci
npm run build

Place the compiled resource in the directory listed by server.json and start the single sanverse-server executable. The supervisor discovers each resource.json, starts resources in the configured order and owns the internal Node host. Environment variables and the transport daemon are development-only implementation details, not part of the server-owner workflow.

import { server } from "@sanverse/server-sdk";

server.on("ready", () => {
// Prepare server-owned state here.
});

server.on("playerJoin", (player) => {
// player.id is the active numeric slot.
// player.accountId is verified by the game plane.
});

server.on("playerState", (player, state) => {
// state contains stateFlags, position, heading, health, armor, weapon and modelId.
});

Client-side scripting and CEF are separate surfaces; see Scripting model. For the controlled character reference, see Server NPC API.

Do not attempt to authenticate players in a resource: ticket consumption stays between the game plane and the control plane.

Lifecycle guarantees

  • ready runs once when the resource host has started.
  • playerJoin runs only after the game plane has authenticated the player and assigned its numeric player slot.
  • playerLeave runs once after that authenticated session is removed from the relay.
  • playerState runs only while that player remains active and carries a validated snapshot.
  • The SDK does not expose a join ticket, endpoint, launcher access token, server credential, socket, filesystem or game-memory API.

Keep resource initialization deterministic: register every resource, call start() once, then let the game plane dispatch authenticated players. A resource should use player.accountId as an identity key for its own server-owned state; it must not treat it as a password or a launcher credential.

Event handlers

Event handlers are registered with server.on(eventName, handler). Register them during resource initialization. A handler may be synchronous or return a Promise. Player handlers receive a verified Player; playerState also receives the validated state snapshot. NPC/Actor lifecycle handlers receive an entity ID. The chat handler returns "relay" or "suppress".

server.on("playerJoin", (player) => {
console.log(`joined: ${player.id}`);
});

server.on("npcDeath", async (entity) => {
console.log(`server NPC ${entity.id} died`);
});

server.on("chat", (player, message) =>
message.startsWith("/") ? "suppress" : "relay");

Event list

EventArgumentsMeaning
readynoneResource host is ready
playerJoinPlayerAuthenticated player joined and received a slot
playerLeavePlayerAuthenticated player left; the slot may be reused
playerStatePlayer, PlayerStateValidated state snapshot
chatPlayer, messageChat before relay; return disposition
npcSpawn{ id }Server NPC became available
npcDeath{ id }Server NPC died
npcFinishPath{ id }Controlled NPC completed a movement path
actorPlayerAim{ id }A player aimed at a static Actor

Command handlers

Command handlers are registered with server.command(name, handler). Names use lowercase letters, digits, hyphens and underscores (maximum 32 characters). The context contains the verified player and parsed arguments; command input never contains credentials.

server.command("heal", ({ player, arguments: args }) => {
// Apply authoritative server-side state, then notify this player.
server.emitClientEvent(player, "ui.notice", `Heal requested: ${args[0] ?? "self"}`);
});

Client-server handlers

The server-to-client equivalent of a TriggerClientEvent is SanVerse's explicit server.emitClientEvent(player, eventName, payload). It targets one authenticated player and serializes a bounded string payload. Client-side code registers the matching event name; it cannot invoke server authority by emitting this event back.

server.on("playerJoin", (player) => {
server.emitClientEvent(player, "hud.welcome", JSON.stringify({ playerId: player.id }));
});

Client-to-server requests will use named, validated request contracts. Never trust client-provided position, health, inventory or entity ownership; the server revalidates every mutation.

Script objects and attributes

All object IDs are server-scoped. Attributes marked readonly are observations; mutations go through the documented server method and are replicated only after validation.

Player

Player: id (active numeric slot) and accountId (persistent verified identity). PlayerState: stateFlags, position { x, y, z }, heading, health, armor, weapon and modelId.

Blips and world

BlipOptions: position, icon and optional color. Use createBlip, removeBlip and clearBlips. World toggles are ambientNpcs, interiors and playerNicknames; ambient NPCs are only globally toggled and never consume player slots.

Server NPCs and Actors

NPC creation uses modelId, position, optional heading, virtualWorld and interior. Server NPC request/response properties include name, skin, health, maxHealth, invulnerable, position, heading, virtualWorld, interior, weapon, ammo and clipAmmo. Actors share the creation attributes and may receive an animation { dictionary, name }, but are static and do not use the Server NPC movement API.

Vehicles

Vehicle creation uses modelId, position and optional heading. The current authoritative vehicle setters are position, health, engine and locked; vehicle IDs are separate from player IDs and the server limits the retained vehicle set to 2000.

Commands

Register commands during initialization, before start(). A command receives only the verified account identity and parsed arguments; it never receives a join ticket or network credential.

server.command("echo", ({ player, arguments }) => {
server.emitClientEvent(player, "ui.notice", arguments.join(" "));
});

Command names accept lowercase letters, digits, hyphens and underscores (maximum 32 characters). Duplicate names, invalid names and unauthenticated dispatches are rejected. Inputs accept /echo hello or echo hello; individual arguments are capped at 128 characters. Use double quotes when an argument contains whitespace, for example /echo "hello San Andreas". Inside quoted arguments, only \" and \\ are accepted escapes; unterminated quotes or malformed escapes are rejected.

Player IDs and chat

Every connected player has both a verified accountId and a server-assigned numeric playerId. Use playerId for active session targeting and accountId for persistent server-owned data. A slot is released after leave and can be reused, so never retain a numeric ID without also checking the current authenticated player identity.

Resources can inspect or suppress a validated chat message before it is relayed:

server.on("chat", (player, message) => {
if (message.startsWith("/")) {
// Handle a local command or moderation action.
return "suppress";
}
return "relay";
});

Messages are UTF-8 and limited to 255 bytes. Resources never receive a socket, join ticket or launcher credential. The DE adapter owns transport framing and its in-game chat surface. A host gets 100 ms to return "suppress"; timeout, malformed output or host loss fail open as "relay" so a resource cannot stall player traffic.

Multiplayer world state

The runtime provides server-owned, retained world state. Defaults are disabled unless a resource explicitly enables them. State changes apply to active authenticated players and are replayed to a player after join.

server.on("ready", () => {
server.world.setDefaultFeature("ambientNpcs", false);
server.world.setDefaultFeature("interiors", false);
server.world.setDefaultFeature("playerNicknames", true);

const spawnBlip = server.world.createBlip({
position: { x: 2495.0, y: -1681.0, z: 13.0 },
icon: 5,
color: 0xff3366ff,
});
});

createBlip returns an optional server-owned ID. It rejects non-finite coordinates and the runtime bounds the global set to 4096 blips. Use removeBlip(id) or clearBlips() to clean up state. Blips and default-world controls are delivered through the versioned server-event boundary; scripts do not write game memory or access a client socket.

Not available in the alpha

The adapter acceptance work is still in progress. Do not assume that a world-state API implies that every single-player DE system is already disabled client-side. Story missions, races, side quests, weapon pickups, police AI, wanted level, cheats, default HUD blips and the final CEF TAB list each require their own tested native-client capability before they are public API. Voice chat is planned separately.

Player-state callbacks remain server-side observations. Scripts must not build against undocumented interfaces or borrow APIs from other multiplayer projects. New capabilities will be added as versioned DE-specific contracts with examples and compatibility notes.

Compatibility rule

The API is versioned and DE-specific. Do not use compatibility layers or APIs designed for other multiplayer platforms.