> For the complete documentation index, see [llms.txt](https://mole-resource.gitbook.io/mole-resource-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mole-resource.gitbook.io/mole-resource-docs/scripts/4-in-line/configuration.md).

# Configuration

Every option is in [`config.lua`](broken://pages/33a45b177e41d5eb349f12e0989c8b4126e211f2).

## Framework & item

```lua
Config.Framework = "ox"     -- "ox" | "qb" | "esx"
Config.ItemName  = "4line"  -- inventory item that places a board
```

Under `ox`, using the item is wired through the item's own `client.event` in `ox_inventory/data/items.lua` — see [Installation](broken://pages/1eb49ba71301c0ebd2168a0a3211f715e07a351d). Under `qb` / `esx` the script registers the usable item automatically.

## Interaction

```lua
Config.Target = "ox_target"    -- "none" | "ox_target" | "sleepless_interact"

Config.Controls = {
    Enter  = 38, -- E — play (PvP)
    Pickup = 47, -- G — pick up the board
    Solo   = 74, -- H — play vs AI
}
```

`Config.Target = "none"` falls back to on-foot proximity prompts using the keys above. With a target system, the same actions appear as target options and the keys are unused.

## Board size

```lua
Config.Grid = {
    Columns = 7,
    Rows    = 6,
    Connect = 4, -- discs in a row needed to win
}
```

This drives everything: the grid string length (`Columns × Rows`), the winning-line table, the AI, and how many bones the board model needs.

> ⚠️ Changing this requires a matching model (`point1` … `pointN`) **and** a wider `grid` column in the database. The shipped `mr_4inline_board` is 7×6 with 42 bones. Leave it alone unless you're authoring your own board.

Cells are numbered row-major from the top-left, so on the default board:

```
       c1   c2   c3   c4   c5   c6   c7
r1  |   1    2    3    4    5    6    7
r2  |   8    9   10   11   12   13   14
r3  |  15   16   17   18   19   20   21
r4  |  22   23   24   25   26   27   28
r5  |  29   30   31   32   33   34   35
r6  |  36   37   38   39   40   41   42   <- discs land here first
```

## Synchronization

```lua
Config.SyncDelay = 20000 -- ms after a player spawns before their boards are built
Config.Live      = true  -- true = nearby spectators see discs appear in real time
```

> Seated players always see moves immediately regardless of `Config.Live`; the flag only controls whether nearby **spectators** (players who never entered the game) get live updates.

`SyncDelay` exists so a player who just joined isn't hit with object-creation work while the game itself is still streaming the world in — boards are held back until it runs out, then requested from the server. Raise it on heavy servers where the world takes longer to load.

> The countdown restarts every time the resource restarts, not just on join. During development either lower this value or use `/4inlineresync`, which opens the gate immediately. A board placed within `Config.Streaming.InstantDistance` of you always spawns straight away, cooldown or not.

## Streaming

```lua
Config.Streaming = {
    Distance        = 150.0, -- boards within this radius (metres) are spawned
    Hysteresis      = 25.0,  -- how much further you must walk before one despawns
    Interval        = 500,   -- how often the distance check runs, in ms
    Budget          = 2,     -- max boards spawned per pass, spreads the cost over several frames
    InstantDistance = 30.0,  -- a new board this close spawns immediately, ignoring SyncDelay
}
```

Boards farther than `Distance` from the player are never spawned as game objects — only their grid state is kept in memory. Each spawned board costs 1 object plus up to 42 disc objects, so without this a server with a few hundred placed boards can exhaust the client's object pool.

* A board spawns once you're within `Distance`.
* It only despawns once you're **past `Distance + Hysteresis`** (175m by default) — the gap between the two prevents it flickering in and out while you stand near the edge.
* `Budget` caps how many boards can be built in a single pass, spreading the cost over several frames instead of stalling on a teleport into a dense area; the streamer automatically retries every 50ms until it catches up, then settles back to the normal `Interval`.
* `InstantDistance` covers the board you just placed yourself — without it you'd wait out the remaining `SyncDelay` before seeing it.
* A board you're actively playing on is never despawned by distance, however far the camera drifts.

To watch this happen, set `Config.Debug = true` and check the client console (F8) while walking away from a board — it prints `Spawned board #id (Xm away)` / `Despawned board #id (Xm away)`. `/4inlinestream` prints a live count of registered vs streamed boards and the current object-pool size.

## Timeouts

```lua
Config.EntryTimeout = 15 -- seconds the server waits for a client to confirm it entered before freeing the seat
Config.TurnTimeout  = 30 -- seconds a player has to make a move; set to 0 or nil to disable
```

`EntryTimeout` guards against a client that fails to build the board after being granted a seat (e.g. a broken model) — the seat is freed automatically so the board doesn't stay stuck forever.

`TurnTimeout` only counts down while the player on turn has their **camera locked** (Space) — i.e. while they can actually pick a column. Freely orbiting the camera with it unlocked doesn't burn their move time. Once the limit is exceeded the turn is skipped to the opponent and both players are notified.

## Gameplay & placement

```lua
Config.Distance        = 2.5                    -- interaction distance (proximity/target)
Config.Rotation        = vector3(0.0, 0.0, 0.0) -- default placement-preview rotation
Config.CellClickRadius = 0.12                   -- screen-space radius for picking a column
Config.MarkOffset      = vector3(0.0, 0.0, 0.0) -- fine XYZ correction of discs (board-local axes)
```

`MarkOffset` moves every disc at once — placed discs, the hovering preview, the drop animation and the debug view all read the same value, so they can't drift apart. Tune it live with `/4inlinebones x` while `Config.Debug = true`; it prints a ready-to-paste line.

> The seats are named `X` and `O` internally — those are the characters stored in the `grid` and `turn` database columns. `X` is the first seat (Black by default), `O` the second (White). You only meet them in the debug commands; everywhere player-facing they appear as colour names.

### Board height correction

The game's ground-snapping leaves a small gap under the board. That's cancelled by:

```lua
Config.PlaceZOffset = -0.039
```

It's applied **after** snapping, so it corrects the gap directly rather than shifting the raycast. It affects the translucent preview too, so what you see under the cursor is what gets saved. Positive raises, negative lowers.

> This offset only applies at placement time. Boards already saved in the database keep the height they were stored with — re-place them, or fix them with a one-off `UPDATE fourinline_boards SET z = z + <offset>;`.

## Column preview

While your camera is locked and it's your turn, a translucent disc in your own colour hovers above the column you're aiming at.

```lua
Config.ColumnHint = {
    Enabled = true,
    Height  = 1.5, -- how high above the board it floats
    Alpha   = 255, -- 0-255 transparency
}
```

`Height` is a multiplier of the board's own cell spacing (read from the model), so it scales automatically if you swap in a differently sized board. Lower `Alpha` if the preview is too easy to mistake for a disc already in play. `Enabled = false` removes it entirely.

## Drop animation

```lua
Config.DropAnimation = {
    Enabled = true,
    Speed   = 3.0, -- higher = faster fall
}
```

The disc is created at the top of the column and accelerates downward into its cell. With `Enabled = false` it simply appears in place.

## Camera

Pitch is the angle above the board: `0` = level with the board, `90` = straight top-down. A vertical board like this one wants a low pitch, which is why the defaults differ from a flat board game.

```lua
Config.Camera = {
    StartPitch    = 12.0,
    MinPitch      = -30.0,
    MaxPitch      = 70.0,
    MinDistance   = 1.0,
    MaxDistance   = 3.5,
    StartDistance = 1.8,
    Height        = 0.0,
}
```

## Disc colours

```lua
Config.Discs = {
    Black = "mr_4inline_diskb",
    White = "mr_4inline_diskw",
}
```

The first player to sit down plays **Black** and always moves first; the second plays **White**.

## Singleplayer (AI)

```lua
Config.AI = {
    Enabled               = true,
    Color                 = "White",  -- "White" | "Black" — the human gets the other colour
    Difficulty            = "hard",   -- "easy" | "medium" | "hard" — used when AllowDifficultyChoice is false, or as a fallback
    MoveDelay             = 600,      -- ms the AI "thinks" before playing
    AllowDifficultyChoice = true,     -- false = players can't pick a difficulty, always uses Difficulty above
}
```

`Color` is case-insensitive. Because Black always moves first, setting the AI to `"Black"` means the AI opens the game.

* `easy` — drops into a random playable column.
* `medium` — 35% random, otherwise a 4-ply search.
* `hard` — 5-ply alpha-beta search with a positional evaluation; it always takes an immediate win and always blocks yours.

Unlike a 3×3 board, a 7×6 board can't be solved exhaustively in a server tick, so `hard` is strong but **not** unbeatable — a good player can out-plan it.

Solo is entered with **H** (`Config.Controls.Solo`) or the **Play vs AI** target option, and requires a completely free board.

### Letting players pick the difficulty

When `AllowDifficultyChoice` is `true` (default), the player controls the AI's strength themselves — no admin action needed. Once inside a solo game, **as long as the board is still empty** (no disc dropped by either side yet), the same instructional-buttons bar used for "Lock camera" / "Drop disc" / "Exit" / "Hold to reset" gets an extra entry:

```
Lock camera | Drop disc | Exit | Hold to reset | 1: AI difficulty: Hard
```

Pressing **1** cycles Easy → Medium → Hard → Easy and updates live; the chosen value is validated and applied server-side immediately (`mr_4inline:setDifficulty`), so it affects the AI's very next move. As soon as the first disc is dropped (by either the player or the AI), that entry disappears and the difficulty is locked in for the rest of the match — it cannot be changed mid-game. This control is hardcoded to the `1` key (control ID 157) and isn't configurable via `Config.Controls`.

Set `AllowDifficultyChoice = false` to remove this entirely — the bar entry never appears, the server rejects any `setDifficulty` attempt, and every solo game always uses the fixed `Config.AI.Difficulty`.

## Static public boards

Boards defined here are recreated on every restart, cannot be picked up, and are not stored in the database.

```lua
Config.Boards = {
    { x = -1465.30, y = -546.51, z = 72.65, rotation = 32.0 },
}
```

## Notifications

```lua
Config.Notify = "ox_lib" -- "ESX" | "ox_lib" | "mythic_notify" | "QB-Core"
```

## Discord logging

```lua
Config.Discord = {
    Webhook = "",             -- webhook URL; empty string disables logging
    Name    = "mr_4inline",
    Logs = {
        place  = true, -- board placed
        pickup = true, -- board picked up
        admin  = true, -- 4inlinedelete used
    }
}
```

> 🔒 Never commit a real webhook. Keep it empty in the shipped config and set it only on your live server.

Log entries always include the player's license identifier (looked up across `license`, `license2`, and a full identifier scan as a last resort), plus their in-character first/last name when `Config.Framework` is `esx` or `qb` (falls back to their Rockstar/Steam name otherwise or under `ox`).

## Debug

```lua
Config.Debug = false -- enables /4inlineitem, /4inlinebones, /4inlinecoords, /4inlinestream, hover-column logging, and streaming spawn/despawn logs
```

Keep this `false` on production.

| Command                | Purpose                                                                                                |
| ---------------------- | ------------------------------------------------------------------------------------------------------ |
| `/4inlineresync`       | always available — re-requests board sync from the server                                              |
| `/4inlineitem`         | gives a free `4line` item                                                                              |
| `/4inlinebones [x\|o]` | spawns a board, labels each `point1..point42` bone, and optionally live-calibrates `Config.MarkOffset` |
| `/4inlinecoords`       | prints the world coordinates of a nearby board's bones                                                 |
| `/4inlinestream`       | prints registered vs streamed board counts and the object-pool size                                    |

Server console only: `4inlineboards` (list boards), `4inlinedelete <id> [playerId]` (force-remove a board).

## Board model & orientation

```lua
Config.Base = "mr_4inline_board" -- board model, must contain bones point1..point42

-- Orientation fix applied on top of the board's own yaw (pitch, roll, yaw).
-- Zero if the model is authored standing upright and facing forward.
Config.BoardRotationFix = vector3(0.0, 0.0, 0.0)
```

Bone numbering is row-major from the top-left — see [Installation](broken://pages/1eb49ba71301c0ebd2168a0a3211f715e07a351d#6-set-the-model-names).

## Sound

The disc-drop sound is not a `Config` option — it's a plain audio file the NUI page plays directly. Drop an `.ogg` file at `html/place.ogg` and list it in `fxmanifest.lua`'s `files{}`. Volume is set in `html/ui.js` (`sounds.place.volume`).

## Locale

Every player-facing string is in `Config.Locale`. Edit the values to translate; keep the keys unchanged. `color-black` / `color-white` are the colour names shown on the HUD, and `score-line` is the running score above the board.
