config handler
Some checks failed
Build / Compute Version (push) Successful in 6s
Build / Build Nuget package (NuGet) (push) Successful in 3m38s
Build / Build Nuget package (CringeBootstrap.Abstractions) (push) Successful in 4m2s
Build / Build Nuget package (SharedCringe) (push) Successful in 4m1s
Build / Build Nuget package (CringePlugins) (push) Successful in 4m18s
Build / Build Launcher (push) Failing after 4m31s

global service provider with our stuff so we stop using statics everywhere
polly retry policy for httpclient
This commit is contained in:
zznty
2025-06-02 22:40:16 +07:00
parent 27b859ea8b
commit eba25bbf88
14 changed files with 670 additions and 395 deletions

View File

@@ -0,0 +1,151 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using CringePlugins.Config.Spec;
using Json.Schema;
using Json.Schema.Generation;
using NLog;
using NuGet;
namespace CringePlugins.Config;
public sealed class ConfigHandler
{
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
private readonly DirectoryInfo _configDirectory;
private readonly JsonSerializerOptions _serializerOptions = new(NuGetClient.SerializerOptions)
{
WriteIndented = true,
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip
};
private readonly EvaluationOptions _evaluationOptions = new()
{
OutputFormat = OutputFormat.List,
RequireFormatValidation = true
};
public event EventHandler<ConfigValue>? ConfigReloaded;
internal ConfigHandler(DirectoryInfo configDirectory)
{
_configDirectory = configDirectory;
}
public ConfigReference<T> RegisterConfig<T>(string name, T? defaultInstance = null) where T : class
{
var spec = IConfigurationSpecProvider.FromType(typeof(T));
var path = Path.Join(_configDirectory.FullName, $"{name}.json");
var backupPath = path + $".bak.{DateTimeOffset.Now.ToUnixTimeSeconds()}";
JsonNode? jsonNode = null;
if (File.Exists(path))
{
using var stream = File.OpenRead(path);
try
{
jsonNode = JsonNode.Parse(stream, new JsonNodeOptions { PropertyNameCaseInsensitive = true },
new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true });
}
catch (JsonException e)
{
Log.Warn(e, "Failed to load config {Name}", name);
}
}
var reference = new ConfigReference<T>(name, this);
if (jsonNode == null || (spec != null && !TryValidate(name, spec, jsonNode)))
{
if (File.Exists(path))
File.Move(path, backupPath);
defaultInstance ??= Activator.CreateInstance<T>();
RegisterChange(name, defaultInstance);
return reference;
}
var instance = jsonNode.Deserialize<T>(_serializerOptions)!;
ConfigReloaded?.Invoke(this, new ConfigValue<T>(name, instance));
return reference;
}
internal void RegisterChange<T>(string name, T newValue)
{
var spec = IConfigurationSpecProvider.FromType(typeof(T));
var jsonNode = JsonSerializer.SerializeToNode(newValue, _serializerOptions)!;
if (spec != null && !TryValidate(name, spec, jsonNode))
throw new JsonException($"Supplied config value for {name} is invalid");
var path = Path.Join(_configDirectory.FullName, $"{name}.json");
using var stream = File.Create(path);
using var writer = new Utf8JsonWriter(stream, new()
{
Indented = true
});
jsonNode.WriteTo(writer, _serializerOptions);
ConfigReloaded?.Invoke(this, new ConfigValue<T>(name, newValue));
}
private bool TryValidate(string name, JsonSchema schema, JsonNode jsonNode)
{
var results = schema.Evaluate(jsonNode, _evaluationOptions);
if (results.IsValid)
return true;
Log.Error("Config {Name} is invalid:", name);
foreach (var detail in results.Details)
{
Log.Error("Property {PropertyPath} is invalid:", detail.EvaluationPath);
foreach (var error in detail.Errors?.Values ?? [])
{
Log.Error("\t- {Error}", error);
}
}
return false;
}
public abstract record ConfigValue(string Name);
public sealed record ConfigValue<T>(string Name, T Value) : ConfigValue(Name);
}
public sealed class ConfigReference<T> : IDisposable
{
private readonly string _name;
private readonly ConfigHandler _instance;
private T? _value;
public T Value
{
get => _value ?? throw new InvalidOperationException("Config has not been loaded yet");
set => _instance.RegisterChange(_name, value);
}
internal ConfigReference(string name, ConfigHandler instance)
{
_name = name;
_instance = instance;
instance.ConfigReloaded += InstanceOnConfigReloaded;
}
private void InstanceOnConfigReloaded(object? sender, ConfigHandler.ConfigValue e)
{
if (e.Name != _name) return;
if (e is ConfigHandler.ConfigValue<T> configValue)
_value = configValue.Value;
}
public void Dispose()
{
_instance.ConfigReloaded -= InstanceOnConfigReloaded;
}
public static implicit operator T(ConfigReference<T> reference) => reference.Value;
}

