← All posts FiveM

Cara Membuat Sistem Job Custom di QBCore (dengan Contoh Kode)

Tutorial membuat job custom di QBCore dari nol: mendaftarkan job dan grade, toggle duty, lokasi kerja, pembayaran yang divalidasi server, sampai cara mencegah exploit uang.

Job adalah tulang punggung server roleplay. Dari tukang ojek sampai barista, setiap pekerjaan memberi pemain alasan untuk berinteraksi, menghasilkan uang, dan membangun cerita karakternya. QBCore sudah membawa beberapa job bawaan, tapi cepat atau lambat kamu ingin membuat job khas kotamu sendiri.

Di tutorial ini kita membuat job barista sederhana di QBCore: mendaftarkan job dan grade, toggle duty, lokasi kerja, dan pembayaran yang aman dari exploit. Polanya bisa kamu pakai untuk job apa pun.

Prasyarat: server QBCore yang sudah berjalan (lihat cara install QBCore dengan txAdmin) dan pemahaman dasar Lua (lihat belajar Lua FiveM dari nol). Referensi API QBCore ada di docs.qbcore.org.

Langkah 1: Daftarkan job di qb-core

Definisi job ada di qb-core/shared/jobs.lua. Tambahkan entri baru di dalam tabel QBShared.Jobs:

['barista'] = {
    label = 'Barista',
    defaultDuty = false,
    offDutyPay = false,
    grades = {
        ['0'] = { name = 'Trainee', payment = 50 },
        ['1'] = { name = 'Barista', payment = 75 },
        ['2'] = { name = 'Head Barista', payment = 100 },
        ['3'] = { name = 'Manager', isboss = true, payment = 125 },
    },
},

Penjelasan singkat:

  • Key job ('barista') adalah nama internal. Gunakan huruf kecil tanpa spasi, karena nama ini dipakai di kode dan database.
  • defaultDuty menentukan apakah pemain langsung on duty saat login.
  • offDutyPay menentukan apakah gaji tetap dibayar saat off duty. Untuk job sipil, biasanya false.
  • grades berisi tingkatan jabatan. Key grade ditulis sebagai string angka sesuai format bawaan QBCore. isboss = true menandai grade yang bisa mengelola karyawan di resource manajemen bisnis.
  • payment adalah gaji per siklus paycheck. Interval dan sumber dananya diatur di config qb-core (cek bagian money di qb-core/config.lua versi kamu).

Format jobs.lua dapat sedikit berbeda antar versi QBCore. Selalu salin struktur dari job bawaan yang ada di file kamu sendiri, misalnya job mechanic, lalu sesuaikan.

Setelah menyimpan, restart server penuh. Kemudian tes dengan perintah admin:

/setjob [id pemain] barista 0

Langkah 2: Buat resource job

resources/[jobs]/noctis-barista/
├── fxmanifest.lua
├── config.lua
├── client.lua
└── server.lua
-- fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
lua54 'yes'

shared_script 'config.lua'
client_script 'client.lua'
server_script 'server.lua'

dependency 'qb-core'
-- config.lua
Config = {}

Config.Job = 'barista'
Config.DutyPoint = vector3(-634.5, 227.4, 81.9)
Config.BrewPoint = vector3(-635.6, 233.1, 81.9)
Config.BrewTime = 5000        -- ms
Config.Cooldown = 15          -- detik antar seduhan per pemain
Config.PayPerCup = 35
Config.MaxDistance = 3.0

Koordinat di atas hanya contoh. Ambil koordinat lokasi kafe di map kamu sendiri, misalnya dengan menu admin atau print GetEntityCoords(PlayerPedId()).

Langkah 3: Client, toggle duty dan aksi kerja

-- client.lua
local QBCore = exports['qb-core']:GetCoreObject()
local PlayerData = {}

AddEventHandler('QBCore:Client:OnPlayerLoaded', function()
    PlayerData = QBCore.Functions.GetPlayerData()
end)

RegisterNetEvent('QBCore:Client:OnJobUpdate', function(job)
    PlayerData.job = job
end)

RegisterNetEvent('QBCore:Client:SetDuty', function(duty)
    if PlayerData.job then PlayerData.job.onduty = duty end
end)

-- Saat resource di-restart ketika pemain sudah login
AddEventHandler('onResourceStart', function(name)
    if name == GetCurrentResourceName() then
        PlayerData = QBCore.Functions.GetPlayerData()
    end
end)

local function isBarista()
    return PlayerData.job and PlayerData.job.name == Config.Job
end

CreateThread(function()
    while true do
        local sleep = 1000
        if isBarista() then
            local pos = GetEntityCoords(PlayerPedId())

            if #(pos - Config.DutyPoint) < 1.5 then
                sleep = 0
                QBCore.Functions.DrawText3D(Config.DutyPoint.x, Config.DutyPoint.y, Config.DutyPoint.z,
                    '[E] Toggle duty')
                if IsControlJustReleased(0, 38) then
                    TriggerServerEvent('QBCore:ToggleDuty')
                end
            elseif PlayerData.job.onduty and #(pos - Config.BrewPoint) < 1.5 then
                sleep = 0
                QBCore.Functions.DrawText3D(Config.BrewPoint.x, Config.BrewPoint.y, Config.BrewPoint.z,
                    '[E] Seduh kopi')
                if IsControlJustReleased(0, 38) then
                    QBCore.Functions.Progressbar('brew_coffee', 'Menyeduh kopi...', Config.BrewTime,
                        false, true, { disableMovement = true }, {}, {}, {}, function()
                            TriggerServerEvent('noctis-barista:server:brew')
                        end)
                end
            end
        end
        Wait(sleep)
    end
end)

