272 lines
9.9 KiB
C#
272 lines
9.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Concurrent;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using System.Xml;
|
|
using System.Xml.Linq;
|
|
using Microsoft.Win32;
|
|
using Mvp.Xml.XInclude;
|
|
|
|
namespace HashCalculator.GUI
|
|
{
|
|
public class ModXml
|
|
{
|
|
protected enum DocumentOption
|
|
{
|
|
Normal,
|
|
IsEntryPoint
|
|
}
|
|
|
|
const string RegistryPath = @"Microsoft\Windows\CurrentVersion\Uninstall\{F6A3F605-7B10-4939-8D3D-4594332C1649}";
|
|
public static readonly IEnumerable<string> NotSupportedAttributes = new[]
|
|
{
|
|
"fragid", "set-xml-id", "encoding", "accept", "accept-language"
|
|
};
|
|
public static XNamespace XInclude { get; } = "http://www.w3.org/2001/XInclude";
|
|
public static XNamespace EalaAsset { get; } = "uri:ea.com:eala:asset";
|
|
public bool SdkNotFound { get; }
|
|
public IReadOnlyCollection<string> Errors => _errors;
|
|
private readonly ConcurrentDictionary<string, byte> _processed = new ConcurrentDictionary<string, byte>();
|
|
private readonly ConcurrentDictionary<AssetEntry, byte> _assets = new ConcurrentDictionary<AssetEntry, byte>();
|
|
private readonly ConcurrentQueue<string> _errors = new ConcurrentQueue<string>();
|
|
private readonly ModUriResolver _uriResolver;
|
|
private readonly string _xmlFullPath;
|
|
|
|
public ModXml(string xmlPath)
|
|
{
|
|
var baseDirectory = new DirectoryInfo(FindBaseDirectory(xmlPath));
|
|
var allMods = baseDirectory.Parent;
|
|
if (!allMods.Name.Equals("Mods", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
throw new DirectoryNotFoundException();
|
|
}
|
|
var allModsParent = allMods.Parent;
|
|
using var hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
|
|
if (!(hklm.GetValue(RegistryPath) is string sdkRootPath))
|
|
{
|
|
SdkNotFound = true;
|
|
sdkRootPath = allModsParent.FullName;
|
|
}
|
|
|
|
IReadOnlyCollection<string> GetPaths(string name) => new string[]
|
|
{
|
|
sdkRootPath,
|
|
allModsParent.FullName,
|
|
Path.Combine(baseDirectory.FullName, name),
|
|
Path.Combine(sdkRootPath, "Mods"),
|
|
allMods.FullName,
|
|
Path.Combine(sdkRootPath, name.Equals("Data", StringComparison.OrdinalIgnoreCase) ? "SageXml" : name)
|
|
};
|
|
|
|
_uriResolver = new ModUriResolver(new Dictionary<string, IReadOnlyCollection<string>>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
{ "Art", GetPaths("Art") },
|
|
{ "Audio", GetPaths("Audio") },
|
|
{ "Data", GetPaths("Data") },
|
|
});
|
|
|
|
_xmlFullPath = Path.GetFullPath(xmlPath);
|
|
}
|
|
|
|
public Task<ICollection<AssetEntry>> ProcessDocument()
|
|
{
|
|
if (_processed.Any() || _assets.Any() || _errors.Any())
|
|
{
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
return Task.Run(() =>
|
|
{
|
|
var includes = ProcessDocumentInternal(_xmlFullPath).AsParallel();
|
|
while (includes.Any())
|
|
{
|
|
includes = from include in includes
|
|
from newInclude in ProcessDocumentInternal(include)
|
|
select newInclude;
|
|
}
|
|
|
|
return _assets.Keys;
|
|
});
|
|
}
|
|
|
|
private string[] ProcessDocumentInternal(string fullPath)
|
|
{
|
|
if (!_processed.TryAdd(fullPath.ToUpperInvariant(), default))
|
|
{
|
|
return Array.Empty<string>();
|
|
}
|
|
|
|
var document = GetFile(fullPath);
|
|
if (document.Root.Name != EalaAsset + "AssetDeclaration" && document.Root.Name != "AssetDeclaration")
|
|
{
|
|
throw new NotSupportedException();
|
|
}
|
|
|
|
var items = from element in document.Root.Elements()
|
|
where element.Attribute("id") != null
|
|
select new AssetEntry(element);
|
|
foreach (var item in items)
|
|
{
|
|
if (!_assets.TryAdd(item, default))
|
|
{
|
|
_errors.Enqueue($"Attempted to add item `{item.Type}:{item.Name}` multiple times");
|
|
}
|
|
}
|
|
|
|
var includes = from include in document.Root.Elements(EalaAsset + "Includes").Elements()
|
|
let includePath = GetIncludePath(include, fullPath)
|
|
where includePath != null
|
|
select includePath;
|
|
return includes.ToArray();
|
|
}
|
|
|
|
private string? GetIncludePath(XElement include, string includerPath)
|
|
{
|
|
if (include.Name != EalaAsset + "Include")
|
|
{
|
|
throw new InvalidDataException();
|
|
}
|
|
|
|
if (include.Attribute("type")?.Value == "reference")
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var source = include.Attribute("source")?.Value;
|
|
if (source == null)
|
|
{
|
|
throw new InvalidDataException();
|
|
}
|
|
|
|
string? includedSource;
|
|
try
|
|
{
|
|
includedSource = _uriResolver.ResolveUri(new Uri(includerPath), source).LocalPath;
|
|
}
|
|
catch (FileNotFoundException error)
|
|
{
|
|
_errors.Enqueue(error.Message);
|
|
return null;
|
|
}
|
|
|
|
return includedSource;
|
|
}
|
|
|
|
private XDocument GetFile(string normalizedPath)
|
|
{
|
|
using var reader = new XIncludingReader(normalizedPath, _uriResolver)
|
|
{
|
|
// I dont know why but looks like it need to be set again
|
|
// Otherwise it wont work
|
|
XmlResolver = _uriResolver
|
|
};
|
|
reader.MoveToContent();
|
|
return XDocument.Load(reader);
|
|
}
|
|
|
|
private static string FindBaseDirectory(string currentPath)
|
|
{
|
|
var file = new FileInfo(currentPath);
|
|
if (File.Exists(Path.Combine(file.DirectoryName, "Data", "mod.xml")))
|
|
{
|
|
return file.DirectoryName;
|
|
}
|
|
return FindBaseDirectory(file.DirectoryName);
|
|
}
|
|
}
|
|
|
|
public class ModUriResolver : XmlResolver
|
|
{
|
|
protected IReadOnlyDictionary<string, IReadOnlyCollection<string>> Paths { get; }
|
|
|
|
public ModUriResolver(IReadOnlyDictionary<string, IReadOnlyCollection<string>> paths)
|
|
{
|
|
Paths = paths;
|
|
}
|
|
|
|
public override object GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn)
|
|
{
|
|
if (absoluteUri == null)
|
|
{
|
|
throw new ArgumentNullException($"{nameof(absoluteUri)} is null");
|
|
}
|
|
if (ofObjectToReturn != null && ofObjectToReturn != typeof(Stream))
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
return File.OpenRead(absoluteUri.LocalPath);
|
|
}
|
|
|
|
[SuppressMessage("Globalization", "CA1303:请不要将文本作为本地化参数传递", Justification = "<挂起>")]
|
|
public override Uri ResolveUri(Uri baseUri, string relativeUri)
|
|
{
|
|
if (baseUri == null)
|
|
{
|
|
if (!Path.IsPathRooted(relativeUri))
|
|
{
|
|
throw new ArgumentException($"{nameof(baseUri)} is null and {nameof(relativeUri)} is not rooted");
|
|
}
|
|
return new Uri(relativeUri);
|
|
}
|
|
if (relativeUri == null)
|
|
{
|
|
throw new ArgumentNullException($"{nameof(relativeUri)} is null");
|
|
}
|
|
return new Uri(Path.GetFullPath(ResolvePath(relativeUri, Path.GetDirectoryName(baseUri.LocalPath))));
|
|
}
|
|
|
|
public override Task<object> GetEntityAsync(Uri absoluteUri, string role, Type ofObjectToReturn)
|
|
{
|
|
return base.GetEntityAsync(absoluteUri, role, ofObjectToReturn);
|
|
}
|
|
|
|
public override bool SupportsType(Uri absoluteUri, Type type)
|
|
{
|
|
return base.SupportsType(absoluteUri, type);
|
|
}
|
|
|
|
[SuppressMessage("Globalization", "CA1303:请不要将文本作为本地化参数传递", Justification = "<挂起>")]
|
|
private string ResolvePath(string href, string currentDirectory)
|
|
{
|
|
var splitted = href.Split(new[] { ':' }, 2);
|
|
if (splitted.Length > 1)
|
|
{
|
|
var prefix = splitted[0];
|
|
var path = splitted[1];
|
|
if (Paths.TryGetValue(prefix, out var collection))
|
|
{
|
|
foreach (var directory in collection)
|
|
{
|
|
if (prefix.Equals("Art", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var artPath = Path.Combine(directory, path.Substring(0, 2), path);
|
|
if (File.Exists(artPath))
|
|
{
|
|
return artPath;
|
|
}
|
|
}
|
|
|
|
var realPath = Path.Combine(directory, path);
|
|
if (File.Exists(realPath))
|
|
{
|
|
return realPath;
|
|
}
|
|
}
|
|
throw new FileNotFoundException("Cannot find the file specified", href);
|
|
}
|
|
throw new FileNotFoundException($"Unknown prefix: {prefix}", href);
|
|
}
|
|
|
|
var relativePath = Path.Combine(currentDirectory, href);
|
|
if (!File.Exists(relativePath))
|
|
{
|
|
throw new FileNotFoundException("Cannot find the file specified", href);
|
|
}
|
|
return relativePath;
|
|
}
|
|
}
|
|
}
|