← All posts FiveM

Panduan State Bags FiveM untuk Sinkronisasi Entity

Pelajari state bags FiveM: cara menyimpan dan menyinkronkan data pada player, entity, dan global state, mendengarkan perubahan dengan change handler, serta aturan keamanan agar tidak bisa disalahgunakan client.

Bayangkan kamu membuat sistem kendaraan dengan bahan bakar. Server tahu sisa bensin setiap mobil, tapi semua pemain di sekitar mobil itu juga perlu tahu, misalnya untuk menampilkan indikator di HUD atau mencegah mesin menyala saat tangki kosong. Cara lama adalah mengirim event bolak-balik ke setiap pemain. Cara yang lebih rapi di FiveM modern adalah state bags.

State bags adalah penyimpanan key-value yang melekat pada sebuah objek (entity, player, atau global) dan bisa direplikasi otomatis dari server ke client. Panduan ini membahas cara memakainya, cara mendengarkan perubahan, dan yang paling penting: cara memakainya dengan aman.

Dokumentasi resminya ada di docs.fivem.net. Artikel ini melengkapinya dengan contoh praktis dan pola yang umum dipakai di server roleplay.

Prasyarat: OneSync

State bags untuk entity dan player membutuhkan OneSync. Hampir semua server roleplay modern sudah menjalankannya, tapi pastikan server.cfg kamu berisi:

set onesync on

Atau setara melalui pengaturan txAdmin. Entity yang ingin diberi state juga harus merupakan networked entity, yaitu entity yang disinkronkan lewat jaringan. Objek lokal yang dibuat hanya di satu client tidak punya state bag yang tersinkron.

Tiga jenis state bag

JenisAkses di serverAkses di clientContoh penggunaan
GlobalGlobalStateGlobalStateCuaca, status event kota, jumlah polisi on duty
PlayerPlayer(source).stateLocalPlayer.state (diri sendiri)Status duty, sedang diborgol, status pingsan
EntityEntity(entity).stateEntity(entity).stateBahan bakar kendaraan, kunci mobil, kondisi pintu

Di server

-- Global: dibaca semua client
GlobalState.policeOnDuty = 0

-- Player
local function setDuty(src, onDuty)
    Player(src).state:set('onDuty', onDuty, true)
end

-- Entity: misalnya saat kendaraan di-spawn dari garasi
local veh = CreateVehicle(`sultan`, 215.0, -810.0, 30.7, 160.0, true, true)
while not DoesEntityExist(veh) do Wait(0) end
Entity(veh).state:set('fuel', 65.0, true)

Parameter ketiga true pada :set(key, value, replicated) berarti nilai itu dikirim ke client. Kalau false, nilai hanya ada di server. Ini berguna untuk data internal yang tidak perlu (atau tidak boleh) dilihat pemain.

Penulisan langsung seperti Entity(veh).state.fuel = 65.0 juga bisa dipakai dan di server otomatis direplikasi. Memakai :set membuat niat kamu lebih jelas saat membaca kode.

Di client

local veh = GetVehiclePedIsIn(PlayerPedId(), false)
if veh ~= 0 then
    local fuel = Entity(veh).state.fuel
    print(('Bensin: %.1f%%'):format(fuel or 0))
end

if LocalPlayer.state.onDuty then
    print('Kamu sedang on duty')
end

print('Polisi on duty:', GlobalState.policeOnDuty)

Membaca state bag di client tidak memicu request jaringan. Nilainya sudah ada di memori lokal setelah direplikasi, jadi relatif murah dibanding memanggil callback ke server.

Mendengarkan perubahan dengan change handler

Kekuatan utama state bags adalah kamu bisa bereaksi saat nilainya berubah, tanpa loop polling. Gunakan AddStateBagChangeHandler:

-- client.lua
AddStateBagChangeHandler('fuel', nil, function(bagName, key, value, _reserved, replicated)
    local entity = GetEntityFromStateBagName(bagName)
    if entity == 0 then return end

    if value and value <= 0.0 then
        SetVehicleEngineOn(entity, false, true, true)
    end
end)

Parameter pertama adalah key yang difilter ('fuel'), parameter kedua filter nama bag (nil berarti semua bag). Nama bag berbentuk string seperti entity:123 atau player:5. Fungsi GetEntityFromStateBagName dan GetPlayerFromStateBagName mengubahnya kembali menjadi handle yang bisa dipakai.

Ada satu jebakan: handler bisa terpanggil ketika entity belum benar-benar ada di client, misalnya entity di luar jarak culling. Karena itu selalu cek hasil GetEntityFromStateBagName sebelum memakainya, dan jangan menganggap entity pasti valid.

Contoh untuk memperbarui HUD saat status duty berubah:

local myBag = ('player:%d'):format(GetPlayerServerId(PlayerId()))

AddStateBagChangeHandler('onDuty', myBag, function(_, _, value)
    SendNUIMessage({ action = 'duty', data = value == true })
end)

Kalau kamu membangun HUD dengan React, pola ini cocok dipadukan dengan tutorial NUI FiveM dengan React dan TypeScript.

Keamanan: siapa yang boleh menulis?

