Skip to main content

Introduction

Client events are essential for handling player-specific actions, UI updates, and local game state changes in RSG Framework. They run on each player’s machine and allow for responsive, individualized gameplay experiences.
Client events run locally on each player’s machine and can be triggered by the server using TriggerClientEvent or by other client-side code using TriggerEvent.

How It Works

Event Flow

Server Script → TriggerClientEvent('eventName', source, args) → Client Listens → Client Processes
Or locally:
Client Script → TriggerEvent('eventName', args) → Same Client Listens → Client Processes

Registering Events

Client events are registered using RegisterNetEvent:
-- CLIENT SIDE
RegisterNetEvent('eventName', function(arg1, arg2)
    -- Process event locally
    print('Received:', arg1, arg2)
end)
Client events are not synchronized between players. Each player’s client runs independently!

Core Lifecycle Events

RSGCore:Client:OnPlayerLoaded

Triggered when a player successfully loads into the server after character selection. This is the most important event for initializing player-specific client-side features.
-- CLIENT SIDE
local RSGCore = exports['rsg-core']:GetCoreObject()

RegisterNetEvent('RSGCore:Client:OnPlayerLoaded', function()
    local PlayerData = RSGCore.Functions.GetPlayerData()

    print('Player loaded:', PlayerData.charinfo.firstname)

    -- Initialize player-specific features
    -- Set up HUD
    -- Load player blips
    -- Start player threads
end)
Use Cases:
  • Initialize HUD elements
  • Start player status monitors
  • Load player-specific blips/markers
  • Set up keybinds
  • Initialize job-specific UI
  • Restore player settings
Complete Example:
-- CLIENT SIDE
RegisterNetEvent('RSGCore:Client:OnPlayerLoaded', function()
    local PlayerData = RSGCore.Functions.GetPlayerData()

    -- Start HUD
    TriggerEvent('hud:client:LoadHud', PlayerData)

    -- Start status update thread
    CreateThread(function()
        while true do
            -- Update hunger/thirst display
            Wait(30000) -- Every 30 seconds
        end
    end)

    -- Show welcome message
    lib.notify({
        title = 'Welcome Back!',
        description = 'Hello, ' .. PlayerData.charinfo.firstname,
        type = 'success',
        duration = 5000
    })

    -- Job-specific initialization
    if PlayerData.job.name == 'police' then
        TriggerEvent('police:client:initPoliceJob')
    end
end)

RSGCore:Client:OnPlayerUnload

Triggered when a player logs out or returns to character selection. Use this to clean up client-side data and stop threads.
-- CLIENT SIDE
RegisterNetEvent('RSGCore:Client:OnPlayerUnload', function()
    print('Player unloading...')

    -- Clean up HUD
    TriggerEvent('hud:client:UnloadHud')

    -- Stop custom threads
    -- Clear temporary blips
    -- Reset UI elements
    -- Save client settings
end)
Use Cases:
  • Stop running threads
  • Clear temporary blips/markers
  • Reset UI to default state
  • Save client-side settings
  • Clean up job-specific features
Complete Example:
-- CLIENT SIDE
local activeThreads = {}
local playerBlips = {}

RegisterNetEvent('RSGCore:Client:OnPlayerUnload', function()
    -- Stop all active threads
    for _, thread in pairs(activeThreads) do
        thread.active = false
    end
    activeThreads = {}

    -- Remove all player blips
    for _, blip in pairs(playerBlips) do
        if DoesBlipExist(blip) then
            RemoveBlip(blip)
        end
    end
    playerBlips = {}

    -- Clear UI
    SendNUIMessage({
        action = 'hideAll'
    })

    print('Player unloaded and cleaned up')
end)

Player Data Events

RSGCore:Player:SetPlayerData

Triggered whenever player data changes (job update, money change, metadata update, etc.). Essential for keeping UI synchronized with server data.
-- CLIENT SIDE
local RSGCore = exports['rsg-core']:GetCoreObject()

RegisterNetEvent('RSGCore:Player:SetPlayerData', function(newData)
    -- Update local PlayerData
    RSGCore.PlayerData = newData

    -- Update HUD with new data
    TriggerEvent('hud:client:UpdatePlayerData', newData)

    print('Player data updated')
end)
Use Cases:
  • Update HUD when money changes
  • Refresh job display when job updates
  • Update status bars when metadata changes
  • Sync inventory UI
  • Update player name displays
Complete Example with Specific Updates:
-- CLIENT SIDE
local RSGCore = exports['rsg-core']:GetCoreObject()
local lastMoney = {}
local lastJob = nil

