Bloga Geri Dön
Guide8 dk. okuma

FiveM Envanter Yönetimi: Sunucu Sahipleri İçin Profesyonel İpuçları

FiveM envanter kurulumunuzu üst seviyeye taşıyın. En iyi rol yapma sunucuları tarafından kullanılan ağırlık konfigürasyonları, istifleme boyutları, eşya görselleri, zula tasarımı ve ince ayar ipuçları.

Agency Scripts

Agency Scripts Kurucusu ve Baş Geliştiricisi

Envanter Mimarisi Neden Önemlidir?

Envanter sistemi, herhangi bir FiveM roleplay sunucusunun belkemiğidir. Bir silahı yerden almaktan birine anahtar vermeye kadar, oyuncunun eşyalarla olan her etkileşimi envanter üzerinden gerçekleşir. Kötü tasarlanmış bir envanter; eşya kopyalama (duplication) açıklarına, senkronizasyon sorunlarına ve eşyalarını kaybeden öfkeli oyunculara yol açar. En sağlam envanter sistemleri, istemcinin asla kendi durumunu bildirmesine güvenmeyip yalnızca sunucunun bildirdiğini görüntülediği sunucu yetkili (server-authoritative) bir modeli izler. Yalnızca bu mimari karar bile, istemciye güvenen envanterlere sahip sunucuların başına bela olan kopyalama açıklarının çoğunu ortadan kaldırır. Envanterinizi planlarken önce veri akışını düşünün: doğru veri sunucuya aittir, istemci bunu ekrana yansıtır ve her değişiklik doğrulanmış bir sunucu eventi üzerinden geçer.

Ağırlık ve Slot Tabanlı Sistemler

Ağırlık tabanlı ve slot tabanlı envanter sistemleri arasında seçim yapmak, oyuncu deneyimini temelden şekillendirir. Slot tabanlı bir sistemde her slot, maksimum yığın boyutuna sahip tek bir eşya türünü tutar ve toplam slot sayısı taşıma kapasitesini belirler. Ağırlık tabanlı bir sistemde her eşyanın bir ağırlık değeri vardır ve oyuncunun maksimum bir taşıma ağırlığı bulunur. Birçok modern framework, düzenleme için slotları kullanarak ancak toplam kapasiteyi ağırlıkla sınırlayarak her iki yaklaşımı birleştirir. İşte ağırlık ve slot özelliklerini birleştiren bir eşya tanımı örneği:

-- Shared item definitions (items.lua)
QBCore.Shared.Items = {
    ['water_bottle'] = {
        name = 'water_bottle',
        label = 'Water Bottle',
        weight = 500,        -- grams
        type = 'item',
        image = 'water_bottle.png',
        unique = false,
        useable = true,
        shouldClose = true,
        description = 'A refreshing bottle of water',
        stackSize = 10,      -- max per slot
    },
    ['lockpick'] = {
        name = 'lockpick',
        label = 'Lockpick',
        weight = 200,
        type = 'item',
        image = 'lockpick.png',
        unique = false,
        useable = true,
        shouldClose = true,
        description = 'Used to pick locks',
        stackSize = 5,
    },
}

The weight alanı hassasiyet için gram cinsinden saklanır ve stackSize bu eşyadan tek bir slota kaç adet sığacağını denetler. Bir oyuncu bir eşyayı almaya çalıştığında, hem boş bir slotun bulunduğunu hem de toplam ağırlığın maksimumu aşmayacağını doğrulayın. Bu çift doğrulama, oyuncuların boş slotları olsa bile gerçekçi olmayan miktarlarda ağır eşya taşımasını önler.

Sunucu Taraflı Doğrulama ve Anti-Exploit

Her envanter eylemi yürürlüğe girmeden önce server üzerinde doğrulanmalıdır. Bir oyuncu bir öğeyi 3. slottan 7. slota sürüklediğinde client bir taşıma isteği gönderir ve server; kaynak slotun gerçekten bu öğeyi içerdiğini, hedef slotun bunu kabul edebileceğini ve miktarların tutarlı olduğunu doğrular. Client'ın öğe sayılarını belirlemesine veya yoktan öğe oluşturmasına asla izin vermeyin. İşte güvenli bir server taraflı taşıma işleyicisi:

