العودة إلى المدونة
Tutorial11 دقيقة قراءة

نظام الراديو والإرسال في FiveM: PMA، pma-voice وCADs

إعداد أنظمة الراديو، الإرسال وCAD في FiveM. قنوات pma-voice، أدوات MDT وأفضل السكريبتات لفرق الشرطة، EMS والميكانيكيين.

Agency Scripts

المؤسس والمطور الرئيسي في Agency Scripts

هيكل نظام الراديو والتوزيع

نظام الراديو والإرسال هو العمود الفقري للاتصالات في خدمات الطوارئ على خادم تمثيل أدوار FiveM. يتيح للشرطة، EMS، وإدارات الإطفاء تنسيق الاستجابات من خلال قنوات راديو منظمة، استقبال تنبيهات الإرسال ذات الأولوية من مكالمات 911 وأنظمة الكشف الآلية، وتتبع مواقع الوحدات في الوقت الحقيقي من خلال تكامل GPS. تتكون البنية من ثلاثة أنظمة مترابطة: إطار قناة الراديو الذي يدير من يمكنه التحدث على أي تردد، قائمة انتظار الإرسال التي تعالج المكالمات الواردة وتوجهها إلى الوحدات المناسبة، وطبقة تتبع GPS التي تصور مواقع الوحدات على خريطة مشتركة. على عكس التواصل البسيط عبر الدردشة، يضيف نظام الراديو الصحيح الانغماس من خلال تبديل الترددات، سماع حديث الراديو فقط عند الضبط، واتباع بروتوكولات اتصال منظمة تحاكي خدمات الطوارئ الواقعية. يعمل النظام بأكمله من خلال إدارة الحالة على جانب الخادم لمنع الغش، مع توفير واجهة راديو ولوحة إرسال على جانب العميل عبر NUI.

نظام قناة الراديو

تنظم قنوات الراديو الاتصال بحيث يمكن للأقسام والوحدات المختلفة التنسيق دون تداخل. تحتوي كل قناة على رقم تردد، قيود وصول بناءً على أدوار الوظائف، وقائمة باللاعبين المتصلين حاليًا. يدعم النظام أنواعًا متعددة من القنوات: قنوات على مستوى القسم للاتصال العام، قنوات تكتيكية لعمليات محددة مثل مطاردة أو استجابة لسرقة بنك، وقنوات بين الأقسام حيث يمكن للشرطة وEMS التنسيق في الحوادث المشتركة. ينضم اللاعبون إلى قناة عبر عنصر الراديو أو واجهة NUI، وتصل كل الصوت أو النص المرسل على تلك القناة فقط إلى اللاعبين المضبوطين على نفس التردد. نفذ نظام المتحدث ذي الأولوية حيث يمكن لمشغلي التوزيع والضباط القائدين البث إلى جميع القنوات في نفس الوقت لتنبيهات الطوارئ:

Config.RadioChannels = {
    -- Police Department
    { frequency = 1, label = 'PD Main',        jobs = {'police'},          type = 'department' },
    { frequency = 2, label = 'PD Patrol',       jobs = {'police'},          type = 'department' },
    { frequency = 3, label = 'PD Tactical',     jobs = {'police'},          type = 'tactical',  maxUsers = 8 },
    { frequency = 4, label = 'PD Detectives',   jobs = {'police'},          type = 'tactical',  minRank = 3 },

    -- EMS / Fire
    { frequency = 10, label = 'EMS Main',       jobs = {'ambulance'},       type = 'department' },
    { frequency = 11, label = 'EMS Field',      jobs = {'ambulance'},       type = 'department' },
    { frequency = 12, label = 'Fire Main',      jobs = {'fire'},            type = 'department' },

    -- Inter-department
    { frequency = 20, label = 'Emergency Joint', jobs = {'police','ambulance','fire'}, type = 'inter' },
    { frequency = 21, label = 'Command Channel', jobs = {'police','ambulance','fire'}, type = 'command', minRank = 5 },

    -- Civilian (if radio item owned)
    { frequency = 50, label = 'Civilian Band',  jobs = {},                  type = 'open' },
}

-- Server-side channel state
local radioState = {} -- [frequency] = { players = {}, priority = false }

