#Subjobs for jobs (e.g. police SWAT/CID/TEU)

1 messages · Page 1 of 1 (latest)

shell estuary
#

Unsure why it doesn't have this in core, sorta makes sense but doesn't anyway heres what we did for my Server and it works well. If ur clueless and don't know what your doing, DO NOT ASK FOR HELP. I'm not helping you, ask quasar they have very good support trust! 🙏

shared/jobs.lua

Add a subjobs table to the job(s) that should have divisions. For police, add it after offDutyPay.

Replace the police job’s opening so it includes subjobs:

["police"] = {
    label = "Police Patrol",
    type = "leo",
    defaultDuty = true,
    offDutyPay = false,
    subjobs = {
        ["patrol"] = {
            label = "Patrol",
            grades = { [0] = { name = "Patrol Officer", payment = 0 } },
        },
        ["swat"] = {
            label = "Special Weapons and Tactics",
            grades = {
                [0] = { name = "SWAT - Trial Operative", isboss = false, bankAuth = false },
                [1] = { name = "SWAT - Operator", isboss = false, bankAuth = false },
                -- add more grades as needed
            },
        },
        ["cid"] = {
            label = "Criminal Investigations Division",
            grades = {
                [0] = { name = "CID - Trial Detective", isboss = false, bankAuth = false },
                -- etc
            },
        },
        ["teu"] = {
            label = "Traffic Enforcement Unit",
            grades = {
                [0] = { name = "TEU - Officer I", isboss = false, bankAuth = false },
                -- etc
            },
        },
    },
    grades = {

Same pattern works for other jobs: add subjobs = { ["subjobname"] = { label = "...", grades = { [0] = { name = "...", ... } } }, ... } to that job.

shared/functions.lua

In GetPlayerGroups https://github.com/Qbox-project/qbx_core/blob/main/shared/functions.lua#L76, after the loop that fills groups from playerData.jobs, add subjob as group so ox_inventory etc. can use it:

After the for job, data in pairs(playerData.jobs) block, before the for gang, data loop, insert:

    local mainJob = playerData.job
    if mainJob and mainJob.name == "police" and mainJob.subjob and mainJob.subjobGrade then
        local subjobAsGroup = { swat = true, cid = true, teu = true }
        if subjobAsGroup[mainJob.subjob] then
            groups[mainJob.subjob] = mainJob.subjobGrade.level
        end
    end

If you add subjobs to other jobs, extend subjobAsGroup or derive it from the job’s subjobs table.


server/groups.lua

After exports("GetGang", GetGang) and before local function upsertJobData. https://github.com/Qbox-project/qbx_core/blob/main/server/groups.lua#L287

Add:

---@param jobName string
---@return table<string, Subjob>?
function GetSubjobs(jobName)
    local job = jobs[jobName]
    return job and job.subjobs or nil
end

exports("GetSubjobs", GetSubjobs)

---@param jobName string
---@param subjobName string
---@return Subjob?
function GetSubjob(jobName, subjobName)
    local subjobs = GetSubjobs(jobName)
    return subjobs and subjobs[subjobName:lower()] or nil
end

exports("GetSubjob", GetSubjob)

#

server/player.lua

toPlayerJob:

Find local function toPlayerJob(jobName, job, grade) https://github.com/Qbox-project/qbx_core/blob/main/server/player.lua#L151. Change signature and body so it accepts and applies subjob.

Replace the function with:

---@param jobName string
---@param job Job
---@param grade integer
---@param subjob? string
---@param subjobGrade? integer
---@return PlayerJob
local function toPlayerJob(jobName, job, grade, subjob, subjobGrade)
    local playerJob = {
        name = jobName,
        label = job.label,
        isboss = job.grades[grade].isboss or false,
        bankAuth = job.grades[grade].bankAuth or false,
        onduty = job.defaultDuty or false,
        payment = job.grades[grade].payment or 0,
        type = job.type,
        grade = {
            name = job.grades[grade].name,
            level = grade
        }
    }
    if subjob and job.subjobs and job.subjobs[subjob] then
        local subjobDef = job.subjobs[subjob]
        local sg = subjobGrade or 0
        if subjobDef.grades and subjobDef.grades[sg] then
            playerJob.subjob = subjob
            playerJob.subjobLabel = subjobDef.label
            playerJob.subjobGrade = {
                name = subjobDef.grades[sg].name,
                level = sg
            }
        end
    end
    return playerJob
end
#

CheckPlayerData

In CheckPlayerData job is built from playerData.job. Find the block that sets playerData.job to a literal table: from local job = GetJob(playerData.job?.name) or GetJob('unemployed') through the if QBX.Shared.ForceJobDefaultDutyAtLogin ... end.

Replace that block (from local job = GetJob(...) through the assignment to playerData.job and its onduty / ForceJobDefaultDutyAtLogin logic) with:

    local jobName = playerData.job?.name or 'unemployed'
    local job = GetJob(jobName) or GetJob('unemployed')
    assert(job ~= nil, 'Unemployed job not found. Does it exist in shared/jobs.lua?')
    local jobGrade = GetJob(playerData.job?.name) and playerData.job.grade.level or 0
    if not job.grades[jobGrade] then
        jobGrade = 0
    end
    local inputSubjob = playerData.job?.subjob
    local inputSubjobGrade = playerData.job?.subjobGrade and (type(playerData.job.subjobGrade) == 'table' and playerData.job.subjobGrade.level or tonumber(playerData.job.subjobGrade)) or 0
    local savedOnduty = playerData.job?.onduty
    playerData.job = toPlayerJob(jobName, job, jobGrade, inputSubjob, inputSubjobGrade)
    playerData.job.onduty = savedOnduty or false
    if QBX.Shared.ForceJobDefaultDutyAtLogin and (job.defaultDuty ~= nil) then
        playerData.job.onduty = job.defaultDuty
    end
#

SetSubjob

Add after exports('SetGang', SetGang)

---@param identifier Source|string
---@param subjobName string
---@param grade? integer
---@return boolean success
---@return ErrorResult? errorResult
function SetSubjob(identifier, subjobName, grade)
    subjobName = subjobName and tostring(subjobName):lower() or nil
    grade = tonumber(grade) or 0

    if not subjobName or subjobName == "" then
        lib.print.error("SetSubjob: subjob name required")
        return false, { code = "invalid_subjob", message = "subjob name required" }
    end

    local player = type(identifier) == "string" and (GetPlayerByCitizenId(identifier) or GetOfflinePlayer(identifier)) or GetPlayer(identifier)
    if not player then
        return false, { code = "player_not_found", message = "player not found" }
    end

    local jobName = player.PlayerData.job.name
    local job = GetJob(jobName)
    if not job or not job.subjobs then
        lib.print.error(("SetSubjob: job %s does not have subjobs"):format(jobName or "nil"))
        return false, { code = "job_no_subjobs", message = ("job %s does not support subjobs"):format(jobName or "unemployed") }
    end

    local subjobDef = GetSubjob(jobName, subjobName)
    if not subjobDef then
        lib.print.error(("SetSubjob: subjob %s does not exist for job %s"):format(subjobName, jobName))
        return false, { code = "subjob_not_found", message = ("subjob %s not found for job %s"):format(subjobName, jobName) }
    end

    if not subjobDef.grades or not subjobDef.grades[grade] then
        lib.print.error(("SetSubjob: subjob %s does not have grade %s"):format(subjobName, grade))
        return false, { code = "invalid_grade", message = ("subjob %s does not have grade %s"):format(subjobName, grade) }
    end

    local oldOnduty = player.PlayerData.job.onduty
    player.PlayerData.job = toPlayerJob(jobName, job, player.PlayerData.job.grade.level, subjobName, grade)
    player.PlayerData.job.onduty = oldOnduty

    if player.Offline then
        SaveOffline(player.PlayerData)
    else
        Save(player.PlayerData.source)
        UpdatePlayerData(player.PlayerData.source)
        TriggerEvent("QBCore:Server:OnJobUpdate", player.PlayerData.source, player.PlayerData.job)
        TriggerClientEvent("QBCore:Client:OnJobUpdate", player.PlayerData.source, player.PlayerData.job)
        TriggerEvent("qbx_core:server:onGroupUpdate", player.PlayerData.source, subjobName, grade)
        TriggerClientEvent("qbx_core:client:onGroupUpdate", player.PlayerData.source, subjobName, grade)
    end

    return true
end

exports("SetSubjob", SetSubjob)
#
---@param identifier Source|string
---@return boolean success
function ClearSubjob(identifier)
    local player = type(identifier) == "string" and (GetPlayerByCitizenId(identifier) or GetOfflinePlayer(identifier)) or GetPlayer(identifier)
    if not player then return false end

    local jobName = player.PlayerData.job.name
    local job = GetJob(jobName)
    if not job then return false end

    local oldSubjob = player.PlayerData.job.subjob
    local oldOnduty = player.PlayerData.job.onduty
    player.PlayerData.job = toPlayerJob(jobName, job, player.PlayerData.job.grade.level, nil, nil)
    player.PlayerData.job.onduty = oldOnduty

    if player.Offline then
        SaveOffline(player.PlayerData)
    else
        Save(player.PlayerData.source)
        UpdatePlayerData(player.PlayerData.source)
        TriggerEvent("QBCore:Server:OnJobUpdate", player.PlayerData.source, player.PlayerData.job)
        TriggerClientEvent("QBCore:Client:OnJobUpdate", player.PlayerData.source, player.PlayerData.job)
        if oldSubjob then
            TriggerEvent("qbx_core:server:onGroupUpdate", player.PlayerData.source, oldSubjob, nil)
            TriggerClientEvent("qbx_core:client:onGroupUpdate", player.PlayerData.source, oldSubjob, nil)
        end
    end

    return true
end

exports("ClearSubjob", ClearSubjob)```
#

Player entity

In CreatePlayer, where other Functions are set (e.g. after self.Functions.SetGang add:

    function self.Functions.SetSubjob(subjobName, grade)
        return SetSubjob(self.PlayerData.source, subjobName, grade)
    end
#

server/commands.lua

/job
Replace the job command handler so it can show subjob:

lib.addCommand("job", {
    help = locale("command.job.help")
}, function(source)
    local PlayerJob = GetPlayer(source).PlayerData.job
    local duty = PlayerJob.onduty and "On" or "Off"
    if PlayerJob.subjob and PlayerJob.subjobGrade then
        Notify(source, locale("info.job_info_subjob", PlayerJob.label, PlayerJob.grade.name, PlayerJob.subjobGrade.name or PlayerJob.subjob, duty))
    else
        Notify(source, locale("info.job_info", PlayerJob.label, PlayerJob.grade.name, duty))
    end
end)

/setsubjob

Add after the setgang command:

lib.addCommand("setsubjob", {
    help = locale("command.setsubjob.help"),
    params = {
        { name = locale("command.setsubjob.params.id.name"), help = locale("command.setsubjob.params.id.help"), type = "playerId" },
        { name = locale("command.setsubjob.params.subjob.name"), help = locale("command.setsubjob.params.subjob.help"), type = "string" },
        { name = locale("command.setsubjob.params.grade.name"), help = locale("command.setsubjob.params.grade.help"), type = "number", optional = true }
    },
    restricted = "group.admin"
}, function(source, args)
    local player = GetPlayer(args[locale("command.setsubjob.params.id.name")])
    if not player then
        Notify(source, locale("error.not_online"), "error")
        return
    end

    local success, errorResult = player.Functions.SetSubjob(args[locale("command.setsubjob.params.subjob.name")], args[locale("command.setsubjob.params.grade.name")] or 0)
    if not success then
        Notify(source, errorResult and errorResult.message or "Failed to set subjob", "error")
        return
    end
    Notify(source, "Subjob set.", "success")
end)
#

locales/en.json

In info, add:

"job_info_subjob": "Job: %s | Grade: %s | Subjob: %s | Duty: %s",

command:
In command, after setgang, add:

"setsubjob": {
    "help": "Set A Players Subjob/Department (Admin Only)",
    "params": {
        "id": { "name": "id", "help": "Player ID" },
        "subjob": { "name": "subjob", "help": "Subjob name (e.g. swat, cid, teu)" },
        "grade": { "name": "grade", "help": "Subjob grade" }
    }
},
forest slate
#

Pretty nice concept, tho much servers just use "licences" to bypass that issue

#

police job

SWAT license [give you access to certain things in the pd job]

#

it doesn't give you separate bank access etc, but you can have multiple things at once, thats the biggest benefit

shell estuary
#

Licenses are good for simple access tbh we wanted division + grade in the job so /job, character data, and stuff like garages/armory all use the same thing

#

Ah Well - I'm sure someone will utilize this.

gloomy tundra
#

I trust you just by you saying "ask quasar they have very good support trust" lmao

harsh scarab
#

Actually goated idea

amber iris
#

@shell estuary whats the difference between subjobs and grades?

shell estuary
gray cliff
#

Thank you @shell estuary

shell estuary
uncut imp
#

Did you manage to be able to utilize qbx_management for each subjob?

shell estuary
uncut imp
#

Nah I did it already I can publish it if someone wants

uncut imp
shell estuary
#

It still used main jobs, unless u made certain features require a sub job?

#

i don’t use qbox police personally so wouldn’t know what it offers

uncut imp
#

I made an entire bridge, that recognized the subjobs as departments with their own iterations of the same menu and ability to manage their own departments

#

Like instead of subjobs I made departments that have their own employees that the commissioner can control only the global department while department heads like chief or sheriff can only manage theirs

still island
#

Do you have to still do /setjob [id] police [grade]? first before doing subjob?

#

as im getting this?

broken cloud