ब्लॉग पर वापस
Guide9 मिनट पढ़ें

FiveM इन्वेंटरी प्रबंधन: सर्वर मालिकों के लिए प्रो टिप्स

अपने FiveM इन्वेंटरी सेटअप को स्तर बढ़ाएं। वजन कॉन्फ़िग, स्टैक आकार, आइटम छवियां, स्टैश डिज़ाइन और शीर्ष रोलप्ले सर्वरों द्वारा उपयोग किए गए ट्यूनिंग टिप्स।

Agency Scripts

Agency Scripts के संस्थापक और प्रमुख डेवलपर

इन्वेंटरी आर्किटेक्चर क्यों महत्वपूर्ण है

इन्वेंटरी सिस्टम किसी भी FiveM रोलप्ले सर्वर की रीढ़ है। आइटम के साथ हर खिलाड़ी इंटरैक्शन, हथियार उठाने से लेकर किसी को चाबी देने तक, इन्वेंटरी के माध्यम से होता है। एक खराब डिज़ाइन की गई इन्वेंटरी आइटम डुप्लीकेशन एक्सप्लॉइट्स, डिसिंक समस्याएं, और निराश खिलाड़ी पैदा करती है जो अपना सामान खो देते हैं। सबसे मजबूत इन्वेंटरी सिस्टम सर्वर-प्राधिकृत मॉडल का पालन करते हैं जहां क्लाइंट केवल वही दिखाता है जो सर्वर बताता है, कभी भी क्लाइंट पर अपनी स्थिति रिपोर्ट करने का भरोसा नहीं करता। यह वास्तुशिल्प निर्णय अकेले उन अधिकांश डुप्लीकेशन एक्सप्लॉइट्स को समाप्त कर देता है जो क्लाइंट-ट्रस्टेड इन्वेंटरी वाले सर्वरों को प्रभावित करते हैं। अपनी इन्वेंटरी की योजना बनाते समय, पहले डेटा प्रवाह के बारे में सोचें: सर्वर सत्य है, क्लाइंट इसे रेंडर करता है, और हर परिवर्तन एक सत्यापित सर्वर इवेंट के माध्यम से जाता है।

वजन बनाम स्लॉट-आधारित सिस्टम

वज़न-आधारित और स्लॉट-आधारित इन्वेंट्री सिस्टम के बीच चयन खिलाड़ी के अनुभव को मौलिक रूप से आकार देता है। स्लॉट-आधारित सिस्टम में, प्रत्येक स्लॉट एक आइटम प्रकार रखता है जिसमें अधिकतम स्टैक आकार होता है, और कुल स्लॉट की संख्या वहन क्षमता को परिभाषित करती है। वज़न-आधारित सिस्टम में, प्रत्येक आइटम का एक वज़न मान होता है, और खिलाड़ी के पास अधिकतम वहन वज़न होता है। कई आधुनिक फ्रेमवर्क दोनों दृष्टिकोणों को मिलाते हैं, संगठन के लिए स्लॉट्स का उपयोग करते हुए लेकिन कुल क्षमता को वज़न द्वारा सीमित करते हैं। यहाँ एक संयुक्त वज़न-और-स्लॉट आइटम परिभाषा का उदाहरण है:

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

यह weight फ़ील्ड को सटीकता के लिए ग्राम में संग्रहीत किया जाता है, और stackSize नियंत्रित करता है कि उस आइटम में एक स्लॉट में कितने फिट होते हैं। जब कोई खिलाड़ी आइटम उठाने का प्रयास करता है, तो यह सत्यापित करें कि स्लॉट उपलब्ध है और कुल वजन अधिकतम से अधिक नहीं होगा। यह दोहरी सत्यापन खिलाड़ियों को भारी आइटमों की अवास्तविक मात्रा ले जाने से रोकता है भले ही उनके पास खाली स्लॉट हों।

सर्वर-साइड सत्यापन और एंटी-एक्सप्लॉइट

हर इन्वेंटरी क्रिया को प्रभावी होने से पहले सर्वर पर मान्य किया जाना चाहिए। जब कोई खिलाड़ी आइटम को स्लॉट 3 से स्लॉट 7 में खींचता है, तो क्लाइंट एक मूव अनुरोध भेजता है, और सर्वर सत्यापित करता है कि स्रोत स्लॉट में वास्तव में वह आइटम है, गंतव्य स्लॉट इसे स्वीकार कर सकता है, और मात्राएँ संगत हैं। कभी भी क्लाइंट को आइटम की गिनती निर्दिष्ट करने या बिना किसी वस्तु के आइटम बनाने की अनुमति न दें। यहाँ एक सुरक्षित सर्वर-साइड मूव हैंडलर है:

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)

