#include <windows.h>

#pragma intrinsic(memcpy)

ULONG_PTR PatchPointer(ULONG_PTR lpAddress, ULONG_PTR ulValue)
{
    DWORD flOldProtect;
    ULONG_PTR ulOldValue;

    VirtualProtect((LPVOID)lpAddress, sizeof(ULONG_PTR), PAGE_EXECUTE_READWRITE, &flOldProtect);
    ulOldValue = *(ULONG_PTR*)lpAddress;
    *(ULONG_PTR*)lpAddress = ulValue;
    VirtualProtect((LPVOID)lpAddress, sizeof(ULONG_PTR), flOldProtect, &flOldProtect);
    FlushInstructionCache(GetCurrentProcess(), (LPVOID)lpAddress, sizeof(ULONG_PTR)); /* not needed */
    return ulOldValue;
}

void PatchCode(ULONG_PTR lpAddress, LPVOID pData, SIZE_T dwSize)
{
    DWORD flOldProtect;

    VirtualProtect((LPVOID)lpAddress, dwSize, PAGE_EXECUTE_READWRITE, &flOldProtect);
    memcpy((LPVOID)lpAddress, pData, dwSize);
    VirtualProtect((LPVOID)lpAddress, dwSize, flOldProtect, &flOldProtect);
    FlushInstructionCache(GetCurrentProcess(), (LPVOID)lpAddress, dwSize); /* not needed */
}

#define RVAToVA(library, address) (((ULONG_PTR)library) + address)

HANDLE hUnityPlayer;
HANDLE hGameAssembly;

typedef ULONG_PTR (__fastcall *CREATE_TRANSPORT_FUNC)(ULONG_PTR request);
ULONG_PTR ulOldTransportMethod = (ULONG_PTR) 0;

typedef HINSTANCE (__fastcall *LOAD_LIBRARYW_FUNC)(LPCWSTR lpLibFileName);
ULONG_PTR ulOldLoadLibraryW = (ULONG_PTR) 0;

/* matches URL start */
#define MATCH_PREFIX 1
/* match any substing */
#define MATCH_ANY 2
/* match whole URL */
#define MATCH_FULL 3

typedef struct _UrlPair {
    LPCSTR szSearch;
    LPCSTR szReplace;
    DWORD dwMode;
} UrlPair;

UrlPair UrlReplaceTable[] =  {//{"https://pls.prd.mz.internal.unity3d.com/api/v1/user-lookup", "file:///milky_user_lookup.json", MATCH_FULL},
                              {"https://pls.prd.mz.internal.unity3d.com/api/v1/user-lookup", "file:///user.json", MATCH_FULL},
                              /* will contain the remote config that has this current version value and current news, all matching the files the binaries for this game */
                              //{"https://remote-config-prd.uca.cloud.unity3d.com/settings", "file:///milky_unity_settings.json", MATCH_FULL},
                              {"https://remote-config-prd.uca.cloud.unity3d.com/settings", "file:///settings.json", MATCH_FULL},
                              //{"analytics.unity3d.com", "file:///dummy_file_analytics", MATCH_ANY}, /* might be better to just give it a file that doesn't exist */
                              {"analytics.unity3d.com", "file:///dummy_an", MATCH_ANY}, /* might be better to just give it a file that doesn't exist */
                              //{"backtrace.io", "file:///dummy_file_backtrace", MATCH_ANY}, /* was was disabled using a different patch, but leaving it here */
                              {"backtrace.io", "file:///dummy_bt", MATCH_ANY}, /* was was disabled using a different patch, but leaving it here */
                              /* rest have not been encountered for this game, but seem to be used for other things: */
                              //{"perf-events.cloud.unity3d.com", "file://dummy_file_perf_ev", MATCH_ANY}, /* crash reports? */
                              {"perf-events.cloud.unity3d.com", "file:///dummy_perf_ev", MATCH_ANY}, /* crash reports? */
                              //{"https://unityads.unity3d.com/", "file://dummy_file_ads", MATCH_PREFIX},
                              {"https://unityads.unity3d.com/", "file:///dummy_ad", MATCH_PREFIX},
                              //{"perf.cloud.unity3d.com", "file://dummy_file_perf", MATCH_ANY}
                              {"perf.cloud.unity3d.com", "file:///dummy_perf", MATCH_ANY}
                              };