RegisterNetEvent('radio:server:joinChannel', function(frequency)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    local channel = nil
    for _, ch in ipairs(Config.RadioChannels) do
        if ch.frequency == frequency then channel = ch break end
    end
    if not channel then return end

    -- Check job access
    if #channel.jobs > 0 then
        local playerJob = Player.PlayerData.job.name
        local hasAccess = false
        for _, job in ipairs(channel.jobs) do
            if job == playerJob then hasAccess = true break end
        end
        if not hasAccess then
            TriggerClientEvent('QBCore:Notify', src, 'No access to this frequency', 'error')
            return
        end
    end

    -- Check rank requirement
    if channel.minRank and Player.PlayerData.job.grade.level < channel.minRank then
        TriggerClientEvent('QBCore:Notify', src, 'Insufficient rank for this channel', 'error')
        return
    end

    -- Check max users for tactical channels
    if channel.maxUsers and radioState[frequency] then
        if #radioState[frequency].players >= channel.maxUsers then
            TriggerClientEvent('QBCore:Notify', src, 'Channel full', 'error')
            return
        end
    end

    -- Leave current channel
    LeaveCurrentChannel(src)

    -- Join new channel
    if not radioState[frequency] then
        radioState[frequency] = { players = {}, priority = false }
    end
    table.insert(radioState[frequency].players, src)

    TriggerClientEvent('radio:client:joined', src, frequency, channel.label)
    TriggerClientEvent('QBCore:Notify', src, 'Tuned to ' .. channel.label .. ' (' .. frequency .. ')', 'success')
end)

نظام 10-رموز

رموز العشرة هي اختصارات راديو موحدة تضيف مصداقية لاتصالات خدمات الطوارئ وتمكن من تحديثات الحالة السريعة عبر نظام الإرسال. بدلاً من تنفيذ رموز العشرة كاختصارات دردشة فقط، قم ببنائها كأوامر وظيفية تحدث حالة الضابط في نظام الإرسال وتطلق ردودًا آلية. عندما يتصل ضابط 10-80 (مطاردة جارية)، يجب على النظام تحديث حالتهم تلقائيًا على لوحة الإرسال، وإنشاء تنبيه مطاردة مرئي لجميع الوحدات، وبدء تتبع GPS لمركبة الضابط. عندما يتصلون 10-97 (عند وصولهم إلى الموقع)، يقوم نظام الإرسال بوضع علامة عليهم كموجودين في الموقع للنداء النشط الذي يستجيبون له. قم ببناء نظام 10 الرموز كجدول قابل للتكوين حتى يتمكن مالكو الخوادم من تخصيص الرموز لتتناسب مع بروتوكول القسم المفضل لديهم:

Config.TenCodes = {
    ['10-4']  = { label = 'Acknowledged',           action = nil,                           status = nil },
    ['10-6']  = { label = 'Busy',                    action = nil,                           status = 'busy' },
    ['10-7']  = { label = 'Out of Service',          action = 'setOffDuty',                  status = 'off_duty' },
    ['10-8']  = { label = 'In Service',              action = 'setOnDuty',                   status = 'available' },
    ['10-11'] = { label = 'Traffic Stop',            action = 'createTrafficStop',            status = 'traffic_stop' },
    ['10-15'] = { label = 'Suspect in Custody',      action = nil,                           status = 'transport' },
    ['10-20'] = { label = 'Location Request',        action = 'shareLocation',               status = nil },
    ['10-23'] = { label = 'Arrived at Scene',        action = 'markOnScene',                 status = 'on_scene' },
    ['10-32'] = { label = 'Person with Weapon',      action = 'createAlert',                 status = 'responding', priority = 'high' },
    ['10-41'] = { label = 'Beginning Tour of Duty',  action = 'clockIn',                     status = 'available' },
    ['10-42'] = { label = 'Ending Tour of Duty',     action = 'clockOut',                    status = 'off_duty' },
    ['10-71'] = { label = 'Shooting',                action = 'createAlert',                 status = 'responding', priority = 'critical' },
    ['10-78'] = { label = 'Officer Needs Assistance', action = 'panicButton',                status = 'emergency', priority = 'critical' },
    ['10-80'] = { label = 'Pursuit in Progress',     action = 'startPursuit',                status = 'pursuit', priority = 'high' },
    ['10-97'] = { label = 'Arriving on Scene',       action = 'markOnScene',                 status = 'on_scene' },
    ['10-99'] = { label = 'Officer Down',            action = 'officerDown',                 status = 'emergency', priority = 'critical' },
    ['code4'] = { label = 'No Further Assistance',   action = 'clearScene',                  status = 'available' },
}

