Skip to content

sagl

The global sagl object: channels to the gamemode, logging, the local player and the server's branding. sagl.ui, sagl.map and sagl.files have pages of their own.


Channels

sagl.on(channel, handler)

sagl.on(channel: string, handler: (data: any) => void): void

Handles what the gamemode sends on a channel (server.channel(name).send/broadcast, player.emit).

Parameter Type Description
channel string The channel's name.
handler function Gets the data, parsed from JSON.

Throws TypeError without a string and a function.

sagl.on("notify", ({ text }) => sagl.ui.messageBox({ title: "Notice", text }));

sagl.off(channel, handler?)

sagl.off(channel: string, handler?: (data: any) => void): void

Removes a handler, or every handler of the channel.

Parameter Type Description
channel string The channel's name.
handler function The one to remove. Left out: all of them.
const once = (data) => { sagl.log("first:", data); sagl.off("tick", once); };
sagl.on("tick", once);

sagl.emit(channel, data?)

sagl.emit(channel: string, data?: any): void

Sends data to the gamemode, where server.channel(channel).on(...) and the clientMessage event get it.

Parameter Type Description
channel string 1-64 characters, not sagl:error.
data any Anything JSON can hold, up to 256 KB. Default null.

Throws RangeError for a bad channel or data over 256 KB.

sagl.emit("shop", { buy: "pistol", quantity: 1 });

Logging

sagl.log(...values)

sagl.log(...values: any[]): void

Writes a line to the player's sagl/sagl.log, as script: .... Values are converted to strings and joined with spaces; JSON.stringify objects to see inside them.

sagl.log("hud ready at", JSON.stringify(sagl.player.position()));

sagl.screenshot()

sagl.screenshot(): Promise<string | null>

Saves the next frame as a PNG in the player's Pictures folder (Pictures\SAGL\SAGL <date> <time>.png), and resolves with its path (null if it couldn't, or one is still saving). What's on screen is what's saved, UI included: hide yours first, as freeroam's camera mode does.

sagl.subtitle(text, ms?)

sagl.subtitle(text: string, ms?: number): void

A subtitle at the bottom of the screen, in the game's style, if the player has Subtitles on in their pause menu. Replaces the one showing; empty text clears it.

sagl.subtitle("Find the keycard.", 4000);

Timers

The standard functions, with async/await and promises:

setTimeout(fn: () => void, ms: number): number
setInterval(fn: () => void, ms: number): number
clearTimeout(id: number): void
clearInterval(id: number): void
const tick = setInterval(() => sagl.emit("heartbeat"), 5000);
sagl.on("stop", () => clearInterval(tick));

const wait = (ms) => new Promise((r) => setTimeout(r, ms));
await wait(1000);

The local player

sagl.player, read-only.

sagl.player.id

readonly id: number

The player's id, as the server knows it (player.id).

sagl.player.position()

position(): { x: number; y: number; z: number }

Returns where the player is, in world coordinates.

const { x, y } = sagl.player.position();
sagl.map.setMarker("me", { x, y, color: "#2fb3a0", label: "You were here" });

sagl.player.heading()

heading(): number

Returns the direction the player faces, in degrees.

sagl.player.health(), sagl.player.armour()

health(): number
armour(): number
setInterval(() => hud.setClass("health", "low", sagl.player.health() < 25), 500);

sagl.player.inVehicle()

inVehicle(): boolean

Returns whether the player is in a vehicle.

sagl.player.velocity(), sagl.player.keys(), sagl.player.onKeys(fn)

velocity(): { x, y, z }          // m/s; their vehicle's while in one
keys(): number                   // the buttons held: Key bits, as the server's
onKeys(fn: (keys: number, old: number) => void): { remove() }

The local player's buttons, as the server's playerKeys has them (the same bits: 1 sprint, 2 jump, 4 enter/exit, 8 fire, 16 action, 64 aim, 256 crouch, 512 look behind...). onKeys calls fn each time they change.

const SPRINT = 1;
sagl.player.onKeys((keys, old) => {
  if (keys & SPRINT && !(old & SPRINT)) sagl.log("sprint pressed");
});

sagl.player.setIndicators(state), setIndicatorKeys(on)

setIndicators(state: "off" | "left" | "right" | "hazard"): boolean
setIndicatorKeys(on: boolean): void

