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
+
+
+