Bloga Geri Dön
Tutorial9 dk. okuma

FiveM Avlanma Lisansı Sistemi: İzinler, Cezalar ve Bölgeler

FiveM'e çalışan bir avlanma ruhsatı sistemi ekleyin. Gerçekçi yaban hayatı kurallarını scriptlemek için izinler, korunan bölgeler, para cezaları, korucu meslekleri ve en iyi scriptler.

Agency Scripts

Agency Scripts Kurucusu ve Baş Geliştiricisi

Rol Yapma Altyapısı Olarak Lisanslar

Lisans ve ruhsat sistemi, her ciddi roleplay sunucusu için temel bir altyapıdır. Bu olmadan, her oyuncu ilk günden itibaren her şeyi yapabilir. Lisanslar aktiviteleri gerçekçi gereksinimlerin ardına bağlayarak ilerleme, sonuçlar ve roleplay etkileşimleri yaratır. Sürücü belgesi, oyuncuların yasal olarak araç kullanmadan önce bir sınavı geçmeleri gerektiği anlamına gelir. Silah ruhsatı, geçmiş kontrolleri ve eğitim gerektirir. Avcılık lisansı, kimin nerede avlanabileceğini sınırlandırır. İşletme ruhsatı, kimin ticari işletme işletebileceğini kontrol eder. Balıkçılık izni, pilot lisansı, tıp lisansı, avukatlık lisansı: Her biri gerçek dünya sistemlerini yansıtan ve vatandaşlar, kamu çalışanları ile kolluk kuvvetleri arasında organik etkileşimler üreten bir düzenleme katmanı oluşturur. Bir polis memuru birini kenara çekip lisans durumunu kontrol ettiğinde, bu yalnızca lisans sistemi bunu mümkün kıldığı için var olan bir roleplay anıdır. Bu rehber, sunucunuzun ihtiyaç duyduğu her türlü izni destekleyen esnek, genişletilebilir bir lisans altyapısı oluşturma sürecini ele almaktadır.

Esnek Lisans Türü Yapılandırması

Belirli lisans türlerini doğrudan koda yazmak yerine, yeni bir lisans türü eklemenin yalnızca bir config tablosuna yeni bir girdi eklemeyi gerektirdiği yapılandırma odaklı bir sistem kurun. Her lisans türü tanımı: benzersiz bir tanımlayıcı, görünen ad, açıklama, başvuru ücreti, sınav gerekip gerekmediği, varsa sınav soruları, yenileme gerekene kadarki geçerlilik süresi, hangi meslek veya rollerin bunu verebileceği ve lisans eksik olduğunda hangi oynanış kısıtlamalarının geçerli olacağını içerir. Bu yaklaşım: avcılık lisansınızın, sürücü belgenizin, silah ruhsatınızın ve işletme ruhsatınızın aynı altyapı üzerinde çalışması anlamına gelir. Sunucunuz büyüdüğünde ve taksi plakası veya inşaat izni gibi yeni bir ruhsat türü gerektiğinde, bir config girdisi eklersiniz ve tüm başvuru, düzenleme ve yaptırım süreci yeni kod yazmaya gerek kalmadan otomatik olarak çalışır.

-- 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'},
    },
}

Başvuru ve Sınav Akışı

Başvuru süreci gerçek bir devlet dairesi etkileşimi gibi hissettirmelidir. Oyuncular başvuruyu başlatmak için bir DMV konumunu, postaneyi veya ilgili resmi binayı ziyaret eder ve bir NPC veya kiosk ile etkileşime girer. NUI mevcut lisans türlerini, maliyetlerini ve gereksinimlerini sunar. Bir lisans seçip ücreti ödedikten sonra sistem ön koşulları kontrol eder. Lisans yazılı bir sınav gerektiriyorsa, oyuncu yapılandırılabilir bir soru havuzundan çekilen çoktan seçmeli soruların bulunduğu bir test arayüzüne girer. Ezberleyerek hile yapılmasını önlemek için soru sırasını ve cevap konumlarını rastgele hale getirin. Genellikle yüzde 70 ila 80 arasında bir minimum geçme puanı zorunlu tutun. Sürüş testleri gibi pratik sınavlar için bir araç spawn edin ve oyuncunun trafik kurallarına uyarak belirli bir süre içinde tamamlaması gereken bir checkpoint rotası oluşturun. Bir direksiyon eğitmeni NPC'si veya DMV mesleğinde çalışan gerçek bir oyuncu sınava gözetmenlik yapabilir; kırmızı ışıkta geçen, aşırı hız yapan veya kaza yapan öğrencileri bırakabilir. Sınav sonucunu kaydedin ve geçmesi durumunda geçerlilik süresi mevcut zaman damgasından başlayacak şekilde lisansı teslim edin.

-- 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)

Kolluk Kuvvetleri Entegrasyonu

