Skip to main content

Overview

These best practices are derived from patterns in the MA2 plugin source code. Following these guidelines ensures your plugins are safe, efficient, and maintainable.

Blind Edit Mode

Always Use Blind Edit for Creation

The most critical best practice: Never modify sequences, presets, or cues while live.
function ColorPickerUpdate_Start()
  fb("---Color Picker Started :DDD---")
  cmd("BlindEdit On") -- Enter blind mode
  
  deletePresets()
  deleteSequence()
  createPresets()
  createSequences()
  createCues()
  assignSequences()
  
  cmd("BlindEdit Off") -- Exit blind mode
  fb("--- Color Picker Update Done---")
end
Creating or modifying sequences outside blind edit will affect live output and can disrupt a show. Always wrap creation operations in BlindEdit On/Off.

When to Use Blind Edit

  • Creating new sequences
  • Storing cues
  • Modifying presets
  • Deleting sequences or cues
  • Building complex effects
  • Batch operations
  • Reading data (exports, getGroup)
  • User input collection
  • Setting appearances
  • Label operations (though safer in blind)
  • View changes

Programmer Management

Always Clear After Operations

One of the most consistent patterns across all plugins:
function createCues()
  for group=grpStart, #groups do
    for start=1, colNb do
      cmd("Group "..group.." At Preset 4."..preset)
      cmd("Store Cue "..start.." Sequence "..seqCurrent)
    end
  end
  clear() -- Critical: clear programmer
end

Clear Function Pattern

-- Standard clear function
function clear()
  cmd('ClearAll')
end

-- Shortcut version
local cmd = gma.cmd
local function clear()
  cmd('ClearAll')
end
Clearing the programmer prevents values from one operation bleeding into the next. This is essential for accurate, predictable plugin behavior.

Variable Initialization and Reset

Initialize at Top of File

-- Color Picker Update example
local groups = {"A", "B", "C", "D", "E", "F", "G"}
local grpStart = 1
local presetStart = 1
local presetWidth = 16
local execPage = 100
local execStart = 101
local seqStart = 300
local updateMode = 0
local colNb = 11

Reset Variables After Use

local function PWG_resetValues()
  PWG_batch = false
  PWG_singleStack = false
  PWG_groups = {}
  PWG_dir = "left"
  PWG_seq = 0
  PWG_exec = 0
  PWG_amount = 1
  PWG_delay = 0.2
  PWG_trigTime = 0.1
  PWG_fade = 0.05
end

local function PulseWaveGen_Start()
  local success = PWG_setup()
  if not success then
    PWG_clear()
    PWG_resetValues() -- Always reset
    return
  end
  
  PWG_create()
  PWG_clear()
  PWG_resetValues() -- Reset on success too
end
Resetting variables ensures the plugin behaves consistently on multiple runs within the same session.

Naming Conventions

Plugin Function Naming

From the source code patterns:
-- Prefix functions with plugin abbreviation
local function PG_setup() -- Pulse Generator
local function PWG_createCuePair() -- Pulse Wave Generator  
local function ColorPickerUpdate_Start()

Variable Naming

-- Use prefixes to avoid conflicts
local PG_grp = 0
local PWG_groups = {}
local PG_seq = 0

-- Use descriptive names
local presetStart = 1
local presetWidth = 16
local colSwatchBook = {"White", "Red", "Orange"}

Shortcut Variables

Every plugin uses this pattern:
-- GrandMA Shortcuts
local text = gma.textinput
local cmd = gma.cmd
local fb = gma.feedback
local getHandle = gma.show.getobj.handle

Preset Organization

Structured Preset Ranges

From Color Picker Update:
local presetStart = 1
local presetWidth = 16 -- Space between groups

for group=1, #groups do
  -- Each group gets 16 preset slots
  local presetCurrent = presetStart + ((group-1) * presetWidth)
  
  for colorIndex=1, colNb do
    local preset = presetCurrent + colorIndex - 1
    -- Store at calculated position
  end
end
Result:
  • Group A: Presets 1-11
  • Group B: Presets 17-27
  • Group C: Presets 33-43
  • etc.
This spacing pattern allows for future expansion without renumbering.

Preset Labeling

-- Descriptive labels with group and color
cmd("Label Preset 4."..preset.." \""..groups[group].." "..colSwatchBook[start].."\"") 
-- Result: "A Red", "B Blue", etc.

-- Delay preset labels with time and direction
cmd('Label Preset 0.'..presetCurrent..' "'..timeList[i]..'s '..directionNames[loop]..'"')
-- Result: "2s <<", "5s V. Out", etc.

User Input Validation

Input Collection Pattern

