From 1fad46b2a5494f73fbac8ed0fb128857d0dba9bd Mon Sep 17 00:00:00 2001 From: compujuckel Date: Sat, 13 Jan 2024 21:47:33 +0100 Subject: [PATCH] Move plugin configuration into separate files --- AssettoServer/Network/Http/Startup.cs | 2 +- .../Configuration/ACServerConfiguration.cs | 157 +++++++++++------- .../ConfigurationJsonSchemaGenerator.cs | 57 ------- .../ConfigurationSchemaGenerator.cs | 108 ++++++++++++ .../Extra/ACExtraConfiguration.cs | 98 +++-------- 5 files changed, 237 insertions(+), 185 deletions(-) delete mode 100644 AssettoServer/Server/Configuration/ConfigurationJsonSchemaGenerator.cs create mode 100644 AssettoServer/Server/Configuration/ConfigurationSchemaGenerator.cs diff --git a/AssettoServer/Network/Http/Startup.cs b/AssettoServer/Network/Http/Startup.cs index c601da56..99ede3a2 100644 --- a/AssettoServer/Network/Http/Startup.cs +++ b/AssettoServer/Network/Http/Startup.cs @@ -111,7 +111,7 @@ public void ConfigureContainer(ContainerBuilder builder) builder.RegisterModule(plugin.Instance); } - _configuration.Extra.LoadPluginConfig(_loader, builder); + _configuration.LoadPluginConfiguration(_loader, builder); } // This method gets called by the runtime. Use this method to add services to the container. diff --git a/AssettoServer/Server/Configuration/ACServerConfiguration.cs b/AssettoServer/Server/Configuration/ACServerConfiguration.cs index 70b31743..63af2f62 100644 --- a/AssettoServer/Server/Configuration/ACServerConfiguration.cs +++ b/AssettoServer/Server/Configuration/ACServerConfiguration.cs @@ -6,11 +6,14 @@ using System.Text.RegularExpressions; using AssettoServer.Server.Configuration.Extra; using AssettoServer.Server.Configuration.Kunos; +using AssettoServer.Server.Plugin; using AssettoServer.Shared.Model; using AssettoServer.Shared.Network.Http.Responses; using AssettoServer.Utils; +using Autofac; using FluentValidation; using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; using Serilog; using YamlDotNet.Serialization; @@ -23,9 +26,9 @@ public class ACServerConfiguration public List Sessions { get; } [YamlIgnore] public string FullTrackName { get; } [YamlIgnore] public CSPTrackOptions CSPTrackOptions { get; } - [YamlIgnore] public string WelcomeMessage { get; } = ""; + [YamlIgnore] public string WelcomeMessage { get; } public ACExtraConfiguration Extra { get; private set; } = new(); - [YamlIgnore] public CMContentConfiguration? ContentConfiguration { get; private set; } + [YamlIgnore] public CMContentConfiguration? ContentConfiguration { get; } public string ServerVersion { get; } [YamlIgnore] public string? CSPExtraOptions { get; } [YamlIgnore] public string BaseFolder { get; } @@ -50,46 +53,86 @@ public ACServerConfiguration(string preset, ConfigurationLocations locations, bo { BaseFolder = locations.BaseFolder; LoadPluginsFromWorkdir = loadPluginsFromWorkdir; + Server = LoadServerConfiguration(locations.ServerCfgPath); + EntryList = LoadEntryList(locations.EntryListPath); + WelcomeMessage = LoadWelcomeMessage(preset); + CSPExtraOptions = LoadCspExtraOptions(locations.CSPExtraOptionsPath); + ContentConfiguration = LoadContentConfiguration(Path.Join(BaseFolder, "cm_content/content.json")); + ServerVersion = ThisAssembly.AssemblyInformationalVersion; + FullTrackName = string.IsNullOrEmpty(Server.TrackConfig) ? Server.Track : Server.Track + "-" + Server.TrackConfig; + CSPTrackOptions = CSPTrackOptions.Parse(Server.Track); + Sessions = PrepareSessions(); + + var extraCfgSchemaPath = ConfigurationSchemaGenerator.WriteExtraCfgSchema(); + LoadExtraConfig(locations.ExtraCfgPath, extraCfgSchemaPath); + ACExtraConfiguration.WriteReferenceConfig(extraCfgSchemaPath); - Log.Debug("Loading server_cfg.ini from {Path}", locations.ServerCfgPath); - if (!File.Exists(locations.ServerCfgPath)) + ApplyConfigurationFixes(); + + var validator = new ACServerConfigurationValidator(); + validator.ValidateAndThrow(this); + } + + private ServerConfiguration LoadServerConfiguration(string path) + { + Log.Debug("Loading server_cfg.ini from {Path}", path); + if (!File.Exists(path)) { - Directory.CreateDirectory(Path.GetDirectoryName(locations.ServerCfgPath)!); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); using var serverCfg = Assembly.GetExecutingAssembly().GetManifestResourceStream("AssettoServer.Assets.server_cfg.ini")!; - using var outFile = File.Create(locations.ServerCfgPath); + using var outFile = File.Create(path); serverCfg.CopyTo(outFile); } - Server = ServerConfiguration.FromFile(locations.ServerCfgPath); + return ServerConfiguration.FromFile(path); + } - Log.Debug("Loading entry_list.ini from {Path}", locations.EntryListPath); - if (!File.Exists(locations.EntryListPath)) + private EntryList LoadEntryList(string path) + { + Log.Debug("Loading entry_list.ini from {Path}", path); + if (!File.Exists(path)) { - Directory.CreateDirectory(Path.GetDirectoryName(locations.EntryListPath)!); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); using var entryList = Assembly.GetExecutingAssembly().GetManifestResourceStream("AssettoServer.Assets.entry_list.ini")!; - using var outFile = File.Create(locations.EntryListPath); + using var outFile = File.Create(path); entryList.CopyTo(outFile); } - EntryList = EntryList.FromFile(locations.EntryListPath); - - ServerVersion = ThisAssembly.AssemblyInformationalVersion; - FullTrackName = string.IsNullOrEmpty(Server.TrackConfig) ? Server.Track : Server.Track + "-" + Server.TrackConfig; - CSPTrackOptions = CSPTrackOptions.Parse(Server.Track); + return EntryList.FromFile(path); + } - string welcomeMessagePath = string.IsNullOrEmpty(preset) ? Server.WelcomeMessagePath : Path.Join(BaseFolder, Server.WelcomeMessagePath); + private string LoadWelcomeMessage(string preset) + { + var welcomeMessage = ""; + var welcomeMessagePath = string.IsNullOrEmpty(preset) ? Server.WelcomeMessagePath : Path.Join(BaseFolder, Server.WelcomeMessagePath); if (File.Exists(welcomeMessagePath)) { - WelcomeMessage = File.ReadAllText(welcomeMessagePath); + welcomeMessage = File.ReadAllText(welcomeMessagePath); } else if(!string.IsNullOrEmpty(welcomeMessagePath)) { Log.Warning("Welcome message not found at {Path}", Path.GetFullPath(welcomeMessagePath)); } - - if (File.Exists(locations.CSPExtraOptionsPath)) + + return welcomeMessage; + } + + private static string? LoadCspExtraOptions(string path) + { + return File.Exists(path) ? File.ReadAllText(path) : null; + } + + private static CMContentConfiguration? LoadContentConfiguration(string path) + { + CMContentConfiguration? contentConfiguration = null; + if (File.Exists(path)) { - CSPExtraOptions = File.ReadAllText(locations.CSPExtraOptionsPath); + contentConfiguration = JsonConvert.DeserializeObject(File.ReadAllText(path)); } + return contentConfiguration; + } + + private List PrepareSessions() + { var sessions = new List(); if (Server.Practice != null) @@ -113,16 +156,15 @@ public ACServerConfiguration(string preset, ConfigurationLocations locations, bo sessions.Add(Server.Race); } - Sessions = sessions; + return sessions; + } + private void ApplyConfigurationFixes() + { if (Server.MaxClients == 0) { Server.MaxClients = EntryList.Cars.Count; } - - LoadExtraConfig(locations.ExtraCfgPath); - WriteReferenceExtraConfig(Path.Join(locations.BaseFolder, "extra_cfg.reference.yml")); - ACExtraConfiguration.WriteSchema(Path.Join(locations.BaseFolder, "schema.json")); if (Extra is { EnableAi: true, AiParams.AutoAssignTrafficCars: true }) { @@ -144,40 +186,52 @@ public ACServerConfiguration(string preset, ConfigurationLocations locations, bo { Extra.AiParams.MaxAiTargetCount = EntryList.Cars.Count(c => c.AiMode == AiMode.None) * Extra.AiParams.AiPerPlayerTargetCount; } - - var validator = new ACServerConfigurationValidator(); - validator.ValidateAndThrow(this); } - private void WriteReferenceExtraConfig(string path) + internal void LoadPluginConfiguration(ACPluginLoader loader, ContainerBuilder builder) { - FileInfo? info = null; - if (File.Exists(path)) - { - info = new FileInfo(path); - info.IsReadOnly = false; - } - - using (var writer = File.CreateText(path)) + foreach (var plugin in loader.LoadedPlugins) { - writer.WriteLine($"# AssettoServer {ThisAssembly.AssemblyInformationalVersion} Reference Configuration"); - writer.WriteLine("# This file serves as an overview of all possible options with their default values."); - writer.WriteLine("# It is NOT read by the server - edit extra_cfg.yml instead!"); - writer.WriteLine(); + if (plugin.ConfigurationType == null) continue; - ACExtraConfiguration.ReferenceConfiguration.ToStream(writer, true); + var schemaPath = ConfigurationSchemaGenerator.WritePluginConfigurationSchema(plugin.ConfigurationType); + var configPath = Path.Join(BaseFolder, PluginConfigurationTypeToFilename(plugin.ConfigurationType.Name)); + if (File.Exists(configPath)) + { + var deserializer = new DeserializerBuilder().Build(); + using var file = File.OpenText(configPath); + var configObj = deserializer.Deserialize(file, plugin.ConfigurationType)!; + + // TODO validation and better error handling + + builder.RegisterInstance(configObj).AsSelf(); + } + else + { + var serializer = new SerializerBuilder().Build(); + using var file = File.CreateText(configPath); + ConfigurationSchemaGenerator.WriteModeLine(file, BaseFolder, schemaPath); + var configObj = Activator.CreateInstance(plugin.ConfigurationType)!; + serializer.Serialize(file, configObj, plugin.ConfigurationType); + } } + } - info ??= new FileInfo(path); - info.IsReadOnly = true; + public static string PluginConfigurationTypeToFilename(string type, string ending = "yml") + { + var strat = new SnakeCaseNamingStrategy(); + type = type.Replace("Configuration", "Cfg"); + return $"{strat.GetPropertyName(type, false)}.{ending}"; } - private void LoadExtraConfig(string path) { + private void LoadExtraConfig(string path, string schemaPath) { Log.Debug("Loading extra_cfg.yml from {Path}", path); if (!File.Exists(path)) { - new ACExtraConfiguration().ToFile(path); + using var file = File.CreateText(path); + ConfigurationSchemaGenerator.WriteModeLine(file, BaseFolder, schemaPath); + new ACExtraConfiguration().ToStream(file); } Extra = ACExtraConfiguration.FromFile(path); @@ -195,15 +249,6 @@ private void LoadExtraConfig(string path) { throw new ConfigurationException(errorMsg) { HelpLink = "https://assettoserver.org/docs/common-configuration-errors#wrong-server-details" }; } } - - if (Extra.EnableServerDetails) - { - string cmContentPath = Path.Join(BaseFolder, "cm_content/content.json"); - if (File.Exists(cmContentPath)) - { - ContentConfiguration = JsonConvert.DeserializeObject(File.ReadAllText(cmContentPath)); - } - } } private (PropertyInfo? Property, object Parent) GetNestedProperty(string key) diff --git a/AssettoServer/Server/Configuration/ConfigurationJsonSchemaGenerator.cs b/AssettoServer/Server/Configuration/ConfigurationJsonSchemaGenerator.cs deleted file mode 100644 index 1d6c20a2..00000000 --- a/AssettoServer/Server/Configuration/ConfigurationJsonSchemaGenerator.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Diagnostics; -using System.Linq; -using AssettoServer.Server.Configuration.Extra; -using Namotion.Reflection; -using NJsonSchema; -using NJsonSchema.Generation; -using YamlDotNet.Serialization; - -namespace AssettoServer.Server.Configuration; - -public class ConfigurationJsonSchemaGenerator : JsonSchemaGenerator, ISchemaProcessor -{ - private static readonly ACExtraConfiguration DefaultConfiguration = new(); - - public override void ApplyDataAnnotations(JsonSchema schema, JsonTypeDescription typeDescription) - { - var yamlMemberAttribute = typeDescription.ContextualType.GetContextAttribute(true); - schema.Description = yamlMemberAttribute?.Description; - - if (typeDescription.Type != JsonObjectType.Object && typeDescription.ContextualType.Context is ContextualPropertyInfo info) - { - object? defaultValue = null; - // TODO improve this - if (info.MemberInfo.DeclaringType == typeof(ACExtraConfiguration)) - { - defaultValue = info.GetValue(DefaultConfiguration); - } - else if (info.MemberInfo.DeclaringType == typeof(AiParams)) - { - defaultValue = info.GetValue(DefaultConfiguration.AiParams); - } - - schema.Default = defaultValue; - } - - base.ApplyDataAnnotations(schema, typeDescription); - } - - public void Process(SchemaProcessorContext context) - { - if (context.ContextualType.GetContextAttribute(true) != null) - { - context.Schema.Title = "IGNORE"; - } - - var ignoredProperties = context.Schema.Properties.Where(p => p.Value.Title == "IGNORE").ToList(); - - foreach (var property in ignoredProperties) - { - context.Schema.Properties.Remove(property.Key); - } - } - - public ConfigurationJsonSchemaGenerator(JsonSchemaGeneratorSettings settings) : base(settings) - { - } -} diff --git a/AssettoServer/Server/Configuration/ConfigurationSchemaGenerator.cs b/AssettoServer/Server/Configuration/ConfigurationSchemaGenerator.cs new file mode 100644 index 00000000..e8e143ed --- /dev/null +++ b/AssettoServer/Server/Configuration/ConfigurationSchemaGenerator.cs @@ -0,0 +1,108 @@ +using System; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Text.Json; +using AssettoServer.Server.Configuration.Extra; +using Namotion.Reflection; +using NJsonSchema; +using NJsonSchema.Generation; +using NJsonSchema.Generation.TypeMappers; +using YamlDotNet.Serialization; + +namespace AssettoServer.Server.Configuration; + +internal class ConfigurationSchemaGenerator : JsonSchemaGenerator, ISchemaProcessor +{ + private const string SchemaBasePath = "cfg/schemas"; + + private ConfigurationSchemaGenerator(JsonSchemaGeneratorSettings settings) : base(settings) + { + } + + public static string WritePluginConfigurationSchema(Type type) + { + return WriteSchema(type, ACServerConfiguration.PluginConfigurationTypeToFilename(type.Name, "schema.json")); + } + + public static string WriteExtraCfgSchema() => WriteSchema(typeof(ACExtraConfiguration), "extra_cfg.schema.json"); + + private static string WriteSchema(Type type, string filename) + { + Directory.CreateDirectory(SchemaBasePath); + + var schema = GenerateSchema(type); + var path = Path.Join(SchemaBasePath, filename); + File.WriteAllText(path, schema); + return path; + } + + private static string GenerateSchema(Type type) + { + var settings = new SystemTextJsonSchemaGeneratorSettings + { + FlattenInheritanceHierarchy = true, + SerializerOptions = new JsonSerializerOptions + { + Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() } + }, + TypeMappers = [ + new ObjectTypeMapper(typeof(Vector3), new JsonSchema + { + Type = JsonObjectType.Object, + Properties = + { + { "X", new JsonSchemaProperty { Type = JsonObjectType.Number, Format = "float" }}, + { "Y", new JsonSchemaProperty { Type = JsonObjectType.Number, Format = "float" }}, + { "Z", new JsonSchemaProperty { Type = JsonObjectType.Number, Format = "float" }} + } + }) + ] + }; + + var generator = new ConfigurationSchemaGenerator(settings); + settings.SchemaProcessors.Add(generator); + + var schema = generator.Generate(type); + return schema.ToJson(); + } + + public override void ApplyDataAnnotations(JsonSchema schema, JsonTypeDescription typeDescription) + { + var yamlMemberAttribute = typeDescription.ContextualType.GetContextAttribute(true); + schema.Description = yamlMemberAttribute?.Description; + + if (typeDescription.Type != JsonObjectType.Object + && typeDescription.ContextualType.Context is ContextualPropertyInfo info + && info.CanRead + && info.PropertyInfo.GetMethod!.GetParameters().Length == 0) // Special case for Vector3.Item[Int32] + { + var declaringObj = Activator.CreateInstance(info.MemberInfo.DeclaringType!)!; + var defaultValue = info.GetValue(declaringObj); + schema.Default = defaultValue; + } + + base.ApplyDataAnnotations(schema, typeDescription); + } + + public void Process(SchemaProcessorContext context) + { + if (context.ContextualType.GetContextAttribute(true) != null) + { + context.Schema.Title = "IGNORE"; + } + + var ignoredProperties = context.Schema.Properties.Where(p => p.Value.Title == "IGNORE").ToList(); + + foreach (var property in ignoredProperties) + { + context.Schema.Properties.Remove(property.Key); + } + } + + public static void WriteModeLine(StreamWriter writer, string baseFolder, string schemaPath) + { + writer.WriteLine($"# yaml-language-server: $schema={Path.GetRelativePath(baseFolder, schemaPath)}"); + writer.WriteLine(); + } +} diff --git a/AssettoServer/Server/Configuration/Extra/ACExtraConfiguration.cs b/AssettoServer/Server/Configuration/Extra/ACExtraConfiguration.cs index d8327997..2132cb2b 100644 --- a/AssettoServer/Server/Configuration/Extra/ACExtraConfiguration.cs +++ b/AssettoServer/Server/Configuration/Extra/ACExtraConfiguration.cs @@ -1,19 +1,11 @@ -using System; using System.Collections.Generic; using System.IO; using System.Numerics; -using System.Reflection; -using System.Text.Json; -using AssettoServer.Server.Plugin; -using Autofac; using CommunityToolkit.Mvvm.ComponentModel; -using FluentValidation; using JetBrains.Annotations; -using NJsonSchema.Generation; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NodeTypeResolvers; using YamlDotNet.Serialization.ObjectGraphVisitors; #pragma warning disable CS0657 @@ -114,7 +106,6 @@ public partial class ACExtraConfiguration : ObservableObject public AiParams AiParams { get; init; } = new(); [YamlIgnore] public int MaxAfkTimeMilliseconds => MaxAfkTimeMinutes * 60_000; - [YamlIgnore] public string Path { get; private set; } = null!; public void ToFile(string path, bool full = false) { @@ -131,24 +122,6 @@ public void ToStream(StreamWriter writer, bool full = false) } builder.Build().Serialize(writer, this); } - - public static void WriteSchema(string path) - { - var settings = new SystemTextJsonSchemaGeneratorSettings - { - FlattenInheritanceHierarchy = true, - SerializerOptions = new JsonSerializerOptions - { - Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() } - }, - }; - - var generator = new ConfigurationJsonSchemaGenerator(settings); - settings.SchemaProcessors.Add(generator); - - var schema = generator.Generate(typeof(ACExtraConfiguration)); - File.WriteAllText(path, schema.ToJson()); - } public static ACExtraConfiguration FromFile(string path) { @@ -162,59 +135,43 @@ public static ACExtraConfiguration FromFile(string path) var extraCfg = deserializer.Deserialize(yamlParser); - extraCfg.Path = path; + if (yamlParser.Accept(out _)) + { + throw new ConfigurationException( + "Plugins are no longer configured via extra_cfg.yml. Please remove your plugin configuration from extra_cfg.yml and transfer it to the plugin-specific config files in your config folder."); + } + return extraCfg; } - - internal void LoadPluginConfig(ACPluginLoader loader, ContainerBuilder builder) + + public static void WriteReferenceConfig(string schemaPath) { - using var stream = File.OpenText(Path); - - var yamlParser = new Parser(stream); - yamlParser.Consume(); - yamlParser.Accept(out _); - yamlParser.Accept(out _); + const string baseFolder = "cfg"; + var path = Path.Join(baseFolder, "extra_cfg.reference.yml"); - var deserializerBuilder = new DeserializerBuilder().WithoutNodeTypeResolver(typeof(PreventUnknownTagsNodeTypeResolver)); - foreach (var plugin in loader.LoadedPlugins) + FileInfo? info = null; + if (File.Exists(path)) { - if (plugin.ConfigurationType != null) - { - deserializerBuilder.WithTagMapping("!" + plugin.ConfigurationType.Name, plugin.ConfigurationType); - } + info = new FileInfo(path); + info.IsReadOnly = false; } - var deserializer = deserializerBuilder.Build(); - - while (yamlParser.Accept(out _)) + using (var writer = File.CreateText(path)) { - var pluginConfig = deserializer.Deserialize(yamlParser)!; + ConfigurationSchemaGenerator.WriteModeLine(writer, baseFolder, schemaPath); + writer.WriteLine($"# AssettoServer {ThisAssembly.AssemblyInformationalVersion} Reference Configuration"); + writer.WriteLine("# This file serves as an overview of all possible options with their default values."); + writer.WriteLine("# It is NOT read by the server - edit extra_cfg.yml instead!"); + writer.WriteLine(); - foreach (var plugin in loader.LoadedPlugins) - { - if (plugin.ConfigurationType == pluginConfig.GetType() && plugin.ValidatorType != null) - { - var validator = Activator.CreateInstance(plugin.ValidatorType)!; - var method = typeof(DefaultValidatorExtensions).GetMethod("ValidateAndThrow")!; - var generic = method.MakeGenericMethod(pluginConfig.GetType()); - try - { - generic.Invoke(null, new[] { validator, pluginConfig }); - } - catch (TargetInvocationException ex) - { - throw ex.InnerException ?? ex; - } - - break; - } - } - - builder.RegisterInstance(pluginConfig).AsSelf(); + ReferenceConfiguration.ToStream(writer, true); } + + info ??= new FileInfo(path); + info.IsReadOnly = true; } - public static readonly ACExtraConfiguration ReferenceConfiguration = new ACExtraConfiguration() + public static readonly ACExtraConfiguration ReferenceConfiguration = new() { LokiSettings = new LokiSettings { @@ -222,8 +179,7 @@ internal void LoadPluginConfig(ACPluginLoader loader, ContainerBuilder builder) Login = "username", Password = "password" }, - UserGroupCommandPermissions = new List() - { + UserGroupCommandPermissions = [ new() { UserGroup = "weather", @@ -233,7 +189,7 @@ internal void LoadPluginConfig(ACPluginLoader loader, ContainerBuilder builder) "setrain" ] } - }, + ], AiParams = new AiParams { CarSpecificOverrides = [