AI context pack¶
Writing an EMS plugin with an AI assistant? Paste this whole page into the chat before you ask for code. It's the entire public API on one page. An assistant that hasn't seen it guesses at method names and gets them wrong; one that has it writes against the real surface.
This page is condensed from the rest of these docs and tracks them. The fuller versions are linked from the API reference. To paste a clean copy, grab the raw markdown.
How to use it¶
- Paste everything below the line into your assistant.
- Describe the plugin you want, concretely.
- Check the result against this page: every
GRIPEMS.API:...call it writes must appear below. If a method isn't here, it doesn't exist, and the assistant invented it. Tell it so and have it redo that part.
The AI-assisted plugin guide has example prompts and the full check-it loop.
GRIP-EMS plugin API, condensed¶
EMS (GRIP - Enhanced Macro Sequencer) is a World of Warcraft Retail addon. A plugin is its own addon that extends EMS through one frozen table, GRIPEMS.API (global alias _G.GRIPEMS_API). You never edit EMS and never reach into _G.GRIPEMS. Everything a plugin contributes through its handle is reverted the moment the user disables it, so the addon returns to stock.
API_VERSION: 3. Documented against GRIP-EMS 2.3.7 on WoW Retail 12.0.7.
Hard rules (follow every one)¶
- The plugin is a separate addon. Its
.tochas## Dependencies: GRIP-EMS, so WoW loads EMS first and refuses to load the plugin without it. - Do setup from
PLAYER_LOGIN; do UI setup after theGEMS_UI_READYevent. Never at file scope. - Handshake before any other call: null-check
GRIPEMS.API, thenAPI:RequireVersion(n). - To contribute anything that should vanish on disable, call
API:RegisterPlugin(id, meta)and work through the handle it returns. - Use only methods on this page. Never read or write
_G.GRIPEMS. - A variable provider's
Resolveand a condition'sEvaluatereturn a plain, non-secret scalar (string, number, or boolean). Never a table, a function, or a 12.0 secret value such asUnitHealth("player"). - Registering or changing returns
okfirst:true, orfalseplus a reason. Read accessors return the value orniland never raise. Call with the colon:API:Method().
Plugin skeleton (the correct shape)¶
-- MyPlugin.toc
-- ## Interface: 120007
-- ## Title: My EMS Plugin
-- ## Author: You
-- ## Version: 1.0.0
-- ## Dependencies: GRIP-EMS
-- MyPlugin.lua
local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_LOGIN")
f:SetScript("OnEvent", function()
local API = GRIPEMS and GRIPEMS.API
if not API then return end -- EMS not loaded
if not API:RequireVersion(2) then return end -- needs v2 for the handle
local handle = API:RegisterPlugin("myplugin", {
name = "My EMS Plugin",
version = "1.0.0",
OnEnable = function(h)
-- register providers, author sequences, mount panels here
end,
OnDisable = function(h)
-- only cleanup EMS can't do; it reverts your contributions for you
end,
})
if not handle then return end
end)
Tier 0 - discovery¶
GRIPEMS.API.API_VERSION-> integer (3)GRIPEMS.API.EMS_VERSION-> string ("2.3.7"); for logs only, don't gate features on itGRIPEMS.API:RequireVersion(n)-> true, or false + reasonGRIPEMS.API:GetCapabilities()-> array of capability id stringsGRIPEMS.API:RegisterPlugin(id, meta)-> handle, or nil + reasonmeta = { name = string, version = string, OnEnable = function(handle), OnDisable = function(handle) }idis your namespace for everything you contribute; a duplicate id is rejected, never overwritten.
Capability ids: events, data, sequences, ui, preview, variables, conditions, stepfunctions, plugins, authoring, panels, views, settings, cvars, stepdata, macro, slash. Check GetCapabilities() before using a tier if you want to degrade on an older EMS.
Tier 1 - events (listen only)¶
GRIPEMS.API:On(event, handler)-> handle string, or false + reason. The handler gets the payload args only, not the event name. It runs insidepcall.GRIPEMS.API:Off(handle)-> nothingGRIPEMS.API:ListEvents()-> array of the subscribable names
On rejects any name not in this set. Payloads that carry a table hand you a deep-copied, read-only snapshot.
| Event | Payload |
|---|---|
SEQUENCE_CREATED |
(name, data) |
SEQUENCE_DELETED |
(name) |
SEQUENCE_IMPORTED |
(results) |
SEQUENCE_UPDATED |
(name, data) |
SEQUENCE_STEP_ADVANCED |
(seqName, step, numSteps) |
KEYBIND_CHANGED |
(seqName, key) - both may be nil on a bulk/clear change |
CONTEXT_CHANGED |
(newContext, oldContext) |
LOADOUT_CHANGED |
(newID, newName, oldID, oldName) |
SETTING_CHANGED |
(key, value) |
PLUGIN_REGISTERED |
(name, version) |
PLUGIN_SEQUENCES_LOADED |
none |
GEMS_UI_READY |
none - do UI setup on this one |
GEMS_UI_LAYOUT_APPLY |
none |
GEMS_UI_NAV_CHANGED |
(viewId) |
GEMS_EDITOR_TAB_CHANGED |
(tabName) |
GEMS_SEQUENCE_SELECTED |
(name) |
GEMS_PREVIEW_MODE_CHANGED |
(mode) |
GEMS_PREVIEW_UPDATE |
none |
PLUGIN_ENABLED and PLUGIN_DISABLED are not public; use your OnEnable / OnDisable callbacks instead.
Tier 2 - data (read-only; you get copies and scalars)¶
GRIPEMS.API:GetSequenceList()-> array of{ name, stepCount, currentStep, stepFunction }GRIPEMS.API:GetSequenceInfo(name)-> table or nil. Fields:name, stepFunction, versionCount, defaultVersion, activeVersionIndex, activeStepCount, contextVersionCount, classID, specID, author, description, help, helplink, changelog, talentString, url, privacyMode, version, createdAt, updatedAt, disabled, keybind, variableDepsGRIPEMS.API:GetSequenceSteps(name)-> array of{ index, spellID, spellName, icon }for the active version, or nil. Public scalars (never secret); the per-step view for action-bar chrome. Pair withSEQUENCE_STEP_ADVANCED. Capabilitystepdata.GRIPEMS.API:GetAuthoredSteps(name)-> array of{ index, spellID, spellName, icon }in AUTHORED base order (the order the user wrote), or nil. Interleave copies suppressed and the version repeatCount not applied; Loops unrolled and IF branches flattened, so it is the base order, not the tree. Distinct from GetSequenceSteps' EXECUTION domain -- its indices do NOT map to currentStep or the SEQUENCE_STEP_ADVANCED step index. Capabilitystepdata. Added in EMS 2.3.7 with NO API_VERSION change, andstepdatapredates it, so neither RequireVersion nor the capability list proves it is present -- guard every call withif API.GetAuthoredSteps then ... end, which is safe because a missing key reads as nil rather than raising.GRIPEMS.API:GetSequenceMacroIndex(name)-> macro slot index or nil (read-only; never creates the macro). Capabilitymacro.GRIPEMS.API:GetCurrentContext()-> string ("none", "Raid", "Arena", ...)GRIPEMS.API:GetSetting(key)-> value or nil. Allowlist:uiLayout(string|nil),debug(boolean). For any other setting, listen toSETTING_CHANGED.GRIPEMS.API:GetRegisteredPlugins()-> array of{ name, version, loaded, sequenceCount }
Tier 3 - UI and layout (GRIPEMS.API.UI)¶
:RegisterLayoutProvider(id, provider)-> ok:SetActiveLayoutProvider(id)-> ok. PersistsuiLayout; call only when the user opts in, not on load.:GetActiveLayoutProvider()-> id string:GetHost(name)-> frame or nil. Hosts:navHost, listHost, metadataHost, editorHost, configHost, globalViewHost, iconFooterHost:MountPanel(panelId, host)-> ok. Content panels:sequenceList, editor, variables, conditions, macros, source.previewgoes through the Preview facade instead.options, import, export, aboutare dialogs and ignore the host.vehiclePet, metadataare reserved and return false.:SetClassicChrome(enabled)-> ok.falsehides the default two-panel chrome so yours shows.:RegisterView(id, def)-> ok.def = { id, name }:SetActiveView(id)-> ok. FiresGEMS_UI_NAV_CHANGED.:GetActiveView()-> id or nil
Layout provider table. EMS calls the callbacks as plain functions (no self):
local provider = {
id = "acme_modern", -- must equal the id you register under
name = "Acme Modern",
OnRegister = function(self) end,
OnInitMainFrame = function(mainFrame, hosts) end, -- before panels
OnInitPanels = function(mainFrame, hosts) end, -- build chrome into hosts here
OnApplyLayout = function(context) end, -- reposition on a layout pass
OnMinimize = function(mainFrame) end,
OnRestore = function(mainFrame) end,
}
SAFETY: hosts are plain, non-secure, reparentable frames. Never place a secure frame or SecureActionButtonTemplate under a host; reparenting it taints the secure execution path. Keep any secure frames your plugin makes outside the hosts.
Tier 3 - preview (GRIPEMS.API.Preview)¶
:GetMode()-> "icons" | "text" | "compiled":SetMode(mode)-> ok (one of the three modes):Update(version)-> true (theversionarg is optional/advisory):Hide()-> nothing:MountSidebar(host)-> ok:MountIconFooter(host)-> ok
Tier 4 - registries¶
GRIPEMS.API:RegisterVariableProvider(id, spec)-> okspec = { id, name, Resolve = function(self, varName) ... end, OnRegister? }Resolvereturns a plain non-secret scalar for names it owns,nilotherwise. Runs at compile time; keep it pure and cheap.
GRIPEMS.API:RegisterCondition(id, spec)-> okspec = { id, name, Evaluate = function(self) ... end, OnRegister? };Evaluatereturns a boolean.
GRIPEMS.API:EvaluateCondition(id)-> clean boolean (users call this inside a variable body)GRIPEMS.API:RegisterStepFunction(id, spec)-> okspec = { id, name, Expand = function(self, resolvedStepTexts) ... end, OnRegister? };Expandreturns an array of macrotext strings in execution order. Pure. Each entry ofresolvedStepTextsis itself a full macrotext line after variable substitution (e.g."/cast [combat] Kill Command"), NOT a bare spell name -- keying a lookup on spell names silently no-ops.
GRIPEMS.API:RegisterSequences(name, version, seqNames, seqTable)-> ok (ships a static set; a user sequence wins on a name clash)
Reversible forms on the handle take the spec alone (id read from spec.id): handle:RegisterVariableProvider(spec), handle:RegisterCondition(spec), handle:RegisterStepFunction(spec), handle:RegisterLayoutProvider(provider).
Tier 5 - authoring (handle methods only; owner-scoped; reverted on disable)¶
handle:CreateSequence(name, data)-> ok (re-applying a name you own is a safe upsert)handle:UpdateSequence(name, data)-> okhandle:DeleteSequence(name)-> okhandle:SelectSequence(name)-> ok (UI; firesGEMS_SEQUENCE_SELECTED)handle:OpenEditor(name)-> ok (UI)handle:RegisterSetting(def)-> ok.def = { key, type, name, desc, ... }(e.g.min, max, stepfor a"range")handle:OverrideSetting(key, value)-> okhandle:RevertSetting(key)-> okhandle:RequestCVarProfile(profileKey)-> ok (routed through the EMS CVar manager; no direct secure-CVar write)handle:RevertCVarProfile()-> okhandle:RegisterImportProvider(spec)-> ok.spec = { id, name, Parse = function(self, text) ... end, Detect? }handle:RegisterExportProvider(spec)-> ok.spec = { id, name, Serialize = function(self, seq) ... end }handle:EnsureSequenceMacro(name)-> macro slot index, or false + reason. Creates the sequence's action-bar macro if absent (journaled, deleted on disable); an existing EMS/user macro is returned without journaling. Guards in-combat, slot-pool-full, and unknown sequence.PickupMacro(index)to drag it onto a bar. Capabilitymacro.handle:RegisterSlashCommand(sub, handler, helpText?)-> ok, or false + reason. Registers a/gemssubcommand;handler(argString)runs in pcall; a built-in or duplicate name is rejected; journaled, removed on disable. Capabilityslash.handle:GetId()-> your plugin id
Ownership is strict: a plugin may create, edit, or delete only sequences it owns. A user's sequence and another plugin's objects are rejected.
Tier 5 - theme (GRIPEMS.API.UI)¶
:RegisterPanelFrame(frame, category, class)-> makes your frame inherit the active EMS skin. Example:(myFrame, "panel", "panel.acmeNav"). Appearance only, no structural authority. Namespaceclassto your plugin.
The lock list (no public write path, ever)¶
Rotation execution; secure buttons and the keybind matrix; authorship and signing; peer-to-peer transmission; direct secure-CVar writes; raw persistence; taint-laundering and secret values. The rule: if a call would change which spell fires, how a key binds, who authored a sequence, what is transmitted, or what is saved, it does not exist on the public API. You can read derived state (active version index, context, summaries); you cannot write any of these. To put a sequence on an action bar, use handle:EnsureSequenceMacro -- the generated 255-char macro is the supported bar path; setting a keybind from a plugin stays locked.
Secret values¶
WoW 12.0 tags some values (a unit's health, for example) as secret so they can't cross into protected code. A variable provider that returns one has it rejected and the variable resolves to nothing; a condition that returns one evaluates to false. Return computed or non-secret values (counts, settings, group size, derived numbers).