From Pulse Wave Generator:
local function PWG_setup()
  -- Initialize
  PWG_groups = {}
  
  -- Collect with validation
  local grpCollect = true
  while grpCollect do
    local grpInput = text("Enter Group " .. (#PWG_groups + 1) .. " (empty to finish)", "")
    if grpInput == nil or grpInput == "" then
      grpCollect = false
    else
      table.insert(PWG_groups, grpInput)
    end
  end
  
  -- Validate result
  if #PWG_groups == 0 then
    fb("No groups entered, exiting.")
    return false
  end
  
  fb("Collected groups: " .. table.concat(PWG_groups, ", "))
  return true
end

Number Validation

-- Convert to number with default
PG_amount = tonumber(text('Pulse Amount?', PG_amount))

-- Validate range
PWG_delay = tonumber(text("Wave Delay? (seconds)", "0.2"))
if not PWG_delay then
  PWG_delay = 0.2
end

if not PWG_amount or PWG_amount < 1 then
  PWG_amount = 1
end

Boolean Input Pattern

local batchInput = text("Batch mode? (true/false)", "false")
PWG_batch = (batchInput == "true")

-- Alternative with string check
if PG_rnd == 'true' then
  cmd('ShuffleSelection')
end

Feedback and Progress

User Feedback

-- Loading confirmation
gma.feedback("Pulse Generator Plugin Loaded :DD")

-- Progress updates
fb("--- Presets Deleted ---")
fb("--- Sequences Created ---")
fb("--- Cues Created ---")

-- Completion message
fb("--- Color Picker Update Done---")

Progress Bars for Long Operations

From Pulse Wave Generator:
if totalPairs > 1 then
  PWG_progressHandle = gma.gui.progress.start("Creating Wave Sequence")
  gma.gui.progress.setrange(PWG_progressHandle, 1, totalPairs)
end

for i = 1, totalPairs do
  if PWG_progressHandle then
    gma.gui.progress.set(PWG_progressHandle, i)
    gma.gui.progress.settext(PWG_progressHandle, "Cue pair " .. i .. " of " .. totalPairs)
  end
  -- ... operation ...
end

if PWG_progressHandle then
  gma.gui.progress.stop(PWG_progressHandle)
  PWG_progressHandle = nil
end
Use progress bars for operations creating more than 5-10 items. Users need visual feedback that the plugin is working.

Cleanup Handlers

local function PWG_Cleanup()
  if PWG_progressHandle then
    gma.gui.progress.stop(PWG_progressHandle)
    PWG_progressHandle = nil
  end
  fb("Plugin cleanup called")
end

return PulseWaveGen_Start, PWG_Cleanup

Sleep and Timing

Sleep Function Pattern

local function sleep(s)
  gma.sleep(s)
end

-- Usage
sleep(0.05) -- Small delay for command processing
sleep(0.5)  -- Longer delay for visual feedback

Strategic Sleep Usage

-- After complex commands
cmd("At 100")
PWG_sleep(0.05)
cmd("Delay " .. delayStr)
PWG_sleep(0.05)

-- Between demo macros
sleep(0.5)
cmd("Go Macro ALLWHITE")
sleep(0.5)
cmd("Go Macro ALLRED")
Small sleeps (0.05s) allow the console to process commands. Longer sleeps (0.5s) provide user feedback time.

File Operations

Temporary Files

From the getGroup function:
-- Select correct drive first
cmd('SelectDrive 1')

local file = {}
file.name = 'tempfile_getgroup.xml' 
file.directory = gma.show.getvar('PATH')..'/'..'importexport'..'/'
file.fullpath = file.directory..file.name

-- Create temp file
cmd('Export Group ' .. grpNum .. ' "' .. file.name .. '"')

-- Read it
local t = {}
for line in io.lines(file.fullpath) do
  t[#t + 1] = line
end

-- CRITICAL: Clean up
os.remove(file.fullpath)
Always delete temporary files after use. Leaving temp files can cause issues on future runs.

Command Safety

No-Confirm Flag Usage

-- Safe: deletion with no confirm (user already confirmed plugin execution)
cmd("Delete Preset "..start.." Thru "..finish.." /nc")
cmd("Delete Sequence "..start.." Thru "..finish.." /nc")

-- Overwrite without confirm
cmd("Store Sequence "..seq.." /o")

Store Options

-- Comprehensive store options
local storeOptions = ' /s /o /so=Prog /use=Active /v=false /vt=true /ef=false'
cmd('Store Preset 0.'..presetNum..storeOptions)
Breakdown:
  • /s - Selective store (only changed values)
  • /o - Overwrite existing
  • /so=Prog - Store from programmer
  • /use=Active - Use active values
  • /v=false - No verbose output
  • /vt=true - Value type specific
  • /ef=false - Exclude effects

Preset Type Operations

PresetType Selection

-- Single preset type
cmd("PresetType \"DIMMER\"")
cmd('At 100')

-- Multiple preset types
local presetTypes = {4} -- Color presets
local pTypes_str = table.concat(presetTypes, ' + ')

for i = 1, #presetTypes do
  gma.cmd('PresetType '..presetTypes[i]..' At Delay '..delayTime)
end

Error Prevention

Validation Before Processing

if #PWG_groups == 0 then
  fb("No groups entered, exiting.")
  PWG_clear()
  PWG_resetValues()
  return
end

if PWG_exec == nil or PWG_exec == "" then
  fb("No executor selected, exiting.")
  PWG_clear()
  PWG_resetValues()
  return
end

Graceful Exit Pattern

local function PulseWaveGen_Start()
  local success = PWG_setup()
  if not success then
    fb("Setup cancelled or failed.")
    PWG_clear()
    PWG_resetValues()
    return -- Exit cleanly
  end
  
  -- Continue with main operation
  PWG_create()
  PWG_clear()
  PWG_resetValues()
end

Plugin Structure Template

Based on all source code patterns, here’s the recommended structure:
-- MADE BY [YOUR NAME] - [YEAR]

-- [Plugin Name] Plugin
gma.feedback("[Plugin Name] Plugin Loaded :DD")

-- Local Variables
local pluginVar1 = defaultValue
local pluginVar2 = defaultValue

-- GrandMA Shortcuts
local text = gma.textinput
local cmd = gma.cmd
local fb = gma.feedback
local getHandle = gma.show.getobj.handle

local function sleep(s)
  gma.sleep(s)
end

local function clear()
  cmd('ClearAll')
end

------------------
-- PLUGIN START --
------------------

local function setup()
  -- Collect user input
  -- Validate input
  -- Return success/failure
end

local function create()
  cmd('BlindEdit On')
  
  -- Main plugin operations
  
  cmd('BlindEdit Off')
end

local function resetValues()
  -- Reset all variables to defaults
end

local function cleanup()
  -- Stop progress bars
  -- Clean up resources
end

-- Plugin Function Selection --
local function PluginName_Start()
  local success = setup()
  if not success then
    fb("Setup cancelled or failed.")
    clear()
    resetValues()
    return
  end
  
  fb("[Plugin Name] Started")
  create()
  clear()
  resetValues()
  fb("[Plugin Name] Done")
end

return PluginName_Start, cleanup

Testing Best Practices

1

Test on Backup Showfile

Never test plugins on a live show file. Create a backup or test showfile first.
2

Verify Group Existence

Ensure all groups referenced by the plugin exist in your showfile.
3

Check Number Ranges

Verify that preset/sequence/executor ranges don’t conflict with existing show data.
4

Test Edge Cases

  • Empty input
  • Single group vs. multiple groups
  • Maximum values
  • Invalid inputs
5

Verify Cleanup

Confirm that temporary files are deleted and programmer is cleared.

Common Pitfalls to Avoid

Problem: Modifying sequences during live showSolution: Always wrap operations in BlindEdit On/Off
Problem: Values from one operation affect the nextSolution: Call clear() after each store operation
Problem: Plugin crashes on invalid inputSolution: Validate all user inputs and provide defaults
Problem: Temp files accumulate and cause issuesSolution: Always os.remove() temporary files
Problem: Second plugin run uses values from first runSolution: Implement and call resetValues() function
Problem: Users don’t know if plugin is working or stuckSolution: Use fb() messages and progress bars

Performance Optimization

Minimize Console Commands

-- Less efficient: multiple individual stores
for i = 1, 100 do
  cmd('Store Preset '..i)
end

-- More efficient: batch operations where possible
-- Build up programmer, then store

Use Progress Indicators

-- Only create progress bar if needed
if itemCount > 1 then
  progressHandle = gma.gui.progress.start("Processing")
end

Strategic Sleep Placement

-- After complex commands that need processing time
cmd('At 100')
sleep(0.05)
cmd('Delay 0 Thru 2')
sleep(0.05)
cmd('Store Cue 1 Sequence 1')

-- Not needed after simple commands
cmd('Label Sequence 1 "Name"') -- No sleep needed

Documentation

File Header Comments

-- MADE BY HUGO OTTH - 2025
-- Pulse Generator Plugin
-- Creates pulsing effects with configurable timing

Inline Comments

-- Only for complex logic
if (#groups > 2) then
  local divCt = math.ceil(#groups/2) - 1
  local interval = chaserWings_mult / divCt
  local proportion = chaserWings_mult -- working backwards for center-out default
end
The code should be self-documenting through clear variable and function names. Add comments only for complex algorithms or non-obvious decisions.

Build docs developers (and LLMs) love