diff --git a/EntryPoint.cs b/EntryPoint.cs index 341dba2..fdae71a 100644 --- a/EntryPoint.cs +++ b/EntryPoint.cs @@ -6,6 +6,7 @@ using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; using System.Threading; +using Andraste.Payload.ExtraVFS; using Andraste.Payload.ModManagement; using Andraste.Payload.Native; using Andraste.Shared.Lifecycle; @@ -52,6 +53,7 @@ public abstract class EntryPoint : IEntryPoint protected readonly Dictionary FeatureParser = new Dictionary(); public readonly ManagerContainer Container; private readonly ModLoader _modLoader; + private readonly ExtraVFSLoader _extraVFSLoader; protected EntryPoint(RemoteHooking.IContext context) { @@ -59,6 +61,7 @@ protected EntryPoint(RemoteHooking.IContext context) ModFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); Container = new ManagerContainer(); _modLoader = new ModLoader(this); + _extraVFSLoader = new ExtraVFSLoader(this); } public virtual void Run(RemoteHooking.IContext context) @@ -91,7 +94,9 @@ public virtual void Run(RemoteHooking.IContext context) Logger.Trace("Implementing Mods"); ImplementMods(); - + + ImplementExtraVFS(); + Logger.Trace("Waking up the Application"); RemoteHooking.WakeUpProcess(); Logger.Trace("Calling Post-Wakeup"); @@ -189,7 +194,14 @@ protected virtual void LoadFeatureParsers() FeatureParser.Add("andraste.builtin.plugin", new PluginFeatureParser()); } #endregion - + + #region ExtraVFS + protected virtual void ImplementExtraVFS() + { + _extraVFSLoader.LoadExtraVFSFromJson(ModFolder); + } + #endregion + #region Lifecycle /// /// This is called when Andraste has been loaded so far and the user diff --git a/ExtraVFS/ExtraVFSLoader.cs b/ExtraVFS/ExtraVFSLoader.cs new file mode 100755 index 0000000..b1a2dca --- /dev/null +++ b/ExtraVFS/ExtraVFSLoader.cs @@ -0,0 +1,103 @@ +using Andraste.Payload.ModManagement; +using Andraste.Payload.VFS; +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Text.Json; +using NLog; + +namespace Andraste.Payload.ExtraVFS +{ + public class ExtraVFSPairs + { + public ExtraVFSPair[] PathPairs { get; set; } + } + public class ExtraVFSPair + { + public string Source { get; set; } + public string Dest { get; set; } + public bool DestHasToExist { get; set; } + } + public class ExtraVFSLoader + { + private EntryPoint _entryPoint; + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + public ExtraVFSLoader(EntryPoint entryPoint) + { + _entryPoint = entryPoint; + } + + public void LoadExtraVFSFromJson(string modFolder) + { + var vfs = _entryPoint.Container.GetManager(); + if (vfs != null) + { + var jsonFile = Path.Combine(modFolder, "extra_vfs.json"); + if (!File.Exists(jsonFile)) + { + var empty_template = new ExtraVFSPairs(); + empty_template.PathPairs = new ExtraVFSPair[1]; + empty_template.PathPairs[0] = new ExtraVFSPair(); + empty_template.PathPairs[0].Source = "C:\\Some\\Savegame\\Path"; + empty_template.PathPairs[0].Dest = "C:\\Some\\Savegame\\Path"; + empty_template.PathPairs[0].DestHasToExist = false; + var empty_template_string = JsonSerializer.SerializeToUtf8Bytes(empty_template, new JsonSerializerOptions { WriteIndented = true}); + try + { + File.WriteAllBytes(jsonFile, empty_template_string); + } + catch(Exception ex) + { + Logger.Warn("Couldn't create template extra_vfs.json, " + ex); + } + } + + try + { + var pairs = JsonSerializer.Deserialize(File.ReadAllText(jsonFile)); + if (pairs == null) + { + Logger.Warn("Couldn't properly parse extra_vfs.json"); + return; + } + + if (pairs.PathPairs == null) + { + return; + } + + foreach (var pair in pairs.PathPairs) + { + if (!Directory.Exists(pair.Dest) && pair.DestHasToExist) + { + try + { + Directory.CreateDirectory(pair.Dest); + } + catch (Exception ex) + { + Logger.Error("Destination " + pair.Dest + " does not exist and cannot be created, " + ex); + throw ex; + } + } + Logger.Trace("adding pair " + pair.Source + ", " + pair.Dest); + vfs.AddPrefixMapping(pair.Source.Replace('/', '\\'), pair.Dest.Replace('/', '\\')); + } + } + catch(Exception ex) + { + Logger.Warn("ExtraVFS: Couldn't properly parse extra_vfs.json, " + ex); + } + return; + } + else + { + Logger.Info($"The Framework {_entryPoint.FrameworkName} has not enabled VFS Features"); + } + } + } +} diff --git a/Native/Kernel32.cs b/Native/Kernel32.cs index 82ad27f..34cc02e 100644 --- a/Native/Kernel32.cs +++ b/Native/Kernel32.cs @@ -26,7 +26,12 @@ public static extern IntPtr CreateFileA(string lpFileName, uint dwDesiredAccess, public delegate IntPtr Delegate_CreateFileA(string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile); - + + [DllImport("kernel32.dll", CharSet = CharSet.Ansi)] + public static extern bool CreateDirectoryA(string lpFileName, IntPtr lpSecurityAttributes); + + public delegate bool Delegate_CreateDirectoryA(string lpFileName, IntPtr lpSecurityAttribute); + [DllImport("kernel32.dll")] public static extern bool VirtualProtect(IntPtr lpAddress, IntPtr dwSize, uint flNewProtect, out uint lpflOldProtect); diff --git a/VFS/BasicFileRedirectingManager.cs b/VFS/BasicFileRedirectingManager.cs index 9083317..28c6fe9 100644 --- a/VFS/BasicFileRedirectingManager.cs +++ b/VFS/BasicFileRedirectingManager.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Numerics; using Andraste.Payload.Hooking; using Andraste.Payload.Native; using Andraste.Shared.Lifecycle; @@ -24,9 +25,17 @@ public class BasicFileRedirectingManager : IManager // TODO: What about "OpenFile" for older applications? What about CreateFileW? private Hook _createFileHook; private Hook _findFirstFileHook; + private Hook _createDirectoryHook; private readonly ConcurrentDictionary _fileMap = new ConcurrentDictionary(); private readonly List _hooks = new List(); + private struct PrefixMap + { + public string Source { get; set; } + public string Dest { get; set; } + } + private readonly List _prefixMap = new List(); + public bool Enabled { get => _hooks.All(hook => hook.IsActive); @@ -52,12 +61,14 @@ public void Load() LocalHook.GetProcAddress("kernel32.dll", "CreateFileA"), (name, access, mode, attributes, disposition, andAttributes, file) => { - var queryFile = SanitizePath(name); + name = ApplyPrefixMapping(SanitizePath(name)); + var queryFile = name; // Debug Logging // _logger.Trace($"CreateFileA {name} ({queryFile}) => {_fileMap.ContainsKey(queryFile)}"); //if (_fileMap.ContainsKey(queryFile)) _logger.Trace($"{queryFile} redirected to {_fileMap[queryFile]}"); //if (!_fileMap.ContainsKey(queryFile)) _logger.Trace($"{queryFile} could not be redirected"); var fileName = _fileMap.ContainsKey(queryFile) ? _fileMap[queryFile] : name; + return _createFileHook.Original(fileName, access, mode,attributes, disposition, andAttributes, file); }, this); @@ -67,6 +78,7 @@ public void Load() LocalHook.GetProcAddress("kernel32.dll", "FindFirstFileA"), (name, data) => { + name = ApplyPrefixMapping(SanitizePath(name)); if (name.Contains("*") || name.Contains("?")) { // Wildcards are not supported yet (we'd need to fake all search results and manage the handle) @@ -75,12 +87,27 @@ public void Load() // Games like Test Drive Unlimited (2006) are abusing FindFirstFile with an explicit file name to // get all file attributes, such as the file size. - var queryFile = SanitizePath(name); + var queryFile = name; var fileName = _fileMap.ContainsKey(queryFile) ? _fileMap[queryFile] : name; - + return _findFirstFileHook.Original(fileName, data); }, this); _hooks.Add(_findFirstFileHook); + + _createDirectoryHook = new Hook( + LocalHook.GetProcAddress("kernel32.dll", "CreateDirectoryA"), + (name, attributes) => + { + //_logger.Trace("CreateDirectoryA hook with " + name); + + // TDU uses CreateDirectoryA to: + // check if playersave/playersave2 directories exists + // check if it's data directory in ProgramData exists + var fileName = ApplyPrefixMapping(SanitizePath(name)); + + return _createDirectoryHook.Original(fileName, attributes); + }, this); + _hooks.Add(_createDirectoryHook); } private string SanitizePath(string fileName) @@ -106,6 +133,20 @@ private string SanitizePath(string fileName) result = result.Replace('/', '\\'); return result; } + private string ApplyPrefixMapping(string sourcePath) + { + //_logger.Trace("processing " + sourcePath); + foreach (var prefixmap in _prefixMap) + { + if (sourcePath.ToLower().StartsWith(prefixmap.Source)) + { + string ret = prefixmap.Dest + sourcePath.Substring(prefixmap.Source.Length); + //_logger.Trace("redirecting " + sourcePath + " to " + ret); + return ret; + } + } + return sourcePath; + } public void Unload() { @@ -117,6 +158,7 @@ public void Unload() public void ClearMappings() { _fileMap.Clear(); + _prefixMap.Clear(); } /// @@ -139,6 +181,30 @@ public void AddMapping(string sourcePath, string destPath) _fileMap[sourcePath.ToLower()] = destPath; } + /// + /// Adds a custom prefix redirect.
+ /// Note that currently, this functionality is currently limited by the + /// use of the correct path separators (e.g. forward vs. backward slashes)
+ /// All paths are treated as case invariant (windows)/lowercase and need to + /// match the target application (e.g. relative versus absolute path).
+ ///
+ /// This method should NOT be called by Mods, only by the modding framework.
+ /// This is because conflicts cannot be handled and would overwrite each-other.
+ /// Instead the Framework should handle this gracefully and use a priority value + /// or ask the user via the Host Application on a per-file basis. + ///
+ /// The path the target application searches for + /// The path of the file that should be redirected to + [ApiVisibility(Visibility = ApiVisibilityAttribute.EVisibility.ModFrameworkInternalAPI)] + public void AddPrefixMapping(string sourcePath, string destPath) + { + _prefixMap.Add(new PrefixMap + { + Source = sourcePath.ToLower(), + Dest = destPath + }); + } + #nullable enable /// /// Allows other file reading utilities inside Andraste to support VFS redirects by querying them.