317 lines
12 KiB
C#
317 lines
12 KiB
C#
using Microsoft.Win32;
|
|
using Mvp.Xml.XInclude;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
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;
|
|
|
|
namespace HashCalculator.GUI
|
|
{
|
|
public class ModXml : IModXml
|
|
{
|
|
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 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, string? sdkRootPath, CancellationToken token)
|
|
{
|
|
BaseDirectory = new DirectoryInfo(FindBaseDirectory(xmlPath));
|
|
var allMods = BaseDirectory.Parent
|
|
?? throw new ArgumentException($"{nameof(xmlPath)}'s {nameof(BaseDirectory)} doesn't have a parent");
|
|
if (!allMods.Name.Equals("Mods", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
throw new DirectoryNotFoundException();
|
|
}
|
|
var allModsParent = allMods.Parent
|
|
?? throw new ArgumentException($"SDK Mods folder doesn't have a parent");
|
|
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 static string? LocateSdkFromRegistry()
|
|
{
|
|
using var hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
|
|
return hklm.GetValue(RegistryPath) as string;
|
|
}
|
|
|
|
public async IAsyncEnumerable<AssetEntry> ProcessDocument()
|
|
{
|
|
if (_processed.Any() || _assets.Any())
|
|
{
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
var task = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await ProcessDocumentInternal(_xmlFullPath);
|
|
}
|
|
catch
|
|
{
|
|
if (!_token.IsCancellationRequested)
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_entries.Complete();
|
|
}
|
|
});
|
|
|
|
while (await _entries.OutputAvailableAsync())
|
|
{
|
|
yield return _entries.Receive();
|
|
}
|
|
|
|
await task;
|
|
}
|
|
|
|
private async Task ProcessDocumentInternal(string fullPath)
|
|
{
|
|
_token.ThrowIfCancellationRequested();
|
|
if (!_processed.TryAdd(fullPath.ToUpperInvariant(), default))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var document = await GetFileAsync(fullPath);
|
|
var rootName = document.Root?.Name
|
|
?? throw new InvalidDataException("Document doesn't have a root");
|
|
if (rootName != EalaAsset + "AssetDeclaration" && rootName != "AssetDeclaration")
|
|
{
|
|
throw new NotSupportedException();
|
|
}
|
|
|
|
var includes = from include in document.Root.Elements(EalaAsset + "Includes").Elements()
|
|
let includePath = GetIncludePath(include, fullPath)
|
|
where includePath != null
|
|
select Task.Run(() => ProcessDocumentInternal(includePath), _token);
|
|
var includesTasks = includes.ToArray();
|
|
|
|
var items = from element in document.Root.Elements()
|
|
where element.Attribute("id") != null
|
|
let entry = AssetEntry.TryParse(element)
|
|
where entry != null
|
|
select entry;
|
|
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}`");
|
|
}
|
|
}
|
|
|
|
await Task.WhenAll(includesTasks);
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (!(error.FileName?.StartsWith("ART:", StringComparison.OrdinalIgnoreCase) is true))
|
|
{
|
|
TracerListener.WriteLine($"[ModXml]: {error}");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
if (!File.Exists(includedSource))
|
|
{
|
|
TracerListener.WriteLine($"[ModXml]: Warning, include path does not exist! It is: {includedSource}");
|
|
return null;
|
|
}
|
|
|
|
return includedSource;
|
|
}
|
|
|
|
private async Task<XDocument> GetFileAsync(string normalizedPath)
|
|
{
|
|
try
|
|
{
|
|
var xml = await File.ReadAllBytesAsync(normalizedPath, _token);
|
|
using var xmlStream = new MemoryStream(xml);
|
|
using var reader = new XIncludingReader(normalizedPath, xmlStream, _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);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
throw new InvalidDataException($"Failed to process XML file: {normalizedPath} - {e.Message}", e);
|
|
}
|
|
}
|
|
|
|
private static string FindBaseDirectory(string currentPath)
|
|
{
|
|
var file = new FileInfo(currentPath);
|
|
var directoryName = file.DirectoryName
|
|
?? throw new ArgumentException($"{nameof(currentPath)} doens't have a directory");
|
|
if (File.Exists(Path.Combine(directoryName, "Data", "mod.xml")))
|
|
{
|
|
return directoryName;
|
|
}
|
|
return FindBaseDirectory(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));
|
|
}
|
|
if (ofObjectToReturn != null && ofObjectToReturn != typeof(Stream))
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
return File.OpenRead(absoluteUri.LocalPath);
|
|
}
|
|
|
|
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));
|
|
}
|
|
var localPath = Path.GetDirectoryName(baseUri.LocalPath)
|
|
?? throw new ArgumentException($"{nameof(baseUri)} doesn't have a local path");
|
|
return new Uri(Path.GetFullPath(ResolvePath(relativeUri, localPath)));
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|