Documentation

App Services

Server exports behind the apps and sites your resource registers, such as their storage, licences, rooms and data sticks.

Apps and sites you register with Register App or Register Site run their page on the player's client, where the JavaScript SDK asks the laptop for storage, the account, rooms, casting and more. Your server sees the same things through the exports on this page: it reads and writes an app's storage, checks access and licences, reads the signed-in account, plays world sounds for the app, talks in the app's rooms, and makes and reads data sticks.

For payments and the store's paid apps, see Payments.

General Information

Every export on this page is a server export, except Set App Badge, which is a client export on the Apps & Sites page.

Most of them only work on an app or site the calling resource registered, and answer false, nil or "notRegistered" for one another resource registered. Config.Sdk in shared/unlocked/sdk.lua turns storage, rooms and casting on or off for the pages, and its allowlist limits them to the resources you name.

A target is a player's server id or an owner key. A server id reads the account the player's laptop is signed in to now, so it only works while their laptop is on and signed in. An owner key names an account directly: nimbus:<cloudId> for a Nimbus account, drive:<serial>:<userId> for a local account on a drive, and character:<identifier> for a licence while Config.Sdk.store.licenceOwner is "character".

Exports marked Yields wait on the database. Call them from a thread, an event handler or a command, not while your script loads.

Types / Classes

SdkAccount, SdkStickFileInput, SdkRoomsRegistration and SdkCastRegistration are in types/sdk.lua inside zyke_laptop. extras/sdk/zyke_laptop.lua declares every export for LuaLS: add it to workspace.library and type the exports with exports.zyke_laptop --[[@as ZykeLaptopServerExports]].

#Storage

An app or site registered with storage = true or storage = {maxBytes = 65536} keeps data per account on the server, which its page reads and writes with Laptop.storage. The data follows the account to every laptop it signs in to. A guest account and Veil keep it only until the laptop powers off, unless Config.Sdk.storage.veil is "persistent", which keeps Veil's per character.

Keys are up to 64 letters, digits, _, -, . and :. Values are anything that encodes to JSON, 8 KiB per key (Config.Sdk.storage.maxValueBytes). An app takes 32 KiB unless its registration asks for more, up to Config.Sdk.storage.maxAppBytes. The newest write of a key wins.

What a page stored came from the client, so check it like any client input before it decides anything.

#Get App Storage

Yields the first time an account is read. Returns one key's value, or every key as a table when key is left out. Returns nil for a key that isn't set, a target with no account, or an app your resource didn't register with storage. Pass "site" as kind for a site's storage.

Example:

lua
---@param target integer | string @ Player server id or owner key
---@param appId string
---@param key? string @ Every key when left out
---@param kind? "app" | "site" @ "app" when left out
---@return any value
local plan = exports["zyke_laptop"]:GetAppStorage(plyId, "fleeca_bank", "plan")

#Set App Storage

Yields the first time an account is read. Writes one key, or removes it when value is nil. Open pages of the app on the account's laptops get the storage event. Returns true, or false and one of:

  • "invalid": the target has no account, the key is invalid or the value can't be JSON.
  • "tooLarge": the value is over 8 KiB.
  • "quota": the app's data would go over its maxBytes.
  • "notRegistered": your resource didn't register the app with storage.

Example:

lua
---@param target integer | string @ Player server id or owner key
---@param appId string
---@param key string
---@param value any @ nil removes it
---@param kind? "app" | "site" @ "app" when left out
---@return boolean saved
---@return string? err
local saved, err = exports["zyke_laptop"]:SetAppStorage(plyId, "fleeca_bank", "plan", {tier = "premium", expires = os.time() + 2592000})

#Access and Licences

An app's access rules and price are set in its registration. The SDK checks them on every call a page makes. These exports answer the same for your own NUI callbacks and server events.

#Can Use App

Yields. Whether the player may use the app now: its access rules, the CanUseLaptopApp hook in server/unlocked/hooks.lua, and its licence when it has a price. Returns true, or false and "notRegistered", "noAccess" or "notLicensed". Answers are kept for 5 seconds per player and app, and a job change clears them at once.

Use it in the callbacks behind Laptop.post, since a page's request can be faked.

Example:

lua
---@param plyId integer
---@param appId string
---@param kind? "app" | "site" @ "app" when left out
---@return boolean allowed
---@return string? code
local allowed, code = exports["zyke_laptop"]:CanUseApp(plyId, "mdt_tools")
if (not allowed) then return {error = code} end

