Skip to content

Commit

Permalink
Implement ExtraVFS for extended VFS usage such as redirecting savegames
Browse files Browse the repository at this point in the history
  • Loading branch information
Kethen committed Jun 1, 2023
1 parent df5eff5 commit 42c8f43
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 6 deletions.
16 changes: 14 additions & 2 deletions EntryPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -52,13 +53,15 @@ public abstract class EntryPoint : IEntryPoint
protected readonly Dictionary<string, IFeatureParser> FeatureParser = new Dictionary<string, IFeatureParser>();
public readonly ManagerContainer Container;
private readonly ModLoader _modLoader;
private readonly ExtraVFSLoader _extraVFSLoader;

protected EntryPoint(RemoteHooking.IContext context)
{
GameFolder = Directory.GetCurrentDirectory();
ModFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
Container = new ManagerContainer();
_modLoader = new ModLoader(this);
_extraVFSLoader = new ExtraVFSLoader(this);
}

public virtual void Run(RemoteHooking.IContext context)
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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
/// <summary>
/// This is called when Andraste has been loaded so far and the user
Expand Down
103 changes: 103 additions & 0 deletions ExtraVFS/ExtraVFSLoader.cs
Original file line number Diff line number Diff line change
@@ -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<BasicFileRedirectingManager>();
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<ExtraVFSPairs>(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");
}
}
}
}
7 changes: 6 additions & 1 deletion Native/Kernel32.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
72 changes: 69 additions & 3 deletions VFS/BasicFileRedirectingManager.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -24,9 +25,17 @@ public class BasicFileRedirectingManager : IManager
// TODO: What about "OpenFile" for older applications? What about CreateFileW?
private Hook<Kernel32.Delegate_CreateFileA> _createFileHook;
private Hook<Kernel32.DelegateFindFirstFileA> _findFirstFileHook;
private Hook<Kernel32.Delegate_CreateDirectoryA> _createDirectoryHook;
private readonly ConcurrentDictionary<string, string> _fileMap = new ConcurrentDictionary<string, string>();
private readonly List<Hook> _hooks = new List<Hook>();

private struct PrefixMap
{
public string Source { get; set; }
public string Dest { get; set; }
}
private readonly List<PrefixMap> _prefixMap = new List<PrefixMap>();

public bool Enabled
{
get => _hooks.All(hook => hook.IsActive);
Expand All @@ -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);
Expand All @@ -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)
Expand All @@ -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<Kernel32.Delegate_CreateDirectoryA>(
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)
Expand All @@ -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()
{
Expand All @@ -117,6 +158,7 @@ public void Unload()
public void ClearMappings()
{
_fileMap.Clear();
_prefixMap.Clear();
}

/// <summary>
Expand All @@ -139,6 +181,30 @@ public void AddMapping(string sourcePath, string destPath)
_fileMap[sourcePath.ToLower()] = destPath;
}

/// <summary>
/// Adds a custom prefix redirect.<br />
/// Note that currently, this functionality is currently limited by the
/// use of the correct path separators (e.g. forward vs. backward slashes)<br />
/// All paths are treated as case invariant (windows)/lowercase and need to
/// match the target application (e.g. relative versus absolute path).<br />
/// <br />
/// This method should NOT be called by Mods, only by the modding framework.<br />
/// This is because conflicts cannot be handled and would overwrite each-other.<br />
/// 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.
/// </summary>
/// <param name="sourcePath">The path the target application searches for</param>
/// <param name="destPath">The path of the file that should be redirected to</param>
[ApiVisibility(Visibility = ApiVisibilityAttribute.EVisibility.ModFrameworkInternalAPI)]
public void AddPrefixMapping(string sourcePath, string destPath)
{
_prefixMap.Add(new PrefixMap
{
Source = sourcePath.ToLower(),
Dest = destPath
});
}

#nullable enable
/// <summary>
/// Allows other file reading utilities inside Andraste to support VFS redirects by querying them.
Expand Down

0 comments on commit 42c8f43

Please sign in to comment.