> For the complete documentation index, see [llms.txt](https://tyrix.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tyrix.gitbook.io/docs/script-resources/tyrix-multi-job/configuration.md).

# Configuration

Tyrix Multijob keeps its settings in two files inside `shared/`:

* `shared/config.lua` — client + server settings. Anything a client needs to know (menu keybind, boss menu locations, wash percentages) lives here.
* `shared/config_sv.lua` — the Discord webhook settings. This one is loaded **server-side only**, so the webhook URL is never sent to a connected player.

Text lives separately, in `locales/json/en.json` — see [Locales](/docs/script-resources/tyrix-multi-job/locales.md).

All of it is loaded automatically by `fxmanifest.lua` and all of it is outside escrow, so it stays plain readable and editable after purchase. You do not need to import or require anything.

Restart the resource after editing. `config.lua` is a shared script, which means a change to it also needs the clients to reconnect or the resource to restart for them to pick it up.

## Framework

```lua
Config.Framework = 'auto' -- 'auto', 'esx', 'qbcore', 'qbox'
```

**Framework** detection runs on start and picks the bridge implementation for ESX, QB-Core or QBox. `'auto'` is right for almost every server: it looks for the framework resource and loads the matching bridge from `bridge/`.

Pin it to an explicit value when auto-detection guesses wrong. That happens on servers that have a renamed core resource, or that still have a leftover `qb-core` started alongside `es_extended` — detection checks `qbx_core`, then `qb-core`, then `es_extended`, and takes the first one that is running. Pinning also removes the detection step from the boot path, which is useful if you are chasing a start-order problem.

## Language

```lua
Config.Locale = 'en' -- 'en', 'es', 'fr'
```

English, Spanish and French ship with the resource, and adding a language is a copy-and-translate job with no code changes. Everything the resource says — notifications *and* every label in the interface — comes from that one file. See [Locales](/docs/script-resources/tyrix-multi-job/locales.md).

## Console output

```lua
Config.Debug = false
```

A healthy start prints a single `ready` line summarising the framework, language, job count, table count, and whether hour tracking and Discord logging are on. Everything else is printed only when it matters: a warning you should act on, or an error.

Turn `Config.Debug` on to also see the verbose trace — bridge initialisation, every periodic hour save, each weekly reset, and any missing locale keys. It is useful when diagnosing a problem and noisy in production, because the hour saver prints on every interval. See [Troubleshooting → Read the console first](/docs/script-resources/tyrix-multi-job/troubleshooting.md#read-the-console-first).

## Job limits

```lua
Config.maxJobs = 4                              -- Maximum jobs per player (false = unlimited)
Config.maxJobsOverrides = {                     -- Override maxJobs for specific players
    -- ["license:abc123def456789"] = 10,        -- ESX: This player can have up to 10 jobs
    -- ["ABC12345"] = false,                    -- QB: This character has unlimited jobs
}
```

`Config.maxJobs` is how many jobs a player may hold at once. Set it to `false` for no limit.

**Per-player overrides** let you sell or grant a larger cap to individual players. The table is keyed by the framework identifier, and which identifier that is matters a great deal:

| Framework      | Key to use                                       | Scope of the override                    |
| -------------- | ------------------------------------------------ | ---------------------------------------- |
| ESX            | Rockstar license, e.g. `license:abc123def456789` | Every character on that Rockstar account |
| QB-Core / QBox | `citizenid`, e.g. `ABC12345`                     | That one character only                  |

An override value may be a whole number (the new cap) or `false` (unlimited for that player). A malformed value — a negative number, a decimal, a string — is ignored with a warning printed to the server console and the player falls back to `Config.maxJobs`, so a typo cannot break the cap for everyone.

The cap is enforced on the server, in the same handler that writes the job row. A modified client that fakes the "save job" request still gets rejected, and the insert is re-counted afterwards so two near-simultaneous requests cannot slip past the check together.

When a player who is already at the cap is given a new job by some other script, the resource does not silently swallow it: the player is notified and then reverted to a job they already hold (the first in their saved list). If they have no saved jobs at all, they are set to the off-duty job instead. This matters when you script job assignment elsewhere — a player at their cap will not end up sitting in an unsaved job that the menu cannot see.

## Job menu

```lua
Config.openJobMenu = {
    enabled = true,     -- Enable/disable the job menu entirely
    command = 'jobs',   -- Command to open menu (e.g., /jobs)
    key = 'F5',          -- Keybind to open menu (set to false to disable)
}
```

Setting `enabled = false` registers neither the command nor the keybind, which is how you run the resource purely as a backend for another script's UI.

`key` takes a FiveM key name string such as `'F5'`, `'F6'` or `'K'`, or `false` to register only the command. When a key is set, the resource registers a **key mapping** rather than hard-binding the key, so players can rebind it themselves in **GTA Settings → Key Bindings → FiveM**.

{% hint style="info" %}
FiveM stores each player's binding the first time a key mapping is registered for them. Changing `key` later will not move the binding for players who have already connected once — they keep the old key until they clear it in the key bindings menu. Choose your default before launch.
{% endhint %}

Renaming `command` renames the key mapping too, because the mapping is bound to the command name. See [Commands](/docs/script-resources/tyrix-multi-job/commands.md).

## Boss menu

```lua
Config.bossMenus = {
    enabled = true,
    interactionType = 'target',           -- 'zones' (ox_lib zones), 'target' (ox_target) or 'qb-target' (qb-target)

    -- Zones mode settings (interactionType = 'zones')
    label = '[E] - Access Boss Menu',   -- TextUI prompt
    key = 38,                            -- Key to open (38 = E)
    distance = 2.0,                      -- Zone radius

    -- Target mode settings (interactionType = 'target' or 'qb-target')
    targetIcon = 'fas fa-briefcase',    -- Icon shown in the target eye
    targetLabel = 'Access Boss Menu',   -- Label shown in the target eye
    targetDistance = 2.5,               -- Target zone radius

    -- General settings
    hireRadius = 5.0,                    -- Radius for detecting nearby players when hiring
    salaryManagement = false,             -- Allow bosses to change rank salaries
    serverSideDistanceCheck = false,     -- Set false if using external boss menu systems (business creator, etc.)

    -- Money wash settings
    moneywashDefault = true,             -- Enable moneywash on all boss menus by default
    moneywashPercent = 0.90,             -- Global wash percent (0.85 = 85% return, 15% fee). Per-location percent overrides this.

    -- Money transfer accounts (cash = 'money', bank = 'bank')
    depositAccount = 'money',
    withdrawalAccount = 'money',

    locations = {
        { coords = vec3(-803.64, 168.12, 72.82), job = 'police', label = 'POLICE', moneywash = true, percent = 0.90 },
        -- { coords = vec3(-799.48, 169.90, 72.82), job = 'ambulance', label = 'EMS' },
        -- { job = 'mechanic', moneywash = true, percent = 0.85 }, -- No coords = config only for external scripts
    },

    bossGrades = {
        ['boss'] = true,
        -- ['owner'] = true,
        -- ['captain'] = true,
    },
}
```

This is the largest block in the file, so the subsections below take it a piece at a time. What the menu itself does is covered in [Boss menu](/docs/script-resources/tyrix-multi-job/boss-menu.md).

### Interaction mode

`interactionType` accepts exactly three values and nothing else. An unrecognised value creates no interaction points at all, and the menu becomes unreachable unless another resource opens it.

| Value         | How bosses open the menu                                   | Extra resource required |
| ------------- | ---------------------------------------------------------- | ----------------------- |
| `'zones'`     | Walk into the zone, a TextUI prompt appears, press the key | none beyond `ox_lib`    |
| `'target'`    | Third-eye the point and pick the option                    | `ox_target`             |
| `'qb-target'` | Third-eye the point and pick the option                    | `qb-target`             |

The settings split by mode, and settings for the mode you are not using are ignored:

| Setting          | Applies to                                                                                                                                                    |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label`          | `'zones'` — the TextUI prompt text                                                                                                                            |
| `key`            | `'zones'` — the key that opens the menu                                                                                                                       |
| `distance`       | `'zones'` — the zone radius in metres. Also feeds the server-side distance check in every mode, see [External boss menu systems](#external-boss-menu-systems) |
| `targetIcon`     | `'target'` and `'qb-target'` — Font Awesome class for the eye option                                                                                          |
| `targetLabel`    | `'target'` and `'qb-target'` — text shown in the eye                                                                                                          |
| `targetDistance` | `'target'` and `'qb-target'` — interaction radius in metres                                                                                                   |

`key` is a **GTA control id**, not a key name — the `38` shipped in the config is `E`. This is the one place in the config that uses control ids rather than key name strings, because it is read every frame while the player stands inside the zone rather than registered as a rebindable mapping. If you change it, remember to change `label` to match, since the prompt text is literal and the resource will not correct `[E]` for you.

Only locations with `coords` produce an interaction point, in every mode. Zones and target points are created once the player has loaded, and the resource removes them again when it stops.

### Access control

```lua
bossGrades = {
    ['boss'] = true,
    -- ['owner'] = true,
    -- ['captain'] = true,
}
```

**bossGrades** decides which ranks count as management. The keys match the `name` column of your framework's `job_grades` table — not the label a player sees, and not the numeric grade. This trips people up regularly, because a grade whose label is `Chief of Police` very often has the name `boss`.

Find the value you need with:

```sql
SELECT job_name, grade, name, label FROM job_grades WHERE job_name = 'police';
```

If that returns a row with `grade = 4`, `name = boss`, `label = Chief of Police`, then the key you add is `['boss'] = true`. Adding `['Chief of Police']` or `[4]` grants nobody anything.

Any number of grade names can be listed, so a job can have several management ranks. If `bossGrades` is missing entirely the resource falls back to treating the grade name `boss` as management, which keeps a broken config from locking every boss out.

Access is not checked once when the menu opens. Every sensitive action — deposits, withdrawals, hiring, firing, bonuses, rank changes, goal and salary edits, money washing — re-verifies on the server that the caller currently holds that exact job at a boss grade. A client that forces the menu open sees an interface that cannot do anything.

### Locations

Each entry in `locations` describes one boss menu point:

| Key         | Required | Purpose                                     |
| ----------- | -------- | ------------------------------------------- |
| `coords`    | no       | `vec3` position of the interaction point    |
| `job`       | yes      | Job name from your framework's `jobs` table |
| `label`     | no       | Your own reference label for the location   |
| `moneywash` | no       | Per-location override of `moneywashDefault` |
| `percent`   | no       | Per-location override of `moneywashPercent` |

`job` must match the job name exactly as it appears in the `jobs` table, because that name is also the society account name and the key used for every server-side permission check.

You can list several entries with the same `job` to give one society multiple physical offices. Each gets its own interaction point.

An entry with **no `coords`** is deliberate and supported. It creates no zone and no target option; it exists purely to hold the money wash configuration for a job whose menu is opened by some other resource — a business creator, for example. See [Integration](broken://spaces/nbepUC6poQxMqxuJkcj7/pages/d64454a88bf46d671c15b40643e268259ea7243b).

{% hint style="warning" %}
A coordless entry also satisfies the server-side distance check for that job, since there is no position to compare against. Do not add a coordless entry for a job that has a real physical office unless you intend to remove the location requirement for it.
{% endhint %}

### Funds

```lua
depositAccount = 'money',
withdrawalAccount = 'money',
```

These choose which of the player's own accounts society money moves to and from. `'money'` is cash in hand; `'bank'` is the bank balance. They are separate settings on purpose — a common setup is `depositAccount = 'money'` so takings have to be physically carried in, and `withdrawalAccount = 'bank'` so payouts land somewhere safe.

Society funds themselves live on the framework side. On ESX they are `esx_addonaccount` shared accounts named `society_<jobname>` — `society_police` for the `police` job. The resource registers a missing account the first time a society needs one, so you do not have to pre-create rows for new jobs. See [Database](/docs/script-resources/tyrix-multi-job/database.md).

Deposits and withdrawals are blocked for the last ten minutes before a txAdmin **scheduled restart**. The resource watches `txAdmin:events:scheduledRestart` and raises a global flag; the boss menu shows funds as locked and both the client and the server refuse transfers while it is set. Money in flight during a restart is the classic way society balances get lost, and this window closes that hole. The flag is cleared on resource start, so a manual restart never leaves funds stuck.

### Hiring

`hireRadius` is the distance in metres used to build the list of nearby players when a boss hires someone. Raising it makes hiring easier at busy locations and also makes it easier to hire the wrong person; `5.0` keeps the list to people actually standing at the desk.

### Salary management

`salaryManagement` ships as `false`. Turn it on and the boss menu gains a **Salaries** tab for editing the salary attached to each rank; with it off the tab is hidden and the server rejects salary edits outright.

Understand what that edit actually does before enabling it: the resource writes the new value to `job_grades.salary` and pushes the refreshed grade list to clients. **It does not pay salaries.** Your framework's own paycheck loop reads `job_grades.salary` and pays it out on its own schedule, so a change here takes effect from the next paycheck the framework issues, and it applies to every player holding that rank on that job.

Because the value is framework-wide job data rather than something scoped to this resource, handing it to players is a real economy decision. Leave it off unless you trust the bosses on your server, and enable [Discord logging](/docs/script-resources/tyrix-multi-job/discord-logging.md) if you do turn it on — salary changes are one of the logged action types.

### Money wash

```lua
moneywashDefault = true,
moneywashPercent = 0.90,
```

**Money wash** converts a player's `black_money` into clean cash at a rate you set. `moneywashPercent` is the fraction the player receives:

| `moneywashPercent` | Player washes | Player receives | Fee |
| ------------------ | ------------- | --------------- | --- |
| `0.90`             | 10,000 dirty  | 9,000 cash      | 10% |
| `0.85`             | 10,000 dirty  | 8,500 cash      | 15% |
| `0.75`             | 10,000 dirty  | 7,500 cash      | 25% |

The fee is not paid into the society account — it is destroyed. Washing is a money sink, not a transfer.

`moneywashDefault` is the on/off state for every society that does not say otherwise. Per-location keys override it:

* `percent = 0.85` on a location overrides the global percentage for that job.
* `moneywash = false` on a location disables washing for that job while every other society keeps it.

The clean amount is floored to a whole number, and a wash that would round down to zero is refused rather than burning the dirty money for no payout.

### External boss menu systems

`serverSideDistanceCheck` controls whether the server insists the player is physically at a configured boss location for every sensitive action, on top of the job and grade checks.

With it `true`, the server compares the player's position against the `coords` of every `locations` entry for that job and rejects the action unless one is within `distance + 2.0` metres. The two-metre allowance absorbs normal drift between where the client thinks the player is and where the server does.

With it `false`, the position check is skipped entirely and only the job and boss-grade checks apply.

Set it to `false` whenever another resource opens the boss menu at coordinates this config does not contain — a business creator that places offices dynamically is the usual case. Leave it `true` on a server where every boss menu is a hard-coded location here, because it is the check that stops a boss from managing their society from the other side of the map. It ships as `false` so that integrations work out of the box; tighten it if you do not need them. See [Integration](broken://spaces/nbepUC6poQxMqxuJkcj7/pages/d64454a88bf46d671c15b40643e268259ea7243b).

## Hours tracking

```lua
Config.hoursTracking = {
    enabled = true,
    weeklyResetDay = 1,         -- Day to reset weekly hours (1=Monday, 7=Sunday)
    defaultWeeklyGoal = 5.00,   -- Default weekly goal in hours
    saveInterval = 5,           -- Auto-save interval in minutes
}
```

**Hours tracking** records clocked-in time per player per job, as an all-time total and a weekly figure. With `enabled = false` the clock in and out flow still works but nothing is recorded and the hours columns disappear from the menus.

`weeklyResetDay` is the day the weekly figure returns to zero, numbered `1` for Monday through `7` for Sunday. Pick the day your server's activity is lowest, since it is the boundary bosses will be judged against.

`defaultWeeklyGoal` is the target in hours applied to any rank that has no explicit goal set in the boss menu. It is a fallback, not a cap — bosses can set a different goal per rank.

`saveInterval` is how often, in minutes, in-progress sessions are flushed to the database. It is not how often hours are counted; time is derived from the clock-in timestamp regardless. What the interval controls is how much of an in-progress session survives a crash or an unclean shutdown: at `5`, a hard crash costs a player at most five minutes of the session they were in the middle of. Lower it for more durability at the cost of more writes; raising it above ten minutes starts to be noticeable to players who lose a session.

The full model — how sessions are opened and closed, how the weekly reset catches offline and always-on players, and how goals feed the boss menu — is in [Hours tracking](/docs/script-resources/tyrix-multi-job/hours-tracking.md).

## Off-duty job

```lua
Config.offDutyJob = {
    name = 'unemployed',
    grade = 0,
}
```

This is the job a player is set to when they clock out, when they are fired, and when they have no jobs left to revert to. It must exist in your framework's `jobs` table with that grade present in `job_grades`, or clocking out will fail on the framework side.

The off-duty job is also excluded from job saving, so being off duty never adds `unemployed` to a player's job list and never consumes one of their job slots.

Routing clock-outs through a real job is what makes every other job-gated script on your server stop treating the player as employed: their framework job genuinely becomes `unemployed`, so vehicle spawners, doors, dispatch and radio all see them as off duty with no integration work on your part. If you use something other than `unemployed` — `civ`, `offduty`, whatever your framework ships — change it here and make sure your other resources treat that name as "not working".

## Job blacklist

```lua
Config.blacklist = {
    ['prisoner'] = true,
    -- ['job_name'] = true,
}
```

A player's jobs are saved **automatically**. There is no "add job" button anywhere in the UI: whenever your framework assigns a player a job by any means, this resource notices and records it. That is the whole design, and the blacklist is the escape hatch.

Any job name listed here is never written to a player's job list. The shipped example is `prisoner`, because jail scripts commonly set that job for the duration of a sentence — without the blacklist, every player who served time would keep `prisoner` as a permanent selectable job, and it would occupy one of their `maxJobs` slots forever.

Add any job that is temporary, scripted or story-driven: prison jobs, event jobs, roles a script assigns and later takes back. Keys are the job name from the `jobs` table, matched exactly.

Blacklisting a job does not stop your framework from assigning it or stop the player from doing that work. It only keeps it out of the job menu and out of the database.

## Discord logging

`shared/config_sv.lua` holds the webhook configuration. `server/logs.lua` is the only file that reads it.

```lua
Config_SV.DiscordLogs = {
    enabled = false, -- Set to true to enable logging
    webhook = '', -- Your Discord webhook URL (create one in Discord channel settings)
    ...
}
```

With `enabled = true` and a webhook URL set, boss menu actions are posted to Discord as embeds. Bot name and avatar, per-action embed colours, and per-action toggles are all configurable — see [Discord logging](/docs/script-resources/tyrix-multi-job/discord-logging.md) for the full list of logged action types and what each embed contains.

## Files you can edit

These are the non-escrowed files. Everything else is compiled and cannot be modified, so anything you need to change should be reachable from this list.

| File                   | What it is for                                                          |
| ---------------------- | ----------------------------------------------------------------------- |
| `shared/config.lua`    | Shared client + server settings — everything on this page               |
| `shared/config_sv.lua` | Server-only Discord webhook settings                                    |
| `locales/json/en.json` | All notification and interface text, for translation and rewording      |
| `client/editable.lua`  | Client-side notification handler — swap in your own notification system |

Keep your edited copies somewhere outside the resource folder before an update. Updating overwrites the whole resource, and a config you have tuned over months is not something you want to rebuild from the shipped defaults. If a setting on this page is not doing what you expect, [Troubleshooting](/docs/script-resources/tyrix-multi-job/troubleshooting.md) covers the common causes.
