Skip to content
RetrogamingDeep Dive Published Updated 8 min readViews unavailable

MAME Plugin Lifecycle: Packaging Lua Extensions and Measuring Machine Events

Create a small MAME Lua plugin with correct metadata, startup hooks, menus, persistence paths, and measurable checks for lifecycle and compatibility.

MAME plugins are useful when a task needs to observe or extend emulator behavior repeatedly: record machine-reset events, expose a small diagnostic menu, communicate with another program, automate a deterministic test, or add a user interface. They are not the same as a one-shot autoboot script. A plugin is a packaged Lua extension that MAME discovers, optionally enables, loads during startup, and invokes through its plugin interface.

The most reliable way to start is to understand the lifecycle and deployment contract before writing substantial logic. A script that is valid Lua can still fail to load because its directory is missing, metadata is malformed, plugins are disabled, or it was written for a different MAME Lua API. This guide builds a minimal plugin from the current upstream sample structure and shows how to test it without confusing a loaded file with a working extension.

Plugin packages and discovery

MAME’s plugin documentation describes two important configuration inputs: the global plugins option and pluginspath. The first enables plugin support; the second tells MAME where plugin folders live. Individual plugins can be selected in the UI, through the command-line -plugin option, or in plugin.ini. Changes made to the enabled plugin list require a complete MAME exit and restart. Plugins that keep settings or data should use the configured homepath rather than assuming the current working directory is writable.

A conventional plugin directory contains plugin.json and init.lua. The metadata file describes the plugin name, description, version, author, type, and whether it starts. MAME’s upstream example represents start as the string "true", not a JSON boolean. The name is also used to locate the Lua module, so it should match the directory and exported module name. For a plugin folder named frameledger, the package file can be:

{
  "plugin": {
    "name": "frameledger",
    "description": "Counts machine resets and stops",
    "version": "1.0.0",
    "author": "Local diagnostic example",
    "type": "plugin",
    "start": "true"
  }
}

This metadata is discovery information, not a security boundary or a compatibility guarantee. A plugin executes in MAME’s Lua environment with the APIs exposed to it. Only install code you understand and can maintain, and keep local changes separate from the MAME distribution’s bundled plugin tree so upgrades do not silently overwrite them.

Start with bounded lifecycle callbacks

The upstream dummy plugin demonstrates the startplugin() entry point, machine reset and stop notifiers, and menu registration. The following small adaptation uses only the reset and stop events. It retains the notifier subscription objects for the life of the plugin, resets its own counter on a machine reset, and reports a summary when MAME stops the emulated machine.

local exports = {
    name = "frameledger",
    version = "1.0.0",
    description = "Counts machine resets and stops"
}

local resets = 0
local reset_subscription
local stop_subscription

function exports.startplugin()
    reset_subscription = emu.add_machine_reset_notifier(function()
        resets = resets + 1
        emu.print_info("frameledger: reset " .. resets .. " on " .. emu.gamename())
    end)

    stop_subscription = emu.add_machine_stop_notifier(function()
        emu.print_info("frameledger: machine stopped after " .. resets .. " resets")
    end)
end

return exports

In Lua, local variables holding subscriptions prevent them from being discarded while callbacks are still expected. The callback should be small, deterministic, and tolerant of repeated sessions. Do not perform filesystem scans, network waits, or expensive machine inspection from a reset notifier. If you need durable totals, define a data format, write at a controlled lifecycle boundary, handle malformed or missing files, and use the plugin data location configured through homepath.

This example intentionally does not claim to count emulated frames or input events. MAME’s event model and exposed APIs have specific semantics; choose a notifier documented for the event you actually want, then test it against a known sequence. For time-sensitive measurement, distinguish emulated time from host wall-clock time, and avoid logging every frame to disk because synchronous I/O can perturb the workload being measured.

Add a menu only when it helps operations

Plugins can provide an entry in MAME’s UI. The upstream dummy example registers a menu callback plus a function that returns menu rows. MAME also exposes menu registration through Lua. A useful diagnostic menu should provide a narrow control such as “reset counters,” “show status,” or “write snapshot.” Keep menu callbacks quick and avoid modifying emulated machine state unless that is the explicitly documented purpose of the plugin.

