نظام وكيل السيارات في FiveM: برمجة وكلاء السيارات بشكل صحيح
ابنِ تاجر سيارات واقعي لـ FiveM. تجارب القيادة، إعداد صالة العرض، خيارات NPC وتشغيل اللاعبين، بالإضافة إلى أفضل سكريبتات الوكالات لسيرفرات QBCore و ESX.
Agency Scripts
المؤسس والمطور الرئيسي في Agency Scripts
هيكلية وكالة بيع المركبات
نظام تاجر سيارات هو أحد المحركات الاقتصادية الرئيسية في أي خادم دور FiveM، يعمل كبوابة يحصل من خلالها اللاعبون على مركباتهم. يتجاوز معرض السيارات المصمم جيدًا قائمة شراء بسيطة، ويقدم ميزات مثل معاينات المركبات في صالة العرض، تجارب القيادة، خيارات التمويل، المبادلات، وتتبع المبيعات لموظفي الوكالة. تنقسم البنية إلى نظام كتالوج يحدد المركبات المتاحة مع التسعير والفئات، عرض صالة عرض يسمح للاعبين بفحص المركبات قبل الشراء، محرك معاملات يدير عمليات الشراء والتمويل، وطبقة إدارة الموظفين لوكالات يديرها اللاعبون. تتواصل كل طبقة عبر أحداث يتم التحقق من صحتها على الخادم لمنع التلاعب بالأسعار وإنشاء المركبات غير المصرح به.
نظام كتالوج وتصنيف المركبات
يحدد كتالوج المركبات كل سيارة متاحة للشراء، منظمًا في فئات لتسهيل التصفح. يتضمن كل إدخال اسم الاستدعاء، التسمية المعروضة، السعر، الفئة، وبيانات اختيارية مثل السرعة القصوى وعدد المقاعد لعرض صالة العرض. خزّن الكتالوج في ملف تكوين مشترك يشير إليه كل من العميل والخادم، مما يضمن تحقق السعر على جانب الخادم بينما يستخدم العميل نفس البيانات للعرض. فيما يلي تعريف كتالوج منظم:
Config.VehicleCatalog = {
categories = {
{id = 'sedan', label = 'Sedans', icon = 'fa-car'},
{id = 'sport', label = 'Sports Cars', icon = 'fa-flag-checkered'},
{id = 'suv', label = 'SUVs & Trucks', icon = 'fa-truck'},
{id = 'muscle', label = 'Muscle Cars', icon = 'fa-bolt'},
{id = 'motorcycle', label = 'Motorcycles', icon = 'fa-motorcycle'},
},
vehicles = {
-- Sedans
{model = 'sultan', label = 'Karin Sultan', price = 24500,
category = 'sedan', seats = 4, testDrive = true},
{model = 'schafter2', label = 'Benefactor Schafter', price = 38000,
category = 'sedan', seats = 4, testDrive = true},
-- Sports
{model = 'elegy2', label = 'Annis Elegy RH8', price = 95000,
category = 'sport', seats = 2, testDrive = true},
{model = 'comet2', label = 'Pfister Comet', price = 110000,
category = 'sport', seats = 2, testDrive = true},
-- SUVs
{model = 'baller', label = 'Gallivanter Baller', price = 55000,
category = 'suv', seats = 4, testDrive = true},
-- Muscle
{model = 'dominator', label = 'Vapid Dominator', price = 42000,
category = 'muscle', seats = 2, testDrive = true},
-- Motorcycles
{model = 'bati', label = 'Pegassi Bati 801', price = 18000,
category = 'motorcycle', seats = 2, testDrive = true},
},
}
-- Build lookup table for fast server-side price validation
Config.VehiclePrices = {}
for _, v in ipairs(Config.VehicleCatalog.vehicles) do
Config.VehiclePrices[v.model] = v.price
end
جدول البحث عن الأسعار Config.VehiclePrices يُمكّن التحقق من السعر بوقت ثابت O(1) على الخادم، مما يمنع العملاء من إرسال أسعار معدلة. تحقق دائمًا من اسم النموذج مقابل هذا الجدول قبل معالجة الشراء. فكر في تحميل أسعار المركبات من جدول قاعدة بيانات بدلاً من ملف التكوين إذا كنت تريد لمسؤولي الخادم تعديل الأسعار عبر لوحة الإدارة دون إعادة تشغيل الخادم.
نظام معاينة صالة العرض
تتيح صالة العرض للاعبين فحص المركبات في بيئة محكومة قبل الالتزام بالشراء. استدعِ مركبة معاينة في موقع صالة عرض مخصص، وطبق كاميرا يمكن للاعب تدويرها حول المركبة، واعرض الإحصائيات بجانب النموذج. يجب أن تكون مركبة المعاينة غير قابلة للتفاعل ويتم إزالتها عند إغلاق اللاعب للقائمة أو اختيار مركبة مختلفة. فيما يلي منطق المعاينة على جانب العميل:
-- client/showroom.lua
local previewVehicle = nil
local previewCam = nil
local camAngle = 0.0
function ShowVehiclePreview(modelName)
-- Clean up previous preview
DestroyPreview()
local model = GetHashKey(modelName)
RequestModel(model)
while not HasModelLoaded(model) do Wait(10) end
local showroomPos = Config.ShowroomPosition -- vector4
previewVehicle = CreateVehicle(model, showroomPos.x, showroomPos.y,
showroomPos.z, showroomPos.w, false, false)
SetEntityInvincible(previewVehicle, true)
SetVehicleDoorsLocked(previewVehicle, 2)
FreezeEntityPosition(previewVehicle, true)
SetVehicleOnGroundProperly(previewVehicle)
SetModelAsNoLongerNeeded(model)
-- Create orbiting camera
previewCam = CreateCam('DEFAULT_SCRIPTED_CAMERA', true)
UpdateCameraPosition()
SetCamActive(previewCam, true)
RenderScriptCams(true, true, 500, true, true)
end
function UpdateCameraPosition()
if not previewCam or not previewVehicle then return end
local vehPos = GetEntityCoords(previewVehicle)
local radius = 6.0
local height = 2.0
local rad = math.rad(camAngle)
local camX = vehPos.x + radius * math.cos(rad)
local camY = vehPos.y + radius * math.sin(rad)
SetCamCoord(previewCam, camX, camY, vehPos.z + height)
PointCamAtEntity(previewCam, previewVehicle, 0.0, 0.0, 0.0, true)
end
function DestroyPreview()
if previewVehicle then
DeleteEntity(previewVehicle)
previewVehicle = nil
end
if previewCam then
SetCamActive(previewCam, false)
RenderScriptCams(false, true, 500, true, true)
DestroyCam(previewCam, false)
previewCam = nil
end
end
السماح بتدوير الكاميرا من خلال حركة الماوس أو تحكمات لوحة المفاتيح أثناء فتح صالة العرض. تتيح طريقة كاميرا المدار للاعبين عرض المركبة من جميع الزوايا دون الحاجة للمشي حولها. أضف تخصيص لون المركبة للمعاينة حتى يتمكن اللاعبون من رؤية طلاءهم المرغوب قبل الشراء، مما يقلل من ندم المشتري وطلبات الدعم.
المشتريات ومعالجة المعاملات
يجب معالجة عمليات شراء المركبات بالكامل على جانب الخادم لمنع الاستغلال. يتحقق الخادم من أن اللاعب يستطيع تحمل تكلفة المركبة، يخصم الدفع، يولد لوحة ترخيص فريدة، ينشئ سجل المركبة في قاعدة البيانات، ويخطر العميل لإنشاء المركبة المشتراة. دعم كل من عمليات الشراء النقدية الكاملة والتحويلات البنكية، ودمج اختياري مع نظامك المصرفي للتحويلات البنكية:
RegisterNetEvent('dealer:server:purchaseVehicle', function(modelName, paymentType)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
-- Validate model exists and get price
local price = Config.VehiclePrices[modelName]
if not price then
TriggerClientEvent('QBCore:Notify', src, 'Vehicle not available', 'error')
return
end
-- Check payment
local moneyType = paymentType == 'bank' and 'bank' or 'cash'
if Player.PlayerData.money[moneyType] < price then
TriggerClientEvent('QBCore:Notify', src, 'Insufficient funds', 'error')
return
end
-- Generate unique plate
local plate = GenerateUniquePlate()
-- Process payment
Player.Functions.RemoveMoney(moneyType, price, 'vehicle-purchase-' .. modelName)
-- Create vehicle record
local vehicleHash = GetHashKey(modelName)
MySQL.insert([[
INSERT INTO player_vehicles
(citizenid, vehicle, hash, plate, garage, state, fuel, engine, body)
VALUES (?, ?, ?, ?, ?, 1, 100, 1000.0, 1000.0)
]], {
Player.PlayerData.citizenid,
modelName,
tostring(vehicleHash),
plate,
'pillboxgarage',
})
-- Log transaction
MySQL.insert([[
INSERT INTO vehicle_sales (citizenid, vehicle, plate, price, sold_at)
VALUES (?, ?, ?, ?, NOW())
]], {Player.PlayerData.citizenid, modelName, plate, price})
TriggerClientEvent('QBCore:Notify', src,
'Vehicle purchased! Plate: ' .. plate, 'success')
TriggerClientEvent('dealer:client:vehiclePurchased', src, modelName, plate)
end)
function GenerateUniquePlate()
local plate
repeat
plate = ''
local chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
for i = 1, 8 do
local idx = math.random(1, #chars)
plate = plate .. chars:sub(idx, idx)
end
local exists = MySQL.scalar.await(
'SELECT 1 FROM player_vehicles WHERE plate = ?', {plate}
)
until not exists
return plate
end
ال GenerateUniquePlate function تستخدم حلقة إعادة محاولة لضمان تفرد اللوحة عبر قاعدة البيانات بأكملها. رغم أن التصادمات نادرة إحصائيًا مع 8 أحرف أبجدية رقمية، إلا أن الفحص يضمن الأمان المطلق. سجّل كل عملية بيع في جدول منفصل للمراجعة الإدارية وتحليلات اقتصاد الخادم، متتبعًا أكثر المركبات شعبية والتدفق النقدي الكلي عبر الوكالة.
نظام تجربة القيادة
تتيح تجارب القيادة للاعبين اختبار مركبة قبل الشراء، وهو أمر مهم خصوصًا للمركبات المكلفة. استدعِ مركبة مؤقتة مع مؤقت وحدود جغرافية، تعيد اللاعب تلقائيًا إلى الوكالة عند انتهاء الوقت أو مغادرة المنطقة المسموح بها. ضع علامة على مركبة تجربة القيادة بحيث لا يمكن تخزينها في المرآب أو تعديلها، مما يمنع اللاعبين من استغلال النظام للحصول على مركبات مجانية:
-- Client: test drive logic
local testDriveVehicle = nil
local testDriveTimer = 0
local testDriveActive = false
function StartTestDrive(modelName, duration)
local model = GetHashKey(modelName)
RequestModel(model)
while not HasModelLoaded(model) do Wait(10) end
local spawnPos = Config.TestDriveSpawn
testDriveVehicle = CreateVehicle(model, spawnPos.x, spawnPos.y,
spawnPos.z, spawnPos.w, true, false)
SetVehicleNumberPlateText(testDriveVehicle, 'TESTDRVE')
TaskWarpPedIntoVehicle(PlayerPedId(), testDriveVehicle, -1)
SetModelAsNoLongerNeeded(model)
testDriveTimer = duration
testDriveActive = true
-- Timer and boundary check thread
CreateThread(function()
while testDriveActive and testDriveTimer > 0 do
Wait(1000)
testDriveTimer = testDriveTimer - 1
-- Show remaining time
SendNUIMessage({
action = 'updateTestDrive',
timeLeft = testDriveTimer
})
-- Check boundary
local playerPos = GetEntityCoords(PlayerPedId())
local dealerPos = Config.DealerLocation
if #(playerPos - dealerPos) > Config.TestDriveRadius then
QBCore.Functions.Notify('Too far from dealer, returning...', 'error')
EndTestDrive()
return
end
end
if testDriveActive then
EndTestDrive()
end
end)
end
function EndTestDrive()
testDriveActive = false
if testDriveVehicle and DoesEntityExist(testDriveVehicle) then
DeleteEntity(testDriveVehicle)
testDriveVehicle = nil
end
local returnPos = Config.DealerLocation
SetEntityCoords(PlayerPedId(), returnPos.x, returnPos.y, returnPos.z)
QBCore.Functions.Notify('Test drive ended', 'info')
end
الحد الجغرافي يمنع اللاعبين من قيادة مركبة الاختبار عبر الخريطة وتركها. اضبط نصف القطر على مسافة معقولة تسمح بقيادة ذات مغزى عبر الشوارع القريبة مع الحفاظ على إمكانية استرداد المركبة. لوحة "TESTDRVE" تعمل كمؤشر بصري للاعبين الآخرين أن المركبة مؤقتة.
التمويل وخطط الدفع
للمركبات المكلفة، قدم خيار تمويل حيث يدفع اللاعبون دفعة أولى ثم يدفعون أقساطًا مع مرور الوقت. تتبع القرض في جدول قاعدة بيانات مع الرصيد المتبقي، جدول الدفع، ومعدل الفائدة. إذا تخلف اللاعب عن الدفع، يمكن وضع علامة على المركبة للاسترداد. تضيف هذه الميزة عمقًا اقتصاديًا وتجعل المركبات الفاخرة متاحة للاعبين الذين لم يجمعوا ما يكفي من النقود للشراء الكامل. نفذ فحص الأقساط كوظيفة متكررة على جانب الخادم تعمل يوميًا بوقت اللعبة، تخصم المدفوعات من حساب اللاعب البنكي وتبلغهم بكل خصم. إذا كان الرصيد البنكي غير كافٍ، زد عداد الدفعات المتأخرة وأصدر تحذيرًا. بعد عدد قابل للتكوين من الدفعات المتأخرة، علم المركبة للاسترداد حيث تُزال من مرآب اللاعب وتُعاد إلى مخزون التاجر.
تتبع مبيعات الموظفين والعمولات
تحتاج وكالات السيارات التي يديرها اللاعبون إلى أدوات لإدارة موظفي المبيعات، وتتبع الأداء، وتوزيع العمولات. عندما يسهل موظف الوكالة بيعًا، يحصل على نسبة مئوية قابلة للتكوين من سعر السيارة كعمولة. تتبع عدد مبيعات كل موظف، والإيرادات الإجمالية التي حققها، والعمولة المكتسبة في جدول قاعدة بيانات. أنشئ لوحة تحكم للموظف يمكن الوصول إليها عبر NUI الوكالة تعرض الإحصائيات الشخصية، وتاريخ المبيعات الأخير، ولوحة المتصدرين التي تقارن الأداء عبر فريق المبيعات. يجب أن يكون لمالك الوكالة أو مدير الدرجة وصول إلى لوحة إدارة لضبط معدلات العمولة، وإضافة أو إزالة الموظفين، وعرض تقارير المبيعات الإجمالية. هذا يحول الوكالة من تفاعل NPC ثابت إلى عمل ديناميكي يديره اللاعب مع مسؤوليات إدارية حقيقية وحوافز تنافسية.
التحسين وتدابير مكافحة الاستغلال
تواجه سكربتات الوكالات التجارية نقاط استغلال شائعة تحتاج إلى التخفيف الاستباقي. الأهم هو التلاعب بالأسعار، حيث يرسل العميل المعدل طلب شراء بسعر أقل. تحقق دائمًا من الأسعار على جانب الخادم مقابل الكتالوج ولا تثق أبدًا بالقيم المبلغ عنها من العميل. قم بتحديد معدل أحداث الشراء لمنع الشراء السريع الذي قد يكرر المركبات أو يخلق ظروف سباق في قاعدة البيانات. لعرض المعاينة في صالة العرض، تأكد من إنشاء مركبة المعاينة مع false لمعامل الشبكة بحيث يوجد محليًا فقط ولا يمكن دخوله أو سرقته من قبل لاعبين آخرين. قم بتنظيف المركبات المعروضة في onResourceStop المعالج لمنع الكيانات اليتيمة إذا تم إعادة تشغيل المورد. لخوادم بها عدة وكالات، خزّن كتالوج المركبات في الذاكرة وأعد تحميله فقط عندما يقوم مسؤول بتشغيل أمر تحديث، متجنبًا قراءة ملفات التكوين المتكررة عند كل فتح NUI. راقب سجلات الشراء للانحرافات مثل شراء نفس اللاعب لعشرات المركبات بسرعة، مما قد يشير إلى استغلال أو تكرار أموال يستدعي التحقيق.