> For the complete documentation index, see [llms.txt](https://docs.rainmad.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.rainmad.com/resources/3d-interactive-minigames-bundle/configuration.md).

# Configuration

Every setting lives in `cfg.lua`. The file is a `shared_script`, so both client and server see the same table. Do not put secrets in it.

***

### Hack points

#### `cfg.hacks.textUI`

```lua
cfg.hacks.textUI = '[E] Hack'
```

The prompt shown to the player when they walk within range of a hack point. Rendered through `lib.showTextUI`, so anything ox\_lib text supports works here.

***

#### `cfg.hacks.pointRadius`

```lua
cfg.hacks.pointRadius = 1.5
```

Distance in metres at which the prompt starts showing. Small values feel snappy but frustrate players who round the prop slightly. 1.5 covers most cases.

***

#### `cfg.hacks.pressKey`

```lua
cfg.hacks.pressKey = 38   -- E
```

FiveM control ID for the interact button. `38` is `E`. See the [FiveM controls list](https://docs.fivem.net/docs/game-references/controls/) for other values.

***

#### `cfg.hacks.adminAce` / `adminWhitelist`

```lua
cfg.hacks.adminAce       = 'command.create_minigame'
cfg.hacks.adminWhitelist = {
    'discord:123456789012345678',
    'license:abcdef0123456789abcdef0123456789abcdef01',
}
```

Gate for `/create_minigame`, `/remove_minigame` and `/test_minigame`.

The two mechanisms are OR'd. A player passes if the ACE matches **or** any of their identifiers is in the whitelist. Leave both empty and everyone can run the commands, which is fine for local testing and terrible for a live server.

Accepted identifier prefixes:

| Prefix      | Example                                               |
| ----------- | ----------------------------------------------------- |
| `discord:`  | `discord:123456789012345678`                          |
| `license:`  | `license:abcdef0123456789abcdef0123456789abcdef01`    |
| `license2:` | `license2:0011223344556677889900aabbccddeeff00112233` |
| `steam:`    | `steam:110000112345678`                               |
| `fivem:`    | `fivem:1234567`                                       |
| `ip:`       | `ip:203.0.113.42`                                     |

Grab yours by opening F8 and running `getplayeridentifiers`, or from txAdmin's player list.

{% hint style="warning" %}
Client-side, only ACE is checked directly. The identifier whitelist is authoritative on the server, so a player without the ACE will fall through to a server callback the first time they run the command. That is fine, it just adds a single round-trip on the first call.
{% endhint %}

***

#### `cfg.hacks.autoSave`

```lua
cfg.hacks.autoSave = true
```

When `true`, every `/create_minigame` and `/remove_minigame` rewrites `data/saved_minigames.lua`. Turn it off if you want to hand-edit that file and never have the server touch it. The commands still work, hack points just do not survive a restart.

{% hint style="danger" %}
Auto-save cannot serialise Lua functions. If a hack point has an `onSuccess.fn` or `onFail.fn`, the next auto-save drops the function and prints a warning. Use `clientEvent` or `serverEvent` instead if you need the hook to survive a restart. See Hooks.
{% endhint %}

***

#### `cfg.hacks.types`

The dictionary of hack types available to `/create_minigame`. Each key is a type name, each value is a preset. Six ship in. Adding a seventh is an entry, not a code change. Full walkthrough on Hack Types.

***

#### `cfg.hacks.onSuccess` / `cfg.hacks.onFail`

```lua
cfg.hacks.onSuccess = {
    serverEvent = 'bank:server:openVault',
    args        = { 'vault_1' },
}
```

Global hooks fired on every hack, regardless of type. Per-point overrides in `saved_minigames.lua` replace these entirely (not merged). Full field list on Hooks.

***

### Placement presets

#### `cfg.monitorPresets`

Camera + DUI settings for `placement = 'monitor'`. Each key is a preset name referenced from a hack type's `monitorPreset` field.

```lua
cfg.monitorPresets.monitor = {
    duiWidth       = 1280,
    duiHeight      = 720,
    renderDistance = 50.0,
    camOffsetX     = 0.0,
    camOffsetY     = -0.55,   -- negative = in front of screen
    camOffsetZ     = 0.35,
    camPitch       = -5.0,
    camRoll        = 0.0,
    camFov         = 45.0,
    camEaseMs      = 800,
}
```

| Field             | Meaning                                             |
| ----------------- | --------------------------------------------------- |
| `duiWidth/Height` | DUI texture resolution. Higher = crisper, more VRAM |
| `renderDistance`  | Metres beyond which the DUI stops being drawn       |
| `camOffsetX/Y/Z`  | Local offset from the prop, in metres               |
| `camPitch/Roll`   | Camera rotation in degrees                          |
| `camFov`          | Field of view. Lower = tighter framing              |
| `camEaseMs`       | Time in ms for the cam to slide in and out          |

The `tabletWall` preset also carries `renderMode = 'quad'` and a bounding-box block. That switches the renderer from `AddReplaceTexture` to a quad draw, so the tablet stays untouched instead of overwriting a texture globally. See Placements.

***

#### `cfg.propPresets.tablet`

Settings for `placement = 'prop'`. Calibrated for the shipped `rm_tablet_02`. You should not need to touch these unless you swap the tablet model.

```lua
cfg.propPresets.tablet = {
    propModel      = 'rm_tablet_02',
    bone           = 28422,               -- right hand
    attachOffset   = vec3(0.00, -0.030, 0.000),
    attachRot      = vec3(20.0, -90.0, 0.0),
    animDict       = 'amb@world_human_tourist_map@male@base',
    animName       = 'base',
    duiWidth       = 1500,
    duiHeight      = 820,
    renderDistance = 50.0,
    bbOffset       = vec3(0.040, -0.006, -0.040),
    bbScaleX       = 0.205,
    bbScaleY       = 0.120,
    bbRot          = -90.0,
    bbPlane        = 'xz',
    camOffset      = vec3(0.040, 0.180, 0.510),
    camPitch       = -49.0,
    camFov         = 29.0,
    camEaseMs      = 600,
}
```

The `bb*` block controls where the DUI quad sits relative to the prop's local origin. If the minigame draws in front of, behind, or through the tablet, this is what to nudge.

***

### Minigame difficulties

Every minigame lives in `cfg` under its own key with two shapes.

#### Shape 1: full difficulty table

```lua
cfg.untangle = {
    default = 'medium',
    difficulty = {
        easy   = { nodeCount = 6,  timeLimit = 90 },
        medium = { nodeCount = 8,  timeLimit = 60 },
        hard   = { nodeCount = 12, timeLimit = 40 },
    },
}
```

`default` is used when the caller does not pass `difficulty`. Each difficulty value is passed straight to the React component as props, so the fields listed per minigame in Minigames are the ones the game reads.

#### Shape 2: difficulty-only

```lua
cfg.rhythmArrows = { default = 'medium' }
```

The React component owns its own tunables and only the difficulty string crosses the bridge. Change `default` to shift the baseline, or override at the callsite with `opts.difficulty = 'hard'`.

***

### Overriding at the callsite

Every field in a difficulty preset can be overridden per-call:

```lua
exports['rm_3dminigames']:untangle({
    difficulty = 'hard',
    timeLimit  = 20,   -- overrides the 40 from cfg.untangle.difficulty.hard
})
```

Anything not listed in the preset that the React component reads also works, provided the component reads it. This is the escape hatch for one-off tuning without editing `cfg.lua`.
