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
endDo 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.
| Method | Required and optional fields |
|---|---|
Section | label; optional id, description |
Card | id, label, child builder or build; optional description, defaultOpen, visible, enabled |
Text | text; optional id, tone, wrap, visible |
Separator | optional id, visible |
Status | label, value; optional id, tone, visible |
Image | id, source; optional width, height, description, visible |
Toggle | id, label, setting or get + set; optional description, visible, enabled |
Slider | id, label, binding, min, max; optional step, format, description, visible, enabled |
Choice | id, label, binding, non-empty homogeneous options; optional description, visible, enabled |
TextInput | id, label, binding; optional maxBytes, multiline, description, visible, enabled |
Color | id, label, binding; optional alpha, description, visible, enabled |
Hotkey | id, label, binding; optional allowMouse, description, visible, enabled |
Button | id, label, onClick; optional primary, description, visible, enabled |
List | id, 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.