Skip to content

Commit

Permalink
Implementing file linking for the client file system (#590)
Browse files Browse the repository at this point in the history
* Replace `File.Copy` to `CreateSymbolicLink` for `KeepChanges` file option

* Update config for test (revert commit latter)

* Add check for OS platform and rename link method

* Add handling exception if hard link isn't established

* Add .NET 4.8 Linux method to create link

* Move link logic to `ImmutableLink` and `Link`

* Refactor link creation functions

* Fix `source` and `destination` arguments for `CreateHardLink` and `CreateSymbolicLink`

* Rename link operation to AlwaysOverwrite_Link

* Fix careless CreateSymlinkFromSource call

* Correct link and symlink definitions

* Update config

* Turn off readonly property when deleting

* Revert making the source file readonly

* Remove if in switch-case statement

* Localize exception messages

* Revert "Remove if in switch-case statement"

This reverts commit a89f6a3.

* Load libc from "libc.so.6"

* Mark link methods as static

* Enforce coding style

* Prefer Marshal.ThrowExceptionForHR()

* Remove symlink codes

since the implementation requires a workaround for legacy Windows. Compared with the one in .NET 6 runtime, our implementation is not robust enough.

* Update libc importing to align with dotnet/msbuild

* Throw a specific exception

* Rename `AlwaysOverwrite_Link` to `LinkAsReadOnly` and update `KeepChanges`

Co-authored-by: Metadorius <[email protected]>

* Move `CreateHardLinkFromSource` to `FileHelper`

Co-authored-by: Metadorius <[email protected]>

* Enable fallback by default

* Improve translation game files with new linking method

* Use new linking method to link `ddraw.dll`

* Fix typo

* Split long line

* Add information to the documentation

* Rename `LinkAsReadOnly` to `AlwaysOverwrite_LinkAsReadOnly`

* Update docs

Co-authored-by: Metadorius <[email protected]>

* Improve text

Co-authored-by: SadPencil <[email protected]>

* Rename LinkAsReadOnly for built-in config

* Code style changes

* Rename FileOperationOptions to the singular form

* Remove unnecessary usings

* options -> option

* Explicitly make FileOperationOption.AlwaysOverwrite as default

* Set readonly property for linked translation files in target path

* Update docs to recommend editing source files for translation

* Update Translation.md

---------

Co-authored-by: SadPencil <[email protected]>
Co-authored-by: Metadorius <[email protected]>
Co-authored-by: SadPencil <[email protected]>
  • Loading branch information
4 people authored Dec 6, 2024
1 parent 248a551 commit b47495f
Show file tree
Hide file tree
Showing 35 changed files with 803 additions and 45 deletions.
94 changes: 94 additions & 0 deletions ClientCore/FileHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading.Tasks;

using ClientCore.Extensions;

using Rampastring.Tools;

namespace ClientCore
{
public class FileHelper
{

/// <summary>
/// Establishes a hard link between an existing file and a new file. This function is only supported on the NTFS file system, and only for files, not directories.
/// <br/>
/// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createhardlinkw
/// </summary>
/// <param name="lpFileName">The name of the new file.</param>
/// <param name="lpExistingFileName">The name of the existing file.</param>
/// <param name="lpSecurityAttributes">Reserved; must be NULL.</param>
/// <returns>If the function succeeds, the return value is nonzero. If the function fails, the return value is zero (0).</returns>
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "CreateHardLinkW")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[SupportedOSPlatform("windows")]
private static extern bool CreateHardLink(string lpFileName, string lpExistingFileName, IntPtr lpSecurityAttributes);

/// <summary>
/// The link function makes a new link to the existing file named by oldname, under the new name newname.
/// <br/>
/// https://www.gnu.org/software/libc/manual/html_node/Hard-Links.html
/// <param name="oldname"></param>
/// <param name="newname"></param>
/// <returns>This function returns a value of 0 if it is successful and -1 on failure.</returns>
[DllImport("libc", EntryPoint = "link", SetLastError = true)]
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("osx")]
private static extern int link([MarshalAs(UnmanagedType.LPUTF8Str)] string oldname, [MarshalAs(UnmanagedType.LPUTF8Str)] string newname);

/// <summary>
/// Creates hard link to the source file or copy that file, if got an error.
/// </summary>
/// <param name="source"></param>
/// <param name="destination"></param>
/// <param name="fallback"></param>
/// <exception cref="IOException"></exception>
/// <exception cref="PlatformNotSupportedException"></exception>
public static void CreateHardLinkFromSource(string source, string destination, bool fallback = true)
{
if (fallback)
{
try
{
CreateHardLinkFromSource(source, destination, fallback: false);
}
catch (Exception ex)
{
Logger.Log($"Failed to create hard link at {destination}. Fallback to copy. {ex.Message}");
File.Copy(source, destination, true);
}

return;
}

if (File.Exists(destination))
{
FileInfo destinationFile = new(destination);
destinationFile.IsReadOnly = false;
destinationFile.Delete();
}

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (!CreateHardLink(destination, source, IntPtr.Zero))
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
if (link(source, destination) != 0)
throw new IOException(string.Format("Unable to create hard link at {0} with the following error code: {1}"
.L10N("Client:DTAConfig:CreateHardLinkFailed"), destination, Marshal.GetLastWin32Error()));
}
else
{
throw new PlatformNotSupportedException();
}
}
}
}
12 changes: 10 additions & 2 deletions DTAConfig/DirectDrawWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,20 @@ public bool IsCompatibleWithOS(OSVersion os)
/// </summary>
public void Apply()
{
string ddrawDllSourcePath = SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), ddrawDLLPath);
string ddrawDllTargetPath = SafePath.CombineFilePath(ProgramConstants.GamePath, "ddraw.dll");

