سكربت نظام البريد في FiveM: الرسائل والفواتير داخل اللعبة
إعداد نظام بريد FiveM للفواتير، ملاحظات الوظائف وإعلانات الخادم. أفضل السكريبتات، دعم الأطر وأفكار لربط البريد بوظائفك.
Agency Scripts
المؤسس والمطور الرئيسي في Agency Scripts
القيمة التمثيلية للبريد الورقي
في عصر تعتمد فيه معظم خوادم FiveM كلياً على إشعارات الهاتف والرسائل الرقمية، يضيف نظام البريد والرسائل الورقية طبقة من الانغماس تبرز. يمكن للاعبين كتابة رسائل بخط اليد لشخصيات أخرى، إرسال طرود تحتوي على عناصر، استلام إشعارات حكومية رسمية، الحصول على فواتير من الشركات، وحتى العثور على ملاحظات مجهولة المصدر. يخلق هذا فرص لعب أدوار لا يمكن للتواصل الرقمي تكرارها ببساطة. رسالة تهديد تُترك في صندوق بريد شخص ما تحمل وزنًا أكبر من رسالة نصية. رسالة حب مكتوبة بخط اليد تبدو أكثر شخصية من بريد إلكتروني. استدعاءات المحكمة التي تُسلم بالبريد تبدو أكثر رسمية. بالإضافة إلى قيمة لعب الأدوار، يخلق نظام البريد فرصة عمل طبيعية لعاملين البريد الذين يجمعون، يصنفون، ويوصلون البريد عبر المدينة. يغطي هذا الدليل بناء نظام بريد كامل من مخطط قاعدة البيانات إلى آليات التوصيل.
مخطط قاعدة البيانات لنظام البريد
تحتاج قاعدة البيانات إلى التعامل مع الرسائل، الطرود، تعيين صناديق البريد، وحالات التسليم. لكل بريد معرف المرسل، معرف المستلم، نوع يميز الرسائل عن الطرود، سطر الموضوع، محتوى النص للرسائل، بيانات العناصر المرفقة للطرود، الطوابع الزمنية للإنشاء والتسليم، وعلامات الحالة التي تتتبع ما إذا كان البريد معلقًا، في النقل، تم تسليمه، أو مقروءًا. ترتبط صناديق البريد بعناوين العقارات بحيث يكون لدى اللاعبين الذين يمتلكون أو يستأجرون مساكن صندوق بريد شخصي. صناديق البريد العامة الموضوعة في أنحاء المدينة تخدم اللاعبين بدون مساكن، مما يتطلب منهم زيارة مكتب البريد لجمع بريدهم. يجب أن يدعم المخطط أيضًا البريد الذي ينشئه النظام للإشعارات الآلية مثل الفواتير، وثائق المحكمة، طلبات الوظائف، والإعلانات الحكومية التي يمكن للسكريبتات تفعيلها دون وجود مرسل لاعب.
-- 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)
);
كتابة وإرسال الرسائل
يجب أن يشعر واجهة كتابة الرسائل كما لو كنت تكتب رسالة حقيقية. قدم للاعبين واجهة NUI تشبه ورقة بها حقول لاسم المستلم، سطر الموضوع، ونص حر. استخدم خطًا بأسلوب كتابة اليد في NUI لتعزيز المظهر الفيزيائي للرسالة. يجب أن يبحث حقل المستلم في قاعدة بيانات الشخصيات بالاسم بدلاً من طلب معرفات داخلية من اللاعبين. عند الانتهاء من الكتابة والنقر على إرسال، يرسل العميل البيانات إلى الخادم الذي يتحقق من المحتوى، ويتأكد من أن المرسل لديه ما يكفي من المال لتغطية البريد، يخصم التكلفة، ويدرج البريد في قاعدة البيانات بحالة معلقة. فرض حدود معقولة على طول النص، ربما 500 حرف للرسائل العادية و1000 للبريد المميز، لمنع الإساءة مع السماح برسائل ذات معنى. ثم تدخل الرسالة خط التسليم وتنتظر موظف البريد لمعالجتها أو انتهاء مؤقت التسليم التلقائي.
-- 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)
نظام الطرود مع مرفقات العناصر
تمدد الطرود نظام البريد إلى ما هو أبعد من النص من خلال السماح للاعبين بإرسال عناصر مادية لبعضهم البعض. يزور اللاعب مكتب البريد، يختار عناصر من مخزونه ليشملها في الطرد، يكتب ملاحظة اختيارية، ويدفع رسوم الطرد. يزيل الخادم العناصر من مخزون المرسل ويخزنها ككتلة JSON في عمود مرفقات عنصر البريد. عندما يفتح المستلم الطرد في صندوق بريده أو مكتب البريد، يقوم الخادم بفك تسلسل بيانات المرفقات ويضيف كل عنصر مرة أخرى إلى مخزون المستلم، مع التحقق أولاً من وجود مساحة متاحة. إذا كان مخزون المستلم ممتلئًا، يبقى الطرد في صندوق بريده حتى يفرغ مساحة. هذا يخلق طريقة شرعية لإرسال العناصر إلى اللاعبين غير المتصلين ويفتح مجال التمثيل الدورى لحزم الرعاية، إسقاط الأدلة، وتبادل الهدايا. تحقق من صحة جميع بيانات العناصر على الخادم لمنع استغلال التكرار، وسجل كل معاملة طرد لمراجعة الإدارة.
-- 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)
وظيفة عامل البريد ومسارات التوصيل
تحول وظيفة عامل البريد تسليم البريد من عملية خلفية إلى محتوى لعب أدوار نشط. اللاعبون الذين يسجلون دخولهم في مكتب البريد يتلقون شاحنة توصيل ومسارًا من عناصر البريد المعلقة للتسليم. يتم إنشاء المسار عن طريق استعلام جميع عناصر البريد المعلقة، وتجميعها حسب منطقة التسليم، وإنشاء تسلسل مرتب لنقاط الطريق يقلل من مسافة السفر. كل توقف على المسار يضع علامة على الخريطة وعلامة عند صندوق البريد الوجهة. عندما يصل عامل البريد إلى العلامة ويضغط مفتاح التفاعل، ينتقل عنصر البريد من حالة معلق إلى حالة تم التوصيل ويصبح متاحًا في صندوق بريد المستلم. يكسب عمال البريد راتبًا أساسيًا بالإضافة إلى مكافآت لكل تسليم، مما يخلق مصدر دخل شريف. بالنسبة للخوادم التي لا يوجد بها عمال بريد نشطون، يتم تنفيذ مؤقت تسليم تلقائي ينقل البريد المعلق إلى حالة تم التوصيل بعد تأخير قابل للتكوين، مما يضمن وصول البريد في النهاية حتى عندما لا يعمل أحد في مسار البريد.
واجهة التفاعل مع صندوق البريد وقراءة الرسائل
عندما يقترب اللاعب من صندوق بريده ويتفاعل معه، يستعلم الخادم عن جميع الرسائل الموجهة إليه بحالة تم التوصيل أو القراءة. تعرض واجهة NUI قائمة بأسلوب صندوق الوارد تظهر كل رسالة مع اسم المرسل، الموضوع، أيقونة النوع، والطابع الزمني. تظهر الرسائل غير المقروءة مع تمييز أو شارة. النقر على رسالة يفتحها في وضع عرض كامل مصمم مثل رسالة ورقية فعلية، مع اسم المرسل والتاريخ في الأعلى، نص الرسالة في الوسط، وزر إغلاق. بالنسبة للطرد، يعرض العرض الملاحظة المرفقة وقائمة العناصر المحتواة مع زر "فتح الطرد" الذي ينقل العناصر إلى مخزون اللاعب. يتم تعليم الرسائل كمقروءة عند فتحها وإزالتها من صندوق البريد بعد فترة احتفاظ قابلة للتكوين لمنع تضخم قاعدة البيانات. أضف زر رد يملأ حقل المستلم باسم المرسل الأصلي، مما يسهل المراسلات المتبادلة. بالنسبة لإشعارات النظام، صممها بشكل مختلف مع رأس رسمي لتمييزها عن البريد المكتوب من اللاعبين.
إشعارات النظام والبريد الآلي
بعيداً عن التواصل بين اللاعبين، يصبح نظام البريد أداة قوية لسكربتات الخادم الأخرى لإرسال إشعارات غامرة. بدلاً من إشعارات التوست المملة، يمكن للسكربتات إرسال بريد رسمي. يرسل نظام المحكمة استدعاءات رسمية على ورق رسمي حكومي. ترسل الأعمال فواتير وتذكيرات بالدفع. يرسل سكربت إدارة الممتلكات إشعارات استحقاق الإيجار وتحذيرات الإخلاء. يرسل نظام الفصائل رسائل تجنيد أو ملخصات مهام. أنشئ تصدير خادم بسيط يمكن لأي مورد استدعاؤه لوضع عنصر بريد نظام في قائمة الانتظار دون الحاجة لفهم تفاصيل نظام البريد. يقبل التصدير معرف المستلم، الموضوع، النص، نوع البريد، والمرفقات الاختيارية. يحول هذا نظام البريد الخاص بك إلى طبقة إشعارات عالمية تشعر بأنها مدمجة في عالم اللعبة بدلاً من أن تكون تراكب واجهة مستخدم مزعج يكسر الانغماس.
-- 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'
-- )