FiveM Hunting License System: Разрешения, штрафы и зоны
Добавьте работающую систему охотничьих лицензий в 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)
);