Ini bagian yang paling sering diremehkan. Client juga bisa menulis ke state bag tertentu, misalnya state bag dirinya sendiri atau entity yang ia kontrol, dan perubahan dengan replicated = true dikirim ke server. Artinya cheater bisa mencoba mengubah nilai seperti fuel atau onDuty dari client.

Aturan praktisnya:

  1. State yang menentukan hak atau uang harus ditulis server. Contoh: status polisi, kepemilikan kunci, saldo. Server adalah sumber kebenaran.
  2. Jangan percaya state yang bisa ditulis client. Saat server memutuskan sesuatu (misalnya membuka akses gudang senjata), validasi dari data server sendiri, bukan dari state bag yang mungkin sudah diubah client.
  3. Pasang handler di server untuk key sensitif. Handler di server terpanggil saat client mengubah state. Kamu bisa mencatat (log) atau mengembalikan nilainya.
-- server.lua: kembalikan perubahan onDuty yang datang dari client
AddStateBagChangeHandler('onDuty', nil, function(bagName, key, value, _reserved, replicated)
    if not replicated then return end -- perubahan dari server sendiri, abaikan
    local src = GetPlayerFromStateBagName(bagName)
    if src == 0 then return end
    print(('[anti-tamper] %s mencoba mengubah onDuty'):format(GetPlayerName(src)))
    -- tulis ulang nilai dari sumber data server di sini
end)

Perilaku persis parameter replicated di handler server dan opsi pembatasan penulisan oleh client bisa berbeda antar versi FXServer. Cek dokumentasi state bags dan changelog artifact yang kamu pakai, lalu uji perilakunya sendiri di server staging sebelum mengandalkannya untuk anti-cheat.

Kapan state bags tepat dipakai?

Cocok untuk

  • Data kecil yang melekat pada entity atau player dan perlu diketahui banyak client: bahan bakar, status kunci, sirine, status diborgol.
  • Status global yang jarang berubah: jumlah polisi on duty, mode event, jam tertentu.
  • Menggantikan pola "broadcast event ke semua pemain setiap kali ada perubahan".

Kurang cocok untuk

  • Data besar, seperti seluruh isi inventory atau riwayat transaksi. Setiap perubahan dikirim lewat jaringan, dan server punya batas laju (rate limit) untuk perubahan state bag. Simpan data besar di server dan kirim ke client yang butuh saja.
  • Data rahasia yang tidak boleh dilihat pemain lain. Data player yang direplikasi bisa dibaca client lain yang mengenal player tersebut. Gunakan replicated = false atau event yang dikirim ke satu client.
  • Data yang berubah sangat sering, misalnya posisi atau kecepatan per frame. Ini sudah disinkronkan oleh game sendiri.
  • Data persisten. State bag hilang saat entity dihapus atau server restart. Simpan ke database jika perlu bertahan.

Contoh lengkap: kunci kendaraan sederhana

-- server.lua
RegisterNetEvent('kunci:server:toggle', function(netId)
    local src = source
    local veh = NetworkGetEntityFromNetworkId(netId)
    if veh == 0 or not DoesEntityExist(veh) then return end

    -- Validasi di server: pemain harus dekat dan pemilik kendaraan.
    local dist = #(GetEntityCoords(GetPlayerPed(src)) - GetEntityCoords(veh))
    if dist > 10.0 then return end
    if Entity(veh).state.owner ~= Player(src).state.citizenid then return end

    local locked = not Entity(veh).state.locked
    Entity(veh).state:set('locked', locked, true)
    SetVehicleDoorsLocked(veh, locked and 2 or 1)
end)

Di contoh ini, owner dan citizenid diasumsikan ditulis oleh server saat kendaraan di-spawn dan saat pemain login. Client hanya mengirim permintaan, sedangkan server yang memvalidasi dan menulis state.

Di sisi client, handler locked bisa memutar suara atau animasi lampu sein untuk semua pemain di sekitar. Jika kamu memakai QBCore, data seperti citizenid sudah tersedia di objek player QBCore. Lihat tutorial job custom QBCore untuk cara mengakses data pemain dengan benar.

Debugging dan performa

  • Print nilai di handler untuk memastikan perubahan benar-benar diterima client.
  • Kalau server terasa berat setelah menambah state bags, cek apakah ada script yang menulis state di loop yang terlalu cepat. Gunakan resmon dan profiler untuk menemukannya.
  • Peringatan rate limit atau hitch setelah perubahan besar adalah tanda kamu menulis terlalu sering atau terlalu besar. Panduan membaca peringatan itu ada di cara mengatasi hitch warning.

Ringkasan

  • State bags = key-value pada global, player, atau entity, bisa direplikasi ke client.
  • Butuh OneSync dan networked entity.
  • Gunakan AddStateBagChangeHandler daripada loop polling.
  • Server menulis state penting, client tidak dipercaya.
  • Simpan data kecil saja. Data besar, rahasia, atau persisten tetap di server dan database.

Bangun dan uji di server sendiri

Perilaku replikasi paling baik dipelajari dengan mencobanya langsung di server dengan dua client. Kalau kamu butuh server development dengan root penuh, lihat hosting FiveM Noctis yang berjalan di AMD Ryzen, DDR5, dan NVMe di Singapore. Untuk perbandingan paket, kunjungi Noctis Cloud.

start here

Building something with Noctis?

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