Skip to content

Commit 7f5763b

Browse files
committed
[Save/Sync] XML Validation System Preparation Work.
1 parent 585ce5e commit 7f5763b

File tree

6 files changed

+342
-226
lines changed

6 files changed

+342
-226
lines changed

Barotrauma/BarotraumaShared/Lua/Content/ModConfig.xml

Whitespace-only changes.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?xml version="1.0"?>
2+
<!-- For: DataType="typeof(Barotrauma.LuaCs.Data.IModConfig)"-->
3+
<schema
4+
xmlns="http://www.w3.org/2001/XMLSchema"
5+
xmlns:xs="http://www.w3.org/2001/XMLSchema-datatypes"
6+
xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning"
7+
targetNamespace="Barotrauma.LuaCs.Data"
8+
vc:minVersion="1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
9+
<!-- ATTRIBUTES -->
10+
<!--Attribute "Folder" restrictions-->
11+
<attribute name="Folder">
12+
<simpleType>
13+
<restriction base="string"/>
14+
</simpleType>
15+
</attribute>
16+
17+
<!--Attribute "File" restrictions-->
18+
<attribute name="File">
19+
<simpleType>
20+
<restriction base="string"/>
21+
</simpleType>
22+
</attribute>
23+
24+
<!--Attribute "Platform" restrictions-->
25+
<attribute name="Platform">
26+
<simpleType>
27+
<restriction base="string">
28+
<pattern value="(Windows|Linux|OSX)"/>
29+
</restriction>
30+
</simpleType>
31+
</attribute>
32+
33+
<!--Attribute "Target" restrictions-->
34+
<attribute name="Target">
35+
<simpleType>
36+
<restriction base="string">
37+
<pattern value="(Client|Server|Any)"/>
38+
</restriction>
39+
</simpleType>
40+
</attribute>
41+
42+
<!--Attribute "Culture" restrictions-->
43+
<attribute name="Culture">
44+
<simpleType>
45+
<restriction base="string">
46+
<pattern value="^[a-z]{2,3}(?:-[A-Z]{2,3}(?:-[a-zA-Z]{4})?)?$"/>
47+
</restriction>
48+
</simpleType>
49+
</attribute>
50+
</schema>

Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,11 @@ void RegisterServices()
6565
_servicesProvider.RegisterServiceType<IProcessorService<IReadOnlyList<ILocalizationResourceInfo>, ILocalizationsResourcesInfo>, ResourceInfoArrayPacker>(ServiceLifetime.Transient);
6666
_servicesProvider.RegisterServiceType<IProcessorService<IReadOnlyList<ILuaScriptResourceInfo>, ILuaScriptsResourcesInfo>, ResourceInfoArrayPacker>(ServiceLifetime.Transient);
6767

68+
// Loaders and Processors (yes the naming is reversed, oops).
6869
_servicesProvider.RegisterServiceType<IConverterService<ContentPackage, IModConfigInfo>, ModConfigService>(ServiceLifetime.Transient);
6970
_servicesProvider.RegisterServiceType<IConverterServiceAsync<ContentPackage, IModConfigInfo>, ModConfigService>(ServiceLifetime.Transient);
7071
_servicesProvider.RegisterServiceType<IConverterServiceAsync<ILocalizationResourceInfo, ImmutableArray<ILocalizationInfo>>, ResourceInfoLoaders>(ServiceLifetime.Transient);
71-
_servicesProvider.RegisterServiceType<IConverterServiceAsync<IConfigResourceInfo, IReadOnlyList<IConfigInfo>>, ResourceInfoLoaders>(ServiceLifetime.Transient);
72-
_servicesProvider.RegisterServiceType<IConverterServiceAsync<IConfigProfileResourceInfo, IReadOnlyList<IConfigProfileInfo>>, ResourceInfoLoaders>(ServiceLifetime.Transient);
73-
72+
_servicesProvider.RegisterServiceType<IConfigIOService, ConfigIOService>(ServiceLifetime.Transient);
7473