ध्यान दें DropPlayer स्पष्ट रूप से असंभव संचालन के लिए कॉल। इन घटनाओं को एक अलग ऑडिट तालिका में लॉग करना आपको शोषण प्रयासों और पैटर्न की पहचान करने में मदद करता है। इन्वेंटरी घटनाओं पर दर सीमित करने पर विचार करें, क्योंकि वैध खिलाड़ी आमतौर पर प्रति सेकंड कुछ ही इन्वेंटरी ऑपरेशन करते हैं, जबकि स्वचालित शोषण अक्सर तेजी से सैकड़ों अनुरोध भेजते हैं।

आइटम मेटाडेटा और यूनिक आइटम्स

मेटाडेटा सरल वस्तुओं को समृद्ध, अद्वितीय वस्तुओं में बदल देता है। एक हथियार अपने सीरियल नंबर, टिकाऊपन, और संलग्न संशोधनों को रख सकता है। एक फोन अपना असाइन किया गया नंबर और संपर्क सूची संदर्भ संग्रहित कर सकता है। खाद्य वस्तुओं में समाप्ति समय हो सकता है। मेटाडेटा को Lua टेबल के रूप में संग्रहित किया जाता है जो JSON में डेटाबेस में सीरियलाइज़ होता है, प्रत्येक वस्तु उदाहरण से जुड़ा होता है। मुख्य अंतर स्टैक करने योग्य वस्तुओं के बीच होता है, जो समान मेटाडेटा साझा करती हैं, और अद्वितीय वस्तुओं के बीच, जिन्हें अपनी खुद की मेटाडेटा टेबल मिलती है। यहाँ बताया गया है कि पूर्ण मेटाडेटा के साथ हथियार कैसे बनाया जाए:

-- 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 में मेटाडेटा के साथ आइटम दिखाते हैं, तो मेटाडेटा को आइटम जानकारी के साथ पास करें ताकि UI टिकाऊपन बार, सीरियल नंबर, और गुणवत्ता रेटिंग जैसी विवरण दिखा सके। यह खिलाड़ियों को उनके आइटम से गहरा जुड़ाव देता है और उन्नत रोलप्ले परिदृश्यों का समर्थन करता है जैसे हथियार पंजीकरण प्रणाली या फोरेंसिक जांच जो सीरियल नंबर को उसके मालिक तक ट्रेस करती है।

ड्रैग-एंड-ड्रॉप NUI प्रदर्शन

इन्वेंटरी UI सबसे प्रदर्शन-संवेदनशील NUI तत्वों में से एक है क्योंकि खिलाड़ी इसके साथ लगातार इंटरैक्ट करते हैं। हर अपडेट पर पूरी इन्वेंटरी ग्रिड को फिर से रेंडर करने से बचें। इसके बजाय, एक वर्चुअल DOM दृष्टिकोण या लक्षित तत्व अपडेट का उपयोग करें जो केवल बदले हुए स्लॉट को संशोधित करता है। जब कोई खिलाड़ी आइटम को खींचता है, तो ड्रैग को पूरी तरह से JavaScript में माउस इवेंट्स का उपयोग करके संभालें बजाय इसके कि ड्रैग के दौरान Lua क्लाइंट को स्थिति अपडेट भेजें। केवल अंतिम ड्रॉप परिणाम को एकल NUI कॉलबैक के रूप में भेजें। यहां एक प्रदर्शनकारी ड्रैग हैंडलर पैटर्न है:

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

दृश्य रेंडरिंग के लिए, स्लॉट लेआउट के लिए CSS Grid का उपयोग करें और इन्वेंटरी आइटम्स पर भारी CSS एनिमेशन से बचें क्योंकि खिलाड़ियों के पास एक साथ दर्जनों स्लॉट दिखाई दे सकते हैं। आइटम आइकन के लिए इमेज स्प्राइट्स व्यक्तिगत इमेज फ़ाइलों की तुलना में तेज़ लोड होते हैं और जब NUI पहली बार खुलता है तो HTTP अनुरोधों को कम करते हैं।

स्टैश और कंटेनर सिस्टम

व्यक्तिगत इन्वेंट्री से परे, खिलाड़ियों को बाहरी भंडारण जैसे वाहन ट्रंक, घर के स्टैश, और साझा संगठन भंडारण तक पहुंच की आवश्यकता होती है। प्रत्येक कंटेनर प्रकार के अपने क्षमता सीमाएँ और पहुंच नियंत्रण नियम होने चाहिए। वाहन ट्रंक वाहन प्लेट का उपयोग एक अद्वितीय पहचानकर्ता के रूप में करते हैं, घर के स्टैश संपत्ति आईडी का उपयोग करते हैं, और नौकरी के स्टैश नौकरी के नाम के साथ ग्रेड जांच का उपयोग करते हैं। कंटेनर इन्वेंट्री को खिलाड़ी इन्वेंट्री से अलग समर्पित डेटाबेस तालिका में संग्रहित करें ताकि क्वेरी कुशल बनी रहे:

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)