if (!string.IsNullOrEmpty(ddrawDLLPath))
{
File.Copy(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), ddrawDLLPath), SafePath.CombineFilePath(ProgramConstants.GamePath, "ddraw.dll"), true);
FileHelper.CreateHardLinkFromSource(ddrawDllSourcePath, ddrawDllTargetPath);
new FileInfo(ddrawDllSourcePath).IsReadOnly = true;
new FileInfo(ddrawDllTargetPath).IsReadOnly = true;
}
else
File.Delete(SafePath.CombineFilePath(ProgramConstants.GamePath, "ddraw.dll"));
{
new FileInfo(ddrawDllTargetPath).IsReadOnly = false;
File.Delete(ddrawDllTargetPath);
}


if (!string.IsNullOrEmpty(ConfigFileName) && !string.IsNullOrEmpty(resConfigFileName)
Expand Down
12 changes: 9 additions & 3 deletions DTAConfig/OptionPanels/DisplayOptionsPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,7 @@ public override bool Save()
IniSettings.Translation.Value = (string)ddTranslation.SelectedItem.Tag;

// copy translation files to the game directory
foreach (TranslationGameFile tgf in ClientConfiguration.Instance.TranslationGameFiles)
ClientConfiguration.Instance.TranslationGameFiles.AsParallel().ForAll(tgf =>
{
string sourcePath = SafePath.CombineFilePath(IniSettings.TranslationFolderPath, tgf.Source);
string targetPath = SafePath.CombineFilePath(ProgramConstants.GamePath, tgf.Target);
Expand All @@ -823,14 +823,20 @@ public override bool Save()
string destinationHash = Utilities.CalculateSHA1ForFile(targetPath);

if (sourceHash != destinationHash)
File.Copy(sourcePath, targetPath, true);
{
FileHelper.CreateHardLinkFromSource(sourcePath, targetPath);
new FileInfo(targetPath).IsReadOnly = true;
}
}
else
{
if (File.Exists(targetPath))
{
new FileInfo(targetPath).IsReadOnly = false;
File.Delete(targetPath);
}
}
}
});

#if TS
IniSettings.BackBufferInVRAM.Value = !chkBackBufferInVRAM.Checked;
Expand Down
8 changes: 4 additions & 4 deletions DTAConfig/Settings/FileSettingCheckBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,11 @@ public bool RefreshSetting()
return Checked != currentValue;
}

public void AddEnabledFile(string source, string destination, FileOperationOptions options)
=> enabledFiles.Add(new FileSourceDestinationInfo(source, destination, options));
public void AddEnabledFile(string source, string destination, FileOperationOption option)
=> enabledFiles.Add(new FileSourceDestinationInfo(source, destination, option));

