Table of Contents

Plugin manifest

Every plugin ships a plugin.json at the root of its folder. The host reads it before it loads any code.

Field Required Meaning
id required Identity, non-empty.
displayName required Shown to the player.
version required The plugin's own version, free-form.
entryDll required The assembly the host loads.
apiVersion required Must be within the range AcDream.Plugin.Abstractions supports.
kinds optional; absent = gameplay gameplay, renderPack, or both.
minHostVersion optional Lowest client version, MAJOR.MINOR.PATCH, inclusive.
maxHostVersion optional Highest client version, inclusive.
skipHostVersions optional Exact client versions with a known breakage.
hosts optional; absent = both Non-empty array of graphical, headless.
capabilitiesVersion optional; required if capabilities is present Which capability vocabulary the entries below are spelled in.
capabilities optional What the plugin does, shown to the player before they install. See Capabilities.

A property name must be unique, case-insensitively, within every object in the document, including objects nested inside arrays; a repeat fails to parse.

A host version is compared on its version core: 0.1.7+3a71d75 and 0.2.0-beta.1 compare as 0.1.7 and 0.2.0. <Version> in Directory.Build.props is bumped only at release, so a build from main reports the last release number: a plugin needing unreleased abstractions sets minHostVersion to the next release.

When a plugin is incompatible

A plugin that fails the version or host check follows the same rule an unsupported kinds value already does: the player asked for it by id, or they didn't.

  • Listed in the session's plugin allow-list: the session status reports pluginFailed with the reason, and no code loads.
  • Not listed, or no allow-list is configured: the plugin is skipped silently.

Duplicate ids

If more than one folder under the configured plugin roots declares the same id, once each has passed its own kinds and compatibility check, the session reports pluginFailed for that id and loads no copy.

Publishing for the launcher

The launcher installs plugins from GitHub releases (eriknihlen/openac-plugins lists them). The same stricter contract applies to a plugin folder unzipped by hand into the plugins directory: the launcher checks it the same way, since minHostVersion and hosts are required for any plugin the launcher runs, not only one it downloaded itself. A plugin author who wants a plugin installable through the launcher follows a stricter contract than the fields above:

Field Launcher install
id must match ^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)+$, for example edwards.buffbot
version SemVer 2.0, equal to the release tag minus a leading v
minHostVersion required
hosts required, non-empty

Release, one per version:

  • Public repository, releases not drafts. The release GitHub marks latest must have no SemVer prerelease part and be the highest version; the launcher refuses a prerelease latest and refuses to install an older release than the one it already has.
  • Tag: v<version>, for example v1.2.0.
  • Three assets, exact names, and an optional fourth:
    • plugin.json, byte-identical to the one at the root of the zip
    • <id>-<version>.zip, with plugin.json at the zip root (not inside a folder) plus the entry DLL and its dependencies
    • <id>-<version>.zip.sha256, the output of shasum -a 256 <zip> (hash, optional whitespace and file name)
    • icon.png, byte-identical to the zip's copy, only if the zip has one

Icon: a plugin may ship one icon.png at the root of its zip, PNG only, exactly 64x64, at most 64 KiB, not animated. No icon is fine, but an icon that breaks a rule refuses the whole install. icon.jpg and icon.jpeg are never accepted at the zip root, in any case; an icon in a subfolder is untouched by this rule.

Managed code only, by allowlist. Every file in the zip must end in one of: .dll, .pdb, .json, .xml, .txt, .md, .png, .jpg, .jpeg, .ttf, .otf. A runtimes/ folder is rejected.

Caps: zip at most 64 MiB. Extraction: 2,000 entries, 64 MiB per entry, 256 MiB total, compression ratio 200. A hand-installed folder is limited the same way, counting files rather than zip entries.

Ignored, not refused: .DS_Store, ._*, Thumbs.db and desktop.ini, in any case, at any depth, on a hand-installed folder.

Recommended: enable immutable releases on the repository.

The launcher never loads, reflects over, or runs a downloaded file. It unzips, verifies the .sha256, and stages the plugin disabled; enabling it is a separate, explicit choice the player makes on the Plugins tab.

Capabilities

The launcher shows a player what a plugin does before they install it, and asks again when an update changes the answer. That disclosure is the author's, declared in the manifest. The client does not read or enforce this field; it is a launcher surface only.

{
  "capabilitiesVersion": 1,
  "capabilities": [
    { "name": "network", "note": "Checks this plugin's own repository for updates." },
    { "name": "chat", "note": "Reads tells addressed to it and replies to them." }
  ]
}
Name Declare it when the plugin
network opens any network connection of its own
analytics reports usage anywhere off the player's machine
fileWrite writes outside its own plugin directory
processLaunch starts another process
nativeCode loads or runs native code
inputAutomation synthesises keyboard or mouse input
chat reads chat or sends it (one flag covers both)

Declaration order is display order. The launcher shows the entries in the order the manifest lists them, so a heavier capability cannot be buried below benign ones.

Every entry needs a note, and the note is the sentence the player reads. It is the author's own words rendered on the install surface, so it is held to a strict shape: at most 120 characters, trimmed, no control characters, no Unicode formatting characters (bidirectional overrides and zero-width characters, which can disguise what a note says), and no links — a note containing :// is refused. A missing or empty note fails to parse.

The launcher refuses what it cannot read. A capability name it does not recognise, a duplicate name (case-insensitively), or a capabilitiesVersion newer than the vocabulary this launcher understands all refuse the install outright rather than installing with an incomplete disclosure. Declaring nothing is fine; declaring something unreadable is not. When an update declares a different set than the installed version, the launcher asks the player to agree again before it applies.

The current vocabulary version is 1.

Beta releases

A player can opt a plugin into a beta channel that offers a pre-release build. To publish one:

  • Mark the release a GitHub prerelease, not the release GitHub marks latest.
  • Tag: v<version>, where <version> has a SemVer prerelease part, for example v1.3.0-beta.1. No build metadata (+...) on a beta tag.
  • Same assets, same checks, as a stable release.
gh release create v1.3.0-beta.1 --prerelease \
  plugin.json edwards.buffbot-1.3.0-beta.1.zip edwards.buffbot-1.3.0-beta.1.zip.sha256

In CI, the prerelease flag can follow the tag itself: prerelease: ${{ contains(github.ref_name, '-') }}.

To promote a beta, publish a new stable release with the higher version; don't edit the beta release in place. The launcher reads prereleases from the repository's releases feed, which shows only the 10 newest releases.