System licencji myśliwskich FiveM: pozwolenia, mandaty i strefy
Dodaj działający system licencji myśliwskich do FiveM. Pozwolenia, strefy chronione, mandaty, prace strażnika leśnego i najlepsze skrypty do oskryptowania realistycznego prawa łowieckiego.
Agency Scripts
Założyciel i główny deweloper w Agency Scripts
Licencje jako infrastruktura roleplay
System licencji i pozwoleń to fundamentalna infrastruktura każdego poważnego serwera roleplay. Bez niego każdy gracz może robić wszystko od pierwszego dnia. Licencje warunkują aktywności realistycznymi wymogami, tworząc progresję, konsekwencje i interakcje roleplay. Prawo jazdy oznacza, że gracze muszą zdać egzamin przed legalnym prowadzeniem pojazdów. Pozwolenie na broń wymaga sprawdzenia przeszłości i szkolenia. Licencja myśliwska reguluje, kto i gdzie może polować. Licencja biznesowa określa, kto może prowadzić działalność gospodarczą. Karta wędkarska, licencja pilota, licencja medyczna, licencja prawnicza, każda z nich tworzy warstwę przepisów odzwierciedlających rzeczywiste systemy i generuje naturalne interakcje między obywatelami, urzędnikami i służbami porządkowymi. Gdy policjant zatrzymuje kogoś do kontroli i sprawdza status jego uprawnień, powstaje moment roleplay, który istnieje tylko dlatego, że umożliwia go system licencji. Ten poradnik przeprowadzi Cię przez proces budowy elastycznego, rozszerzalnego frameworku licencji, który obsłuży każdy rodzaj zezwolenia potrzebny na Twoim serwerze.
Elastyczna konfiguracja typów licencji
Zamiast wpisywać na sztywno konkretne typy licencji, stwórz system oparty na konfiguracji, w którym dodanie nowego typu licencji wymaga jedynie nowego wpisu w tabeli konfiguracyjnej. Każda definicja typu licencji zawiera unikalny identyfikator, nazwę wyświetlaną, opis, koszt wniosku, informację, czy wymagany jest egzamin, ewentualne pytania egzaminacyjne, czas ważności przed koniecznością odnowienia, frakcje lub role uprawnione do jej wydawania oraz ograniczenia w rozgrywce obowiązujące przy braku licencji. Dzięki temu podejściu licencja na polowanie, prawo jazdy, pozwolenie na broń i licencja biznesowa działają na tym samym silniku bazowym. Kiedy twój serwer się rozrośnie i będzie potrzebować nowego zezwolenia, takiego jak licencja na taksówkę czy pozwolenie na budowę, dodajesz wpis w configu, a cały proces składania wniosków, wydawania i egzekwowania działa automatycznie bez pisania nowego kodu.
-- 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'},
},
}
Przebieg aplikacji i egzaminu
Proces składania wniosku powinien przypominać prawdziwą wizytę w urzędzie. Gracze odwiedzają placówkę DMV, pocztę lub odpowiedni budynek rządowy i wchodzą w interakcję z NPC lub kioskiem, aby rozpocząć procedurę. NUI prezentuje dostępne rodzaje licencji, ich koszty oraz wymagania. Po wybraniu licencji i uiszczeniu opłaty system sprawdza warunki wstępne. Jeśli licencja wymaga egzaminu pisemnego, gracz przechodzi do interfejsu testu z pytaniami wielokrotnego wyboru pobieranymi z konfigurowalnej puli pytań. Losuj kolejność pytań i układ odpowiedzi, aby zapobiec ściąganiu z pamięci. Wymagaj minimalnego wyniku zaliczającego, zazwyczaj od 70 do 80 procent. W przypadku egzaminów praktycznych, takich jak testy na prawo jazdy, zespawnuj pojazd i utwórz trasę z punktami kontrolnymi, którą gracz musi pokonać w wyznaczonym czasie, przestrzegając przepisów ruchu drogowego. Egzaminatorem może być NPC instruktor jazdy lub prawdziwy gracz pracujący w DMV, który może oblać kursantów przejeżdżających na czerwonym świetle, znacznie przekraczających prędkość lub powodujących kolizje. Zapisz wynik egzaminu i po zaliczeniu wydaj licencję z okresem ważności liczonym od bieżącego znacznika czasu.
-- 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)
Integracja ze służbami porządkowymi
Prawdziwa siła systemu licencji ujawnia się wtedy, gdy służby porządkowe mogą sprawdzać i modyfikować statusy licencji podczas interakcji roleplay. Funkcjonariusze policji potrzebują możliwości sprawdzania statusu licencji obywatela podczas kontroli drogowych, weryfikowania pozwoleń na broń podczas przeszukań oraz zawieszania lub odbierania licencji w konsekwencji działań przestępczych. Zbuduj integrację z policyjnym MDT, która wyświetla wszystkie licencje posiadane przez obywatela wraz z ich statusem, datą wydania i datą ważności. Dodaj komendy lub akcje MDT do zawieszania licencji z podaniem powodu i czasu trwania, trwałego jej odbierania oraz przywracania po upływie okresu zawieszenia. Gdy licencja zostanie zawieszona, jej posiadacz powinien otrzymać powiadomienie wyjaśniające powód i czas trwania zawieszenia. Zintegruj kontrole licencji z istniejącymi procedurami służb. Jeśli gracz zostanie przyłapany na jeździe bez ważnego prawa jazdy, system policyjny może automatycznie to oznaczyć podczas kontroli drogowej. Jeśli ktoś zostanie przyłapany na polowaniu bez pozwolenia, strażnicy leśni mogą wystawić mandaty wiążące się z grzywnami i potencjalnym odebraniem licencji.
-- 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)
System odnawiania i obsługa wygasania
Licencje z datą ważności generują cykliczne przychody i regularne interakcje z NPC, dzięki czemu świat gry sprawia wrażenie żywego. Gdy licencja zbliża się do terminu wygaśnięcia, wyślij posiadaczowi powiadomienie przez system telefonu lub system poczty opisany w osobnym poradniku. Zapewnij okres karencji wynoszący kilka dni w grze, w którym licencja formalnie wygasła, ale gracz nie jest od razu karany, co daje czas na jej odnowienie. Odnowienie powinno być prostsze niż pierwszy wniosek: wymaga jedynie opłaty i nie wymaga ponownego egzaminu, chyba że licencja została wcześniej zawieszona lub cofnięta. Wdróż zadanie cykliczne po stronie serwera (scheduled task), które okresowo sprawdza wygasłe licencje i aktualizuje ich status. W przypadku licencji wymagających regularnego odnawiania, takich jak pozwolenia na wędkowanie o krótkim okresie ważności, rozważ opcję odnowienia zbiorczego, w której gracze mogą zapłacić za kilka okresów z góry z niewielkim rabatem. Nagradza to zaangażowanych graczy, którzy planują z wyprzedzeniem, zachowując jednocześnie cykl odnawiania, który nadaje systemowi dynamiki.
Egzekwowanie ograniczeń pomiędzy skryptami
System licencji ma znaczenie tylko wtedy, gdy inne skrypty faktycznie egzekwują ograniczenia. Utwórz scentralizowaną funkcję export, którą dowolny skrypt może wywołać, aby sprawdzić, czy gracz posiada ważną licencję danego typu. Twój skrypt pojazdów sprawdza prawo jazdy przed zezwoleniem na uruchomienie silnika lub gdy gracz wsiada na miejsce kierowcy. Twój skrypt broni sprawdza pozwolenie na broń, gdy gracz wyposaża broń palną. Twój skrypt myślistwa sprawdza licencję łowiecką przed zezwoleniem na oprawianie zwierząt. Twój skrypt wędkarstwa sprawdza kartę wędkarską przed zezwoleniem na połów. Każdy skrypt decyduje o własnych konsekwencjach za naruszenia. Niektóre mogą po prostu uniemożliwić akcję, wyświetlając powiadomienie. Inne mogą zezwolić na działanie, ale oznaczyć gracza dla organów ścigania, uruchamiając automatyczny poziom poszukiwania lub alert. Poziom rygoru zależy od filozofii roleplayu na twoim serwerze. System licencji stanowi warstwę danych, a skrypty egzekwujące decydują o konsekwencjach. Ta separacja oznacza, że możesz dostosować rygor egzekwowania bez modyfikowania samego systemu licencji.
Schemat bazy danych i narzędzia administracyjne
Schemat bazy danych wymaga tabel dla aktywnych licencji (definicje licencji znajdują się już w configu), wniosków, wyników egzaminów oraz dziennika audytu. Dziennik audytu ma kluczowe znaczenie dla rozliczalności: każde wydanie, zawieszenie, odebranie i przywrócenie licencji jest zapisywane ze znacznikiem czasu, identyfikatorem funkcjonariusza oraz powodem. Komendy administracyjne powinny umożliwiać zespołowi bezpośrednie wydawanie dowolnej licencji na potrzeby eventów, znoszenie wszystkich zawieszeń podczas amnestii, masowe unieważnianie licencji określonego typu przy resetach systemu oraz przeglądanie pełnej historii licencji dowolnego gracza. Zbuduj panel administracyjny w dashboardzie webowym serwera lub MDT, który zapewnia szybki wgląd w statystyki dotyczące aktywnych licencji według typu, oczekujących wniosków, ostatnich zawieszeń i zbliżających się wygaśnięć. Dane te pomogą ci zrozumieć, jak gracze korzystają z systemu oraz czy opłaty i okresy ważności wymagają dostosowania w celu utrzymania dobrej aktywności.
-- 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)
);