#Has App Licence

Yields. Whether the player's current account, or their character with Config.Sdk.store.licenceOwner = "character", owns the app. Returns true for an app without a price and while Config.Sdk.store.paidApps is off, and false for an unknown app or a guest or Veil session under "account". Works for any resource's app.

Example:

lua
---@param plyId integer
---@param appId string
---@param kind? "app" | "site" @ "app" when left out
---@return boolean licensed
local licensed = exports["zyke_laptop"]:HasAppLicence(plyId, "fleeca_bank")

#Grant App Licence

Yields. Gives a licence to one of your resource's apps without a payment, such as for a promotion. The app counts as bought. Returns false for another resource's app, an unknown app, or a target that can't own a licence, such as a guest.

Example:

lua
---@param target integer | string @ Player server id or owner key
---@param appId string
---@return boolean granted
exports["zyke_laptop"]:GrantAppLicence(plyId, "fleeca_bank")

#Revoke App Licence

Yields. Takes a licence to one of your resource's apps away. The app counts as not installed again and its server calls answer notLicensed. Returns false when there was no licence to take. A refunded store purchase takes its licence back on its own.

Example:

lua
---@param target integer | string @ Player server id or owner key
---@param appId string
---@return boolean revoked
exports["zyke_laptop"]:RevokeAppLicence(plyId, "fleeca_bank")

#Account

#Get Laptop Account

Yields. The account the player's laptop is signed in to now, or nil while it is off, signed out or in the Crowbar recovery toolkit. It never asks the player, unlike Laptop.account().

  • kind is "nimbus", "local", "guest" or "live" (Veil and other systems booted from a stick, named "amnesia").
  • address is the Nimbus address, only for a Nimbus account.
  • appUserId is the same id Laptop.account() gives the page: stable per account and app, random per guest and live session, and never an identifier. It only comes when appId is an app or site your resource registered.
  • job is {name, label, grade, gradeLabel, onDuty}, only while Config.Sdk.account.job is on, and never on Veil or as guest.

Example:

lua
---@param plyId integer
---@param appId? string
---@param kind? "app" | "site" @ "app" when left out
---@return SdkAccount? account
local account = exports["zyke_laptop"]:GetLaptopAccount(plyId, "fleeca_bank")
if (account) then print(account.kind, account.name, account.appUserId) end

#Sounds

#Play App Sound

Plays one of the app sounds at the player's ped, for everyone near them, through zyke_sounds: "keypad", "plug", "notice" or "cardReader". Use it for something physical others should hear, such as a card or PIN step. Their files, volume and range are the sdk keys in shared/unlocked/sounds.lua.

Returns false for an unknown name, a resource with no registered app or site, a player whose laptop is off, more than a few sounds a second for one player, or the same sound in the same area within the last second.

For a sound only the player at the laptop hears, the page calls Laptop.sound.play instead.

Example:

lua
---@param plyId integer
---@param name "keypad" | "plug" | "notice" | "cardReader"
---@return boolean played
exports["zyke_laptop"]:PlayAppSound(plyId, "cardReader")

#Casting

An app registered with cast shows its own viewer page on the screens the laptop casts to. Your snapshot, delta and signal functions check what the window sends, and what they return is what every viewer sees. See Casting for the registration and Laptop.cast in the Casting.

#Is Cast Image Allowed

Whether every viewer's game may load an image, so your check can drop the rest. true for one of the laptop's own pictures by its relative path, a file of a resource at a nui:// or https://cfx-nui- address, a data:image/png, jpeg, webp or gif base64 image, and an https:// address on a host in cast.imageHosts in the Config. Any other host would learn who looked at the screen. Addresses over 512 characters and paths with .. are refused.

Example:

lua
---@param url any
---@return boolean allowed
local logo = exports["zyke_laptop"]:IsCastImageAllowed(content.logo) and content.logo or nil

#To Cast Text

Cleans text for a viewer: drops control characters and cuts it to maxLength characters. Returns nil for a value that isn't a string, isn't valid UTF-8, holds nothing but control characters, or is over four times maxLength in bytes.

Example:

lua
---@param value any
---@param maxLength integer @ Characters
---@return string? text
local title = exports["zyke_laptop"]:ToCastText(content.title, 80)

#Rooms

An app or site registered with rooms lets its pages on different laptops talk in realtime with Laptop.rooms.join. A room exists while it has members. See Rooms for the registration and the join check.

