Skip to content

Writing a plugin ​

API 2 plugins live only under External/Plugins/<folder>/. Each folder contains manifest.lua and init.lua; the init chunk receives its scoped plugin context as its vararg. There is one Plugins root. Do not add files to another root or call framework registrars directly.

lua
-- manifest.lua
return { id = "Visuals", name = "Visuals", api = 2 }

The context is local plugin = .... It owns plugin.require, plugin.log, plugin.asset, plugin.rotation, plugin.settings and plugin.menu. Plugin settings use local ids; the framework namespaces them by owner.

A visuals-only plugin ​

This example declares a menu page and draws separately. Nyx.Draw is a frame overlay API, not an ImGui declaration and not a replacement for the menu:

lua
local plugin = ...
local label = plugin.asset("label.png")
local enabled = true
local page

local function invalidate()
    if page then page:Invalidate() end
end

page = assert(plugin.menu:RegisterPage({
    id = "visuals", label = "Visuals", icon = label,
    build = function(ui)
        ui:Section({ id = "heading", label = "World overlay" })
        ui:Toggle({ id = "enabled", label = "Enabled",
            get = function() return enabled end,
            set = function(value) enabled = value; invalidate() end })
        ui:Image({ id = "icon", source = label, width = 32, height = 32 })
    end,
}))

Nyx.DrawOverlay = true
function Nyx.Draw.Update()
    Nyx.Draw.Clear()
    if enabled then Nyx.Draw.Circle(400, 300, 40, 0.2, 0.8, 1, 1) end
end

Do not create a standalone window, call raw ImGui, or use Nyx.Engine.menuCommit, menuEvents or menuDrop. Those are internal engine natives. The host owns the window, input, scrolling, layout, and rendering.

Pages and subsections ​

RegisterPage takes id, label, build, and optional icon, order and subsections.

A page with no subsections is one sidebar row showing one view, built with build(ui, nil).

Declaring subsections — { id, label } pairs — makes the page a group instead. Its row names the group and nothing else: clicking it opens the first tab, and the tabs are drawn indented beneath it only while that group is the one being shown. Every other group stays collapsed. build(ui, subsectionId) is called once per tab, and never with nil, so a page never has to decide what an absent subsection would mean.

lua
plugin.menu:RegisterPage({
    id = "rogue", label = "Rogue", icon = "class_rogue", order = 40,
    subsections = {
        { id = "rotation",  label = "Rotation" },
        { id = "cooldowns", label = "Cooldowns" },
    },
    build = function(ui, subsectionId)
        if subsectionId == "rotation" then
            -- ...that tab's cards
        end
    end,
})

Control ids are unique across all of a page's views, not just within one, so two tabs cannot both declare a node called enabled.

Declaration schemas ​

The table lists each declaration's own fields. Section is the exception to the display fields: it has no visible or enabled field. Interactive declarations accept description, enabled, and either a setting or matching get and set callbacks where listed.

MethodRequired and optional fields
Sectionlabel; optional id, description
Cardid, label, child builder or build; optional description, defaultOpen, visible, enabled
Texttext; optional id, tone, wrap, visible
Separatoroptional id, visible
Statuslabel, value; optional id, tone, visible
Imageid, source; optional width, height, description, visible
Toggleid, label, setting or get + set; optional description, visible, enabled
Sliderid, label, binding, min, max; optional step, format, description, visible, enabled
Choiceid, label, binding, non-empty homogeneous options; optional description, visible, enabled
TextInputid, label, binding; optional maxBytes, multiline, description, visible, enabled
Colorid, label, binding; optional alpha, description, visible, enabled
Hotkeyid, label, binding; optional allowMouse, description, visible, enabled
Buttonid, label, onClick; optional primary, description, visible, enabled
Listid, rows, row builder; optional emptyText, description, visible, enabled

Choice options are { label = "...", value = ... } and all values must have the same scalar type. Colors use r/g/b/a values from 0 to 1 in custom callbacks; setting-backed colors use "R,G,B,A" byte strings. Text input limits are UTF-8 bytes. Hotkeys are normalized by the framework. In the table, binding means either setting or matching get and set callbacks.

Invalidation and failures ​

Builders run during a commit and must only declare nodes. Change plugin-owned state in a callback, then call the page handle's Invalidate() exactly once. A watcher or plugin.menu:Invalidate(pageId) may do the same. Idle frames do not rebuild.

An invalid declaration, unsafe path, callback error, native resource-limit violation, or native rejection fails the candidate. The previous last-good page remains active and diagnostics identify the plugin and failure. Missing, unreadable, malformed, or undecodable PNGs are nonfatal: the checker records a warning and substitutes the built-in missing-image marker. Valid PNGs must be below Assets/, end in .png, have dimensions from 1 through 2048 pixels inclusive on each axis, and fit within the plugin's aggregate 32 MiB decoded RGBA budget. A plugin may reference at most 128 images. plugin.asset rejects absolute paths, traversal and other extensions.

See the generated plugin context, menu, and declarations reference pages for signatures and the complete runtime contract.

API reference generated from source.