diff --git a/src/Prospect.Agent/Prospect.Agent.vcxproj b/src/Prospect.Agent/Prospect.Agent.vcxproj
new file mode 100644
index 0000000..d930485
--- /dev/null
+++ b/src/Prospect.Agent/Prospect.Agent.vcxproj
@@ -0,0 +1,119 @@
+
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ 16.0
+ Win32Proj
+ {a9ba7d25-f239-4320-a0dc-85e8001fd669}
+ ProspectAgent
+ 10.0
+
+
+
+ DynamicLibrary
+ true
+ v143
+ Unicode
+
+
+ DynamicLibrary
+ false
+ v143
+ true
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ $(ProjectDir)bin\$(Platform)\$(Configuration)\
+ $(ProjectDir)bin\intermediate\$(Platform)\$(Configuration)\
+ $(LibraryPath)
+
+
+ false
+ $(ProjectDir)bin\$(Platform)\$(Configuration)\
+ $(ProjectDir)bin\intermediate\$(Platform)\$(Configuration)\
+ $(LibraryPath)
+
+
+ true
+
+
+ true
+
+
+ true
+
+
+
+ Level3
+ true
+ _DEBUG;PROSPECTAGENT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+ NotUsing
+ pch.h
+ %(AdditionalIncludeDirectories)
+
+
+ Windows
+ true
+ false
+ %(AdditionalDependencies)
+
+
+
+
+ Level3
+ true
+ true
+ true
+ NDEBUG;PROSPECTAGENT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+ NotUsing
+ pch.h
+ %(AdditionalIncludeDirectories)
+ MinSpace
+
+
+ Windows
+ true
+ true
+ true
+ false
+ %(AdditionalDependencies)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Prospect.Agent/Prospect.Agent.vcxproj.filters b/src/Prospect.Agent/Prospect.Agent.vcxproj.filters
new file mode 100644
index 0000000..4f007b6
--- /dev/null
+++ b/src/Prospect.Agent/Prospect.Agent.vcxproj.filters
@@ -0,0 +1,39 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;h++;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
+
+
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+
\ No newline at end of file
diff --git a/src/Prospect.Agent/SDK.h b/src/Prospect.Agent/SDK.h
new file mode 100644
index 0000000..8b897f6
--- /dev/null
+++ b/src/Prospect.Agent/SDK.h
@@ -0,0 +1,105 @@
+#pragma once
+
+#include
+#include
+#include
+#include "SDK_Memory.h"
+
+namespace SDK
+{
+ template
+ struct TArray
+ {
+ friend struct FString;
+
+ TArray()
+ {
+ Data = nullptr;
+ Count = Max = 0;
+ }
+
+ int Num() const
+ {
+ return Count;
+ }
+
+ T& operator[](int i)
+ {
+ return Data[i];
+ }
+
+ const T& operator[](int i) const
+ {
+ return Data[i];
+ }
+
+ bool IsValidIndex(int i) const
+ {
+ return i < Num();
+ }
+
+ private:
+ T* Data;
+ int32_t Count;
+ int32_t Max;
+ };
+
+ struct FString : private TArray
+ {
+ FString(const wchar_t* other)
+ {
+ const size_t charLen = std::wcslen(other);
+
+ if (charLen == 0)
+ {
+ Count = 0;
+ Max = 0;
+ Data = nullptr;
+ }
+ else
+ {
+ Count = charLen + 1;
+ Max = Count;
+ Data = static_cast((*GMalloc)->Malloc(Count * 2));
+
+ memset(Data, 0, Count * 2);
+ memcpy(Data, other, charLen * 2);
+ }
+ }
+
+ FString(const std::wstring& other) : FString(other.c_str())
+ {
+
+ }
+
+ bool IsValid() const
+ {
+ return Data != nullptr;
+ }
+
+ const wchar_t* c_str() const
+ {
+ return Data;
+ }
+
+ std::string ToString() const
+ {
+ const auto length = std::wcslen(Data);
+
+ std::string str(length, '\0');
+ std::use_facet>(std::locale()).narrow(Data, Data + length, '?', &str[0]);
+
+ return str;
+ }
+ };
+
+ struct UPlayFabAPISettings
+ {
+ FString VerticalName;
+ FString BaseServiceHost;
+ FString TitleId;
+ FString AdvertisingIdType;
+ FString AdvertisingIdValue;
+ bool DisableAdvertising = false;
+ };
+}
diff --git a/src/Prospect.Agent/SDK_Memory.h b/src/Prospect.Agent/SDK_Memory.h
new file mode 100644
index 0000000..8927b14
--- /dev/null
+++ b/src/Prospect.Agent/SDK_Memory.h
@@ -0,0 +1,79 @@
+#pragma once
+
+#include
+
+#define DEFAULT_ALIGNMENT 0
+
+namespace SDK
+{
+ class FExec
+ {
+ public:
+ virtual ~FExec() = default;
+ private:
+ virtual bool Exec(void* world, void* cmd, void* ar) = 0;
+ };
+
+ class FMalloc : FExec
+ {
+ public:
+ virtual void* Malloc(SIZE_T Count, uint32_t Alignment = DEFAULT_ALIGNMENT) = 0;
+
+ virtual void* TryMalloc(SIZE_T Count, uint32_t Alignment = DEFAULT_ALIGNMENT) = 0;
+
+ virtual void* Realloc(void* Original, SIZE_T Count, uint32_t Alignment = DEFAULT_ALIGNMENT) = 0;
+
+ virtual void* TryRealloc(void* Original, SIZE_T Count, uint32_t Alignment = DEFAULT_ALIGNMENT) = 0;
+
+ virtual void Free(void* Original) = 0;
+
+ virtual SIZE_T QuantizeSize(SIZE_T Count, uint32_t Alignment)
+ {
+ return Count;
+ }
+
+ virtual bool GetAllocationSize(void* Original, SIZE_T& SizeOut)
+ {
+ return false;
+ }
+
+ virtual void Trim(bool bTrimThreadCaches)
+ {
+ }
+
+ virtual void SetupTLSCachesOnCurrentThread()
+ {
+ }
+
+ virtual void ClearAndDisableTLSCachesOnCurrentThread()
+ {
+ }
+
+ virtual void InitializeStatsMetadata();
+
+ virtual bool Exec(void* InWorld, void* Cmd, void* Ar) override
+ {
+ return false;
+ }
+
+ virtual void UpdateStats() = 0;
+
+ virtual void GetAllocatorStats(void* out_Stats) = 0;
+
+ virtual void DumpAllocatorStats(class FOutputDevice& Ar) = 0;
+
+ virtual bool IsInternallyThreadSafe() const
+ {
+ return false;
+ }
+
+ virtual bool ValidateHeap()
+ {
+ return(true);
+ }
+
+ virtual const TCHAR* GetDescriptiveName() = 0;
+ };
+
+ FMalloc** GMalloc;
+}
diff --git a/src/Prospect.Agent/hooks.h b/src/Prospect.Agent/hooks.h
new file mode 100644
index 0000000..4e2a47a
--- /dev/null
+++ b/src/Prospect.Agent/hooks.h
@@ -0,0 +1,3 @@
+#pragma once
+
+#include "SDK.h"
\ No newline at end of file
diff --git a/src/Prospect.Agent/logger.cpp b/src/Prospect.Agent/logger.cpp
new file mode 100644
index 0000000..24eed78
--- /dev/null
+++ b/src/Prospect.Agent/logger.cpp
@@ -0,0 +1,85 @@
+#include "logger.h"
+
+#include
+#include
+
+HANDLE h_out = nullptr, h_old_out = nullptr;
+HANDLE h_err = nullptr, h_old_err = nullptr;
+HANDLE h_in = nullptr, h_old_in = nullptr;
+
+namespace logger
+{
+ void Attach()
+ {
+ h_old_out = GetStdHandle(STD_OUTPUT_HANDLE);
+ h_old_err = GetStdHandle(STD_ERROR_HANDLE);
+ h_old_in = GetStdHandle(STD_INPUT_HANDLE);
+
+ AllocConsole() && AttachConsole(GetCurrentProcessId());
+
+ h_out = GetStdHandle(STD_OUTPUT_HANDLE);
+ h_err = GetStdHandle(STD_ERROR_HANDLE);
+ h_in = GetStdHandle(STD_INPUT_HANDLE);
+
+ SetConsoleMode(h_out,
+ ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT);
+
+ SetConsoleMode(h_in,
+ ENABLE_INSERT_MODE | ENABLE_EXTENDED_FLAGS |
+ ENABLE_PROCESSED_INPUT | ENABLE_QUICK_EDIT_MODE);
+ }
+
+ void Detach()
+ {
+ if (h_out && h_err && h_in) {
+ FreeConsole();
+
+ if (h_old_out)
+ {
+ SetStdHandle(STD_OUTPUT_HANDLE, h_old_out);
+ }
+
+ if (h_old_err)
+ {
+ SetStdHandle(STD_ERROR_HANDLE, h_old_err);
+ }
+
+ if (h_old_in)
+ {
+ SetStdHandle(STD_INPUT_HANDLE, h_old_in);
+ }
+ }
+ }
+
+ bool Print(const char* fmt, ...)
+ {
+ if (!h_out)
+ {
+ return false;
+ }
+
+ char buf[1024];
+ va_list va;
+
+ va_start(va, fmt);
+ _vsnprintf_s(buf, 1024, fmt, va);
+ va_end(va);
+
+ return !!WriteConsoleA(h_out, buf, static_cast(strlen(buf)), nullptr, nullptr);
+ }
+
+ char ReadKey()
+ {
+ if (!h_in)
+ {
+ return -1;
+ }
+
+ auto key = char{ 0 };
+ auto keysread = DWORD{ 0 };
+
+ ReadConsoleA(h_in, &key, 1, &keysread, nullptr);
+
+ return key;
+ }
+};
diff --git a/src/Prospect.Agent/logger.h b/src/Prospect.Agent/logger.h
new file mode 100644
index 0000000..9317173
--- /dev/null
+++ b/src/Prospect.Agent/logger.h
@@ -0,0 +1,12 @@
+#pragma once
+
+namespace logger
+{
+ void Attach();
+
+ void Detach();
+
+ bool Print(const char* fmt, ...);
+
+ char ReadKey();
+}
\ No newline at end of file
diff --git a/src/Prospect.Agent/main.cpp b/src/Prospect.Agent/main.cpp
new file mode 100644
index 0000000..687c6da
--- /dev/null
+++ b/src/Prospect.Agent/main.cpp
@@ -0,0 +1,135 @@
+#define WIN32_LEAN_AND_MEAN
+#include
+#include
+#include
+#include "logger.h"
+#include "SDK.h"
+#include "SDK_Memory.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+std::wstring g_backendUrl;
+
+// trim from start (in place)
+inline void ltrim(std::wstring &s) {
+ s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
+ return !std::isspace(ch);
+ }));
+}
+
+// trim from end (in place)
+inline void rtrim(std::wstring &s) {
+ s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
+ return !std::isspace(ch);
+ }).base(), s.end());
+}
+
+inline void trim(std::wstring &s) {
+ rtrim(s);
+ ltrim(s);
+}
+
+constexpr uintptr_t OffsetGMalloc = 0x63EC4A0;
+
+/**
+ * Function offset, find with "?sdk=".
+ */
+constexpr uintptr_t OffsetPlayFabApiGetUrl = 0xCEEF00;
+
+/**
+ * Function hooks.
+ */
+typedef SDK::FString(__fastcall* tPlayFabApiGetUrl)(SDK::UPlayFabAPISettings*, const SDK::FString&);
+
+tPlayFabApiGetUrl OldPlayFabApiGetUrl;
+
+SDK::FString __fastcall PlayFabApiGetUrlProxy(SDK::UPlayFabAPISettings* thiz, const SDK::FString& callPath) {
+ logger::Print("[Agent] GetURL called with callPath %s\n", callPath.ToString().c_str());
+
+ if (!g_backendUrl.empty())
+ {
+ return g_backendUrl + std::wstring(callPath.c_str());
+ }
+
+ return std::wstring(L"https://127.0.0.1:8443") + std::wstring(callPath.c_str());
+}
+
+DWORD WINAPI OnDllAttach(LPVOID base) {
+ logger::Print("[Agent] DLL Attached\n");
+
+ const auto hModule = GetModuleHandleW(nullptr);
+ const auto hModulePtr = reinterpret_cast(hModule);
+
+ // Initialize GMalloc.
+ SDK::GMalloc = reinterpret_cast(hModulePtr + OffsetGMalloc);
+
+ // Initialize MinHook.
+ if (MH_Initialize() != MH_OK) {
+ logger::Print("[Agent] Failed to initialize MinHook\n");
+ FreeLibraryAndExitThread(hModule, 1);
+ }
+
+ // Hook UPlayFabAPISettings::GetUrl.
+ const auto pTarget = reinterpret_cast(hModulePtr + OffsetPlayFabApiGetUrl);
+ const auto ppOriginal = reinterpret_cast(&OldPlayFabApiGetUrl);
+ const auto mhStatus = MH_CreateHook(pTarget, &PlayFabApiGetUrlProxy, ppOriginal);
+
+ if (mhStatus != MH_OK) {
+ logger::Print("[Agent] Failed to create PlayFabAPIGetUrl hook (%d)\n", mhStatus);
+ FreeLibraryAndExitThread(hModule, 1);
+ }
+
+ if (MH_EnableHook(pTarget) != MH_OK) {
+ logger::Print("[Agent] Failed to enable PlayFabAPIGetUrl hook\n");
+ FreeLibraryAndExitThread(hModule, 1);
+ }
+
+ const wchar_t* filePath = L"backend.txt";
+ std::wifstream f(filePath);
+ if (f) {
+ std::wstringstream buffer;
+ buffer << f.rdbuf();
+ g_backendUrl = buffer.str();
+ trim(g_backendUrl);
+ if (g_backendUrl.rfind(L"https://", 0) != 0) {
+ logger::Print("[Agent] URL must start with https://\n");
+ g_backendUrl.clear();
+ } else {
+ logger::Print("[Agent] Loaded custom URL: %S\n", g_backendUrl.c_str());
+ }
+ } else {
+ logger::Print("[Agent] Failed to open file: %S\n", filePath);
+ }
+
+ logger::Print("[Agent] DLL Initialized\n");
+ ExitThread(1);
+}
+
+BOOL WINAPI DllMain(
+ const _In_ HINSTANCE hinstDll,
+ const _In_ DWORD fdwReason,
+ const _In_opt_ LPVOID lpvReserved
+) {
+ if (fdwReason == DLL_PROCESS_ATTACH) {
+ DisableThreadLibraryCalls(hinstDll);
+
+ // Allocate a console.
+ AllocConsole();
+ freopen_s(reinterpret_cast(stdout), "CONOUT$", "w", stdout);
+
+ // Attach logger.
+ logger::Attach();
+
+ // Spawn thread.
+ CreateThread(nullptr, 0, OnDllAttach, hinstDll, 0, nullptr);
+ } else if (fdwReason == DLL_PROCESS_DETACH) {
+ // Detach logger.
+ logger::Detach();
+ }
+
+ return TRUE;
+}
diff --git a/src/Prospect.Agent/vcpkg-configuration.json b/src/Prospect.Agent/vcpkg-configuration.json
new file mode 100644
index 0000000..ba06dad
--- /dev/null
+++ b/src/Prospect.Agent/vcpkg-configuration.json
@@ -0,0 +1,14 @@
+{
+ "default-registry": {
+ "kind": "git",
+ "baseline": "2960d7d80e8d09c84ae8abf15c12196c2ca7d39a",
+ "repository": "https://github.com/microsoft/vcpkg"
+ },
+ "registries": [
+ {
+ "kind": "artifact",
+ "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip",
+ "name": "microsoft"
+ }
+ ]
+}
diff --git a/src/Prospect.Agent/vcpkg.json b/src/Prospect.Agent/vcpkg.json
new file mode 100644
index 0000000..53b61fa
--- /dev/null
+++ b/src/Prospect.Agent/vcpkg.json
@@ -0,0 +1,5 @@
+{
+ "dependencies": [
+ "minhook"
+ ]
+}
diff --git a/src/Prospect.sln b/src/Prospect.sln
index ef9f299..daef9cf 100644
--- a/src/Prospect.sln
+++ b/src/Prospect.sln
@@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Unreal.Generator",
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Prospect.Client.Loader", "Prospect.Client.Loader\Prospect.Client.Loader.vcxproj", "{04B1D963-2BCB-4887-9D6D-04CB3366A348}"
EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Prospect.Agent", "Prospect.Agent\Prospect.Agent.vcxproj", "{A9BA7D25-F239-4320-A0DC-85E8001FD669}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@@ -207,6 +209,30 @@ Global
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Release|x64.Build.0 = Release|x64
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Release|x86.ActiveCfg = Release|Win32
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Release|x86.Build.0 = Release|Win32
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Debug|x64.ActiveCfg = Debug|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Debug|x64.Build.0 = Debug|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Debug|x86.ActiveCfg = Debug|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Debug|x86.Build.0 = Debug|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Release|x64.ActiveCfg = Release|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Release|x64.Build.0 = Release|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Release|x86.ActiveCfg = Release|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Release|x86.Build.0 = Release|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Debug|x64.ActiveCfg = Debug|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Debug|x64.Build.0 = Debug|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Debug|x86.ActiveCfg = Debug|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Debug|x86.Build.0 = Debug|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Release|x64.ActiveCfg = Release|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Release|x64.Build.0 = Release|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Release|x86.ActiveCfg = Release|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Release|x86.Build.0 = Release|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Debug|x64.ActiveCfg = Debug|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Debug|x64.Build.0 = Debug|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Debug|x86.ActiveCfg = Debug|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Debug|x86.Build.0 = Debug|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Release|x64.ActiveCfg = Release|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Release|x64.Build.0 = Release|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Release|x86.ActiveCfg = Release|x64
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Release|x86.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -219,6 +245,7 @@ Global
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34} = {168B6247-2D33-4942-AB3F-C5041B3BDFEA}
{71E3E262-49C5-4B6C-9670-D0741CEDB63B} = {76364BAA-BC9D-4A92-AA2A-8914FAE22834}
{04B1D963-2BCB-4887-9D6D-04CB3366A348} = {31D3D5D2-B011-4F9B-B6CC-D0A35675D797}
+ {A9BA7D25-F239-4320-A0DC-85E8001FD669} = {31D3D5D2-B011-4F9B-B6CC-D0A35675D797}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {12888C62-D3A2-4DD7-A9A5-1481D95865D0}