When adding a menu, decide whether the UI should be available only during emulation or at the system selection screen. Verify behavior with the MAME version you ship, because the plugin API is MAME-specific and can evolve. Use the included plugin documentation and current upstream sample code as a compatibility baseline. A source example on the moving master branch is not a promise that an older packaged release exports identical methods.

If menu state or settings need to persist, store only plugin-owned configuration under the documented plugin data directory. Keep game-specific data keyed by the MAME short system name rather than display text, and validate data before using it. Never silently rewrite MAME’s core cfg files from a plugin: those files belong to the emulator’s own configuration lifecycle and may contain state your code does not understand.

Enable and locate the plugin deliberately

First check the exact MAME executable and its version. Create the plugin folder under a directory MAME searches, then set pluginspath to include its parent. Enable plugins globally and enable the plugin by short name. A command-line diagnostic launch can make the intent explicit:

mame -plugins -plugin frameledger -verbose pacman

The precise available syntax should be checked with the installed binary’s -showusage or command-line documentation. The plugin documentation also allows enabling a plugin through the UI or plugin.ini; when using a persistent setup, prefer one source of truth to avoid a frontend command line silently disagreeing with the saved plugin list. Fully exit and restart after changing plugin enablement.

The plugin data directory is separate from the code search path. MAME documents homepath as the location plugins should use for data, defaulting to the working directory. A launcher that changes its working directory can therefore change where default plugin data appears. Configure a stable per-user path for systems with multiple frontends, and back up plugin settings with emulator configuration if they are operationally important.

Diagnose load failures in layers

Use a progressive test rather than adding all plugin behavior at once:

  1. Run the exact binary with verbose output and confirm plugin support is enabled.
  2. Confirm pluginspath contains the parent directory, and that the folder name, plugin.json name, metadata name, and Lua module name agree.
  3. Start a minimal plugin that returns metadata and defines startplugin() without callbacks. Confirm MAME reports it as enabled.
  4. Add one notifier at a time. Use a controlled reset and clean machine stop, then verify exactly the expected messages appear.
  5. Add menu registration only after lifecycle callbacks work. Test opening, navigating, and closing the menu, then restart MAME to check persistence and enablement behavior.
  6. Test both a system that should trigger the plugin and a second system with a different device configuration. Avoid assuming every machine exposes the same device tags or state.

Syntax validation by a generic Lua interpreter is useful but insufficient. It cannot prove that MAME provides emu.add_machine_reset_notifier, that a callback runs at the expected lifecycle point, that the menu is registered correctly, or that the plugin folder is discovered by the installed build. Validate syntax first, then run MAME itself with verbose diagnostics. If no MAME executable is available on the development host, label runtime behavior unverified rather than presenting a successful parser check as integration evidence.

Keep a plugin maintainable

Treat a plugin like a small application. Keep external state minimal, document each API dependency, include a version or revision in logs, and handle stop/restart paths. Do not keep long-lived references to machine devices after a machine is torn down unless the API guarantees their lifecycle. Avoid globals that collide with other plugins; the exported module table and local variables are a cleaner boundary.

For data collection, include timestamps only when their clock source is clear. A host timestamp does not represent emulated time, and emulated time may pause or run at a different speed. When a plugin communicates with an external process, use bounded queues and timeouts, avoid blocking emulator callbacks, and test behavior when the peer is absent. These are operational design practices, not guarantees supplied automatically by the plugin framework.

The best acceptance criteria are observable and small: MAME discovers the plugin; enabling it requires the documented restart; startup does not change the game; one deliberate reset produces one event; clean exit produces one stop summary; plugin settings persist only where intended; and disabling the plugin returns behavior to baseline. Run the same checks after each MAME upgrade, because this extension depends on a host-specific scripting API.

MAME’s plugin model makes extensions practical without modifying a driver or rebuilding the emulator. A disciplined package, narrow callbacks, controlled persistence, and runtime checks keep that power from becoming an opaque layer that makes game behavior harder to reproduce.

Related:

Sources:

Comments