Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/CollapseLauncher/Collapse/llms.txt

Use this file to discover all available pages before exploring further.

This guide walks you through building a minimal plugin DLL that Collapse Launcher can load. The reference language is C# with NativeAOT, but any language that can produce a Windows DLL with the correct C ABI exports works.
You need the Hi3Helper.Plugin.Core NuGet package. Add it to your project before following the steps below.
1

Create a NativeAOT class library project

Create a new .NET class library and configure it for NativeAOT output:
MyGame.Plugin.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0-windows</TargetFramework>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <PublishAot>true</PublishAot>
    <!-- Produce a native DLL, not an EXE -->
    <OutputType>Library</OutputType>
    <!-- Disable reflection trimming warnings for COM interop -->
    <TrimmerRootDescriptor>TrimmerRoots.xml</TrimmerRootDescriptor>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Hi3Helper.Plugin.Core" Version="*" />
  </ItemGroup>
</Project>
Set <PublishAot>true</PublishAot> — this is what enables NativeAOT. Regular dotnet build still works for development; use dotnet publish to produce the final native DLL.
2

Implement IPlugin

Create your main plugin class implementing IPlugin. Every method that Collapse calls must be implemented:
MyGamePlugin.cs
using System;
using System.Runtime.InteropServices;
using Hi3Helper.Plugin.Core;
using Hi3Helper.Plugin.Core.Management.PresetConfig;
using Hi3Helper.Plugin.Core.Update;

namespace MyGame.Plugin;

[ComVisible(true)]
public sealed class MyGamePlugin : IPlugin
{
    private static readonly MyGamePlugin _instance = new();
    private static IPluginPresetConfig[] _presetConfigs = [new MyGameGlobalPreset()];

    // Called by Collapse to retrieve the plugin name
    public void GetPluginName(out string? name) => name = "My Game";

    public void GetPluginDescription(out string? description)
        => description = "Community plugin for My Game";

    public void GetPluginAuthor(out string? author) => author = "Your Name";

    public unsafe void GetPluginCreationDate(out DateTime* creationDate)
    {
        // Return null to let Collapse fall back to the manifest date
        creationDate = null;
    }

    public void GetPresetConfigCount(out int count) => count = _presetConfigs.Length;

    public void GetPresetConfig(int index, out IPluginPresetConfig presetConfig)
        => presetConfig = _presetConfigs[index];

    // Return null if the plugin does not implement self-updating via IPluginSelfUpdate
    public void GetPluginSelfUpdater(out IPluginSelfUpdate? selfUpdater) => selfUpdater = null;

    // Collapse passes the launcher's current UI locale ID so the plugin can localise its strings
    public void SetPluginLocaleId(string localeId) { /* update internal locale */ }

    // COM boilerplate
    public void Free() { }
    public Guid GetPluginGuid() => typeof(MyGamePlugin).GUID;

    internal static MyGamePlugin Instance => _instance;
}
3

Implement IPluginPresetConfig

Each preset config represents one game region. Implement every method your region needs:
MyGameGlobalPreset.cs
using Hi3Helper.Plugin.Core.Management.PresetConfig;
using Hi3Helper.Plugin.Core.Management.Api;
using Hi3Helper.Plugin.Core.Management;
using Hi3Helper.Plugin.Core.DiscordPresence;
using Hi3Helper.Plugin.Core;
using System;
using System.Runtime.InteropServices;

namespace MyGame.Plugin;

[ComVisible(true)]
public sealed class MyGameGlobalPreset : IPluginPresetConfig
{
    private readonly MyGameManager       _gameManager  = new();
    private readonly MyGameMediaApi      _mediaApi     = new();
    private readonly MyGameNewsApi       _newsApi      = new();

    // Called once after the preset is constructed
    public void InitAsync(Guid cancelToken, out nint asyncResult)
    {
        // Return a completed task result — use Task.Run for real async work
        asyncResult = nint.Zero; // 0 = success (synchronous)
    }

    public void comGet_GameManager(out IGameManager? gameManager)
        => gameManager = _gameManager;

    public void comGet_LauncherApiMedia(out ILauncherApiMedia? media)
        => media = _mediaApi;

    public void comGet_LauncherApiNews(out ILauncherApiNews? news)
        => news = _newsApi;

    public void comGet_GameName(out string result)        => result = "My Game";
    public void comGet_ZoneName(out string result)        => result = "Global";
    public void comGet_ZoneFullName(out string result)    => result = "My Game — Global";
    public void comGet_ProfileName(out string result)     => result = "MyGame";
    public void comGet_ZoneDescription(out string result) => result = "Official global server";

