diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..21c1fdf --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc +nupkg/ + +# Visual Studio Code +.vscode + +# Rider +.idea + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +msbuild.log +msbuild.err +msbuild.wrn + +# Visual Studio 2015 +.vs/ + +# Project-specific +FodyWeavers.xsd +Costura32/xlpack.dll +Costura32/tbb.dll diff --git a/Costura32/.gitkeep b/Costura32/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/FileUtil.cs b/FileUtil.cs new file mode 100644 index 0000000..7992b9d --- /dev/null +++ b/FileUtil.cs @@ -0,0 +1,21 @@ +using System.IO; + +public class FileUtil { + // https://stackoverflow.com/a/937558 + public static bool IsFileLocked(string path) { + try { + using (FileStream stream = new FileInfo(path).Open(FileMode.Open, FileAccess.Read, FileShare.None)) { + stream.Close(); + } + } catch (IOException) { + // the file is unavailable because it is: + // still being written to + // or being processed by another thread + // or does not exist (has already been processed) + return true; + } + + // file is not locked + return false; + } +} diff --git a/FodyWeavers.xml b/FodyWeavers.xml new file mode 100644 index 0000000..f00ec6d --- /dev/null +++ b/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..682d3b5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Indrek Ardel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..3749aa1 --- /dev/null +++ b/Program.cs @@ -0,0 +1,87 @@ +if (args.Length < 2) { + Console.Error.WriteLine($"Usage: "); + Environment.Exit(0); +} + +string gameFolderStr = args[0]; +string patchPakStr = args[1]; + +string gameFolder = Path.GetFullPath(gameFolderStr); +string gamePakStr = Path.Join(gameFolder, "game_pak"); +string gamePak = Path.GetFullPath(gamePakStr); +string patchPak = Path.GetFullPath(patchPakStr); + +if (!Directory.Exists(gameFolder)) { + Console.Error.WriteLine($"game directory '{gameFolder}' does not exist."); + Environment.Exit(1); +} + +if (!File.Exists(gamePak)) { + Console.Error.WriteLine($"game pak not found at path '{gamePak}'"); + Environment.Exit(1); +} + +if (!File.Exists(patchPak)) { + Console.Error.WriteLine($"patch pak not found at path '{patchPak}'"); + Environment.Exit(1); +} + +if (FileUtil.IsFileLocked(gamePak)) { + Console.Error.WriteLine($"game pak '{gamePak}' is being used by another process."); + Environment.Exit(1); +} + +Action> step = (text, func) => +{ + Console.Write($"{text}... "); + bool result = func(); + Console.WriteLine(result ? "SUCCESS" : "FAILURE"); + + if (!result) { + Environment.Exit(1); + } +}; + +Dictionary handles = new Dictionary(); + +step("Creating temporary file system", () => XLPack.CreateFileSystem()); + +step($"Mounting '{gamePak}' to /master", () => { + IntPtr handle = XLPack.Mount("/master", gamePak, true); + + handles["master"] = handle; + + return handle != IntPtr.Zero; +}); + +step($"Applying '{patchPak} to /master'", () => XLPack.ApplyPatchPak("/master", patchPak)); + +step("Unmounting '/master'", () => XLPack.Unmount(handles["master"])); + +step($"Mounting '{gamePak}' to /src", () => { + IntPtr handle = XLPack.Mount("/src", gamePak, true); + + handles["src"] = handle; + + return handle != IntPtr.Zero; +}); + +step($"Mounting '{gameFolder}' to /dst", () => { + IntPtr handle = XLPack.Mount("/dst", gameFolder + @"\", true); + + handles["dst"] = handle; + + return handle != IntPtr.Zero; +}); + +step($"Copying directory 'src/bin32' to 'dst/bin32'", () => XLPack.CopyDir("src/bin32", "dst/bin32")); +step($"Copying directory 'src/bin64' to 'dst/bin64'", () => XLPack.CopyDir("src/bin64", "dst/bin64")); +step($"Copying directory 'src/easyanticheat' to 'dst/easyanticheat'", () => XLPack.CopyDir("src/easyanticheat", "dst/easyanticheat")); +step($"Copying file 'src/launch_game.exe' to 'dst/launch_game.exe'", () => XLPack.Copy("src/launch_game.exe", "dst/launch_game.exe")); + +step("Unmounting '/src'", () => XLPack.Unmount(handles["src"])); +step("Unmounting '/dst'", () => XLPack.Unmount(handles["dst"])); +step("Destroying temporary file system", () => { + XLPack.DestroyFileSystem(); + return true; +}); diff --git a/README.md b/README.md new file mode 100644 index 0000000..fb6daff --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# ArcheAge patcher + +Application for applying a patch PAK to an existing ArcheAge installation. + +The patching procedure is identical to what [KakaoGames ArcheAge Launcher](https://archeage.playkakaogames.com/download) performs. + +## Basic usage + +Download the pre-built binary from [releases page](https://github.com/Ingramz/aapatcher/releases). + +Then run from command line: +``` +aapatcher.exe C:\KakaoGames\ArcheAge C:\path\to\a\patch.pak +``` + +Output: +``` +Creating temporary file system... SUCCESS +Mounting 'C:\KakaoGames\ArcheAge\game_pak' to /master... SUCCESS +Applying 'C:\path\to\a\patch.pak to /master'... SUCCESS +Unmounting '/master'... SUCCESS +Mounting 'C:\KakaoGames\ArcheAge\game_pak' to /src... SUCCESS +Mounting 'C:\KakaoGames\ArcheAge' to /dst... SUCCESS +Copying directory 'src/bin32' to 'dst/bin32'... SUCCESS +Copying directory 'src/bin64' to 'dst/bin64'... SUCCESS +Copying directory 'src/easyanticheat' to 'dst/easyanticheat'... SUCCESS +Copying file 'src/launch_game.exe' to 'dst/launch_game.exe'... SUCCESS +Unmounting '/src'... SUCCESS +Unmounting '/dst'... SUCCESS +Destroying temporary file system... SUCCESS +``` + +## Building the project + +Install [.NET SDK](https://dotnet.microsoft.com/en-us/download). + +Copy `xlpack.dll` and `tbb.dll` from an installed [ArcheAge Launcher](https://archeage.playkakaogames.com/download) to `Costura32` directory. + +Create the release binary: +``` +dotnet publish -c Release +``` + +## Acknowledgements + +[XlPakTool](https://github.com/nikes/XlPakTool) for `xlpack.dll` DLL imports. diff --git a/XLPack.cs b/XLPack.cs new file mode 100644 index 0000000..cac1ddd --- /dev/null +++ b/XLPack.cs @@ -0,0 +1,81 @@ +using System; +using System.Runtime.InteropServices; + +public class XLPack { + [DllImport("xlpack.dll", EntryPoint = "?ApplyPatchPak@@YA_NPBD0@Z", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern bool ApplyPatchPak([MarshalAs(UnmanagedType.LPStr)] string to, [MarshalAs(UnmanagedType.LPStr)] string pathToPak); + + [DllImport("xlpack.dll", EntryPoint = "?Copy@@YA_NPBD0@Z", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern bool Copy([MarshalAs(UnmanagedType.LPStr)] string from, [MarshalAs(UnmanagedType.LPStr)] string to); + + [DllImport("xlpack.dll", EntryPoint = "?CopyDir@@YA_NPBD0@Z", CharSet = CharSet.Ansi)] + public static extern bool CopyDir([MarshalAs(UnmanagedType.LPStr)] string from, [MarshalAs(UnmanagedType.LPStr)] string to); + + //?CopyListFile@@YA_NPBD00@Z + + [DllImport("xlpack.dll", EntryPoint = "?CreateFileSystem@@YA_NXZ", CallingConvention = CallingConvention.Cdecl)] + public static extern bool CreateFileSystem(); + + //?CreatePak@@YA_NPBD_N@Z + + //?DeleteDir@@YA_NPBD@Z + + //?DeleteList@@YA_NPBD0@Z + + [DllImport("xlpack.dll", EntryPoint = "?DestroyFileSystem@@YAXXZ", CallingConvention = CallingConvention.Cdecl)] + public static extern void DestroyFileSystem(); + + //?FClose@@YAXAAPAUFile@@@Z + + //?FDelete@@YA_NPBD@Z + + //?FEof@@YAHPAUFile@@@Z + + //?FFlush@@YAHPAUFile@@@Z + + //?FGetStat@@YA_NPAUFile@@PAUpack_stat_t@@@Z + + //?FOpen@@YAPAUFile@@PBD0@Z + + //?FRead@@YA_JPAUFile@@PAD_J@Z + + //?FReadAll@@YA_JPAUFile@@PAPADAA_J@Z + + //?FReserveSize@@YA_NPAUFile@@_J@Z + + //?FSeek@@YAHAAPAUFile@@_JH@Z + + //?FSetStat@@YA_NPAUFile@@PBUpack_stat_t@@@Z + + //?FSize@@YA_JPAUFile@@@Z + + //?FTell@@YA_JPAUFile@@@Z + + //?FWrite@@YA_JPAUFile@@PBD_J@Z + + ///?GetFileArchivePath@@YAPBDPAUFile@@@Z + + ///?GetModificationTime@@YA_JPAUFile@@@Z + + //?GetPakPath@@YAPBDPAUFile@@@Z + + //?Getc@@YAHPAUFile@@@Z + + //?IsFileExist@@YA_NPBD@Z + + //?IsInPak@@YA_NPAUFile@@@Z + + //?MergePatchDeletedList@@YAXPBD0@Z + + [DllImport("xlpack.dll", EntryPoint = "?Mount@@YAPAXPBD0_N@Z", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr Mount([MarshalAs(UnmanagedType.LPStr)] string where, [MarshalAs(UnmanagedType.LPStr)] string which, [MarshalAs(UnmanagedType.Bool)] bool editable); + + //?ReadableLength@@YA_JPAUFile@@@Z + + //?Ungetc@@YAHHPAUFile@@@Z + + [DllImport("xlpack.dll", EntryPoint = "?Unmount@@YA_NPAX@Z", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern bool Unmount(IntPtr handle); + + //?Unmount@@YA_NPBD@Z +} diff --git a/aapatcher.csproj b/aapatcher.csproj new file mode 100644 index 0000000..1860714 --- /dev/null +++ b/aapatcher.csproj @@ -0,0 +1,30 @@ + + + + Exe + net6.0 + enable + enable + true + true + win-x86 + true + true + true + true + + + + + + + + + + all + + + all + + +