Aller au contenu

Exports et state bags

Deux portes d’entrĂ©e, et la plus Ă©conomique d’abord.

phoneapi <id> affiche tout ce qui suit pour un joueur, dans la console du serveur. Si quelque chose ne renvoie pas le rĂ©sultat attendu, exĂ©cutez cette commande avant d’aller plus loin — c’est bien plus rapide que de deviner de quel cĂŽtĂ© vient l’erreur.

Un state bag ne nĂ©cessite aucun callback, aucun Ă©vĂ©nement et aucun accord avec cette ressource. Il s’agit d’une table lue localement sur la machine qui pose la question, elle survit au redĂ©marrage de cette ressource en arriĂšre-plan, et elle fonctionne de la mĂȘme maniĂšre cĂŽtĂ© client et cĂŽtĂ© serveur.

-- server
if Player(src).state.onCallWith then ... end
-- client, about somebody else
if Player(GetPlayerFromServerId(id)).state.phoneOpen then ... end
BagType
phoneNumberstringle numéro du téléphone transporté
phoneNamestringle nom affichĂ© par ce tĂ©lĂ©phone lorsqu’il appelle quelqu’un
phoneDevicestring | nilidentifiant de l’item tĂ©lĂ©phone en mode item. nil quand le tĂ©lĂ©phone appartient au personnage
phoneOpenbooleanle tĂ©lĂ©phone est sorti et visible Ă  l’écran
onCallWithnumber | nilla source du correspondant à l’autre bout
callAnsweredbooleanquelqu’un a dĂ©crochĂ©. false tant que la sonnerie retentit
speakerphonebooleanhaut-parleur activé
mutedCallbooleanle joueur s’est mis en sourdine
otherMutedCallbooleanle correspondant s’est mis en sourdine
flashlightbooleanla lampe torche est allumée. Chaque client affiche le faisceau sur ce ped
phoneCamerabooleanle viseur est ouvert : le joueur est sur le point de prendre une photo
phoneSignalnumbernombre de barres, de 0 à Config.Signal.Bars. 0 signifie téléphone injoignable

Trois points importants :

Ils sont renseignĂ©s par le serveur. Le client en a Ă©galement connaissance et pourrait les publier en moins de lignes, mais un client peut mentir sur chacune de ces valeurs, et un state bag lu par un anticheat doit ĂȘtre digne de confiance.

La sonnerie compte comme un appel en cours. onCallWith est dĂ©fini dĂšs qu’un tĂ©lĂ©phone commence Ă  sonner, car la ligne est occupĂ©e — ce qui n’est pas encore acquis est que quelqu’un ait dĂ©crochĂ©, ce qui correspond Ă  callAnswered.

onCallWith peut valoir nil au cours d’un appel rĂ©el. L’autre correspondant peut utiliser son navigateur web plutĂŽt que d’ĂȘtre un joueur prĂ©sent dans la ville. callAnswered reste true ; il n’y a simplement aucun ID de source FiveM Ă  dĂ©signer.

Le state bag flashlight n’a Ă©tĂ© implĂ©mentĂ© que lorsque la torche a rĂ©ellement Ă©clairĂ© la scĂšne, partant du principe qu’un bag valant constamment false est pire qu’un bag absent — l’un est une fonctionnalitĂ© manquante, l’autre est une fausse information. La torche Ă©claire dĂ©sormais, l’état est donc publiĂ©.

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 renvoie ce qui aurait Ă©tĂ© signifiĂ© au tĂ©lĂ©phone lui-mĂȘme — { ok = false, reason = 'busy' }, 'airplane', 'unknown', 'offline' — car il emprunte le mĂȘme gestionnaire que le composeur natif du tĂ©lĂ©phone. Il n’existe qu’une seule branche de code et un seul ensemble de rĂšgles susceptibles d’échouer.

Un export dont la rĂ©ponse est nil ne renvoie aucune valeur, et non une valeur nil. PlacĂ© directement dans une liste d’arguments, il ne devient pas "nil" — il raccourcit la liste. Stockez-le d’abord dans une variable locale :

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

Les questions qu’une autre ressource devait sans cesse trancher par ses propres moyens. Chacune correspond Ă  une vĂ©rification que le tĂ©lĂ©phone effectue dĂ©jĂ , garantissant un comportement strictement identique.

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

Le fait que GetBattery rĂ©ponde nil n’est pas Ă©quivalent Ă  rĂ©pondre 0 : nil indique que ce serveur n’a jamais activĂ© la gestion de la batterie. Un script traitant les deux cas de la mĂȘme maniĂšre empĂȘcherait d’appeler les secours dans une ville oĂč les tĂ©lĂ©phones ne se dĂ©chargent jamais.

AddPhoto valide l’URL par rapport aux hĂ©bergeurs autorisĂ©s par ce serveur, exactement comme le clichĂ© pris depuis l’appareil photo. Une galerie capable d’afficher n’importe quelle adresse serait une faille permettant d’afficher n’importe quoi.

SocialPost et SetRadioChannel appellent les gestionnaires internes du tĂ©lĂ©phone, et respectent donc les mĂȘmes rĂšgles que l’interface : l’absence de compte sur cette application empĂȘche la publication, et un canal interdit Ă  ce mĂ©tier est Ă©galement refusĂ© ici.

