Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion PluginTest/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ private static async Task<int> 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);
Expand Down Expand Up @@ -144,7 +151,7 @@ private static bool LogInvokeTest<T>(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
Expand Down
31 changes: 31 additions & 0 deletions PluginTest/Test.GameSettings.cs
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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/))
* [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/))
1 change: 1 addition & 0 deletions SharedStatic.V1Ext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ static SharedStaticV1Ext()
InitExtension_Update3Exports();
InitExtension_Update4Exports();
InitExtension_Update5Exports();
InitExtension_Update6Exports();
}

/// <summary>
Expand Down
117 changes: 117 additions & 0 deletions SharedStatic.V1Ext_Update6.cs
Original file line number Diff line number Diff line change
@@ -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<T>
{
private static unsafe void InitExtension_Update6Exports()
{
TryRegisterApiExport<GetGameSettingsPageDelegate>("GetGameSettingsPage", GetGameSettingsPage);
TryRegisterApiExport<SetGameSettingValueDelegate>("SetGameSettingValue", SetGameSettingValue);
TryRegisterApiExport<ApplyGameSettingsDelegate>("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<IPluginPresetConfig>(
(ComWrappers.ComInterfaceDispatch*)presetConfigP);
#else
return ComInterfaceMarshaller<IPluginPresetConfig>.ConvertToManaged((void*)presetConfigP)
?? throw new InvalidCastException("Cannot convert the preset config pointer to IPluginPresetConfig");
#endif
}

/// <summary>
/// Returns the declarative settings page for a game preset, or <c>null</c> when the preset has no settings page.
/// </summary>
protected virtual GameSettingsPage? GetGameSettingsPageCore(IPluginPresetConfig presetConfig) => null;

/// <summary>
/// Receives a value edited by the user. Values use invariant strings; conversion and validation belong to the plugin.
/// </summary>
protected virtual void SetGameSettingValueCore(IPluginPresetConfig presetConfig, string key, string value) { }

/// <summary>
/// Persists the edited settings for the specified game preset.
/// </summary>
protected virtual void ApplyGameSettingsCore(IPluginPresetConfig presetConfig) { }
}
2 changes: 1 addition & 1 deletion SharedStatic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions UI/Settings/GameSettingChoice.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Hi3Helper.Plugin.Core.UI.Settings;

/// <summary>
/// Defines one selectable value for a <see cref="GameSettingKind.Choice"/> setting.
/// </summary>
public sealed class GameSettingChoice(string value, string title)
{
public string Value { get; init; } = value;
public string Title { get; init; } = title;
}
85 changes: 85 additions & 0 deletions UI/Settings/GameSettingEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Globalization;

namespace Hi3Helper.Plugin.Core.UI.Settings;

/// <summary>
/// Defines one editable setting in a plugin-provided game settings page.
/// </summary>
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<GameSettingChoice>? 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<GameSettingChoice> 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
};
}
}
13 changes: 13 additions & 0 deletions UI/Settings/GameSettingKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Hi3Helper.Plugin.Core.UI.Settings;

/// <summary>
/// Identifies the launcher control used to edit a game setting.
/// </summary>
public enum GameSettingKind
{
Toggle,
Text,
Number,
Slider,
Choice
}
12 changes: 12 additions & 0 deletions UI/Settings/GameSettingsPage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Collections.Generic;

namespace Hi3Helper.Plugin.Core.UI.Settings;

/// <summary>
/// Declaratively describes a game settings page rendered by the launcher.
/// </summary>
public sealed class GameSettingsPage(IReadOnlyList<GameSettingsSection> sections)
{
public string? Title { get; init; }
public IReadOnlyList<GameSettingsSection> Sections { get; init; } = sections;
}
20 changes: 20 additions & 0 deletions UI/Settings/GameSettingsPageSerializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Hi3Helper.Plugin.Core.UI.Settings;

/// <summary>
/// Serializes the declarative settings contract passed across the plugin ABI.
/// </summary>
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;
13 changes: 13 additions & 0 deletions UI/Settings/GameSettingsSection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Collections.Generic;

namespace Hi3Helper.Plugin.Core.UI.Settings;

/// <summary>
/// Groups related entries on a plugin-provided game settings page.
/// </summary>
public sealed class GameSettingsSection(string title, IReadOnlyList<GameSettingEntry> entries)
{
public string Title { get; init; } = title;
public string? Description { get; init; }
public IReadOnlyList<GameSettingEntry> Entries { get; init; } = entries;
}
Loading