Ir al contenido

Exports y state bags

Dos formas de entrar, y la barata primero.

phoneapi <id> imprime todo lo de abajo para un jugador, en la consola del servidor. Si algo de aquí no responde lo que esperas, ejecútalo antes de seguir leyendo: es más rápido que adivinar qué lado está mal.

State bags: las respuestas que no cuestan nada

Sección titulada «State bags: las respuestas que no cuestan nada»

Un state bag no necesita callback, ni evento, ni ningún acuerdo con este recurso. Es una tabla que se lee en la máquina que pregunta, sobrevive a que este recurso se reinicie por debajo de ti y funciona igual desde el cliente que desde el servidor.

-- server
if Player(src).state.onCallWith then ... end
-- client, about somebody else
if Player(GetPlayerFromServerId(id)).state.phoneOpen then ... end
BagTipo
phoneNumberstringel número del teléfono que lleva encima
phoneNamestringel nombre que muestra ese teléfono cuando llama a alguien
phoneDevicestring | nilqué ítem de teléfono es, en modo ítem. nil cuando los teléfonos pertenecen a personajes
phoneOpenbooleanel teléfono está fuera y en pantalla
onCallWithnumber | nilel source del otro extremo
callAnsweredbooleanalguien ha contestado. false mientras sigue sonando
speakerphoneboolean
mutedCallbooleanse ha silenciado a sí mismo
otherMutedCallbooleanel otro extremo se ha silenciado
flashlightbooleantiene la linterna encendida. Todos los clientes dibujan el haz en ese ped
phoneCamerabooleanhay un visor abierto: está a punto de hacer una foto
phoneSignalnumbercuántas barras, de 0 a Config.Signal.Bars. 0 es un teléfono al que la ciudad no puede llegar

Tres cosas que conviene saber:

Los escribe el servidor. El cliente también lo sabe todo y podría publicarlo en menos líneas, pero un cliente puede mentir sobre todo ello, y un bag que lee un anticheat tiene que merecer la pena leerlo.

Que esté sonando cuenta como estar en una llamada. onCallWith se establece en el momento en que suena un teléfono, porque la línea está ocupada; lo que todavía no es cierto es que alguien haya contestado, y eso es callAnswered.

onCallWith puede ser nil durante una llamada real. El otro extremo puede ser el navegador de alguien en lugar de un jugador en la ciudad. callAnswered sigue siendo true; simplemente no hay ningún source que nombrar.

No hubo bag flashlight hasta que la linterna iluminó algo de verdad, porque un bag que siempre es false es peor que un bag que no existe: uno es una función que falta, el otro es una respuesta incorrecta. Ahora ilumina algo, así que se publica.

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 devuelve lo que se le habría dicho al propio teléfono ({ ok = false, reason = 'busy' }, 'airplane', 'unknown', 'offline'), porque ejecuta el mismo handler que ejecuta el marcador del propio teléfono. Hay un solo camino de código y un solo conjunto de reglas que pueden fallar.

Un export cuya respuesta es nil no devuelve ningún valor, no un valor nil. Si lo metes directamente en una lista de argumentos no se convierte en "nil": acorta la lista. Guárdalo antes en una variable local:

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

Las preguntas que otros recursos tenían que responderse por su cuenta una y otra vez. Cada una es una consulta que el teléfono ya hace, así que hay un solo camino de código y un solo conjunto de reglas que pueden fallar.

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

Que GetBattery responda nil no es lo mismo que responder 0: nil significa que este servidor nunca activó la batería. Un script que los trata igual impide que alguien llame a una ambulancia en una ciudad donde el teléfono nunca se queda sin batería.

AddPhoto comprueba la URL contra los hosts que permite este servidor, exactamente igual que se comprueba una foto hecha con la cámara. Una galería que muestra cualquier dirección es una galería a la que alguien puede apuntar a cualquier cosa.

SocialPost y SetRadioChannel ejecutan los handlers del propio teléfono, así que obedecen lo mismo que obedece la pantalla: sin cuenta en esa app no hay publicación, y un canal que este trabajo no puede tener se rechaza aquí también.

