> For the complete documentation index, see [llms.txt](https://guides.moonlight.zip/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://guides.moonlight.zip/for-developers/lua-plugin-system.md).

# Lua Plugin System

Moonlight AI includes a sandboxed Lua scripting engine that lets you write custom plugins to override or extend any stage of the aim pipeline. Plugins are `.lua` files dropped into the `plugins/` folder — no compilation, no restarts.

***

### Quick Start

1. Open the **Plugins** tab in the UI
2. Click **Open Folder** to open the `plugins/` directory
3. Drop a `.lua` file in (or edit one of the included examples)
4. Click **Reload All** — your plugin appears in the list
5. Select it and check **Enable**
6. Adjust any config sliders the plugin exposes

That's it. The plugin is now live in the aim loop.

***

### How It Works

Each plugin gets its own isolated Lua runtime (powered by [LuaJIT](https://luajit.org/) via [lupa](https://github.com/scoder/lupa)). On every frame, Moonlight passes live aim data into your script and calls whichever hook functions you define. Your function receives the current values, does whatever math you want, and returns the modified result. Multiple plugins chain together — output from one feeds into the next.

```
Detection → [Filter Targets] → [Select Target] → [Aim Point]
         → [Prediction] → [Smoothing] → [Humanization] → [Output] → Mouse
                ↑               ↑             ↑               ↑
            Your plugins hook into any of these stages
```

***

### Plugin Structure

Every plugin is a single `.lua` file with four required metadata fields, an optional config table, and one or more hook functions.

#### Minimal Example

```lua
plugin_name        = "My Plugin"
plugin_description = "Does something cool"
plugin_author      = "YourName"
plugin_version     = "1.0"

function override_smoothing(cur_x, cur_y, tgt_x, tgt_y, delta_ms)
    local t = 0.15 * (delta_ms / 16.67)
    return cur_x + (tgt_x - cur_x) * t,
           cur_y + (tgt_y - cur_y) * t
end
```

#### Metadata Fields

| Field                | Required | Description                       |
| -------------------- | -------- | --------------------------------- |
| `plugin_name`        | Yes      | Display name shown in the UI      |
| `plugin_description` | Yes      | Short description of what it does |
| `plugin_author`      | Yes      | Author name                       |
| `plugin_version`     | Yes      | Version string (e.g. `"1.0"`)     |

***

### Config System

Plugins can expose sliders in the UI by defining a `config` table. Each entry automatically generates a slider with label, tooltip, min/max bounds, and a default value.

#### Defining Config

```lua
config = {
    strength = {
        type    = "number",
        default = 0.5,
        min     = 0.0,
        max     = 1.0,
        label   = "Strength",
        tooltip = "How aggressively the effect is applied"
    },
    threshold = {
        type    = "number",
        default = 50.0,
        min     = 10.0,
        max     = 200.0,
        label   = "Threshold",
        tooltip = "Distance threshold in pixels"
    }
}
```

#### Reading Config Values

Inside any hook function, read the current slider value with a fallback:

```lua
local str = config.strength.value or 0.5
```

Always provide an `or` fallback — config values sync from the UI asynchronously, and on the very first frame the value may not be populated yet.

#### Persistence

Config values are automatically saved to your active config file and restored on next launch. No extra code needed.

***

### Hook Functions

Define any combination of these functions. You only need to implement the hooks you care about — unimplemented hooks are skipped with zero overhead.

#### override\_filter\_targets(targets)

**When:** After detection, before target selection. **Purpose:** Remove, reorder, or annotate the raw detection list.

```lua
function override_filter_targets(targets)
    -- targets is a list of target tables
    -- Return the (possibly modified) list
    return targets
end
```

#### override\_select\_target(targets, current\_lock)

**When:** During target selection. **Purpose:** Override which target gets locked.

```lua
function override_select_target(targets, current_lock)
    -- Return a single target table, or nil to use default selection
    return targets[1]
end
```

#### override\_aim\_point(target)

**When:** After target is selected. **Purpose:** Calculate the exact pixel to aim at on the target (e.g. custom bone targeting).

```lua
function override_aim_point(target)
    local x = target.x
    local y = target.y - 5  -- Aim slightly above center
    return x, y
end
```

#### override\_prediction(target, aim\_x, aim\_y)

**When:** After aim point is calculated. **Purpose:** Add lead/prediction compensation for moving targets.

```lua
function override_prediction(target, aim_x, aim_y)
    local lead_x = aim_x + target.velocity_x * 0.016
    local lead_y = aim_y + target.velocity_y * 0.016
    return lead_x, lead_y
end
```

#### override\_smoothing(cur\_x, cur\_y, tgt\_x, tgt\_y, delta\_ms)

**When:** During aim smoothing. **Purpose:** Replace the built-in smoothing algorithm with your own easing curve.

| Parameter  | Description                           |
| ---------- | ------------------------------------- |
| `cur_x/y`  | Current crosshair position            |
| `tgt_x/y`  | Target position to move toward        |
| `delta_ms` | Time since last frame in milliseconds |

```lua
function override_smoothing(cur_x, cur_y, tgt_x, tgt_y, delta_ms)
    local t = 0.2 * (delta_ms / 16.67)  -- Normalize to 60fps
    t = math.min(t, 1.0)
    return cur_x + (tgt_x - cur_x) * t,
           cur_y + (tgt_y - cur_y) * t
end
```

#### override\_humanization(aim\_x, aim\_y)

**When:** After smoothing, before output. **Purpose:** Add human-like imperfection (tremor, jitter, micro-corrections).

```lua
function override_humanization(aim_x, aim_y)
    local noise_x = (math.random() - 0.5) * 0.4
    local noise_y = (math.random() - 0.5) * 0.4
    return aim_x + noise_x, aim_y + noise_y
end
```

#### override\_output(delta\_x, delta\_y)

**When:** Final stage, right before the mouse move is sent. **Purpose:** Clamp, scale, or transform the final movement vector.

```lua
function override_output(delta_x, delta_y)
    -- Example: cap maximum movement per frame
    local max_move = 50
    delta_x = math.max(-max_move, math.min(max_move, delta_x))
    delta_y = math.max(-max_move, math.min(max_move, delta_y))
    return delta_x, delta_y
end
```

***

### Aim State

Every frame, Moonlight pushes live data into the global `aim` table. This is read-only — use it to make context-aware decisions in your hooks.

```lua
-- Target info
aim.target.x              -- Target X position (pixels)
aim.target.y              -- Target Y position (pixels)
aim.target.distance       -- Distance to target (pixels)
aim.target.velocity_x     -- Horizontal velocity (px/sec)
aim.target.velocity_y     -- Vertical velocity (px/sec)
aim.target.confidence     -- Detection confidence (0.0 - 1.0)
aim.target.class_id       -- Detection class ID

-- Frame timing
aim.frame.delta_ms        -- Milliseconds since last frame
aim.frame.fps             -- Current FPS
aim.frame.time            -- Frame timestamp
aim.frame.number          -- Frame counter

-- Weapon context
aim.weapon.name           -- Current weapon name (from OCR)
aim.weapon.class          -- Weapon class ("Default", "Rifles", etc.)

-- Input state
aim.mouse.x               -- Mouse X position
aim.mouse.y               -- Mouse Y position
aim.crosshair.x           -- Crosshair X
aim.crosshair.y           -- Crosshair Y

-- Current settings
aim.settings.smooth_x     -- Active X smoothing factor
aim.settings.smooth_y     -- Active Y smoothing factor
aim.settings.fov           -- Active FOV radius
```

#### Example: Using Aim State

```lua
function override_smoothing(cur_x, cur_y, tgt_x, tgt_y, delta_ms)
    local dist = aim.target.distance
    local conf = aim.target.confidence

    -- More aggressive smoothing for high-confidence, close targets
    local t = 0.1
    if dist < 30 and conf > 0.8 then
        t = 0.3
    end

    t = t * (delta_ms / 16.67)
    return cur_x + (tgt_x - cur_x) * t,
           cur_y + (tgt_y - cur_y) * t
end
```

***

### Built-in Utility Functions

These are injected into every plugin's sandbox:

| Function                   | Description                               |
| -------------------------- | ----------------------------------------- |
| `clamp(value, min, max)`   | Clamp a number to a range                 |
| `lerp(a, b, t)`            | Linear interpolation between a and b      |
| `distance(x1, y1, x2, y2)` | Euclidean distance between two points     |
| `normalize(x, y)`          | Normalize a 2D vector to unit length      |
| `log(message)`             | Print to the Moonlight log (rate-limited) |

`log()` is rate-limited to 10 messages per second to prevent accidental log floods from per-frame logging.

***

### Standard Lua Available

The full Lua math library is available:

```lua
math.sin(x)      math.cos(x)      math.tan(x)
math.sqrt(x)     math.abs(x)      math.floor(x)
math.ceil(x)     math.min(a, b)   math.max(a, b)
math.random()    math.random(n)   math.random(m, n)
math.pi          math.huge
```

String operations (`string.len`, `string.sub`, `string.format`, etc.) and table operations (`table.insert`, `table.remove`, `table.sort`, `ipairs`, `pairs`) are also available.

***

### Sandbox Restrictions

For security, plugins run in a restricted environment. The following are **blocked**:

| Blocked              | Reason                      |
| -------------------- | --------------------------- |
| `os`, `io`           | No filesystem access        |
| `require`, `package` | No loading external modules |
| `loadfile`, `dofile` | No executing external files |
| `loadstring`, `load` | No dynamic code generation  |
| `debug`              | No runtime introspection    |
| `rawget`, `rawset`   | No bypassing metatables     |
| `string.dump`        | No bytecode extraction      |
| `collectgarbage`     | No GC manipulation          |

An **instruction limit** of 1,000,000 per frame prevents infinite loops from freezing the aim loop. If your script exceeds this, it is automatically terminated for that frame.

***

### Plugin Chaining

When multiple plugins are enabled, they execute in list order. Each hook chains — the output of one plugin becomes the input to the next.

**Example with two smoothing plugins enabled:**

```
Frame data → Plugin A: override_smoothing() → result A
          → Plugin B: override_smoothing(result A) → final result → Mouse
```

Drag plugins in the UI list to reorder them. Place coarse adjustments first, fine adjustments last.

***

### Error Handling

Plugins are fault-isolated:

* If a hook function throws an error, it is caught and logged
* After **3 consecutive errors**, the failing hook is automatically disabled (the plugin stays loaded, other hooks still run)
* A broken plugin never crashes Moonlight or affects other plugins
* Errors are visible in the plugin info panel in the UI

***

### Performance

* Hook execution time is tracked internally and visible via `get_hook_timings()`
* Each plugin runs in its own LuaJIT runtime — near-native speed
* Hooks that aren't defined are skipped entirely (no overhead)
* The instruction limit prevents runaway scripts from causing frame drops

For best performance:

* Avoid calling `log()` every frame
* Keep math simple in per-frame hooks
* Use local variables (faster than global lookups in Lua)

***

### File Structure

```
C:\MoonLightAI\
└── plugins/
    ├── example_smoothing.lua       -- Included: distance-based smoothing
    ├── example_humanization.lua    -- Included: hand tremor simulation
    └── my_custom_plugin.lua        -- Your plugins go here
```

The `plugins/` folder is created automatically on first launch. Drop `.lua` files in, click **Reload All** in the Plugins tab, and they appear immediately.

***

### Full Example: Distance Smoothing

This is the included `example_smoothing.lua` — a complete working plugin that applies slower smoothing to close targets and faster smoothing to distant ones.

```lua
plugin_name        = "Distance Smoothing"
plugin_description = "Smoother aim when close to target, faster when far away"
plugin_author      = "MoonLightAI"
plugin_version     = "1.0"

config = {
    base_smooth = {
        type    = "number",
        default = 0.15,
        min     = 0.01,
        max     = 1.0,
        label   = "Base Smooth",
        tooltip = "Base smoothing factor (higher = faster movement)"
    },
    distance_scale = {
        type    = "number",
        default = 0.5,
        min     = 0.0,
        max     = 2.0,
        label   = "Distance Scale",
        tooltip = "How much distance affects smoothing speed"
    },
    close_threshold = {
        type    = "number",
        default = 50.0,
        min     = 10.0,
        max     = 200.0,
        label   = "Close Threshold",
        tooltip = "Distance threshold for 'close' targets (pixels)"
    },
    max_smooth = {
        type    = "number",
        default = 0.5,
        min     = 0.1,
        max     = 1.0,
        label   = "Max Smooth",
        tooltip = "Maximum smoothing factor cap"
    }
}

function override_smoothing(cur_x, cur_y, tgt_x, tgt_y, delta_ms)
    local dx = tgt_x - cur_x
    local dy = tgt_y - cur_y
    local dist = math.sqrt(dx * dx + dy * dy)

    local base        = config.base_smooth.value or 0.15
    local scale        = config.distance_scale.value or 0.5
    local close_thresh = config.close_threshold.value or 50.0
    local max_t        = config.max_smooth.value or 0.5

    -- Close = slower (precise), far = faster (snap)
    local t = base
    if dist > close_thresh then
        t = base + ((dist - close_thresh) / 200.0) * scale
    end

    t = math.min(t, max_t)
    t = t * (delta_ms / 16.67)  -- Normalize to 60fps

    return cur_x + dx * t,
           cur_y + dy * t
end
```

### Full Example: Human Tremor

The included `example_humanization.lua` — simulates physiological hand tremor using layered sine waves plus random micro-jitter.

```lua
plugin_name        = "Human Tremor"
plugin_description = "Adds realistic hand tremor and micro-corrections"
plugin_author      = "MoonLightAI"
plugin_version     = "1.0"

config = {
    tremor_amount = {
        type    = "number",
        default = 0.3,
        min     = 0.0,
        max     = 2.0,
        label   = "Tremor Amount",
        tooltip = "Magnitude of hand tremor (pixels)"
    },
    tremor_speed = {
        type    = "number",
        default = 10.0,
        min     = 1.0,
        max     = 20.0,
        label   = "Tremor Speed",
        tooltip = "Tremor oscillation speed (Hz)"
    },
    noise_amount = {
        type    = "number",
        default = 0.2,
        min     = 0.0,
        max     = 1.0,
        label   = "Random Noise",
        tooltip = "Random jitter amount (pixels)"
    }
}

local time_accumulator = 0

function override_humanization(aim_x, aim_y)
    local delta = aim.frame.delta_ms or 16.67
    time_accumulator = time_accumulator + delta / 1000.0

    local tremor = config.tremor_amount.value or 0.3
    local speed  = config.tremor_speed.value or 10.0
    local noise  = config.noise_amount.value or 0.2

    -- Layered sine waves (8-12 Hz = physiological tremor range)
    local t = time_accumulator
    local tremor_x = math.sin(t * speed) * tremor * 0.6
                   + math.sin(t * speed * 1.3) * tremor * 0.4
    local tremor_y = math.cos(t * speed * 0.9) * tremor * 0.5
                   + math.cos(t * speed * 1.4) * tremor * 0.3

    -- Random micro-corrections
    local noise_x = (math.random() - 0.5) * 2 * noise
    local noise_y = (math.random() - 0.5) * 2 * noise

    return aim_x + tremor_x + noise_x,
           aim_y + tremor_y + noise_y
end
```