The indicators of the vehicle the local player drives, as their keys would set them (everyone sees them). Returns false if they don't drive one, or the server holds them (vehicle.indicators). setIndicatorKeys(false) turns their keys for them off (, . ; by default, rebound in Settings, Controls), for a script of its own; the server's indicators.keys off wins. sagl.world.vehicles() says what each vehicle's show.

sagl.player.setIndicatorKeys(false);
sagl.commands.register(sagl.commands.command("hazards").executes(() => {
  if (!sagl.player.setIndicators("hazard")) sagl.chat.print("You're not driving");
}));

sagl.actions

sagl.actions.register(id: string, options: { label: string; key?: string }, handler?: () => void): void
sagl.actions.remove(id: string): void
sagl.actions.on(id: string, fn: () => void, options?: { server?: boolean }): void
sagl.actions.keyName(id: string): string | null
sagl.actions.keyIcon(id: string): string
sagl.actions.isDown(id: string): boolean
sagl.actions.KEY_NAMES: string[]

Things the player does with a key they choose, as the server's actions: - register adds one of this script's own. It shows in the player's Settings → Controls under the server's name, with key as its default key, and handler runs when it's pressed. - on listens too: to this script's action, or with { server: true } to the server's action of that id. The server still hears its own. - keyName is the key the player has for an action (the script's, the server's, or one of SAGL's such as "chat"), as their keyboard labels it, or null if it has none. Use it for hints. - keyIcon is this script's action's key as RML for a hint in your UI: the key's icon, or its label on a blank key ("" if it has none). - isDown is whether this script's action's key is held down right now (not while the player types in the chat or a menu). Check it each frame for things that go on while a key is held, such as zooming.

A script's or the server's action takes precedence over SAGL's own on the same key: freeroam's camera mode puts "save a picture" on F8, which is otherwise the debug panel, only while it's on.

sagl.actions.register("wave", { label: "Wave", key: "J" }, () => sagl.player.playAnim("ON_LOOKERS", "wave_loop", { ms: 3000 }));
sagl.actions.on("engine", () => sagl.subtitle("Engine"), { server: true });
sagl.subtitle(`Press ${sagl.actions.keyName("engine") ?? "a key (Settings, Controls)"} to start the engine`);

sagl.player.setSearchlight(on), setSmoke(on)

setSearchlight(on: boolean | null): void
setSmoke(on: boolean | null): void

