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.

Lifecycle overview

A plugin progresses through the following states from the perspective of PluginInfo and PluginManager:
[Discovered] → [Pending routines applied] → [Loaded / Disabled]

                                              [Initialized]

                                     ┌───────────────┴──────────────┐
                               [Running]                       [Disabled]
                                  ↓                                 ↓
                    [Update check] → [Update staged]        [Re-enabled]

                                   [Applied on next launch]

                                    [Uninstalled]

Phase 1: Discovery and pending routines

At launcher startup, PluginManager.LoadPlugins scans the plugin folder for subdirectories matching Hi3Helper.Plugin.*. Before any plugin is loaded, ApplyPendingRoutines runs once over the entire plugin folder.

_markPendingDeletion

If _markPendingDeletion exists in a plugin directory, ApplyPendingUninstallRoutine runs:
  1. Reads manifest.json to obtain the asset list.
  2. Deletes every file listed in Assets[].
  3. Deletes manifest.json itself.
  4. Deletes _markPendingDeletion.
  5. Removes the directory if it is now empty.
This is the only time uninstallation actually removes files. Marking a plugin for deletion while it is loaded (from the Plugin Manager) sets the sentinel file on disk; the files are removed the next time the launcher starts.

_markPendingUpdate / _markPendingUpdateApply

If _markPendingUpdate/<plugin-dir>/_markPendingUpdateApply exists, ApplyPendingUpdateRoutine runs:
  1. Deletes _markPendingUpdateApply.
  2. Removes all files from the parent plugin directory that are not inside _markPendingUpdate/.
  3. Moves all files from _markPendingUpdate/ into the parent directory.
  4. Cleans up the now-empty _markPendingUpdate/ staging directory.

Phase 2: Load or skip

For each plugin directory that has a valid manifest.json:
  • If _markDisabled exists, the plugin is constructed without loading the DLL (load = false). It appears in the Plugin Manager as disabled. No exports are resolved and no COM objects are created.
  • If _markDisabled is absent, the plugin DLL is loaded and the full initialization sequence runs.

Phase 3: DLL loading and initialization

When loading the DLL:
  1. NativeLibrary.TryLoad maps the DLL into the process. If it fails, the error is thrown and the plugin is skipped.
  2. The four required exports are resolved. Missing any one aborts the load.
  3. The logger callback is injected via SetLoggerCallback.
  4. GetPluginStandardVersion and GetPluginVersion are called. Their results are stored on the PluginInfo object.
  5. GetPlugin is called and the pointer is marshalled to IPlugin.
  6. IPlugin.GetPluginSelfUpdater is called to obtain an optional IPluginSelfUpdate reference.
  7. GetPluginUpdateCdnList is optionally resolved and called to obtain the CDN URL list.
  8. IPlugin.GetPluginName, GetPluginDescription, GetPluginAuthor, and GetPluginCreationDate are called.
  9. IPlugin.SetPluginLocaleId is called with the launcher’s current UI language.
  10. All IPluginPresetConfig instances are enumerated and wrapped into PluginPresetConfigWrapper objects.
  11. If the speed limiter is active, RegisterSpeedThrottlerService is called.
  12. PluginInfo.Initialize is called, which sets up DNS resolvers (if configured) and calls IPluginPresetConfig.InitAsync on each preset config.
Collapse checks if the plugin DLL is UPX-compressed during load. UPX-packed plugins are flagged with a warning log entry. The plugin still loads, but runtime stability is not guaranteed.

Phase 4: Running

Once loaded and initialized, a plugin is “running”. Collapse calls into the plugin on demand:
  • Home page loadPluginLauncherApiWrapper.LoadAsync calls IGameManager.InitPluginComAsync, ILauncherApiMedia.InitPluginComAsync, and ILauncherApiNews.InitPluginComAsync in parallel. On completion, background images, news articles, carousels, and social media entries are converted from plugin types to Collapse’s internal structures.
  • Game stateIGameManager.IsGameInstalled, IsGameHasUpdate, and IsGameHasPreload are called whenever the launcher needs to refresh game status.
  • LaunchIGameManager provides the path; the launcher’s game launch infrastructure invokes the game directly.
  • Locale changePluginManager.SetPluginLocaleId propagates the new locale to every loaded plugin via IPlugin.SetPluginLocaleId.

Sentinel files

Sentinel files are plain text files placed in the plugin directory. Their presence or absence is the state mechanism for enable/disable and deferred operations.
FileLocationMeaning
_markDisabled<pluginDir>/Plugin is disabled. DLL not loaded. Removed when the user re-enables the plugin.
_markPendingDeletion<pluginDir>/Plugin is queued for uninstallation. Files are deleted on next launcher startup.
_markPendingUpdate/<pluginDir>/Staging directory containing new plugin files downloaded during an update.
_markPendingUpdateApply<pluginDir>/_markPendingUpdate/Stamp file written when update download completes. Triggers ApplyPendingUpdateRoutine on next launch.
Sentinel files contain their own name as content (e.g., _markDisabled contains the text _markDisabled). The launcher only checks for file existence, not content.