The server exports below only reach rooms of an app your resource registered, not of a site, and answer false or an empty list for a room nobody is in.

#Send Room Message

Sends data to every member of the room, or to one player in it, as a message from "server". The data has to encode to JSON within the room's messageBytes. Returns false for an empty or unknown room, data that is too large, or a player who isn't a member.

Example:

lua
---@param appId string
---@param room string
---@param data any
---@param toPlyId? integer @ One member, everyone when left out
---@return boolean sent
exports["zyke_laptop"]:SendRoomMessage("fleeca_bank", "lobby", {type = "rates", savings = 0.02})

#Get Room Members

Returns the members of the room as {plyId, memberId, data}, where data is what your join check gave them. Pages only ever see the memberId.

Example:

lua
---@param appId string
---@param room string
---@return {plyId: integer, memberId: string, data?: any}[] members
local members = exports["zyke_laptop"]:GetRoomMembers("fleeca_bank", "lobby")

#Kick From Room

Removes a player from the room. Their page gets close with the reason "kicked", the others get leave. Returns false when they aren't a member.

Example:

lua
---@param appId string
---@param room string
---@param plyId integer
---@return boolean kicked
exports["zyke_laptop"]:KickFromRoom("fleeca_bank", "lobby", plyId)

#Close Room

Removes every member. Their pages get close with the reason "closed". Returns false for an empty or unknown room.

Example:

lua
---@param appId string
---@param room string
---@return boolean closed
exports["zyke_laptop"]:CloseRoom("fleeca_bank", "lobby")

#Room Event

zyke_laptop:RoomEvent is a server local event that fires when a player joins or leaves a room of any app or site, including a kick, a close, a dropped player and a laptop that powers off or goes offline. data is what the join check gave the member.

lua
---@param appId string @ The app or site id
---@param room string
---@param action "join" | "leave"
---@param plyId integer
---@param data any
AddEventHandler("zyke_laptop:RoomEvent", function(appId, room, action, plyId, data)
    if (appId ~= "fleeca_bank") then return end

    print(("%d %s %s"):format(plyId, action == "join" and "joined" or "left", room))
end)

#Data Sticks

Data sticks are the laptop's USB drives. Their files are kept on the server by the stick's serial and go wherever the item goes. These exports make sticks with files and read and change any stick by its serial, such as a heist that hands out plans and checks that a file came back.

They need ports.dataStick in the Config to be set; with it false they answer "disabled".

  • A path is relative, such as "plans/vault.txt": up to 8 parts split by /, each up to 255 characters, without ., .., control characters or a leading /. Missing folders are made on write.
  • A file's kind is "text", the default, or "key".
  • All of a stick's files together, encoded, have to fit ports.dataStick.maxBytes, 128 KiB by default. A write that doesn't fit fails with "full" and changes nothing.
  • A label is trimmed and cut to 32 characters, and can't hold control characters.
  • Laptops with the stick plugged in reload it after every change, and zyke_laptop:DataStickChanged fires.

#Create Data Stick

Yields. Makes a stick with files without giving the item. Returns the serial and the item metadata to give the stick with, or nil, nil and "invalid", "full" or "disabled". Up to 200 files, and two files on the same path are invalid.

Example:

lua
---@param options {label?: string, files?: SdkStickFileInput[]}
---@return string? serial
---@return table? metadata
---@return string? err
local serial, metadata, err = exports["zyke_laptop"]:CreateDataStick({
    label = "VAULT_PLANS",
    files = {
        {path = "plans/vault.txt", content = "Door A opens at 03:00"},
        {path = "keys/door_a.key", content = "4f2a...", kind = "key"},
    },
})

#Give Data Stick

Yields. Makes a stick like Create Data Stick and adds it to the player's inventory. Returns the serial, or nil and "invalid", "full", "disabled" or "noSpace" when the inventory refused the item.

Example:

lua
---@param plyId integer
---@param options {label?: string, files?: SdkStickFileInput[]}
---@return string? serial
---@return string? err
local serial, err = exports["zyke_laptop"]:GiveDataStick(plyId, {
    label = "VAULT_PLANS",
    files = {{path = "plans/vault.txt", content = "Door A opens at 03:00"}},
})

#Data Stick Exists

Yields. Whether a stick with the serial exists.

Example:

lua
---@param serial string
---@return boolean exists
local exists = exports["zyke_laptop"]:DataStickExists(serial)