Six devises tarifĂ©es par le serveur via une minuterie inaccessible depuis un client. Tout ce qui suit est formulĂ© en devises entiĂšres et en argent — jamais dans les millioniĂšmes utilisĂ©s pour les calculs internes —, ainsi un script versant un quart d’unitĂ© transmettra 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 rejette la transaction plutĂŽt que de crĂ©er un solde nĂ©gatif, sa rĂ©ponse indique donc Ă©galement si le joueur Ă©tait en mesure de payer — ce qui est exactement la question qu’une boutique acceptant la cryptomonnaie souhaite poser :

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

Le portefeuille est rattachĂ© soit au personnage soit au tĂ©lĂ©phone, selon la configuration de Config.Apps.Crypto.Wallet. En mode appareil, voler un tĂ©lĂ©phone subtilise le portefeuille d’actifs ; en mode personnage, un tĂ©lĂ©phone volĂ© prĂ©sente une application vide au voleur. Les exports acceptent un ID source dans les deux cas et n’ont pas besoin de connaĂźtre ce mode.

phonecrypto dans la console affiche l’état du marchĂ©, phonecrypto <id> dĂ©taille les avoirs d’un joueur, et phonecrypto price <coin> <money> ajuste le cours manuellement.

Toutes deux s’activent ou se dĂ©sactivent depuis la configuration — shared/config/battery.lua et Config.Flashlight — la batterie Ă©tant dĂ©sactivĂ©e par dĂ©faut et la torche activĂ©e.

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

Un chargeur n’a pas nĂ©cessairement besoin d’ĂȘtre un item dans une poche. Config.Battery.Charger.Item en fournit un clĂ© en main, et ChargePhone couvre toutes les autres intĂ©grations : cĂąble allume-cigare en vĂ©hicule, prise murale, accessoire de bureau.

phonebattery [charge|unplug|full|empty] <id> force l’état, idĂ©al pour tester la batterie sans mettre sa propre patience Ă  l’épreuve.

Les administrateurs en jeu (ainsi que la console) disposent de quelques commandes pour diagnostiquer un comportement inattendu. Elles permettent d’inspecter ou de modifier dĂ©libĂ©rĂ©ment le tĂ©lĂ©phone d’un joueur ; aucune n’invente de donnĂ©es.

phoneapi <id>state bags, exports et batterie d’un joueur
phoneappsliste des applications telles que résolues par la config
phonedevice [backup|restore] <id>quel téléphone le joueur possÚde et sauvegardes associées
phonebattery [charge|unplug|full|empty] <id>

phoneapi affiche Ă©galement si ce tĂ©lĂ©phone capte du rĂ©seau et les donnĂ©es en attente d’acheminement, ce qui rĂ©sume la chaĂźne d’évĂ©nements entre “aucun signal” et “la notification est arrivĂ©e” en deux lignes. Ce flux traversant quatre Ă©tapes, il Ă©tait auparavant complexe d’identifier laquelle bloquait.

Le niveau n’est pas un compte Ă  rebours. Une ligne enregistre le niveau prĂ©cĂ©dent, l’horodatage et l’état du tĂ©lĂ©phone — ouvert, en poche ou en charge — et le niveau actuel est calculĂ© mathĂ©matiquement Ă  l’instant T. Cent joueurs mobilisent cent nombres au lieu de cent timers actifs, et un tĂ©lĂ©phone laissĂ© en charge pendant un redĂ©marrage du serveur revient rechargĂ© plutĂŽt que figĂ©.

EmpĂȘcher votre interface d’apparaĂźtre dans les photos des joueurs

Section intitulĂ©e « EmpĂȘcher votre interface d’apparaĂźtre dans les photos des joueurs »

(Cette notion ainsi que les rĂšgles de cohabitation — le ped, le prop, le mode item — sont rassemblĂ©es dans votre ressource Ă  ses cĂŽtĂ©s.)

Une photo capture l’écran entier. Tout ce que votre ressource affichait au moment du dĂ©clenchement — texte d’aide, marqueur 3D, barre de progression — figurera sur l’image, et cette ressource ne peut pas vous empĂȘcher de l’afficher.

phoneCamera est mis Ă  disposition pour vous permettre de suspendre ces affichages. Ce state bag est true dĂšs l’ouverture du viseur jusqu’à sa fermeture ; une seule condition dans votre boucle de rendu rĂ©sout le problĂšme :

if LocalPlayer.state.phoneCamera then goto skip end

L’interface propre au tĂ©lĂ©phone s’efface d’elle-mĂȘme : Config.Apps.Camera.HideHud masque la mini-carte et les jauges de vie du jeu tant que l’appareil photo est actif, assurant que le clichĂ© obtenu corresponde fidĂšlement au cadrage du joueur.

La torche s’appuie sur le state bag flashlight, de sorte que tous les autres clients projettent le faisceau lumineux depuis le ped de ce joueur. Elle ne reste allumĂ©e que lorsque le tĂ©lĂ©phone est sorti et s’éteint dĂšs qu’il est rangĂ©.

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 = })

Rien ici ne contourne les contrĂŽles que le tĂ©lĂ©phone s’applique Ă  lui-mĂȘme. Open et OpenApp rencontrent les mĂȘmes refus qu’un joueur appuyant sur F1, pour les mĂȘmes raisons — un tĂ©lĂ©phone qui s’ouvrirait alors que son propriĂ©taire est mort sous prĂ©texte qu’une ressource le demande poliment n’est pas un systĂšme fiable. CanOpen et Restricted existent pour vous permettre de vĂ©rifier l’éligibilitĂ© en amont, afin de griser une option plutĂŽt que de proposer un bouton inopĂ©rant.

Les noms de state bags correspondent à ceux de lb-phone lorsque la signification est identique, permettant à un script conçu pour ce dernier de fonctionner ici sans la moindre modification.