public void AddDisabledFile(string source, string destination, FileOperationOptions options)
=> disabledFiles.Add(new FileSourceDestinationInfo(source, destination, options));
public void AddDisabledFile(string source, string destination, FileOperationOption option)
=> disabledFiles.Add(new FileSourceDestinationInfo(source, destination, option));

public override void Load()
{
Expand Down
4 changes: 2 additions & 2 deletions DTAConfig/Settings/FileSettingDropDown.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,15 @@ public bool RefreshSetting()
return SelectedIndex != currentValue;
}

public void AddFile(int itemIndex, string source, string destination, FileOperationOptions options)
public void AddFile(int itemIndex, string source, string destination, FileOperationOption option)
{
if (itemIndex < 0 || itemIndex >= Items.Count)
return;

if (itemFilesList.Count < itemIndex + 1)
itemFilesList.Add(new List<FileSourceDestinationInfo>());

itemFilesList[itemIndex].Add(new FileSourceDestinationInfo(source, destination, options));
itemFilesList[itemIndex].Add(new FileSourceDestinationInfo(source, destination, option));
}

public override void Load()
Expand Down
86 changes: 53 additions & 33 deletions DTAConfig/Settings/FileSourceDestinationInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ sealed class FileSourceDestinationInfo
public string DestinationPath => SafePath.CombineFilePath(ProgramConstants.GamePath, destinationPath);
/// <summary>
/// A path where the files edited by user are saved if
/// <see cref="FileOperationOptions"/> is set to <see cref="FileOperationOptions.KeepChanges"/>.
/// <see cref="FileOperationOption"/> is set to <see cref="FileOperationOption.KeepChanges"/>.
/// </summary>
public string CachedPath => SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, "SettingsCache", sourcePath);

public FileOperationOptions FileOperationOptions { get; }
public FileOperationOption FileOperationOption { get; }

public FileSourceDestinationInfo(string source, string destination, FileOperationOptions options)
public FileSourceDestinationInfo(string source, string destination, FileOperationOption option)
{
sourcePath = source;
destinationPath = destination;
FileOperationOptions = options;
FileOperationOption = option;
}

/// <summary>
Expand All @@ -40,13 +40,13 @@ public FileSourceDestinationInfo(string value)
throw new ArgumentException($"{nameof(FileSourceDestinationInfo)}: " +
$"Too few parameters specified in parsed value", nameof(value));

FileOperationOptions options = default(FileOperationOptions);
FileOperationOption option = default(FileOperationOption);
if (parts.Length >= 3)
Enum.TryParse(parts[2], out options);
Enum.TryParse(parts[2], out option);

sourcePath = parts[0];
destinationPath = parts[1];
FileOperationOptions = options;
FileOperationOption = option;
}

/// <summary>
Expand Down Expand Up @@ -78,13 +78,13 @@ public static List<FileSourceDestinationInfo> ParseFSDInfoList(IniSection sectio

/// <summary>
/// Performs file operations from <see cref="SourcePath"/> to
/// <see cref="DestinationPath"/> according to <see cref="FileOperationOptions"/>.
/// <see cref="DestinationPath"/> according to <see cref="FileOperationOption"/>.
/// </summary>
public void Apply()
{
switch (FileOperationOptions)
switch (FileOperationOption)
{
case FileOperationOptions.OverwriteOnMismatch:
case FileOperationOption.OverwriteOnMismatch:
string sourceHash = Utilities.CalculateSHA1ForFile(SourcePath);
string destinationHash = Utilities.CalculateSHA1ForFile(DestinationPath);

Expand All @@ -93,62 +93,81 @@ public void Apply()

break;

case FileOperationOptions.DontOverwrite:
case FileOperationOption.DontOverwrite:
if (!File.Exists(DestinationPath))
File.Copy(SourcePath, DestinationPath, false);

break;

case FileOperationOptions.KeepChanges:
case FileOperationOption.KeepChanges:
if (!File.Exists(DestinationPath))
{
if (File.Exists(CachedPath))
File.Copy(CachedPath, DestinationPath, false);
else
File.Copy(SourcePath, DestinationPath, false);
SafePath.GetDirectory(Path.GetDirectoryName(CachedPath)).Create();

if (!File.Exists(CachedPath))
File.Copy(SourcePath, CachedPath, true);
}

Directory.CreateDirectory(Path.GetDirectoryName(CachedPath));
File.Copy(DestinationPath, CachedPath, true);
FileHelper.CreateHardLinkFromSource(CachedPath, DestinationPath, fallback: true);

break;

case FileOperationOptions.AlwaysOverwrite:
case FileOperationOption.AlwaysOverwrite:
File.Copy(SourcePath, DestinationPath, true);
break;

case FileOperationOption.AlwaysOverwrite_LinkAsReadOnly:
FileHelper.CreateHardLinkFromSource(sourcePath, destinationPath, fallback: true);
new FileInfo(DestinationPath).IsReadOnly = true;
new FileInfo(SourcePath).IsReadOnly = true;
break;

default:
throw new InvalidOperationException($"{nameof(FileSourceDestinationInfo)}: " +
$"Invalid {nameof(FileOperationOptions)} value of {FileOperationOptions}");
$"Invalid {nameof(FileOperationOption)} value of {FileOperationOption}");
}
}

