diff --git a/Hi3Helper.Plugin.Core.csproj b/Hi3Helper.Plugin.Core.csproj
index 84e292f..eb717a7 100644
--- a/Hi3Helper.Plugin.Core.csproj
+++ b/Hi3Helper.Plugin.Core.csproj
@@ -22,7 +22,7 @@
-
+
diff --git a/PluginTest/PluginTest.csproj b/PluginTest/PluginTest.csproj
index 5dcf828..9f9e1fa 100644
--- a/PluginTest/PluginTest.csproj
+++ b/PluginTest/PluginTest.csproj
@@ -13,8 +13,8 @@
-
-
+
+
diff --git a/PluginTest/Program.cs b/PluginTest/Program.cs
index 40292d2..4df6f28 100644
--- a/PluginTest/Program.cs
+++ b/PluginTest/Program.cs
@@ -23,6 +23,13 @@ private static async Task Main(string[] args)
args = [..args.Where(x => !x.Equals("NoWait", StringComparison.OrdinalIgnoreCase))];
+ if (args.Contains("GameSettingsContract", StringComparer.OrdinalIgnoreCase))
+ {
+ Test.TestGameSettingsContract();
+ Console.WriteLine("Game settings contract passed!");
+ return 0;
+ }
+
foreach (var arg in args)
{
var result = await TryPerformLibraryTest(arg);
@@ -144,7 +151,7 @@ private static bool LogInvokeTest(nint libraryHandle, string entryPointName,
}
private static void PrintHelp()
- => Console.WriteLine($"Usage:\r\n{Path.GetFileName(Environment.ProcessPath)} Path_to_dll_1 Path_to_dll_2 ...");
+ => Console.WriteLine($"Usage:\r\n{Path.GetFileName(Environment.ProcessPath)} Path_to_dll_1 Path_to_dll_2 ...\r\n{Path.GetFileName(Environment.ProcessPath)} GameSettingsContract");
}
public class InvokeLogger : ILogger
diff --git a/PluginTest/Test.GameSettings.cs b/PluginTest/Test.GameSettings.cs
new file mode 100644
index 0000000..ad489e0
--- /dev/null
+++ b/PluginTest/Test.GameSettings.cs
@@ -0,0 +1,31 @@
+using Hi3Helper.Plugin.Core.UI.Settings;
+using System;
+
+namespace PluginTest;
+
+internal static partial class Test
+{
+ internal static void TestGameSettingsContract()
+ {
+ GameSettingsPage source = new([
+ new GameSettingsSection("General", [
+ GameSettingEntry.Toggle("enabled", "Enabled", true),
+ GameSettingEntry.Text("name", "Name", "Collapse"),
+ GameSettingEntry.Number("count", "Count", 3, 0, 10),
+ GameSettingEntry.Slider("volume", "Volume", 75, 0, 100),
+ GameSettingEntry.Choice("language", "Language", "en", [
+ new GameSettingChoice("en", "English")
+ ])
+ ])
+ ]) { Title = "Settings" };
+
+ string json = GameSettingsPageSerializer.Serialize(source);
+ GameSettingsPage result = GameSettingsPageSerializer.Deserialize(json)
+ ?? throw new InvalidOperationException("Deserialization returned null");
+
+ if (result.Title != source.Title || result.Sections.Count != 1 || result.Sections[0].Entries.Count != 5)
+ {
+ throw new InvalidOperationException("The game settings contract did not round-trip");
+ }
+ }
+}
diff --git a/README.md b/README.md
index 64b0fea..c8ad6e7 100644
--- a/README.md
+++ b/README.md
@@ -11,9 +11,9 @@ Make sure that your code is **as reflection-free** as possible, as the code is e
# What's Included?
This Core Library includes various APIs to make the plugin development faster, without needing to implement the entire functions from scratch. Here's a list of what's included currently:
-### V1 (v0.1.5.0) Implementation Standard
+### V1 (v0.1.6.0) Implementation Standard
-This repository follows the V1 implementation standard (current library version: ``v0.1.5.0``). The standard collects base API contracts, COM interop helpers, marshallers and small utility primitives that plugin authors and the launcher can rely on.
+This repository follows the V1 implementation standard (current library version: ``v0.1.6.0``). The standard collects base API contracts, COM interop helpers, marshallers and small utility primitives that plugin authors and the launcher can rely on.
> [!WARNING]
> The API contracts and implementations are still under development, so expect some breaking changes in the future.
@@ -95,4 +95,4 @@ Quick-start links:
To see the example of how the plugin implemented using this Core Library, check the link below:
* [Hi3Helper.Plugin.HBR](https://github.com/CollapseLauncher/Hi3Helper.Plugin.HBR) (A basic plugin implementation for Game: [Heaven Burns Red](https://heavenburnsred.yo-star.com/) by [Key](https://key.visualarts.gr.jp/))
* [Hi3Helper.Plugin.Wuwa](https://github.com/CollapseLauncher/Hi3Helper.Plugin.Wuwa) (A plugin implementation for [Wuthering Waves](https://wutheringwaves.kurogames.com/en/main) by [Kuro Games](https://kurogames.com))
-* [Hi3Helper.Plugin.DNA](https://github.com/CollapseLauncher/Hi3Helper.Plugin.DNA) (A plugin implementation for [Duet Night Abyss](https://duetnightabyss.dna-panstudio.com//) by [Hero Games](https://herogame.com/))
\ No newline at end of file
+* [Hi3Helper.Plugin.DNA](https://github.com/CollapseLauncher/Hi3Helper.Plugin.DNA) (A plugin implementation for [Duet Night Abyss](https://duetnightabyss.dna-panstudio.com//) by [Hero Games](https://herogame.com/))
diff --git a/SharedStatic.V1Ext.cs b/SharedStatic.V1Ext.cs
index e3cc036..9ace8c1 100644
--- a/SharedStatic.V1Ext.cs
+++ b/SharedStatic.V1Ext.cs
@@ -26,6 +26,7 @@ static SharedStaticV1Ext()
InitExtension_Update3Exports();
InitExtension_Update4Exports();
InitExtension_Update5Exports();
+ InitExtension_Update6Exports();
}
///
diff --git a/SharedStatic.V1Ext_Update6.cs b/SharedStatic.V1Ext_Update6.cs
new file mode 100644
index 0000000..6b9da9e
--- /dev/null
+++ b/SharedStatic.V1Ext_Update6.cs
@@ -0,0 +1,117 @@
+using Hi3Helper.Plugin.Core.Management.PresetConfig;
+using Hi3Helper.Plugin.Core.UI.Settings;
+using Hi3Helper.Plugin.Core.Utility;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.Marshalling;
+
+namespace Hi3Helper.Plugin.Core;
+
+public partial class SharedStaticV1Ext
+{
+ internal unsafe delegate HResult GetGameSettingsPageDelegate(nint presetConfig,
+ out PluginDisposableMemoryMarshal pageJson);
+ internal unsafe delegate HResult SetGameSettingValueDelegate(nint presetConfig,
+ char* key, int keyLength,
+ char* value, int valueLength);
+ internal delegate HResult ApplyGameSettingsDelegate(nint presetConfig);
+}
+
+public partial class SharedStaticV1Ext
+{
+ private static unsafe void InitExtension_Update6Exports()
+ {
+ TryRegisterApiExport("GetGameSettingsPage", GetGameSettingsPage);
+ TryRegisterApiExport("SetGameSettingValue", SetGameSettingValue);
+ TryRegisterApiExport("ApplyGameSettings", ApplyGameSettings);
+ }
+
+ private static unsafe HResult GetGameSettingsPage(nint presetConfigP,
+ out PluginDisposableMemoryMarshal pageJson)
+ {
+ pageJson = PluginDisposableMemoryMarshal.Empty;
+ try
+ {
+ IPluginPresetConfig presetConfig = GetPresetConfig(presetConfigP);
+ GameSettingsPage? page = ThisExtensionExport.GetGameSettingsPageCore(presetConfig);
+ if (page == null)
+ {
+ return HResult.False;
+ }
+
+ pageJson = GameSettingsPageSerializer.Serialize(page);
+ return HResult.Ok;
+ }
+ catch (Exception ex)
+ {
+ InstanceLogger.LogError(ex, "An error occurred while retrieving the plugin game settings page");
+ return Marshal.GetHRForException(ex);
+ }
+ }
+
+ private static unsafe HResult SetGameSettingValue(nint presetConfigP,
+ char* key, int keyLength,
+ char* value, int valueLength)
+ {
+ try
+ {
+ IPluginPresetConfig presetConfig = GetPresetConfig(presetConfigP);
+ string keyString = new(key, 0, keyLength);
+ string valueString = new(value, 0, valueLength);
+ ThisExtensionExport.SetGameSettingValueCore(presetConfig, keyString, valueString);
+ return HResult.Ok;
+ }
+ catch (Exception ex)
+ {
+ InstanceLogger.LogError(ex, "An error occurred while updating plugin game setting {Key}",
+ key == null ? null : new string(key, 0, keyLength));
+ return Marshal.GetHRForException(ex);
+ }
+ }
+
+ private static HResult ApplyGameSettings(nint presetConfigP)
+ {
+ try
+ {
+ ThisExtensionExport.ApplyGameSettingsCore(GetPresetConfig(presetConfigP));
+ return HResult.Ok;
+ }
+ catch (Exception ex)
+ {
+ InstanceLogger.LogError(ex, "An error occurred while applying plugin game settings");
+ return Marshal.GetHRForException(ex);
+ }
+ }
+
+ private static unsafe IPluginPresetConfig GetPresetConfig(nint presetConfigP)
+ {
+ if (presetConfigP == nint.Zero)
+ {
+ throw new ArgumentNullException(nameof(presetConfigP));
+ }
+
+#if MANUALCOM
+ return ComWrappers.ComInterfaceDispatch.GetInstance(
+ (ComWrappers.ComInterfaceDispatch*)presetConfigP);
+#else
+ return ComInterfaceMarshaller.ConvertToManaged((void*)presetConfigP)
+ ?? throw new InvalidCastException("Cannot convert the preset config pointer to IPluginPresetConfig");
+#endif
+ }
+
+ ///
+ /// Returns the declarative settings page for a game preset, or null when the preset has no settings page.
+ ///
+ protected virtual GameSettingsPage? GetGameSettingsPageCore(IPluginPresetConfig presetConfig) => null;
+
+ ///
+ /// Receives a value edited by the user. Values use invariant strings; conversion and validation belong to the plugin.
+ ///
+ protected virtual void SetGameSettingValueCore(IPluginPresetConfig presetConfig, string key, string value) { }
+
+ ///
+ /// Persists the edited settings for the specified game preset.
+ ///
+ protected virtual void ApplyGameSettingsCore(IPluginPresetConfig presetConfig) { }
+}
diff --git a/SharedStatic.cs b/SharedStatic.cs
index 11e9f14..725ee89 100644
--- a/SharedStatic.cs
+++ b/SharedStatic.cs
@@ -103,7 +103,7 @@ static unsafe SharedStatic()
internal static string? ProxyPassword;
public static string PluginLocaleCode { get; internal set; } = "en-us";
- public static readonly GameVersion LibraryStandardVersion = new(0, 1, 5, 0);
+ public static readonly GameVersion LibraryStandardVersion = new(0, 1, 6, 0);
public static readonly ILogger InstanceLogger = new SharedLogger();
#if DEBUG
diff --git a/UI/Settings/GameSettingChoice.cs b/UI/Settings/GameSettingChoice.cs
new file mode 100644
index 0000000..0359b44
--- /dev/null
+++ b/UI/Settings/GameSettingChoice.cs
@@ -0,0 +1,10 @@
+namespace Hi3Helper.Plugin.Core.UI.Settings;
+
+///
+/// Defines one selectable value for a setting.
+///
+public sealed class GameSettingChoice(string value, string title)
+{
+ public string Value { get; init; } = value;
+ public string Title { get; init; } = title;
+}
diff --git a/UI/Settings/GameSettingEntry.cs b/UI/Settings/GameSettingEntry.cs
new file mode 100644
index 0000000..5b9c883
--- /dev/null
+++ b/UI/Settings/GameSettingEntry.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace Hi3Helper.Plugin.Core.UI.Settings;
+
+///
+/// Defines one editable setting in a plugin-provided game settings page.
+///
+public sealed class GameSettingEntry
+{
+ public required string Key { get; init; }
+ public required string Title { get; init; }
+ public string? Description { get; init; }
+ public required GameSettingKind Kind { get; init; }
+ public required string Value { get; init; }
+ public string? Placeholder { get; init; }
+ public double Minimum { get; init; }
+ public double Maximum { get; init; } = 100;
+ public double Step { get; init; } = 1;
+ public IReadOnlyList? Choices { get; init; }
+
+ public static GameSettingEntry Toggle(string key, string title, bool value, string? description = null) =>
+ new()
+ {
+ Key = key,
+ Title = title,
+ Description = description,
+ Kind = GameSettingKind.Toggle,
+ Value = value ? bool.TrueString : bool.FalseString
+ };
+
+ public static GameSettingEntry Text(string key, string title, string? value = null, string? description = null,
+ string? placeholder = null) =>
+ new()
+ {
+ Key = key,
+ Title = title,
+ Description = description,
+ Kind = GameSettingKind.Text,
+ Value = value ?? string.Empty,
+ Placeholder = placeholder
+ };
+
+ public static GameSettingEntry Number(string key, string title, double value, double minimum = double.MinValue,
+ double maximum = double.MaxValue, double step = 1,
+ string? description = null) =>
+ Numeric(key, title, value, minimum, maximum, step, description, GameSettingKind.Number);
+
+ public static GameSettingEntry Slider(string key, string title, double value, double minimum, double maximum,
+ double step = 1, string? description = null) =>
+ Numeric(key, title, value, minimum, maximum, step, description, GameSettingKind.Slider);
+
+ public static GameSettingEntry Choice(string key, string title, string value,
+ IReadOnlyList choices,
+ string? description = null) =>
+ new()
+ {
+ Key = key,
+ Title = title,
+ Description = description,
+ Kind = GameSettingKind.Choice,
+ Value = value,
+ Choices = choices
+ };
+
+ private static GameSettingEntry Numeric(string key, string title, double value, double minimum, double maximum,
+ double step, string? description, GameSettingKind kind)
+ {
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(minimum, maximum);
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(step);
+
+ return new GameSettingEntry
+ {
+ Key = key,
+ Title = title,
+ Description = description,
+ Kind = kind,
+ Value = value.ToString(CultureInfo.InvariantCulture),
+ Minimum = minimum,
+ Maximum = maximum,
+ Step = step
+ };
+ }
+}
diff --git a/UI/Settings/GameSettingKind.cs b/UI/Settings/GameSettingKind.cs
new file mode 100644
index 0000000..853dfe6
--- /dev/null
+++ b/UI/Settings/GameSettingKind.cs
@@ -0,0 +1,13 @@
+namespace Hi3Helper.Plugin.Core.UI.Settings;
+
+///
+/// Identifies the launcher control used to edit a game setting.
+///
+public enum GameSettingKind
+{
+ Toggle,
+ Text,
+ Number,
+ Slider,
+ Choice
+}
diff --git a/UI/Settings/GameSettingsPage.cs b/UI/Settings/GameSettingsPage.cs
new file mode 100644
index 0000000..c9fbfd8
--- /dev/null
+++ b/UI/Settings/GameSettingsPage.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+
+namespace Hi3Helper.Plugin.Core.UI.Settings;
+
+///
+/// Declaratively describes a game settings page rendered by the launcher.
+///
+public sealed class GameSettingsPage(IReadOnlyList sections)
+{
+ public string? Title { get; init; }
+ public IReadOnlyList Sections { get; init; } = sections;
+}
diff --git a/UI/Settings/GameSettingsPageSerializer.cs b/UI/Settings/GameSettingsPageSerializer.cs
new file mode 100644
index 0000000..abb9744
--- /dev/null
+++ b/UI/Settings/GameSettingsPageSerializer.cs
@@ -0,0 +1,20 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Hi3Helper.Plugin.Core.UI.Settings;
+
+///
+/// Serializes the declarative settings contract passed across the plugin ABI.
+///
+public static class GameSettingsPageSerializer
+{
+ public static string Serialize(GameSettingsPage page) =>
+ JsonSerializer.Serialize(page, GameSettingsPageJsonContext.Default.GameSettingsPage);
+
+ public static GameSettingsPage? Deserialize(string json) =>
+ JsonSerializer.Deserialize(json, GameSettingsPageJsonContext.Default.GameSettingsPage);
+}
+
+[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
+[JsonSerializable(typeof(GameSettingsPage))]
+internal sealed partial class GameSettingsPageJsonContext : JsonSerializerContext;
diff --git a/UI/Settings/GameSettingsSection.cs b/UI/Settings/GameSettingsSection.cs
new file mode 100644
index 0000000..a84ee0e
--- /dev/null
+++ b/UI/Settings/GameSettingsSection.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+
+namespace Hi3Helper.Plugin.Core.UI.Settings;
+
+///
+/// Groups related entries on a plugin-provided game settings page.
+///
+public sealed class GameSettingsSection(string title, IReadOnlyList entries)
+{
+ public string Title { get; init; } = title;
+ public string? Description { get; init; }
+ public IReadOnlyList Entries { get; init; } = entries;
+}
diff --git a/Utility/GameManagerExtension.cs b/Utility/GameManagerExtension.cs
index 945da6d..54a59a9 100644
--- a/Utility/GameManagerExtension.cs
+++ b/Utility/GameManagerExtension.cs
@@ -115,10 +115,11 @@ public DateTime GameLaunchStartTime
return (false, new NotSupportedException("Plugin doesn't have LaunchGameFromGameManagerAsync export in its API definition!"));
}
+ PrintGameLog? printGameLogCallback = context.PrintGameLogCallback;
nint gameManagerP = GetPointerFromInterface(context.GameManager);
nint pluginP = GetPointerFromInterface(context.Plugin);
nint presetConfigP = GetPointerFromInterface(context.PresetConfig);
- nint printGameLogCallbackP = context.PrintGameLogCallback == null ? nint.Zero : Marshal.GetFunctionPointerForDelegate(context.PrintGameLogCallback);
+ nint printGameLogCallbackP = printGameLogCallback == null ? nint.Zero : Marshal.GetFunctionPointerForDelegate(printGameLogCallback);
if (gameManagerP == nint.Zero)
{
@@ -160,7 +161,14 @@ public DateTime GameLaunchStartTime
return (false, Marshal.GetExceptionForHR(hResult));
}
- return await ExecuteSuccessAsyncTask(context.Plugin, taskResult, cancelTokenGuid, token);
+ try
+ {
+ return await ExecuteSuccessAsyncTask(context.Plugin, taskResult, cancelTokenGuid, token);
+ }
+ finally
+ {
+ GC.KeepAlive(printGameLogCallback);
+ }
}
///
@@ -437,4 +445,4 @@ private static unsafe nint GetPointerFromInterface(this T interfaceSource)
return (false, ex);
}
}
-}
\ No newline at end of file
+}
diff --git a/Utility/GameSettingsExtension.cs b/Utility/GameSettingsExtension.cs
new file mode 100644
index 0000000..8afbe26
--- /dev/null
+++ b/Utility/GameSettingsExtension.cs
@@ -0,0 +1,116 @@
+using Hi3Helper.Plugin.Core.Management.PresetConfig;
+using Hi3Helper.Plugin.Core.UI.Settings;
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.Marshalling;
+
+namespace Hi3Helper.Plugin.Core.Utility;
+
+///
+/// Provides launcher-side access to the optional v0.1.6 game settings exports.
+///
+public static class GameSettingsExtension
+{
+ public sealed class GameSettingsContext
+ {
+ private readonly SharedStaticV1Ext.GetGameSettingsPageDelegate? _getPage;
+ private readonly SharedStaticV1Ext.SetGameSettingValueDelegate? _setValue;
+ private readonly SharedStaticV1Ext.ApplyGameSettingsDelegate? _apply;
+
+ public IPluginPresetConfig PresetConfig { get; }
+ public bool IsFeatureAvailable => _getPage != null && _setValue != null && _apply != null;
+ public bool HasPage => TryGetPage(out _, out _);
+
+ public GameSettingsContext(nint pluginHandle, IPluginPresetConfig presetConfig)
+ {
+ PresetConfig = presetConfig;
+ pluginHandle.TryGetExport("GetGameSettingsPage", out SharedStaticV1Ext.GetGameSettingsPageDelegate getPage);
+ pluginHandle.TryGetExport("SetGameSettingValue", out SharedStaticV1Ext.SetGameSettingValueDelegate setValue);
+ pluginHandle.TryGetExport("ApplyGameSettings", out SharedStaticV1Ext.ApplyGameSettingsDelegate apply);
+ _getPage = getPage;
+ _setValue = setValue;
+ _apply = apply;
+ }
+
+ public unsafe bool TryGetPage(out GameSettingsPage? page, out Exception? error)
+ {
+ page = null;
+ error = null;
+ if (!IsFeatureAvailable)
+ {
+ return false;
+ }
+
+ nint presetConfigP = (nint)ComInterfaceMarshaller.ConvertToUnmanaged(PresetConfig);
+ try
+ {
+ int hResult = _getPage!(presetConfigP, out PluginDisposableMemoryMarshal pageJson);
+ using var memory = pageJson.ToManagedSpan();
+ if (hResult != 0)
+ {
+ error = Marshal.GetExceptionForHR(hResult);
+ return false;
+ }
+
+ string? json = pageJson.Handle == 0 || pageJson.Length <= 0 ? null : memory;
+ if (string.IsNullOrWhiteSpace(json))
+ {
+ return false;
+ }
+
+ page = GameSettingsPageSerializer.Deserialize(json);
+ return page != null;
+ }
+ catch (Exception ex)
+ {
+ error = ex;
+ return false;
+ }
+ finally
+ {
+ ComInterfaceMarshaller.Free((void*)presetConfigP);
+ }
+ }
+
+ public unsafe void SetValue(string key, string value)
+ {
+ if (_setValue == null)
+ {
+ throw new NotSupportedException("The plugin does not expose game settings");
+ }
+
+ nint presetConfigP = (nint)ComInterfaceMarshaller.ConvertToUnmanaged(PresetConfig);
+ try
+ {
+ fixed (char* keyP = key)
+ fixed (char* valueP = value)
+ {
+ int hResult = _setValue(presetConfigP, keyP, key.Length, valueP, value.Length);
+ Marshal.ThrowExceptionForHR(hResult);
+ }
+ }
+ finally
+ {
+ ComInterfaceMarshaller.Free((void*)presetConfigP);
+ }
+ }
+
+ public unsafe void Apply()
+ {
+ if (_apply == null)
+ {
+ throw new NotSupportedException("The plugin does not expose game settings");
+ }
+
+ nint presetConfigP = (nint)ComInterfaceMarshaller.ConvertToUnmanaged(PresetConfig);
+ try
+ {
+ Marshal.ThrowExceptionForHR(_apply(presetConfigP));
+ }
+ finally
+ {
+ ComInterfaceMarshaller.Free((void*)presetConfigP);
+ }
+ }
+ }
+}
diff --git a/docs/advanced.md b/docs/advanced.md
index 686cb62..e3fcf34 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -286,3 +286,66 @@ Offset Field Type Description
- If the plugin exports `SetPerFileProgressCallback`, Collapse registers its handler on startup and shows per-file progress in the install UI.
- If the plugin **does not** export the callback (i.e. older plugin versions), Collapse falls back to mirroring the aggregate `InstallProgressDelegate` as an approximation of per-file progress.
+
+---
+
+## 6. Game settings pages
+
+The `v0.1-update6` extension lets a plugin describe a native game settings page without taking a dependency on WinUI. Collapse renders the page using its own controls and styling. The supported entry kinds are toggle, text, number, slider, and choice.
+
+Override the three game-settings methods on your `SharedStaticV1Ext` implementation:
+
+```csharp
+using Hi3Helper.Plugin.Core;
+using Hi3Helper.Plugin.Core.Management.PresetConfig;
+using Hi3Helper.Plugin.Core.UI.Settings;
+using System;
+using System.Globalization;
+
+public sealed class PluginExports : SharedStaticV1Ext
+{
+ private bool _fullscreen = true;
+ private double _volume = 80;
+ private string _language = "en";
+
+ protected override GameSettingsPage? GetGameSettingsPageCore(IPluginPresetConfig presetConfig) =>
+ new([
+ new GameSettingsSection("Display", [
+ GameSettingEntry.Toggle("fullscreen", "Fullscreen", _fullscreen),
+ GameSettingEntry.Slider("volume", "Volume", _volume, 0, 100, 1),
+ GameSettingEntry.Choice("language", "Language", _language, [
+ new GameSettingChoice("en", "English"),
+ new GameSettingChoice("ja", "Japanese")
+ ])
+ ])
+ ]) { Title = "Game settings" };
+
+ protected override void SetGameSettingValueCore(IPluginPresetConfig presetConfig,
+ string key, string value)
+ {
+ switch (key)
+ {
+ case "fullscreen":
+ _fullscreen = bool.Parse(value);
+ break;
+ case "volume":
+ _volume = double.Parse(value, CultureInfo.InvariantCulture);
+ break;
+ case "language":
+ _language = value;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(key));
+ }
+ }
+
+ protected override void ApplyGameSettingsCore(IPluginPresetConfig presetConfig)
+ {
+ // Validate and persist the values received by SetGameSettingValueCore.
+ }
+}
+```
+
+Each entry must have a key unique within the page. Collapse sends booleans and numbers as invariant strings. `SetGameSettingValueCore` should update pending plugin state; persist that state in `ApplyGameSettingsCore`. Throwing from either callback returns the error to the launcher and displays it on the page.
+
+The page is optional per preset: return `null` from `GetGameSettingsPageCore` when the selected preset has no game settings. Older plugins do not export the update6 functions, so Collapse keeps their Game Settings navigation item hidden.
diff --git a/docs/introduction.md b/docs/introduction.md
index 35be950..6153b39 100644
--- a/docs/introduction.md
+++ b/docs/introduction.md
@@ -38,7 +38,7 @@ Plugin.dll (NativeAOT)
## API standard versioning
-The current API standard version is **v0.1.5**. All plugins must implement at minimum the **v0.1 core** exports. Optional feature sets are versioned as update packages (`v0.1-update1`, `v0.1-update2`, etc.) and are handled automatically by `SharedStaticV1Ext`.
+The current API standard version is **v0.1.6**. All plugins must implement at minimum the **v0.1 core** exports. Optional feature sets are versioned as update packages (`v0.1-update1`, `v0.1-update2`, etc.) and are handled automatically by `SharedStaticV1Ext`.
| Update | Export(s) added | Description |
|--------|----------------|-------------|
@@ -47,6 +47,7 @@ The current API standard version is **v0.1.5**. All plugins must implement at mi
| v0.1-update3 | `StartResizableWindowHookAsync` | Resizable window hook |
| v0.1-update4 | `RegisterSpeedThrottlerService` | Download speed throttling |
| v0.1-update5 | `SetPerFileProgressCallback` | Per-file install/download progress reporting |
+| v0.1-update6 | `GetGameSettingsPage`, `SetGameSettingValue`, `ApplyGameSettings` | Declarative, launcher-rendered game settings pages |
## Next steps
diff --git a/index.md b/index.md
index bb2683b..74119d7 100644
--- a/index.md
+++ b/index.md
@@ -11,7 +11,7 @@ _layout: landing
- **Full NativeAOT support** — designed to be compiled with .NET NativeAOT; reflection-free when targeting `MANUALCOM`/`USELIGHTWEIGHTJSONPARSER` configurations
- **COM interop via `System.Runtime.InteropServices.Marshalling`** — no custom COM registration; the launcher discovers everything through a single `GetApiExport` entry point
- **Batteries-included utilities** — pre-configured `HttpClient` builder with automatic proxy and DNS resolver integration, retry-able download streams, speed limiting, and unmanaged memory helpers
-- **Versioned extension API** — optional feature sets (`v0.1-update1` through `v0.1-update5`) for game launch, Discord Rich Presence, resizable-window hook, download throttling, and per-file install progress are all opt-in
+- **Versioned extension API** — optional feature sets (`v0.1-update1` through `v0.1-update6`) for game launch, Discord Rich Presence, resizable-window hook, download throttling, install progress, and declarative game settings are all opt-in
## Getting started
@@ -28,4 +28,4 @@ _layout: landing
- [Hi3Helper.Plugin.HBR](https://github.com/CollapseLauncher/Hi3Helper.Plugin.HBR) — Heaven Burns Red
- [Hi3Helper.Plugin.Wuwa](https://github.com/CollapseLauncher/Hi3Helper.Plugin.Wuwa) — Wuthering Waves
-- [Hi3Helper.Plugin.DNA](https://github.com/CollapseLauncher/Hi3Helper.Plugin.DNA) — Duet Night Abyss
\ No newline at end of file
+- [Hi3Helper.Plugin.DNA](https://github.com/CollapseLauncher/Hi3Helper.Plugin.DNA) — Duet Night Abyss
diff --git a/packages.lock.json b/packages.lock.json
index a9365d6..ec63eff 100644
--- a/packages.lock.json
+++ b/packages.lock.json
@@ -4,23 +4,23 @@
"net10.0": {
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Direct",
- "requested": "[10.0.10, )",
- "resolved": "10.0.10",
- "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==",
+ "requested": "[10.0.11, )",
+ "resolved": "10.0.11",
+ "contentHash": "Ljd0Uxoq5XpScD2Bg0nM/r3mwx7Ao5Uq24eo2ARxbGvqJ7Zht6rt2cJtwVRH4Cv+1ZVMdXz6TB43KbpmsxRrvQ==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11"
}
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
- "requested": "[10.0.10, )",
- "resolved": "10.0.10",
- "contentHash": "f5VCIE7AJpd5YvzNTeMGVzQIgyE9tX+AreTYwQF+REbu+DZo/2Ae+jNSwhPEYrVz6RRkd7y8ubXjk6Nn6Ka+Cg=="
+ "requested": "[10.0.11, )",
+ "resolved": "10.0.11",
+ "contentHash": "IBf7lbovvjGWVWXZX5cJ/cO0WXbId0Zq4BuSeT94mGZuOAP66oMeH9PTBZ9Jpp3Jb6jtK0qm/NyUbPRo1gC/wQ=="
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.10",
- "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA=="
+ "resolved": "10.0.11",
+ "contentHash": "/a1aJz4m7ylhEDf25ugQChLQoN5XwoGjWw/BoR/ZWWKsO1v4DdJElS1uyngahz4B/eOzjFk1KNTkarRLE5wsIg=="
}
}
}