Bir lisans sisteminin gerçek gücü, kolluk kuvvetlerinin rol yapma etkileşimleri sırasında lisans durumlarını sorgulayabilmesi ve değiştirebilmesiyle ortaya çıkar. Polis memurları, trafik kontrolleri sırasında bir vatandaşın lisans durumunu kontrol etme, aramalar sırasında silah ruhsatlarını doğrulama ve suç teşkil eden davranışların sonucu olarak lisansları askıya alma veya iptal etme yeteneğine ihtiyaç duyar. Bir vatandaşın sahip olduğu tüm lisansları durumları, veriliş tarihleri ve son kullanma tarihleriyle birlikte gösteren bir polis MDT entegrasyonu oluşturun. Bir lisansı sebep ve süre belirterek askıya almak, kalıcı olarak iptal etmek ve askıya alma süresinden sonra yeniden etkinleştirmek için komutlar veya MDT eylemleri ekleyin. Bir lisans askıya alındığında, lisans sahibi askıya alma nedenini ve süresini açıklayan bir bildirim almalıdır. Lisans kontrollerini mevcut kolluk kuvveti iş akışlarına entegre edin. Bir oyuncu geçerli bir lisans olmadan araç kullanırken yakalanırsa, polis sistemi bunu bir trafik çevirmesinde otomatik olarak işaretleyebilir. Birisi izinsiz avlanırken yakalanırsa, korucular para cezası ve olası lisans iptali içeren cezalar kesebilir.

-- 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)

Yenileme Sistemi ve Süre Aşımı Yönetimi

Son kullanma tarihi olan lisanslar, mükerrer gelir ve dünyanın canlı hissettirmesini sağlayan düzenli NPC etkileşimleri oluşturur. Bir lisansın süresi dolmak üzereyken, sahibine telefon sistemi veya ayrı bir eğitimde ele alınan posta sistemi üzerinden bildirim gönderin. Lisansın teknik olarak süresinin dolduğu ancak oyuncunun hemen cezalandırılmadığı birkaç oyun içi günlük bir ek süre tanıyarak yenileme için zaman verin. Yenileme işlemi ilk başvurudan daha basit olmalı, lisans daha önce askıya alınmadıkça veya iptal edilmedikçe yalnızca ücret gerektirmeli ve yeniden sınav gerektirmemelidir. Süresi dolmuş lisansları kontrol etmek ve durumlarını güncellemek için periyodik olarak çalışan sunucu taraflı zamanlanmış bir görev uygulayın. Kısa geçerlilik süresine sahip balıkçılık izinleri gibi periyodik yenileme gerektiren lisanslar için oyuncuların küçük bir indirimle peşin olarak birden fazla dönem için ödeme yapabileceği toplu yenileme seçenekleri sunmayı değerlendirin. Bu, sistemi dinamik tutan yenileme döngüsünü korurken önceden plan yapan sadık oyuncuları ödüllendirir.

Script'ler Arasında Kısıtlama Uygulama

Lisans sistemi, yalnızca diğer scriptler kısıtlamaları gerçekten uyguluyorsa önem taşır. Herhangi bir scriptin bir oyuncunun belirli bir türde geçerli bir lisansa sahip olup olmadığını kontrol etmek için çağırabileceği merkezi bir export fonksiyonu oluşturun. Araç scriptiniz, motorun çalıştırılmasına izin vermeden önce veya bir oyuncu sürücü koltuğuna oturduğunda sürücü belgesini kontrol eder. Silah scriptiniz, bir oyuncu ateşli silah kuşandığında silah ruhsatını kontrol eder. Avcılık scriptiniz, hayvan toplamaya izin vermeden önce avlanma lisansını kontrol eder. Balıkçılık scriptiniz, balık tutulmasına izin vermeden önce balıkçılık iznini kontrol eder. Her script ihlaller için kendi sonucuna kendisi karar verir. Bazıları bir bildirimle eylemi basitçe engelleyebilir. Diğerleri eyleme izin verebilir ancak oyuncuyu kolluk kuvvetlerinin dikkatine sunarak otomatik bir aranma seviyesi veya uyarı tetikleyebilir. Katılık düzeyi tamamen sunucunuzun roleplay felsefesine bağlıdır. Lisans sistemi veri katmanını sağlar, yaptırım scriptleri ise sonuçlara karar verir. Bu ayrım, lisans sisteminin kendisine dokunmadan yaptırım katılığını ayarlayabileceğiniz anlamına gelir.

Veri Tabanı Şeması ve Yönetici Araçları

Veritabanı şemasında lisans tanımları (zaten config içinde yer alır), aktif lisanslar, başvurular, sınav sonuçları ve bir denetim günlüğü (audit log) için tablolar bulunmalıdır. Denetim günlüğü hesap verebilirlik açısından kritik öneme sahiptir. Her lisans verilmesi, askıya alınması, iptali ve iadesi bir zaman damgası, işlemi gerçekleştiren yetkilinin tanımlayıcısı ve gerekçesiyle birlikte kaydedilir. Admin komutları, personelin etkinlik amacıyla herhangi bir lisansı doğrudan vermesine, genel af etkinliklerinde tüm askıya almaları kaldırmasına, sistem sıfırlamaları için belirli bir türdeki lisansların süresini topluca sonlandırmasına ve herhangi bir oyuncunun tam lisans geçmişini görüntülemesine olanak tanımalıdır. Sunucunuzun web panelinde veya MDT'sinde türe göre aktif lisanslar, bekleyen başvurular, son askıya almalar ve yaklaşan son kullanma tarihleri hakkında bir bakışta istatistik sağlayan bir admin paneli oluşturun. Bu veriler, oyuncuların sistemle nasıl etkileşime girdiğini ve sağlıklı katılımı sürdürmek için ücretlerin ve geçerlilik sürelerinin ayarlanması gerekip gerekmediğini anlamanıza yardımcı olur.

-- 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)
);

Başlamaya Hazır mısınız?

Scriptleri mağazamızdan edinin veya destek, güncellemeler ve gelecek yeniliklere göz atmak için Discord'a katılın.