    public void comGet_GameExecutableName(out string result)          => result = "MyGame.exe";
    public void comGet_LauncherGameDirectoryName(out string result)   => result = "MyGame";
    public void comGet_GameRegistryKeyName(out string? result)        => result = null;
    public void comGet_GameVendorName(out string result)              => result = "MyStudio";
    public void comGet_GameMainLanguage(out string result)            => result = "en-us";
    public void comGet_GameLogFileName(out string? result)            => result = "output_log.txt";
    public void comGet_GameAppDataPath(out string? result)            => result = null;
    public void comGet_ZoneLogoUrl(out string result)                 => result = "https://example.com/logo.png";
    public void comGet_ZonePosterUrl(out string result)               => result = "https://example.com/poster.jpg";
    public void comGet_ZoneHomePageUrl(out string result)             => result = "https://example.com/";

    public void comGet_ReleaseChannel(out GameReleaseChannel result)  => result = GameReleaseChannel.Stable;

    public void comGet_GameSupportedLanguagesCount(out int count)     => count = 1;
    public void comGet_GameSupportedLanguages(int index, out string result) => result = "en-us";

    public void comGet_GameInstaller(out IGameInstaller? installer)   => installer = null;

    public void Free() { }
}
4

Add the required C ABI exports

Collapse resolves these exports by name using NativeLibrary.TryGetExport. All four are required:
Exports.cs
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
using Hi3Helper.Plugin.Core;
using Hi3Helper.Plugin.Core.Update;
using Microsoft.Extensions.Logging;

namespace MyGame.Plugin;

public static unsafe class Exports
{
    // Collapse reads the plugin standard version for compatibility checks
    [UnmanagedCallersOnly(EntryPoint = "GetPluginStandardVersion", CallConvs = [typeof(CallConvCdecl)])]
    public static GameVersion* GetPluginStandardVersion()
    {
        // Must match the standard version declared in your manifest
        return (GameVersion*)Unsafe.AsPointer(ref PluginVersions.Standard);
    }

    // The actual version of this plugin release
    [UnmanagedCallersOnly(EntryPoint = "GetPluginVersion", CallConvs = [typeof(CallConvCdecl)])]
    public static GameVersion* GetPluginVersion()
    {
        return (GameVersion*)Unsafe.AsPointer(ref PluginVersions.Plugin);
    }

    // Returns the IPlugin COM object pointer
    [UnmanagedCallersOnly(EntryPoint = "GetPlugin", CallConvs = [typeof(CallConvCdecl)])]
    public static void* GetPlugin()
    {
        return ComInterfaceMarshaller<IPlugin>.ConvertToUnmanaged(MyGamePlugin.Instance);
    }

    // Collapse injects a logging callback so plugin logs appear in its log system
    private static SharedLoggerCallback? _loggerCallback;

    [UnmanagedCallersOnly(EntryPoint = "SetLoggerCallback", CallConvs = [typeof(CallConvCdecl)])]
    public static void SetLoggerCallback(nint callbackPtr)
    {
        if (callbackPtr == nint.Zero)
        {
            _loggerCallback = null;
            return;
        }
        _loggerCallback = Marshal.GetDelegateForFunctionPointer<SharedLoggerCallback>(callbackPtr);
    }

    // Called when the plugin is being unloaded — free all unmanaged resources here
    [UnmanagedCallersOnly(EntryPoint = "FreePlugin", CallConvs = [typeof(CallConvCdecl)])]
    public static void FreePlugin()
    {
        // Release COM objects, dispose native memory, etc.
    }

    // Helper: write a log message through the injected callback
    internal static unsafe void Log(LogLevel level, string message)
    {
        if (_loggerCallback == null) return;
        fixed (char* msgPtr = message)
        {
            EventId eventId = default;
            _loggerCallback(&level, &eventId, msgPtr, message.Length);
        }
    }
}

internal static class PluginVersions
{
    // Standard version: must match the plugin SDK standard version
    public static GameVersion Standard = new(1, 0, 0, 0);
    // Plugin version: your own release version
    public static GameVersion Plugin   = new(1, 0, 0, 0);
}
GetPluginStandardVersion and GetPluginVersion return pointers to static GameVersion structs. Ensure those fields have process lifetime (e.g., static readonly fields). Returning a pointer to a stack variable will cause memory corruption.
5

