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,nilor"notRegistered"for one another resource registered.Config.Sdkinshared/unlocked/sdk.luaturns storage, rooms and casting on or off for the pages, and itsallowlistlimits 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, andcharacter:<identifier>for a licence whileConfig.Sdk.store.licenceOwneris"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,SdkRoomsRegistrationandSdkCastRegistrationare intypes/sdk.luainsidezyke_laptop.extras/sdk/zyke_laptop.luadeclares every export for LuaLS: add it toworkspace.libraryand type the exports withexports.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
keyis left out. Returnsnilfor a key that isn't set, a target with no account, or an app your resource didn't register withstorage. Pass"site"askindfor 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
valueisnil. Open pages of the app on the account's laptops get thestorageevent. Returnstrue, orfalseand 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 itsmaxBytes."notRegistered": your resource didn't register the app withstorage.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
accessrules, theCanUseLaptopApphook inserver/unlocked/hooks.lua, and its licence when it has a price. Returnstrue, orfalseand"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. Returnstruefor an app without a price and whileConfig.Sdk.store.paidAppsis off, andfalsefor 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
falsefor 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. Returnsfalsewhen 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
nilwhile it is off, signed out or in the Crowbar recovery toolkit. It never asks the player, unlikeLaptop.account().
kindis"nimbus","local","guest"or"live"(Veil and other systems booted from a stick, named"amnesia").addressis the Nimbus address, only for a Nimbus account.appUserIdis the same idLaptop.account()gives the page: stable per account and app, random per guest and live session, and never an identifier. It only comes whenappIdis an app or site your resource registered.jobis{name, label, grade, gradeLabel, onDuty}, only whileConfig.Sdk.account.jobis 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 thesdkkeys inshared/unlocked/sounds.lua.Returns
falsefor 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.playinstead.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.
truefor one of the laptop's own pictures by its relative path, a file of a resource at anui://orhttps://cfx-nui-address, adata:image/png,jpeg,webporgifbase64 image, and anhttps://address on a host incast.imageHostsin 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
maxLengthcharacters. Returnsnilfor a value that isn't a string, isn't valid UTF-8, holds nothing but control characters, or is over four timesmaxLengthin 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
datato every member of the room, or to one player in it, as a messagefrom"server". The data has to encode to JSON within the room'smessageBytes. Returnsfalsefor 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}, wheredatais what yourjoincheck gave them. Pages only ever see thememberId.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
closewith the reason"kicked", the others getleave. Returnsfalsewhen 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
closewith the reason"closed". Returnsfalsefor 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
kindis"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:DataStickChangedfires.
#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,niland"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
niland"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.
modifiedis in milliseconds. Returnsnilfor 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
niland"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, orfalseand"invalid"(also for an unknown stick),"exists"when a file is already there andoverwriteisn'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, orfalseand"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, orfalseand"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_healthprints, by resource: every resource with a registration, or the one given. Each entry holds:
state, the resource's state, andregistrations, its apps and sites.roomsandmembersin its live rooms, andcastsrunning.events: counts in the last hour, such asnotRegistered,rateLimited,castErrorandroomJoinError.payments:{pending, settling, review, unfulfilled}, whereunfulfilledcounts settled payments not fulfilled after 10 minutes. Only whileConfig.Sdk.payments.enabledis 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:
| Command | Description |
|---|---|
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.