HashCalculator.GUI/ModXml.cs
2020-04-03 04:48:57 +02:00

306 lines
11 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;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
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 DirectoryInfo BaseDirectory { get; }
public int TotalFilesProcessed => _processed.Count;
public int TotalAssets => _assets.Count;
private readonly ConcurrentDictionary<string, byte> _processed = new ConcurrentDictionary<string, byte>();
private readonly ConcurrentDictionary<AssetEntry, string> _assets = new ConcurrentDictionary<AssetEntry, string>();
private readonly ModUriResolver _uriResolver;
private readonly string _xmlFullPath;
private readonly CancellationToken _token;
private readonly BufferBlock<AssetEntry> _entries;
public ModXml(string xmlPath, CancellationToken token)
{
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);
_token = token;
_entries = new BufferBlock<AssetEntry>();
}
public async IAsyncEnumerable<AssetEntry> ProcessDocument()
{
if (_processed.Any() || _assets.Any())
{
throw new InvalidOperationException();
}
var task = Task.Run(() =>
{
try
{
var includes = ProcessDocumentInternal(_xmlFullPath);
while (includes.Any())
{
var result = from include in includes.AsParallel()
from newInclude in ProcessDocumentInternal(include)
select newInclude;
includes = result.ToArray();
}
}
catch
{
if (!_token.IsCancellationRequested)
{
throw;
}
}
finally
{
_entries.Complete();
}
});
while(await _entries.OutputAvailableAsync().ConfigureAwait(false))
{
yield return _entries.Receive();
}
await task.ConfigureAwait(false);
}
private string[] ProcessDocumentInternal(string fullPath)
{
_token.ThrowIfCancellationRequested();
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, fullPath))
{
_entries.Post(item);
}
else
{
var previousPath = _assets[item];
TracerListener.WriteLine($"[ModXml] `{fullPath}`: Attempted to add item `{item.Type}:{item.Name}` multiple times - already processed in `{previousPath}`");
}
}
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)
{
TracerListener.WriteLine($"[ModXml]: {error}");
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;
}
}
}