RegisterNetEvent('inventory:server:moveItem', function(fromSlot, toSlot, fromAmount)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    local fromItem = Player.PlayerData.items[fromSlot]
    if not fromItem then
        -- Source slot is empty, possible exploit attempt
        DropPlayer(src, 'Invalid inventory operation')
        return
    end

    if fromAmount > fromItem.amount or fromAmount < 1 then
        DropPlayer(src, 'Invalid inventory amount')
        return
    end

    local toItem = Player.PlayerData.items[toSlot]

    if toItem and toItem.name == fromItem.name and not fromItem.unique then
        -- Stack items together
        local maxStack = QBCore.Shared.Items[fromItem.name].stackSize or 50
        local canStack = maxStack - toItem.amount
        local moveAmount = math.min(fromAmount, canStack)

        if moveAmount > 0 then
            toItem.amount = toItem.amount + moveAmount
            fromItem.amount = fromItem.amount - moveAmount
            if fromItem.amount <= 0 then
                Player.PlayerData.items[fromSlot] = nil
            end
        end
    else
        -- Swap items between slots
        Player.PlayerData.items[toSlot] = fromItem
        Player.PlayerData.items[fromSlot] = toItem
    end

    Player.Functions.SetPlayerData('items', Player.PlayerData.items)
end)

Şuna dikkat edin: DropPlayer çağrıları tamamen imkansız işlemler içindir. Bu olayları ayrı bir denetim tablosuna kaydetmek, exploit girişimlerini ve kalıplarını tespit etmenize yardımcı olur. Envanter olaylarında rate limit uygulamayı da düşünün, çünkü meşru oyuncular saniyede nadiren birkaçtan fazla envanter işlemi yaparken, otomatik exploit'ler genellikle hızla yüzlerce istek gönderir.

İtem Metadatası ve Benzersiz İtemler

Metadata, basit eşyaları zengin ve benzersiz nesnelere dönüştürür. Bir silah: seri numarasını, dayanıklılığını ve takılı modifikasyonlarını taşıyabilir. Bir telefon: atanan numarasını ve kişi listesi referansını saklayabilir. Yiyecekler bir son kullanma tarihi zaman damgasına sahip olabilir. Metadata, veritabanında JSON olarak serileştirilmiş bir Lua tablosu şeklinde, her bir eşya örneğine eklenerek saklanır. Temel ayrım, aynı metadata'yı paylaşan istiflenebilir eşyalar ile her biri kendi metadata tablosunu alan benzersiz eşyalar arasındadır. İşte eksiksiz metadata'ya sahip bir silah oluşturma yöntemi:

-- Creating a weapon with metadata
function CreateWeaponItem(src, weaponName, serial)
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return false end

    local metadata = {
        serial = serial or GenerateSerial(),
        durability = 100.0,
        ammo = 0,
        attachments = {},
        registered = false,
        registeredTo = nil,
        quality = math.random(85, 100),
        created = os.time(),
    }

    return Player.Functions.AddItem(weaponName, 1, nil, metadata)
end

