Skip to content

Exports and state bags

Two ways in, and the cheap one first.

phoneapi <id> prints everything below for one player, in the server console. If something here is not answering what you expect, run that before you read any further — it is faster than guessing which side is wrong.

State bags — the answers that cost nothing

Section titled “State bags — the answers that cost nothing”

A state bag needs no callback, no event and no agreement with this resource. It is a table read on whichever machine is asking, it survives this resource being restarted underneath you, and it works from the client and the server alike.

-- server
if Player(src).state.onCallWith then ... end
-- client, about somebody else
if Player(GetPlayerFromServerId(id)).state.phoneOpen then ... end
BagType
phoneNumberstringthe number of the phone they are carrying
phoneNamestringthe name that phone shows when it rings somebody
phoneDevicestring | nilwhich phone item this is, in item mode. nil when phones belong to characters
phoneOpenbooleanthe phone is out and on screen
onCallWithnumber | nilthe source at the other end
callAnsweredbooleansomebody picked up. false while it is still ringing
speakerphoneboolean
mutedCallbooleanthey muted themselves
otherMutedCallbooleanthe other end muted themselves
flashlightbooleantheir torch is on. Every client draws the beam on that ped
phoneCamerabooleana viewfinder is up: they are about to take a photograph
phoneSignalnumberhow many bars, 0 to Config.Signal.Bars. 0 is a phone the city cannot reach

Three things worth knowing:

They are written by the server. The client knows all of it too and could publish it in fewer lines, but a client can lie about all of it, and a bag an anticheat reads has to be worth reading.

Ringing counts as being on a call. onCallWith is set the moment a phone rings, because the line is taken — what is not yet true is that anybody answered, and that is callAnswered.

onCallWith can be nil during a real call. The other end may be somebody’s browser rather than a player in the city. callAnswered is still true; there is simply no source to name.

There was no flashlight bag until the torch actually lit something, on the grounds that a bag which is always false is worse than a bag that is not there — one is a missing feature, the other is a wrong answer. It lights something now, so it is published.

exports.mic_phone:GetNumber(src) --> '555-010-2048'
exports.mic_phone:GetName(src) --> 'Jean Dupont'
exports.mic_phone:GetIdentifier(src) --> 'char1:abc…' or 'device:abc…' in item mode
exports.mic_phone:GetDevice(src) --> the phone item's id, or nil
exports.mic_phone:HasPhone(src) --> are they carrying one at all
exports.mic_phone:IsOpen(src)
exports.mic_phone:IsInCall(src)
exports.mic_phone:CallWith(src) --> the other source, or nil
exports.mic_phone:GetSource(number) --> src, for a number
exports.mic_phone:NumberOf(identifier) --> works for somebody who is not online
exports.mic_phone:IdentifierOf(number)
exports.mic_phone:Call(src, number) --> rings it from their phone, exactly as dialling would
exports.mic_phone:EndCall(src)
exports.mic_phone:AddContact(src, name, number, { email =, notes =, favorite = })
exports.mic_phone:RemoveContact(src, number)
exports.mic_phone:SendNotification(src, { app =, title =, body = })
exports.mic_phone:OpenApp(src, 'messages')
exports.mic_phone:OpenPhone(src) / ClosePhone(src)
exports.mic_phone:SendMessage(number, text, from)
exports.mic_phone:SendMail(identifierOrNumber, fromName, fromAddress, subject, body)
exports.mic_phone:SendInvoice(senderSrc, targetNumber, label, amount, society)

Call returns what the phone itself would have been told — { ok = false, reason = 'busy' }, 'airplane', 'unknown', 'offline' — because it runs the same handler the phone’s own dialler runs. There is one code path and one set of rules to get wrong.

An export whose answer is nil returns no value at all, not a nil one. Dropped straight into an argument list it does not become "nil" — it shortens the list. Put it in a local first:

local with = exports.mic_phone:CallWith(src) -- and not print(exports.mic_phone:CallWith(src), x)

The questions another resource kept having to answer for itself. Each one is a lookup the phone already does, so there is one code path and one set of rules to get wrong.

exports.mic_phone:GetBattery(src) --> 0-100, or nil when this city has no battery at all
exports.mic_phone:SetBattery(src, 40) --> your own charger, or a story that drains one
exports.mic_phone:IsCharging(src)
exports.mic_phone:HasAirplaneMode(src) --> the city cannot reach them; their own phone still works
exports.mic_phone:GetSignal(src) --> 0 to Config.Signal.Bars. Also the `phoneSignal` state bag
exports.mic_phone:SetJammed(src, true) --> takes their bars away: a heist, a prison, a story
exports.mic_phone:GetGallery(src) --> every picture: { id, url, width, height, kind, created }
exports.mic_phone:AddPhoto(src, url, w, h) --> puts one in it, and it appears without a reboot
exports.mic_phone:GetContacts(src)
exports.mic_phone:GetInvoices(src) --> every invoice on that phone, paid and unpaid
exports.mic_phone:UnpaidInvoices(src) --> count, total
exports.mic_phone:SendLocation(number, x, y, 'The docks', from) --> a map pin in a conversation
exports.mic_phone:GetSocialHandle(src, 'instapic') --> 'nightshift', or nil if they never signed up
exports.mic_phone:SocialPost(src, 'twixel', { text = 'Anybody seen a blue Sultan?' })
exports.mic_phone:SetVerified(src, 'twixel', true) --> the tick beside their name, per app
exports.mic_phone:GetRadioChannel(src) --> 12, or nil
exports.mic_phone:SetRadioChannel(src, 12) --> 0 takes them off it

