No API entries match your search.

Getting started

An addon is a folder of Lua that lives under addons/. Every .lua file inside addons/<name>/lua/autorun/ is run once at startup, in alphabetical order. A downloaded mod arrives instead as a single addons/<name>.ccmod file — a plain ZIP, mounted in place and never unpacked, so it stays obvious which mods are yours (folders) and which came from the site (packages). Worker scripts load from inside the package too; a loose folder with the same name out-ranks a package of that name.

layoutaddons/<modname>/lua/autorun/*.lua

All addons run in one shared sandbox: a global you set in one addon is readable in every other addon, functions can be called across mods, and hook events fired by one mod reach listeners in another. Only two things are per-addon: data (your private storage folder) and MOD_NAME (your folder name).

A minimal addon — a pinned FPS-style HUD plus a button:
-- addons/hello/lua/autorun/hello.lua
log("hello loaded as " .. MOD_NAME)

-- A free-form HUD element, always on screen (mode = "pinned")
overlay.add("hello.clock", {
    mode = "pinned",
    paint = function(d)
        d:rect(d:w() - 150, 16, 130, 34, 0, 0, 0, 150)
        d:text(d:w() - 138, 24, os.date("%H:%M:%S"), 20, 255, 255, 255)
    end,
})

-- A button in the overlay menu (Shift+Tab)
overlay.add("hello.hi", {
    mode = "menu",
    button = { label = "Say hi", onClick = function() log("hi!") end },
})

Globals

Functions and values available directly in every addon.

functionscreenshot(path?, x?, y?, w?, h?) → nil

Queues a PNG capture of the current video frame (overlay UI is not included). Fulfilled at the end of the frame on a background thread, so it never stalls rendering. A relative path is resolved against the app's screenshots/ folder; omit it for an auto-named file. Pass all four of x, y, w, h to capture only that pixel region instead of the whole frame.

ParamTypeNotes
pathstring (optional)Output path, e.g. "screenshots/shot.png". Omit or pass nil for an auto-named file.
x, ynumber (optional)Top-left corner of the region to capture, in pixels.
w, hnumber (optional)Region width & height, in pixels. Region is used only if all four of x, y, w, h are supplied; otherwise the whole frame is captured.
hook.Add("KeyPress", "mymod", function(key)
    if key == "F12" then
        screenshot("screenshots/grab_" .. os.time() .. ".png")   -- whole frame
    elseif key == "F11" then
        screenshot("screenshots/hud.png", 20, 20, 260, 90)       -- just a region
    end
end)
functionlog(msg) → nil

Writes a line to the console/log stream, prefixed with [mod]. Handy for debugging.

ParamTypeNotes
msgstringThe message to log.
fieldMOD_NAMEstring

The addon's folder name. Also the name of your private data folder (data/<MOD_NAME>/).

functionperf.latencyMs() → number

Current frame-to-screen latency in milliseconds (a smoothed average) — from when the OS timestamps a received frame to when it's presented. This is the software path only; it does not include the capture card + USB latency. Handy for testing your mod's rendering cost. Takes no parameters.

functionperf.fps() → number

Frames presented in the last second. Takes no parameters.

functionperf.nowMs() → number

Milliseconds since the app started — the same clock the frame loop runs on. Use it for anything that has to fit inside a frame; os.clock measures CPU time and is too coarse. Takes no parameters.

hook

GMod-style event system. Register named callbacks for engine events; the name lets you replace or remove your own hook later.

functionhook.Add(event, name, fn)

Registers fn for event under a unique name. Adding again with the same event + name replaces the previous callback.

ParamTypeNotes
eventstringEvent name, e.g. "OverlayElement".
namestringUnique identifier for this callback.
fnfunctionCalled with the event's arguments.
functionhook.Remove(event, name) → nil

Unregisters the callback previously added under this event + name.

ParamTypeNotes
eventstringThe event name.
namestringThe unique identifier used in hook.Add.
functionhook.Run(event, ...) → nil · alias: hook.Call

Fires an event, calling every registered callback with the given arguments. The engine uses this to dispatch events, but you can also define and fire your own custom events between mods.

ParamTypeNotes
eventstringThe event name to fire.
...anyAny number of arguments, passed through to each callback.

Engine events

Events the engine fires into hook. Subscribe with hook.Add.

event"OnVideoResized" → your callback receives (w, h)

The incoming video changed size — a different capture mode, a different console output setting, a stream restart. Fired before the first frame of the new size reaches OnFrameReceived, so anything you measured against the old frames can be thrown out in time. Note this is about the video, not the app window: resizing the window never moves a pixel of frame space, so there is deliberately no event for it.

event"KeyPress" → your callback receives (key)

Fired on key-down while the overlay is closed (when it's open, keys drive the UI instead).

Callback argTypeNotes
keystringThe key's display name, e.g. "P", "Space", "F1".
hook.Add("KeyPress", "screenshotter", function(key)
    if key == "P" then screenshot("screenshots/" .. os.time() .. ".png") end
end)
event"OverlayElement" → your callback receives (el)

Fired once per element, every frame, just before it is drawn — for every element, native ones included. Mutate the passed element table to restyle, rename, hide (el.hidden = true), or re-mode anything on the overlay.

Callback argTypeNotes
elElement (table)The live Element being drawn. Its el.id tells you which one; mutating it takes effect this frame.
hook.Add("OverlayElement", "myskin", function(el)
    if el.id == "options" then el.button.label = "Settings" end
    if el.id == "close_overlay" then el.hidden = true end
end)
event"OnFrameReceived" → your callback receives (frame)

Fired for every captured video frame, before it is displayed. Mutate the frame's pixels in place (through the Frame object) to alter what's shown — no return value needed. Handlers run in turn; if one returns a non-nil value, the remaining handlers are skipped for that frame.

Callback argTypeNotes
frameFrameA read/write view of the raw pixel buffer. See Frame.
Performance: this fires ~60×/second and byte access crosses into C each call, so heavy per-pixel loops in Lua are costly. Touch as few bytes as you can, and check frame:format() — capture frames are usually NV12 (planar YUV), not RGBA.
hook.Add("OnFrameReceived", "dim-top", function(frame)
    local pitch = frame:pitch()
    for row = 0, 40 do                 -- NV12: Y plane is the first rows
        for col = 0, frame:width() - 1 do
            frame:setByte(row * pitch + col, 16)   -- Y=16 -> black band
        end
    end
end)

Frame

A read/write view of a raw captured frame, passed to OnFrameReceived. Byte access is 0-based into the pixel buffer, whose layout depends on format() — capture frames are typically NV12 (a full-size Y/luma plane, then an interleaved half-size UV plane).

methodframe:width() → number

Frame width in pixels. Takes no parameters.

methodframe:height() → number

Frame height in pixels. Takes no parameters.

methodframe:pitch() → number

Bytes per row of the first (luma) plane. Index a pixel's row with row * pitch() + col. Takes no parameters.

methodframe:format() → string

The pixel format name, e.g. "SDL_PIXELFORMAT_NV12". Takes no parameters.

methodframe:size() → number

Total pixel-buffer size in bytes — the valid range for byte indices. Takes no parameters.

methodframe:getByte(i) → number

Reads one byte of the pixel buffer.

ParamTypeNotes
inumber0-based byte index (0 .. size()-1). Out of range returns 0.
methodframe:region(x, y, w, h) → string

Read a rectangle in one call. Returns w*h bytes, row-major — index it with string.byte. Anything outside the frame comes back as 0 rather than being refused.

ParamTypeNotes
x, ynumberTop-left, in bytes across and rows down the pixel buffer.
w, hnumberSize. Each is capped at 4096.
Use this, not a loop of getByte. Every call from Lua into the engine costs about 1.6µs whatever it carries, so the price of reading an area is its pixel count: one 150×38 window was 5700 calls and about 9ms — most of a frame at 60fps. The same window read a row at a time is 38 calls, and the per-pixel work stays in Lua where it's cheap. Reading the Pokémon mod's windows this way took it from 51fps back to 60 while scanning four times as often.
-- one row of the luma plane, sampled every 6th pixel
local row = frame:region(x0, y, 600, 1)
for i = 1, 600, 6 do
    local luma = row:byte(i)
end
methodframe:setByte(i, v) → nil

Writes one byte of the pixel buffer — this is how you change what's displayed.

ParamTypeNotes
inumber0-based byte index. Out of range is ignored.
vnumberByte value, 0–255.

browser

Show a web page to the user. This is deliberately a command, not a mechanism: your mod says "show this", and the engine decides how. Today it hands the address to the system browser; if the overlay later grows an embedded view, every mod calling this follows automatically with no change. Don't try to open pages any other way.

functionbrowser.open(url) → boolean

Open a web page. Returns true if it was handed off successfully.

ParamTypeNotes
urlstringMust begin http:// or https://. Anything else — a file:// path, a program name, a custom scheme — is refused and returns false, so this can't become a way out of the sandbox. Percent-encode anything exotic in the address yourself.
browser.open("https://bulbapedia.bulbagarden.net/wiki/Pikachu_(Pok%C3%A9mon)")

game

Attach a mod to one or more games. Every registered game becomes a cartridge in the row along the bottom of the overlay; only the selected game's mods run, and everything else lies dormant. Hovering a cartridge fans out the mods registered for it, and picking one opens that mod's settings.

functiongame.register(def) → GameMod

Register this mod against a game (or several) and get back the object you hang your callbacks and settings on. Pass a handle string, one definition table, or an array of them — an array means the same mod serves several games (e.g. FireRed and LeafGreen).

Field of a definitionTypeNotes
handlestringStable id for the game, e.g. "pkmn_firered". Two mods using the same handle share one cartridge.
namestring?Shown under the cartridge. Defaults to the handle.
imagestring?Path to cartridge art, relative to the app folder (e.g. "addons/"..MOD_NAME.."/cart.png"). Falls back to the name if it can't be loaded.
local g = game.register({
    { handle="pkmn_firered",   name="Pokemon FireRed",   image="addons/"..MOD_NAME.."/fr.png" },
    { handle="pkmn_leafgreen", name="Pokemon LeafGreen", image="addons/"..MOD_NAME.."/lg.png" },
})
fieldGameMod.Thinkfunction(self)

Overridable: called once per frame, but only while one of your games is selected. Dormant mods cost nothing.

fieldGameMod.OnFramefunction(self, frame)

Overridable: called with each captured Frame while your game is selected — the gated version of OnFrameReceived. Use it to read or alter pixels only when your game is actually on.

fieldGameMod.OnActivate / OnDeactivatefunction(self)

Overridable: your game was just selected / deselected. Switching between two games that both belong to your mod does not fire these — you stay active.

fieldGameMod.LateUnreliableThinkfunction(self)

Overridable: for work too heavy to finish inside one frame. Offered every frame, but a call that hasn't returned yet keeps running instead of a new one starting — see Threading for what that costs you and how to yield.

methodg:addSetting(key, label, kind, options?, default?) → GameMod

Declare a setting. You only say what it's called, how it should be shown, and what kind of input it is — the engine owns everything else: it stores the value, remembers it between runs, validates it, and builds the settings menu. There is nothing to draw and nothing to save.

ParamTypeNotes
keystringHow you read it back with g:get(key).
labelstringShown in the settings menu.
kindstring"bool" (yes/no), "choice" (pick one), or "text".
optionsstring[]?The choices, for "choice". Values outside this list are rejected.
defaultany?Used until the user changes it. Defaults to true for bool, the first option for choice, empty for text.
g:addSetting("show_stats", "Show Pokemon stats", "bool", nil, true)
g:addSetting("route_info", "Show route information", "choice",
             { "on", "off", "overlay only" }, "on")

if g:get("show_stats") then ... end
methodg:get(key) → boolean | string | nil

Current value of one of your settings — a boolean for "bool", otherwise a string. nil if you never declared that key.

methodg:set(key, value) → nil

Change a setting yourself. Saved immediately; invalid values for a choice are ignored.

functiongame.active() → string | nil

Handle of the selected game, or nil if none is. Takes no parameters.

functiongame.isActive(handle) → boolean

Whether that particular game is the selected one — handy when your mod serves several and needs to tell them apart.

functiongame.select(handle) → nil

Select a game as if its cartridge had been clicked. Fires the relevant OnDeactivate/OnActivate. The choice is remembered and restored next launch.

functiongame.list() → table[]

Every registered game in registration order, as { handle, name, image }. Takes no parameters.

functiongame.gamesOfMod(modName) → string[]

The handles of the games a given mod registered, e.g. game.gamesOfMod("pkmn_stats"){"pkmn_firered", "pkmn_leafgreen"}. Used by the engine's upload form to fill itself in from the mod rather than asking you to retype what the mod already declared — handles rather than display names, because the site matches on internal names that look like handles.

ParamTypeNotes
modNamestringAn addon folder name — the same value as MOD_NAME.

camera

Read-only. Everything the capture card reports about itself — its name, the live frame format, and every format it advertises — plus one thing the user tells us: source(). A USB capture card exposes nothing about the game, the source console, or signal presence, so anything content-aware must come from the pixels (via OnFrameReceived); the source label is the one exception, and it's user-declared, not detected. The table is locked: assigning to it or replacing its metatable raises an error.

functioncamera.isOpen() → boolean

Whether a capture device is currently open and streaming. Takes no parameters.

functioncamera.name() → string | nil

The device's name, e.g. "UGREEN-25854" — the card's USB name, not the console or game. nil if no device is open. Takes no parameters.

functioncamera.source() → string | nil

The user-declared label for whatever HDMI device is plugged into this card, e.g. "Nintendo Switch". The card cannot detect this — the user types it in Options → Connected device, and it's remembered per card name. nil until they set it. Useful for source-specific mods: if camera.source() == "Nintendo Switch" then …. Takes no parameters.

functioncamera.width() → number

Width in pixels of the live frame format (0 if none). Takes no parameters.

functioncamera.height() → number

Height in pixels of the live frame format (0 if none). Takes no parameters.

functioncamera.format() → string | nil

Pixel format name of the live frame, e.g. "SDL_PIXELFORMAT_NV12". Matches frame:format(). nil if none. Takes no parameters.

functioncamera.fps() → number

Frame rate of the live format in frames per second (0 if unknown). Takes no parameters.

functioncamera.colorspace() → number

The frame colorspace as SDL's numeric SDL_Colorspace enum value (0 if none). Niche — most mods won't need it. Takes no parameters.

functioncamera.formats() → table[]

Every format/resolution/framerate the device advertises, as an array of tables. A fresh copy each call — safe to modify. Takes no parameters.

Field of each entryTypeNotes
width, heightnumberResolution in pixels.
fpsnumberFrame rate for this combination.
formatstringPixel format name, e.g. "SDL_PIXELFORMAT_MJPG".
if camera.isOpen() then
    log(camera.name() .. ": " .. camera.width() .. "x" .. camera.height()
        .. " " .. camera.format() .. " @ " .. camera.fps() .. " fps")
    log("advertises " .. #camera.formats() .. " formats")
end

account

Read-only. Who is signed in to their dasmaffin.com account, if anyone. Signing in is the engine's job — there is a Sign in button on the overlay — and a mod cannot start, end, or inspect a session beyond the two properties here. The access and refresh tokens are not reachable from Lua at all: the sandbox is shared between every mod, so exposing them to one would expose them to all. The table is locked: assigning to it or replacing its metatable raises an error.

It is one account across the whole thing: the same one signs in on the website and here, and a person may have several ways in to it (a password, Steam) that all lead to the same id.
functionaccount.signedIn() → boolean

Whether somebody is signed in right now. Everything else here is nil when this is false. Takes no parameters.

functionaccount.name() → string | nil

Their display name, for showing — e.g. greeting them in a HUD. Not an identity: it is user-chosen, changeable, and not unique. nil when signed out or unset. Takes no parameters.

functionaccount.id() → number | nil

The account id, and the only identity there is — not the email address, since one account can have several logins and the address is merely one of them. Key any per-user state a mod stores on this. nil when signed out. Takes no parameters.

-- remember something per signed-in user
local id = account.id()
if id then data.write("prefs_" .. id .. ".txt", "...") end

overlay

The native Steam-style overlay (toggled with Shift+Tab) and its element registry. Everything shown on the overlay — buttons, panels, HUDs — is an Element.

functionoverlay.add(id, spec) → Element

Registers an element under a unique string id and returns the element table (the same spec, now with .id set). Adding with an existing id replaces it.

ParamTypeNotes
idstringUnique; namespace it, e.g. "mymod.panel".
specElementSee Element.
functionoverlay.remove(id) → nil

Removes the element registered under id. Does nothing if no such element exists.

ParamTypeNotes
idstringThe element's unique id (the same string you passed to overlay.add).
functionoverlay.get(id) → Element | nil

Returns the registered element table for id. Mutating the returned table changes the element live.

ParamTypeNotes
idstringThe element's unique id.
returnsThe Element table, or nil if not found.
functionoverlay.toggle() → nil

Toggles the overlay open/closed (same as Shift+Tab). Takes no parameters.

functionoverlay.open() → nil

Opens the overlay. Takes no parameters.

functionoverlay.close() → nil

Closes the overlay (and clears any text-field focus). Takes no parameters.

functionoverlay.isOpen() → boolean

Whether the overlay is currently open. Takes no parameters. Returns a boolean.

functionoverlay.setUserMode(id, mode) → nil

Sets a user override for an element's visibility mode. Ignored for elements whose lockMode is true.

ParamTypeNotes
idstringThe element's unique id.
modestringOne of "menu", "pinned", "hud" (see Element).
functionoverlay.getUserMode(id) → string | nil

Returns the user override mode for an element.

ParamTypeNotes
idstringThe element's unique id.
returnsstring mode, or nil if none is set.
functionoverlay.focus(elemId, childIdx) → nil

Gives keyboard focus to a textbox widget. Useful right after creating an element so the user can type immediately.

ParamTypeNotes
elemIdstringThe id of the panel element that holds the textbox.
childIdxnumber1-based index of the textbox within that panel's children.
Internal: overlay._elements (id → element) and overlay._order (draw order) back the registry. Prefer add/get/remove over touching them directly.
functionoverlay.screenW() → number

Width of the window in pixels — use it to centre or right-align things. Takes no parameters.

functionoverlay.screenH() → number

Height of the window in pixels. Takes no parameters.

functionoverlay.mouseX() → number

Pointer X in screen pixels, or -1 until the mouse has moved once. Lets an element work out where inside itself the cursor is — which row of a list it is over, say — rather than only that it is somewhere inside. Still reported while the pointer is auto-hidden. Takes no parameters.

functionoverlay.mouseY() → number

Pointer Y in screen pixels, or -1 until the mouse has moved once. Takes no parameters.

Element

The table you pass to overlay.add. Every element has an id, a visibility mode, and exactly one visual form: a button, a paint callback, or a panel of widgets.

fieldmodestring = "menu"

Controls when the element is visible:

ModeOverlay openOverlay closed
"menu"✓ shown— hidden
"pinned"✓ shown✓ shown
"hud"— hidden✓ shown
fieldlockModeboolean = false

When true, the element's mode is forced and overlay.setUserMode is ignored for it. When false, the user may override the mode.

fieldhiddenboolean

Reset to false each frame, then read after the OverlayElement hook — set it there to hide the element for that frame.

fieldpassThroughboolean = false

Mouse behaviour of the element's rectangle. false (default): the element consumes mouse events over its bounds — it receives the callbacks below, and elements beneath it get nothing. true: the element (including its widgets) ignores the mouse entirely and events fall through to the next element under the cursor. Elements are tested topmost-first (last drawn = on top), so the highest non-passThrough element under the cursor is always the one that receives events.

fieldhoveredboolean

Maintained by the engine: true while this element is the current mouse-hover target, false otherwise. Read-only in spirit — useful inside paint to restyle on hover without writing callbacks.

fieldOnHoverEnterfunction(self)

Overridable callback (GMod-panel style; nil = no-op): called once when the mouse moves onto this element.

Callback argTypeNotes
selfElement (table)The element itself.
fieldOnHoverExitfunction(self)

Overridable callback: called once when the mouse leaves this element (or another element takes the hover).

Callback argTypeNotes
selfElement (table)The element itself.
fieldOnMouseDownfunction(self, x, y, button)

Overridable callback: a mouse button was pressed on this element (and no child widget took the click — widgets inside a panel get first crack, left button only).

Callback argTypeNotes
selfElement (table)The element itself.
x, ynumberPress position in pixels, relative to the element's top-left corner.
buttonnumber1 = left, 2 = middle, 3 = right.
fieldOnMouseUpfunction(self, x, y, button)

Overridable callback: the button was released. Sent to the element that received the OnMouseDown (capture semantics — a drag that ends outside the element still notifies it).

Callback argTypeNotes
selfElement (table)The element itself.
x, ynumberRelease position in pixels, relative to the element's top-left corner.
buttonnumber1 = left, 2 = middle, 3 = right.
Bounds required: mouse events use the element's x, y, w, h. Panels always have them; a paint element only takes part if you give it explicit bounds. Pinned / hud elements receive mouse events even while the overlay is closed.
local pnl = overlay.add("mymod.box", {
    mode = "pinned", x = 40, y = 40, w = 180, h = 90,
    children = { { type = "label", x = 10, y = 10, text = "drag me" } },
})
function pnl.OnMouseDown(self, x, y, button) log("down at " .. x .. "," .. y) end
function pnl.OnMouseUp(self, x, y, button)   log("released")                  end
function pnl.OnHoverEnter(self) self.style.border = {120,200,255,255} end
function pnl.OnHoverExit(self)  self.style.border = {200,200,215,255} end
fieldbuttontable { label, onClick }

Makes the element an auto-laid-out button in the centered overlay menu column. Button elements are ordinary elements underneath: they inherit passThrough, hovered, and the OnHoverEnter / OnHoverExit / OnMouseDown / OnMouseUp callbacks, and the engine writes the computed x, y, w, h back onto the element each frame so you can read where the button ended up.

FieldTypeNotes
labelstringButton text.
onClickfunctionCalled (no arguments) on left-click, after the element's own OnMouseDown.
fieldpaintfunction(d)

Makes the element a free-form drawing. Called every frame it's visible with a DrawContext d. Paint elements have no built-in input handling.

fieldchildrenWidget[]

Makes the element a panel: a styled box at x, y, w, h containing native Widgets (positioned relative to the panel). Requires x, y, w, h; accepts a style.

FieldTypeNotes
x, y, w, hnumberPanel position & size, in screen pixels.
styletable?{ bg = {r,g,b,a?}, border = {r,g,b,a?} }
childrenWidget[]Array of Widgets.

Widgets

Native, styleable controls placed inside a panel element's children. Colors are Lua arrays {r, g, b} or {r, g, b, a} (0–255); font is the text height in pixels. All positions are relative to the parent panel.

Every widget type accepts hidden = true, which skips both its drawing and its click area. Use it to show different rows in different states rather than rebuilding children — a rebuild throws away whatever is half-typed into a textbox.
widgettype = "label"

Static text. One line at a point by default; give it w and h and it wraps at word boundaries inside that box instead, clipping when it runs out of height. Size h for the longest text the label can be given — at font f each line costs f + 3, and the last one fits while it starts at or before h - f. Undersize it and the tail vanishes with nothing to show for it; the engine logs label doesn't fit its w/h to the developer console when that happens.

FieldTypeNotes
x, ynumberPosition within the panel.
w, hnumber?Optional. Both set = wrap inside this box. Omit for a single unwrapped line.
textstringThe text to show. \n starts a new line when wrapping.
hiddenboolean?Skip drawing entirely.
styletable?{ fg = {…}, font = number }
widgettype = "button"

A clickable button with a hover highlight.

FieldTypeNotes
x, y, w, hnumberPosition & size within the panel.
textstringLabel, centered.
onClickfunctionCalled on left-click.
styletable?{ bg, bgHover, border, fg, font }
widgettype = "textbox"

An editable text field. Click it (or call overlay.focus) to focus; the engine handles typing, the blinking caret, and editing keys natively. Read/write its current contents via the text field.

FieldTypeNotes
x, y, w, hnumberPosition & size within the panel.
textstringCurrent contents (updated live as the user types).
placeholderstring?Shown dimmed when empty and unfocused.
multilineboolean?If true, Enter inserts a newline; otherwise Enter defocuses.
passwordboolean?Draw one * per character instead of the text. text still holds the real value.
onChangefunction?Called with the new text whenever it changes.
hiddenboolean?Skip drawing and hit-testing entirely.
styletable?{ bg, fg, border, font }
Editing keys: Backspace deletes, Ctrl+V pastes (appended, capped at 4096 characters), Tab jumps to the next textbox in the same panel (wrapping round, skipping hidden ones), Enter = newline (multiline) or defocus, Esc = defocus. Tab order is the order the widgets appear in children. Only textboxes take focus — buttons can't be pressed from the keyboard, so tabbing onto one would be a dead end. Text input requires the app window to have OS keyboard focus.
There is no copy or cut, on purpose: the field most in need of pasting is a password one, and putting a password back onto the clipboard is a step in the wrong direction. Pasted control characters are dropped, and newlines survive only in a multiline box.
Use password = true for any secret. This app exists to put a screen in front of other people, and often a recording of it — a visible password field is on camera by definition. Masking is the engine's job, so don't substitute your own characters into text: you'd break what the user is actually typing.
widgettype = "combo"

A dropdown. Shows the selected option; clicking opens a list of options (drawn on top of everything), and picking one collapses it.

FieldTypeNotes
x, y, w, hnumberPosition & size of the collapsed box within the panel.
optionsstring[]The list of choices (a Lua array of strings).
selectednumber1-based index of the current choice. Updated for you when the user picks.
onSelectfunction?Called as onSelect(index, value) when a choice is made.
styletable?{ bg, fg, border, font }
Lists longer than 12 options are paged, with a < 3/17 (196) > row under the list — click either half to turn the page. Opening the dropdown jumps to the page holding the current selection. You don't have to do anything for this; hand it as many options as you like.
widgettype = "slider"

A draggable value bar. Click or drag the track to set the value.

FieldTypeNotes
x, y, w, hnumberPosition & size within the panel.
min, maxnumberValue range. Default 0 and 1.
valuenumberCurrent value. Updated for you as the user drags.
onChangefunction?Called as onChange(value) while dragging.
styletable?{ bg, fill, border }
A panel with a label, a textbox, and a button:
overlay.add("mymod.form", {
    mode = "menu", x = 40, y = 60, w = 260, h = 120,
    style = { bg = {30,32,42,235}, border = {120,130,160,255} },
    children = {
        { type = "label", x = 12, y = 10, text = "Name:",
          style = { fg = {230,230,240}, font = 16 } },
        { type = "textbox", x = 12, y = 34, w = 236, h = 30,
          placeholder = "type here",
          onChange = function(t) log("name = " .. t) end },
        { type = "button", x = 12, y = 74, w = 100, h = 32, text = "OK",
          onClick = function() overlay.close() end,
          style = { bg = {56,60,78}, bgHover = {74,80,104} } },
    },
})

DrawContext

The drawing surface d passed to an element's paint callback. Pixel coordinates; colors are separate r, g, b arguments (0–255) with optional a (default 255). Use d:w() / d:h() for responsive layout, like GMod's ScrW() / ScrH().

methodd:w() → number

Current screen width in pixels.

methodd:h() → number

Current screen height in pixels.

methodd:rect(x, y, w, h, r, g, b, a?) → nil

Fills a rectangle.

ParamTypeNotes
x, ynumberTop-left corner, in pixels.
w, hnumberWidth & height, in pixels.
r, g, bnumberColour channels, 0–255.
anumber (optional)Alpha 0–255. Defaults to 255 (opaque).
methodd:outline(x, y, w, h, r, g, b, a?) → nil

Draws a 1px rectangle outline.

ParamTypeNotes
x, ynumberTop-left corner, in pixels.
w, hnumberWidth & height, in pixels.
r, g, bnumberColour channels, 0–255.
anumber (optional)Alpha 0–255. Defaults to 255.
methodd:line(x1, y1, x2, y2, r, g, b, a?) → nil

Draws a line between two points.

ParamTypeNotes
x1, y1numberStart point, in pixels.
x2, y2numberEnd point, in pixels.
r, g, bnumberColour channels, 0–255.
anumber (optional)Alpha 0–255. Defaults to 255.
methodd:text(x, y, str, size, r, g, b, a?) → nil

Draws text with the built-in font.

ParamTypeNotes
x, ynumberTop-left of the text, in pixels.
strstringThe text to draw.
sizenumberText height in pixels (each glyph is ~size wide).
r, g, bnumberColour channels, 0–255.
anumber (optional)Alpha 0–255. Defaults to 255.
methodd:image(path, x, y, w, h) → boolean

Draw a PNG or JPEG, scaled into that rectangle. Returns false if it can't be loaded, so you can fall back to drawing a label. Images are cached after the first draw, and a failed path is remembered too rather than retried every frame.

ParamTypeNotes
pathstringRelative to the app folder, e.g. "addons/"..MOD_NAME.."/cart.png". Absolute paths and .. are refused.
x, y, w, hnumberDestination rectangle in pixels.
methodd:image(path, x, y, w, h) → boolean

Draws a PNG or JPEG scaled into the given rectangle. Returns true if it drew, false if the image couldn't be loaded — check the result and fall back to text rather than assuming it worked. Images are loaded once and cached, so calling this every frame is cheap (the first call does the disk read).

ParamTypeNotes
pathstringPath relative to the app folder, e.g. "addons/" .. MOD_NAME .. "/cart.png". Absolute paths and .. are refused.
x, ynumberTop-left corner, in pixels.
w, hnumberSize to scale the image to, in pixels.
if not d:image("addons/" .. MOD_NAME .. "/logo.png", 20, 20, 64, 64) then
    d:text(20, 20, "MyMod", 16, 230, 230, 240)   -- fallback
end

data

Per-mod file storage. Every call is sandboxed to data/<MOD_NAME>/ — a mod can only read and write inside its own folder. Absolute paths and .. are rejected.

functiondata.write(name, contents) → boolean

Writes contents to name (overwriting), creating sub-folders as needed.

ParamTypeNotes
namestringFile path relative to your folder, e.g. "save.txt" or "sub/x.dat".
contentsstringData to write (may contain binary bytes).
returnsbooleantrue on success; false if the write failed or the path was rejected.
functiondata.read(name) → string | nil

Reads a file back.

ParamTypeNotes
namestringFile path relative to your folder.
returnsstring with the contents, or nil if it doesn't exist / can't be read.
functiondata.append(name, contents) → boolean

Appends contents to the end of name (creating it if needed).

ParamTypeNotes
namestringFile path relative to your folder.
contentsstringData to append.
returnsboolean — success.
functiondata.exists(name) → boolean

Whether a file exists in your data folder.

ParamTypeNotes
namestringFile path relative to your folder.
returnsboolean.
functiondata.delete(name) → boolean

Deletes a file.

ParamTypeNotes
namestringFile path relative to your folder.
returnsboolean — whether a file was removed.
functiondata.list() → string[]

Lists your saved files. Takes no parameters. Returns a Lua array (table) of string relative paths of all files in your data folder, recursively.

functiondata.path() → string

The absolute path of your data folder (informational). Takes no parameters. Returns a string.

Persist and restore a value:
local score = tonumber(data.read("score.txt")) or 0
score = score + 1
data.write("score.txt", tostring(score))
log(MOD_NAME .. " score is now " .. score)

Threading

The main thread renders. Anything you do in OnFrameReceived or OnFrame is paid for out of the frame budget — about 16 ms at 60fps — and spending it is what makes the picture late, which is the one thing this app exists not to do. Two ways out: a worker, which is a real OS thread with its own Lua state, and LateUnreliableThink, which is a coroutine on the main thread that spreads one long job over several frames.

Pick the worker when the work is heavy and its inputs can be copied. Pick LateUnreliableThink when the work is heavy but needs to see mod state directly. Reading pixels is neither: the Frame is only alive for the duration of the call it was handed to, so sample on the main thread and send the samples.

functionworker.spawn(path) → Worker | nil

Start a background thread running a script from your own addon folder. Only while your mod is loading — a worker is long-lived, and load time is when the engine still knows whose folder to read from. Returns nil (and logs) if the file isn't there.

ParamTypeNotes
pathstringRelative to addons/<MOD_NAME>/, e.g. "workers/reader.lua". Absolute paths and .. are rejected.
local w = worker.spawn("workers/reader.lua")
methodw:post(job) → boolean

Hand over one job. False means it is still busy — and that is the whole scheduling model: ask every frame, and a frame where the answer is no is a frame you simply skip. Nothing queues, so a slow pass can never build a backlog of work about frames that have already gone.

ParamTypeNotes
jobanyCopied, not shared — see what can cross below. Usually a table.
methodw:collect() → any[]

Everything the worker has finished since you last asked, as an array. Never blocks and returns an empty table when there is nothing — call it every frame. Each entry is whatever that job's onJob returned.

methodw:share(name, value) → nil

Give the worker a value once, as a global on its side. For the things that never change and are too big to send every frame — a font atlas, a lookup table. It is a snapshot: changing your copy afterwards changes nothing over there until you share it again.

w:share("FONT", PKMN.font)      -- a few hundred KB, sent once
w:post({ rows = sampled })      -- a few KB, sent every frame
methodw:busy() → boolean

Whether a pass is running. You rarely need it — post already answers the same question by refusing.

methodw:alive() → boolean

False if the script failed to load or defines no onJob. A worker that throws inside a job stays alive: one bad frame shouldn't stop the mod being processed for the rest of the session.

fieldonJob(job) → any

In the worker script, not in your mod: the one function the engine calls, once per posted job. What you return comes back through collect. A worker script that doesn't define it is refused at spawn.

The worker gets a smaller sandbox than a mod: string, table, math, utf8 and the base library, plus log(msg) and now() (milliseconds, for timing a pass). No os, no data, and none of the engine APIs — no overlay, no hook, no game. They all reach shared state, and reaching is the thing this design exists to avoid.

-- workers/reader.lua
function onJob(job)
    local out = {}
    for _, w in ipairs(job.windows) do out[#out+1] = classify(w, FONT) end
    return { seq = job.seq, windows = out }
end
noteWhat can cross a thread

nil, booleans, numbers, strings and tables of those, nested. Everything else — functions, userdata, metatables, a Frame — is dropped, and the engine logs how many values it left behind rather than failing quietly. Cycles are not followed.

This is a copy, in both directions. The two Lua states share no memory at all, which is what makes it safe to run one while the other is drawing; it is also why there are no globals to reach for over there. Package what the worker needs into the job, and expect the answer back the same way.

functiongame.slice() → nil

Inside LateUnreliableThink: yields if this frame's share of time is spent, and resumes here on the next one. Call it wherever stopping is safe. Without a slice call the pass runs to completion in one frame and you have gained nothing.

Why a coroutine rather than a thread: there is one Lua state shared by every mod (that is what makes cross-mod globals work), and a second OS thread touching it would corrupt it. So the slicing is cooperative, and the budget is 2 ms of a ~16 ms frame. "Unreliable" is the promise: passes never overlap and never queue, but nothing says how often one runs or how long it takes.

function g:LateUnreliableThink()
    for i = 1, 100000 do
        crunch(i)
        if i % 500 == 0 then game.slice() end
    end
end

Sandbox

Mods run deny-by-default. Only the standard Lua listed below is available, plus the engine APIs on this page. There is no raw filesystem, process, or code-loading access — write files through data. All mods share one global environment (globals, functions, and hooks cross mod boundaries — like GMod); only data and MOD_NAME are per-mod.

availableStandard Lua

print, pairs, ipairs, next, type, tostring, tonumber, select, error, assert, pcall, xpcall, setmetatable, getmetatable, rawget, rawset, rawequal, rawlen, and the string, table, math, coroutine, utf8 libraries.

limitedos

A time-only subset: os.time, os.date, os.clock, os.difftime. (No execute, remove, rename, getenv, exit.)

blockedio · require · load · loadfile · dofile · package

Removed entirely. These are the usual ways to escape a sandbox — file I/O goes through data, and there is no runtime code loading.

engine onlyauth · settings · video · audio · capture · console

These exist, but not for mods. They change the user's device, window, volume or sign-in state, and auth additionally holds the session tokens — in a sandbox shared by every mod, one of these reachable would be all of them reachable. The read-only slices mods do get are camera (what the card reports) and account (who is signed in).