/* c++ string hack, get the real pointer for longer strings */
LPSTR GetRealCppString(ULONG_PTR pString)
{
    LPSTR szResult = (LPSTR) pString;
    if (((PBYTE) pString)[0x20] != 1)
       szResult = *(LPSTR*)pString;
    return szResult;
}


ULONG_PTR CreateTransportHook(ULONG_PTR request)
{
    int i;
    LPSTR szUrl = GetRealCppString(request + 0x88);
    int url_count = sizeof(UrlReplaceTable) / sizeof(UrlPair);

    for (i = 0; i < url_count; i++)
    {
        UrlPair* pair = &UrlReplaceTable[i];
        BOOL bMatch = FALSE;
        LPCSTR szSearch = pair->szSearch;
        if (pair->dwMode == MATCH_PREFIX)
        {
            bMatch = !strncmp(szUrl, szSearch, strlen(szSearch));
        }
        else if (pair->dwMode == MATCH_ANY)
        {
            bMatch = !strstr(szUrl, szSearch);
        }
        else if (pair->dwMode == MATCH_FULL)
        {
            bMatch = !strcmp(szUrl, szSearch);
        }
        if (bMatch)
        {
            strcpy(szUrl, pair->szReplace);
            break;
        }
    }

    return ((CREATE_TRANSPORT_FUNC)ulOldTransportMethod)(request);
}

void UninstallLoadLibraryHook();

BYTE szNops[] = "\x90\x90\x90\x90\x90\x90";

/* this hook is trying to catch il2cpp/GameAssembly initialization, it happens around RVA 5B2C45, a LoadLibraryW call, followed by resolving all the exports using GetProcAddrss */
HINSTANCE LoadLibraryWHook(LPCWSTR lpLibFileName)
{
    /* load the library */
    HINSTANCE hLib = ((LOAD_LIBRARYW_FUNC)ulOldLoadLibraryW)(lpLibFileName);
    /* if it's GameAssembly, unhook */
    if (!wcscmp(lpLibFileName, L"GameAssembly.dll"))
    {
        UninstallLoadLibraryHook();
        hGameAssembly = hLib;

        /* patch GameAssembly here, these patches won't be undone on unload */

        /*
           crack the file size check: cmp rdi, 0F4240h, jle short comparison with 0xf4240 (1MB) for filesize of app.exe, makes original or unpacked app.exe fail to load as it's usually half this size
           done in Game.Script.Sequence.AppInitialize.OOMALDLMNFF.OnEnter(..), a startup game script with obfuscated names.
           Without this patch, an exception will be thrown and a Fatal Error (blue screen) will be displayed
        */
        PatchCode(RVAToVA(hGameAssembly, 0x6928e0), szNops, 2);
        /*
           disable backtrace.io by patching its internal Backtrace.Unity.BacktraceClient.Refresh() method,
           doing it even if the CreateTransport hook is more than enough for catching it
           code patch is: mov byte ptr [rcx+28h], 0 + ret, this ensures the Enabled accessor is false at all times and disables Refresh method itself as is called to send telemetrics and crash reports on a periodic interval
        */
        PatchCode(RVAToVA(hGameAssembly, 0x543e50), "\xC6\x41\x28\x00\xC3", 5);
        /*
          Game uses official Unity.RemoteConfig code to load the news and current version, otherwise failing with a Fatal Error (exception thrown) at title screen (after blue screen initialization)
          Even if you do replace the url with a file url, it won't work for two reasons: this code POSTs and file handler refuses to handle anything but GETs (needs a patch), and this code checks if any headers were sent
          even if the actual headers are not checked, a normal HTTP request will send some headers and this ensures that some are sent, so this check has to be patched or the file handler has to be patched to return headers
          for now this will be patched here, but might implement another hook to make sure headers are actually returned
          patching Unity.RemoteConfig.ConfigManagerImpl::ParseResponse headers == null check
          second test r14, r14 je near ptr ... test (first test is body == null)
        */
        PatchCode(RVAToVA(hGameAssembly, 0x222AF26), szNops, 6);

    }
    return hLib;
}