RegisterNetEvent('radio:server:tenCode', function(code)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    local codeConfig = Config.TenCodes[code]
    if not codeConfig then return end

    -- Update officer status
    if codeConfig.status then
        UpdateUnitStatus(src, codeConfig.status)
    end

    -- Execute associated action
    if codeConfig.action then
        ExecuteCodeAction(codeConfig.action, src, codeConfig)
    end

    -- Broadcast to channel
    local channel = GetPlayerChannel(src)
    if channel then
        local name = GetUnitCallsign(src) or Player.PlayerData.charinfo.firstname
        BroadcastToChannel(channel, {
            type = 'tencode',
            code = code,
            label = codeConfig.label,
            unit = name,
            priority = codeConfig.priority,
        })
    end
end)

قائمة انتظار مكالمات الإرسال

قائمة انتظار الإرسال هي المحور المركزي الذي يعالج جميع مكالمات الطوارئ الواردة ويوجهها إلى الوحدات المناسبة. عندما يتصل مدني برقم 911، أو عندما يتم تشغيل إنذار تلقائي في بنك أو متجر، أو عندما يطلب ضابط شرطة الدعم، ينشئ النظام إدخال إرسال بمستوى أولوية، الموقع، الوصف، ونوع الخدمة المطلوبة. تعرض القائمة جميع المكالمات النشطة مرتبة حسب الأولوية على لوحة NUI للإرسال، مع عرض وقت المكالمة، الموقع، الحالة، والوحدات المعينة. يمكن لدور مشغل الإرسال المخصص تعيين المكالمات يدويًا إلى وحدات محددة، أو يمكن للضباط تعيين أنفسهم بالنقر على مكالمة وتحديد أنهم يستجيبون. نفذ تصعيد المكالمات التلقائي حيث تزيد أولوية المكالمات غير المجابة بعد مهلة قابلة للتكوين، لضمان عدم تجاهل أي حالة طارئة خلال فترات الانشغال:

local dispatchQueue = {}
local callIdCounter = 0

function CreateDispatchCall(data)
    callIdCounter = callIdCounter + 1

    local call = {
        id = callIdCounter,
        type = data.type or 'general',          -- police, ems, fire, general
        priority = data.priority or 'standard', -- low, standard, urgent, critical
        code = data.code or nil,                -- e.g., '10-71', '211' (robbery)
        title = data.title,
        description = data.description or '',
        coords = data.coords,
        street = GetStreetName(data.coords),
        blip = data.blip or nil,
        caller = data.caller or 'Unknown',
        callerId = data.callerId or nil,
        timestamp = os.time(),
        status = 'pending',                     -- pending, assigned, responding, on_scene, resolved
        assignedUnits = {},
        escalationTimer = nil,
        metadata = data.metadata or {},
    }

    -- Set escalation timer
    if call.priority ~= 'critical' then
        call.escalationTimer = os.time() + Config.EscalationTimeout
    end

    table.insert(dispatchQueue, call)

    -- Notify appropriate department
    local targetJobs = GetJobsForCallType(call.type)
    local players = QBCore.Functions.GetQBPlayers()

    for _, player in pairs(players) do
        local job = player.PlayerData.job
        if job.onduty and TableContains(targetJobs, job.name) then
            TriggerClientEvent('dispatch:client:newCall', player.PlayerData.source, call)
        end
    end

    -- Play alert sound based on priority
    if call.priority == 'critical' then
        PlayDispatchAlert('critical', targetJobs)
    end

    return call.id
end

-- Automated dispatch triggers
AddEventHandler('banking:robberyStarted', function(bankId, coords)
    CreateDispatchCall({
        type = 'police',
        priority = 'critical',
        code = '211',
        title = 'Bank Robbery in Progress',
        description = 'Silent alarm triggered at ' .. GetBankLabel(bankId),
        coords = coords,
        blip = { sprite = 500, color = 1, scale = 1.5, flash = true },
    })
end)

