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
pluginFailedwith 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 examplev1.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, withplugin.jsonat the zip root (not inside a folder) plus the entry DLL and its dependencies<id>-<version>.zip.sha256, the output ofshasum -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 examplev1.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.