Script de correo para FiveM: mensajes y facturas en el juego
Configura un sistema de correo en FiveM para facturas, notas de trabajo y anuncios. Descubre los mejores scripts, frameworks compatibles e ideas para integrarlo en tus empleos.
Agency Scripts
Fundador y desarrollador principal en Agency Scripts
El valor del correo físico para el roleplay
En una época en la que la mayoría de los servidores de FiveM dependen por completo de las notificaciones del teléfono y la mensajería digital, un sistema físico de correo y cartas añade un nivel de inmersión que destaca. Los jugadores pueden escribir cartas con estilo manuscrito a otros personajes, enviar paquetes con objetos, recibir notificaciones oficiales del gobierno, recibir facturas de negocios e incluso encontrar notas anónimas misteriosas. Esto crea oportunidades de roleplay que la comunicación digital sencillamente no puede replicar. Una carta amenazante que alguien deja en el buzón de otra persona tiene más peso que un mensaje de texto. Una carta de amor escrita a mano se siente más personal que un correo electrónico. Las citaciones judiciales entregadas por correo parecen más oficiales. Además de su valor para el roleplay, un sistema de correo crea una oportunidad laboral natural para carteros que recogen, clasifican y entregan correo por toda la ciudad. Este tutorial explica cómo crear un sistema de correo completo, desde el esquema de la base de datos hasta la mecánica de entrega.
Esquema de la base de datos para el sistema de correo
La base de datos debe gestionar cartas, paquetes, asignaciones de buzones y estados de entrega. Cada envío tiene un identificador del remitente, un identificador del destinatario, un tipo que distingue las cartas de los paquetes, un asunto, el contenido de las cartas, los datos de los objetos adjuntos a los paquetes, marcas de tiempo de creación y entrega, e indicadores de estado para saber si el envío está pendiente, en tránsito, entregado o leído. Los buzones están vinculados a las direcciones de las propiedades para que los jugadores que sean propietarios o alquilen una vivienda tengan un buzón personal. Los buzones públicos repartidos por la ciudad sirven a los jugadores sin vivienda, que tendrán que ir a una oficina de correos a recoger sus envíos. El esquema también debe admitir correo generado por el sistema para notificaciones automatizadas, como facturas, documentos judiciales, solicitudes de empleo y anuncios del gobierno que los scripts puedan enviar sin un remitente jugador.
-- SQL schema
CREATE TABLE IF NOT EXISTS mail_items (
id INT AUTO_INCREMENT PRIMARY KEY,
sender_id VARCHAR(64),
sender_name VARCHAR(64) DEFAULT 'Unknown',
recipient_id VARCHAR(64) NOT NULL,
mail_type ENUM('letter','package','notice') DEFAULT 'letter',
subject VARCHAR(128) NOT NULL,
body TEXT,
attachments JSON DEFAULT '[]',
postage_paid INT DEFAULT 0,
status ENUM('pending','transit','delivered','read','returned')
DEFAULT 'pending',
mailbox_id INT DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
delivered_at TIMESTAMP NULL,
read_at TIMESTAMP NULL,
INDEX idx_recipient (recipient_id, status),
INDEX idx_status (status)
);
CREATE TABLE IF NOT EXISTS mailboxes (
id INT AUTO_INCREMENT PRIMARY KEY,
owner_id VARCHAR(64),
property_id INT DEFAULT NULL,
location_x FLOAT NOT NULL,
location_y FLOAT NOT NULL,
location_z FLOAT NOT NULL,
box_type ENUM('residential','public','business') DEFAULT 'residential',
capacity INT DEFAULT 20,
UNIQUE KEY unique_owner (owner_id)
);
Escribir y enviar cartas
La interfaz de escritura debe transmitir la sensación de redactar una carta real. Muestra a los jugadores una NUI que parezca una hoja de papel, con campos para el nombre del destinatario, el asunto y un cuerpo de texto libre. Usa una fuente de estilo manuscrito en la NUI para reforzar la estética de una carta física. El campo del destinatario debe buscar en la base de datos de personajes por nombre, en lugar de exigir que los jugadores conozcan identificadores internos. Cuando el jugador termine de escribir y haga clic en enviar, el cliente envía los datos al servidor, que valida el contenido, comprueba que el remitente tenga suficiente dinero para pagar el franqueo, descuenta el costo e inserta el envío en la base de datos con estado pendiente. Establece límites razonables para la longitud del cuerpo, quizá 500 caracteres para las cartas estándar y 1000 para el franqueo prémium, para evitar abusos y permitir mensajes significativos. Luego, la carta entra en el proceso de entrega y espera a que un cartero la gestione o a que termine el temporizador de entrega automática.
-- server/mail.lua
local Config = {
postageCost = 50,
premiumPostage = 150,
maxBodyLength = 500,
premiumMaxBody = 1000,
autoDeliverTime = 900, -- 15 minutes if no postal worker
packagePostage = 200,
}
RegisterNetEvent('mail:send', function(data)
local src = source
local sender = GetPlayerIdentifier(src, 0)
-- Validate recipient exists
local recipient = MySQL.single.await([[
SELECT identifier, CONCAT(firstname,' ',lastname) as name
FROM characters WHERE CONCAT(firstname,' ',lastname) LIKE ?
LIMIT 1
]], {'%' .. data.recipientName .. '%'})
if not recipient then
TriggerClientEvent('mail:notify', src, 'Recipient not found.')
return
end
-- Check postage funds
local cost = data.premium and Config.premiumPostage or Config.postageCost
local maxLen = data.premium and Config.premiumMaxBody or Config.maxBodyLength
if #data.body > maxLen then
TriggerClientEvent('mail:notify', src, 'Letter exceeds maximum length.')
return
end
-- Deduct postage (framework-specific money removal)
local paid = exports['framework']:RemoveMoney(src, cost, 'cash')
if not paid then
TriggerClientEvent('mail:notify', src, 'Not enough cash for postage.')
return
end
-- Insert mail
MySQL.insert([[
INSERT INTO mail_items
(sender_id, sender_name, recipient_id, mail_type,
subject, body, postage_paid, status)
VALUES (?, ?, ?, 'letter', ?, ?, ?, 'pending')
]], {
sender, data.senderName, recipient.identifier,
data.subject, data.body, cost
})
TriggerClientEvent('mail:notify', src, 'Letter sent! Postage: $' .. cost)
end)
Sistema de paquetes con objetos adjuntos
Los paquetes amplían el sistema de correo más allá del texto, ya que permiten que los jugadores se envíen objetos físicos entre sí. Un jugador visita una oficina de correos, selecciona los objetos de su inventario que quiere incluir en un paquete, escribe una nota opcional y paga la tarifa de envío del paquete. El servidor retira los objetos del inventario del remitente y los guarda como un bloque JSON en la columna de archivos adjuntos del envío. Cuando el destinatario abre el paquete en su buzón o en la oficina de correos, el servidor deserializa los datos adjuntos y añade cada objeto al inventario del destinatario, comprobando primero que haya espacio disponible. Si el inventario del destinatario está lleno, el paquete se queda en su buzón hasta que haga espacio. Esto ofrece una forma legítima de enviar objetos a jugadores desconectados y abre oportunidades de roleplay para paquetes de ayuda, entregas de pruebas e intercambios de regalos. Valida todos los datos de los objetos en el servidor para evitar exploits de duplicación y registra todas las transacciones de paquetes para que los administradores las revisen.
-- server/packages.lua
RegisterNetEvent('mail:sendPackage', function(data)
local src = source
local sender = GetPlayerIdentifier(src, 0)
-- Validate items exist in sender inventory
local validItems = {}
for _, item in ipairs(data.items) do
local hasItem = exports['ox_inventory']:Search(src, 'count', item.name)
if hasItem < item.count then
TriggerClientEvent('mail:notify', src,
'You do not have enough ' .. item.name)
return
end
table.insert(validItems, {
name = item.name,
count = item.count,
meta = item.metadata or {}
})
end
-- Remove items from sender
for _, item in ipairs(validItems) do
exports['ox_inventory']:RemoveItem(src, item.name, item.count)
end
-- Deduct postage
local paid = exports['framework']:RemoveMoney(
src, Config.packagePostage, 'cash')
if not paid then
-- Refund items if payment fails
for _, item in ipairs(validItems) do
exports['ox_inventory']:AddItem(src, item.name, item.count)
end
return
end
MySQL.insert([[
INSERT INTO mail_items
(sender_id, sender_name, recipient_id, mail_type,
subject, body, attachments, postage_paid, status)
VALUES (?, ?, ?, 'package', ?, ?, ?, ?, 'pending')
]], {
sender, data.senderName, data.recipientId,
data.subject or 'Package',
data.note or '',
json.encode(validItems),
Config.packagePostage
})
end)
Trabajo de cartero y rutas de entrega
El trabajo de cartero transforma la entrega de correo de un proceso en segundo plano en contenido de roleplay activo. Los jugadores que inicien su turno en la oficina de correos reciben una furgoneta de reparto y una ruta con los envíos pendientes que deben entregar. La ruta se genera consultando todos los envíos pendientes, agrupándolos por zona de entrega y creando una secuencia ordenada de puntos de referencia que minimiza la distancia recorrida. Cada parada de la ruta muestra un blip en el mapa y un marcador en el buzón de destino. Cuando el cartero llega al marcador y pulsa la tecla de interacción, el envío pasa del estado pendiente al estado entregado y queda disponible en el buzón del destinatario. Los carteros reciben un salario base más bonificaciones por cada entrega, lo que crea una fuente de ingresos honrada. En los servidores sin carteros activos, implementa un temporizador de entrega automática que cambie los envíos pendientes al estado entregado tras un retraso configurable, para que el correo llegue tarde o temprano incluso cuando nadie trabaje en la ruta postal.
Interacción con el buzón e interfaz de lectura
Cuando un jugador se acerca a su buzón e interactúa con él, el servidor consulta todos los envíos dirigidos a ese jugador que tengan estado entregado o leído. La NUI muestra una lista estilo bandeja de entrada con cada envío, el nombre del remitente, el asunto, un icono de tipo y la marca de tiempo. Las cartas sin leer aparecen resaltadas o con un indicador. Al hacer clic en una carta, se abre en una vista completa con el estilo de una carta física sobre papel, con el remitente y la fecha en la parte superior, el texto en el centro y un botón para cerrar. En los paquetes, la vista muestra la nota adjunta y una lista de los objetos incluidos, con un botón "Abrir paquete" que transfiere los objetos al inventario del jugador. Marca las cartas como leídas cuando se abran y elimínalas del buzón después de un período de conservación configurable para evitar que la base de datos crezca demasiado. Añade un botón para responder que rellene previamente el campo del destinatario con el nombre del remitente original, para facilitar el intercambio de correspondencia. Aplica un estilo distinto a las notificaciones del sistema, con membrete oficial, para diferenciarlas del correo escrito por los jugadores.
Notificaciones del sistema y correo automatizado
Además de la comunicación entre jugadores, el sistema de correo se convierte en una herramienta poderosa para que otros scripts del servidor envíen notificaciones inmersivas. En vez de notificaciones emergentes insulsas, los scripts pueden enviar correo formal. Un sistema judicial envía citaciones oficiales con membrete del gobierno. Un negocio envía facturas y recordatorios de pago. Un script de gestión de propiedades envía avisos de alquiler pendiente y advertencias de desalojo. Un sistema de facciones envía cartas de reclutamiento o instrucciones para misiones. Crea una exportación sencilla del servidor que cualquier recurso pueda usar para poner en cola un envío del sistema, sin necesidad de conocer el funcionamiento interno del sistema de correo. La exportación acepta un identificador del destinatario, el asunto, el cuerpo, el tipo de correo y archivos adjuntos opcionales. Así, tu sistema de correo se convierte en una capa universal de notificaciones que se integra con el mundo del juego, en lugar de ser una superposición de interfaz intrusiva que rompe la inmersión.
-- server/exports.lua
-- Universal mail export for other resources
function SendSystemMail(recipientId, subject, body, mailType, attachments)
mailType = mailType or 'notice'
attachments = attachments or '[]'
if type(attachments) == 'table' then
attachments = json.encode(attachments)
end
MySQL.insert([[
INSERT INTO mail_items
(sender_id, sender_name, recipient_id, mail_type,
subject, body, attachments, status)
VALUES ('system', 'City of Los Santos', ?, ?, ?, ?, ?, 'pending')
]], {recipientId, mailType, subject, body, attachments})
end
exports('SendSystemMail', SendSystemMail)
-- Usage from another resource:
-- exports['mailsystem']:SendSystemMail(
-- playerId, 'Rent Due',
-- 'Your rent of $2,500 is due in 3 days.',
-- 'notice'
-- )