❓How to Create Job2 on ESX

This guide will walk you through the process of adding this functionality directly to your ESX core.

Step 1 - Modify the Database Schema

First, we need to tell the database to store the new job2 and job_grade2 information for each player. We'll do this by adding two new columns to the users table.

Run the following SQL query in your database management tool (like phpMyAdmin, HeidiSQL, or through a console command).

ALTER TABLE `users`
    ADD COLUMN `job2` VARCHAR(20) DEFAULT 'unemployed',
    ADD COLUMN `job_grade2` INT(11) DEFAULT 0;

Step 2 - Add the Client-Side Event Handler

Now, we need to teach the client-side part of the script how to handle updates to the player's secondary job. This ensures the player's data updates in real-time without needing to relog.

  1. Navigate to: es_extended/client/modules/events.lua

  2. Add the following code block to the file. This listens for a secure network event from the server and updates the local player data accordingly.

ESX.SecureNetEvent("esx:setJob2", function(Job2)
    ESX.SetPlayerData('job2', Job2)
end)

Step 3 - Overhaul the Server-Side Player Loading

This is the most involved step. We need to modify the server-side code to load, validate, and manage the secondary job data when a player connects.

3.1. Update the Database Query

We first need to tell the server to request the new job2 and job_grade2 columns from the database.

  1. Navigate to: es_extended/server/main.lua

  2. Find the line that begins with local loadPlayer =.

  3. Replace it with the following line to include the new columns in the selection:

local loadPlayer = "SELECT `accounts`, `job`, `job_grade`, `job2`, `job_grade2`, `group`, `position`, `inventory`, `skin`, `loadout`, `metadata`"

3.2. Integrate Job2 into the Player Object

