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)¶
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.off(channel, handler?)¶
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?)¶
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.
Logging¶
sagl.log(...values)¶
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.screenshot()¶
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?)¶
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.
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¶
The player's id, as the server knows it (player.id).
sagl.player.position()¶
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()¶
Returns the direction the player faces, in degrees.
sagl.player.health(), sagl.player.armour()¶
sagl.player.inVehicle()¶
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)¶
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?)¶
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)¶
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.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?)¶
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.