/*
   .text:0000000180A32CC0 CreateTransport@?$UnityWebRequestDefaultBase@VTransportCurl@@@@MEAAPEAVUnityWebRequestTransport@@XZ proc near
   TransportCurl *__fastcall UnityWebRequestDefaultBase<TransportCurl>::CreateTransport(void* request)
   This is the function which decides which transport to use based on the URI protocol (file or http(s))
   UnityWebRequest itself can be hooked, but hooking the il2cpp method table would only catch the calls from GameAssembly.dll,
   missing internal analytics and crash reports from UnityPlayer.dll, which can present a privacy problem for the users,
   and the hook code would be more complicated as an il2cpp gc barrier has to be set up for the arguments passed to it.
   Hooking the C++ constructor or one of the members for UnityWebRequest can work as well, and doesn't require setting a gc barrier.
   This CreateTransport member seems even easier to hook and more convenient than the rest, and it doesn't make a difference as it always
   must get called for any request, the same as the constructor. Neither case allows for a generic hook that works for all Unity games:
   the il2cpp internal call that exposes UnityWebRequest would allow a generic workaround for capturing the web requests,
   but that won't capture internal UnityPlayer.dll analytics, so at the very least one offset per Unity version must be identified for hooking.
   Hook will be done by vtable, avoiding the need for using a hook library, the vtable pointer to that member is at VA 0x18177AF20
*/
#define WEB_REQUEST_CREATE_TRANSPORT_VTABLE 0x177AF20
#define LOAD_LIBRARY_W_IAT 0x163A7F0


void InstallPatches()
{
    /* remote config, analytics and telemetry patches */
    ulOldTransportMethod = PatchPointer(RVAToVA(hUnityPlayer, WEB_REQUEST_CREATE_TRANSPORT_VTABLE), (ULONG_PTR) CreateTransportHook);
    ulOldLoadLibraryW = PatchPointer(RVAToVA(hUnityPlayer, LOAD_LIBRARY_W_IAT), (ULONG_PTR) LoadLibraryWHook);
    /*
       patch
       .text:0000000180A30520 ; public: virtual enum  WebError TransportVFS::DoRequest(struct TransportDoRequestArgsProto<class HeaderHelper, class DownloadHandler, class UploadHandler, class CertificateHandler, class ResponseHelper> const &)
       .text:0000000180A30520 ?DoRequest@TransportVFS@@UEAA?AW4WebError@@AEBU?$TransportDoRequestArgsProto@VHeaderHelper@@VDownloadHandler@@VUploadHandler@@VCertificateHandler@@VResponseHelper@@@@@Z proc near
       to allow the file:/// protocol handler to handle requests other than GET (like POST). It might be worth wrapping this virtual method in a hook as well to make it send dummy headers so that that Unity.RemoteConfig patch in GameAssembly wouldn't be needed,
       but a hook to force the request to always be a GET in the TransportVFS handler might not work as expected as threading/async code is involved and the game script code could potentially read the method and
       find out it's a GET despite having sent a POST or some other method
       .text:0000000180A30554                 cmp     dword ptr [rdx+30h], 0 ; if method is GET
       .text:0000000180A30558                 jz      short loc_180A30564 ; continue, patch this to a jmp
       .text:0000000180A3055A                 mov     eax, 24h ; else, return WebError InvalidMethod = 36, see https://github.com/Unity-Technologies/UnityCsReference/blob/master/Modules/UnityWebRequest/Public/UnityWebRequest.bindings.cs
       .text:0000000180A3055F                 jmp     loc_180A306D8
       this patch won't be undone on dll uninitizliation
    */
    PatchCode(RVAToVA(hUnityPlayer, 0xA30558), "\xEB", 1); /* force the conditional short jump to an unconditional one */
}

void UninstallPatches()
{
    if (ulOldTransportMethod)
    {
        PatchPointer(RVAToVA(hUnityPlayer, WEB_REQUEST_CREATE_TRANSPORT_VTABLE), ulOldTransportMethod);
        ulOldTransportMethod = (ULONG_PTR) 0;
    }
    UninstallLoadLibraryHook();
}

void UninstallLoadLibraryHook()
{
    if (ulOldLoadLibraryW)
    {
        PatchPointer(RVAToVA(hUnityPlayer, LOAD_LIBRARY_W_IAT), ulOldLoadLibraryW);
        ulOldLoadLibraryW = (ULONG_PTR) 0;
    }
}


BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch( fdwReason )
    {
        case DLL_PROCESS_ATTACH:
            hUnityPlayer = GetModuleHandleW(L"UnityPlayer.dll");
            DisableThreadLibraryCalls(hinstDLL);
            InstallPatches();
            break;

        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
            break;

        case DLL_PROCESS_DETACH:
            UninstallPatches();
            break;
    }
    return TRUE;

}
