diff --git a/.gitignore b/.gitignore index 2e451550..f00cb308 100644 --- a/.gitignore +++ b/.gitignore @@ -138,4 +138,4 @@ libs/FNA.dll .idea/ #Custom properties -Custom.props +Custom.props \ No newline at end of file diff --git a/BepInEx.Hacknet/BepInEx.Hacknet.csproj b/BepInEx.Hacknet/BepInEx.Hacknet.csproj index 93e14800..017ac374 100644 --- a/BepInEx.Hacknet/BepInEx.Hacknet.csproj +++ b/BepInEx.Hacknet/BepInEx.Hacknet.csproj @@ -1,4 +1,4 @@ - + @@ -21,6 +21,9 @@ + + + + + + <_BepInExCoreFiles Include="$(MSBuildThisFileDirectory)bin/$(Configuration)/*" /> + + + + + + + + + + + + + + + + + + diff --git a/Configurations.props b/Configurations.props index ccce199d..51adc232 100644 --- a/Configurations.props +++ b/Configurations.props @@ -1,4 +1,4 @@ - + @@ -38,6 +38,7 @@ $(PathfinderSolutionDir)libs/ $(PathfinderSolutionDir)PathfinderPatcher/ $(PatcherDir)bin/$(Configuration)/ + $(PathfinderSolutionDir).debug/ $(AssemblySearchPaths); $(LibsDir); diff --git a/Configurations.targets b/Configurations.targets new file mode 100644 index 00000000..61997748 --- /dev/null +++ b/Configurations.targets @@ -0,0 +1,65 @@ + + + + + + <_PathfinderTempMoveFiles Include="$(HacknetDir)HacknetPathfinder.*" /> + <_PathfinderTempMoveFiles Include="$(HacknetDir)StartPathfinder.sh" /> + <_PathfinderTempMoveFiles Include="$(HacknetDir)Linux/intercept.so" /> + <_PathfinderTempMoveFiles Include="$(Hacknetdir)BepInEx" /> + <_PathfinderUnixRunFiles Include="$(PathfinderSolutionDir)Linux/StartPathfinder.sh" /> + <_PathfinderUnixRunFiles Include="$(PathfinderSolutionDir)Linux/intercept.so" /> + <_HacknetKickstartFiles Include="$(HacknetDir)Hacknet.bin.*" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Hacknet-Pathfinder.sln b/Hacknet-Pathfinder.sln index 05149308..226fb187 100644 --- a/Hacknet-Pathfinder.sln +++ b/Hacknet-Pathfinder.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PathfinderAPI", "Pathfinder EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PathfinderUpdater", "PathfinderUpdater\PathfinderUpdater.csproj", "{A21A9ADF-50A9-4F73-AA14-59CF85E4CA9B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PathfinderBuildTasks", "PathfinderBuildTasks\PathfinderBuildTasks.csproj", "{915CE70F-2E7E-4EF2-9083-98CF1A6EF887}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {A21A9ADF-50A9-4F73-AA14-59CF85E4CA9B}.Debug|Any CPU.Build.0 = Debug|Any CPU {A21A9ADF-50A9-4F73-AA14-59CF85E4CA9B}.Release|Any CPU.ActiveCfg = Release|Any CPU {A21A9ADF-50A9-4F73-AA14-59CF85E4CA9B}.Release|Any CPU.Build.0 = Release|Any CPU + {915CE70F-2E7E-4EF2-9083-98CF1A6EF887}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {915CE70F-2E7E-4EF2-9083-98CF1A6EF887}.Debug|Any CPU.Build.0 = Debug|Any CPU + {915CE70F-2E7E-4EF2-9083-98CF1A6EF887}.Release|Any CPU.ActiveCfg = Release|Any CPU + {915CE70F-2E7E-4EF2-9083-98CF1A6EF887}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/PathfinderAPI/PathfinderAPI.csproj b/PathfinderAPI/PathfinderAPI.csproj index 853862da..06e29117 100644 --- a/PathfinderAPI/PathfinderAPI.csproj +++ b/PathfinderAPI/PathfinderAPI.csproj @@ -1,4 +1,4 @@ - + @@ -26,4 +26,28 @@ False + + + + + + + + + + + + + + diff --git a/PathfinderBuildTasks/MoveDir.cs b/PathfinderBuildTasks/MoveDir.cs new file mode 100644 index 00000000..442490d7 --- /dev/null +++ b/PathfinderBuildTasks/MoveDir.cs @@ -0,0 +1,318 @@ +using System.Diagnostics; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Security; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace PathfinderBuildTasks +{ + public class MoveDir : Task, ICancelableTask + { + private bool _canceling; + + [Required] + public ITaskItem[] SourceDirectories { get; set; } + + public ITaskItem DestinationFolder { get; set; } + + public ITaskItem[] DestinationDirectories { get; set; } + + public bool OverwriteReadOnlyFiles { get; set; } + + public bool UseSymlinkOrJunction { get; set; } + + [Output] + public ITaskItem[] MovedDirectories { get; set; } + + public void Cancel() + { + _canceling = true; + } + + public override bool Execute() + { + bool success = true; + + if (SourceDirectories == null || SourceDirectories.Length == 0) + { + SourceDirectories = new ITaskItem[0]; + return true; + } + + if (DestinationDirectories == null && DestinationFolder == null) + { + Log.LogError($"${nameof(SourceDirectories)} needs a {nameof(DestinationDirectories)} or {nameof(DestinationFolder)}"); + return false; + } + + if (DestinationDirectories != null && DestinationDirectories.Length != SourceDirectories.Length) + { + Log.LogErrorWithCodeFromResources("General.TwoVectorsMustHaveSameLength", DestinationDirectories.Length, SourceDirectories.Length, nameof(DestinationDirectories), nameof(SourceDirectories)); + return false; + } + + if (DestinationDirectories == null) + { + DestinationDirectories = new ITaskItem[SourceDirectories.Length]; + + for (int i = 0; i < SourceDirectories.Length; ++i) + { + // Build the correct path. + string destinationDir; + try + { + destinationDir = Path.Combine(DestinationFolder.ItemSpec, Path.GetFileName(SourceDirectories[i].ItemSpec)); + } + catch (ArgumentException e) + { + Log.LogError($"Could not move {SourceDirectories[i].ItemSpec} to {DestinationFolder.ItemSpec}: {e.Message}"); + + // Clear the outputs. + DestinationDirectories = new ITaskItem[0]; + return false; + } + + // Initialize the DestinationFolder item. + DestinationDirectories[i] = new TaskItem(destinationDir); + } + } + + // Build up the sucessfully moved subset + var DestinationDirectoriesSuccessfullyMoved = new List(); + + // Now that we have a list of DestinationFolder files, move from source to DestinationFolder. + for (int i = 0; i < SourceDirectories.Length && !_canceling; ++i) + { + string sourceDir = SourceDirectories[i].ItemSpec; + string destinationDir = DestinationDirectories[i].ItemSpec; + + try + { + if (UseSymlinkOrJunction + ? CreateSymlinkOrJunctionWithLogging(sourceDir, destinationDir) + : MoveDirectoryWithLogging(sourceDir, destinationDir)) + { + SourceDirectories[i].CopyMetadataTo(DestinationDirectories[i]); + DestinationDirectoriesSuccessfullyMoved.Add(DestinationDirectories[i]); + } + else + { + success = false; + } + } + catch (Exception e) when ( + e is UnauthorizedAccessException + || e is NotSupportedException + || (e is ArgumentException && !(e is ArgumentNullException)) + || e is SecurityException + || e is IOException) + { + Log.LogError($"Could not move {sourceDir} to {destinationDir}: {e.Message}"); + success = false; + + // Continue with the rest of the list + } + } + + MovedDirectories = DestinationDirectoriesSuccessfullyMoved.ToArray(); + + return success && !_canceling; + } + + private static void MakeWriteableIfReadOnly(string directory) + { + var info = new DirectoryInfo(directory); + if ((info.Attributes & FileAttributes.ReadOnly) != 0) + { + info.Attributes &= ~FileAttributes.ReadOnly; + } + } + + private bool MoveDirectoryWithLogging( + string sourceDirectory, + string destinationDirectory + ) + { + if (Directory.Exists(destinationDirectory)) + { + Log.LogError($"Destination {destinationDirectory} already exists, could not move {sourceDirectory}"); + return false; + } + + // Check the source exists. + if (!Directory.Exists(sourceDirectory)) + { + Log.LogError($"Source {sourceDirectory} does not exist"); + return false; + } + + if (OverwriteReadOnlyFiles && File.Exists(destinationDirectory)) + { + MakeWriteableIfReadOnly(destinationDirectory); + } + + string destinationFolder = Path.GetDirectoryName(destinationDirectory); + + if (!string.IsNullOrEmpty(destinationFolder) && !Directory.Exists(destinationFolder)) + { + Log.LogMessage(MessageImportance.Normal, $"Creating directory {destinationFolder}"); + Directory.CreateDirectory(destinationFolder); + } + + // Do not log a fake command line as well, as it's superfluous, and also potentially expensive + Log.LogMessage(MessageImportance.Normal, $"Moving directory {sourceDirectory} to {destinationDirectory}"); + + Directory.Move(sourceDirectory, destinationDirectory); + var result = Directory.Exists(destinationDirectory); + + if (!result) + { + // It failed so we need a nice error message. Unfortunately + // Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); and + // throw new IOException((new Win32Exception(error)).Message) + // do not produce great error messages (eg., "The operation succeeded" (!)). + // For this reason the BCL has is own mapping in System.IO.__Error.WinIOError + // which is unfortunately internal. + // So try to get a nice message by using the BCL Move(), which will likely fail + // and throw. Otherwise use the "correct" method. + + Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); + } + + if (Directory.Exists(destinationDirectory)) + { + // Make it writable + MakeWriteableIfReadOnly(destinationDirectory); + } + + return true; + } + + private bool CreateSymlinkOrJunctionWithLogging( + string sourceDirectory, + string destinationDirectory + ) + { + if (Directory.Exists(destinationDirectory)) + { + Log.LogError($"Destination {destinationDirectory} already exists, could not link {sourceDirectory}"); + return false; + } + + // Check the source exists. + if (!Directory.Exists(sourceDirectory)) + { + Log.LogError($"Source {sourceDirectory} does not exist"); + return false; + } + + if (OverwriteReadOnlyFiles && File.Exists(destinationDirectory)) + { + MakeWriteableIfReadOnly(destinationDirectory); + } + + string destinationFolder = Path.GetDirectoryName(destinationDirectory); + + if (!string.IsNullOrEmpty(destinationFolder) && !Directory.Exists(destinationFolder)) + { + Log.LogMessage(MessageImportance.Normal, $"Creating directory {destinationFolder}"); + Directory.CreateDirectory(destinationFolder); + } + + // Do not log a fake command line as well, as it's superfluous, and also potentially expensive + Log.LogMessage(MessageImportance.Normal, $"Linking directory {sourceDirectory} to {destinationDirectory}"); + + CreateSymlinkOrJunction(sourceDirectory, destinationDirectory); + // Directory.Move(sourceDirectory, destinationDirectory); + var result = Directory.Exists(destinationDirectory); + + // if (!result) + // { + // // It failed so we need a nice error message. Unfortunately + // // Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); and + // // throw new IOException((new Win32Exception(error)).Message) + // // do not produce great error messages (eg., "The operation succeeded" (!)). + // // For this reason the BCL has is own mapping in System.IO.__Error.WinIOError + // // which is unfortunately internal. + // // So try to get a nice message by using the BCL Move(), which will likely fail + // // and throw. Otherwise use the "correct" method. + + // Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); + // } + + if (Directory.Exists(destinationDirectory)) + { + // Make it writable + MakeWriteableIfReadOnly(destinationDirectory); + } + + return true; + } + + private void CreateSymlinkOrJunction(string source, string dest) + { + ProcessStartInfo info = new ProcessStartInfo { UseShellExecute = false }; + switch(GetCurrentPlatform(out var osType)) + { + case Platform.Linux: + case Platform.MacOs: + info.FileName = "ln"; + info.Arguments = $"-srd \"{source}\" \"{dest}\""; + break; + case Platform.Windows: + info.FileName = "mklink"; + info.Arguments = $"/J \"{source}\" \"{dest}\""; + break; + default: throw new PlatformNotSupportedException(osType); + } + var process = new Process() { StartInfo = info }; + process.Start(); + process.WaitForExit(); + } + + public enum Platform + { + Unknown, + Windows, + Linux, + MacOs + } + + private static Platform GetCurrentPlatform() => GetCurrentPlatform(out var osType); + + private static Platform GetCurrentPlatform(out string osType) + { + osType = ""; + string windir = Environment.GetEnvironmentVariable("windir"); + if (!string.IsNullOrEmpty(windir) && windir.Contains(@"\") && Directory.Exists(windir)) + { + return Platform.Windows; + } + else if (File.Exists(@"/proc/sys/kernel/ostype")) + { + osType = File.ReadAllText(@"/proc/sys/kernel/ostype"); + if (osType.StartsWith("Linux", StringComparison.OrdinalIgnoreCase)) + { + // Note: Android gets here too + return Platform.Linux; + } + else + { + return Platform.Unknown; + } + } + else if (File.Exists(@"/System/Library/CoreServices/SystemVersion.plist")) + { + // Note: iOS gets here too + return Platform.MacOs; + } + else + { + return Platform.Unknown; + } + } + } +} \ No newline at end of file diff --git a/PathfinderBuildTasks/PathfinderBuildTasks.csproj b/PathfinderBuildTasks/PathfinderBuildTasks.csproj new file mode 100644 index 00000000..f5ccc3e2 --- /dev/null +++ b/PathfinderBuildTasks/PathfinderBuildTasks.csproj @@ -0,0 +1,12 @@ + + + + net48 + tasks + + + + + + + diff --git a/PathfinderBuildTasks/tasks/Microsoft.Build.Framework.dll b/PathfinderBuildTasks/tasks/Microsoft.Build.Framework.dll new file mode 100644 index 00000000..709e75a9 Binary files /dev/null and b/PathfinderBuildTasks/tasks/Microsoft.Build.Framework.dll differ diff --git a/PathfinderBuildTasks/tasks/Microsoft.Build.Utilities.Core.dll b/PathfinderBuildTasks/tasks/Microsoft.Build.Utilities.Core.dll new file mode 100644 index 00000000..e9fd61b5 Binary files /dev/null and b/PathfinderBuildTasks/tasks/Microsoft.Build.Utilities.Core.dll differ diff --git a/PathfinderBuildTasks/tasks/PathfinderBuildTasks.dll b/PathfinderBuildTasks/tasks/PathfinderBuildTasks.dll new file mode 100644 index 00000000..851fc79f Binary files /dev/null and b/PathfinderBuildTasks/tasks/PathfinderBuildTasks.dll differ diff --git a/PathfinderPatcher/PathfinderPatcher.csproj b/PathfinderPatcher/PathfinderPatcher.csproj index 26f50447..c08c88c5 100644 --- a/PathfinderPatcher/PathfinderPatcher.csproj +++ b/PathfinderPatcher/PathfinderPatcher.csproj @@ -1,10 +1,11 @@ - + Exe + diff --git a/PathfinderUpdater/PathfinderUpdater.csproj b/PathfinderUpdater/PathfinderUpdater.csproj index 613cd767..026fbce6 100644 --- a/PathfinderUpdater/PathfinderUpdater.csproj +++ b/PathfinderUpdater/PathfinderUpdater.csproj @@ -1,40 +1,56 @@ - + - + - - - - - - $(LibsDir)HacknetPathfinder.exe - False - - - - - + + + + + + $(LibsDir)HacknetPathfinder.exe + False + + + + + - - - - - {64faeda5-e87c-47ed-8200-e1de1f263040} - BepInEx.Hacknet - False - - - {4de0a4cf-ec60-46e1-ad96-be3a0f5be406} - PathfinderAPI - False - - - + + + + + {64faeda5-e87c-47ed-8200-e1de1f263040} + BepInEx.Hacknet + False + + + {4de0a4cf-ec60-46e1-ad96-be3a0f5be406} + PathfinderAPI + False + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 9dcd2849..77360e0c 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,11 @@ Install the mod by placing it in Hacknet/BepInEx/plugins or a folder called Plug ### Testing contiburitons -1. Copy libs/HacknetPathfinder.exe over to the Hacknet directory -2. Everything in BepInEx.Hacknet/bin/Debug goes into Hacknet/BepInEx/core. -3. Copy/symlink the .dll (or their containing folder) for PathfinderAPI and optionally ExampleMod to Hacknet/BepInEx/plugins +Just run `dotnet build /target::RunGame` and it automatically prepares and executes the game for that project. It also cleans up after itself and preserves your regular game files. Applicable projects are BepInEx.Hacknet, PathfinderAPI, and PathfinderUpdater + +* Do note that if any project name contains `%`, `$`, `@`, `;`, `.`, `(`, `)`, or `'` it must be replaced with `_` (eg: `BepInEx.Hacknet` becomes `BepInEx_Hacknet`) + +* Additional plugins or config modifications will be found in `Hacknet-Pathfinder/.debug` which is treated as `Hacknet/BepInEx` when RunGame is executed. ## Links