Add optional exports (recommended)

Optional exports enable additional integration. Implement them as needed:
Exports.Optional.cs
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Hi3Helper.Plugin.Core;

namespace MyGame.Plugin;

public static unsafe class OptionalExports
{
    // Provide CDN base URLs for managed update checking.
    // Collapse will fetch manifest.json from each URL to check for updates.
    [UnmanagedCallersOnly(EntryPoint = "GetPluginUpdateCdnList", CallConvs = [typeof(CallConvCdecl)])]
    public static void GetPluginUpdateCdnList(out int count, out nint urlArrayPtr)
    {
        // Return a null-terminated UTF-16 string array allocated in native memory
        // See Hi3Helper.Plugin.Core documentation for the expected layout
        count      = 0;
        urlArrayPtr = nint.Zero;
    }

    // Collapse injects a synchronous DNS resolver so plugin HTTP clients
    // use the same custom DNS servers as the launcher.
    [UnmanagedCallersOnly(EntryPoint = "SetDnsResolverCallback", CallConvs = [typeof(CallConvCdecl)])]
    public static void SetDnsResolverCallback(nint callbackPtr) { /* store and use */ }

    // Async variant of the DNS resolver callback
    [UnmanagedCallersOnly(EntryPoint = "SetDnsResolverCallbackAsync", CallConvs = [typeof(CallConvCdecl)])]
    public static void SetDnsResolverCallbackAsync(nint callbackPtr) { /* store and use */ }

    // Register with the launcher's global speed throttler.
    // Pass nint.Zero for both parameters to deregister.
    [UnmanagedCallersOnly(EntryPoint = "RegisterSpeedThrottlerService", CallConvs = [typeof(CallConvCdecl)])]
    public static HResult RegisterSpeedThrottlerService(nint addBytesCallback, nint getThrottleCallback)
    {
        // Store callbacks and integrate with your HTTP download logic
        return HResult.S_OK;
    }
}
6

Build the native DLL with NativeAOT

Publish a self-contained native DLL:
dotnet publish MyGame.Plugin.csproj \
  --configuration Release \
  --runtime win-x64 \
  --self-contained true \
  -p:PublishAot=true
The output directory will contain MyGame.Plugin.dll (native, no .NET runtime dependency) alongside any PDB and other files. Only the .dll is required by Collapse.
You can cross-compile for win-x64 from Linux or macOS with the NativeAOT cross-compilation toolchain, but building on Windows is the most straightforward path.
7

Create manifest.json

Create manifest.json in the same directory as your DLL. All fields are required:
manifest.json
{
  "MainLibraryName": "MyGame.Plugin.dll",
  "MainPluginName": "My Game",
  "MainPluginDescription": "Community plugin for My Game — Global server",
  "MainPluginAuthor": "Your Name",
  "PluginVersion": "1.0.0.0",
  "PluginStandardVersion": "1.0.0.0",
  "PluginCreationDate": "2026-01-01T00:00:00Z",
  "Assets": [
    {
      "FilePath": "MyGame.Plugin.dll",
      "FileHash": "a1b2c3d4e5f6..."
    }
  ]
}
FileHash is the MD5 hash of the file as a lowercase hex string. Collapse verifies this hash during managed updates.See the manifest reference for full field documentation.
8

Package as a ZIP

Zip the directory contents (not the directory itself) so manifest.json is at the archive root:
# On Windows (PowerShell)
Compress-Archive -Path .\* -DestinationPath MyGame.Plugin.zip

# On Linux/macOS
zip -j MyGame.Plugin.zip manifest.json MyGame.Plugin.dll
The importer accepts Brotli-compressed files inside the ZIP (.br extension). Collapse automatically decompresses them during installation.
9

Install via Plugin Manager

  1. Open Collapse Launcher and go to Settings.
  2. Navigate to the Plugin Manager section.
  3. Click Install Plugin and select either the .zip package or the manifest.json file directly.
  4. Collapse copies all assets to %AppData%\CollapseLauncher\Plugins\Hi3Helper.Plugin.MyGame.Plugin\ and registers the plugin.
  5. The new plugin appears in the Plugin Manager list. No restart is required for the plugin to be listed, but a full launcher restart may be needed for it to appear in the game selector.
If you install from a manifest.json file rather than a ZIP, all asset files referenced in Assets[] must be present in the same directory as the manifest.

Build docs developers (and LLMs) love