> ## 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.

# Hooks

> Run your own code at key fishing moments — hide a HUD during minigames, award custom rewards on a catch, notify on rare pulls — all without touching protected logic.

## What hooks are

atlasFishing is uploaded to keymaster with **asset escrow** — the gameplay, client, and server logic is encrypted and not editable. Two files are deliberately left open so you can extend the resource:

```text theme={null}
atlasFishing/editable/client.lua   -- Fishing.Hooks (client side)
atlasFishing/editable/server.lua   -- Fishing.Hooks (server side)
```

Both are listed under `escrow_ignore` in `fxmanifest.lua`, and each is loaded **before** the rest of its side:

```lua fxmanifest.lua theme={null}
client_scripts {
    'editable/client.lua', -- user-editable hooks (Fishing.Hooks) — load first
    'client/main.lua',
    -- ...
}

server_scripts {
    'editable/server.lua', -- user-editable hooks (Fishing.Hooks) — load first
    'server/main.lua',
    -- ...
}
```

Because they load first, the `Fishing.Hooks` table always exists by the time the protected code calls into it. You edit the function bodies in these two files; you never edit anything else. An empty body is a no-op — the script checks each hook exists before calling it, so deleting a hook you don't use is also fine.

<Note>
  These hooks supersede and expand the short "Editable hooks" section on the [Configuration](/atlas-fishing/configuration#editable-hooks) page. The hook names and arguments here are the source of truth.
</Note>

## Client vs server hooks

Hooks are split by side, and each side can only do what that side can do.

<Tabs>
  <Tab title="Client — editable/client.lua">
    Runs on the player's machine. Use these for **presentation**: toggling a HUD, playing a sound, a screen effect, a local notification. They have no authority over the catch or the player's inventory.

    * `OnMinigameOpen(id)`
    * `OnMinigameClose(id, result)`
    * `OnCastStart()`
    * `OnCatch(data)`
    * `OnCancel(reason)`
  </Tab>

  <Tab title="Server — editable/server.lua">
    Runs on the server. Use these for **authority**: granting items, paying money, logging, analytics. This is the only safe place to hand out rewards.

    * `OnFishCaught(src, fish)`
    * `OnSell(src, count, value)`
  </Tab>
</Tabs>

<Warning>
  **Hooks are observers, not gates.** Every hook is fire-and-forget — the script ignores whatever you return. **Returning `false` (or a table) from any hook does nothing**: it will not cancel a cast, block a catch, or override a reward. See [Gating and canceling](#gating-and-canceling-what-hooks-cannot-do) for what to do instead.
</Warning>

### Death is already handled

When a player dies mid-attempt, the bridge's built-in `Bridge.OnDeath` fires and atlasFishing calls `Fishing.Cancel('death')`, which unwinds the cast / bite-wait / minigame cleanly (toggle with `Config.death.enabled`). You do **not** need a hook for this. If you want to *react* to that cancel — for example to make sure your HUD comes back — use the client `OnCancel(reason)` hook, where `reason` will be `'death'`.

## Hook reference

### Client hooks — `editable/client.lua`

<ParamField path="OnMinigameOpen(id)" type="function">
  Fires the instant a minigame's NUI is about to open (right after NUI focus is granted).

  <ParamField path="id" type="string">
    Which minigame: `'tension'`, `'whirlpool'`, `'castmeter'`, or `'rhythm'`.
  </ParamField>

  Return value is ignored.
</ParamField>

<ParamField path="OnMinigameClose(id, result)" type="function">
  Fires after the minigame resolves and NUI focus has been released.

  <ParamField path="id" type="string">
    The minigame that just closed (same set as above).
  </ParamField>

  <ParamField path="result" type="string">
    The outcome reported by the minigame: `'success'`, `'fail'`, `'timeout'`, or `'cancel'`. (A death/cancel resolves the minigame through its `'fail'` path.)
  </ParamField>

  Return value is ignored.
</ParamField>

<ParamField path="OnCastStart()" type="function">
  Fires once per attempt, right after the rod is equipped and just before the casting progress bar. No arguments. Return value is ignored.
</ParamField>

<ParamField path="OnCatch(data)" type="function">
  Fires on the catching player's client just before the catch presentation (the fish spawning on the ground, the flop, the pickup, the hold-up). This is *presentation only* — the item has not been granted yet; the server grants it on claim. For reward logic, use the server `OnFishCaught` hook instead.

  <ParamField path="data" type="table">
    The presentation payload:

    | Field   | Type    | Meaning                                                         |
    | ------- | ------- | --------------------------------------------------------------- |
    | `label` | string  | Display name, e.g. `"Golden Koi"`.                              |
    | `item`  | string  | Inventory item key, e.g. `"fish_koi"`.                          |
    | `tier`  | string  | `junk` · `common` · `uncommon` · `rare` · `epic` · `legendary`. |
    | `prop`  | string  | Prop model used in the presentation.                            |
    | `junk`  | boolean | `true` for junk pulls (shells/critters — no item granted).      |
  </ParamField>

  Return value is ignored.
</ParamField>

<ParamField path="OnCancel(reason)" type="function">
  Fires when an in-progress attempt is canceled via `Fishing.Cancel`.

  <ParamField path="reason" type="string">
    Currently `'death'`. Treat any other value as a generic cancel — the set may grow.
  </ParamField>

  Return value is ignored.
</ParamField>

### Server hooks — `editable/server.lua`

<ParamField path="OnFishCaught(src, fish)" type="function">
  Fires server-side **after** a real fish has been added to the player's inventory (in the `fishing:claim` callback). Does **not** fire for junk pulls. This is the correct, authoritative place to award extra rewards or log catches.

  <ParamField path="src" type="number">
    The player's server ID.
  </ParamField>

  <ParamField path="fish" type="table">
    The matching `Config.fish` entry — `key`, `label`, `tier`, `price`, `prop`, and any custom fields you added to that entry.
  </ParamField>

  Return value is ignored.
</ParamField>

<ParamField path="OnSell(src, count, value)" type="function">
  Fires server-side after a successful sale, once the money has been paid into the configured account.

  <ParamField path="src" type="number">
    The player's server ID.
  </ParamField>

  <ParamField path="count" type="number">
    Number of fish sold.
  </ParamField>

  <ParamField path="value" type="number">
    Total amount paid out.
  </ParamField>

  Return value is ignored.
</ParamField>

## Worked examples

All examples are copy-paste-ready into the matching `editable/` file. They use the global `Bridge` (provided by `atlasBridge`), which is loaded before atlasFishing, so it is always available inside your hooks.

### Hide a custom HUD during a minigame

The minigame takes NUI focus and fills the screen, so a busy HUD underneath looks cluttered. Hide it on open and restore it on close. The pattern works with `SendNUIMessage` to your own HUD resource, or with an export your HUD exposes.

<CodeGroup>
  ```lua editable/client.lua (export) theme={null}
  Fishing.Hooks = {
      OnMinigameOpen = function(id)
          exports['my-hud']:setVisible(false)
      end,

      OnMinigameClose = function(id, result)
          exports['my-hud']:setVisible(true)
      end,

      -- If the player dies during a minigame the close hook still runs, but add a
      -- belt-and-braces restore on cancel so the HUD can never get stuck hidden.
      OnCancel = function(reason)
          exports['my-hud']:setVisible(true)
      end,

      OnCastStart = function() end,
      OnCatch = function(data) end,
  }
  ```

  ```lua editable/client.lua (SendNUIMessage) theme={null}
  Fishing.Hooks = {
      OnMinigameOpen = function(id)
          SendNUIMessage({ action = 'setHudVisible', visible = false })
      end,

      OnMinigameClose = function(id, result)
          SendNUIMessage({ action = 'setHudVisible', visible = true })
      end,

      OnCancel = function(reason)
          SendNUIMessage({ action = 'setHudVisible', visible = true })
      end,

      OnCastStart = function() end,
      OnCatch = function(data) end,
  }
  ```
</CodeGroup>

<Note>
  `SendNUIMessage` targets atlasFishing's *own* NUI page, so it only works here if your HUD lives in the same resource. To talk to a separate HUD resource, use that resource's export (or `exports['my-hud']:...`) as in the first tab.
</Note>

### Award a custom item or bonus money on a catch

Use the **server** hook `OnFishCaught` — it runs after the fish is already in the player's inventory, with full server authority. Here we drop occasional bonus cash and a guaranteed extra item on legendary catches.

```lua editable/server.lua theme={null}
Fishing.Hooks = {
    OnFishCaught = function(src, fish)
        -- 10% chance of a small cash tip on any catch
        if math.random() < 0.10 then
            Bridge.AddMoney(src, 'cash', 50, 'fishing-bonus')
            Bridge.Notify(src, { title = 'Lucky!', description = 'You found $50 tucked in the catch.', type = 'success' })
        end

        -- Every legendary catch also grants a collectible item
        if fish.tier == 'legendary' then
            if Bridge.Inventory.CanCarry(src, 'fishing_trophy', 1) then
                Bridge.Inventory.AddItem(src, 'fishing_trophy', 1)
            end
        end
    end,

    OnSell = function(src, count, value) end,
}
```

<Warning>
  Only grant rewards from a **server** hook. `OnCatch` runs on the client and has no authority — anything you "give" there is cosmetic and trivially spoofable. `fishing_trophy` (and any item you add) must exist in your inventory definitions, exactly like the fish keys on the [Items & Icons](/atlas-fishing/items) page.
</Warning>

### Notify on rare catches

A simple flourish: announce only the good pulls. Use the client `OnCatch` hook so the message lands the moment the catch is presented. `data.tier` tells you the rarity; `data.junk` lets you skip junk.

```lua editable/client.lua theme={null}
local RARE_TIERS = { rare = true, epic = true, legendary = true }

Fishing.Hooks = {
    OnCatch = function(data)
        if data.junk then return end
        if RARE_TIERS[data.tier] then
            Bridge.Notify({
                title = 'Rare Catch!',
                description = ('You reeled in a %s.'):format(data.label),
                type = 'success',
            })
        end
    end,

    OnMinigameOpen = function(id) end,
    OnMinigameClose = function(id, result) end,
    OnCastStart = function() end,
    OnCancel = function(reason) end,
}
```

<Note>
  Server-side notifications take the player id first: `Bridge.Notify(src, { ... })`. Client-side they take just the payload: `Bridge.Notify({ ... })`. Mind which side your hook is on.
</Note>

### Reward sellers (server `OnSell`)

`OnSell` is handy for loyalty bonuses, taxes, or logging revenue. Here, a flat 5% bonus on top of any sale of 10+ fish:

```lua editable/server.lua theme={null}
Fishing.Hooks = {
    OnFishCaught = function(src, fish) end,

    OnSell = function(src, count, value)
        if count >= 10 then
            local bonus = math.floor(value * 0.05)
            Bridge.AddMoney(src, 'bank', bonus, 'fishing-bulk-bonus')
            Bridge.Notify(src, { title = 'Bulk Bonus', description = ('+%d for selling %d fish.'):format(bonus, count), type = 'success' })
        end
    end,
}
```

## Gating and canceling — what hooks cannot do

You may want to **require** something before a player can fish — an item, a job, a zone — and block the attempt otherwise. **Hooks cannot do this.** They are observers: `OnCastStart` fires *after* the rod is already equipped and the cast has begun, and its return value is ignored, so there is no hook that can stop a cast, reject a catch, or veto a sale.

Use the **supported, server-authoritative** levers instead:

<Steps>
  <Step title="Require an item — use bait as the gate">
    Fishing already requires the rod item and consumes a bait per attempt (rolled and removed server-side in `fishing:begin`). To require a permit or licence, make it a bait the player must own, or sell the rod only to players who qualify. This is the intended progression lever — see [Baits](/atlas-fishing/configuration#baits-config-baits) and the [supplies shop](/atlas-fishing/configuration#supplies-shop-config-shop).
  </Step>

  <Step title="Restrict zones — use multipliers, not blocks">
    `Config.zones` boost or reduce odds per tier; by design they **never gate** fishing — players can fish anywhere. Set a tier's multiplier to `0.0` to make it impossible in open water but possible only inside a zone, which is the closest supported "you must be here for X" behavior. See [Hotspot zones](/atlas-fishing/configuration#hotspot-zones-config-zones).
  </Step>

  <Step title="Gate by job/permission — do it where you control the rod">
    The cleanest job/permission gate is to control who can *obtain* the rod (e.g. only sell it to a `fisherman` job), since possessing the rod is what unlocks fishing. The bridge exposes `Bridge.GetJob(src)` and `Bridge.IsAdmin(src)` server-side if you build that check into your own shop or item-granting flow.
  </Step>

  <Step title="Soft client feedback — fine, but never trust it">
    You *can* use the client `OnCastStart` to show a message ("You need a fishing licence") — but it runs after casting begins and can't stop anything, so treat it as cosmetic only. Any real restriction must be enforced server-side as above.
  </Step>
</Steps>

<Tip>
  If you genuinely need a hard server-side cast gate (a hook that can return `false` to cancel `fishing:begin`), that is a feature request, not something the current hooks support — don't try to fake it from a client hook.
</Tip>

## Testing your hooks

<Steps>
  <Step title="Enable debug logging">
    Turn on the bridge's debug output so you can see hook-adjacent activity in the server/client consoles:

    ```cfg server.cfg theme={null}
    setr atlas:debug "true"
    ```

    (atlasFishing also has its own `Config.debug` flag in `config.lua` for extra fishing-specific output.)
  </Step>

  <Step title="Reload after every edit">
    The `editable/` files are plain scripts — changes only take effect on a resource restart:

    ```text theme={null}
    restart atlasFishing
    ```

    A `refresh` is not needed for edits to existing files, but restart the resource so the new hook bodies load.
  </Step>

  <Step title="Drive the hooks quickly">
    Use the built-in admin command to hand yourself fish and bait without grinding:

    ```text theme={null}
    /givefish fish_koi 1
    ```

    Then fish (or sell) to fire `OnCatch` / `OnFishCaught` / `OnSell`, and watch your `print` output or HUD react.
  </Step>

  <Step title="Sanity-check with prints first">
    Before wiring a real HUD or reward, drop a `print` in each hook to confirm it fires with the data you expect:

    ```lua editable/server.lua theme={null}
    OnFishCaught = function(src, fish)
        print(('[fishing] %s caught %s (%s)'):format(src, fish.key, fish.tier))
    end,
    ```
  </Step>
</Steps>