AddEventHandler('hospital:911call', function(callerId, coords, description, injuries)
    CreateDispatchCall({
        type = 'ems',
        priority = injuries and injuries.maxSeverity >= 80 and 'critical' or 'urgent',
        title = 'Medical Emergency',
        description = description,
        coords = coords,
        callerId = callerId,
        caller = GetPlayerName(callerId),
        blip = { sprite = 153, color = 3, scale = 1.2 },
        metadata = { injuries = injuries },
    })
end)

تتبع GPS وخريطة الوحدة

يوفر تتبع GPS رؤية في الوقت الحقيقي لمواقع جميع الوحدات النشطة عبر الخريطة، مما يمكّن المرسلين والضباط القائدين من اتخاذ قرارات تعيين مستنيرة. يتم بث موقع كل ضابط في الخدمة إلى نظام الإرسال بفاصل زمني قابل للتكوين، عادةً كل 5-10 ثوانٍ، ويُعرض على لوحة خريطة NUI مشتركة يمكن للمشغلين والمشرفين الوصول إليها. تُظهر الخريطة نقاط الوحدات ملونة حسب القسم والحالة، مع رموز تشير إلى ما إذا كانوا متاحين، يستجيبون، في الموقع، أو في مطاردة. تتضمن ميزة تتبع المسار التي ترصد حركة الوحدة خلال الدقائق القليلة الماضية حتى يتمكن المرسلون من رؤية اتجاه السفر أثناء المطاردات. تنفيذ تصور نصف قطر المكالمة الذي يرسم دائرة حول المكالمات النشطة تُظهر منطقة الاستجابة الموصى بها. عندما يحتاج المرسل إلى العثور على أقرب وحدة متاحة لمكالمة جديدة، يمكن للنظام حساب المسافات من جميع الوحدات المتاحة واقتراح التعيين الأمثل. حافظ على كفاءة بث GPS عن طريق إرسال تحديثات الموقع فقط عندما تتحرك الوحدة أكثر من حد مسافة أدنى لتقليل حركة الشبكة على الخوادم المزدحمة.

نظام التنبيه وزر الذعر

نظام التنبيه يتعامل مع الإشعارات ذات الأولوية العالية التي تتطلب اهتمامًا فوريًا من جميع الوحدات. التنبيه الأكثر حرجًا هو زر الذعر، الذي يتم تفعيله عندما يتصل ضابط 10-78 أو 10-99، التي ترسل موقع GPS الخاص بهم إلى جميع الوحدات المناوبة مع وميض ونغمة إنذار مميزة. يجب أن تتجاوز تنبيهات الذعر أولوية قائمة الانتظار العادية للاتصال وتعرض بشكل بارز على شاشة كل ضابط حتى يتم الاعتراف بها. نفذ مستويات تنبيه مختلفة تحفز استجابات تصاعدية: التنبيه العادي ينشئ مكالمة اتصال مع صوت إشعار، التنبيه العاجل يومض لوحة الاتصال ويشغل نغمة تحذير، والتنبيه الحرج يطلق إشعارًا كامل الشاشة مع صوت صفارة وتوجيه GPS تلقائي إلى موقع التنبيه. تتكامل التنبيهات الآلية مع أنظمة الخادم الأخرى بحيث تحفز مناطق كشف إطلاق النار التنبيهات عند إطلاق الأسلحة في الأماكن العامة، وكاميرات السرعة تضع علامات على المركبات التي تتجاوز حدود السرعة، وأنظمة إنذار المتاجر تنشئ مكالمات اتصال عند بدء السطو. لكل نوع تنبيه فترات تبريد قابلة للتكوين لمنع الرسائل المزعجة من التنبيهات المتكررة في نفس المنطقة.

دور مشغل الإرسال