The searchlight of the helicopter the local player flies, and the smoke trail of the plane: true on, false off (the pilot's own switch, the horn or the look-behind button, then does nothing), null as the pilot has it. It holds for whatever they fly next, until set again. Everyone sees it. What the server forces (vehicle.searchlight, vehicle.smoke) comes first. sagl.world.vehicles() says whether each is on.

const { command } = sagl.commands;
sagl.player.setSearchlight(false);  // the horn no longer switches it
sagl.commands.register(command("light").executes(() => {
  const v = sagl.world.vehicle(sagl.world.player(sagl.player.id)?.vehicle);
  sagl.player.setSearchlight(!v?.searchlight);
}));

Drawing on screen

sagl.draw

sagl.draw.text(text, x, y, options?): Drawn
sagl.draw.box(x, y, width, height, color?, options?): Drawn
sagl.draw.image(file, x, y, width, height, options?): Drawn

The same as the server's ScreenDraw: text, boxes and client-file images, pinned to an anchor with the same placement and options. Each returns a handle with set(changes) and remove(). A script's draws are its own, apart from the server's, and go when scripts reload.

const score = sagl.draw.text("Score: ~g~0", 0, 30, { anchor: "top", align: "center", font: "sa-menu", size: 14, outline: 1 });
sagl.on("score", (n) => score.set({ text: "Score: ~g~" + n }));

sagl.audio.play(file, options?)

sagl.audio.play(file: string, options?: { at?: { x, y, z }; range?: number; loop?: boolean; volume?: number; effects?; realism?: boolean }): { duration, done, stop() }

Plays a client file (MP3, WAV, FLAC) here only: from a spot (heard up to range m, default 30), or everywhere, through effects if given (presets and stages as the server's audio effects; sagl.audio.presets lists the presets), and with realism if asked. As the server's server.audio. Stops when scripts reload. duration is how long it plays (ms), done a promise that resolves when it has played through or is stopped.

const beep = sagl.audio.play("audio/beep.wav", { at: sagl.player.position() });
await beep.done;   // played through

sagl.audio.playGame(sound, options?)

sagl.audio.playGame(sound: number, options?: { at?: { x, y, z } }): void

One of the game's own sounds, here only: as the server's server.audio.playGame() (the numbers are GameSound's).

sagl.audio.playMission(id, options?)

sagl.audio.playMission(id: number, options?: { at?: { x, y, z } | { player | ped | vehicle: id }; range?: number; effects?; realism?: boolean }): { duration, done, stop() }

One of the game's mission audio, here only: as the server's server.audio.playMission(), returning the same as sagl.audio.play.

sagl.audio.setEnvironment(effects)

sagl.audio.setEnvironment(effects: AudioEffects | null): void

Effects every sound of ours passes through here (see the server's setEnvironment): while set it wins over the server's; null goes back to the server's.

sagl.effects

sagl.effects.play(name: string, at, options?: { offset?; loop?: boolean }): { stop(): void }
sagl.effects.dust(at, options?: { color?; carColor?; count?; size?; spread?; rise?; life?; offset? }): void

The game's particle effects and coloured dust, here only: as the server's server.effects. at is a spot { x, y, z }, or what it follows: { player: id }, { ped: id }, { vehicle: id } or { object: id } (the server's ids). An effect the game doesn't have throws. They stop when scripts reload.

sagl.effects.play("explosion_crate", sagl.player.position());
sagl.effects.dust({ player: sagl.player.id }, { color: "#30c030", count: 30 });

sagl.player.playAnim(block, name, options?), stopAnim(), setFrozen(frozen)

Animates the local player (everyone sees it: the server passes it on), as player.playAnim(), and freezes or unfreezes them here. playAnim resolves after options.ms; options.upperBody plays it on the upper body only (they can walk).

sagl.player.setFrozen(true);
sagl.player.playAnim("PED", "phone_talk", { loop: true });

sagl.camera

sagl.camera.set(shot): Promise<boolean>
sagl.camera.play(steps, options?): { done, onStep(fn), stop() }
sagl.camera.freecam(options?), stop(), reset()
sagl.camera.pose(): { x, y, z, pan, tilt, roll, fov, focus, aperture }
sagl.camera.lens({ fov?, roll?, focus?, aperture? }): void
sagl.camera.move({ x?, y?, z? }): void
sagl.camera.freecamScale(scale): void

This player's camera, as the server's player.camera: the same shots, sequences, paths and freecam, played here without a trip to the server. Things are given by their server ids ({ vehicle: 12 }). pose() is where the camera is now, its angles and its lens (focus in metres, "auto" or null), whoever moves it: freeroam's camera mode saves shots from it. lens() changes the lens where the camera is, in the freecam or a held shot: freeroam's camera mode zooms, rolls and focuses with it. move() moves it by an offset in metres where it is (the freecam, or a held shot): camera mode's Page Up and Page Down. A script's camera and the server's replace each other, and the script's is handed back when the scripts stop.

sagl.player.slideTo(position, heading?, ms?)

Slides the local player there on foot and turns them (the game's own, as its vending script), as the server's player.slideTo(); resolves when they're there.

sagl.player.attachObject(slot, model, bone, options?), removeAttachedObject(slot)

An object in the local player's hands, as the server's player.attachObject(): slot 0-9 (the same slots as the server's: setting one replaces what's there), a bone from sagl.Bone (LeftHand, RightHand, Head...), { offset?, rotation?, scale? }. Shown at once; the server shows the others, unless it refuses (playerAttachObject).

// A phone at the ear while they're on it.
sagl.player.attachObject(8, 330, sagl.Bone.LeftHand, { offset: { x: 0, y: 0.06, z: 0 }, rotation: { x: -90, y: 90, z: 0 } });
// ...
sagl.player.removeAttachedObject(8);

sagl.interactions.onReach(fn)

sagl.interactions.onReach(fn: ((r: { id, model, position, heading, inReach }) => boolean) | null): void

Hears when one of the server's interactions comes in reach (inReach true) and goes. Returning true (coming in reach) shows your prompt instead of the help box.

sagl.interactions.onReach(({ inReach }) => {
  if (inReach) prompt.show(); else prompt.hide();
  return true;
});

sagl.gxt(key, options?)

sagl.gxt(key: string, options?: { table?: string; numbers?: number[] }): string

The game's own text for a GXT key, from this player's game: with ~1~ numbers and ~k~~ACTION~ keys filled in, colour codes kept (they work in sagl.draw and sagl.hud). As the server's gxt(), but returns the text itself.

const help = sagl.gxt("RY2_64", { table: "RYDER2" });   // "Press LMB when in the forklift truck to ..."

sagl.hud

sagl.hud.missionPassed(subtitle?): void
sagl.hud.missionFailed(subtitle?): void
sagl.hud.missionResult(title, subtitle?): void
sagl.hud.notice(text, ms?): void
sagl.hud.help(text, ms?): void
sagl.hud.clearHelp(): void
sagl.hud.set(parts: { visible?; radar?; areaNames?; vehicleNames?; clock?; health?; armour?; chat? }): void
sagl.hud.serverHidden: { visible, radar, areaNames, vehicleNames, clock, health, armour, chat }
sagl.hud.onChange(fn: (serverHidden) => void): void

The result screens, the game's notices and its help box, shown on this game only; set hides parts of the game's HUD, as the server's player.hud.set (a part shows only if neither the server nor a script hides it).

serverHidden is what the server hides (each part true if hidden), and onChange hears it change: for a HUD of the script's own to follow it, as the server hides the radar in a mod shop or for a cutscene. A <sagl-map mode="radar"> hides itself while the server hides the radar.

const follow = (hidden) => (hidden.radar || hidden.visible ? hud.hide() : hud.show());
follow(sagl.hud.serverHidden);
sagl.hud.onChange(follow);

sagl.splash

sagl.splash.show(options?: { text?: string; fadeMs?: number }): void
sagl.splash.hide(options?: { fadeMs?: number }): void
sagl.splash.visible: boolean

The splash screen, on this game: the server's loading screen over everything, the game's sound muted, and hiding it waits for the world around the camera to load.


Chat

sagl.chat

sagl.chat.print(text, color?, options?: { markdown? }): void   // a line in this player's chat ({RRGGBB} colours, or markdown)
sagl.chat.send(text): void            // as if typed: chat, or a /command
sagl.chat.open(text?), close(), isOpen(): boolean
sagl.chat.clear(): void               // every line in this player's chat gone

sagl.commands

const { command, arg, CommandError } = sagl.commands;
sagl.commands.register(...commands); sagl.commands.unregister(name);

The script's own commands, built as the server's (Chat and commands): suggested in the chat with the server's, run here, and the server never hears them. The executor is fn(args, line) (there's no player: it's this one). arg.player() gives { id, name, ... } as sagl.world.players() has; requires and custom arguments are the server's only.

sagl.commands.register(
  command("dice").description("Roll a die").argument("sides", arg.integer({ min: 2 }), { optional: true })
    .executes(({ sides = 6 }) => sagl.chat.print(`You rolled ${1 + Math.floor(Math.random() * sides)}`)),
);

Dialogs

sagl.dialog(options), sagl.hideDialog()

As the server's player.dialog: same styles, markdown option and answer, here only.

sagl.screenEffects

sagl.screenEffects.add(effect: string, options?: { params?; order?; fadeMs? }): { file, params, active, set(params), remove(fadeMs?) }
sagl.screenEffects.define(name: string, file: string, defaults?: Record<string, number | number[]>): void
sagl.screenEffects.names: string[]

As the server's player.screenEffects: the built-in effects or your shaders over this player's screen (with list, clear, save, restore and snapshot too), here only, changed without a trip to the server (to follow something each frame). They go when the scripts stop. Names a script defines are its own, apart from the server's.

sagl.menu(options)

As the server's player.menu: the same options, and the same menu back (row, isOpen, update, close). Its onChoose runs here at once, with no trip to the server, so previews are instant. A server's menu and a script's replace each other.

The world

sagl.world

sagl.world.players(): { id, name, local, here, position, heading, health, armour, vehicle }[]
sagl.world.peds(): { id, model, here, position, heading, health, armour, vehicle, dead }[]
sagl.world.vehicles(): { id, model, here, position, heading, health, searchlight, smoke, indicators }[]
sagl.world.objects(): { id, model, here, position, heading, local }[]
sagl.world.player(id), ped(id), vehicle(id), object(id)   // one, or null
sagl.world.near(at, radius): { players, peds, vehicles, objects }
sagl.world.distance(a, b): number
sagl.world.trafficLights(): { northSouth, eastWest, pedestrians, frozen }

What this game knows of the world, read only, as it is now (each call reads it again): every player (this one: local), the server's peds, vehicles and objects. here is false for one this game hasn't made (too far away); then position and heading are null. vehicle is the id of the vehicle they're in, or null. Positions are an entity's middle (a player's is about 1 m above the ground). objects() lists this script's own too (local: true). A vehicle's searchlight (helicopters) and smoke (planes) are whether they're on, indicators what its indicators show ("off", "left", "right", "hazard"). trafficLights() is the traffic lights now, as the server runs them (each "green", "yellow", "red", "off" or "flashing"; see TrafficLights).

// Is anyone else stood at the phone?
const busy = sagl.world.players().some((p) => !p.local && p.position &&
  Math.hypot(p.position.x - PHONE.x, p.position.y - PHONE.y) < 1.2);

Things of the script's own

Objects, labels, pickups and interactions a script makes are its own: shown on this game only, handled by the script (nothing goes to the server unless it says so with sagl.emit), gone when scripts reload. They work as the server's (Objects, pickups and labels, Interactions).

sagl.objects.create(model, position, rotation?, options?)

sagl.objects.create(model, position, rotation?: { x?, y?, z? }, options?: { collision?: boolean; breakable?: boolean; interior?: number; streamDistance?: number })
  : { id, model, position, rotation, exists, move(position, rotation?, ms?): Promise<void>, collision (set), onHit(fn), destroy({ shatter? }),
      setMaterial(index, options), setMaterialText(index, text, options?), resetMaterial(index?) }

A breakable one (breakable: true) reports this player's shots and crashes to onHit(fn(damage, weapon)); breaking it is the script's to decide (destroy({ shatter: true })).

const crate = sagl.objects.create(1224, { x: 2472, y: -1670, z: 13.7 }, { z: 20 }, { breakable: true });
let health = 60;
crate.onHit((damage) => { if ((health -= damage) <= 0) crate.destroy({ shatter: true }); });

setMaterial, setMaterialText and resetMaterial draw its materials otherwise, as the server's (Materials):

const atm = sagl.objects.create(2942, { x: 2486, y: -1670, z: 13.34 }, { z: 180 });
atm.setMaterialText(1, "{ff4040}OUT OF\n{ffffff}ORDER", { fontSize: 44, bold: true, align: 1, background: "#000000" });

sagl.models.add(id, { base, dff, txd? })

A model of the script's own, as the server's server.models.add: DFF and TXD client files, an id from -1000 to -30000, used by its objects, pickups and sagl.player.attachObject. The server's and scripts' ids are one set: the first to take an id has it. Only this game has it (others see its base, if anything of the script's shows them it).

sagl.labels.add(text, at, options?)

sagl.labels.add(text, at, options?: { color?; distance?; throughWalls?; offset?; interior? }): { text, color (set), remove() }

Floating text, as the server's: at is a spot, one of the script's objects or pickups, or { player | ped | vehicle | object | pickup: id } (the server's).

sagl.pickups.create(options)

sagl.pickups.create(options: { position; model?; weapon?; inVehicle?; label?; interior? })
  : { id, position, onPickup(fn), hide(), show(), remove() }

A pickup only this player sees: onPickup(fn) when they walk (or, with inVehicle, drive) into it, again each second while they're on it (hide or remove it). What they get is the script's to decide: give something the server owns (money, weapons, health) by asking it with sagl.emit.

sagl.interactions.add(options)

sagl.interactions.add(options: { models?: number[]; at?; radius?; prompt? })
  : { id, onUse(fn(use)), onReach(fn(use, inReach)), remove() }

As the server's interactions, handled here: use is { model, position, heading }.

const phone = sagl.interactions.add({ at: PHONE, prompt: "Press ~k~~VEHICLE_ENTER_EXIT~ to answer the phone" });
phone.onUse(() => answer());

Branding

sagl.branding

readonly branding: {
  name: string;               // the server's name
  primary: string | null;     // "#rrggbb", or null for SAGL's own
  secondary: string | null;
  logo: string | null;        // a URL for RML's src, once it's downloaded
}

The server's branding, as it was when the scripts started.

const { name, primary, logo } = sagl.branding;
doc.setText("title", name);
if (primary) doc.setAttribute("title", "style", `color: ${primary}`);
if (logo) doc.setHtml("logo", `<img src="${logo}" width="48dp" height="48dp"/>`);