#Get Data Stick Files

Yields. Lists every file on the stick, sorted by path, and returns the stick's label second. modified is in milliseconds. Returns nil for an unknown stick.

Example:

lua
---@param serial string
---@return {path: string, kind: string, size: integer, modified: integer}[]? files
---@return string? label
local files, label = exports["zyke_laptop"]:GetDataStickFiles(serial)

#Read Data Stick File

Yields. Returns a file's content, or nil and "invalid", "notFound" or "disabled".

Example:

lua
---@param serial string
---@param path string
---@return string? content
---@return string? err
local content = exports["zyke_laptop"]:ReadDataStickFile(serial, "found/codes.txt")

#Write Data Stick File

Yields. Writes a file. Returns true, or false and "invalid" (also for an unknown stick), "exists" when a file is already there and overwrite isn't set or a folder holds the path, "full" or "disabled".

Example:

lua
---@param serial string
---@param path string
---@param content string
---@param options? {overwrite?: boolean, kind?: "text" | "key"}
---@return boolean written
---@return string? err
exports["zyke_laptop"]:WriteDataStickFile(serial, "plans/vault.txt", "Door A opens at 04:00", {overwrite = true})

#Delete Data Stick File

Yields. Deletes a file. Folders are left alone. Returns true, or false and "invalid", "notFound" or "disabled".

Example:

lua
---@param serial string
---@param path string
---@return boolean deleted
---@return string? err
exports["zyke_laptop"]:DeleteDataStickFile(serial, "plans/vault.txt")

#Find Data Stick Files

Yields. Returns the sorted paths of the files whose path matches a Lua pattern, or contains the text with plain = true. An unknown stick or a broken pattern finds nothing.

Example:

lua
---@param serial string
---@param pattern string
---@param options? {plain?: boolean}
---@return string[] paths
local keys = exports["zyke_laptop"]:FindDataStickFiles(serial, "%.key$")

#Set Data Stick Label

Yields. Renames the stick. Returns true, or false and "invalid", "notFound" or "disabled".

Example:

lua
---@param serial string
---@param label string
---@return boolean labelled
---@return string? err
exports["zyke_laptop"]:SetDataStickLabel(serial, "EMPTY")

#Data Stick Changed

zyke_laptop:DataStickChanged is a server local event that fires after a stick's files or label changed. by is "player" when a player saved it from a laptop, or the name of the resource whose export changed it.

lua
---@param serial string
---@param by string @ "player" or a resource name
AddEventHandler("zyke_laptop:DataStickChanged", function(serial, by)
    if (by ~= "player" or serial ~= heistStick) then return end

    local content = exports["zyke_laptop"]:ReadDataStickFile(serial, "found/codes.txt")
    if (content and content:find("7351", 1, true)) then finishHeist() end
end)

#Developer Tools

#Get Sdk Health

Yields. What laptop_sdk_health prints, by resource: every resource with a registration, or the one given. Each entry holds:

  • state, the resource's state, and registrations, its apps and sites.
  • rooms and members in its live rooms, and casts running.
  • events: counts in the last hour, such as notRegistered, rateLimited, castError and roomJoinError.
  • payments: {pending, settling, review, unfulfilled}, where unfulfilled counts settled payments not fulfilled after 10 minutes. Only while Config.Sdk.payments.enabled is on.
  • storage: {rows, bytes} of its apps' stored data.

Example:

lua
---@param resource? string @ Every resource that uses the SDK when left out
---@return table<string, table> health @ By resource
local health = exports["zyke_laptop"]:GetSdkHealth("fleeca_bank")["fleeca_bank"]
if (health.payments and health.payments.review > 0) then print("Payments need a staff check") end

The commands, run from the server console or by players with the ACE in Config.Sdk.dev.permission:

CommandDescription
laptop_sdk_check [resource]Lists the registrations and the warnings they printed, pages that aren't files of the resource, apps without an icon, payees the config refuses and unknown access jobs. It also finds code still using names the SDK removed, such as the client RegisterApp.
laptop_sdk_health [resource]Prints what Get Sdk Health returns.
laptop_sdk_reload <resource>Reloads the resource's open pages on every laptop without a restart. Needs Config.Sdk.dev.hotReload.

With Config.Sdk.dev.hotReload = true, restarting a resource keeps its windows open: its registrations stay for 5 seconds, and when it registers again in time the pages reload and get their saveState back. Leave it off on a live server.