[Save/Sync] In-Progress rewrite of ConfigService and ModConfigService

This commit is contained in:
Maplewheels
2026-01-03 04:59:33 -05:00
parent 7d39c092c6
commit 595470ccfb
5 changed files with 16 additions and 339 deletions

View File

@@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using FluentResults;
namespace Barotrauma.LuaCs.Services.Processing;
public partial class ModConfigService
{
private partial async Task<Result<IModConfigInfo>> GetModConfigInfoAsync(ContentPackage package, XElement root)
{
var asm = root.GetChildElements("Assembly").ToImmutableArray();
var cfg = root.GetChildElements("Config").ToImmutableArray();
var lua = root.GetChildElements("Lua").ToImmutableArray();
return FluentResults.Result.Ok<IModConfigInfo>(new ModConfigInfo()
{
Package = package,
PackageName = package.Name,
Assemblies = asm.Any() ? GetAssemblies(package, asm) : ImmutableArray<IAssemblyResourceInfo>.Empty,
Configs = cfg.Any() ? GetConfigs(package, cfg) : ImmutableArray<IConfigResourceInfo>.Empty,
ConfigProfiles = cfg.Any() ? GetConfigProfiles(package, cfg) : ImmutableArray<IConfigProfileResourceInfo>.Empty,
LuaScripts = lua.Any() ? GetLuaScripts(package, lua) : ImmutableArray<ILuaScriptResourceInfo>.Empty
});
}
}

View File

@@ -54,10 +54,7 @@ namespace Barotrauma
// TODO: INetworkingService
// TODO: [Resource Converter/Parser Services]
// Loaders and Processors (yes the naming is reversed, oops).
_servicesProvider.RegisterServiceType<IParserService<ContentPackage, IModConfigInfo>, ModConfigService>(ServiceLifetime.Transient);
_servicesProvider.RegisterServiceType<IParserServiceAsync<ContentPackage, IModConfigInfo>, ModConfigService>(ServiceLifetime.Transient);
_servicesProvider.RegisterServiceType<IConfigIOService, ConfigIOService>(ServiceLifetime.Transient);
_servicesProvider.RegisterServiceType<IModConfigService, ModConfigService>(ServiceLifetime.Transient);
// service config data
_servicesProvider.RegisterServiceType<IStorageServiceConfig, StorageServiceConfig>(ServiceLifetime.Singleton);

View File