/// <summary>
/// Performs file operations to undo changes made by <see cref="Apply"/>
/// to <see cref="DestinationPath"/> according to <see cref="FileOperationOptions"/>.
/// to <see cref="DestinationPath"/> according to <see cref="FileOperationOption"/>.
/// </summary>
public void Revert()
{
switch (FileOperationOptions)
switch (FileOperationOption)
{
case FileOperationOptions.KeepChanges:
case FileOperationOption.KeepChanges:
if (File.Exists(DestinationPath))
{
SafePath.GetDirectory(Path.GetDirectoryName(CachedPath)).Create();
File.Copy(DestinationPath, CachedPath, true);
string cacheHash = Utilities.CalculateSHA1ForFile(CachedPath);
string destinationHash = Utilities.CalculateSHA1ForFile(DestinationPath);

if (cacheHash != destinationHash)
File.Copy(DestinationPath, CachedPath, true);

File.Delete(DestinationPath);
}
break;

case FileOperationOptions.OverwriteOnMismatch:
case FileOperationOptions.DontOverwrite:
case FileOperationOptions.AlwaysOverwrite:
File.Delete(DestinationPath);
case FileOperationOption.AlwaysOverwrite_LinkAsReadOnly:
case FileOperationOption.OverwriteOnMismatch:
case FileOperationOption.DontOverwrite:
case FileOperationOption.AlwaysOverwrite:
if (File.Exists(DestinationPath))
{
FileInfo destinationFile = new(DestinationPath);
destinationFile.IsReadOnly = false;
destinationFile.Delete();
}

if (FileOperationOption == FileOperationOption.AlwaysOverwrite_LinkAsReadOnly)
new FileInfo(SourcePath).IsReadOnly = false;

break;

default:
throw new InvalidOperationException($"{nameof(FileSourceDestinationInfo)}: " +
$"Invalid {nameof(FileOperationOptions)} value of {FileOperationOptions}");
$"Invalid {nameof(FileOperationOption)} value of {FileOperationOption}");
}
}
}
Expand All @@ -157,11 +176,12 @@ public void Revert()
/// Defines the expected behavior of file operations performed with
/// <see cref="FileSourceDestinationInfo"/>.
/// </summary>
public enum FileOperationOptions
public enum FileOperationOption
{
AlwaysOverwrite,
AlwaysOverwrite = 0,
OverwriteOnMismatch,
DontOverwrite,
KeepChanges
KeepChanges,
AlwaysOverwrite_LinkAsReadOnly,
}
}
20 changes: 20 additions & 0 deletions DXMainClient/Resources/DTA/Compatibility/Configs/aqrit.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
;;; www.bitpatch.com ;;;

RealDDrawPath = AUTO
BltMirror = 0
BltNoTearing = 0
ColorFix = 0
DisableHighDpiScaling = 0
FakeVsync = 0
FakeVsyncInterval = 0
ForceBltNoTearing = 0
ForceDirectDrawEmulation = 1
NoVideoMemory = 0
SingleProcAffinity = 0
ShowFPS = 0






Loading

0 comments on commit b47495f

Please sign in to comment.