diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0374fe1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+empires/
+/obj
+/.vs
+src/.vs
+*.zip
\ No newline at end of file
diff --git a/README.md b/README.md
index 8f586bb..7029366 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,24 @@
# empires-assert-patcher
+## Usage
+* extract the latest release to your `Empires Dedicated Server` folder
+* edit `run_patched_server.bat` with your favourite text editor
+* make necessary changes to `SET "ARGS=-console -toconsole -insecure +map emp_arid +sv_lan 1 +maxplayers 32 -nobreakpad"`
+* start `run_patched_server.bat` to patch and run the server
+### original run_patched_server
+```
+:: DO NOT MODIFY
+@ECHO OFF
+SETLOCAL
+
+:: MODIFY AS NECESSARY
+SET "ARGS=-console -toconsole -insecure +map emp_arid +sv_lan 1 +maxplayers 32 -nobreakpad"
+
+:: DO NOT MODIFY
+empires\bin\empires-assert-patcher.exe
+IF %ERRORLEVEL% 1 (
+ PAUSE
+ EXIT /B
+)
+start "" srcds.exe -game "empires" %ARGS%
+```
\ No newline at end of file
diff --git a/run_patched_server.bat b/run_patched_server.bat
new file mode 100644
index 0000000..0569ee9
--- /dev/null
+++ b/run_patched_server.bat
@@ -0,0 +1,14 @@
+:: DO NOT MODIFY
+@ECHO OFF
+SETLOCAL
+
+:: MODIFY AS NECESSARY
+SET "ARGS=-console -toconsole -insecure +map emp_arid +sv_lan 1 +maxplayers 32 -nobreakpad"
+
+:: DO NOT MODIFY
+empires\bin\empires-assert-patcher.exe
+IF %ERRORLEVEL% 1 (
+ PAUSE
+ EXIT /B
+)
+start "" srcds.exe -game "empires" %ARGS%
\ No newline at end of file
diff --git a/src/empires-assert-patcher.sln b/src/empires-assert-patcher.sln
new file mode 100644
index 0000000..1f4be77
--- /dev/null
+++ b/src/empires-assert-patcher.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29709.97
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "empires-assert-patcher", "empires-assert-patcher.vcxproj", "{9E17EDBC-F74F-452C-9D53-1047CB0FEBC5}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {9E17EDBC-F74F-452C-9D53-1047CB0FEBC5}.Debug|x64.ActiveCfg = Debug|x64
+ {9E17EDBC-F74F-452C-9D53-1047CB0FEBC5}.Debug|x64.Build.0 = Debug|x64
+ {9E17EDBC-F74F-452C-9D53-1047CB0FEBC5}.Debug|x86.ActiveCfg = Debug|Win32
+ {9E17EDBC-F74F-452C-9D53-1047CB0FEBC5}.Debug|x86.Build.0 = Debug|Win32
+ {9E17EDBC-F74F-452C-9D53-1047CB0FEBC5}.Release|x64.ActiveCfg = Release|x64
+ {9E17EDBC-F74F-452C-9D53-1047CB0FEBC5}.Release|x64.Build.0 = Release|x64
+ {9E17EDBC-F74F-452C-9D53-1047CB0FEBC5}.Release|x86.ActiveCfg = Release|Win32
+ {9E17EDBC-F74F-452C-9D53-1047CB0FEBC5}.Release|x86.Build.0 = Release|Win32
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {34AC155B-66F3-4F18-9C85-36D2D0B870CF}
+ EndGlobalSection
+EndGlobal
diff --git a/src/empires-assert-patcher.vcxproj b/src/empires-assert-patcher.vcxproj
new file mode 100644
index 0000000..7c6c693
--- /dev/null
+++ b/src/empires-assert-patcher.vcxproj
@@ -0,0 +1,156 @@
+
+
+
+
+ Debug
+ Win32
+
+
+ Release
+ Win32
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ 16.0
+ {9E17EDBC-F74F-452C-9D53-1047CB0FEBC5}
+ empiresassertpatcher
+ 10.0
+
+
+
+ Application
+ true
+ v142
+ Unicode
+
+
+ Application
+ false
+ v142
+ true
+ Unicode
+
+
+ Application
+ true
+ v142
+ Unicode
+
+
+ Application
+ false
+ v142
+ true
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+ true
+ $(SolutionDir)..\empires\bin\
+ $(SolutionDir)..\obj\$(Configuration)-$(Platform)\
+ $(ProjectName)
+
+
+ false
+
+
+ false
+ $(SolutionDir)..\empires\bin\
+ $(SolutionDir)..\obj\$(Configuration)-$(Platform)\
+ $(ProjectName)
+ false
+
+
+
+ Level3
+ true
+ _DEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ true
+
+
+ Console
+ true
+
+
+
+
+ Level3
+ true
+ _DEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ true
+ stdcpp17
+
+
+ Console
+ true
+
+
+
+
+ Level3
+ true
+ true
+ true
+ NDEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ true
+
+
+ Console
+ true
+ true
+ true
+
+
+
+
+ Level3
+ true
+ true
+ true
+ NDEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ true
+ stdcpp17
+ Speed
+
+
+ Console
+ true
+ true
+ false
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/empires-assert-patcher.vcxproj.filters b/src/empires-assert-patcher.vcxproj.filters
new file mode 100644
index 0000000..a91ad0e
--- /dev/null
+++ b/src/empires-assert-patcher.vcxproj.filters
@@ -0,0 +1,22 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;hm;inl;inc;ipp;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+
+
+ Source Files
+
+
+
\ No newline at end of file
diff --git a/src/empires-assert-patcher.vcxproj.user b/src/empires-assert-patcher.vcxproj.user
new file mode 100644
index 0000000..88a5509
--- /dev/null
+++ b/src/empires-assert-patcher.vcxproj.user
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..860dba3
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,204 @@
+/* empires-assert-patcher created by ryuku@ennui.software - license can be found in included LICENSE file */
+
+#define VC_EXTRALEAN
+#define WIN32_LEAN_AND_MEAN
+
+#include
+#include
+#include
+#include
+
+/*
+ server.dll+30C90 - 68 80915917 - push server.dll+3C9180 { ("i:\git-runner\builds\e2c1ae76\0\empires_code\empires_main\mp\sr") }
+ server.dll+30C95 - 8B F0 - mov esi,eax
+ server.dll+30C97 - FF 15 ECF15817 - call dword ptr [server.dll+3BF1EC] { ->tier0.CallAssertFailedNotifyFunc }
+ server.dll+30C9D - 83 C4 20 - add esp,20 { 32 }
+ server.dll+30CA0 - 85 F6 - test esi,esi
+ server.dll+30CA2 - 75 20 - jne server.dll+30CC4 { patch to EB 20 }
+ server.dll+30CA4 - FF 15 F4F15817 - call dword ptr [server.dll+3BF1F4] { ->tier0.ShouldUseNewAssertDialog }
+ server.dll+30CAA - 84 C0 - test al,al
+ server.dll+30CAC - 74 15 - je server.dll+30CC3
+ server.dll+30CAE - 57 - push edi
+ server.dll+30CAF - 6A 1C - push 1C { 28 }
+ server.dll+30CB1 - 68 80915917 - push server.dll+3C9180 { ("i:\git-runner\builds\e2c1ae76\0\empires_code\empires_main\mp\sr") }
+ server.dll+30CB6 - FF 15 F0F15817 - call dword ptr [server.dll+3BF1F0] { ->tier0.DoNewAssertDialog }
+ server.dll+30CBC - 83 C4 0C - add esp,0C { 12 }
+ server.dll+30CBF - 84 C0 - test al,al
+ server.dll+30CC1 - 74 01 - je server.dll+30CC4
+ server.dll+30CC3 - CC - int 3
+ server.dll+30CC4 - 5F - pop edi
+
+ 83 C4 20 85 F6 75 20 FF 15 F4 F1 58 17 84 C0 74 15
+ 83 C4 20 85 F6 75 20 FF 15 ?? ?? ?? ?? 84 C0 74 15
+ */
+
+// comment for better performance
+#define WORKING_INDICATOR
+
+const std::wstring fileName = L"server.dll";
+const char* mask = "xxxxx?xxx????xxxx";
+const char* aob = "\x83\xC4\x20\x85\xF6\x75\x20\xFF\x15\xF4\xF1\x58\x17\x84\xC0\x74\x15";
+const DWORD aobSize = 17;
+
+int DoPatch(const BYTE* pMapped, const DWORD& offset)
+{
+ *(BYTE*)(pMapped + offset + 5) = '\xEB';
+ wprintf(L"patched: ");
+ for (DWORD i = 0; i < aobSize; i++) wprintf(L"%02x ", (BYTE)*(pMapped + offset + i));
+ return 0;
+}
+
+int lastMatch = 0;
+
+bool VerifyPatch(const BYTE* pMapped, const DWORD& offset, const std::vector& matches)
+{
+ if (matches.size() > lastMatch)
+ {
+ if (*(BYTE*)(pMapped + matches[lastMatch] + 5) == (BYTE)'\xEB')
+ {
+ wprintf(L"existing patch found, nothing to do\n");
+ return true;
+ }
+
+ lastMatch++;
+ }
+
+ return false;
+}
+
+int SearchFailed(const std::vector& matches)
+{
+ if (matches.size() > 0) wprintf(L"only expected a single match, patch failed\n");
+ else wprintf(L"patch not found, patch failed\n");
+ return 1;
+}
+
+namespace fs = std::filesystem;
+void GetExePath(std::wstring& exePath);
+int AOBSearch(const std::wstring filePath);
+
+int main()
+{
+ std::wstring exePath;
+ GetExePath(exePath);
+
+ wprintf(L"%s...", fileName.c_str());
+ if (!fs::exists(exePath + fileName))
+ {
+ wprintf(
+ L"not found\n"
+ "this executable needs to be in the same folder as %s", fileName.c_str()
+ );
+
+ return 1;
+ }
+
+ wprintf(L"found\n");
+ return AOBSearch(exePath + fileName);
+}
+
+void GetExePath(std::wstring& exePath)
+{
+ DWORD bufSize = MAX_PATH;
+ wchar_t* buf = new wchar_t[bufSize];
+
+ bufSize = GetModuleFileNameW(NULL, buf, bufSize);
+ if (bufSize > MAX_PATH && GetLastError() == ERROR_INSUFFICIENT_BUFFER)
+ {
+ delete[] buf;
+ buf = new wchar_t[bufSize];
+ GetModuleFileNameW(NULL, buf, bufSize);
+ }
+
+ exePath = std::wstring(buf);
+ exePath = exePath.substr(0, exePath.find_last_of('\\') + 1);
+ delete[] buf;
+}
+
+int AOBSearch(const std::wstring filePath)
+{
+ int exitcode = 0;
+ HANDLE hFile, hMapped;
+ DWORD dwSize = 0;
+ DWORD offset = 0;
+ BYTE* pMapped;
+ std::vector matches;
+
+ // open file to prepare for mapping
+ if ((hFile = CreateFileW(filePath.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)) == INVALID_HANDLE_VALUE)
+ {
+ wprintf(L"error: failed to open %s\n", filePath.c_str());
+ return 1;
+ }
+
+ // get file size, needed to determine when EOF
+ dwSize = GetFileSize(hFile, NULL);
+
+ // map to memory
+ if ((hMapped = CreateFileMappingW(hFile, NULL, PAGE_READWRITE, 0, 0, NULL)) == NULL)
+ {
+ wprintf(L"error: failed to create file mapping\n");
+ goto CLOSE_FILE;
+ }
+
+ if ((pMapped = (BYTE*)MapViewOfFile(hMapped, FILE_MAP_ALL_ACCESS, 0, 0, 0)) == NULL)
+ {
+ wprintf(L"error: failed to map view of file\n");
+ goto CLOSE_MAPPED;
+ }
+
+#ifndef WORKING_INDICATOR
+ wprintf(L"searching for aob...\n");
+#endif
+
+ // iterate through BYTEs of file
+ for (;offset < dwSize - aobSize; offset++)
+ {
+
+#ifdef WORKING_INDICATOR
+
+ static int dotCount = 0;
+ dotCount = dotCount == 5 ? 0 : dotCount + 1;
+ wprintf(L"\rsearching for aob%.*s%*s", dotCount, L".....", 5 - dotCount, L"");
+ fflush(stdout);
+
+#endif
+
+ // perform BYTE compare to find aob
+ for (DWORD i = 0; i < aobSize; i++)
+ {
+ // if not wildcard and not matching BYTE, stop checking from this offset
+ if (mask[i] != '?' && (BYTE)*(pMapped + offset + i) != (BYTE)*(aob + i)) break;
+
+ // if reached expected size, found a matching aob
+ if (i == aobSize - 1)
+ {
+ matches.push_back(offset);
+ break;
+ }
+ }
+
+ // if patch has been verified, no need to continue
+ if (VerifyPatch(pMapped, offset, matches)) goto EXIT;
+ }
+
+ wprintf(L"%llu matches found\n", matches.size());
+
+ for (const auto& m : matches)
+ {
+ wprintf(L"offset: %x - ", m);
+ for (DWORD i = 0; i < aobSize; i++) wprintf(L"%02x ", (BYTE)*(pMapped + m + i));
+ wprintf(L"\n");
+ }
+
+ exitcode = matches.size() == 1 ? DoPatch(pMapped, matches[0]) : SearchFailed(matches);
+
+EXIT:
+ UnmapViewOfFile(pMapped);
+CLOSE_MAPPED:
+ CloseHandle(hMapped);
+CLOSE_FILE:
+ CloseHandle(hFile);
+
+ return exitcode;
+}
\ No newline at end of file
diff --git a/zip_release.bat b/zip_release.bat
new file mode 100644
index 0000000..25469f5
--- /dev/null
+++ b/zip_release.bat
@@ -0,0 +1,5 @@
+@ECHO OFF
+SETLOCAL
+IF EXIST empires-assert-patcher.zip DEL empires-assert-patcher.zip
+IF EXIST empires-assert-patcher.zip EXIT /B
+7z a empires-assert-patcher.zip empires/bin/empires-assert-patcher.exe run_patched_server.bat LICENSE README.md
\ No newline at end of file