Hooking the Lua State in Train Simulator is easy, due to the Lua Library being statically linked in the games code, rather than being a .DLL. This has some advantages but also some disadvantages.

DllMain Function

Here we create a DllMain function so that we can inject our DLL to the game. We first set a global variable to the hInstDLL and then create ourselves a thread to work in. MainThread is our thread

typedef unsigned __int64 QWORD;

HMODULE g_instance = nullptr;
unsigned long lua_State = 0;

BOOL WINAPI DllMain(HINSTANCE hInstDLL, DWORD reason, LPVOID lpReserved)
{
	switch (reason)
	{
	case DLL_PROCESS_ATTACH:
		DisableThreadLibraryCalls(hInstDLL);
		g_instance = (HMODULE)hInstDLL;
		CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)&MainThread, nullptr, 0, nullptr);
		break;

	case DLL_THREAD_ATTACH:
		break;

	case DLL_THREAD_DETACH:
		break;

	case DLL_PROCESS_DETACH:
		break;
	}

	return true;
}

MainThread Function

This is where we will do anything that calls Lua methods

DWORD WINAPI MainThread(LPVOID)
{
	AllocConsole(); // Allocates a console so yo ucan log things
	SetConsoleTitleA("Console"); // Sets the consoles window title
	freopen_s((FILE**)stdin, "conin$", "r", stdin);
	freopen_s((FILE**)stdout, "conout$", "w", stdout);

	while (true)
	{
		// END Key - This ends everything and cleans up
		if (GetAsyncKeyState(VK_END) & 0x8000)
		{
			// READ THE NEXT SECTION BEFORE UNCOMMNENTING
			// DetourTransactionBegin();
			// DetourUpdateThread( GetCurrentThread() );
			// DetourDetach( &(LPVOID&)lua_gettop_p, (PBYTE)_gettop );
			// DetourTransactionCommit();

			fclose((FILE*)stdin);
			fclose((FILE*)stdout);
			FreeConsole(); // Deallocates the console
			FreeLibraryAndExitThread(g_instance, 0); // Clean up our DLL and uninject
		}
		Sleep(100);
	}

	return 0;
}

Writing a Detour Function

Here we are creating a method which we can call to detour the gettop method in the game. This allows us to run our own code in place of the games code. But for it to still work as intended we must return the correct value.

typedef int(__cdecl *gettop)(unsigned long);
gettop lua_gettop_p = (gettop)0x18121A870;

// This is our version of the gettop function
DWORD _gettop(__int64 state)
{
	// If Lua State is not already set then log it and set it
	if (lua_State == 0) {
		std::cout << "[Lua] _gettop called - State:" << state << std::endl;
		lua_State = state;
	}

	// This is from the decompiled function and cannot be changed
	return (unsigned long)(*((QWORD *)state + 2) - *((QWORD *)state + 3)) >> 4;
}

// Here we setup the detour to "replace" the games function with ours
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(LPVOID&)lua_gettop_p, (PBYTE)_gettop);
DetourTransactionCommit();

TypeDefs and Function Addresses

These are mostly untested and could be wrong if the game is updated

typedef int(__cdecl *call)(unsigned long, int, unsigned int);
typedef int(__cdecl *dostring)(unsigned long, const char*);
typedef int(__cdecl *error)(unsigned long, const char*, ...);
typedef int(__cdecl *gettop)(unsigned long);
typedef int(__cdecl *gettable)(unsigned long, int);
typedef int(__cdecl *pushstring)(unsigned long, const char*);
typedef int(__cdecl *pushvalue)(unsigned long, int);
typedef int(__cdecl *print)(unsigned long);
typedef int(__cdecl *settable)(unsigned long, int);
typedef int(__cdecl *settop)(unsigned long, int);
typedef const char*(__cdecl *tostring)(unsigned long, int);

call lua_call_p = (call)0x18121B420;
dostring lua_dostring_p = (dostring)0x18122E850;
error luaL_error_p = (error)0x18122E850;
gettop lua_gettop_p = (gettop)0x18121A870;
gettable lua_gettable_p = (gettable)0x18121B0C0;
pushstring lua_pushstring_p = (pushstring)0x18121AEE0;
pushvalue lua_pushvalue_p = (pushvalue)0x18121A9C0;
print luaB_print_p = (print)0x1812337F0;
settable lua_settable_p = (settable)0x18121B270;
settop lua_settop_p = (settop)0x18121A880;
tostring lua_tostring_p = (tostring)0x18121AC50;

Building Microsoft Detours Package

  1. Download the latest release
  2. Open the folder in Visual Studio