Packaging and testing

Build the quick-start plugin first. A distribution package contains your assembly, a manifest, and the files it actually needs at runtime. It does not include a private copy of the host SDK.

Manifest and compatibility

Use a version 2 plugin.json alongside the entry DLL. The quick-start manifest shows the required fields; this example adds compatibility and dependency metadata:

plugin.json
{
  "manifestVersion": 2,
  "id": "com.example.myplugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "entryPoint": "MyPlugin.dll",
  "description": "Adds a text prefix node and an example page.",
  "authors": ["Example author"],
  "sdkVersion": "1.15.0",
  "platforms": ["win-x64"],
  "tags": ["flow"]
}

Replace the example identity and description with your own. id uses lowercase letters and digits separated by dots, underscores, or hyphens. entryPoint names the assembly inside the package. Keep id and version consistent with the entry class, and use semantic versions.

  • minAppVersion is the oldest app version you have verified. Set it when your plugin needs APIs or behavior absent from older apps.
  • sdkVersion records the SDK you compiled against. The host rejects a newer SDK major version; it does not enforce availability of every minor-version API.
  • platforms lists supported OS/architecture identifiers. win-x64, linux-x64, and osx-arm64 are examples. An omitted or empty list removes the platform gate; it does not make native code portable.
  • dependencies accepts objects with id, optional range, and optional optional. These are plugin-package requirements, distinct from NuGet assemblies bundled with your plugin.

The SDK source includes plugin.schema.json for editor validation. Runtime validation is not a guarantee of complete API compatibility. Test supported app versions, including the oldest one you declare.

The current host does not automatically install manifest dependencies or enforce their version ranges during plugin initialization. Implement IPluginWithDependencies for runtime ID checks and ordering, and document which other packages users must install. See dependencies.

Package layout

Use a dedicated folder so your dependencies do not mix with other plugins:

MyPlugin/
  plugin.json
  MyPlugin.dll
  MyPlugin.deps.json
  MyPlugin.pdb
  Dependency.dll
  runtimes/
    win-x64/
      native/
        NativeDependency.dll

Only include files that apply to your plugin. PDB files are optional debug symbols. Include required managed dependencies and native libraries with their expected paths; a build that succeeds on your machine can still fail on a clean machine missing those files.

The host shares MultiShock.PluginSdk.dll and framework assemblies from its own load context. Do not bundle the SDK, host executable, or an entire .NET runtime. Use compatible framework package references instead of attempting to override the host's System.* or Microsoft.* assemblies.

For private managed dependencies that are not copied by default, build with CopyLocalLockFileAssemblies enabled and review the resulting files:

dotnet build .\MyPlugin.csproj -c Release -p:CopyLocalLockFileAssemblies=true

Copy only your runtime payload into a clean staging directory. For the basic quick-start plugin with no private dependencies:

New-Item -ItemType Directory -Path .\package
Copy-Item -LiteralPath .\bin\Release\net10.0\MyPlugin.dll -Destination .\package
Copy-Item -LiteralPath .\plugin.json -Destination .\package

Use a new or empty staging directory for every release. Add your actual private dependencies and metadata files before packaging; these two copies are sufficient only for the basic SDK-only example.

Install locally

  1. Save work, stop automation, and close MultiShock.
  2. Open %LocalAppData%\PiShock\MultiShock\Plugins\ on Windows.
  3. Copy the staged directory there as Plugins\MyPlugin\, with plugin.json directly inside it.
  4. Remove or move aside older loose DLLs or packages with the same plugin ID. Keep only one installed copy.
  5. Start MultiShock. Check Manage plugins and Debug & diagnostics → Plugins for the loaded version and any errors.
  6. Turn your page to Shown under Configure → Sidebar pages.

Loose DLL loading is also supported. Install from file copies the selected DLL and matching metadata files, not every private dependency or a directory's manifest. Use a manifest folder or package for a multi-file plugin.

The host loads shadow copies to reduce source-file locking. Those copies are temporary implementation data, not your install location or settings directory. Always update the original installed source and store settings under GetPluginDataPath.

Development reload

For a local DLL or manifest-directory installation:

  1. Create an empty MyPlugin.dll.dev next to the entry DLL, or an empty .dev file in that plugin's directory.
  2. Restart MultiShock once. Confirm the plugin shows Dev mode.
  3. Rebuild and copy the changed payload to the same installed location.
  4. Open Manage plugins → Developer and click Reload for the plugin.

The Developer tab also provides controls for development plugins. Reload replaces plugin pages and services; it does not update the flow runtime's existing node type registrations. Restart the app after flow-node changes and when checking startup compatibility or changed manifests.

Use PluginServiceLocator for plugin-specific services in reloadable pages so they resolve from the current plugin provider. If you own listeners, sockets, hotkeys, or background tasks, implement reload cleanup as described in lifecycle. A .dev marker enables a loader mode; it does not clean up your resources for you.

Do not distribute .dev markers with release packages. The host can also remember development mode in plugin configuration, so verify the mode when testing a release in an existing profile.

Create a distributable archive

A .msplugin file is a ZIP archive with plugin.json and the entry DLL at its root. Archive the contents of the staging folder, not a parent folder:

Compress-Archive -Path .\package\* -DestinationPath .\MyPlugin.zip
Move-Item -LiteralPath .\MyPlugin.zip -Destination .\MyPlugin.msplugin

Choose unused output names or move previous build artifacts aside first. Inspect the archive before distributing it. Users can copy it into their plugin folder and restart; see installing plugins. Keep plugin-owned settings and credentials out of the archive.

For catalog contributions, use the plugins repository and its current contribution/build workflow. Do not assume publishing a DLL or a NuGet package makes it appear in Browse. Catalog downloads carry a SHA-256 checksum that the host verifies before installation.

Verify before release

  • Test a fresh app launch with the actual staged package and required dependencies. Your development output directory is not a substitute for the archive.
  • Check the declared plugin ID/version, app and SDK compatibility, native platform, sidebar pages, configuration save/load after restart, and missing optional integrations.
  • Exercise node defaults, invalid inputs, trigger start/stop, and two simultaneous instances using the node guide.
  • Test disconnect and initialization failure paths, then inspect logs for useful errors and accidental secrets.
  • Test reload only if you support it, and confirm listeners or hotkeys are not duplicated. Test normal startup again without a development marker.
  • Keep device-output testing deliberate: choose an explicit target and vibration, verify saved limits, and keep Stop everything reachable.

Diagnose loading and reload failures

SymptomCheck
Plugin never appearsCorrect data folder, manifest, entry filename, public parameterless entry class, private dependencies, and diagnostics errors
Compatibility rejectionminAppVersion, SDK major version, and platforms; then actual API compatibility
Installed twiceAn old DLL, extracted directory, or archive with the same plugin ID
Page missingFull normalized @page route, navigation Href, and Sidebar pages visibility
Service missing after reloadCurrent provider through PluginServiceLocator, service registration, and initialization errors
Native load failureOS/architecture, native payload, transitive native prerequisites, and runtimes/<rid>/native/ paths
Old flow-node behaviorRestart the app after node changes
File copy deniedClose the app and other processes holding the source files; copy to the original install directory

Collect Recent Plugin Errors, the app and plugin versions, and the main/plugin logs from the failing run. See troubleshooting.

Contributors

Thank you to everyone who has helped improve this documentation.