---
title: Scripts and GC
nav_order: 10
---

# Scripts & GC

## getscripts

```lua
function getscripts(root: Instance?): { Instance }
```

Collects `LocalScript` / `Script` / `ModuleScript` under `root`, or DataModel if omitted. Capped — large places return a truncated list.

## getscriptbytecode

```lua
function getscriptbytecode(script: Instance): string
```

Raw bytecode bytes as a Lua string, or `""`.

## decompile

```lua
function decompile(script: Instance | string): string
```

Decompiles via in-process Fission (+ normalize for signed/RSB1). Prefer the explorer **decompile** button for big ModuleScripts — calling this from a hot script can hitch.

On failure returns a short comment string, not a Lua error.

## getscripthash / getgamename

Not implemented (Matcha has these).

## getsenv

```lua
function getsenv(script: Instance): table
```

**Stub** — always `{}`. External VM has no real script env.

## `scripts` table

All of the functions above are also grouped under a `scripts` table, same behavior:

```lua
scripts.get(root: Instance?): { Instance }        -- = getscripts
scripts.bytecode(script: Instance): string        -- = getscriptbytecode
scripts.decompile(script: Instance | string): string -- = decompile
scripts.env(script: Instance): table              -- = getsenv, stub {}
```

## GC (game Luau heaps)

Scan **Roblox** Lua states from outside (not your script's tables). Two independent capabilities live under the same `setgc`/`gc.set` name — a real GC-control mode and a key/value write mode.

```lua
getgc()              -- stats / info table
getgc(true)          -- iterator of table proxies
findgc(key)          -- iterator
findgc(key, value)
findgc(nil, value)
findgc({"A","B",...})-- one walk, map key -> array of proxies
getgc_info()
gcprobe()
getrawkeys(proxy)
```

Proxies: string-key `__index`; `__newindex` for existing number/bool keys only.

Heavy GC walks can hitch the cheat — keep samples small in tests.

### setgc — GC control mode

```lua
setgc(opt: string, value: any?): any
```

`opt` is one of the classic `lua_gc` verbs, applied to the **largest** live game Luau state:

| `opt` | Effect |
|-------|--------|
| `"stop"` | disables the GC (sets threshold to -1) |
| `"restart"` | re-enables it |
| `"count"` | returns heap size in KB |
| `"isrunning"` | returns boolean |
| `"pause"` / `"setpause"` | get/set gcpause (0–1000) |
| `"stepmul"` / `"setstepmul"` | get/set gcstepmul (0–10000) |
| `"collect"` / `"step"` | **always errors** — `"collect/step need ingame call"`, cannot be driven from outside the process |

### setgc / gc.set — key/value write mode

```lua
setgc(key: string | {string}, value: number | boolean): number
gc.set(key, value): number
```

If the first argument is **not** one of the GC verbs above (or is a table), `setgc` scans every live table in every game Luau VM for a string key equal to `key` (or any key in the array form) whose **current** value has the same type as `value` (`number` or `boolean` — no strings), and overwrites it in place. Returns how many tables were touched.

```lua
setgc("ShootCooldown", 0)          -- zero it everywhere it's found
gc.set("ShootCooldown", 0)         -- identical, table form
gc.set({"FireRate", "Damage"}, 999)
```

This is a one-shot write — if the game's own code overwrites the field again next tick, the change does not stick. For that, use `lockgc` / `gc.lock`.

### lockgc / gc.lock — persistent write

```lua
lockgc(key: string | {string}, value: number | boolean, interval_ms: number?): number
gc.lock(key, value, interval_ms?): number
```

Same key/value matching as `setgc`, but keeps re-writing the value from a background thread instead of writing once. Returns the initial hit count.

- `interval_ms` (default 100, clamped 10–5000) sets how often the background thread re-applies **all** active locks — it's a shared interval, not per-lock.
- The lock survives the end of the script that created it; it keeps running until explicitly removed or the cheat unloads.
- If a locked table stops matching (respawn, weapon switch, table got rebuilt), the lock detects the miss and **rescans** for tables with that key — but no more than once every 2 seconds, so it does not re-walk the whole heap on every tick.
- Locking the same key(s) again replaces the previous lock with the same tag instead of stacking.

```lua
lockgc("ShootCooldown", 0)                 -- keep it pinned at 0, default 100ms
gc.lock("Ammo", 999, 250)                  -- pin Ammo=999, re-apply every 250ms
gc.lock({"MinDamage", "MaxDamage"}, 9999)
```

### unlockgc / gc.unlock

```lua
unlockgc(key: string?): number   -- returns how many locks were removed
gc.unlock(key?): number
```

No argument removes **all** active locks; with a key (or the same comma-joined tag used for a multi-key lock) removes just that one.

### gc.locks()

```lua
gc.locks(): { { key: string, value: number | boolean, hits: number } }
```

Lists currently active locks: the key/tag, the pinned value, and how many table nodes are currently being held.

### `gc` table

All GC functions are also grouped under a `gc` table — same C functions, same behavior as the flat globals:

```lua
gc.set(key, value)      -- = setgc
gc.get(...)              -- = getgc
gc.find(...)             -- = findgc
gc.info()                -- = getgc_info
gc.lock(key, value, interval_ms?)   -- = lockgc
gc.unlock(key?)          -- = unlockgc
gc.locks()               -- list active locks
gc.keys(proxy)           -- = getrawkeys
gc.probe()               -- = gcprobe
```

The legacy flat globals (`setgc`, `getgc`, `getgc_info`, `findgc`, `gcprobe`, `getrawkeys`, `lockgc`, `unlockgc`) are unchanged and keep working exactly as before — `gc.*` is an additional namespaced form, not a replacement.