Seis monedas, con precio fijado por el servidor en un temporizador que nadie puede alcanzar desde un cliente. Todo lo de abajo está en monedas y en dinero (nunca en las millonésimas con las que cuenta el recurso), así que un script que paga a alguien un cuarto de algo pone 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 se niega en lugar de quedar en negativo, así que su respuesta también es “¿ha podido pagar?”, que es justo lo que quiere preguntar una tienda que vende algo a cambio de monedas:

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

El monedero pertenece al personaje o al teléfono, y eso lo decide Config.Apps.Crypto.Wallet. En modo dispositivo, robar un teléfono es robar la cartera con él; en modo personaje, un teléfono robado le muestra al ladrón una app vacía. Los exports reciben un source en ambos casos y no necesitan saber cuál es.

phonecrypto en la consola imprime el mercado, phonecrypto <id> imprime lo que tiene ese jugador y phonecrypto price <coin> <money> lo mueve a mano.

Las dos se activan o desactivan desde la config (shared/config/battery.lua y Config.Flashlight): la batería viene desactivada por defecto y la linterna activada.

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 cargador no tiene por qué ser un ítem en el bolsillo. Config.Battery.Charger.Item te da uno de serie, y ChargePhone es para todo lo demás: un cable en un coche, un enchufe en la pared, un prop detrás de un escritorio.

phonebattery [charge|unplug|full|empty] <id> la mueve, que es como pruebas una batería sin poner a prueba tu paciencia.

Los administradores en el juego (y la consola) tienen unos cuantos comandos para averiguar por qué algo no hace lo que querías. Solo miran, o cambian el teléfono de un jugador a propósito; ninguno se inventa nada.

phoneapi <id>state bags, exports y la batería de un jugador
phoneappsla lista de apps tal como la ha resuelto la config
phonedevice [backup|restore] <id>qué teléfono tiene en la mano y las copias que conserva
phonebattery [charge|unplug|full|empty] <id>

phoneapi también imprime si ese teléfono tiene señal y qué se le está guardando, que es la cadena desde “sin señal” hasta “ha llegado la notificación” en dos líneas. Pasa por cuatro sitios y no había forma de ver cuál había fallado.

El nivel no es un temporizador. Una fila recuerda dónde estaba, cuándo y qué estaba haciendo el teléfono (abierto, en el bolsillo o cargando), y el nivel actual es aritmética sobre el reloj. Cien jugadores cuestan cien números en lugar de cien temporizadores, y un teléfono que se quedó cargando durante un reinicio del servidor vuelve cargado en lugar de congelado.

Que tu aviso no acabe en el álbum de fotos de nadie

Sección titulada «Que tu aviso no acabe en el álbum de fotos de nadie»

(Esto y el resto de la convivencia —el ped, el prop, el modo ítem— está reunido en tu recurso junto a él.)

Una fotografía es el encuadre entero. Lo que tu recurso estuviera dibujando cuando saltó el obturador (un aviso de ayuda, una etiqueta de marcador, una barra de progreso) sale en la foto, y este recurso no puede impedir que lo dibujes.

phoneCamera está ahí para que puedas impedírtelo tú. Es true desde el momento en que se abre un visor hasta que se cierra, y una línea en un bucle de dibujo es todo el arreglo:

if LocalPlayer.state.phoneCamera then goto skip end

El HUD del propio teléfono ya se aparta: Config.Apps.Camera.HideHud oculta el minimapa y las barras de salud del juego mientras la cámara está abierta, así que lo que alguien encuadra es lo que obtiene.

La linterna es un state bag flashlight, así que todos los demás clientes dibujan el haz en el ped de ese jugador. Solo alumbra mientras el teléfono está fuera, y se apaga cuando el teléfono se guarda.

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

Nada de esto va más allá de las comprobaciones que el teléfono se aplica a sí mismo. Open y OpenApp reciben la misma negativa que recibiría un jugador pulsando F1, por las mismas razones: un teléfono que se abre mientras su dueño está muerto porque un recurso lo ha pedido amablemente no es un teléfono con reglas. CanOpen y Restricted están ahí para que puedas preguntar antes de pedir, y poner en gris un botón en lugar de ofrecer uno que no hace nada.

Los nombres de los bags coinciden con los de lb-phone cuando el significado es el mismo, así que un script escrito para ese teléfono lee este sin cambios.