'Unity as Library' Change Data Folder

I’m using the Unity as Library UnityLibrary.dll, and calling the UnityMain, but I really do not want the data folder next to my executable.

Can you please add, say, a command line option to change the data directory to a different path? Even as much as allowing me to set current directory first if you really need to.

I’m sure there would be many pleased in general if you allowed a player command line to change the data directory as well.

+1.
Try use unity as library but can’t. Unity data search path hardcoded to main process executable, but not UnityPlayer.dll module and can’t be changed.

Pass in “-datafolder” command line argument when invoking UnityMain, like “-datafolder path\to\my\data\folder”.

“-datafolder absolutepath” not work at least for Unity 2021.3.24.
For some reason Unity still not use defined folder, but searching for “{exename}_data” in directory there exe located.
Error:

Application folder:
D:/ProjectDir/bin/Debug/net6.0
There should be 'SimpleEngine_Data'
folder next to the executable

Ah darn, that was added to Unity 2022.2.

Ah darn, that was added to Unity 2022.2.

Tried this with Unity 2023.3. It did not seem to work. Is this feature actually implemented?

I couldn’t find it in the command-line documentation: Unity - Manual: Unity Standalone Player command line arguments

EDIT: This seems to work, however without the mono runtime (MonoBleedingEdge) being in the same folder as the executable, this seems to still fail.

This folder structure Unity requires is quite rigid. Is there no hope for developers who would like to execute UnityMain from a path that’s completely outside of Unity’s required folder structure?

That should work with IL2CPP, I believe. MonoBleedingEdge folder is hardcoded to be next to the executable, though :(.

Unity seems to use GetModuleFileNameA/GetModuleFileNameW to determine the “base directory” that it’ll search in for MonoBleedingEdge, D3D12, the crash handler, etc. You can modify the import address table (IAT) of UnityPlayer.dll before you call UnityMain to redirect those calls to your own function that provides a different path.

Assuming your executable is written in C++17, here’s some sample code that will change an entry in the IAT:

static PIMAGE_IMPORT_DESCRIPTOR GetImportDirectory(HMODULE module)
{
    auto dosHeader = (PIMAGE_DOS_HEADER)module;
    auto ntHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)module + dosHeader->e_lfanew);
    auto directory = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    return (PIMAGE_IMPORT_DESCRIPTOR)((DWORD_PTR)module + directory.VirtualAddress);
}

static void SetModuleImport(HMODULE module, LPCSTR importModuleName, LPCSTR importName, DWORD_PTR function)
{
    auto imports = GetImportDirectory(module);
    auto importModule = GetModuleHandleA(importModuleName);

    for (int i = 0; imports[i].Name != 0; i++)
    {
        if (auto handle = GetModuleHandleA((LPCSTR)((DWORD_PTR)module + imports[i].Name)); handle != importModule)
            continue;

        auto thunks = (PIMAGE_THUNK_DATA)((DWORD_PTR)module + imports[i].FirstThunk);
        auto originalThunks = (PIMAGE_THUNK_DATA)((DWORD_PTR)module + imports[i].OriginalFirstThunk);

        for (int j = 0; originalThunks[j].u1.AddressOfData != 0; j++)
        {
            auto import = (PIMAGE_IMPORT_BY_NAME)((DWORD_PTR)module + originalThunks[j].u1.AddressOfData);

            if (strcmp(import->Name, importName) == 0)
            {
                DWORD oldProtect;
                VirtualProtect(&thunks[j].u1.Function, sizeof DWORD_PTR, PAGE_READWRITE, &oldProtect);
                thunks[j].u1.Function = function;
                VirtualProtect(&thunks[j].u1.Function, sizeof DWORD_PTR, oldProtect, &oldProtect);
                return;
            }
        }
    }
}

If you want the engine to search for MonoBleedingEdge etc next to UnityPlayer.dll, you can use these functions:

static DWORD WINAPI DGetModuleFileNameA(HMODULE hModule, LPSTR lpFilename, DWORD nSize)
{
    if (hModule == GetModuleHandleA(nullptr))
        hModule = GetModuleHandleA("UnityPlayer.dll");

    return GetModuleFileNameA(hModule, lpFilename, nSize);
}

static DWORD WINAPI DGetModuleFileNameW(HMODULE hModule, LPWSTR lpFilename, DWORD nSize)
{
    if (hModule == GetModuleHandleA(nullptr))
        hModule = GetModuleHandleA("UnityPlayer.dll");

    return GetModuleFileNameW(hModule, lpFilename, nSize);
}

And enable them using:

auto module = LoadLibraryW(L"path/to/UnityPlayer.dll");
SetModuleImport(module, "KERNEL32", "GetModuleFileNameA", (DWORD_PTR)&DGetModuleFileNameA);
SetModuleImport(module, "KERNEL32", "GetModuleFileNameW", (DWORD_PTR)&DGetModuleFileNameW);

Nice workaround!