feat(config): add toml loading support (#804)

This commit is contained in:
Michael Wilson
2025-03-25 19:12:29 +10:00
committed by GitHub
parent c02d31cb2e
commit c50213c442
5 changed files with 3104 additions and 147 deletions

View File

@@ -1,38 +1,60 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes; using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Attributes.Registration;
namespace WithConfig; using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
public class SampleConfig : BasePluginConfig using CounterStrikeSharp.API.Modules.Config;
{ using CounterStrikeSharp.API.Modules.Extensions;
[JsonPropertyName("ChatPrefix")] public string ChatPrefix { get; set; } = "My Cool Plugin";
namespace WithConfig;
[JsonPropertyName("ChatInterval")] public float ChatInterval { get; set; } = 60;
} public class SampleConfig : BasePluginConfig
{
[MinimumApiVersion(80)] [JsonPropertyName("ChatPrefix")] public string ChatPrefix { get; set; } = "My Cool Plugin";
public class WithConfigPlugin : BasePlugin, IPluginConfig<SampleConfig>
{ [JsonPropertyName("ChatInterval")] public float ChatInterval { get; set; } = 60;
public override string ModuleName => "Example: With Config"; }
public override string ModuleVersion => "1.0.0";
[MinimumApiVersion(80)]
public SampleConfig Config { get; set; } public class WithConfigPlugin : BasePlugin, IPluginConfig<SampleConfig>
{
public void OnConfigParsed(SampleConfig config) public override string ModuleName => "Example: With Config";
{ public override string ModuleVersion => "1.0.0";
// Do manual verification of the config and override any invalid values
if (config.ChatInterval > 60) public SampleConfig Config { get; set; }
{
config.ChatInterval = 60; public void OnConfigParsed(SampleConfig config)
} {
// Do manual verification of the config and override any invalid values
if (config.ChatPrefix.Length > 25) if (config.ChatInterval > 60)
{ {
throw new Exception($"Invalid value has been set to config value 'ChatPrefix': {config.ChatPrefix}"); config.ChatInterval = 60;
} }
// Once we've validated the config, we can set it to the instance if (config.ChatPrefix.Length > 25)
Config = config; {
} throw new Exception($"Invalid value has been set to config value 'ChatPrefix': {config.ChatPrefix}");
} }
// Once we've validated the config, we can set it to the instance
Config = config;
}
[ConsoleCommand("css_reload_config", "Reloads the plugin config")]
public void OnReloadConfig(CCSPlayerController? player, CommandInfo commandInfo)
{
commandInfo.ReplyToCommand("Chat Interval before reload: " + Config.ChatInterval);
Config.Reload();
commandInfo.ReplyToCommand("Chat Interval after reload: " + Config.ChatInterval);
}
[ConsoleCommand("css_reset_config", "Resets the plugin config")]
public void OnResetConfig(CCSPlayerController? player, CommandInfo commandInfo)
{
commandInfo.ReplyToCommand("Chat Interval before reset: " + Config.ChatInterval);
Config.ChatInterval = 60;
Config.Update();
commandInfo.ReplyToCommand("Chat Interval after reset: " + Config.ChatInterval);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -39,6 +39,7 @@
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0"/> <PackageReference Include="Serilog.Sinks.Console" Version="5.0.0"/>
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0"/> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0"/>
<PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0"/> <PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0"/>
<PackageReference Include="Tomlyn" Version="0.19.0"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Core\Schema\"/> <Folder Include="Core\Schema\"/>
@@ -59,9 +60,9 @@
</PropertyGroup> </PropertyGroup>
<Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation"> <Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation">
<Exec <Exec
Command="git describe --long --always --exclude=* --abbrev=7" Command="git describe --long --always --exclude=* --abbrev=7"
ConsoleToMSBuild="True" ConsoleToMSBuild="True"
IgnoreExitCode="False" IgnoreExitCode="False"
> >
<Output PropertyName="SourceRevisionId" TaskParameter="ConsoleOutput"/> <Output PropertyName="SourceRevisionId" TaskParameter="ConsoleOutput"/>
</Exec> </Exec>

View File

@@ -14,18 +14,21 @@
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. * * along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/ */
using System;
using System.IO;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Logging; using CounterStrikeSharp.API.Core.Logging;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Tomlyn;
namespace CounterStrikeSharp.API.Modules.Config namespace CounterStrikeSharp.API.Modules.Config
{ {
enum ConfigType
{
Json,
Toml
}
public static class ConfigManager public static class ConfigManager
{ {
private static readonly DirectoryInfo? _rootDir; private static readonly DirectoryInfo? _rootDir;
@@ -33,47 +36,59 @@ namespace CounterStrikeSharp.API.Modules.Config
private static readonly string _pluginConfigsFolderPath; private static readonly string _pluginConfigsFolderPath;
private static ILogger _logger = CoreLogging.Factory.CreateLogger("ConfigManager"); private static ILogger _logger = CoreLogging.Factory.CreateLogger("ConfigManager");
internal static JsonSerializerOptions JsonSerializerOptions { get; } = new()
{
WriteIndented = true,
ReadCommentHandling = JsonCommentHandling.Skip
};
internal static TomlModelOptions TomlModelOptions { get; } = new()
{
ConvertPropertyName = name => name
};
static ConfigManager() static ConfigManager()
{ {
_rootDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.Parent; _rootDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.Parent;
_pluginConfigsFolderPath = Path.Combine(_rootDir.FullName, "configs", "plugins"); _pluginConfigsFolderPath = Path.Combine(_rootDir.FullName, "configs", "plugins");
} }
public static T Load<T>(string pluginName) where T : IBasePluginConfig, new() public static T Load<T>(string pluginName) where T : class, IBasePluginConfig, new()
{ {
string directoryPath = Path.Combine(_pluginConfigsFolderPath, pluginName); string directoryPath = Path.Combine(_pluginConfigsFolderPath, pluginName);
string configPath = Path.Combine(directoryPath, $"{pluginName}.json"); string configPath = Path.Combine(directoryPath, $"{pluginName}");
string exampleConfigPath = Path.Combine(directoryPath, $"{pluginName}.example.json"); string exampleConfigPath = Path.Combine(directoryPath, $"{pluginName}.example");
T config = (T)Activator.CreateInstance(typeof(T))!; string[] configFilePaths =
[
$"{configPath}.toml",
$"{configPath}.json",
];
if (!File.Exists(configPath) && !File.Exists(exampleConfigPath)) foreach (var path in configFilePaths)
{ {
try if (File.Exists(path))
{ {
if (!Directory.Exists(directoryPath)) return Deserialize<T>(path);
{ }
Directory.CreateDirectory(directoryPath); }
}
StringBuilder builder = new StringBuilder(); string[] exampleFilePaths =
builder.Append( [
$"// This configuration was automatically generated by CounterStrikeSharp for plugin '{pluginName}', at {DateTimeOffset.Now:yyyy/MM/dd hh:mm:ss}\n"); $"{exampleConfigPath}.toml",
builder.Append(JsonSerializer.Serialize<T>(config, $"{exampleConfigPath}.json"
new JsonSerializerOptions { WriteIndented = true })); ];
File.WriteAllText(configPath, builder.ToString());
return config; foreach (var path in exampleFilePaths)
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to generate configuration file for {PluginName}", pluginName);
}
} else if (File.Exists(exampleConfigPath) && !File.Exists(configPath))
{ {
if (!File.Exists(path)) continue;
try try
{ {
_logger.LogInformation("Copying example configuration file for {PluginName}", pluginName); _logger.LogInformation("Copying example configuration file for {PluginName}", pluginName);
File.Copy(exampleConfigPath, configPath); var destPath = Path.Combine(directoryPath, Path.GetFileName(path).Replace(".example", ""));
File.Copy(path, destPath);
return Deserialize<T>(destPath);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -83,14 +98,58 @@ namespace CounterStrikeSharp.API.Modules.Config
try try
{ {
config = JsonSerializer.Deserialize<T>(File.ReadAllText(configPath), new JsonSerializerOptions() { ReadCommentHandling = JsonCommentHandling.Skip })!; if (!Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
var config = new T();
var output = Serialize(config, ConfigType.Json, pluginName);
File.WriteAllText(Path.Combine(directoryPath, $"{pluginName}.json"), output);
return config;
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Failed to parse configuration file for {PluginName}", pluginName); _logger.LogError(ex, "Failed to generate configuration file for {PluginName}", pluginName);
return new T();
}
}
private static T Deserialize<T>(string path) where T : class, IBasePluginConfig, new()
{
switch (Path.GetExtension(path))
{
case ".toml":
return Toml.ToModel<T>(File.ReadAllText(path), options: TomlModelOptions);
case ".json":
return JsonSerializer.Deserialize<T>(File.ReadAllText(path), JsonSerializerOptions)!;
} }
return config; throw new NotSupportedException("Unsupported configuration file format");
}
private static string Serialize<T>(T config, ConfigType configType, string pluginName) where T : class, IBasePluginConfig, new()
{
StringBuilder builder = new StringBuilder();
string comment =
$"This configuration was automatically generated by CounterStrikeSharp for plugin '{pluginName}', at {DateTimeOffset.Now:yyyy/MM/dd hh:mm:ss}\n";
switch (configType)
{
case ConfigType.Json:
builder.Append($"// {comment}");
builder.Append(JsonSerializer.Serialize<T>(config, JsonSerializerOptions));
break;
case ConfigType.Toml:
builder.Append($"# {comment}");
builder.Append(Toml.FromModel(config, options: TomlModelOptions));
break;
default:
throw new NotSupportedException("Unsupported configuration file format");
}
return builder.ToString();
} }
} }
} }

View File

@@ -1,17 +1,14 @@
using System.Text.Json; using System.Text.Json;
using System.Reflection; using System.Reflection;
using System.Runtime.Serialization;
using CounterStrikeSharp.API.Modules.Config;
using Tomlyn;
namespace CounterStrikeSharp.API.Modules.Extensions; namespace CounterStrikeSharp.API.Modules.Extensions;
public static class PluginConfigExtensions public static class PluginConfigExtensions
{ {
private static readonly JsonSerializerOptions _jsonSerializerOptions = new() public static JsonSerializerOptions JsonSerializerOptions => ConfigManager.JsonSerializerOptions;
{
WriteIndented = true,
ReadCommentHandling = JsonCommentHandling.Skip
};
public static JsonSerializerOptions JsonSerializerOptions => _jsonSerializerOptions;
/// <summary> /// <summary>
/// Gets the configuration file path /// Gets the configuration file path
@@ -21,7 +18,24 @@ public static class PluginConfigExtensions
public static string GetConfigPath<T>(this T _) where T : BasePluginConfig, new() public static string GetConfigPath<T>(this T _) where T : BasePluginConfig, new()
{ {
string assemblyName = typeof(T).Assembly.GetName().Name ?? string.Empty; string assemblyName = typeof(T).Assembly.GetName().Name ?? string.Empty;
return Path.Combine(Server.GameDirectory, "csgo", "addons", "counterstrikesharp", "configs", "plugins", assemblyName, $"{assemblyName}.json");
string[] configFilePaths =
[
Path.Combine(Server.GameDirectory, "csgo", "addons", "counterstrikesharp", "configs", "plugins", assemblyName,
$"{assemblyName}.json"),
Path.Combine(Server.GameDirectory, "csgo", "addons", "counterstrikesharp", "configs", "plugins", assemblyName,
$"{assemblyName}.toml"),
];
foreach (var path in configFilePaths)
{
if (File.Exists(path))
{
return path;
}
}
return configFilePaths[0];
} }
/// <summary> /// <summary>
@@ -37,7 +51,22 @@ public static class PluginConfigExtensions
{ {
using var stream = new FileStream(configPath, FileMode.Create, FileAccess.Write, FileShare.None); using var stream = new FileStream(configPath, FileMode.Create, FileAccess.Write, FileShare.None);
using var writer = new StreamWriter(stream); using var writer = new StreamWriter(stream);
writer.Write(JsonSerializer.Serialize(config, JsonSerializerOptions));
switch (Path.GetExtension(configPath))
{
case ".json":
{
writer.Write(JsonSerializer.Serialize(config, ConfigManager.JsonSerializerOptions));
break;
}
case ".toml":
writer.Write(Toml.FromModel(config, ConfigManager.TomlModelOptions));
break;
default:
throw new NotSupportedException($"Configuration file type '{Path.GetExtension(configPath)}' is not supported.");
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -63,8 +92,22 @@ public static class PluginConfigExtensions
var configContent = File.ReadAllText(configPath); var configContent = File.ReadAllText(configPath);
var newConfig = JsonSerializer.Deserialize<T>(configContent, JsonSerializerOptions) T? newConfig = null;
?? throw new JsonException($"Deserialization failed for configuration file '{configPath}'."); switch (Path.GetExtension(configPath))
{
case ".json":
newConfig = JsonSerializer.Deserialize<T>(configContent, ConfigManager.JsonSerializerOptions)
?? throw new JsonException($"Deserialization failed for configuration file '{configPath}'.");
break;
case ".toml":
newConfig = Toml.ToModel<T>(configContent, options: ConfigManager.TomlModelOptions);
break;
}
if (newConfig is null)
{
throw new SerializationException($"Deserialization failed for configuration file '{configPath}'.");
}
foreach (var property in typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)) foreach (var property in typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public))
{ {