Next, we need to integrate the loaded data into the ESX player object, mirroring the functionality of the primary job.

  1. Inside the same file, find the function function loadESXPlayer.

  2. Locate the section where the primary job data is loaded and validated (look for local job, grade =).

  3. After the block of code for the primary job (you'll see an end statement), add the following code for job2:

-- Job2
local job2, grade2 = result.job2, tostring(result.job_grade2)

if not ESX.DoesJobExist(job2, grade2) then
    print(("[^3WARNING^7] Ignoring invalid job2 for ^5%s^7 [job2: ^5%s^7, grade: ^5%s^7]"):format(identifier, job2, grade2))
    job2, grade2 = "unemployed", "0"
end

local job2Object, grade2Object = ESX.Jobs[job2], ESX.Jobs[job2].grades[grade2]

userData.job2 = {
    id = job2Object.id,
    name = job2Object.name,
    label = job2Object.label,
    
    grade = tonumber(grade),
    grade_name = grade2Object.name,
    grade_label = grade2Object.label,
    grade_salary = grade2Object.salary,
    
    skin_male = grade2Object.skin_male and json.decode(grade2Object.skin_male) or {},
    skin_female = grade2Object.skin_female and json.decode(grade2Object.skin_female) or {},
}

3.3. Modify the Player Constructor

Now, we need to pass this new userData.job2 into the function that creates the player object.

  1. Find the line: local xPlayer = CreateExtendedPlayer(

  2. In the list of arguments being passed, locate userData.job.

  3. Add userData.job2 as a new argument immediately after it.

local xPlayer = CreateExtendedPlayer(
    playerId,
    identifier,
    userData.group,
    userData.accounts,
    userData.inventory,
    userData.weight,
    userData.job,
    userData.job2, -- <<-- NEW ARGUMENT ADDED HERE
    userData.loadout,
    GetPlayerName(playerId),
    userData.coords,
    userData.metadata
)

3.4. Update Server Callbacks

Finally, we need to ensure the job2 data is included when other scripts request player information. We'll update two key callbacks.

  1. Find the callback: ESX.RegisterServerCallback("esx:getPlayerData"

  2. Inside the callback function, locate where the data table is built (cb({...})).

  3. Add a line for job2 right after the line for job.

ESX.RegisterServerCallback("esx:getPlayerData", function(source, cb)
    local xPlayer = ESX.GetPlayerFromId(source)

    cb({
        identifier = xPlayer.identifier,
        accounts = xPlayer.getAccounts(),
        inventory = xPlayer.getInventory(),
        job = xPlayer.getJob(),
        job2 = xPlayer.getJob2(), -- <<-- NEW LINE ADDED HERE
        loadout = xPlayer.getLoadout(),
        money = xPlayer.getMoney(),
        position = xPlayer.getCoords(true),
        metadata = xPlayer.getMeta(),
    })
end)
  1. Repeat this process for the other player data callback: ESX.RegisterServerCallback("esx:getOtherPlayerData".

ESX.RegisterServerCallback("esx:getOtherPlayerData", function(_, cb, target)
    local xPlayer = ESX.GetPlayerFromId(target)

    cb({
        identifier = xPlayer.identifier,
        accounts = xPlayer.getAccounts(),
        inventory = xPlayer.getInventory(),
        job = xPlayer.getJob(),
        job2 = xPlayer.getJob2(), -- <<-- NEW LINE ADDED HERE
        loadout = xPlayer.getLoadout(),
        money = xPlayer.getMoney(),
        position = xPlayer.getCoords(true),
        metadata = xPlayer.getMeta(),
    })
end)

Step 4 - Ensure Data is Saved Correctly

Now that the server can load job2 data, we must ensure it gets saved back to the database properly. We'll update the functions responsible for saving player data, both for individual players and for bulk saves.

4.1. Update the Single Player Save Function

  1. Navigate to: es_extended/server/functions.lua

  2. Find the function function Core.SavePlayer(xPlayer, cb).

  3. Replace the entire function with the following code. The key changes are adding xPlayer.job2.name and xPlayer.job2.grade to the SQL parameters.

function Core.SavePlayer(xPlayer, cb)
    if not xPlayer.spawned then
        return cb and cb()
    end

    updateHealthAndArmorInMetadata(xPlayer)
    local parameters <const> = {
        json.encode(xPlayer.getAccounts(true)),
        xPlayer.job.name,
        xPlayer.job.grade,
        xPlayer.job2.name, -- NEW: Save job2 name
        xPlayer.job2.grade, -- NEW: Save job2 grade
        xPlayer.group,
        json.encode(xPlayer.getCoords(false, true)),
        json.encode(xPlayer.getInventory(true)),
        json.encode(xPlayer.getLoadout(true)),
        json.encode(xPlayer.getMeta()),
        xPlayer.identifier,
    }

    -- This MySQL query is updated to include job2 and job_grade2
    MySQL.prepare( 
        "UPDATE `users` SET `accounts` = ?, `job` = ?, `job_grade` = ?, `job2` = ?, `job_grade2` = ?, `group` = ?, `position` = ?, `inventory` = ?, `loadout` = ?, `metadata` = ? WHERE `identifier` = ?",
        parameters,
        function(affectedRows)
            if affectedRows == 1 then
                print(('[^2INFO^7] Saved player ^5"%s^7"'):format(xPlayer.name))
                TriggerEvent("esx:playerSaved", xPlayer.playerId, xPlayer)
            end
            if cb then
                cb()
            end
        end
    )
end

4.2. Update the Bulk Save Function

This function saves all online players at once, typically during a server restart.

  1. In the same file, find the function function Core.SavePlayers(cb).

  2. Replace the entire function with the code below. Again, we add the job2 data to the parameters for every player.

function Core.SavePlayers(cb)
    local xPlayers <const> = ESX.Players
    if not next(xPlayers) then
        return
    end

    local startTime <const> = os.time()
    local parameters = {}

    for _, xPlayer in pairs(ESX.Players) do
        updateHealthAndArmorInMetadata(xPlayer)
        parameters[#parameters + 1] = {
            json.encode(xPlayer.getAccounts(true)),
            xPlayer.job.name,
            xPlayer.job.grade,
            xPlayer.job2.name, -- NEW: Save job2 name for each player
            xPlayer.job2.grade, -- NEW: Save job2 grade for each player
            xPlayer.group,
            json.encode(xPlayer.getCoords(false, true)),
            json.encode(xPlayer.getInventory(true)),
            json.encode(xPlayer.getLoadout(true)),
            json.encode(xPlayer.getMeta()),
            xPlayer.identifier,
        }
    end

    -- Updated bulk query to include the new job2 columns
    MySQL.prepare(
        "UPDATE `users` SET `accounts` = ?, `job` = ?, `job_grade` = ?, `job2` = ?, `job_grade2` = ?,`group` = ?, `position` = ?, `inventory` = ?, `loadout` = ?, `metadata` = ? WHERE `identifier` = ?",
        parameters,
        function(results)
            if not results then
                return
            end

            if type(cb) == "function" then
                return cb()
            end

            print(("[^2INFO^7] Saved ^5%s^7 %s over ^5%s^7 ms"):format(#parameters, #parameters > 1 and "players" or "player", ESX.Math.Round((os.time() - startTime) / 1000000, 2)))
        end
    )
end

Step 5 - Extend the Player Class

The final step is to fully integrate job2 into the core Player object. This involves modifying its constructor, state bags, and adding methods to get and set the secondary job.

  1. Navigate to: es_extended/server/classes/player.lua

5.1. Modify the Constructor

We need to accept the job2 parameter when a player object is created and store it internally.

  1. Find the CreateExtendedPlayer function definition.

  2. Add job2 to the list of parameters.

function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, weight, job, job2, loadout, name, coords, metadata)
  1. Inside the function, find where self.job = job is set.

  2. Add a line directly below it to store the secondary job.

self.job = job
self.job2 = job2 -- NEW: Store the job2 data

5.2. Add State Bag Support

State bags allow data to be efficiently synchronized between the server and client. We need to add one for job2.

  1. Find the line: stateBag:set("job", self.job, true)

  2. Add a new line below it to create a state bag for job2.

stateBag:set("job", self.job, true)
stateBag:set("job2", self.job2, true) -- NEW: Sync job2 to the client

5.3. Create a Getter Function

We need a method for other scripts to retrieve the player's job2 data.

  1. Find the function function self.getJob().

  2. Add a new function directly below it to get the secondary job.

function self.getJob2()
    return self.job2
end

5.4. Create a Setter Function

This is the most crucial part. We need a method to change a player's job2, which will handle validation, update the player object, trigger events, and sync the change to the client instantly.

  1. Find the function function self.setJob(newJob, grade, onDuty).

  2. Add a new function directly below it to set the secondary job. This function mirrors the logic of setJob but for the job2 property.

function self.setJob2(newJob2, grade2, onDuty)
    grade2 = tostring(grade2)
    local lastJob = self.job2
    if not ESX.DoesJobExist(newJob2, grade2) then
        return print(("[ESX] [^3WARNING^7] Ignoring invalid ^5.setJob2()^7 usage for ID: ^5%s^7, Job2: ^5%s^7"):format(self.source, newJob2))
    end
    local jobObject2, gradeObject2 = ESX.Jobs[newJob2], ESX.Jobs[newJob2].grades[grade2]
    self.job2 = {
        id = jobObject2.id,
        name = jobObject2.name,
        label = jobObject2.label,
        grade = tonumber(grade2),
        grade_name = gradeObject2.name,
        grade_label = gradeObject2.label,
        grade_salary = gradeObject2.salary,
        skin_male = gradeObject2.skin_male and json.decode(gradeObject2.skin_male) or {},
        skin_female = gradeObject2.skin_female and json.decode(gradeObject2.skin_female) or {},
    }
    TriggerEvent("esx:setJob2", self.source, self.job2, lastJob)
    self.triggerEvent("esx:setJob2", self.job2, lastJob)
    Player(self.source).state:set("job2", self.job2, true)
end

Step 6 - Create an Admin Command for Testing

Now let's add a convenient admin command. This will allow you to change a player's secondary job on the fly.

  1. Navigate to: es_extended/server/modules/commands.lua

  2. Register a new command by adding the following code block to the file. This leverages the setJob2 function we created in the previous step.

ESX.RegisterCommand(
    "setjob2",
    "admin",
    function(xPlayer, args, showError)
        if not ESX.DoesJobExist(args.job2, args.grade2) then
            return showError(TranslateCap("command_setjob_invalid"))
        end

        args.playerId.setJob2(args.job2, args.grade2)
    end,
    true,
    {
        help = TranslateCap("command_setjob"),
        validate = true,
        arguments = {
            { name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
            { name = "job2", help = "Job2", type = "string" },
            { name = "grade2", help = "Grade2", type = "number" },
        },
    }
)

πŸŽ‰ Implementation Complete!

You have now successfully integrated a complete secondary job system into your ESX framework. The functionality includes:

  • Database Storage: Saving and loading from the users table.

  • Client-Sync: Real-time updates via events and state bags.

  • Server-Side API: Methods to get and set a player's job2.

  • Persistence: Data is correctly saved during manual and automatic save operations.

  • Accessibility: An admin command for easy testing and management.

Important Final Note: The job2 system works by reusing your existing jobs and job_grades database tables. This is a efficient design choice. It means you can assign any job you've already defined (e.g., 'police', 'mechanic', 'gang') to a player as either their primary job (job) or their secondary job (job2). They are separate slots that draw from the same list of available occupations.

To test your work:

  1. Restart your server.

  2. Join your server.

  3. Use the command: /setjob2 [PLAYER_ID] [JOB2_NAME] [JOB2_GRADE]

    • Example: /setjob2 1 gang 0

If everything is configured correctly, the target player's secondary job will update immediately, and the change will be saved to the database. You can now use xPlayer.setJob2('gangname', 0) in your server scripts and ESX.PlayerData.job2 in your client scripts, just like you would with the primary job.

Last updated