View File

@@ -0,0 +1,21 @@
using System.Reflection;
using Json.Schema;
using Json.Schema.Generation;
namespace CringePlugins.Config.Spec;
public interface IConfigurationSpecProvider
{
static abstract JsonSchema Spec { get; }
static JsonSchema? FromType(Type type)
{
if (type.IsAssignableTo(typeof(IConfigurationSpecProvider)))
{
return (JsonSchema)type.GetProperty(nameof(Spec), BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly)!
.GetValue(null)!;
}
return null;
}
}

View File

@@ -8,6 +8,7 @@
<UseWindowsForms>true</UseWindowsForms>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
@@ -23,6 +24,7 @@
<ItemGroup>
<PackageReference Include="Basic.Reference.Assemblies.Net90" Version="1.8.0" PrivateAssets="all" />
<PackageReference Include="JsonSchema.Net.Generation" Version="5.0.2" />
<PackageReference Include="NLog" Version="5.4.0" />
<PackageReference Include="Lib.Harmony.Thin" Version="2.3.4-torch" />
<PackageReference Include="ImGui.NET.DirectX" Version="1.91.0.1" />

View File

@@ -13,10 +13,11 @@ using NuGet.Frameworks;
using NuGet.Models;
using NuGet.Versioning;
using SharedCringe.Loader;
using VRage.FileSystem;
namespace CringePlugins.Loader;
public class PluginsLifetime(string gameFolder) : ILoadingStage
public class PluginsLifetime(ConfigHandler configHandler, HttpClient client) : ILoadingStage
{
public static ImmutableArray<DerivedAssemblyLoadContext> Contexts { get; private set; } = [];
@@ -28,53 +29,43 @@ public class PluginsLifetime(string gameFolder) : ILoadingStage
// TODO move this as api for other plugins
private readonly DirectoryInfo _dir = Directory.CreateDirectory(Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "CringeLauncher"));
private readonly NuGetRuntimeFramework _runtimeFramework = new(NuGetFramework.ParseFolder("net9.0-windows10.0.19041.0"), RuntimeInformation.RuntimeIdentifier);
private ConfigReference<PackagesConfig>? _configReference;
public async ValueTask Load(ISplashProgress progress)
{
progress.DefineStepsCount(6);
progress.Report("Discovering local plugins");
DiscoverLocalPlugins(_dir.CreateSubdirectory("plugins"));
progress.Report("Loading config");
PackagesConfig? packagesConfig = null;
_configReference = configHandler.RegisterConfig("packages", PackagesConfig.Default);
var packagesConfig = _configReference.Value;
var configDir = _dir.CreateSubdirectory("config");
var configPath = Path.Join(configDir.FullName, "packages.json");
if (File.Exists(configPath))
await using (var stream = File.OpenRead(configPath))
packagesConfig = await JsonSerializer.DeserializeAsync<PackagesConfig>(stream, NuGetClient.SerializerOptions)!;
if (packagesConfig == null)
{
packagesConfig = PackagesConfig.Default;
await using var stream = File.Create(configPath);
await JsonSerializer.SerializeAsync(stream, packagesConfig, NuGetClient.SerializerOptions);
}
progress.Report("Resolving packages");
var sourceMapping = new PackageSourceMapping(packagesConfig.Sources);
var sourceMapping = new PackageSourceMapping(packagesConfig.Sources, client);
// TODO take into account the target framework runtime identifier
var resolver = new PackageResolver(_runtimeFramework.Framework, packagesConfig.Packages, sourceMapping);
var packages = await resolver.ResolveAsync();
progress.Report("Downloading packages");
var builtInPackages = await BuiltInPackages.GetPackagesAsync(_runtimeFramework);
var cachedPackages = await resolver.DownloadPackagesAsync(_dir.CreateSubdirectory("cache"), packages, builtInPackages.Keys.ToHashSet(), progress);
progress.Report("Loading plugins");
//we can move this, but it should be before plugin init
RenderHandler.Current.RegisterComponent(new NotificationsComponent());
await LoadPlugins(cachedPackages, sourceMapping, packagesConfig, builtInPackages);
RenderHandler.Current.RegisterComponent(new PluginListComponent(packagesConfig, sourceMapping, configPath, gameFolder, _plugins));
RenderHandler.Current.RegisterComponent(new PluginListComponent(_configReference, sourceMapping, MyFileSystem.ExePath, _plugins));
}
public void RegisterLifetime()