مشغل الإرسال هو دور مخصص يجلس في محطة عمل ويدير تدفق مكالمات الطوارئ إلى الوحدات الميدانية. على عكس الضباط الذين يرون إشعار إرسال مبسط، لدى المشغل وحدة تحكم إرسال كاملة بنظام NUI مع مدير قائمة الانتظار، قائمة الوحدات، خريطة حية، وأدوات اتصال. يمكن للمشغل ترتيب الأولويات بسحب المكالمات في القائمة، تعيين وحدات محددة للمكالمات بناءً على القرب والتوفر، تعليم المكالمات كمُنجزة عندما تبلغ الوحدات بأنها خالية، وبث رسائل لجميع الوحدات على تردد القسم. نفذ نظام محطة عمل حيث يجب أن يكون مشغلو الإرسال جالسين على مكتب إرسال مخصص للوصول إلى الوحدة الكاملة، مما يمنعهم من تشغيل نظام الإرسال أثناء التواجد في الميدان. يجب أن يكون لدور المشغل تقدم وظيفي خاص حيث يحصل المرسلون ذوو الخبرة على أدوات تنسيق بين الأقسام وقدرات بث الطوارئ. تتبع مؤشرات أداء الإرسال مثل متوسط وقت الاستجابة، المكالمات التي تم التعامل معها لكل وردية، ومعدل حل المكالمات لتزويد المشغلين بتعليقات على أدائهم ومنح مديري الخادم بيانات لاتخاذ قرارات التوظيف.

تكامل راديو الصوت

لخوادم تستخدم حلول الدردشة الصوتية مثل pma-voice أو mumble-voip، دمج نظام الراديو مع الصوت القريب لإنشاء اتصال راديو واقعي. عندما يبث اللاعب على قناة راديو بالضغط على مفتاح، يجب أن يسمع صوته جميع اللاعبين على نفس التردد بغض النظر عن القرب الجسدي، مع تطبيق تأثير فلتر الراديو لتمييز الاتصال الراديوي عن المحادثة وجهًا لوجه. نفذ نظام كتم حيث يجب على اللاعبين تحرير مفتاح البث قبل أن يتمكن لاعب آخر من الكلام، لمحاكاة سلوك الراديو نصف المزدوج الحقيقي. أضف مؤثرات صوتية للراديو عند الضغط والإفلات لتوفير ردود فعل صوتية عند بدء وإيقاف البث. يجب أن يحترم التكامل الصوتي ضوابط الوصول للقناة المعرفة في نظام الراديو، بحيث لا يمكن للاعبين الذين لم ينضموا إلى تردد عبر NUI الراديو سماع أو بث عليه حتى لو عرفوا معرف قناة mumble. للخوادم بدون دردشة صوتية، استخدم نظام راديو نصي حيث تظهر الرسائل المرسلة على تردد في تنسيق دردشة منسق مع رمز المتصل ومعرف القناة.

الدمج وقابلية التوسع

نظام الراديو والإرسال الخاص بك يجب أن يوفر API نظيف يمكن للموارد الأخرى استخدامه لإنشاء مكالمات الإرسال، إرسال التنبيهات، واستعلام حالة الوحدات دون الحاجة لفهم التنفيذ الداخلي. صدّر وظائف مثل CreateDispatchCall, SendAlert, GetAvailableUnits، و GetUnitStatus حتى يتمكن أي مورد على الخادم من التكامل مع نظام الإرسال. عندما يبدأ سرقة بنك، يستدعي مورد البنك تصدير الإرسال لإنشاء مكالمة. عندما يتلقى EMS مكالمة 911، يقوم مورد المستشفى بتوجيهها عبر نفس نظام الإرسال. يضمن هذا النهج المركزي أن تتدفق جميع الاتصالات الطارئة عبر نظام واحد مع معالجة أولوية متسقة، وتتبع الوحدات، وتسجيل الدخول. تخزين سجل مكالمات الإرسال في قاعدة البيانات لمراجعة المسؤول وإنشاء تقارير الوردية التي تلخص حجم المكالمات، وأوقات الاستجابة، ونشاط الوحدات لكل فترة عمل. ربط نظام الإرسال بخطافات ويب Discord بحيث تظهر التنبيهات الحرجة مثل مكالمات سقوط الضباط أو سرقات البنوك في قناة مخصصة للموظفين، مما يبقي مديري الخادم على اطلاع حتى عندما لا يكونون داخل اللعبة.

جاهز للبدء؟

احصل على السكربتات من متجرنا، أو انضم إلى Discord للدعم والتحديثات ونظرة على ما هو قادم.