function GenerateSerial()
    local chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
    local serial = ''
    for i = 1, 10 do
        local idx = math.random(1, #chars)
        serial = serial .. chars:sub(idx, idx)
    end
    return serial
end

NUI içinde meta veriye sahip eşyaları görüntülerken, meta verileri eşya bilgileriyle birlikte iletin: böylece arayüz dayanıklılık çubukları, seri numaraları ve kalite derecelendirmeleri gibi ayrıntıları gösterebilir. Bu, oyuncuların eşyalarıyla daha derin bir bağ kurmasını sağlar ve silah kayıt sistemleri veya bir seri numarasını sahibine kadar takip eden adli tıp soruşturmaları gibi gelişmiş roleplay senaryolarını destekler.

Sürükle Bırak NUI Performansı

Envanter arayüzü, oyuncular sürekli etkileşime girdiği için performansa en duyarlı NUI ögelerinden biridir. Her güncellemede tüm envanter ızgarasını yeniden render etmekten kaçının. Bunun yerine, sanal bir DOM yaklaşımı veya yalnızca değişen yuvaları güncelleyen hedefli öge güncellemeleri kullanın. Bir oyuncu bir eşyayı sürüklediğinde, sürükleme sırasında Lua istemcisine konum güncellemeleri göndermek yerine sürükleme işlemini fare olaylarını kullanarak tamamen JavaScript içinde yönetin. Yalnızca son bırakma sonucunu tek bir NUI callback olarak gönderin. İşte yüksek performanslı bir sürükleme işleyici kalıbı:

// Inventory NUI - performant drag and drop
let draggedItem = null;
let dragElement = null;

document.addEventListener('mousedown', (e) => {
    const slot = e.target.closest('.inv-slot[data-has-item="true"]');
    if (!slot) return;

    draggedItem = {
        slot: parseInt(slot.dataset.slot),
        item: JSON.parse(slot.dataset.itemInfo),
    };

    dragElement = slot.cloneNode(true);
    dragElement.classList.add('dragging-ghost');
    dragElement.style.position = 'fixed';
    dragElement.style.pointerEvents = 'none';
    dragElement.style.zIndex = '9999';
    document.body.appendChild(dragElement);

    moveDragElement(e.clientX, e.clientY);
});

document.addEventListener('mousemove', (e) => {
    if (!dragElement) return;
    moveDragElement(e.clientX, e.clientY);
});

document.addEventListener('mouseup', (e) => {
    if (!draggedItem) return;

    const targetSlot = e.target.closest('.inv-slot');
    if (targetSlot) {
        const toSlot = parseInt(targetSlot.dataset.slot);
        // Send only the final result to Lua
        fetch(`https://${GetParentResourceName()}/moveItem`, {
            method: 'POST',
            body: JSON.stringify({
                fromSlot: draggedItem.slot,
                toSlot: toSlot,
                amount: draggedItem.item.amount,
            }),
        });
    }

    if (dragElement) dragElement.remove();
    draggedItem = null;
    dragElement = null;
});

Görsel işleme için slot düzeninde CSS Grid kullanın ve envanter eşyalarında ağır CSS animasyonlarından kaçının, çünkü oyuncuların aynı anda onlarca slotu görünür olabilir. Eşya simgeleri için resim sprite'ları, ayrı resim dosyalarından daha hızlı yüklenir ve NUI ilk açıldığında HTTP isteklerini azaltır.

Stash ve Konteyner Sistemleri

Kişisel envanterin ötesinde, oyuncuların araç bagajları, ev depoları ve paylaşılan organizasyon depoları gibi harici depolama alanlarına erişmesi gerekir. Her konteyner türünün kendi kapasite sınırları ve erişim kontrol kuralları olmalıdır. Araç bagajları benzersiz tanımlayıcı olarak araç plakasını, ev depoları mülk ID'lerini ve meslek depoları ise rütbe kontrolüyle birleştirilmiş meslek adını kullanır. Sorguları verimli tutmak için konteyner envanterlerini oyuncu envanterlerinden ayrı özel bir veritabanı tablosunda saklayın:

CREATE TABLE IF NOT EXISTS stash_items (
    id INT AUTO_INCREMENT PRIMARY KEY,
    stash_id VARCHAR(100) NOT NULL,
    slot INT NOT NULL,
    item_name VARCHAR(50) NOT NULL,
    amount INT DEFAULT 1,
    metadata LONGTEXT DEFAULT '{}',
    UNIQUE KEY unique_stash_slot (stash_id, slot),
    INDEX idx_stash_id (stash_id)
);

-- Example stash_id values:
-- 'trunk_ABC123'        (vehicle trunk by plate)
-- 'house_42'            (house stash by property id)
-- 'police_evidence_1'   (job stash with identifier)

Bir oyuncu bir depolama alanını açtığında, içeriğini veritabanından yükleyin ve birden fazla oyuncunun aynı anda erişmesini önlemek için kilitleyin. Hangi zula ID'lerinin şu anda açık olduğunu ve kim tarafından açıldığını takip eden sunucu taraflı bir kilit tablosu kullanın. Oyuncu depolama alanını kapattığında veya bağlantısı kesildiğinde kilidi kaldırın. Bu, iki oyuncunun aynı bagajı açıp aynı eşyaları çekmesiyle oluşan klasik kopyalama açığını önler.

Envanter Senkronizasyonu ve Kalıcılık

Envanter verilerini sunucu belleği, veritabanı ve istemci ekranı arasında senkronize tutmak dikkatli bir koordinasyon gerektirir. Veritabanı yazma yükünü azaltmak için oyuncu envanterini her bir eşya değişikliğinde değil, periyodik olarak veritabanına kaydedin. 30 ila 60 saniyelik bir kaydetme aralığı çoğu sunucu için iyi çalışır. Ayrıca, oyuncu bağlantıyı kestiğinde ve sunucu kapandığında her zaman şu yöntemi kullanarak kaydedin: playerDropped event ve bir shutdown handler. Envanterin son kayıttan bu yana gerçekten değiştiği durumlarda veritabanına yalnızca yazma yapan bir dirty flag sistemi uygulayın:

local inventoryDirty = {}

-- Mark inventory as needing save
function MarkDirty(citizenid)
    inventoryDirty[citizenid] = true
end

-- Periodic save loop
CreateThread(function()
    while true do
        Wait(30000) -- 30 seconds
        for citizenid, dirty in pairs(inventoryDirty) do
            if dirty then
                local Player = QBCore.Functions.GetPlayerByCitizenId(citizenid)
                if Player then
                    SaveInventoryToDatabase(citizenid, Player.PlayerData.items)
                end
                inventoryDirty[citizenid] = nil
            end
        end
    end
end)

AddEventHandler('playerDropped', function()
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if Player then
        local citizenid = Player.PlayerData.citizenid
        if inventoryDirty[citizenid] then
            SaveInventoryToDatabase(citizenid, Player.PlayerData.items)
            inventoryDirty[citizenid] = nil
        end
    end
end)

İstemci tarafı için NUI güncellemelerini toplu hale getirin, böylece bir üretim işleminden birkaç eşya alma gibi birden fazla hızlı envanter değişikliği eşya başına bir yenileme yerine tek bir arayüz yenilemesiyle sonuçlanır. Bu, oyuncuların envanter ızgaralarında eşyalar teker teker göründüğünde gördükleri titreme etkisini ortadan kaldırır.

Performans İzleme ve Optimizasyon

Envanter sistemi performansınızı temel metrikleri takip ederek izleyin: ortalama veritabanı kaydetme süresi, event işleme hızı ve NUI render sıklığı. Envanter callback'lerinizdeki darboğazları tespit etmek için FiveM profiler kullanın. Yaygın performans tuzakları arasında yerdeki eşyaları alma gibi mesafeye dayalı özellikler için her frame'de tüm oyuncu envanterlerini yinelemek, büyük metadata nesnelerini gereksiz yere serileştirmek ve envanter kapalıyken NUI frame'lerini aktif bırakmak yer alır. Yerdeki eşyalar için sunucudaki her bırakılan eşyayı taramak yerine yalnızca oyuncunun hücresindeki eşyaları kontrol eden bir mekansal grid sistemi kullanın. Eşya özelliklerini almanın O(n) dizi taraması yerine O(1) hash araması olması için eşya tanımlarını eşya adına göre indekslenmiş bir arama tablosunda önbelleğe alın. Son olarak, yüzlerce slota sahip depolar için bile hem veritabanı sorgularını hem de NUI render işlemini hafif tutan, yalnızca görünür slotları yükleyen ve oyuncu kaydırdıkça ek satırları getiren çok büyük kapasiteli konteynerler için envanter sayfalaması uygulamayı düşünün.

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.