Enabling and disabling

Setting PluginInfo.IsEnabled = false creates _markDisabled. Setting it to true deletes it. The change takes effect on the next launcher startup — a running plugin cannot be hot-reloaded. The Plugin Manager shows a restart-required indicator when IsChanged is set.

Marking for deletion

Setting PluginInfo.IsMarkedForDeletion = true creates _markPendingDeletion. The Plugin Manager allows restoring a plugin before the next launch by setting the value back to false, which deletes the sentinel.

Plugin updates

Updates can be driven in two ways, which Collapse tries in order:

Managed update path (CDN)

Requires GetPluginUpdateCdnList to return at least one URL.
  1. Collapse picks a random URL from the list and fetches <url>/manifest.json.
  2. The remote PluginVersion is compared to the installed version. If equal, no update is available.
  3. If an update is available, each asset in the remote manifest is downloaded to <pluginDir>/_markPendingUpdate/<asset.FilePath>.
  4. MD5 hashes are verified for every downloaded file. Up to five retries per file on error.
  5. On completion, _markPendingUpdateApply is written inside _markPendingUpdate/.
  6. On next launch, ApplyPendingUpdateRoutine moves the staged files into place.

Unmanaged update path (IPluginSelfUpdate)

Used if IPlugin.GetPluginSelfUpdater returns a non-null IPluginSelfUpdate.
  1. IPluginSelfUpdate.TryPerformUpdateAsync is called with checkOnly = true to check availability.
  2. The returned SelfUpdateReturnInfo is inspected for UpdateIsAvailable and the new version.
  3. If an update is available, TryPerformUpdateAsync is called again with checkOnly = false. The plugin downloads its own files into outputDir (_markPendingUpdate/).
  4. The stamp file _markPendingUpdateApply is written by Collapse on success.
  5. On next launch, ApplyPendingUpdateRoutine applies the staged files.
If both GetPluginUpdateCdnList and IPluginSelfUpdate are available, Collapse tries the managed CDN path first. The unmanaged path is only used if the managed path returns false.

Auto-update

When IsEnablePluginAutoUpdate is true in launcher settings, Collapse runs StartBackgroundUpdateTask on startup. This calls RunCheckUpdateTask and, if an update is found, immediately calls RunUpdateTask for each eligible plugin in parallel.

Logging via SetLoggerCallback

After SetLoggerCallback is called, the plugin receives a function pointer with this signature:
void LoggerCallback(LogLevel* logLevel, EventId* eventId, char* messageBuffer, int messageLength)
The messageBuffer is a UTF-16 character array of length messageLength (not null-terminated). Log output appears in Collapse’s unified log with the plugin’s relative path as the logger name (<directoryName>/<dllName>). When the plugin is disposed, Collapse calls SetLoggerCallback(nint.Zero). The plugin must treat a zero pointer as a signal to stop logging and release any references to the callback.
// Example: logging from the plugin
private static unsafe void Log(LogLevel level, string message)
{
    if (_loggerCallback == null) return;
    fixed (char* msgPtr = message)
    {
        LogLevel lvl = level;
        EventId  eid = default;
        _loggerCallback(&lvl, &eid, msgPtr, message.Length);
    }
}

Speed throttler integration

When the user enables the download speed limiter in Collapse Settings, the launcher calls RegisterSpeedThrottlerService on every loaded plugin that exports it:
HResult RegisterSpeedThrottlerService(nint addBytesCallback, nint getThrottleCallback)
  • addBytesCallback — Call this with the number of bytes downloaded in each chunk. The callback may block (or return a task) if the throttle limit has been reached.
  • getThrottleCallback — Query the current throttle byte-per-second limit.
When the speed limiter is disabled, Collapse calls RegisterSpeedThrottlerService(nint.Zero, nint.Zero) to deregister. Your plugin should check for zero pointers and skip throttle integration when they are null.
// Example integration inside a download loop
while ((read = await responseStream.ReadAsync(buffer)) > 0)
{
    await fileStream.WriteAsync(buffer.AsMemory(0, read));
    // Report bytes to throttler (may await if limit reached)
    if (_addBytesCallback != null)
        await _addBytesCallback(read);
}

Disposal

When the launcher exits or UnloadPlugins is called, each PluginInfo is disposed in order:
  1. DisableDnsResolver — passes nint.Zero to SetDnsResolverCallback and SetDnsResolverCallbackAsync.
  2. SetLoggerCallback(nint.Zero) — detaches the logger.
  3. Each PluginPresetConfigWrapper.Dispose — releases COM references for ILauncherApiMedia, ILauncherApiNews, IGameManager, IGameInstaller, and IPluginPresetConfig.
  4. FreePlugin() — called if the export exists. This is the preferred teardown path.
  5. If FreePlugin is absent, Instance?.Free() is called directly on the IPlugin COM object.
  6. ComMarshal<IPlugin>.TryReleaseComObject — decrements the COM reference count.
  7. NativeLibrary.Free(Handle) — unmaps the DLL from the process.
  8. The logger GC handle is freed and the callback delegate is nullified.

Build docs developers (and LLMs) love