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.
Create a NativeAOT class library project
Create a new .NET class library and configure it for NativeAOT output:<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.
Implement IPlugin
Create your main plugin class implementing IPlugin. Every method that Collapse calls must be implemented: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;
}
Implement IPluginPresetConfig
Each preset config represents one game region. Implement every method your region needs: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() { }
}
Add the required C ABI exports
Collapse resolves these exports by name using NativeLibrary.TryGetExport. All four are required: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.
Add optional exports (recommended)
Optional exports enable additional integration. Implement them as needed: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;
}
}
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.
Create manifest.json
Create manifest.json in the same directory as your DLL. All fields are required:{
"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. 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. Install via Plugin Manager
- Open Collapse Launcher and go to Settings.
- Navigate to the Plugin Manager section.
- Click Install Plugin and select either the
.zip package or the manifest.json file directly.
- Collapse copies all assets to
%AppData%\CollapseLauncher\Plugins\Hi3Helper.Plugin.MyGame.Plugin\ and registers the plugin.
- 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.