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!