GetBattery answering nil is not the same as answering 0: nil means this server never switched the battery on. A script that treats them the same stops somebody calling an ambulance on a city where the phone never goes flat.

AddPhoto checks the URL against the hosts this server allows, exactly as a photo the camera took is checked. A gallery that will display any address is a gallery somebody can point at anything.

SocialPost and SetRadioChannel run the phone’s own handlers, so they obey what the screen obeys: no account on that app means no post, and a channel this job may not have is refused here too.

Six coins, priced by the server on a timer nobody can reach from a client. Everything below is in coins and in money — never in the millionths the resource counts in — so a script paying somebody a quarter of something says 0.25.

exports.mic_phone:Coins() --> every coin: id, name, symbol, price, change, window, history
exports.mic_phone:CoinPrice('santo') --> 42137.55, in money. nil when there is no such coin
exports.mic_phone:GetCrypto(src, 'santo') --> 0.4218
exports.mic_phone:GetCryptoValue(src) --> what their whole portfolio is worth, in money
exports.mic_phone:AddCrypto(src, 'omen', 12.5) --> true when it landed
exports.mic_phone:RemoveCrypto(src, 'omen', 12.5) --> false when they did not have it, and nothing moves
exports.mic_phone:SetCoinPrice('omen', 0.4) --> move the market for a story

RemoveCrypto refuses rather than going negative, so its answer is also “were they able to pay” — which is what a shop selling something for coin actually wants to ask:

if exports.mic_phone:RemoveCrypto(src, 'omen', price) then giveThem(thing) end

The wallet belongs to the character or to the phone, and that is Config.Apps.Crypto.Wallet. In device mode stealing a phone steals the portfolio with it; in character mode a stolen phone shows the thief an empty app. The exports take a source either way and do not need to know which it is.

phonecrypto in the console prints the market, phonecrypto <id> prints what that player holds, and phonecrypto price <coin> <money> moves it by hand.

Both are off or on from the config — shared/config/battery.lua and Config.Flashlight — and both are off by default for the battery, on for the torch.

exports.mic_phone:BatteryLevel(src) --> 0-100, or 100 when the battery is switched off
exports.mic_phone:IsPhoneCharging(src)
exports.mic_phone:ChargePhone(src, true) -- plug in; false unplugs

A charger does not have to be an item in a pocket. Config.Battery.Charger.Item gives you one out of the box, and ChargePhone is for everything else: a cable in a car, a plug on a wall, a prop behind a desk.

phonebattery [charge|unplug|full|empty] <id> moves it, which is how you test a battery without testing your own patience.

Admins in game (and the console) have a few commands for finding out why something is not doing what you meant. They only look, or change one player’s phone on purpose; none of them invents anything.

phoneapi <id>state bags, exports and the battery for one player
phoneappsthe app list as the config resolved it
phonedevice [backup|restore] <id>which phone they are holding, and the copies they keep
phonebattery [charge|unplug|full|empty] <id>

phoneapi also prints whether that phone has signal and what is being held for it, which is the chain from “no signal” to “the notification arrived” in two lines. It runs through four places and there was no way to see which one had gone wrong.

The level is not a timer. A row remembers where it was, when, and what the phone was doing — open, pocketed or charging — and the level now is arithmetic on the clock. A hundred players cost a hundred numbers rather than a hundred timers, and a phone left charging across a server restart comes back charged instead of frozen.

Keeping your prompt out of somebody’s photo album

Section titled “Keeping your prompt out of somebody’s photo album”

(This and the rest of coexisting — the ped, the prop, item mode — are collected in your resource beside it.)

A photograph is the whole frame. Whatever your resource was drawing when the shutter went — a help prompt, a marker label, a progress bar — is in the picture, and this resource cannot stop you drawing it.

phoneCamera is there so you can stop yourself. It is true from the moment a viewfinder opens until it closes, and one line in a draw loop is the whole fix:

if LocalPlayer.state.phoneCamera then goto skip end

The phone’s own HUD is already out of the way: Config.Apps.Camera.HideHud hides the game’s minimap and health bars for as long as the camera is up, so what somebody frames is what they get.

The torch is a flashlight state bag, so every other client draws the beam on that player’s ped. It only shines while the phone is out, and it goes out when the phone goes away.

exports.mic_phone:Open() / Close() / Toggle() / IsOpen()
exports.mic_phone:CanOpen() --> ok, reason
exports.mic_phone:Restricted() --> 'Dead' | 'Cuffed' | 'Swimming' | … | nil
exports.mic_phone:OpenApp('bank') / CloseApp()
exports.mic_phone:GetNumber() / GetDevice() / HasPhone() / IsBooted()
exports.mic_phone:IsInCall() / IsTablet() / SetTablet(true)
exports.mic_phone:Notify({ app =, title =, body = })

Nothing here reaches past the checks the phone applies to itself. Open and OpenApp get the same refusal a player pressing F1 would get, for the same reasons — a phone that opens while its owner is dead because a resource asked nicely is not a phone with rules. CanOpen and Restricted are there so you can ask before you ask, and grey out a button rather than offering one that does nothing.

The bag names match lb-phone’s where the meaning is the same, so a script written against that phone reads this one without changes.