FiveM फ़ोन संपर्क समन्वय: कई डिवाइसों के संपर्क सिस्टम
FiveM फ़ोन, टैबलेट और कई किरदारों के स्लॉट के बीच संपर्क समन्वयित करें। डेटाबेस के तरीके, ब्रिज स्क्रिप्ट और नंबर एकसमान रखने का सरल उपाय जानें।
Agency Scripts
Agency Scripts के संस्थापक और प्रमुख डेवलपर
FiveM में फोन डेटा की चुनौती
FiveM के लिए फोन सिस्टम बनाना केवल खिलाड़ी की स्क्रीन पर एक सुंदर UI प्रस्तुत करने से कहीं अधिक है। असली इंजीनियरिंग चुनौती सत्रों के बीच स्थायी डेटा प्रबंधन है: संपर्क, संदेश थ्रेड, कॉल लॉग, फोटो, और ऐप सेटिंग्स को सर्वर पुनःआरंभ, कैरेक्टर स्विच, और मल्टी-कैरेक्टर वातावरण में जीवित रहना होता है। Agency Phone को डेटा अखंडता को एक मूल सिद्धांत के रूप में लेकर शुरू से डिजाइन किया गया था। हर डेटा का टुकड़ा एक सर्वर-प्राधिकृत पाइपलाइन के माध्यम से गुजरता है जहाँ क्लाइंट क्रियाओं का अनुरोध करता है और सर्वर उन्हें सत्यापित, संसाधित, और स्थायी बनाता है फिर क्लाइंट को पुष्टि करता है। यह लेख यह बताता है कि Agency Phone संपर्क सिंक्रोनाइज़ेशन, संदेश भंडारण, फोटो साझा करना, कॉल लॉगिंग, और प्राइवेसी बाय डिज़ाइन को कैसे संभालता है ताकि डेवलपर्स और सर्वर मालिक उत्पाद के पीछे की वास्तुकला को समझ सकें।
संपर्क संग्रह वास्तुकला
Agency Phone में संपर्क प्रति-कैरेक्टर संग्रहीत होते हैं, प्रति-प्लेयर नहीं। यह अंतर महत्वपूर्ण है क्योंकि एक खिलाड़ी के पास एक ही सर्वर पर तीन कैरेक्टर हो सकते हैं, जिनके सामाजिक दायरे पूरी तरह से अलग हो सकते हैं। संपर्क तालिका मालिक के फोन नंबर और संपर्क के फोन नंबर की संयुक्त कुंजी का उपयोग करती है, साथ ही डिस्प्ले नाम, अवतार URL, और पसंदीदा फ़्लैग के लिए अतिरिक्त फ़ील्ड्स होती हैं। जब कोई खिलाड़ी अपने संपर्क ऐप को खोलता है, तो क्लाइंट सर्वर को एक ही अनुरोध भेजता है, जो डेटाबेस से क्वेरी करता है और एक ही बैच में पूरी संपर्क सूची लौटाता है। इससे वाटरफॉल पैटर्न से बचा जाता है जहाँ प्रत्येक संपर्क अपनी खुद की डेटाबेस क्वेरी ट्रिगर करता, जो उन सर्वरों पर विनाशकारी होता है जहाँ खिलाड़ियों के सैकड़ों संपर्क होते हैं।
-- How contacts are structured internally
-- Each contact belongs to a specific phone number (character)
local contactSchema = {
owner_number = 'string', -- the character's phone number
contact_number = 'string', -- the saved contact's number
display_name = 'string', -- custom name set by player
avatar = 'string|nil', -- optional avatar URL
is_favorite = 'boolean', -- pinned to top of list
created_at = 'timestamp', -- when contact was added
}
-- Server: fetch all contacts for a character
lib.callback.register('phone:contacts:getAll', function(source)
local phoneNumber = GetPlayerPhoneNumber(source)
if not phoneNumber then return {} end
local contacts = MySQL.query.await([[
SELECT contact_number, display_name, avatar, is_favorite
FROM phone_contacts
WHERE owner_number = ?
ORDER BY is_favorite DESC, display_name ASC
]], { phoneNumber })
return contacts or {}
end)
रीयल-टाइम संपर्क सिंक
जब कोई खिलाड़ी संपर्क जोड़ता, संपादित करता या हटाता है, तो परिवर्तन तुरंत उनके डिवाइस पर प्रतिबिंबित होना चाहिए और डेटाबेस में स्थायी होना चाहिए। Agency Phone आशावादी अपडेट पैटर्न का उपयोग करता है: क्लाइंट तुरंत अपनी स्थानीय स्थिति अपडेट करता है ताकि त्वरित प्रतिक्रिया मिले, जबकि एक ही समय में म्यूटेशन सर्वर को भेजता है। यदि सर्वर सत्यापन विफलता के कारण परिवर्तन को अस्वीकार करता है, तो क्लाइंट पिछली स्थिति पर वापस लौटता है और त्रुटि दिखाता है। यह एक प्रतिक्रियाशील उपयोगकर्ता अनुभव बनाता है जो मूल जैसा लगता है जबकि डेटा पर सर्वर अधिकार बनाए रखता है। आवश्यक होने पर सर्वर अन्य जुड़े क्लाइंट्स को प्रासंगिक परिवर्तन भी प्रसारित करता है, जैसे जब कोई खिलाड़ी अपना प्रोफ़ाइल नाम अपडेट करता है जो अन्य खिलाड़ियों की संपर्क सूची में दिखाई देता है।
-- Server: add a new contact with validation
lib.callback.register('phone:contacts:add', function(source, data)
local phoneNumber = GetPlayerPhoneNumber(source)
if not phoneNumber then return { success = false, error = 'NO_PHONE' } end
-- Validate the contact number exists in the system
local numberExists = MySQL.scalar.await(
'SELECT COUNT(*) FROM phone_numbers WHERE number = ?',
{ data.contact_number }
)
if numberExists == 0 then
return { success = false, error = 'NUMBER_NOT_FOUND' }
end
-- Prevent duplicate contacts
local existing = MySQL.scalar.await(
'SELECT COUNT(*) FROM phone_contacts WHERE owner_number = ? AND contact_number = ?',
{ phoneNumber, data.contact_number }
)
if existing > 0 then
return { success = false, error = 'ALREADY_EXISTS' }
end
-- Insert the contact
MySQL.insert.await([[
INSERT INTO phone_contacts (owner_number, contact_number, display_name, avatar)
VALUES (?, ?, ?, ?)
]], { phoneNumber, data.contact_number, data.display_name, data.avatar })
return { success = true }
end)
संदेश संग्रहण और थ्रेडिंग
संदेश किसी भी फोन सिस्टम की सबसे डेटा-गहन विशेषता हैं। Agency Phone संदेशों को बातचीत के थ्रेड्स में व्यवस्थित करता है जिन्हें फोन नंबर के जोड़े द्वारा पहचाना जाता है। इसका मतलब है कि नंबर A और नंबर B के बीच की बातचीत हमेशा एक ही थ्रेड से जुड़ी रहती है चाहे इसे किसने शुरू किया हो। थ्रेड के भीतर संदेश कालानुक्रमिक रूप से संग्रहित होते हैं, प्रेषक की पहचान, पढ़े जाने की स्थिति, और वैकल्पिक संलग्नक के साथ। थ्रेडिंग मॉडल समूह संदेशों का भी समर्थन करता है जहां तीन या अधिक नंबर साझा बातचीत में भाग लेते हैं। समूह थ्रेड्स एक अलग पहचानकर्ता का उपयोग करते हैं जो समूह के बनने पर उत्पन्न होता है, और प्रत्येक सदस्य अपनी पढ़ाई सूचक को बनाए रखता है ताकि बिना पढ़े गए संदेशों की संख्या प्रत्येक प्रतिभागी के लिए सही हो।
-- Message thread resolution
-- Ensures A->B and B->A map to the same conversation
local function GetThreadId(number1, number2)
-- Sort numbers to create a deterministic thread ID
local sorted = { number1, number2 }
table.sort(sorted)
return sorted[1] .. ':' .. sorted[2]
end
-- Server: send a message
lib.callback.register('phone:messages:send', function(source, data)
local senderNumber = GetPlayerPhoneNumber(source)
if not senderNumber then return { success = false } end
local threadId = GetThreadId(senderNumber, data.to)
local messageId = MySQL.insert.await([[
INSERT INTO phone_messages (thread_id, sender_number, recipient_number, content, attachment, sent_at)
VALUES (?, ?, ?, ?, ?, NOW())
]], { threadId, senderNumber, data.to, data.content, data.attachment })
-- Notify recipient if online
local recipientSource = GetPlayerByPhoneNumber(data.to)
if recipientSource then
TriggerClientEvent('phone:messages:receive', recipientSource, {
id = messageId,
thread_id = threadId,
sender = senderNumber,
sender_name = GetContactName(data.to, senderNumber),
content = data.content,
attachment = data.attachment,
sent_at = os.time()
})
end
return { success = true, id = messageId }
end)
फोटो साझा करना और मीडिया हैंडलिंग
FiveM फोन में फोटो साझा करना पारंपरिक वेब एप्लिकेशन से अलग दृष्टिकोण मांगता है क्योंकि आप सीधे खिलाड़ी की फ़ाइल प्रणाली तक पहुँच नहीं सकते। Agency Phone फोटो को दो तरीकों से संभालता है: GTA स्क्रीनशॉट फ़ंक्शन का उपयोग करके इन-गेम स्क्रीनशॉट्स, और URL-आधारित छवियाँ जो खिलाड़ी बाहरी छवि होस्टिंग सेवाओं से चिपकाते हैं। इन-गेम स्क्रीनशॉट्स नेटिव स्क्रीनशॉट API का उपयोग करके लिए जाते हैं, डेटा URL में परिवर्तित किए जाते हैं, और एक कॉन्फ़िगर करने योग्य स्टोरेज बैकएंड पर अपलोड किए जाते हैं। सर्वर फ़ाइल आकार सीमाओं और सामग्री प्रकार को सत्यापित करता है इससे पहले कि URL को स्थायी किया जाए। जब फोटो संदेशों में साझा किए जाते हैं, तो केवल URL संदर्भ संदेश रिकॉर्ड में संग्रहीत होता है, जिससे संदेश तालिका हल्की रहती है। वास्तविक छवि डेटा मीडिया स्टोरेज बैकएंड में रहता है, जिसे स्थानीय डिस्क, S3-संगत स्टोरेज, या बाहरी छवि CDN के रूप में कॉन्फ़िगर किया जा सकता है, जो सर्वर मालिक के इन्फ्रास्ट्रक्चर पर निर्भर करता है।
-- Server: handle photo upload from in-game camera
lib.callback.register('phone:photos:upload', function(source, imageData)
local phoneNumber = GetPlayerPhoneNumber(source)
if not phoneNumber then return { success = false } end
-- Validate size (max 2MB base64)
if #imageData > 2 * 1024 * 1024 * 1.37 then
return { success = false, error = 'FILE_TOO_LARGE' }
end
-- Generate unique filename
local filename = ('%s_%s.jpg'):format(phoneNumber, os.time())
-- Store via configured backend (webhook, local, S3)
local url = StorageBackend:upload(filename, imageData)
if not url then
return { success = false, error = 'UPLOAD_FAILED' }
end
-- Save photo reference in gallery
MySQL.insert.await([[
INSERT INTO phone_photos (owner_number, url, created_at)
VALUES (?, ?, NOW())
]], { phoneNumber, url })
return { success = true, url = url }
end)
कॉल लॉग और इतिहास
कॉल लॉग प्रत्येक आने वाली, जाने वाली, और मिस्ड कॉल को टाइमस्टैम्प और अवधि के साथ रिकॉर्ड करता है। जब कोई खिलाड़ी कॉल शुरू करता है, तो "dialing" स्थिति के साथ एक कॉल रिकॉर्ड बनाया जाता है। यदि प्राप्तकर्ता कॉल स्वीकार करता है, तो स्थिति "active" में अपडेट होती है और एक प्रारंभ टाइमस्टैम्प रिकॉर्ड किया जाता है। कॉल समाप्त होने पर, अवधि की गणना की जाती है और रिकॉर्ड अंतिम रूप दिया जाता है। मिस्ड कॉल तब होती है जब प्राप्तकर्ता टाइमआउट अवधि के भीतर उत्तर नहीं देता या स्पष्ट रूप से अस्वीकार करता है। कॉल लॉग फोन के हाल की टैब में कॉल दिशा और स्थिति के दृश्य संकेतों के साथ प्रदर्शित होता है। खिलाड़ी मिस्ड कॉल प्रविष्टि पर टैप करके तुरंत वापस कॉल कर सकते हैं, या नंबर को संपर्कों में जोड़ने के लिए लंबा दबा सकते हैं। सर्वर कॉन्फ़िगर किए गए प्रतिधारण अवधि से पुराने कॉल लॉग को हटाता है, जो डिफ़ॉल्ट रूप से 30 दिन है, ताकि लंबे समय तक चलने वाले सर्वरों पर तालिका अनंत तक बढ़ने से रोकी जा सके।
-- Server: create and manage call records
local activeCalls = {}
function StartCallRecord(callerNumber, receiverNumber)
local callId = MySQL.insert.await([[
INSERT INTO phone_calls (caller_number, receiver_number, status, started_at)
VALUES (?, ?, 'dialing', NOW())
]], { callerNumber, receiverNumber })
activeCalls[callId] = {
caller = callerNumber,
receiver = receiverNumber,
answeredAt = nil
}
return callId
end
function AnswerCall(callId)
if not activeCalls[callId] then return end
activeCalls[callId].answeredAt = os.time()
MySQL.update.await(
'UPDATE phone_calls SET status = ?, answered_at = NOW() WHERE id = ?',
{ 'active', callId }
)
end
function EndCall(callId)
local call = activeCalls[callId]
if not call then return end
local duration = call.answeredAt and (os.time() - call.answeredAt) or 0
local status = call.answeredAt and 'completed' or 'missed'
MySQL.update.await(
'UPDATE phone_calls SET status = ?, duration = ?, ended_at = NOW() WHERE id = ?',
{ status, duration, callId }
)
activeCalls[callId] = nil
end
डिज़ाइन द्वारा गोपनीयता
Agency Phone के पूरे आर्किटेक्चर में privacy-by-design सिद्धांतों का पालन किया गया है। फोन नंबर बेतरतीब ढंग से जनरेट होते हैं और किसी वास्तविक दुनिया के पहचानकर्ता से जुड़े नहीं होते। संदेश का कंटेंट डेटाबेस में स्टोर होता है, लेकिन मान्य server callbacks के ज़रिए सिर्फ़ भेजने वाले और पाने वाले को ही उपलब्ध होता है। ऐसा कोई global message search नहीं है जिसका इस्तेमाल कोई admin स्पष्ट डेटाबेस एक्सेस के बिना निजी बातचीत पढ़ने के लिए कर सके। कॉन्टैक्ट लिस्ट हर किरदार के लिए अलग होती है और किरदारों के बीच डेटा लीक नहीं होता। किसी किरदार को डिलीट करने पर, उससे जुड़ा फोन का सारा डेटा, जिसमें कॉन्टैक्ट, संदेश, कॉल लॉग और फ़ोटो शामिल हैं, डेटाबेस से cascade-delete हो जाता है, जिससे कोई निजी डेटा बिना संबद्ध रिकॉर्ड के नहीं बचता। स्टोरेज से पहले फ़ोटो अपलोड सिस्टम EXIF मेटाडेटा हटा देता है, ताकि लोकेशन या डिवाइस की जानकारी अनजाने में लीक न हो, हालाँकि गेम के माहौल में यह व्यावहारिक चिंता से ज़्यादा एक अच्छी प्रथा है।
स्केल पर प्रदर्शन
Agency Phone को 200 या इससे अधिक समवर्ती खिलाड़ियों वाले सर्वरों के लिए टेस्ट और ऑप्टिमाइज़ किया गया है। प्रदर्शन की अहम रणनीतियों में message threads को lazy load करना शामिल है, ताकि फोन खुलने पर सिर्फ़ हाल की बातचीत फ़ेच हो और पुरानी बातचीत स्क्रॉल करने पर लोड हो। शुरुआती फ़ेच के बाद कॉन्टैक्ट लिस्ट क्लाइंट-साइड कैश होती है और सिर्फ़ कोई बदलाव होने पर रीफ़्रेश होती है। लाखों पंक्तियों वाली टेबल पर भी sub-millisecond लुकअप सुनिश्चित करने के लिए डेटाबेस क्वेरी में फोन नंबर और timestamps पर सही indexes इस्तेमाल किए जाते हैं। डेटाबेस से संपर्क किए बिना तुरंत प्राप्तकर्ता खोजने के लिए सर्वर ऑनलाइन खिलाड़ियों के फोन नंबरों का in-memory map रखता है। जहाँ संभव हो, सभी NUI संचार को बैच किया जाता है, इसलिए मैसेज ऐप खोलने पर एक ही server request में सबसे हालिया संदेश के preview सहित सभी threads मिल जाते हैं, बजाय हर thread के लिए अलग request करने के। इन ऑप्टिमाइज़ेशन से व्यस्ततम सर्वर घंटों में भी फोन तेज़ी से काम करता है, जब दर्जनों खिलाड़ी एक साथ संदेश भेज रहे होते हैं और कॉल कर रहे होते हैं।