7574
_servicesProvider.Compile();
7675
}
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Collections.Generic;
4+
using System.Collections.Immutable;
5+
using System.Linq;
6+
using System.Text.RegularExpressions;
7+
using System.Threading.Tasks;
8+
using System.Xml.Linq;
9+
using Barotrauma.LuaCs.Data;
10+
using FarseerPhysics.Common;
11+
using FluentResults;
12+
using OneOf;
13+
14+
namespace Barotrauma.LuaCs.Services.Processing;
15+
16+
public class ConfigIOService : IConfigIOService
17+
{
18+
private readonly IStorageService _storageService;
19+
private readonly IConfigServiceConfig _configServiceConfig;
20+
21+
public ConfigIOService(IStorageService storageService, IConfigServiceConfig configServiceConfig)
22+
{
23+
this._storageService = storageService;
24+
storageService.UseCaching = true;
25+
_configServiceConfig = configServiceConfig;
26+
}
27+
28+
public void Dispose()
29+
{
30+
// stateless service
31+
return;
32+
}
33+
34+
// stateless service
35+
public bool IsDisposed => false;
36+
public FluentResults.Result Reset()
37+
{
38+
_storageService.PurgeCache();
39+
return FluentResults.Result.Ok();
40+
}
41+
42+
public async Task<Result<IReadOnlyList<IConfigInfo>>> TryParseResourceAsync(IConfigResourceInfo src)
43+
{
44+
if (src?.OwnerPackage is null || src.FilePaths.IsDefaultOrEmpty)
45+
return FluentResults.Result.Fail($"{nameof(TryParseResourceAsync)}: Config resource and/or components were null.");
46+
47+
try
48+
{
49+
var infos = await _storageService.LoadPackageXmlFilesAsync(src.OwnerPackage, src.FilePaths);
50+
if (infos.IsDefaultOrEmpty)
51+
return FluentResults.Result.Fail($"{nameof(TryParseResourceAsync)}: No resources found.");
52+
53+
var errList = new List<IError>();
54+
55+
var resList = infos.Select(info =>
56+
{
57+
if (info.Item2.Errors.Any())
58+
errList.AddRange(info.Item2.Errors);
59+
if (info.Item2.IsFailed || info.Item2.Value is not { } configXDoc)
60+
{
61+
errList.Add(new Error($"Unable to parse file: {info.Item1}"));
62+
return default;
63+
}
64+
65+
return (info.Item1, configXDoc);
66+
})
67+
.Where(doc => !doc.Item1.IsNullOrWhiteSpace() && doc.configXDoc != null)
68+
.SelectMany(doc => doc.configXDoc.Root.GetChildElements("Configuration"))
69+
.SelectMany(cfgContainer => cfgContainer.GetChildElements("Configs"))
70+
.SelectMany(cfgContainer => cfgContainer.GetChildElements("Config"))
71+
.Select(async cfgElement =>
72+
{
73+
try
74+
{
75+
OneOf.OneOf<string, XElement> defaultValue = cfgElement.GetChildElement("Value");
76+
if (defaultValue.AsT1 is null)
77+
defaultValue = cfgElement.GetAttributeString("Value", string.Empty);
78+
79+
var internalName = cfgElement.GetAttributeString("Name", string.Empty);
80+
if (internalName.IsNullOrWhiteSpace())
81+
return null;
82+
83+
return new ConfigInfo()
84+
{
85+
DataType = Type.GetType(cfgElement.GetAttributeString("Type", "string")),
86+
OwnerPackage = src.OwnerPackage,
87+
DefaultValue = defaultValue,
88+
Value = await LoadConfigDataFromLocal(src.OwnerPackage, internalName) is { IsSuccess: true } res
89+
? res.Value : defaultValue,
90+
EditableStates = cfgElement.GetAttributeBool("ReadOnly", false)
91+
? RunState.Unloaded // read-only
92+
: RunState.Running, // editable at runtime
93+
InternalName = internalName,
94+
NetSync = Enum.Parse<NetSync>(
95+
cfgElement.GetAttributeString("NetSync", nameof(NetSync.None))),
96+
#if CLIENT
97+
DisplayName = cfgElement.GetAttributeString("DisplayName", null),
98+
Description = cfgElement.GetAttributeString("Description", null),
99+
DisplayCategory = cfgElement.GetAttributeString("Category", null),
100+
ShowInMenus = cfgElement.GetAttributeBool("ShowInMenus", true),
101+
Tooltip = cfgElement.GetAttributeString("Tooltip", null),
102+
ImageIconPath = cfgElement.GetAttributeString("Image", null)
103+
#endif
104+
};
105+
}
106+
catch (Exception e)
107+
{
108+
errList.Add(new Error($"Failed to parse config var for package {src.OwnerPackage}"));
109+
errList.Add(new ExceptionalError(e));
110+
return null;
111+
}
112+
})
113+
.Where(task => task is not null)
114+
.ToImmutableArray();
115+
116+
var result = (await Task.WhenAll(resList)).ToImmutableArray();
117+
118+
var ret = FluentResults.Result.Ok((IReadOnlyList<IConfigInfo>)result);
119+
if (errList.Any())
120+
ret.Errors.AddRange(errList);
121+
return ret;
122+
}
123+
catch(Exception e)
124+
{
125+
return FluentResults.Result.Fail($"Failed to parse config resource for package {src.OwnerPackage}");
126+
}
127+
}
128+
129+
public async Task<ImmutableArray<Result<IReadOnlyList<IConfigInfo>>>> TryParseResourcesAsync(IEnumerable<IConfigResourceInfo> sources)
130+
{
131+
var results = new ConcurrentQueue<Result<IReadOnlyList<IConfigInfo>>>();
132+
133+
var src = sources.ToImmutableArray();
134+
if (!src.Any())
135+
return ImmutableArray<Result<IReadOnlyList<IConfigInfo>>>.Empty;
136+
137+
await src.ParallelForEachAsync(async cfg =>
138+
{
139+
var res = await TryParseResourceAsync(cfg);
140+
results.Enqueue(res);
141+
}, 2); // we only need 2 parallels to buffer against disk loading.
142+
143+
return results.ToImmutableArray();
144+
}
145+
146+
public async Task<Result<IReadOnlyList<IConfigProfileInfo>>> TryParseResourceAsync(IConfigProfileResourceInfo src)
147+
{
148+
if (src?.OwnerPackage is null || src.FilePaths.IsDefaultOrEmpty)
149+
return FluentResults.Result.Fail($"{nameof(TryParseResourceAsync)}: Profile resource and/or components were null.");
150+
151+
try
152+
{
153+
var infos = await _storageService.LoadPackageXmlFilesAsync(src.OwnerPackage, src.FilePaths);
154+
if (infos.IsDefaultOrEmpty)
155+
return FluentResults.Result.Fail($"{nameof(TryParseResourceAsync)}: No resources found.");
156+
157+
var errList = new List<IError>();
158+
159+
var resList = infos.Select(info =>
160+
{
161+
if (info.Item2.Errors.Any())
162+
errList.AddRange(info.Item2.Errors);
163+
if (info.Item2.IsFailed || info.Item2.Value is not { } configXDoc)
164+
{
165+
errList.Add(new Error($"Unable to parse file: {info.Item1}"));
166+
return null;
167+
}
168+
169+
return configXDoc;
170+
})
171+
.Where(doc => doc is not null)
172+
.SelectMany(doc => doc.Root.GetChildElements("Configuration"))
173+
.SelectMany(cfgContainer => cfgContainer.GetChildElements("Profiles"))
174+
.SelectMany(cfgContainer => cfgContainer.GetChildElements("Profile"))
175+
.Select(cfgElement =>
176+
{
177+
try
178+
{
179+
return new ConfigProfileInfo()
180+
{
181+
OwnerPackage = src.OwnerPackage,
182+
InternalName = cfgElement.GetAttributeString("Name", null),
183+
ProfileValues = cfgElement.GetChildElements("ConfigValue")
184+
.Select<XElement, (string ConfigName, OneOf.OneOf<string, XElement> Value)>(element =>
185+
{
186+
if (element.GetAttributeString("Name", null) is not { } name)
187+
return default;
188+
if (element.GetAttributeString("Value", null) is { } value)
189+
return (name, value);
190+
if (element.GetChildElement("Value") is { } xValue)
191+
return (name, xValue);
192+
return default;
193+
})
194+
.Where(val => val.ConfigName is not null && val.Value.Match<bool>(
195+
s => !s.IsNullOrWhiteSpace(),
196+
element => element is not null))
197+
.ToList()
198+
};
199+
}
200+
catch (Exception e)
201+
{
202+
errList.Add(new Error($"Failed to parse profile var for package {src.OwnerPackage}"));
203+
errList.Add(new ExceptionalError(e));
204+
return null;
205+
}
206+
})
207+
.Where(cfgInfo => cfgInfo != null && !cfgInfo.InternalName.IsNullOrWhiteSpace())
208+
.ToImmutableArray();
209+
210+
var ret = FluentResults.Result.Ok((IReadOnlyList<IConfigProfileInfo>)resList);
211+
if (errList.Any())
212+
ret.Errors.AddRange(errList);
213+
return ret;
214+
}
215+
catch(Exception e)
216+
{
217+
return FluentResults.Result.Fail($"Failed to parse profile resource for package {src.OwnerPackage}");
218+
}
219+
}
220+
221+
public async Task<ImmutableArray<Result<IReadOnlyList<IConfigProfileInfo>>>> TryParseResourcesAsync(IEnumerable<IConfigProfileResourceInfo> sources)
222+
{
223+
var results = new ConcurrentQueue<Result<IReadOnlyList<IConfigProfileInfo>>>();
224+
225+
var src = sources.ToImmutableArray();
226+
if (!src.Any())
227+
return ImmutableArray<Result<IReadOnlyList<IConfigProfileInfo>>>.Empty;
228+
229+
await src.ParallelForEachAsync(async cfg =>
230+
{
231+
var res = await TryParseResourceAsync(cfg);
232+
results.Enqueue(res);
233+
}, 2); // we only need 2 parallels to buffer against disk loading.
234+
235+
return results.ToImmutableArray();
236+
}
237+
238+
private static readonly Regex RemoveInvalidChars = new Regex($"[{Regex.Escape(new string(System.IO.Path.GetInvalidFileNameChars()))}]",
239+
RegexOptions.Singleline | RegexOptions.Compiled | RegexOptions.CultureInvariant);
240+
241+
private string SanitizedFileName(string fileName, string replacement = "_")
242+
{
243+
return RemoveInvalidChars.Replace(fileName, replacement);
244+
}
245+
246+
public async Task<FluentResults.Result> SaveConfigDataLocal(ContentPackage package, string configName, XElement serializedValue)
247+
{
248+
if (package is null || package.Name.IsNullOrWhiteSpace() || configName.IsNullOrWhiteSpace() || serializedValue is null)
249+
return FluentResults.Result.Fail($"{nameof(SaveConfigDataLocal)}: Argument(s) were null");
250+
251+
var res = await LoadPackageConfigDocInternal(package);
252+
253+
}
254+
255+
public async Task<Result<OneOf<string, XElement>>> LoadConfigDataFromLocal(ContentPackage package, string configName)
256+
{
257+
if (package is null || package.Name.IsNullOrWhiteSpace() || configName.IsNullOrWhiteSpace())
258+
return FluentResults.Result.Fail($"{nameof(LoadConfigDataFromLocal)}: Argument(s) were null");
259+
260+
var filePath = _configServiceConfig.LocalConfigPathPartial.Replace(
261+
_configServiceConfig.FileNamePattern,
262+
$"{SanitizedFileName(package.Name)}.xml");
263+
264+
var res = await _storageService.LoadLocalXmlAsync(package, filePath);
265+
}
266+
267+
private async Task<FluentResults.Result<XDocument>> LoadPackageConfigDocInternal(ContentPackage package)
268+
{
269+
var filePath = _configServiceConfig.LocalConfigPathPartial.Replace(
270+
_configServiceConfig.FileNamePattern,
271+
$"{SanitizedFileName(package.Name)}.xml");
272+
}
273+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Collections.Generic;
2+
using System.Threading.Tasks;
3+
using System.Xml.Linq;
4+
using Barotrauma.LuaCs.Data;
5+
using Barotrauma.LuaCs.Services.Processing;
6+
7+
namespace Barotrauma.LuaCs.Services.Processing;
8+
9+
public interface IConfigIOService : IReusableService,
10+
IConverterServiceAsync<IConfigResourceInfo, IReadOnlyList<IConfigInfo>>,
11+
IConverterServiceAsync<IConfigProfileResourceInfo, IReadOnlyList<IConfigProfileInfo>>
12+
{
13+
Task<FluentResults.Result> SaveConfigDataLocal(ContentPackage package, string configName, XElement serializedValue);
14+
Task<FluentResults.Result<OneOf.OneOf<string, XElement>>> LoadConfigDataFromLocal(ContentPackage package, string configName);
15+
}

0 commit comments

Comments
 (0)