Pulse Phone: exports, events and state bags
This is the whole surface other resources can use. The phone resource is named phone.
phone_api in the server console prints the add-on (app) API as a short summary.
To add your own app to the phone, see CUSTOM_APPS.md next to this file.
1. State bags
The server writes these to Player(src).state. They are replicated, so clients read them as
LocalPlayer.state.<key>, or Player(serverId).state.<key> for other players. The names match
the ones other popular phone resources use, so existing HUD, dispatch and evidence scripts read
them unchanged.
| Key | Type | Meaning | Set from |
|---|---|---|---|
phoneNumber |
string | false | number of the phone in hand (turn off with Config.Public.numberState = false) |
device load, provisioning, unique-phone switch |
phoneOpen |
boolean | phone out, screen shown | client open/close, throttled flag event |
phoneDisabled |
boolean | disabled by SetPhoneDisabled |
server export |
flashlight |
boolean | torch on | client torch toggle |
airplaneMode |
boolean | airplane mode | client toggle |
inCall |
boolean | has a live call: dialling, ringing out or connected | server call records |
callAnswered |
boolean | the call is connected | server |
onCallWith |
number | false | server id of the other party (player-to-player) | server |
speakerphone |
boolean | speaker on during a call | server (phone:call:speaker) |
mutedCall |
boolean | this player muted their microphone on the call | server (phone:call:signal) |
otherMutedCall |
boolean | the other party muted | server |
phoneItem |
boolean | carries a phone item (Config.Item.required or unique phones) |
server inventory check |
phoneBlocked |
boolean | the player's state blocks the phone (dead, cuffed, escorted ... Config.Block) |
client state check, verified by the server |
phoneEmergencyOnly |
boolean | the phone opens on the emergency screen only (for example downed, Config.Block) |
client state check, verified by the server |
phoneColour |
string | false | unique phones only: finish id of the phone in hand | item metadata / Config.Item.variants |
phoneBattery |
number | battery percent (0-100); other players' copies may lag (see below) | server battery loop |
phoneCharging |
boolean | charging now | server |
phoneDead |
boolean | battery empty: apps refused, calls ring out | server |
phone:anim, phone:anim:prop |
internal | animation mirroring for other clients | anim controller |
Two limits:
- An incoming call sets
inCallon the callee only once it is answered. - The three client flags (
phoneOpen,flashlight,airplaneMode) are only accepted as booleans, through one rate-limited event. No other key can be set from a client. phoneBatteryis throttled for everyone except the phone's owner: it is written at once when charging starts or stops, when the phone dies, at 0, 20 and 100 %, and at every step below 20 %; otherwise it can lag up to 30 s. The owner'sexports.phone:GetBattery()(client) and the server'sGetBattery(src)are always exact.
2. Server events
Listen with AddEventHandler(name, fn) in a server script. Only the phone raises these. They
are not net events, so a client cannot forge them.
| Event | Arguments |
|---|---|
phone:numberChanged |
src, number \| nil, oldNumber \| nil |
phone:toggled |
src, open |
phone:callStarted |
callId, fromNumber, toNumber, fromSrc, toSrc (nil: not known yet), video |
phone:callAnswered |
fromNumber, toNumber, fromSrc, toSrc, callId |
phone:callEnded |
fromNumber, toNumber, reason, durationMs, fromSrc, toSrc, callId |
phone:emergencyCall |
src, service ('police' \| 'ems' \| 'fire' \| ...), reason \| nil, coords (vector3) \| nil (Smart 911) |
phone:emergencyText |
src, service, reason \| nil, body (Smart 911 emergency text) |
phone:messageSent |
fromNumber, toNumber, body |
phone:groupMessageSent |
fromNumber, groupId, body |
phone:mailSent |
fromAddress, toAddress, subject |
phone:wallet:transfer |
fromNumber, toNumber, amount, note |
phone:photoSaved |
ownerNumber, url, kind ('photo' \| 'portrait' \| 'video') |
phone:socialPost |
authorNumber, app, postId, caption, url |
phone:mediaStored |
url, kind: a file the phone's upload stored |
phone:mediaRefused |
src \| nil, url, where: a link the allow-list refused |
phone:calendar:saved |
ownerNumber, event |
phone:battery:died |
src |
3. Client events
Listen with AddEventHandler in a client script. These are local and cannot be sent from the network.
| Event | Arguments |
|---|---|
phone:toggled |
open, reason? |
phone:openRefused |
reason: 'disabled', 'nui-focused' or 'check:<resource>' |
phone:numberChanged |
number \| nil |
4. Server exports (exports.phone:…)
| Export | Returns | Notes |
|---|---|---|
GetPhoneNumber(src) |
number | nil | from cache; nil until the player has loaded |
SourceOfNumber(number) |
src | nil | online players only |
FormatNumber(number) |
string | in Config.Numbers.format |
GetEmail(src) |
address | nil | |
GetEmailOfNumber(number, cb) |
cb(address \| nil) |
works for offline players too |
HasPhoneItem(src, number?) |
boolean | always true when Config.Item.required is off; with unique phones number asks for that phone |
Notify(src, { app?, title, body, icon?, colour?, sticky?, actions?, deepLink? }) |
boolean | |
SetBadge(src, appId, count) |
boolean | registered add-ons only |
SendMessage(src, number, text) |
none | as that player |
SendMail(address, sender, subject, body) |
none | |
AddContact(ownerNumber, { name, number, avatar? }) |
boolean | owner must be online |
Call(src, number, video?) |
none | places a call |
IsInCall(src) |
boolean, callId | |
EndCall(src) |
boolean | |
SetPhoneDisabled(src, bool) / IsPhoneDisabled(src) |
boolean | closes the phone if it is open |
RecordTransaction(number, 'sent' \| 'received', amount, party, note?) |
boolean | statement line only; does not move money |
GetWalletBalance(number, cb) |
cb(balance \| nil) |
|
UploadMedia(dataUrl, kind, cb) |
cb(url, reason) |
through the configured provider |
IsMediaAllowed(url) |
boolean | the server allow-list (Config.Media.allow) |
Log(action, title, fields, src?) |
none | writes to the phone's Discord/Fivemanage log |
GetBattery(src) / SetBattery(src, percent) |
number | server-authoritative level |
IsCharging(src) / IsPhoneDead(src) |
boolean | |
ToggleCharging(src, on?, ratePerMin?) |
boolean | an external charger (a cable item, a station script) |
UsePowerbank(src) |
boolean | single-use (reusable = false, the default): removes one Config.Battery.powerbank.item and charges amount % over time; reusable bank (reusable = true): plugs the phone in or out, charging to 100% |
SaveBattery(src) / SaveAllBatteries() |
none | levels also save every minute and on leave |
RegisterApp / UnregisterApp / EmitApp / BroadcastApp / GetApps / Set / Get |
add-on apps (phone_api) |
|
RegisterProvider / UnregisterProvider / GetProvider / HasCapability / BridgeReport / GetCharacterId |
providers (custom_provider_template.lua in this folder) |
|
RegisterFrameworkBridge, RegisterSite, RegisterMarketsEconomy, AddDirectoryEntry |
see the files named in phone_api |
|
PhoneTaken / PhoneReturned / PhoneLocked / PhoneBypass / PhoneCustody / PhoneImei |
stolen phones | |
DropPhone / DroppedPhones, PairAccessory / UnpairAccessory / SetAccessoryBattery / RegisterAccessoryKind, IsBlocked, MarketsQuotes / MarketsShock |
as named | |
ClearNotifications(src, app?) |
boolean | empties the notification list on that player's phone (all, or one app id) |
StartActivity(src, def) |
id | false, reason |
an Island activity on the Island and Lock Screen (section 14) |
UpdateActivity(src, id, patch) / EndActivity(src, id) / EndAllActivities(src?) |
boolean / count | your own resource's activities only (section 14) |
PlaceScriptCall(target, opts) |
callId | false, reason |
ring a player from a script identity: "Unknown", "Dispatch" (section 15) |
EndScriptCall(callId) / GetScriptCallState(callId) |
boolean / 'ringing' | 'connected' | nil |
your own script calls only (section 15) |
5. Client exports (exports.phone:…)
| Export | Notes |
|---|---|
Open() / Close() / IsOpen() |
Open respects every veto below |
AddOpenCheck(fn) → id / RemoveOpenCheck(id) |
fn() returning false refuses the open. Checks are removed when their resource stops |
SetDisabled(bool) / ToggleDisabled() / IsDisabled() |
a local switch; the server's SetPhoneDisabled drives the same one |
GetPhoneNumber() |
from the state bag |
FormatNumber(number) |
|
IsInCall() |
|
Notify({ app?, title, body, icon?, colour?, sticky? }) |
shown by this player's phone only |
SetFlashlight(bool) / GetFlashlight() / GetAirplaneMode() |
|
GetSignalBars() / SetAirplaneMode(bool) |
|
RegisterCaptureProvider(p) / CaptureAvailable() |
camera capture backends |
Rpc(name, data) |
the phone's own request channel; internal |
useItem(data, slot) |
ox_inventory item hook for unique phones (client = { export = 'phone.useItem' }) |
usePowerbank() |
ox_inventory item hook for the power bank (client = { export = 'phone.usePowerbank' }) |
GetBattery() / IsCharging() / IsPhoneDead() |
this player's own level, exact; setting the level is server-only |
SetRestriction(key, mode \| nil) |
a player state of your own for Config.Block (a jail, a cutscene, surgery): 'block', 'emergency-only' or nil to clear it. Kept per calling resource and cleared when that resource stops. Add key to Config.Block.calls to choose what happens to a live call (unlisted: the call is kept) |
StartActivity(def) / UpdateActivity(id, patch) / EndActivity(id) / EndAllActivities() |
an Island activity on this player's phone, from a client script (section 14) |
Open vetoes, in order: disabled, then another resource holding NUI focus
(Config.Open.blockWhenNuiFocused), then each AddOpenCheck. A refusal raises
phone:openRefused with the reason. Checks made before this one still apply: the player's state
(Config.Block: dead, downed, cuffed, escorted, carried, jailed, under water, in the pause menu,
or a SetRestriction of another script) and the phone item.
6. Compatibility with other phones
Many scripts (police, ambulance, boss menus, garages, deliveries) send mail, texts,
notifications and job alerts using the event names other phone resources use. The phone
answers those events itself, so those scripts work without edits. phone_api in the server
console lists every event it answers.
A script that calls another phone's exports by that phone's resource name needs one change:
call exports.phone instead. These seven server exports cover what such scripts do, and any
script may use them directly:
| Export | What it does |
|---|---|
ResolvePhone(target[, cb]) |
target is { source }, { number }, { email }, { identifier } or { imei }. Returns { number, email, name, identifier, imei, source }: with cb, online or offline; without it, online only, immediately. |
DeliverMailTo(target, mail[, cb]) |
mail = { sender, subject, body, attachments? }. Calls cb(true, mailId) or cb(false, reason). |
SendText(from, to, body[, opts][, cb]) |
From an online phone, a stored message. From anything else, a live service message named opts.name. |
DispatchAlert(jobs, { title, body, coords? }) |
Notifies every on-duty member of the jobs; tapping the alert opens Maps at coords. Returns how many it reached. |
CompanyMessage(src \| nil, jobOrCompany, body[, opts][, cb]) |
From a player, a message to the company's Services inbox, or an alert when anonymous. From a script, an alert. |
PayBill(src, billId[, cb]) |
Pays one of the player's bills, as the Wallet does. |
IsOnDuty(src[, job]) |
Whether the player is on duty in job, or in their own job when job is omitted. |
7. Housing providers (Home app)
These are built in and chosen automatically when one of the scripts is running.
- Scripts:
qb-houses,qbx_properties,qs-housing,loaf_housing,vms_housing,rtx_housing,ps-housing,bcs_housing,nolag_properties,esx_property(bridge/server/housing.lua+bridge/client/housing.lua). - Priority: 6, so a provider you register at 10 wins.
- Pinning:
Config.Providers.housing = '<name>'pins one;'none'turns housing off. - Client relay: some scripts only expose client exports, or only accept their own menu's net
events. For those, the phone asks the owner's client to call that export or fire that event, and
the housing script's server validates it as usual. The relay (
phone:housing:ask/phone:housing:answer) is token-bound to the player it asked.
Vendor API each adapter uses (from each script's published source or documentation, cited in the adapter; not verified against live servers). "Owner's client" means the phone asks the owner's client to fire the event the housing script's own menu fires.
| Script | List | Lock | Keys |
|---|---|---|---|
qb-houses |
its tables player_houses, houselocations (read only) |
its own lock event, from the owner's client | its own give/remove events, from the owner's client |
qbx_properties |
its properties table (read only) |
none (shells) | list only (its key events need the owner inside) |
qs-housing |
server export GetPlayerHouses |
none exposed | none exposed |
loaf_housing |
client export GetOwnedHouses |
server exports IsDoorLocked / SetDoorLocked |
client exports GiveKey, RemoveKeyHolder, GetKeyHolders |
vms_housing |
server exports GetPlayerProperties / GetProperty |
none exposed | its give/remove key events, from the owner's client |
rtx_housing |
client export GetPlayerOwnedProperties |
its lock event + GlobalState | its permission events, from the owner's client |
ps-housing |
its properties table (read only) |
none (door lock script) | its addAccess / removeAccess events, from the owner's client |
bcs_housing |
server export GetOwnedHomeKeys |
server exports isLocked / LockHome (toggle) |
server exports GetKeyHolders, RemoveKeyHolder (giving a key stays in its menu) |
nolag_properties |
server export GetAllProperties |
server export ToggleDoorlock (sets the state) |
server exports GetKeyHolders, AddKey, RemoveKey, as the player |
esx_property |
server export GetProperties (with lock state) |
none exposed | list from GetProperties |
The loaf_housing calls could not be re-checked: its current version publishes no exports page.
8. Unique phones
Set Config.Item.unique = true. The number lives in the item's metadata:
| Key | Meaning |
|---|---|
phoneNumber |
the number |
phoneColour |
the finish |
description |
tooltip text |
Device rows for item phones are keyed item:<id>. Face Unlock enrols the character
(Identity.CharacterKey), not the handset. The item's finish reaches the interface as
bootstrap.device.colour and the phoneColour state bag. Drawing the handset in that finish
(Device3D finishId) belongs to the OS layer and is not wired yet.
9. Browser sites (RegisterSite)
The Browser app cannot reach the internet. A site is registered by a resource on the server, and a page is data (a title and a list of blocks) that the phone draws in its own style. A site cannot carry script, a stylesheet or a URL outside the game. Add-on pages in the The add-on page kinds (list, detail, board, form) are accepted and converted.
exports.phone:RegisterSite({
id = 'pdm', -- [a-z0-9_], also the event namespace
name = 'Premium Deluxe', -- shown on the start page tile
host = 'pdm.ls', -- optional; default '<id>.<Config.Browser.tld>'
colour = '#B8322F', glyph = 'car', -- tile art (built-in glyph set)
keywords = { 'cars', 'dealer' }, -- optional, for the search box
pages = { ['/'] = { title = 'Showroom', blocks = { ... } } },
resolve = function(src, path, cb) cb(page) end, -- optional, dynamic pages
submit = function(src, path, form, values, reply) end, -- optional, forms
})
Blocks: heading, text, stats, list, cards, links, form, directory, hero (a
branded banner with an optional picture), posts (short posts with author and time) and empty
(a designed "nothing here" panel). A list row, card or post may carry app + route to open a
phone app (Services, Wallet, Echo...) instead of a page.
Built-in sites. Eight ship by default, each drawn from data the server really has or
labelled as a brochure. Three are generated: the server's front page (name, players online,
phones issued, the apps on the phone), the phone directory, and a help site built from
Config. Five are in-world (<tld> is Config.Browser.tld):
| Site | Host | Content |
|---|---|---|
| City Services | city.<tld> |
Config.Services, live: who is on duty, call the line, message the company (opens Services) |
| City Wire | citywire.<tld> |
public Echo posts and LS Pages adverts from the server's own tables; a designed empty page otherwise |
| Harbor Trust | harbortrust.<tld> |
the caller's real balance from the bank provider, and the way into Wallet |
| Redline Motors | redline.<tld> |
a showroom brochure (Config.Browser.showroom): class, seats and list price per model, a page per car |
| Chromeworks | chromeworks.<tld> |
a customs price list (Config.Browser.customs) and the mechanic's line from Config.Services |
Replace the brochure data in config, or turn a site off with
Config.Browser.builtin = { redline = false, ... }.
10. Player data controls (requests for the interface)
Server requests the interface calls through the normal request bridge (fetchNui('phone:<name>', data)
on the page, phone:rpc on the wire). Each acts on the caller's own phone only, is rate limited by
the security layer, and answers { ok, reason? }.
| Request | Data | Effect |
|---|---|---|
calls:history:delete |
{ id } (a Recents row's id from bootstrap) |
hides that call from this phone's Recents; the other party keeps theirs. reason = 'not-found' for an id that is not this phone's |
calls:history:clear |
{} |
hides every call from this phone's Recents |
messages:thread:delete |
{ number } or { group } |
"Delete Conversation" for this phone only (same as messages:conversation:delete) |
messages:threads:delete |
{ threads = [ { number } \| { group }, ... ] } (at most 50) |
the same for several at once; answers { ok, deleted, failed } |
notifications:clear |
{ app? } |
empties the notification list (all, or one app id); the server relays it to the page as the NUI message notifications:clear { app? }, which the page must handle by calling clearNotifications(app) |
A call row stays in the database until both sides have deleted it. Recents from bootstrap no longer include deleted calls.
11. Owner tools (console, or ace phone.admin)
<id|number> is an online server id (up to five digits) or any phone number, online or offline.
Every command writes a security-log row and an activity-log entry.
| Command | Does |
|---|---|
phone:resetpin <id\|number> |
removes the passcode and Face Unlock; the lockout clears |
phone:wipe <id\|number> confirm |
erases the phone's own data (contacts, photos, notes, reminders, calendar, voicemail, memos, saved places, playlists, albums, blocks, mail) and hides its calls and conversations for that phone, then factory resets it. Without confirm it only says what it would erase |
phone:number <id\|number> <newnumber> |
a new number: must fit Config.Numbers.format and be unused; the phone's own data follows it (conversations and call history stay under the old number). phone:number with no arguments shows your own number. Refused with unique phones on (the number is on the item) |
phone:inspect <id\|number> |
a summary: number, mail, IMEI, created, online, setup, power, passcode, counts |
phone:retention [now] |
runs Config.Retention pruning now (now is required while retention is off) |
phone:health |
prints the start-up panel again, including voice conflicts and config problems |
12. Clock and retention settings
Config.Clock.mode = 'server' (the default) publishes the server's clock in the global state bag
phone:clock ({ hour, minute }), so every phone shows the same time; utcOffset pins a zone.
'game' follows the game's time of day (and so any weather/time sync script), 'real' is each
player's own computer clock, 'custom' a function in config.
Config.Retention (off by default) prunes messages, social DMs, Dark Chat, call history, social
activity and optionally mail older than the configured days, once a day at hour, in batches.
13. Bills, carrier, charging, store and payphones
All server side unless marked.
Server exports
| Export | Returns | Notes |
|---|---|---|
SendBill(sender, targetSrc, amount, reason, society?, cb?) |
id | false, reason |
sender: a player's server id, or a name for a script ('police', 'city_hall'). targetSrc must be online. society: optional job name ('police' or 'society_police'); payment then goes to that company's account. A script name with no society bills for the account of that name. cb(ok, idOrReason) runs once the bill is stored. Reasons: disabled, target, amount (not a whole number from 1 to Config.Bills.maxAmount), society, sender, self, rate (Config.Bills.perMinute), no-device |
GetBills(src, cb?) |
{ unpaid = {...}, paid = {...} } |
the phone's own bills plus esx_billing / okokBilling / qb-core phone_invoices rows, newest first; the paid list is capped at Config.Bills.paidHistory. Without cb it waits for the answer, so call it from a thread. Each bill: { id, source, from, company, number, reason, amount, at, status, paidAt } |
IsWalletCardLocked(src) |
boolean | true while that player has locked their phone's card in Wallet > Cards. A script that charges "the phone" (a shop's tap to pay) should refuse while it is locked, as the phone's own payments do |
HasService(src, service?) |
boolean | may this phone make a call / send a text / use data now ('call' default, 'text', 'data'). Always true while Config.Carrier.enabled is false |
GetCarrierStatus(src) |
table | nil | nil while the carrier is off or the line is not loaded. { service ('ok' \| 'no-plan' \| 'suspended' \| 'no-service'), bars, roaming, carrier, plan, job, line = { number, iccid, eid, status, autorenew, renewsAt, graceUntil, ... }, usage = { talkSeconds, texts, dataKB } } |
UseMagPack(src, slot?) |
boolean | as if the player used a MagPack item (ak47_inventory's onUse) |
UseChargingCable(src, slot?) |
boolean | as if the player used the cable item |
DetachChargers(src) |
takes the MagPack and cable off the phone | |
GetChargingState(src) |
{ pack, cable, source?, mode?, packLevel? } |
what is attached, and where the cable is plugged in |
SetPowerSource(src, name \| false) |
boolean | a power source your script provides (a house, a charging station): while set, the cable can be plugged in anywhere. false clears it and unplugs a cable using it |
UseBox(src, slot?, name?) |
boolean | as if the player used the sealed phone box (Config.Store.boxItem, or a per-finish box name) |
Client exports (ox_inventory item hooks)
| Export | Item entry |
|---|---|
usePack |
client = { export = 'phone.usePack' } on pulse_magpack |
useCable |
client = { export = 'phone.useCable' } on pulse_cable |
useBox |
client = { export = 'phone.useBox' } on pulse_phone_box (and the per-finish boxes) |
The server re-reads the slot and the item for each of these; nothing the client sends is trusted.
Ready-made item entries are in config/items/.
Server events
| Event | Arguments |
|---|---|
phone:bills:sent |
billId, ownerNumber, amount, reason, job \| nil |
phone:bills:paid |
fullId ('bill:<source>:<id>'), payerNumber, amount, job \| nil |
State bags
| Key | Type | Meaning |
|---|---|---|
phoneCharger |
'pack' | 'cable' | 'both' | nil |
the accessories on the phone in this player's hand; written only by the server |
phone:unbox |
internal | the unboxing scene other players see; written only by the server |
Payphone calls
A payphone call is an ordinary call of the phone's call system, so phone:callStarted,
phone:callAnswered and phone:callEnded fire for it. The caller's "number" is what
Config.Payphone.callerId shows: Config.Payphone.callerLabel (default Payphone), Unknown,
or the booth's own number. The payphone itself has no exports.
14. Island activities (Island and Lock Screen)
Another resource can show an Island activity on a player's phone: the same Island and Lock Screen card the phone's own timers and calls use, drawn by the same code. A delivery route, a heist countdown, a tow truck on its way.
Server (exports.phone:…)
local id, why = exports.phone:StartActivity(src, {
id = 'delivery', -- optional, your own key (1-32 of A-Z a-z 0-9 _ -); starting it again replaces it
title = 'Delivery route', -- required, 1-40 characters
detail = '3 stops left', -- optional, up to 60
compact = '1/4', -- optional, up to 12: the short text beside the camera
progress = 0.25, -- optional, 0 to 1: a bar
-- or a countdown the phone runs itself (no update per second needed):
-- countdown = 90, -- seconds (1 s to 24 h), or endsAt = os.time() + 90
-- endOnZero = true, -- remove it when it reaches zero (else it stays at 0:00)
glyph = 'truck', -- a glyph (list below), or appIcon = 'my_app' (an app's icon)
colour = '#FF9F0A', -- #rrggbb, must reach 3:1 contrast on black
link = 'my_app/route/42', -- optional: where a tap goes ('messages', 'my_app', 'my_app/<route>')
})
if not id then print('not shown: ' .. why) end
exports.phone:UpdateActivity(src, id, { detail = '2 stops left', progress = 0.5 })
exports.phone:UpdateActivity(src, id, { detail = false }) -- false (or '') clears a field
exports.phone:EndActivity(src, id)
exports.phone:EndAllActivities(src) -- every activity YOUR resource shows on that player (nil = on everyone)
UpdateActivity takes any of the same fields except id; countdown = false stops a countdown,
link = false removes the link. It returns true, or false, reason.
Client (exports.phone:…, this player's phone only)
local id = exports.phone:StartActivity({ title = 'Fuel', glyph = 'fuel', progress = 0.6 })
exports.phone:UpdateActivity(id, { progress = 0.55, compact = '55%' })
exports.phone:EndActivity(id)
Same definition and limits. endsAt is read against GetCloudTimeAsInt() on the client. A
link or appIcon naming an add-on works once the phone has received this player's add-on list.
What the player sees
| Where | What |
|---|---|
| Island, one activity | the glyph or app icon leading; compact trailing (else the countdown, else detail) |
| Island, two or three | small pills either side of the camera. The phone's own call, timer and music always stay in front of yours |
| Island, held | title, detail and the progress bar |
| Lock Screen | the card: title, detail, progress bar and a stripe in your colour |
| Tap | opens link (an app of the phone, or a registered add-on at the route). No link: the tap expands it |
A countdown shows as m:ss (h:mm:ss over an hour) after your detail ("Drill running · 1:24")
and fills the bar as it runs.
Glyphs: the Island's call, music, timer, navigation, record, camera, download,
job, delivery, event, emergency, and the add-on set shield, cross, wrench, cart,
bank, key, car, house, briefcase, radio, pin, bolt, book, chart, dice, fuel,
gavel, heart, lock, note, plane, tag, truck. Default: event.
Limits (config/api.lua, Config.Api.activities)
| Setting | Default | Meaning |
|---|---|---|
perResource |
2 | activities one resource may show on one phone at once ('limit-resource') |
perPlayer |
4 | from every script together ('limit-player'); the interface also never holds more than 6 |
updateMs |
500 | one activity updates at most this often. Faster updates are merged and the newest state is sent when the interval ends, so the last one always arrives |
startsPerMinute |
20 | starts per resource per player per minute ('rate-limited') |
enabled |
true | false refuses every start ('disabled') |
Text lengths are fixed by the screen: title 40, detail 60, compact 12 characters. Longer text is
refused, not cut, with a reason such as 'title is longer than 40 characters'; an unknown
field (detial = …) is refused too. Each refusal also prints one console line naming your
resource (at most once a minute per reason).
Other reasons: 'player-offline', 'no-phone' (the player's phone has not loaded),
'unknown-activity' (not yours, or already ended), 'id cannot change'.
Cleanup
Your activities end when you end them, when your resource stops (the server and client halves
each clear their own), when the player leaves, and - with endOnZero - when the countdown runs
out. One resource can never update or end another's: activities are keyed by the calling resource.
15. Script calls (a custom or hidden caller id)
A server script can ring a player from an identity that is not a phone - a mission's
"Unknown" caller, a dispatcher - through the phone's own call system: the ringtone, the call
screen, answer and decline, call waiting when they are already talking, Recents, and the public
call events (phone:callStarted, phone:callAnswered, phone:callEnded).
local callId, why = exports.phone:PlaceScriptCall(target, { -- target: server id, or a phone number held by someone online
label = 'Unknown', -- required, 1-32 characters: what the call screen and Recents show
number = '555-0142', -- optional, 3-15 digits (+ - ( ) and spaces allowed)
hidden = false, -- default: true without a number, false with one
speaker = dispatcherSrc, -- optional: a player whose voice carries the call
ringSeconds = 30, -- 5 to 60 (default Config.Api.scriptCalls.ringSeconds)
onAnswer = function(callId, src) end,
onEnd = function(callId, reason, durationMs) end, -- 'ended' | 'missed' | 'declined' | 'dropped' | 'busy'
})
exports.phone:EndScriptCall(callId) -- hang up from your side
exports.phone:GetScriptCallState(callId) -- 'ringing' | 'connected' | nil
Audio. With speaker, that player is the calling end - their own phone shows the call - and
the phone's existing voice routing puts both in one call channel (pma-voice, mumble-voip,
SaltyChat, TokoVOIP, YACA), exactly as for a phone-to-phone call. Without speaker nobody is on
the far end: the line connects and is silent, and your script supplies what the player hears or
reads (onAnswer is where to start it).
Server only. These are server exports and no net event leads to them, so a client can never
place a script call. EndScriptCall and GetScriptCallState only answer the resource that placed
the call, and a player's own call can never be ended through them.
Logged. Every script call writes Script call placed and Script call ended to the phone's
activity log (calls: the resource, the label, the number or withheld, the callee, the reason)
and a call-history row, whose caller column is the number shown, or the label when the number is
withheld. A script call never offers voicemail (there is no one to offer it to).
Refusals (false, reason): 'unavailable' (target offline, phone not loaded, dead battery or
no service), 'busy' (already ringing or dialling), 'speaker-busy', 'speaker-is-target',
'limit-resource' (Config.Api.scriptCalls.perResource, default 8 at once), 'disabled', and a
sentence for a bad option ('label is required', 'number must have 3 to 15 digits',
'unknown field "video"'). Your resource stopping hangs up its calls; the callbacks are not called
then, because the resource is gone.
A working example of sections 14 and 15: pulse-phone-devapi-demo/ (in this folder) (/devkit_delivery,
/devkit_heist, /devkit_call, /devkit_dispatch <id>, /devkit_fuel).
Post in #pulse-issues (we go through it every week), or open a ticket in #open-a-ticket if it's urgent. Paste the start-up check from your server console.