2024-12-22 16:38:47 +03:00
|
|
|
using System.Diagnostics.CodeAnalysis;
|
2024-12-27 08:22:17 +03:00
|
|
|
using System.Text.Json;
|
2025-07-10 15:22:15 +03:00
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
2025-05-02 20:06:33 +03:00
|
|
|
using Nebula.Shared.FileApis.Interfaces;
|
2025-07-10 15:22:15 +03:00
|
|
|
using Nebula.Shared.Models;
|
2025-05-05 20:43:28 +03:00
|
|
|
using Nebula.Shared.Services.Logging;
|
2025-05-02 20:06:33 +03:00
|
|
|
using Robust.LoaderApi;
|
2024-12-22 16:38:47 +03:00
|
|
|
|
2025-01-05 17:05:23 +03:00
|
|
|
namespace Nebula.Shared.Services;
|
2024-12-22 16:38:47 +03:00
|
|
|
|
2024-12-27 19:15:33 +03:00
|
|
|
public class ConVar<T>
|
2024-12-22 16:38:47 +03:00
|
|
|
{
|
2025-06-19 21:12:42 +03:00
|
|
|
internal ConfigurationService.OnConfigurationChangedDelegate<T?>? OnValueChanged;
|
|
|
|
|
|
2024-12-28 08:29:01 +03:00
|
|
|
public ConVar(string name, T? defaultValue = default)
|
2024-12-22 16:38:47 +03:00
|
|
|
{
|
2024-12-28 08:29:01 +03:00
|
|
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
2024-12-22 16:38:47 +03:00
|
|
|
DefaultValue = defaultValue;
|
|
|
|
|
}
|
2025-01-14 22:10:16 +03:00
|
|
|
|
|
|
|
|
public string Name { get; }
|
|
|
|
|
public Type Type => typeof(T);
|
|
|
|
|
public T? DefaultValue { get; }
|
2024-12-27 19:15:33 +03:00
|
|
|
}
|
2024-12-22 16:38:47 +03:00
|
|
|
|
2024-12-27 19:15:33 +03:00
|
|
|
public static class ConVarBuilder
|
|
|
|
|
{
|
|
|
|
|
public static ConVar<T> Build<T>(string name, T? defaultValue = default)
|
2024-12-22 16:38:47 +03:00
|
|
|
{
|
2024-12-28 08:29:01 +03:00
|
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
|
|
|
throw new ArgumentException("ConVar name cannot be null or whitespace.", nameof(name));
|
|
|
|
|
|
2024-12-27 19:15:33 +03:00
|
|
|
return new ConVar<T>(name, defaultValue);
|
2024-12-22 16:38:47 +03:00
|
|
|
}
|
2025-07-10 15:22:15 +03:00
|
|
|
|
|
|
|
|
public static ConVar<T> BuildWithMigration<T>(string name, IConfigurationMigration migration, T? defaultValue = default)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
|
|
|
throw new ArgumentException("ConVar name cannot be null or whitespace.", nameof(name));
|
|
|
|
|
|
|
|
|
|
ConfigurationService.AddConfigurationMigration(migration);
|
|
|
|
|
|
|
|
|
|
return new ConVar<T>(name, defaultValue);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public interface IConfigurationMigration
|
|
|
|
|
{
|
|
|
|
|
public Task DoMigrate(ConfigurationService configurationService, IServiceProvider serviceProvider, ILoadingHandler loadingHandler);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public abstract class BaseConfigurationMigration<T1,T2> : IConfigurationMigration
|
|
|
|
|
{
|
|
|
|
|
protected ConVar<T1> OldConVar;
|
|
|
|
|
protected ConVar<T2> NewConVar;
|
|
|
|
|
|
|
|
|
|
public BaseConfigurationMigration(string oldName, string newName)
|
|
|
|
|
{
|
|
|
|
|
OldConVar = ConVarBuilder.Build<T1>(oldName);
|
|
|
|
|
NewConVar = ConVarBuilder.Build<T2>(newName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task DoMigrate(ConfigurationService configurationService, IServiceProvider serviceProvider, ILoadingHandler loadingHandler)
|
|
|
|
|
{
|
|
|
|
|
var oldValue = configurationService.GetConfigValue(OldConVar);
|
|
|
|
|
if(oldValue == null) return;
|
|
|
|
|
|
|
|
|
|
var newValue = await Migrate(serviceProvider, oldValue, loadingHandler);
|
|
|
|
|
configurationService.SetConfigValue(NewConVar, newValue);
|
|
|
|
|
configurationService.ClearConfigValue(OldConVar);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected abstract Task<T2> Migrate(IServiceProvider serviceProvider, T1 oldValue, ILoadingHandler loadingHandler);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class MigrationQueue(List<IConfigurationMigration> migrations) : IConfigurationMigration
|
|
|
|
|
{
|
|
|
|
|
public async Task DoMigrate(ConfigurationService configurationService, IServiceProvider serviceProvider , ILoadingHandler loadingHandler)
|
|
|
|
|
{
|
|
|
|
|
foreach (var migration in migrations)
|
|
|
|
|
{
|
|
|
|
|
await migration.DoMigrate(configurationService, serviceProvider, loadingHandler);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class MigrationQueueBuilder
|
|
|
|
|
{
|
|
|
|
|
public static MigrationQueueBuilder Instance => new();
|
|
|
|
|
|
|
|
|
|
private readonly List<IConfigurationMigration> _migrations = [];
|
|
|
|
|
|
|
|
|
|
public MigrationQueueBuilder With(IConfigurationMigration migration)
|
|
|
|
|
{
|
|
|
|
|
_migrations.Add(migration);
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public MigrationQueue Build()
|
|
|
|
|
{
|
|
|
|
|
return new MigrationQueue(_migrations);
|
|
|
|
|
}
|
2024-12-22 16:38:47 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[ServiceRegister]
|
|
|
|
|
public class ConfigurationService
|
|
|
|
|
{
|
2025-07-10 15:22:15 +03:00
|
|
|
private readonly IServiceProvider _serviceProvider;
|
|
|
|
|
private static List<IConfigurationMigration> _migrations = [];
|
|
|
|
|
|
|
|
|
|
public static void AddConfigurationMigration(IConfigurationMigration configurationMigration)
|
|
|
|
|
{
|
|
|
|
|
_migrations.Add(configurationMigration);
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-10 19:19:30 +03:00
|
|
|
public delegate void OnConfigurationChangedDelegate<in T>(T value);
|
|
|
|
|
|
2025-07-10 15:22:15 +03:00
|
|
|
public IReadWriteFileApi ConfigurationApi { get; }
|
2025-05-10 19:19:30 +03:00
|
|
|
|
2025-05-05 20:43:28 +03:00
|
|
|
private readonly ILogger _logger;
|
2024-12-27 08:22:17 +03:00
|
|
|
|
2025-07-10 15:22:15 +03:00
|
|
|
public ConfigurationService(FileService fileService, DebugService debugService, IServiceProvider serviceProvider)
|
2024-12-22 16:38:47 +03:00
|
|
|
{
|
2025-07-10 15:22:15 +03:00
|
|
|
_serviceProvider = serviceProvider;
|
2025-05-05 20:43:28 +03:00
|
|
|
_logger = debugService.GetLogger(this);
|
2025-05-02 20:06:33 +03:00
|
|
|
ConfigurationApi = fileService.CreateFileApi("config");
|
2024-12-22 16:38:47 +03:00
|
|
|
}
|
|
|
|
|
|
2025-07-10 15:22:15 +03:00
|
|
|
public void MigrateConfigs(ILoadingHandler loadingHandler)
|
|
|
|
|
{
|
|
|
|
|
Task.Run(async () =>
|
|
|
|
|
{
|
|
|
|
|
foreach (var migration in _migrations)
|
|
|
|
|
{
|
|
|
|
|
await migration.DoMigrate(this, _serviceProvider, loadingHandler);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (loadingHandler is IDisposable disposable)
|
|
|
|
|
{
|
|
|
|
|
disposable.Dispose();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-19 21:12:42 +03:00
|
|
|
public ConfigChangeSubscriberDisposable<T> SubscribeVarChanged<T>(ConVar<T> convar, OnConfigurationChangedDelegate<T?> @delegate, bool invokeNow = false)
|
2025-05-10 19:19:30 +03:00
|
|
|
{
|
2025-06-19 21:12:42 +03:00
|
|
|
convar.OnValueChanged += @delegate;
|
|
|
|
|
if (invokeNow)
|
|
|
|
|
{
|
|
|
|
|
@delegate(GetConfigValue(convar));
|
|
|
|
|
}
|
2025-05-10 19:19:30 +03:00
|
|
|
|
2025-06-19 21:12:42 +03:00
|
|
|
return new ConfigChangeSubscriberDisposable<T>(convar, @delegate);
|
2025-05-10 19:19:30 +03:00
|
|
|
}
|
2025-05-02 20:06:33 +03:00
|
|
|
|
2024-12-27 19:15:33 +03:00
|
|
|
public T? GetConfigValue<T>(ConVar<T> conVar)
|
2024-12-22 16:38:47 +03:00
|
|
|
{
|
2024-12-28 08:29:01 +03:00
|
|
|
ArgumentNullException.ThrowIfNull(conVar);
|
2024-12-22 16:38:47 +03:00
|
|
|
|
2024-12-27 08:22:17 +03:00
|
|
|
try
|
|
|
|
|
{
|
2025-05-02 20:06:33 +03:00
|
|
|
if (ConfigurationApi.TryOpen(GetFileName(conVar), out var stream))
|
2024-12-28 08:29:01 +03:00
|
|
|
using (stream)
|
|
|
|
|
{
|
|
|
|
|
var obj = JsonSerializer.Deserialize<T>(stream);
|
|
|
|
|
if (obj != null)
|
|
|
|
|
{
|
2025-05-05 20:43:28 +03:00
|
|
|
_logger.Log($"Successfully loaded config: {conVar.Name}");
|
2024-12-28 08:29:01 +03:00
|
|
|
return obj;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-12-27 08:22:17 +03:00
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
2025-05-05 20:43:28 +03:00
|
|
|
_logger.Error($"Error loading config for {conVar.Name}: {e.Message}");
|
2024-12-27 08:22:17 +03:00
|
|
|
}
|
2024-12-28 08:29:01 +03:00
|
|
|
|
2025-05-05 20:43:28 +03:00
|
|
|
_logger.Log($"Using default value for config: {conVar.Name}");
|
2024-12-28 08:29:01 +03:00
|
|
|
return conVar.DefaultValue;
|
2024-12-22 16:38:47 +03:00
|
|
|
}
|
2025-05-01 20:58:43 +03:00
|
|
|
|
|
|
|
|
public bool TryGetConfigValue<T>(ConVar<T> conVar,
|
|
|
|
|
[NotNullWhen(true)] out T? value)
|
|
|
|
|
{
|
|
|
|
|
ArgumentNullException.ThrowIfNull(conVar);
|
|
|
|
|
value = default;
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-05-02 20:06:33 +03:00
|
|
|
if (ConfigurationApi.TryOpen(GetFileName(conVar), out var stream))
|
2025-05-01 20:58:43 +03:00
|
|
|
using (stream)
|
|
|
|
|
{
|
|
|
|
|
var obj = JsonSerializer.Deserialize<T>(stream);
|
|
|
|
|
if (obj != null)
|
|
|
|
|
{
|
2025-05-05 20:43:28 +03:00
|
|
|
_logger.Log($"Successfully loaded config: {conVar.Name}");
|
2025-05-01 20:58:43 +03:00
|
|
|
value = obj;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
2025-05-05 20:43:28 +03:00
|
|
|
_logger.Error($"Error loading config for {conVar.Name}: {e.Message}");
|
2025-05-01 20:58:43 +03:00
|
|
|
}
|
|
|
|
|
|
2025-05-05 20:43:28 +03:00
|
|
|
_logger.Log($"Using default value for config: {conVar.Name}");
|
2025-05-01 20:58:43 +03:00
|
|
|
return false;
|
|
|
|
|
}
|
2024-12-22 16:38:47 +03:00
|
|
|
|
2025-07-10 15:22:15 +03:00
|
|
|
public void ClearConfigValue<T>(ConVar<T> conVar)
|
|
|
|
|
{
|
|
|
|
|
ConfigurationApi.Remove(GetFileName(conVar));
|
|
|
|
|
conVar.OnValueChanged?.Invoke(conVar.DefaultValue);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-03 12:17:15 +03:00
|
|
|
public void SetConfigValue<T>(ConVar<T> conVar, T? value)
|
2024-12-22 16:38:47 +03:00
|
|
|
{
|
2025-06-19 21:12:42 +03:00
|
|
|
if (value == null)
|
|
|
|
|
{
|
2025-07-10 15:22:15 +03:00
|
|
|
ClearConfigValue(conVar);
|
2025-06-19 21:12:42 +03:00
|
|
|
return;
|
|
|
|
|
}
|
2025-01-14 22:10:16 +03:00
|
|
|
|
2024-12-28 08:29:01 +03:00
|
|
|
if (!conVar.Type.IsInstanceOfType(value))
|
|
|
|
|
{
|
2025-05-05 20:43:28 +03:00
|
|
|
_logger.Error(
|
2025-01-14 22:10:16 +03:00
|
|
|
$"Type mismatch for config {conVar.Name}. Expected {conVar.Type}, got {value.GetType()}.");
|
2024-12-28 08:29:01 +03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-27 08:22:17 +03:00
|
|
|
try
|
|
|
|
|
{
|
2025-05-05 20:43:28 +03:00
|
|
|
_logger.Log($"Saving config: {conVar.Name}");
|
2024-12-28 08:29:01 +03:00
|
|
|
var serializedData = JsonSerializer.Serialize(value);
|
|
|
|
|
|
|
|
|
|
using var stream = new MemoryStream();
|
2024-12-30 22:23:55 +03:00
|
|
|
using var writer = new StreamWriter(stream);
|
|
|
|
|
writer.Write(serializedData);
|
|
|
|
|
writer.Flush();
|
|
|
|
|
stream.Seek(0, SeekOrigin.Begin);
|
2024-12-28 08:29:01 +03:00
|
|
|
|
2025-05-02 20:06:33 +03:00
|
|
|
ConfigurationApi.Save(GetFileName(conVar), stream);
|
2025-06-19 21:12:42 +03:00
|
|
|
conVar.OnValueChanged?.Invoke(value);
|
2024-12-27 08:22:17 +03:00
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
2025-05-05 20:43:28 +03:00
|
|
|
_logger.Error($"Error saving config for {conVar.Name}: {e.Message}");
|
2024-12-27 08:22:17 +03:00
|
|
|
}
|
2024-12-22 16:38:47 +03:00
|
|
|
}
|
|
|
|
|
|
2024-12-28 08:29:01 +03:00
|
|
|
private static string GetFileName<T>(ConVar<T> conVar)
|
2024-12-22 16:38:47 +03:00
|
|
|
{
|
2024-12-28 08:29:01 +03:00
|
|
|
return $"{conVar.Name}.json";
|
2024-12-27 08:22:17 +03:00
|
|
|
}
|
2025-06-19 21:12:42 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public sealed class ConfigChangeSubscriberDisposable<T> : IDisposable
|
|
|
|
|
{
|
|
|
|
|
private readonly ConVar<T> _convar;
|
|
|
|
|
private readonly ConfigurationService.OnConfigurationChangedDelegate<T> _delegate;
|
|
|
|
|
|
|
|
|
|
public ConfigChangeSubscriberDisposable(ConVar<T> convar, ConfigurationService.OnConfigurationChangedDelegate<T> @delegate)
|
|
|
|
|
{
|
|
|
|
|
_convar = convar;
|
|
|
|
|
_delegate = @delegate;
|
|
|
|
|
}
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
_convar.OnValueChanged -= _delegate;
|
|
|
|
|
}
|
2024-12-22 16:38:47 +03:00
|
|
|
}
|