RegisterNetEvent('RSGCore:Player:SetPlayerData', function(newData)
    local oldData = RSGCore.PlayerData
    RSGCore.PlayerData = newData

    -- Check for money changes
    if oldData.money then
        for moneyType, amount in pairs(newData.money) do
            if lastMoney[moneyType] and lastMoney[moneyType] ~= amount then
                local difference = amount - lastMoney[moneyType]
                if difference > 0 then
                    lib.notify({
                        description = '+$' .. difference .. ' ' .. moneyType,
                        type = 'success'
                    })
                end
            end
            lastMoney[moneyType] = amount
        end
    end

    -- Check for job changes
    if newData.job and newData.job.name ~= lastJob then
        lib.notify({
            title = 'Job Updated',
            description = 'You are now: ' .. newData.job.label,
            type = 'info'
        })
        lastJob = newData.job.name

        -- Trigger job-specific initialization
        TriggerEvent('job:client:onJobChange', newData.job)
    end

    -- Update HUD
    SendNUIMessage({
        action = 'updatePlayerData',
        data = newData
    })
end)

Job & Duty Events

RSGCore:Client:OnJobUpdate

Triggered when a player’s job changes.
-- CLIENT SIDE
RegisterNetEvent('RSGCore:Client:OnJobUpdate', function(job)
    print('Job updated to:', job.label)
    print('Grade:', job.grade.name)
    print('On Duty:', job.onduty)

    -- Update job-specific UI
    SendNUIMessage({
        action = 'updateJob',
        job = job
    })

    -- Job-specific logic
    if job.name == 'police' then
        TriggerEvent('police:client:setupPoliceJob')
    elseif job.name == 'doctor' then
        TriggerEvent('doctor:client:setupDoctorJob')
    end
end)
Complete Example:
-- CLIENT SIDE
local currentJobBlips = {}

