Exports und State Bags
Zwei Wege zum Ziel, und der ressourcenschonende zuerst.
phoneapi <id> gibt in der Server-Konsole alle folgenden Werte für einen bestimmten Spieler aus. Wenn hier etwas nicht das antwortet, was du erwartest, führe diesen Befehl aus, bevor du weitersuchst — das ist schneller, als zu raten, auf welcher Seite der Fehler liegt.
State Bags — Die Antworten zum Nulltarif
Abschnitt betitelt „State Bags — Die Antworten zum Nulltarif“Ein State Bag benötigt keinen Callback, kein Event und keine vorherige Abstimmung mit dieser Ressource. Es ist eine Tabelle, die lokal auf der anfragenden Maschine gelesen wird, einen Neustart dieser Ressource im Hintergrund übersteht und auf dem Client genauso funktioniert wie auf dem Server.
-- serverif Player(src).state.onCallWith then ... end
-- client, about somebody elseif Player(GetPlayerFromServerId(id)).state.phoneOpen then ... end| Bag | Typ | |
|---|---|---|
phoneNumber | string | die Rufnummer des mitgeführten Telefons |
phoneName | string | der Name, den das Telefon anzeigt, wenn es jemanden anruft |
phoneDevice | string | nil | welches Telefon-Item dies im Item-Modus ist. nil, wenn Telefone zu Charakteren gehören |
phoneOpen | boolean | das Telefon ist herausgeholt und auf dem Bildschirm sichtbar |
onCallWith | number | nil | die Server-ID der Gegenseite |
callAnswered | boolean | jemand hat abgenommen. false, solange es noch klingelt |
speakerphone | boolean | Freisprechlautsprecher aktiv |
mutedCall | boolean | der Spieler hat sich selbst stummgeschaltet |
otherMutedCall | boolean | die Gegenseite hat sich stummgeschaltet |
flashlight | boolean | die Taschenlampe ist an. Jeder Client rendert den Lichtkegel an diesem Ped |
phoneCamera | boolean | der Kamerasucher ist offen: Ein Foto wird vorbereitet |
phoneSignal | number | Anzahl der Balken, 0 bis Config.Signal.Bars. 0 bedeutet für die Stadt unerreichbar |
Drei wissenswerte Details:
Sie werden vom Server geschrieben. Der Client wüsste zwar ebenfalls alles und könnte es kürzer publizieren, doch ein Client kann lügen, und ein vom Anticheat gelesener State Bag muss verlässlich sein.
Klingeln zählt bereits als aktives Telefonat. onCallWith wird gesetzt, sobald ein Telefon klingelt, da die Leitung belegt ist — noch hat jedoch niemand abgehoben, was über callAnswered geprüft wird.
onCallWith kann während eines echten Gesprächs nil sein. Das andere Ende kann der Browser einer Person außerhalb des Spiels sein. callAnswered ist dennoch true; es gibt schlicht keine FiveM-Server-ID als Gegenstelle.
Es gab keinen flashlight-State-Bag, bis die Taschenlampe die Umgebung tatsächlich beleuchtete — nach dem Grundsatz, dass ein State Bag, der immer false ist, schlimmer ist als ein fehlender: Das eine ist ein fehlendes Feature, das andere eine Falschaussage. Jetzt spendet sie Licht und wird entsprechend publiziert.
Server-Exports
Abschnitt betitelt „Server-Exports“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 modeexports.mic_phone:GetDevice(src) --> the phone item's id, or nilexports.mic_phone:HasPhone(src) --> are they carrying one at allexports.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 numberexports.mic_phone:NumberOf(identifier) --> works for somebody who is not onlineexports.mic_phone:IdentifierOf(number)
exports.mic_phone:Call(src, number) --> rings it from their phone, exactly as dialling wouldexports.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 liefert exakt das zurück, was auch das Telefon selbst zurückerhalten hätte — { ok = false, reason = 'busy' }, 'airplane', 'unknown', 'offline' —, da es denselben internen Handler aufruft wie das Nummernfeld des Telefons. Es gibt nur einen einzigen Codepfad und ein einheitliches Regelwerk.
Ein Export mit dem Ergebnis nil liefert keinen Rückgabewert, nicht einen Wert nil. Direkt in eine Argumentenliste übergeben, wird daraus kein "nil" — es verkürzt die Liste. Fange es daher zuerst in einer lokalen Variable ab:
local with = exports.mic_phone:CallWith(src) -- and not print(exports.mic_phone:CallWith(src), x)Das Telefon selbst
Abschnitt betitelt „Das Telefon selbst“Fragen, die andere Ressourcen sich ständig selbst beantworten mussten. Jede davon ist eine Abfrage, die das Telefon ohnehin durchführt; es existiert also nur ein einziger Codepfad.
exports.mic_phone:GetBattery(src) --> 0-100, or nil when this city has no battery at allexports.mic_phone:SetBattery(src, 40) --> your own charger, or a story that drains oneexports.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 bagexports.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 rebootexports.mic_phone:GetContacts(src)
exports.mic_phone:GetInvoices(src) --> every invoice on that phone, paid and unpaidexports.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 upexports.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 nilexports.mic_phone:SetRadioChannel(src, 12) --> 0 takes them off itDass GetBattery den Wert nil zurückgibt, ist nicht dasselbe wie 0: nil bedeutet, dass der Server das Akkusystem gar nicht aktiviert hat. Ein Skript, das beides gleich behandelt, würde Spielern in einer Stadt ohne Akkuverbrauch den Notruf verwehren.
AddPhoto gleicht die URL mit den erlaubten Hosts des Servers ab — genau wie bei einem regulären Foto der Kamera. Eine Galerie, die jede beliebige Adresse laden würde, könnte für beliebige Inhalte missbraucht werden.
SocialPost und SetRadioChannel laufen über die internen Handler des Telefons und unterliegen denselben Regeln wie das Interface: Kein Account in der App bedeutet kein Post, und ein für diesen Job gesperrter Funkkanal wird auch hier verweigert.
Der Markt
Abschnitt betitelt „Der Markt“Sechs Coins, bewertet vom Server über einen Timer, an den kein Client herankommt. Alle folgenden Angaben erfolgen in Coins und Spielgeld — nie in den internen Millionsteln —, ein Skript für ein Viertel eines Coins übergibt also 0.25.
exports.mic_phone:Coins() --> every coin: id, name, symbol, price, change, window, historyexports.mic_phone:CoinPrice('santo') --> 42137.55, in money. nil when there is no such coinexports.mic_phone:GetCrypto(src, 'santo') --> 0.4218exports.mic_phone:GetCryptoValue(src) --> what their whole portfolio is worth, in money
exports.mic_phone:AddCrypto(src, 'omen', 12.5) --> true when it landedexports.mic_phone:RemoveCrypto(src, 'omen', 12.5) --> false when they did not have it, and nothing movesexports.mic_phone:SetCoinPrice('omen', 0.4) --> move the market for a storyRemoveCrypto bricht ab, anstatt ins Minus zu laufen. Die Rückgabe beantwortet daher auch die Frage, ob die Person zahlungsfähig war — genau die Information, die ein Shop mit Krypto-Zahlung benötigt:
if exports.mic_phone:RemoveCrypto(src, 'omen', price) then giveThem(thing) endDas Wallet gehört entweder zum Charakter oder zum Telefon (Config.Apps.Crypto.Wallet). Im Device-Modus stiehlt ein Telefondiebstahl das gesamte Portfolio; im Character-Modus sieht der Dieb eine leere App. Den Exports wird in beiden Fällen eine Server-ID übergeben, ohne dass sie den Modus kennen müssen.
phonecrypto in der Konsole gibt den Marktstatus aus, phonecrypto <id> zeigt das Portfolio eines Spielers und phonecrypto price <coin> <money> verändert den Kurs manuell.
Akku und Taschenlampe
Abschnitt betitelt „Akku und Taschenlampe“Beide Funktionen werden in der Config gesteuert — shared/config/battery.lua und Config.Flashlight — standardmäßig ist der Akku aus und die Taschenlampe an.
exports.mic_phone:BatteryLevel(src) --> 0-100, or 100 when the battery is switched offexports.mic_phone:IsPhoneCharging(src)exports.mic_phone:ChargePhone(src, true) -- plug in; false unplugsEin Ladegerät muss kein Inventar-Item sein. Config.Battery.Charger.Item liefert ein solches standardmäßig mit, und ChargePhone dient allen übrigen Einsatzzwecken: Ein Ladekabel im Auto, eine Steckdose an der Wand oder ein Prop auf dem Schreibtisch.
phonebattery [charge|unplug|full|empty] <id> setzt den Ladezustand sofort, um das System testen zu können, ohne die eigene Geduld auf die Probe zu stellen.
Admin-Befehle
Abschnitt betitelt „Admin-Befehle“Admins im Spiel (und die Server-Konsole) haben Zugriff auf Befehle zur Fehleranalyse. Sie dienen rein der Einsichtnahme oder gezielten Modifikation für einen Spieler; nichts davon erfindet Daten frei.
phoneapi <id> | State Bags, Exports und Akkustand eines Spielers |
phoneapps | die aus der Config aufgelöste App-Liste |
phonedevice [backup|restore] <id> | aktives Telefon und hinterlegte Backups |
phonebattery [charge|unplug|full|empty] <id> |
phoneapi gibt außerdem aus, ob das Telefon Empfang hat und welche Benachrichtigungen dafür zurückgehalten werden — das zeigt die Kette von “kein Netz” bis “Benachrichtigung zugestellt” in zwei Zeilen. Da hierbei vier Stationen durchlaufen werden, war ein Fehlschlag zuvor kaum lokalisierbar.
Der Akkustand ist kein Timer. Eine Datenbankzeile merkt sich den vorherigen Zustand, den Zeitpunkt und die Aktivität des Telefons — geöffnet, eingesteckt oder am Ladekabel — und der aktuelle Akkustand wird rein rechnerisch aus der Uhrzeit ermittelt. Hundert Spieler bedeuten somit hundert Zahlen statt hundert aktiver Timer, und ein über einen Server-Neustart hinweg ladendes Telefon ist anschließend voll aufgeladen statt eingefroren.
Eigene UI-Elemente aus Spielerfotos heraushalten
Abschnitt betitelt „Eigene UI-Elemente aus Spielerfotos heraushalten“(Dies und alle weiteren Aspekte des Zusammenspiels — Ped, Prop, Item-Modus — sind unter deine Ressource daneben* zusammengefasst.)*
Ein Foto erfasst den gesamten Bildschirm. Alles, was deine Ressource beim Auslösen des Verschlusses gerendert hat — Hilfetexte, Marker-Beschriftungen, Ladebalken —, landet unwiderruflich auf dem Bild, und dieses Telefonskript kann dein Zeichnen nicht von außen stoppen.
phoneCamera existiert, damit du dein Rendern selbst pausieren kannst. Der Wert ist true vom Öffnen des Kamerasuchers bis zum Schließen; eine einzige Zeile in deiner Render-Schleife genügt:
if LocalPlayer.state.phoneCamera then goto skip endDas HUD des Telefons blendet sich bereits selbst aus: Config.Apps.Camera.HideHud verbirgt Minimap und Lebensanzeigen des Spiels für die Dauer der Kameraaktivität, damit das Foto genau dem gewählten Bildausschnitt entspricht.
Die Taschenlampe arbeitet über den State Bag flashlight, sodass alle anderen Clients den Lichtschein am Ped des Spielers anzeigen. Sie leuchtet nur, während das Telefon aktiv in der Hand gehalten wird, und erlischt beim Wegstecken.
Client-Exports
Abschnitt betitelt „Client-Exports“exports.mic_phone:Open() / Close() / Toggle() / IsOpen()exports.mic_phone:CanOpen() --> ok, reasonexports.mic_phone:Restricted() --> 'Dead' | 'Cuffed' | 'Swimming' | … | nilexports.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 = })Nichts hiervon umgeht die internen Selbstprüfungen des Telefons. Open und OpenApp erhalten bei einem toten Spieler dieselbe Abweisung wie beim Druck auf F1 — ein Telefon, das sich bei einem toten Spieler öffnet, nur weil eine andere Ressource höflich darum bittet, besitzt keine verlässlichen Regeln. CanOpen und Restricted erlauben dir die Abfrage im Vorfeld, um Buttons in deiner UI auszugrauen, anstatt funktionslose Klicks anzubieten.
Bezeichnungen
Abschnitt betitelt „Bezeichnungen“Die State-Bag-Namen stimmen bei gleicher Bedeutung mit denen von lb-phone überein. Für dieses System geschriebene Skripte können mic_phone daher ohne Codeänderungen auslesen.