जब कोई खिलाड़ी कंटेनर खोलता है, तो इसके सामग्री डेटाबेस से लोड करें और इसे लॉक करें ताकि कई खिलाड़ी एक साथ एक्सेस न कर सकें। एक सर्वर-साइड लॉक तालिका का उपयोग करें जो ट्रैक करती है कि कौन से स्टैश आईडी वर्तमान में खुले हैं और किसके द्वारा। लॉक तब रिलीज़ करें जब खिलाड़ी कंटेनर बंद करता है या डिस्कनेक्ट होता है। यह क्लासिक डुप्लिकेशन एक्सप्लॉइट को रोकता है जहां दो खिलाड़ी एक ही ट्रंक खोलते हैं और दोनों एक ही आइटम निकाल लेते हैं।

इन्वेंटरी सिंक्रोनाइज़ेशन और स्थिरता

सर्वर मेमोरी, डेटाबेस, और क्लाइंट डिस्प्ले के बीच इन्वेंटरी डेटा को सिंक्रनाइज़ करना सावधानीपूर्वक समन्वय की मांग करता है। डेटाबेस पर हर आइटम परिवर्तन पर नहीं, बल्कि समय-समय पर खिलाड़ी की इन्वेंटरी को सेव करें ताकि डेटाबेस लिखने का लोड कम हो। अधिकांश सर्वरों के लिए 30 से 60 सेकंड का सेव इंटरवल अच्छा काम करता है। इसके अतिरिक्त, खिलाड़ी डिस्कनेक्ट और सर्वर शटडाउन पर हमेशा सेव करें, उपयोग करते हुए playerDropped एक इवेंट और एक शटडाउन हैंडलर। एक डर्टी फ्लैग सिस्टम लागू करें जो केवल तब डेटाबेस में लिखता है जब इन्वेंटरी वास्तव में पिछली बचत के बाद से बदली हो:

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)

क्लाइंट साइड के लिए, बैच NUI अपडेट करें ताकि कई तेज़ इन्वेंटरी परिवर्तन, जैसे कि क्राफ्टिंग ऑपरेशन से कई आइटम प्राप्त करना, एकल UI रिफ्रेश में परिणत हों न कि प्रति आइटम। इससे वह चमकदार प्रभाव समाप्त हो जाता है जो खिलाड़ी तब देखते हैं जब आइटम एक-एक करके उनकी इन्वेंटरी ग्रिड में प्रकट होते हैं।

प्रदर्शन निगरानी और अनुकूलन

अपने इन्वेंटरी सिस्टम के प्रदर्शन की निगरानी करें मुख्य मेट्रिक्स को ट्रैक करके: औसत डेटाबेस सहेजने का समय, इवेंट प्रोसेसिंग दर, और NUI रेंडर आवृत्ति। FiveM प्रोफाइलर का उपयोग करके अपनी इन्वेंटरी कॉलबैक्स में बाधाओं की पहचान करें। सामान्य प्रदर्शन जालों में शामिल हैं: निकटता-आधारित सुविधाओं जैसे ग्राउंड आइटम पिकअप के लिए हर फ्रेम में सभी खिलाड़ी इन्वेंटरी पर पुनरावृत्ति करना, बड़े मेटाडेटा ऑब्जेक्ट्स को अनावश्यक रूप से सीरियलाइज़ करना, और जब इन्वेंटरी बंद हो तो NUI फ्रेम्स को सक्रिय छोड़ना। ग्राउंड आइटम्स के लिए, एक स्थानिक ग्रिड सिस्टम का उपयोग करें जो केवल खिलाड़ी सेल के भीतर आइटमों की जांच करता है बजाय सर्वर पर हर छोड़े गए आइटम को स्कैन करने के। आइटम नाम द्वारा अनुक्रमित एक लुकअप तालिका में आइटम परिभाषाओं को कैश करें ताकि आइटम गुणों को प्राप्त करना O(1) हैश लुकअप हो न कि O(n) एरे स्कैन। अंत में, बहुत बड़ी क्षमता वाले कंटेनरों के लिए इन्वेंटरी पेजिनेशन लागू करने पर विचार करें, केवल दृश्य स्लॉट लोड करें और खिलाड़ी के स्क्रॉल करने पर अतिरिक्त पंक्तियाँ प्राप्त करें, जो डेटाबेस क्वेरियों और NUI रेंडरिंग दोनों को हल्का रखता है, यहां तक कि सैकड़ों स्लॉट वाले गोदामों के लिए भी।

शुरू करने के लिए तैयार?

हमारी शॉप से स्क्रिप्ट लें, या सपोर्ट, अपडेट और आगे आने वाली चीज़ों की झलक के लिए Discord से जुड़ें।