@@ -1,278 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using FarseerPhysics.Common;
using FluentResults;
using OneOf;
namespace Barotrauma.LuaCs.Services.Processing;
public class ConfigIOService : IConfigIOService
{
private readonly IStorageService _storageService;
private readonly IConfigServiceConfig _configServiceConfig;
public ConfigIOService(IStorageService storageService, IConfigServiceConfig configServiceConfig)
{
this._storageService = storageService;
storageService.UseCaching = true;
_configServiceConfig = configServiceConfig;
}
public void Dispose()
{
// stateless service
return;
}
// stateless service
public bool IsDisposed => false;
public FluentResults.Result Reset()
{
_storageService.PurgeCache();
return FluentResults.Result.Ok();
}
public async Task<Result<IReadOnlyList<IConfigInfo>>> TryParseResourceAsync(IConfigResourceInfo src)
{
if (src?.OwnerPackage is null || src.FilePaths.IsDefaultOrEmpty)
return FluentResults.Result.Fail($"{nameof(TryParseResourceAsync)}: Config resource and/or components were null.");
try
{
var infos = await _storageService.LoadPackageXmlFilesAsync(src.OwnerPackage, [..src.FilePaths.Select(fp => fp.FullPath)]);
if (infos.IsDefaultOrEmpty)
return FluentResults.Result.Fail($"{nameof(TryParseResourceAsync)}: No resources found.");
var errList = new List<IError>();
var resList = infos.Select(info =>
{
if (info.Item2.Errors.Any())
errList.AddRange(info.Item2.Errors);
if (info.Item2.IsFailed || info.Item2.Value is not { } configXDoc)
{
errList.Add(new Error($"Unable to parse file: {info.Item1}"));
return default;
}
return (info.Item1, configXDoc);
})
.Where(doc => !doc.Item1.IsNullOrWhiteSpace() && doc.configXDoc != null)
.SelectMany(doc => doc.configXDoc.Root.GetChildElements("Configuration"))
.SelectMany(cfgContainer => cfgContainer.GetChildElements("Configs"))
.SelectMany(cfgContainer => cfgContainer.GetChildElements("Config"))
.Select(async cfgElement =>
{
try
{
OneOf.OneOf<string, XElement> defaultValue = cfgElement.GetChildElement("Value");
if (defaultValue.AsT1 is null)
defaultValue = cfgElement.GetAttributeString("Value", string.Empty);
var internalName = cfgElement.GetAttributeString("Name", string.Empty);
if (internalName.IsNullOrWhiteSpace())
return null;
return new ConfigInfo()
{
DataType = Type.GetType(cfgElement.GetAttributeString("Type", "string")),
OwnerPackage = src.OwnerPackage,
DefaultValue = defaultValue,
Value = await LoadConfigDataFromLocal(src.OwnerPackage, internalName) is { IsSuccess: true } res
? res.Value : defaultValue,
EditableStates = cfgElement.GetAttributeBool("ReadOnly", false)
? RunState.Unloaded // read-only
: RunState.Running, // editable at runtime
InternalName = internalName,
NetSync = Enum.Parse<NetSync>(
cfgElement.GetAttributeString("NetSync", nameof(NetSync.None))),
#if CLIENT
DisplayName = cfgElement.GetAttributeString("DisplayName", null),
Description = cfgElement.GetAttributeString("Description", null),
DisplayCategory = cfgElement.GetAttributeString("Category", null),
ShowInMenus = cfgElement.GetAttributeBool("ShowInMenus", true),
Tooltip = cfgElement.GetAttributeString("Tooltip", null),
ImageIconPath = cfgElement.GetAttributeString("Image", null)
#endif
};
}
catch (Exception e)
{
errList.Add(new Error($"Failed to parse config var for package {src.OwnerPackage}"));
errList.Add(new ExceptionalError(e));
return null;
}
})
.Where(task => task is not null)
.ToImmutableArray();
var result = (await Task.WhenAll(resList)).ToImmutableArray();
var ret = FluentResults.Result.Ok((IReadOnlyList<IConfigInfo>)result);
if (errList.Any())
ret.Errors.AddRange(errList);
return ret;
}
catch(Exception e)
{
return FluentResults.Result.Fail($"Failed to parse config resource for package {src.OwnerPackage}");
}
}
public async Task<ImmutableArray<Result<IReadOnlyList<IConfigInfo>>>> TryParseResourcesAsync(IEnumerable<IConfigResourceInfo> sources)
{
var results = new ConcurrentQueue<Result<IReadOnlyList<IConfigInfo>>>();
var src = sources.ToImmutableArray();
if (!src.Any())
return ImmutableArray<Result<IReadOnlyList<IConfigInfo>>>.Empty;
await src.ParallelForEachAsync(async cfg =>
{
var res = await TryParseResourceAsync(cfg);
results.Enqueue(res);
}, 2); // we only need 2 parallels to buffer against disk loading.
return results.ToImmutableArray();
}
public async Task<Result<IReadOnlyList<IConfigProfileInfo>>> TryParseResourceAsync(IConfigProfileResourceInfo src)
{
if (src?.OwnerPackage is null || src.FilePaths.IsDefaultOrEmpty)
return FluentResults.Result.Fail($"{nameof(TryParseResourceAsync)}: Profile resource and/or components were null.");
try
{
var infos = await _storageService.LoadPackageXmlFilesAsync(src.OwnerPackage, src.FilePaths);
if (infos.IsDefaultOrEmpty)
return FluentResults.Result.Fail($"{nameof(TryParseResourceAsync)}: No resources found.");
var errList = new List<IError>();
var resList = infos.Select(info =>
{
if (info.Item2.Errors.Any())
errList.AddRange(info.Item2.Errors);
if (info.Item2.IsFailed || info.Item2.Value is not { } configXDoc)
{
errList.Add(new Error($"Unable to parse file: {info.Item1}"));
return null;
}
return configXDoc;
})
.Where(doc => doc is not null)
.SelectMany(doc => doc.Root.GetChildElements("Configuration"))
.SelectMany(cfgContainer => cfgContainer.GetChildElements("Profiles"))
.SelectMany(cfgContainer => cfgContainer.GetChildElements("Profile"))
.Select(cfgElement =>
{
try
{
return new ConfigProfileInfo()
{
OwnerPackage = src.OwnerPackage,
InternalName = cfgElement.GetAttributeString("Name", null),
ProfileValues = cfgElement.GetChildElements("ConfigValue")
.Select<XElement, (string ConfigName, OneOf.OneOf<string, XElement> Value)>(element =>
{
if (element.GetAttributeString("Name", null) is not { } name)
return default;
if (element.GetAttributeString("Value", null) is { } value)
return (name, value);
if (element.GetChildElement("Value") is { } xValue)
return (name, xValue);
return default;
})
.Where(val => val.ConfigName is not null && val.Value.Match<bool>(
s => !s.IsNullOrWhiteSpace(),
element => element is not null))
.ToList()
};
}
catch (Exception e)
{
errList.Add(new Error($"Failed to parse profile var for package {src.OwnerPackage}"));
errList.Add(new ExceptionalError(e));
return null;
}
})
.Where(cfgInfo => cfgInfo != null && !cfgInfo.InternalName.IsNullOrWhiteSpace())
.ToImmutableArray();
var ret = FluentResults.Result.Ok((IReadOnlyList<IConfigProfileInfo>)resList);
if (errList.Any())
ret.Errors.AddRange(errList);
return ret;
}
catch(Exception e)
{
return FluentResults.Result.Fail($"Failed to parse profile resource for package {src.OwnerPackage}");
}
}
public async Task<ImmutableArray<Result<IReadOnlyList<IConfigProfileInfo>>>> TryParseResourcesAsync(IEnumerable<IConfigProfileResourceInfo> sources)
{
var results = new ConcurrentQueue<Result<IReadOnlyList<IConfigProfileInfo>>>();
var src = sources.ToImmutableArray();
if (!src.Any())
return ImmutableArray<Result<IReadOnlyList<IConfigProfileInfo>>>.Empty;
await src.ParallelForEachAsync(async cfg =>
{
var res = await TryParseResourceAsync(cfg);
results.Enqueue(res);
}, 2); // we only need 2 parallels to buffer against disk loading.
return results.ToImmutableArray();
}
private static readonly Regex RemoveInvalidChars = new Regex($"[{Regex.Escape(new string(System.IO.Path.GetInvalidFileNameChars()))}]",
RegexOptions.Singleline | RegexOptions.Compiled | RegexOptions.CultureInvariant);
private string SanitizedFileName(string fileName, string replacement = "_")
{
return RemoveInvalidChars.Replace(fileName, replacement);
}
public async Task<FluentResults.Result> SaveConfigDataLocal(ContentPackage package, string configName, XElement serializedValue)
{
if (package is null || package.Name.IsNullOrWhiteSpace() || configName.IsNullOrWhiteSpace() || serializedValue is null)
return FluentResults.Result.Fail($"{nameof(SaveConfigDataLocal)}: Argument(s) were null");
var res = await LoadPackageConfigDocInternal(package);
throw new NotImplementedException(); //TODO: Complete once the locally saved data structure is finalized.
}
public async Task<Result<OneOf<string, XElement>>> LoadConfigDataFromLocal(ContentPackage package, string configName)
{
if (package is null || package.Name.IsNullOrWhiteSpace() || configName.IsNullOrWhiteSpace())
return FluentResults.Result.Fail($"{nameof(LoadConfigDataFromLocal)}: Argument(s) were null");
var filePath = _configServiceConfig.LocalConfigPathPartial.Replace(
_configServiceConfig.FileNamePattern,
$"{SanitizedFileName(package.Name)}.xml");
var res = await _storageService.LoadLocalXmlAsync(package, filePath);
throw new NotImplementedException(); //TODO: Complete once the locally saved data structure is finalized.
}
private async Task<FluentResults.Result<XDocument>> LoadPackageConfigDocInternal(ContentPackage package)
{
var filePath = _configServiceConfig.LocalConfigPathPartial.Replace(
_configServiceConfig.FileNamePattern,
$"{SanitizedFileName(package.Name)}.xml");
throw new NotImplementedException(); //TODO: Complete once the locally saved data structure is finalized.
}
}

