> ## Documentation Index
> Fetch the complete documentation index at: https://docs.atlasscripts.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Framework Adapter

> Wire an unsupported framework into atlasBridge by filling the editable adapter against the normalized contract.

If your framework is not `es_extended`, `qb-core`, or `qbx_core`, you can still run every Atlas resource by filling in the **custom framework adapter**. An adapter normalizes your framework to one fixed shape, so consumer scripts call `Bridge.GetJob(src)` and `Bridge.AddMoney(src, 'bank', 500)` without ever branching on which framework you run.

This is the most important page for unsupported setups. Work through the contract once and every Atlas resource works.

<Note>
  Consumers always use `Bridge.Framework` (and its shortcuts like `Bridge.GetJob`) — never the raw `Fw*` exports the bridge generates internally. You only implement the adapter; the bridge wires the rest.
</Note>

## How it works

The bridge selects a framework **provider** with the `atlas:framework` convar and loads the matching **adapter**. When you set it to `custom`, the bridge loads **your** editable stub:

```cfg theme={null}
setr atlas:framework "custom"
```

* Server → `editable/framework/server.lua`
* Client → `editable/framework/client.lua`

Both files live outside the escrow (they are meant to be edited). If your `custom` adapter fails to load — a syntax error or a runtime error at load time — the bridge falls back to the `_default` adapter so the server still boots. That fallback is why a half-finished adapter can look like "money silently fails": the default adapter's no-op methods are running, not yours.

## The single-return rule

<Warning>
  Every adapter method returns **exactly one value**. The bridge re-exposes each method as an export, and exports cross a Lua VM boundary where multi-return is lost. Returning `value, err` will silently drop the second value. Pick one return and stick to it.
</Warning>

## Normalized shapes

Your methods must return these exact shapes so consumer scripts keep working unchanged:

```lua theme={null}
-- job
{ name = 'police', label = 'Police', grade = 2, gradeLabel = 'Sergeant', isBoss = false, onDuty = true }

-- name
{ first = 'John', last = 'Doe' }

-- money accounts are normalized to these three keys:
'cash' | 'bank' | 'dirty'
```

Map your framework's account names to `cash` / `bank` / `dirty` inside the adapter. If your framework has no dirty-money concept, return `0` for it.

## Server contract

Implement these in `editable/framework/server.lua`. Required methods must return correct data for Atlas resources to function; optional methods can return a safe no-op (`0` / `false` / `{}`) if your framework lacks the feature.

<ParamField path="GetIdentifier(src)" type="→ string" required>
  The player's unique identifier (license, char id, etc.).
</ParamField>

<ParamField path="GetPlayers()" type="→ int[]" required>
  Source ids of all currently loaded players.
</ParamField>

<ParamField path="GetName(src)" type="→ { first, last }" required>
  The player's character name.
</ParamField>

<ParamField path="GetJob(src)" type="→ job" required>
  The normalized job shape `{ name, label, grade, gradeLabel, isBoss, onDuty }`.
</ParamField>

<ParamField path="SetJob(src, name, grade)" type="→ bool" required>
  Set the player's job. Return `true` on success.
</ParamField>

<ParamField path="GetMoney(src, account)" type="→ number" required>
  Balance of `'cash' | 'bank' | 'dirty'`.
</ParamField>

<ParamField path="AddMoney(src, account, amount, reason?)" type="→ bool" required>
  Add money to an account. `reason` is an optional audit string. Return `true` on success.
</ParamField>

<ParamField path="RemoveMoney(src, account, amount, reason?)" type="→ bool" required>
  Remove money. Return `false` if the player has insufficient funds.
</ParamField>

<ParamField path="IsAdmin(src)" type="→ bool" required>
  Whether the player is a server admin.
</ParamField>

<ParamField path="GetPlayersByJob(jobName)" type="→ int[]" required>
  Source ids of online players with that job.
</ParamField>

<ParamField path="SetDuty(src, onDuty)" type="→ bool" required>
  Set the player's duty state. Return `true` on success.
</ParamField>

<ParamField path="GetSociety(job)" type="→ number" optional>
  Society (job) account balance. Return `0` if you have no society banking.
</ParamField>

<ParamField path="AddSociety(job, amount)" type="→ bool" optional>
  Add to a society account. Return `false` if unsupported.
</ParamField>

<ParamField path="RemoveSociety(job, amount)" type="→ bool" optional>
  Remove from a society account. Return `false` if unsupported.
</ParamField>