RegisterNetEvent('RSGCore:Client:OnJobUpdate', function(job)
    -- Clean up old job blips
    for _, blip in pairs(currentJobBlips) do
        if DoesBlipExist(blip) then
            RemoveBlip(blip)
        end
    end
    currentJobBlips = {}

    -- Add new job blips
    if job.name == 'police' then
        -- Add police station blip
        local blip = Citizen.InvokeNative(0x554D9D53F696D002, 1664425300, Config.PoliceStation.x, Config.PoliceStation.y, Config.PoliceStation.z)
        SetBlipSprite(blip, -1282792512, 1)
        currentJobBlips[#currentJobBlips + 1] = blip
    end

    lib.notify({
        title = 'Job Changed',
        description = 'You are now a ' .. job.label,
        type = 'info'
    })
end)

RSGCore:Client:SetDuty

Triggered when a player’s duty status changes.
-- CLIENT SIDE
RegisterNetEvent('RSGCore:Client:SetDuty', function(onDuty)
    print('Duty status:', onDuty and 'ON DUTY' or 'OFF DUTY')

    -- Update duty indicator
    SendNUIMessage({
        action = 'setDuty',
        onDuty = onDuty
    })

    -- Show/hide duty-specific features
    if onDuty then
        -- Enable job features
    else
        -- Disable job features
    end
end)

Vehicle Events

RSGCore:Command:SpawnVehicle

Spawns a vehicle near the player.
-- CLIENT SIDE
RegisterNetEvent('RSGCore:Command:SpawnVehicle', function(model)
    local ped = PlayerPedId()
    local coords = GetEntityCoords(ped)
    local heading = GetEntityHeading(ped)

    -- Load vehicle model
    local modelHash = GetHashKey(model)
    lib.requestModel(modelHash)

    -- Spawn vehicle
    local vehicle = CreateVehicle(modelHash, coords.x + 2, coords.y, coords.z, heading, true, false)

    -- Set player in vehicle
    SetPedIntoVehicle(ped, vehicle, -1)

    -- Clean up
    SetModelAsNoLongerNeeded(modelHash)

    lib.notify({
        description = 'Vehicle spawned: ' .. model,
        type = 'success'
    })
end)
Trigger from Server:
-- SERVER SIDE
RSGCore.Commands.Add('spawnveh', 'Spawn vehicle', {{name = 'model', help = 'Vehicle model'}}, true, function(source, args)
    TriggerClientEvent('RSGCore:Command:SpawnVehicle', source, args[1])
end, 'admin')
Trigger from Client:
-- CLIENT SIDE
RegisterCommand('spawnveh', function(_, args)
    local model = args[1]
    if not model then
        lib.notify({description = 'Usage: /spawnveh [model]', type = 'error'})
        return
    end

    TriggerEvent('RSGCore:Command:SpawnVehicle', model)
end)

RSGCore:Command:DeleteVehicle

Deletes the vehicle the player is in or near.
-- CLIENT SIDE
RegisterNetEvent('RSGCore:Command:DeleteVehicle', function()
    local ped = PlayerPedId()
    local vehicle = GetVehiclePedIsIn(ped, false)

    -- If not in vehicle, get closest vehicle
    if vehicle == 0 then
        local coords = GetEntityCoords(ped)
        vehicle = GetClosestVehicle(coords.x, coords.y, coords.z, 5.0, 0, 70)
    end

    if vehicle ~= 0 then
        DeleteEntity(vehicle)
        lib.notify({
            description = 'Vehicle deleted',
            type = 'success'
        })
    else
        lib.notify({
            description = 'No vehicle nearby',
            type = 'error'
        })
    end
end)
Trigger from Command:
-- CLIENT SIDE
RegisterCommand('dv', function()
    TriggerEvent('RSGCore:Command:DeleteVehicle')
end)

Item & Inventory Events

RSGCore:Client:UseItem

Triggered when a player uses an item. Often called by the server after validation.
-- CLIENT SIDE
RegisterNetEvent('RSGCore:Client:UseItem', function(item)
    print('Using item:', item.name)
    print('Amount:', item.amount)
    print('Slot:', item.slot)

    -- Play animation
    local ped = PlayerPedId()
    lib.requestAnimDict('mech_inventory@eating')
    TaskPlayAnim(ped, 'mech_inventory@eating', 'eat', 8.0, 8.0, 3000, 0, 0, true, true, true)

    -- Wait for animation
    Wait(3000)

    -- Clean up
    ClearPedTasks(ped)
    RemoveAnimDict('mech_inventory@eating')
end)
Complete Example with Progress Bar:
-- CLIENT SIDE
RegisterNetEvent('RSGCore:Client:UseItem', function(item)
    if item.name == 'bread' then
        -- Show progress bar
        if lib.progressBar({
            duration = 3000,
            label = 'Eating bread...',
            useWhileDead = false,
            canCancel = true,
            disable = {
                move = false,
                car = true,
                combat = true
            },
            anim = {
                dict = 'mech_inventory@eating@multi_bite@wedge_sandwich',
                clip = 'eat_wedge_sandwich'
            }
        }) then
            -- Notify server item was consumed
            TriggerServerEvent('consumables:server:eatFood', item.slot)

            lib.notify({
                description = 'You ate bread',
                type = 'success'
            })
        else
            lib.notify({
                description = 'Cancelled',
                type = 'error'
            })
        end
    end
end)

System Events

RSGCore:Client:PvpHasToggled

Triggered when PVP mode is enabled or disabled for the player.
-- CLIENT SIDE
RegisterNetEvent('RSGCore:Client:PvpHasToggled', function(pvpEnabled)
    print('PVP mode:', pvpEnabled and 'ENABLED' or 'DISABLED')

    -- Update network mode
    SetCanAttackFriendly(PlayerPedId(), true, pvpEnabled)
    NetworkSetFriendlyFireOption(pvpEnabled)

    -- Notify player
    lib.notify({
        description = pvpEnabled and 'PVP Enabled' or 'PVP Disabled',
        type = 'info'
    })

    -- Update UI
    SendNUIMessage({
        action = 'setPvp',
        enabled = pvpEnabled
    })
end)

RSGCore:Client:UpdateObject

Used to refresh the RSGCore object when using shared exports. Important for hot-reloading.
-- CLIENT SIDE
local RSGCore = exports['rsg-core']:GetCoreObject()

RegisterNetEvent('RSGCore:Client:UpdateObject', function()
    RSGCore = exports['rsg-core']:GetCoreObject()
    print('Core object updated')
end)
This event is automatically triggered when rsg-core restarts. Include it in your resources to support hot-reloading!

Notification Events

Common Notification Pattern

-- CLIENT SIDE
RegisterNetEvent('myresource:client:notify', function(message, type)
    lib.notify({
        description = message,
        type = type or 'info'
    })
end)

-- Trigger from server
-- SERVER SIDE
TriggerClientEvent('myresource:client:notify', source, 'Action completed!', 'success')

Best Practices

1. Always Get Fresh PlayerData

-- GOOD
RegisterNetEvent('myEvent', function()
    local PlayerData = RSGCore.Functions.GetPlayerData()
    -- Use fresh data
end)

-- BAD (can be outdated)
local PlayerData = RSGCore.Functions.GetPlayerData()
RegisterNetEvent('myEvent', function()
    -- Using old PlayerData
end)

2. Clean Up on Player Unload

-- GOOD
local myBlips = {}
local myThreads = {}

RegisterNetEvent('RSGCore:Client:OnPlayerUnload', function()
    -- Clean up blips
    for _, blip in pairs(myBlips) do
        if DoesBlipExist(blip) then
            RemoveBlip(blip)
        end
    end

    -- Stop threads
    for _, thread in pairs(myThreads) do
        thread.active = false
    end
end)

3. Use Progress Bars for Actions

-- GOOD
if lib.progressBar({
    duration = 5000,
    label = 'Mining...',
    useWhileDead = false,
    canCancel = true,
    disable = {
        move = true,
        car = true,
        combat = true
    }
}) then
    -- Action completed
    TriggerServerEvent('mining:server:collectOre')
else
    -- Action cancelled
end

4. Validate Before Processing

-- GOOD
RegisterNetEvent('myEvent', function(data)
    if not data or type(data) ~= 'table' then
        print('Invalid data received')
        return
    end

    -- Process data
end)

Common Patterns

Pattern 1: Animation with Callback

RegisterNetEvent('animations:client:playAnim', function(dict, anim, duration)
    local ped = PlayerPedId()

    lib.requestAnimDict(dict)
    TaskPlayAnim(ped, dict, anim, 8.0, 8.0, duration or -1, 0, 0, true, true, true)

    if duration and duration > 0 then
        Wait(duration)
        ClearPedTasks(ped)
    end

    RemoveAnimDict(dict)
end)

Pattern 2: Proximity Activation

CreateThread(function()
    local shopCoords = vector3(2633.0, -1220.0, 53.0)
    local inRange = false

    while true do
        local ped = PlayerPedId()
        local coords = GetEntityCoords(ped)
        local distance = #(coords - shopCoords)

        if distance < 2.0 and not inRange then
            inRange = true
            lib.showTextUI('[E] Open Shop')
        elseif distance >= 2.0 and inRange then
            inRange = false
            lib.hideTextUI()
        end

        if inRange and IsControlJustPressed(0, RSGCore.Shared.Keybinds['E']) then
            TriggerServerEvent('shop:server:openShop')
        end

        Wait(inRange and 0 or 1000)
    end
end)

Pattern 3: Status Monitor

RegisterNetEvent('RSGCore:Client:OnPlayerLoaded', function()
    CreateThread(function()
        while true do
            local PlayerData = RSGCore.Functions.GetPlayerData()

            if PlayerData.metadata then
                -- Update HUD
                SendNUIMessage({
                    action = 'updateStatus',
                    hunger = PlayerData.metadata.hunger,
                    thirst = PlayerData.metadata.thirst
                })
            end

            Wait(5000) -- Update every 5 seconds
        end
    end)
end)

Troubleshooting

Event Not Triggering

Some events only work after RSGCore:Client:OnPlayerLoaded
Event names are case-sensitive and must match exactly
Press F8 to open client console and look for error messages

PlayerData is Nil

-- Wait for player to load
RegisterNetEvent('myEvent', function()
    local PlayerData = RSGCore.Functions.GetPlayerData()

    if not PlayerData or not PlayerData.citizenid then
        print('Player not loaded yet')
        return
    end

    -- Safe to use PlayerData
end)

UI Not Updating

-- Make sure to listen to PlayerData updates
RegisterNetEvent('RSGCore:Player:SetPlayerData', function(newData)
    -- Update UI
    SendNUIMessage({
        action = 'update',
        data = newData
    })
end)

Summary

EventPurposeTriggered By
RSGCore:Client:OnPlayerLoadedPlayer loginServer
RSGCore:Client:OnPlayerUnloadPlayer logoutServer
RSGCore:Player:SetPlayerDataData updateServer
RSGCore:Client:OnJobUpdateJob changeServer
RSGCore:Client:SetDutyDuty toggleServer
RSGCore:Command:SpawnVehicleSpawn vehicleServer/Client
RSGCore:Command:DeleteVehicleDelete vehicleServer/Client
RSGCore:Client:UseItemUse itemServer
RSGCore:Client:PvpHasToggledPVP toggleServer
RSGCore:Client:UpdateObjectRefresh coreServer

Next Steps


Need more help? Join the RSG Framework Discord!