Perhatikan pola sleep: loop hanya berjalan setiap frame (Wait(0)) ketika pemain dekat titik kerja. Di luar itu loop tidur 1 detik. Ini perbedaan besar untuk performa client. Loop Wait(0) tanpa syarat adalah salah satu penyebab paling umum resource terlihat berat di resmon.

Fungsi DrawText3D dan Progressbar adalah helper QBCore. Ketersediaan dan signature-nya bisa berbeda antar versi. Banyak server kini memakai qb-target atau ox_target untuk interaksi dan ox_lib untuk progress bar, lihat cara install ox_lib di QBCore.

Langkah 4: Server, validasi lalu bayar

Ini bagian terpenting. Event noctis-barista:server:brew bisa dipicu siapa saja yang memakai executor. Server harus memeriksa semuanya sendiri:

-- server.lua
local QBCore = exports['qb-core']:GetCoreObject()
local lastBrew = {}

RegisterNetEvent('noctis-barista:server:brew', function()
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    local job = Player.PlayerData.job
    -- 1. Job dan status duty
    if job.name ~= Config.Job or not job.onduty then return end

    -- 2. Jarak dari titik kerja, dihitung dari posisi server
    local pos = GetEntityCoords(GetPlayerPed(src))
    if #(pos - Config.BrewPoint) > Config.MaxDistance then
        print(('[barista] %s memicu brew dari jarak jauh'):format(GetPlayerName(src)))
        return
    end

    -- 3. Cooldown per pemain
    local now = os.time()
    if lastBrew[src] and now - lastBrew[src] < Config.Cooldown then return end
    lastBrew[src] = now

    -- 4. Bayar dengan nilai dari config server, bukan dari client
    Player.Functions.AddMoney('cash', Config.PayPerCup, 'barista-brew')
    TriggerClientEvent('QBCore:Notify', src, ('Kamu mendapat $%d'):format(Config.PayPerCup), 'success')
end)

AddEventHandler('playerDropped', function()
    lastBrew[source] = nil
end)

Empat pemeriksaan di atas (job, duty, jarak, cooldown) adalah standar minimum. Prinsip dasarnya: client hanya meminta, server yang memutuskan. Jangan pernah menerima jumlah uang, nama item, atau harga sebagai argumen event dari client.

Cooldown 15 detik dan bayaran $35 di contoh ini bukan angka rekomendasi. Sesuaikan dengan ekonomi servermu, karena payout job harus seimbang dengan harga barang, kendaraan, dan properti.

Langkah 5: Tambahkan bahan baku (opsional)

Job terasa lebih hidup kalau ada rantai produksi, misalnya biji kopi dibeli di toko lalu diseduh menjadi minuman yang bisa dijual. Dengan qb-inventory bawaan:

local item = Player.Functions.GetItemByName('biji_kopi')
if not item or item.amount < 1 then
    return TriggerClientEvent('QBCore:Notify', src, 'Butuh biji kopi', 'error')
end
Player.Functions.RemoveItem('biji_kopi', 1)
Player.Functions.AddItem('kopi_susu', 1)

Item biji_kopi dan kopi_susu harus didefinisikan dulu di qb-core/shared/items.lua. Kalau server kamu memakai ox_inventory, pakai export-nya seperti di artikel install ox_inventory di QBCore.

Langkah 6: Daftarkan dan uji

ensure qb-core
# ...
ensure noctis-barista

Checklist uji:

  1. Pemain tanpa job barista tidak melihat prompt sama sekali.
  2. Toggle duty berfungsi dan status tetap benar setelah restart noctis-barista.
  3. Off duty tidak bisa menyeduh.
  4. Memicu event dari jarak jauh atau berulang cepat ditolak server (uji dengan console client TriggerServerEvent di server development).
  5. Uang bertambah sesuai config dan tercatat di log.

Kesalahan umum

  • Job tidak dikenali saat /setjob: salah ketik key job, atau server belum di-restart penuh setelah mengubah shared/jobs.lua.
  • PlayerData.job nil: kamu membaca data sebelum pemain login, atau lupa menangani onResourceStart.
  • Uang tidak masuk: tipe akun salah. QBCore memakai 'cash' dan 'bank'.
  • Resmon tinggi: loop Wait(0) tanpa jeda. Ukur dengan resmon dan profiler.

Mengembangkan lebih jauh

  • Simpan status job seperti "kafe buka" di GlobalState agar semua pemain tahu kafe sedang beroperasi.
  • Buat menu manajemen karyawan dengan NUI React.
  • Tambahkan whitelist dan pelatihan untuk job seperti polisi dan EMS. Dari sisi pemain, gambaran perannya ada di cara jadi polisi, EMS, atau mechanic.

Butuh tempat menjalankan kota kamu?

Noctis Roleplay sendiri dibangun di atas QBCore yang kami kembangkan in-house, dan berjalan di infrastruktur yang sama dengan yang kami sewakan. Kalau kamu sedang membangun server sendiri, lihat hosting FiveM Noctis: vDedicated AMD Ryzen, DDR5, NVMe, root penuh, dan anti-DDoS di Singapore. Daftar paket lengkap ada di Noctis Cloud.

start here

Building something with Noctis?

Tell us what you need. A real person on our team reads every message.