View File

@@ -0,0 +1,13 @@
using Sandbox;
namespace CringePlugins.Services;
public static class GameServicesExtension
{
internal static IServiceProvider GameServices { get; set; } = null!;
extension(MySandboxGame)
{
public static IServiceProvider Services => GameServices;
}
}

View File

@@ -32,25 +32,23 @@ internal class PluginListComponent : IRenderComponent
private bool _changed;
private bool _open = true;
private PackagesConfig _packagesConfig;
private readonly ConfigReference<PackagesConfig> _packagesConfig;
private readonly PackageSourceMapping _sourceMapping;
private readonly JsonSerializerOptions _serializerOptions = new(JsonSerializerDefaults.Web);
private ImmutableHashSet<PackageSource>? _selectedSources;
private readonly string _configPath;
private readonly string _gameFolder;
private ImmutableArray<PluginInstance> _plugins;
private (SearchResultEntry entry, NuGetClient client)? _selected;
private (PackageSource source, int index)? _selectedSource;
public PluginListComponent(PackagesConfig packagesConfig, PackageSourceMapping sourceMapping, string configPath, string gameFolder,
public PluginListComponent(ConfigReference<PackagesConfig> packagesConfig, PackageSourceMapping sourceMapping, string gameFolder,
ImmutableArray<PluginInstance> plugins)
{
_packagesConfig = packagesConfig;
_sourceMapping = sourceMapping;
_configPath = configPath;
_gameFolder = gameFolder;
_plugins = plugins;
_packages = packagesConfig.Packages.ToImmutableDictionary(b => b.Id, b => b.Range,
_packages = packagesConfig.Value.Packages.ToImmutableDictionary(b => b.Id, b => b.Range,
StringComparer.OrdinalIgnoreCase);
MyScreenManager.ScreenAdded += ScreenChanged;
@@ -153,9 +151,9 @@ internal class PluginListComponent : IRenderComponent
TableSetupColumn("Url", ImGuiTableColumnFlags.None, .8f);
TableHeadersRow();
for (var index = 0; index < _packagesConfig.Sources.Length; index++)
for (var index = 0; index < _packagesConfig.Value.Sources.Length; index++)
{
var source = _packagesConfig.Sources[index];
var source = _packagesConfig.Value.Sources[index];
TableNextRow();
TableNextColumn();
@@ -211,15 +209,15 @@ internal class PluginListComponent : IRenderComponent
if (Button("Save"))
{
var array = _packagesConfig.Sources.RemoveAt(index).Insert(index, selectedSource);
var array = _packagesConfig.Value.Sources.RemoveAt(index).Insert(index, selectedSource);
_packagesConfig = _packagesConfig with
_packagesConfig.Value = _packagesConfig.Value with
{
Sources = array
};
_selectedSource = null;
Save();
}
@@ -227,15 +225,15 @@ internal class PluginListComponent : IRenderComponent
if (Button("Delete"))
{
var array = _packagesConfig.Sources.RemoveAt(index);
var array = _packagesConfig.Value.Sources.RemoveAt(index);
_packagesConfig = _packagesConfig with
_packagesConfig.Value = _packagesConfig.Value with
{
Sources = array
};
_selectedSource = null;
Save();
}
}
@@ -246,10 +244,10 @@ internal class PluginListComponent : IRenderComponent
if (Button("Add New"))
{
var source = new PackageSource("source name", "", "https://url.to/index.json");
var array = _packagesConfig.Sources.Add(source);
_packagesConfig = _packagesConfig with
var array = _packagesConfig.Value.Sources.Add(source);
_packagesConfig.Value = _packagesConfig.Value with
{
Sources = array
};
@@ -274,11 +272,11 @@ internal class PluginListComponent : IRenderComponent
if (configSerializer.Deserialize(fs) is PluginLoaderConfig oldConfig)
{
_packagesConfig = oldConfig.MigratePlugins(_packagesConfig);
_packagesConfig.Value = oldConfig.MigratePlugins(_packagesConfig);
Save(false);
_packages = _packagesConfig.Packages.ToImmutableDictionary(b => b.Id, b => b.Range, StringComparer.OrdinalIgnoreCase);
_packages = _packagesConfig.Value.Packages.ToImmutableDictionary(b => b.Id, b => b.Range, StringComparer.OrdinalIgnoreCase);
}
}
@@ -346,15 +344,15 @@ internal class PluginListComponent : IRenderComponent
_selectedSources.Count > 2 ? $"{_selectedSources.First().Name} +{_selectedSources.Count - 1}" :
string.Join(",", _selectedSources.Select(b => b.Name)), ImGuiComboFlags.WidthFitPreview))
{
foreach (var source in _packagesConfig.Sources)
foreach (var source in _packagesConfig.Value.Sources)
{
var selected = _selectedSources?.Contains(source) ?? true;
if (Selectable(source.Name, ref selected))
{
_selectedSources = selected
? (_selectedSources?.Count ?? 0) + 1 == _packagesConfig.Sources.Length ? null : _selectedSources?.Add(source)
: (_selectedSources ?? _packagesConfig.Sources.ToImmutableHashSet()).Remove(source);
? (_selectedSources?.Count ?? 0) + 1 == _packagesConfig.Value.Sources.Length ? null : _selectedSources?.Add(source)
: (_selectedSources ?? _packagesConfig.Value.Sources.ToImmutableHashSet()).Remove(source);
_searchTask = RefreshAsync();
return;
}
@@ -488,7 +486,7 @@ internal class PluginListComponent : IRenderComponent
Text("Pulled from");
SameLine();
var url = _selected.Value.client.ToString();
TextLinkOpenURL(_packagesConfig.Sources.FirstOrDefault(b => b.Url == url)?.Name ?? url, url);
TextLinkOpenURL(_packagesConfig.Value.Sources.FirstOrDefault(b => b.Url == url)?.Name ?? url, url);
if (selected.Authors is not null)
{
@@ -563,14 +561,12 @@ internal class PluginListComponent : IRenderComponent
private void Save(bool keepPackages = true)
{
_changed = true;
using var stream = File.Create(_configPath);
JsonSerializer.Serialize(stream, keepPackages ? _packagesConfig with
_packagesConfig.Value = keepPackages ? _packagesConfig.Value with
{
Packages = [.._packages.Select(b => new PackageReference(b.Key, b.Value))]
} : _packagesConfig, NuGetClient.SerializerOptions);
Packages = [.. _packages.Select(b => new PackageReference(b.Key, b.Value))]
} : _packagesConfig;
_changed = true;
}
private static unsafe int ComparePlugins(PluginInstance x, PluginInstance y, ImGuiTableSortSpecsPtr specs)

View File

@@ -21,11 +21,16 @@
"type": "Direct",
"requested": "[1.91.0.1, )",
"resolved": "1.91.0.1",
"contentHash": "PpW1gQ9g97h6Hm/h/tkSBOmsBYgGwN8wKNmlJomcQFD/zRY1HPkJZz18XRSfRLHPmH2eeh4hhhZv1KHug7dF9g==",
"contentHash": "PpW1gQ9g97h6Hm/h/tkSBOmsBYgGwN8wKNmlJomcQFD/zRY1HPkJZz18XRSfRLHPmH2eeh4hhhZv1KHug7dF9g=="
},
"JsonSchema.Net.Generation": {
"type": "Direct",
"requested": "[5.0.2, )",
"resolved": "5.0.2",
"contentHash": "+khIPgLqOyFOWjgHSzXMjJijwbQb85/cFRf4NwTaV6QBoGM9IT8LeLCnmwazruwKsx16HB1UFX3mslUujfjVpg==",
"dependencies": {
"System.Buffers": "4.5.1",
"System.Numerics.Vectors": "4.5.0",
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
"Humanizer.Core": "2.14.1",
"JsonSchema.Net": "7.3.4"
}
},
"Krafs.Publicizer": {
@@ -40,8 +45,7 @@
"resolved": "2.3.4-torch",
"contentHash": "UnLUnLLiXfHZdKa1zhi6w8cl8tJTrpVixLtvjFEVtlDA6Rkf06OcZ2gSidcbcgKjTcR+fk5Qsdos3mU5oohzfg==",
"dependencies": {
"MonoMod.Core": "1.2.2",
"System.Text.Json": "9.0.0"
"MonoMod.Core": "1.2.2"
}
},
"NLog": {
@@ -53,8 +57,8 @@
"SpaceEngineersDedicated.ReferenceAssemblies": {
"type": "Direct",
"requested": "[1.*, )",
"resolved": "1.206.30",
"contentHash": "xk/EgMhbG7oT4fPzW1DcFT8tYkxJFPK3+j+t4vms9a/wz8cCmszbilA2Y+JWIpmauUDcfovX8eqAOKlgz3dpcg==",
"resolved": "1.206.32",
"contentHash": "uFhkUUxmumct/turcfMeM2f+jJHxuiB6jAE4JMGa/AOFKCsWIr+ZWTX9hW2muEoJpUNKrzCbGrxH8ssaJUZpig==",
"dependencies": {
"SharpDX": "4.2.0-keen-cringe",
"protobuf-net": "1.0.0"
@@ -66,6 +70,33 @@
"resolved": "20.1.0",
"contentHash": "+GntwnyJ5tCNvUIaQxv2+ehDvZJzGUqlSB5xRBk1hTj1qqBJ6s4vK/OfGD/jae7aTmXiGSm8wpJORosNtQevJQ=="
},
"Humanizer.Core": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw=="
},
"Json.More.Net": {
"type": "Transitive",
"resolved": "2.1.1",
"contentHash": "ZXAKl2VsdnIZeUo1PFII3Oi1m1L4YQjEyDjygHfHln5vgsjgIo749X6xWkv7qFYp8RROES+vOEfDcvvoVgs8kA=="
},
"JsonPointer.Net": {
"type": "Transitive",
"resolved": "5.3.1",
"contentHash": "3e2OJjU0OaE26XC/klgxbJuXvteFWTDJIJv0ITYWcJEoskq7jzUwPSC1s0iz4wPPQnfN7vwwFmg2gJfwRAPwgw==",
"dependencies": {
"Humanizer.Core": "2.14.1",
"Json.More.Net": "2.1.1"
}
},
"JsonSchema.Net": {
"type": "Transitive",
"resolved": "7.3.4",
"contentHash": "7GggWrdzKrtGWETRn3dcMnmuLSyWaDkBK94TK80LEHQEVz4bmsQc7FYO7qL40RDdZU2YPz5d98aT9lW5OYExuA==",
"dependencies": {
"JsonPointer.Net": "5.3.1"
}
},
"Microsoft.Bcl.AsyncInterfaces": {
"type": "Transitive",
"resolved": "6.0.0",
@@ -81,9 +112,7 @@
"resolved": "4.11.0",
"contentHash": "djf8ujmqYImFgB04UGtcsEhHrzVqzHowS+EEl/Yunc5LdrYrZhGBWUTXoCF0NzYXJxtfuD+UVQarWpvrNc94Qg==",
"dependencies": {
"Microsoft.CodeAnalysis.Analyzers": "3.3.4",
"System.Collections.Immutable": "8.0.0",
"System.Reflection.Metadata": "8.0.0"
"Microsoft.CodeAnalysis.Analyzers": "3.3.4"
}
},
"Mono.Cecil": {
@@ -145,16 +174,6 @@
"resolved": "4.2.0-keen-cringe",
"contentHash": "LaJN3h1Gi1FWVdef2I5WtOH9gwzKCBniH0CragarbkN2QheYY6Lqm+91PcOfp1w/4wdVb+k8Kjv3sO393Tphtw=="
},
"System.Buffers": {
"type": "Transitive",
"resolved": "4.5.1",
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
},
"System.Collections.Immutable": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg=="
},
"System.Linq.Async": {
"type": "Transitive",
"resolved": "6.0.1",
@@ -163,29 +182,6 @@
"Microsoft.Bcl.AsyncInterfaces": "6.0.0"
}
},
"System.Numerics.Vectors": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
},
"System.Reflection.Metadata": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==",
"dependencies": {
"System.Collections.Immutable": "8.0.0"
}
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
},
"System.Text.Json": {
"type": "Transitive",
"resolved": "9.0.0",
"contentHash": "js7+qAu/9mQvnhA4EfGMZNEzXtJCDxgkgj8ohuxq/Qxv+R56G+ljefhiJHOxTNiw54q8vmABCWUwkMulNdlZ4A=="
},
"cringebootstrap.abstractions": {
"type": "Project"
},
@@ -211,12 +207,7 @@
"type": "Direct",
"requested": "[1.91.0.1, )",
"resolved": "1.91.0.1",
"contentHash": "PpW1gQ9g97h6Hm/h/tkSBOmsBYgGwN8wKNmlJomcQFD/zRY1HPkJZz18XRSfRLHPmH2eeh4hhhZv1KHug7dF9g==",
"dependencies": {
"System.Buffers": "4.5.1",
"System.Numerics.Vectors": "4.5.0",
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
"contentHash": "PpW1gQ9g97h6Hm/h/tkSBOmsBYgGwN8wKNmlJomcQFD/zRY1HPkJZz18XRSfRLHPmH2eeh4hhhZv1KHug7dF9g=="
},
"Steamworks.NET": {
"type": "Direct",