نظام رخصة الصيد في FiveM: التصاريح، الغرامات، والمناطق
إضافة نظام رخصة صيد يعمل إلى FiveM. تصاريح، مناطق محمية، غرامات، وظائف الحراس وأفضل السكربتات لبرمجة قوانين الحياة البرية الواقعية.
Agency Scripts
المؤسس والمطور الرئيسي في Agency Scripts
الرخص كبنية تحتية للعب الأدوار
نظام الرخص والتصاريح هو بنية تحتية أساسية لأي خادم رول بلاي جاد. بدونه، يمكن لكل لاعب فعل كل شيء من اليوم الأول. الرخص تحجب الأنشطة خلف متطلبات واقعية، مما يخلق تقدمًا، عواقب، وتفاعلات رول بلاي. رخصة القيادة تعني أن اللاعبين يجب أن يجتازوا اختبارًا قبل تشغيل المركبات قانونيًا. تصريح الأسلحة يتطلب فحوصات خلفية وتدريب. رخصة الصيد تقيد من يمكنه الصيد وأين. رخصة الأعمال تتحكم بمن يمكنه تشغيل المؤسسات التجارية. تصريح الصيد، رخصة الطيار، رخصة طبية، رخصة قانونية -- كل منها يخلق طبقة من التنظيم تعكس الأنظمة الواقعية وتولد تفاعلات عضوية بين المواطنين، موظفي الحكومة، وإنفاذ القانون. عندما يوقف ضابط شرطة شخصًا ويفحص حالة رخصته، تلك لحظة رول بلاي لا توجد إلا لأن نظام الرخص يجعلها ممكنة. هذا الدرس يشرح بناء إطار رخص مرن وقابل للتوسع يدعم أي نوع من التصاريح يحتاجه خادمك.
تكوين نوع الترخيص المرن
بدلاً من ترميز أنواع الرخص المحددة، قم ببناء نظام يعتمد على التكوين حيث يتطلب إضافة نوع رخصة جديد إدخالًا جديدًا في جدول التكوين فقط. يتضمن تعريف كل نوع رخصة معرفًا فريدًا، اسم العرض، الوصف، تكلفة التقديم، ما إذا كان يتطلب امتحانًا، أسئلة الامتحان إذا كان ذلك مناسبًا، مدة الصلاحية قبل الحاجة للتجديد، الوظائف أو الأدوار التي يمكنها إصداره، وما هي القيود في طريقة اللعب عند فقدان الرخصة. تعني هذه الطريقة أن رخصة الصيد، رخصة القيادة، تصريح الأسلحة، ورخصة الأعمال كلها تعمل على نفس المحرك الأساسي. عندما ينمو خادمك ويحتاج إلى نوع تصريح جديد مثل ميدالية التاكسي أو تصريح البناء، تضيف إدخالًا في التكوين وتعمل عملية التقديم والإصدار والتنفيذ تلقائيًا دون كتابة رمز جديد.
-- shared/config.lua
Config = {}
Config.LicenseTypes = {
driving = {
label = 'Driving License',
description = 'Required to legally operate motor vehicles',
cost = 500,
requiresExam = true,
examType = 'practical',
validDays = 365,
issuedBy = {'dmv', 'police'},
restrictions = {'vehicle_operation'},
},
weapons = {
label = 'Weapons Permit',
description = 'Required to legally carry firearms',
cost = 2500,
requiresExam = true,
examType = 'written',
validDays = 180,
issuedBy = {'police'},
restrictions = {'weapon_carry'},
prerequisites = {'driving'},
},
hunting = {
label = 'Hunting License',
description = 'Required for legal hunting activities',
cost = 750,
requiresExam = true,
examType = 'written',
validDays = 90,
issuedBy = {'ranger', 'dmv'},
restrictions = {'hunting_activity'},
},
fishing = {
label = 'Fishing Permit',
description = 'Required for legal fishing activities',
cost = 200,
requiresExam = false,
validDays = 30,
issuedBy = {'dmv', 'ranger'},
restrictions = {'fishing_activity'},
},
business = {
label = 'Business License',
description = 'Required to operate a commercial business',
cost = 5000,
requiresExam = false,
validDays = 365,
issuedBy = {'government'},
restrictions = {'business_operation'},
},
pilot = {
label = 'Pilot License',
description = 'Required to operate aircraft',
cost = 10000,
requiresExam = true,
examType = 'practical',
validDays = 180,
issuedBy = {'faa'},
restrictions = {'aircraft_operation'},
prerequisites = {'driving'},
},
}
تدفق التطبيق والفحص
يجب أن يشعر عملية التقديم وكأنها تفاعل حكومي حقيقي. يزور اللاعبون موقع DMV، مكتب البريد، أو مبنى حكومي ذي صلة ويتفاعلون مع NPC أو كشك لبدء التقديم. تعرض NUI أنواع الرخص المتاحة، تكاليفها، ومتطلباتها. بعد اختيار رخصة ودفع الرسوم، يتحقق النظام من المتطلبات المسبقة. إذا كانت الرخصة تتطلب امتحانًا كتابيًا، يدخل اللاعب واجهة اختبار تحتوي على أسئلة متعددة الخيارات مأخوذة من مجموعة أسئلة قابلة للتكوين. عشوِ ترتيب الأسئلة ومواقع الإجابات لمنع الغش بالتذكر. يتطلب حدًا أدنى للنجاح، عادة 70 إلى 80 بالمئة. للامتحانات العملية مثل اختبارات القيادة، يتم استدعاء مركبة وإنشاء مسار نقاط تفتيش يجب على اللاعب إكماله ضمن حد زمني مع اتباع قوانين المرور. يمكن لـ NPC مدرب قيادة أو لاعب حقيقي يعمل في وظيفة DMV مراقبة الاختبار ورسوب الطلاب الذين يتجاوزون الإشارات الحمراء، يسرعون بشكل مفرط، أو يصطدمون. يخزن النظام نتيجة الامتحان، وعند النجاح يصدر الرخصة مع فترة صلاحيتها التي تبدأ من الطابع الزمني الحالي.
-- server/licenses.lua
function ApplyForLicense(playerId, licenseType)
local config = Config.LicenseTypes[licenseType]
if not config then return false, 'Invalid license type' end
local identifier = GetPlayerIdentifier(playerId, 0)
-- Check prerequisites
if config.prerequisites then
for _, prereq in ipairs(config.prerequisites) do
local has = HasValidLicense(identifier, prereq)
if not has then
return false, 'Missing prerequisite: ' ..
Config.LicenseTypes[prereq].label
end
end
end
-- Check if already has active license
local existing = MySQL.single.await([[
SELECT id, status, expires_at FROM licenses
WHERE identifier = ? AND license_type = ?
AND status IN ('active','suspended')
]], {identifier, licenseType})
if existing and existing.status == 'active' then
return false, 'You already hold this license'
end
-- Deduct application fee
local paid = exports['framework']:RemoveMoney(
playerId, config.cost, 'bank')
if not paid then
return false, 'Insufficient funds ($' .. config.cost .. ' required)'
end
-- Create pending application
local appId = MySQL.insert.await([[
INSERT INTO license_applications
(identifier, license_type, status, applied_at)
VALUES (?, ?, 'pending', NOW())
]], {identifier, licenseType})
if config.requiresExam then
return true, 'Application submitted. Please complete the exam.', appId
else
-- No exam required, issue directly
IssueLicense(identifier, licenseType)
return true, 'License issued successfully!'
end
end
function IssueLicense(identifier, licenseType)
local config = Config.LicenseTypes[licenseType]
local expiresAt = os.time() + (config.validDays * 86400)
MySQL.insert([[
INSERT INTO licenses
(identifier, license_type, status, issued_at, expires_at)
VALUES (?, ?, 'active', NOW(), FROM_UNIXTIME(?))
ON DUPLICATE KEY UPDATE
status = 'active', issued_at = NOW(),
expires_at = FROM_UNIXTIME(?)
]], {identifier, licenseType, expiresAt, expiresAt})
end
function HasValidLicense(identifier, licenseType)
local result = MySQL.single.await([[
SELECT id FROM licenses
WHERE identifier = ? AND license_type = ?
AND status = 'active' AND expires_at > NOW()
]], {identifier, licenseType})
return result ~= nil
end
exports('HasValidLicense', HasValidLicense)
تكامل إنفاذ القانون
تظهر القوة الحقيقية لنظام الرخص عندما يمكن لإنفاذ القانون الاستعلام وتعديل حالات الرخص أثناء تفاعلات لعب الأدوار. يحتاج ضباط الشرطة إلى القدرة على التحقق من حالة رخصة المواطن أثناء توقفات المرور، التحقق من تصاريح الأسلحة أثناء التفتيش، وتعليق أو إلغاء الرخص كعقوبات على السلوك الإجرامي. أنشئ تكامل MDT للشرطة يعرض جميع الرخص التي يحملها المواطن مع حالتها، تاريخ الإصدار، وتاريخ الانتهاء. أضف أوامر أو إجراءات MDT لتعليق الرخصة مع سبب ومدة، إلغائها بشكل دائم، وإعادتها بعد فترة تعليق. عند تعليق الرخصة، يجب أن يتلقى الحامل إشعارًا يشرح سبب التعليق ومدة التعليق. دمج فحوصات الرخص في سير عمل الإنفاذ الحالي. إذا تم القبض على لاعب يقود بدون رخصة صالحة، يمكن لنظام الشرطة تلقائيًا الإشارة إلى ذلك أثناء توقف المرور. إذا تم القبض على شخص يصطاد بدون تصريح، يمكن للحراس إصدار مخالفات تحمل غرامات وإمكانية إلغاء الرخصة.
-- server/enforcement.lua
RegisterNetEvent('licenses:checkCitizen', function(targetId)
local src = source
-- Verify requesting player is law enforcement
local job = exports['framework']:GetPlayerJob(src)
if job ~= 'police' and job ~= 'ranger' and job ~= 'sheriff' then
return
end
local targetIdentifier = GetPlayerIdentifier(targetId, 0)
local licenses = MySQL.query.await([[
SELECT license_type, status, issued_at, expires_at,
suspended_reason, suspended_until
FROM licenses WHERE identifier = ?
]], {targetIdentifier})
TriggerClientEvent('licenses:showResults', src, licenses)
end)
RegisterNetEvent('licenses:suspend', function(targetIdentifier, licType, reason, days)
local src = source
local job = exports['framework']:GetPlayerJob(src)
if job ~= 'police' and job ~= 'judge' then return end
local suspendUntil = os.time() + (days * 86400)
MySQL.update([[
UPDATE licenses SET
status = 'suspended',
suspended_reason = ?,
suspended_until = FROM_UNIXTIME(?)
WHERE identifier = ? AND license_type = ?
]], {reason, suspendUntil, targetIdentifier, licType})
-- Notify the affected player if online
local targetPlayer = GetPlayerFromIdentifier(targetIdentifier)
if targetPlayer then
TriggerClientEvent('licenses:notify', targetPlayer,
'Your ' .. Config.LicenseTypes[licType].label ..
' has been suspended for ' .. days .. ' days. Reason: ' .. reason)
end
-- Log the action
MySQL.insert([[
INSERT INTO license_logs
(identifier, license_type, action, performed_by, reason)
VALUES (?, ?, 'suspend', ?, ?)
]], {targetIdentifier, licType, GetPlayerIdentifier(src, 0), reason})
end)
نظام التجديد والتعامل مع انتهاء الصلاحية
الرخص التي تحتوي على تواريخ انتهاء تخلق إيرادات متكررة وتفاعلات منتظمة مع NPC تحافظ على إحساس العالم بالحيوية. عندما تقترب الرخصة من تاريخ انتهاء صلاحيتها، أرسل لحاملها إشعارًا إما عبر نظام الهاتف أو نظام البريد الذي يغطيه درس تعليمي منفصل. امنح فترة سماح لبضعة أيام داخل اللعبة حيث تكون الرخصة من الناحية الفنية منتهية الصلاحية لكن اللاعب لا يعاقب فورًا، مما يتيح وقتًا للتجديد. يجب أن يكون التجديد أبسط من التقديم الأولي، ويتطلب فقط الرسوم ولا يحتاج إلى إعادة فحص إلا إذا تم تعليق الرخصة أو إلغاؤها سابقًا. نفذ مهمة مجدولة على جانب الخادم تعمل بشكل دوري للتحقق من الرخص المنتهية وتحديث حالتها. بالنسبة للرخص التي تتطلب تجديدًا دوريًا مثل تصاريح الصيد ذات فترات صلاحية قصيرة، فكر في تقديم خيارات تجديد بالجملة حيث يمكن للاعبين الدفع لعدة فترات مقدمًا بخصم طفيف. هذا يكافئ اللاعبين المخلصين الذين يخططون مسبقًا مع الحفاظ على دورة التجديد التي تحافظ على النظام ديناميكيًا.
تطبيق القيود عبر السكربتات
نظام الرخصة مهم فقط إذا كانت السكربتات الأخرى تطبق القيود فعليًا. أنشئ دالة تصدير مركزية يمكن لأي سكربت استدعاؤها للتحقق مما إذا كان اللاعب يحمل رخصة صالحة من نوع معين. يتحقق سكربت المركبة الخاص بك من رخصة القيادة قبل السماح بتشغيل المحرك أو عند دخول اللاعب مقعد السائق. يتحقق سكربت الأسلحة من تصريح الأسلحة عند تجهيز اللاعب لسلاح ناري. يتحقق سكربت الصيد من رخصة الصيد قبل السماح بصيد الحيوانات. يتحقق سكربت الصيد البحري من تصريح الصيد قبل السماح بالصيد. يقرر كل سكربت عاقبته الخاصة على الانتهاكات. قد يمنع البعض الإجراء مع إشعار فقط. قد يسمح البعض الآخر بالإجراء لكنه يضع علامة على اللاعب لجذب انتباه إنفاذ القانون، مما يطلق مستوى مطلوب تلقائيًا أو تنبيه. الصرامة تعتمد على فلسفة لعب الأدوار في خادمك. يوفر نظام الرخصة طبقة البيانات؛ تحدد سكربتات الإنفاذ العواقب. هذا الفصل يعني أنه يمكنك تعديل صرامة الإنفاذ دون لمس نظام الرخصة نفسه.
مخطط قاعدة البيانات وأدوات الإدارة
يحتاج مخطط قاعدة البيانات إلى جداول لتعريفات الرخص الموجودة بالفعل في التكوين، الرخص النشطة، الطلبات، نتائج الامتحانات، وسجل التدقيق. سجل التدقيق ضروري للمساءلة. يتم تسجيل كل إصدار، تعليق، إلغاء، وإعادة تفعيل للرخصة مع طابع زمني، معرف الضابط المنفذ، والسبب. يجب أن تسمح أوامر الإدارة للموظفين بإصدار أي رخصة مباشرة لأغراض الأحداث، مسح جميع التعليقات خلال أحداث العفو، انتهاء صلاحية جماعية لرخص من نوع معين لإعادة تعيين النظام، وعرض سجل كامل للرخص لأي لاعب. أنشئ لوحة إدارة في لوحة تحكم الويب الخاصة بخادمك أو MDT توفر إحصائيات فورية عن الرخص النشطة حسب النوع، الطلبات المعلقة، التعليقات الأخيرة، والانتهاء القادم. تساعد هذه البيانات في فهم كيفية تفاعل اللاعبين مع النظام وما إذا كانت الرسوم وفترات الصلاحية بحاجة إلى تعديل للحفاظ على تفاعل صحي.
-- SQL schema
CREATE TABLE IF NOT EXISTS licenses (
id INT AUTO_INCREMENT PRIMARY KEY,
identifier VARCHAR(64) NOT NULL,
license_type VARCHAR(32) NOT NULL,
status ENUM('active','suspended','revoked','expired')
DEFAULT 'active',
issued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NULL,
suspended_reason VARCHAR(255) DEFAULT NULL,
suspended_until TIMESTAMP NULL,
UNIQUE KEY unique_license (identifier, license_type),
INDEX idx_status (status),
INDEX idx_expires (expires_at)
);
CREATE TABLE IF NOT EXISTS license_applications (
id INT AUTO_INCREMENT PRIMARY KEY,
identifier VARCHAR(64) NOT NULL,
license_type VARCHAR(32) NOT NULL,
status ENUM('pending','passed','failed','cancelled')
DEFAULT 'pending',
exam_score INT DEFAULT NULL,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP NULL
);
CREATE TABLE IF NOT EXISTS license_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
identifier VARCHAR(64) NOT NULL,
license_type VARCHAR(32) NOT NULL,
action VARCHAR(32) NOT NULL,
performed_by VARCHAR(64),
reason VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_identifier (identifier)
);