βHow to Create Job2 on ESX
This guide will walk you through the process of adding this functionality directly to your ESX core.
Important
This tutorial is specifically tailored for ESX 1.12.4+. The structure of the framework can change between versions, so the exact file paths and code blocks might differ if you're on a newer or older release. You'll need to locate the equivalent sections in your version of es_extended
. Always back up your files before making any changes! A simple mistake can take your server offline.
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.
Navigate to:
es_extended/client/modules/events.lua
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.
Navigate to:
es_extended/server/main.lua
Find the line that begins with
local loadPlayer =
.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.
Inside the same file, find the function
function loadESXPlayer
.Locate the section where the primary
job
data is loaded and validated (look forlocal job, grade =
).After the block of code for the primary job (you'll see an
end
statement), add the following code forjob2
:
-- 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.
Find the line:
local xPlayer = CreateExtendedPlayer(
In the list of arguments being passed, locate
userData.job
.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.
Find the callback:
ESX.RegisterServerCallback("esx:getPlayerData"
Inside the callback function, locate where the data table is built (
cb({...})
).Add a line for
job2
right after the line forjob
.
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)
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
Navigate to:
es_extended/server/functions.lua
Find the function
function Core.SavePlayer(xPlayer, cb)
.Replace the entire function with the following code. The key changes are adding
xPlayer.job2.name
andxPlayer.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.
In the same file, find the function
function Core.SavePlayers(cb)
.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.
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.
Find the
CreateExtendedPlayer
function definition.Add
job2
to the list of parameters.
function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, weight, job, job2, loadout, name, coords, metadata)
Inside the function, find where
self.job = job
is set.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
.
Find the line:
stateBag:set("job", self.job, true)
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.
Find the function
function self.getJob()
.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.
Find the function
function self.setJob(newJob, grade, onDuty)
.Add a new function directly below it to set the secondary job. This function mirrors the logic of
setJob
but for thejob2
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.
Navigate to:
es_extended/server/modules/commands.lua
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
andset
a player'sjob2
.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:
Restart your server.
Join your server.
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