<ParamField path="RegisterUsable(item, onUse)" type="→ bool" optional>
  Register `item` as a usable item, calling `onUse(src)` when used. See [Usable items](#usable-items).
</ParamField>

## Client contract

Implement these in `editable/framework/client.lua`:

<ParamField path="GetPlayerData()" type="→ { identifier, name, job, money? }" required>
  The local player's data. `name` is `{ first, last }`; `job` is the normalized job shape; `money` is optional.
</ParamField>

<ParamField path="GetJob()" type="→ job" required>
  The local player's normalized job shape.
</ParamField>

<ParamField path="IsOnDuty()" type="→ bool" required>
  Whether the local player is on duty.
</ParamField>

## Lifecycle events you must fire

The bridge does not poll your framework. You translate your framework's events into the normalized bridge events so consumers can react. Fire these as the matching thing happens:

<Tabs>
  <Tab title="Server">
    ```lua theme={null}
    TriggerEvent('atlasBridge:playerLoaded', src)        -- player finished loading
    TriggerEvent('atlasBridge:jobChange', src, job)      -- job is the normalized shape
    TriggerEvent('atlasBridge:playerUnload', src)        -- player dropped / logged out
    ```
  </Tab>

  <Tab title="Client">
    ```lua theme={null}
    TriggerEvent('atlasBridge:client:playerLoaded')      -- local player loaded
    TriggerEvent('atlasBridge:client:jobChange', job)    -- local job changed
    ```
  </Tab>
</Tabs>

<Warning>
  Forgetting these is the most common mistake. Without `atlasBridge:playerLoaded`, Atlas resources never initialize the player; without `atlasBridge:jobChange`, job-gated features never update.
</Warning>

## Usable items

Usable-item registration lives on the **framework** adapter (not inventory), because most frameworks own item usage. Implement `RegisterUsable(item, onUse)` to register a usable with your framework that calls `onUse(src)`.

When a consumer registers a usable through `Bridge.Inventory.RegisterUsableItem`, the bridge calls your `RegisterUsable` with an `onUse` that fires a per-resource bus event:

```text theme={null}
atlasBridge:useItem:<resource>:<item>
```

You do not fire that event yourself — just make sure `onUse(src)` runs when the item is used.

## Worked example

A complete, copy-pasteable server adapter for a fictional `MyFramework`, modeled on the real `es_extended` adapter: an account map, a `normalizeJob` helper, every contract method, and the three lifecycle translations.

```lua theme={null}
-- editable/framework/server.lua
-- Enable with:  setr atlas:framework "custom"

local Fw = exports['my_framework']:GetCoreObject()

-- Map normalized accounts -> your framework's account names.
local ACCOUNT = { cash = 'cash', bank = 'bank', dirty = 'dirty_money' }

-- Build the normalized job shape from your framework's job table.
local function normalizeJob(job)
    if type(job) ~= 'table' then
        return { name = 'unemployed', label = 'Unemployed', grade = 0, gradeLabel = '', isBoss = false, onDuty = true }
    end
    return {
        name       = job.name,
        label      = job.label,
        grade      = job.grade,
        gradeLabel = job.gradeLabel or '',
        isBoss     = job.isBoss == true,
        onDuty     = job.onDuty ~= false,
    }
end

local M = {}

---@param src number @return string|nil
function M.GetIdentifier(src)
    local player = Fw.GetPlayer(src)
    if not player then return nil end
    return player.identifier
end

---@return number[]
function M.GetPlayers()
    local out = {}
    for _, player in pairs(Fw.GetPlayers()) do
        out[#out + 1] = player.source
    end
    return out
end

---@param src number @return { first:string, last:string }
function M.GetName(src)
    local player = Fw.GetPlayer(src)
    if not player then return { first = 'Player', last = '' } end
    return { first = player.firstName or 'Player', last = player.lastName or '' }
end

---@param src number @return table job
function M.GetJob(src)
    local player = Fw.GetPlayer(src)
    if not player then return normalizeJob(nil) end
    return normalizeJob(player.getJob())
end

---@param src number @param name string @param grade number @return boolean
function M.SetJob(src, name, grade)
    if type(name) ~= 'string' then return false end
    local player = Fw.GetPlayer(src)
    if not player then return false end
    player.setJob(name, grade or 0)
    return true
end

---@param src number @param account string @return number
function M.GetMoney(src, account)
    local player = Fw.GetPlayer(src)
    if not player then return 0 end
    local acc = ACCOUNT[account or 'cash']
    if not acc then return 0 end
    return player.getBalance(acc) or 0
end

---@param src number @param account string @param amount number @param reason? string @return boolean
function M.AddMoney(src, account, amount, reason)
    amount = tonumber(amount) or 0
    if amount <= 0 then return false end
    local player = Fw.GetPlayer(src)
    if not player then return false end
    local acc = ACCOUNT[account or 'cash']
    if not acc then return false end
    player.addMoney(acc, amount, reason)
    return true
end

---@param src number @param account string @param amount number @param reason? string @return boolean
function M.RemoveMoney(src, account, amount, reason)
    amount = tonumber(amount) or 0
    if amount <= 0 then return false end
    local player = Fw.GetPlayer(src)
    if not player then return false end
    local acc = ACCOUNT[account or 'cash']
    if not acc then return false end
    if (player.getBalance(acc) or 0) < amount then return false end   -- insufficient funds
    player.removeMoney(acc, amount, reason)
    return true
end

---@param src number @return boolean
function M.IsAdmin(src)
    local player = Fw.GetPlayer(src)
    if not player then return false end
    local group = player.getGroup()
    return group == 'admin' or group == 'superadmin'
end

---@param jobName string @return number[]
function M.GetPlayersByJob(jobName)
    local out = {}
    if type(jobName) ~= 'string' then return out end
    for _, player in pairs(Fw.GetPlayers()) do
        local job = player.getJob()
        if job and job.name == jobName then out[#out + 1] = player.source end
    end
    return out
end

---@param src number @param onDuty boolean @return boolean
function M.SetDuty(src, onDuty)
    local player = Fw.GetPlayer(src)
    if not player then return false end
    player.setDuty(onDuty == true)
    return true
end

-- Society accounts (optional — return 0/false if you don't use them).
function M.GetSociety(job) return 0 end
function M.AddSociety(job, amount) return false end
function M.RemoveSociety(job, amount) return false end

-- Usable items (optional). Register a usable that calls onUse(src).
---@param item string @param onUse fun(src:number) @return boolean
function M.RegisterUsable(item, onUse)
    Fw.RegisterUsableItem(item, function(src)
        onUse(src)
    end)
    return true
end

-- Lifecycle translation: your framework's events -> normalized bridge events.
AddEventHandler('my_framework:playerLoaded', function(src)
    TriggerEvent('atlasBridge:playerLoaded', src)
end)

AddEventHandler('my_framework:jobChanged', function(src, job)
    TriggerEvent('atlasBridge:jobChange', src, normalizeJob(job))
end)

AddEventHandler('playerDropped', function()
    TriggerEvent('atlasBridge:playerUnload', source)
end)

return M
```

And the matching client adapter:

```lua theme={null}
-- editable/framework/client.lua
-- Enable with:  setr atlas:framework "custom"

local Fw = exports['my_framework']:GetCoreObject()

local function normalizeJob(job)
    if type(job) ~= 'table' then
        return { name = 'unemployed', label = 'Unemployed', grade = 0, gradeLabel = '', isBoss = false, onDuty = true }
    end
    return {
        name = job.name, label = job.label, grade = job.grade,
        gradeLabel = job.gradeLabel or '', isBoss = job.isBoss == true, onDuty = job.onDuty ~= false,
    }
end

local M = {}

---@return table
function M.GetPlayerData()
    local data = Fw.GetLocalPlayer()
    return {
        identifier = data and data.identifier or nil,
        name = { first = data and data.firstName or 'Player', last = data and data.lastName or '' },
        job  = M.GetJob(),
    }
end

---@return table job
function M.GetJob()
    return normalizeJob(Fw.GetLocalPlayer() and Fw.GetLocalPlayer().getJob())
end

---@return boolean
function M.IsOnDuty()
    return M.GetJob().onDuty == true
end

-- Fire as your framework loads / changes job:
AddEventHandler('my_framework:client:playerLoaded', function()
    TriggerEvent('atlasBridge:client:playerLoaded')
end)

AddEventHandler('my_framework:client:jobChanged', function(job)
    TriggerEvent('atlasBridge:client:jobChange', normalizeJob(job))
end)

return M
```

## Test it

<Steps>
  <Step title="Enable debug">
    Add `setr atlas:debug "true"` and `setr atlas:framework "custom"`, then restart `atlasBridge`.
  </Step>

  <Step title="Confirm the adapter loaded">
    Look for `[Framework] server adapter: custom` in the console. If you instead see `_default`, your stub errored at load — fix the error and restart.
  </Step>

  <Step title="Read money and job back">
    From a test command, call `Bridge.GetJob(src)` and `Bridge.GetMoney(src, 'bank')` and confirm they return real values for a loaded player.
  </Step>

  <Step title="Buy or receive an item">
    Run a flow that removes money and grants an item, and confirm the notification, the balance change, and the item all land.
  </Step>

  <Step title="Turn debug off">
    Set `setr atlas:debug "false"` before production.
  </Step>
</Steps>

## Pitfalls

<AccordionGroup>
  <Accordion title="Money silently 'fails'">
    Usually the `custom` adapter errored at load and the bridge fell back to `_default`, whose money methods are no-ops that return `false`. Check the console for `server adapter: _default` and fix the load error.
  </Accordion>

  <Accordion title="Returning two values">
    Adapter methods are single-return — exports drop the second value. Never `return ok, err`; return one value.
  </Accordion>

  <Accordion title="Wrong job or account shape">
    Consumers read `job.name` / `job.grade` / `job.isBoss` and the accounts `cash` / `bank` / `dirty`. If your framework uses different keys, normalize them inside the adapter — do not pass the raw framework table through.
  </Accordion>

  <Accordion title="Lifecycle events not fired">
    If players never initialize or job-gated features never update, you likely forgot `atlasBridge:playerLoaded` or `atlasBridge:jobChange`. The bridge relies entirely on these.
  </Accordion>
</AccordionGroup>

<Card title="Custom inventory adapter" icon="https://mintcdn.com/atlasscripts/BwePy2Q7bMLnl0yQ/icons/box-filled.svg?fit=max&auto=format&n=BwePy2Q7bMLnl0yQ&q=85&s=c228a3b89453b292c0cedb29c252b3ab" href="/atlas-bridge/custom-inventory" width="24" height="24" data-path="icons/box-filled.svg">
  Running an unsupported inventory too? Fill the inventory adapter next.
</Card>