View File

@@ -1,15 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs.Services.Processing;
namespace Barotrauma.LuaCs.Services.Processing;
public interface IConfigIOService : IReusableService,
IParserServiceAsync<IConfigResourceInfo, IReadOnlyList<IConfigInfo>>,
IParserServiceAsync<IConfigProfileResourceInfo, IReadOnlyList<IConfigProfileInfo>>
{
Task<FluentResults.Result> SaveConfigDataLocal(ContentPackage package, string configName, XElement serializedValue);
Task<FluentResults.Result<OneOf.OneOf<string, XElement>>> LoadConfigDataFromLocal(ContentPackage package, string configName);
}

View File

@@ -9,6 +9,8 @@ using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using FluentResults;
using Microsoft.Toolkit.Diagnostics;
using MoonSharp.VsCodeDebugger.SDK;
namespace Barotrauma.LuaCs.Services.Processing;
@@ -33,7 +35,7 @@ public sealed class ModConfigService : IModConfigService
_configProfileParserService = configProfileParserService;
}
#region Disposal
#region Dispose
public void Dispose()
{
@@ -44,15 +46,14 @@ public sealed class ModConfigService : IModConfigService
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
protected set => ModUtils.Threading.SetBool(ref _isDisposed, value);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
#endregion
public async Task<Result<IModConfigInfo>> CreateConfigAsync(ContentPackage src)
{
if (src is null)
ArgumentNullException.ThrowIfNull($"{nameof(CreateConfigAsync)}: Source is null.");
Guard.IsNotNull(src, nameof(src));
if (await TryGetModConfigXmlAsync(src) is { IsSuccess: true, Value: { } config })
{
@@ -64,20 +65,23 @@ public sealed class ModConfigService : IModConfigService
public async Task<ImmutableArray<(ContentPackage Source, Result<IModConfigInfo> Config)>> CreateConfigsAsync(ImmutableArray<ContentPackage> src)
{
var builder = ImmutableArray.CreateBuilder<(ContentPackage Source, Result<IModConfigInfo> Config)>();
var builder = new ConcurrentQueue<(ContentPackage Source, Result<IModConfigInfo> Config)>();
foreach (var package in src)
await src.ParallelForEachAsync(async package =>
{
builder.Add((package, await CreateConfigAsync(package)));
}
return builder.ToImmutable();
var res = await CreateConfigAsync(package);
builder.Enqueue((package, res));
});
return builder.OrderBy(pkg => src.IndexOf(pkg.Source)).ToImmutableArray();
}
//--- Helpers
private async Task<Result<XElement>> TryGetModConfigXmlAsync(ContentPackage src)
{
return await _storageService.LoadPackageXmlAsync(src, "ModConfig.xml") is { IsSuccess: true, Value: { Root: {} config} }
? FluentResults.Result.Ok(config)
: FluentResults.Result.Fail<XElement>("ModConfig.xml not found");
}
private async Task<Result<IModConfigInfo>> CreateFromConfigXmlAsync(XElement src)