I wrote a League of Legends mod where you control your character with a controller. League of Legends is a PC-only MOBA, which does not support controllers at all. I wanted to see if I could put my porting knowledge from Nixxes together with my reverse engineering knowledge and create something that could be considered good enough to be first-party. I wanted it to work on the Steam Deck, and without manual patch support. I even got to use my request/ratelimiter knowledge from my API/Javascript gamedev days!

Unfortunately (spoiler alert), I never got to completely finish it. It works fine, but my goal to get it on the Steam Deck never came to fruition. I can’t make the source code public due to League of Legends internals, it wasn’t popular enough and I do do not have the time to stay ahead of the inevitable upgrade anti-cheat update they’ve announced, as well as professional and personal factors.

Before I even really started..

It started quite a long time ago, somewhere in 2016, when I watched a Youtube video of BoxBox, a popular League of Legends youtuber, play with a Playstation controller. He bound his left analog stick to move around the mouse, effectively making it a mouse emulator. I felt like this would be a hassle to control, and far too difficult to get into the game with. I felt like I could do better than that.

So I opened Visual Studio, made a small C++ console application and messed around with it a bit. The mouse would center on the character (which was a hard-coded offset on screen), and your mouse would move into the relative direction you were moving your left analog stick pointed in. I recently found out that another Youtuber, TanTan, did the same 3 years ago!

https://www.youtube.com/watch?v=VOBn__YZT3w

I advanced on the topic a bit more by making the left mouse stick input really fast mouse inputs in the relative direction, so that it would move your character. Changed the “real” mouse to be on the right stick instead. This would effectively make the game a twin-stick shooter, if you were a ranged character. Instantly when playing I knew this was the right direction for the project to go into, but something was off, the mouse inputs were not fast enough to control in 2 directions at the same time, which made for awkward mistakes. It needed more internal knowledge, as aiming was hard. I dropped the project to work on other things instead, but it never really left my mind.

When the Steam Deck was announced, I knew I wanted it, I was looking at other handheld PCs at the time, but always found them to be too expensive. I also knew that it would be the perfect reason to pick this project back up again. So I did, but this time, I also planned it out quite extensively. I knew that it would need to read the game memory to do slight aim assist, and to move around.

I started off modding Unity

Knowing the constraints I had to deal with, I decided to make LeagueController a DLL that you would inject into the game. I knew I needed:

  • A high performance response application in the foreground
  • Debug overlay rendering to capture live what was happening
  • Internal League of Legends memory, and potentially calling functions that the game would use.

I could potentially forego the calling the functions, run the game in windowed or borderless fullscreen and make an external application, but that would mean I would have to externally read memory, which is slower, especially with the amount I would be doing it. I’ll get into why exactly later on.

So, League would have to run every single time I tested the game. Starting a League of Legends game is a relatively large hassle for a couple of reasons:

  1. You’re dependent on the service to be live (which is less than you think when you generally code personal projects at 6 AM!)
  2. You need to log in, click 7 buttons, wait a 10 second timer, and wait for the loading screen to complete.
  3. Anti-cheat services disallow you to attach a debugger.

So I needed an alternative game that used the same rendering API (DirectX 11) and started extremely fast. I knew the fastest and easiest way to do this was to make an empty Unity game, which is how DontCareDX11 was born, an empty 800x600 DX11 Unity build that started pretty much instantly. I don’t know how I got to this name.

DontCareDX11. It’s not even the real one, I had to recreate it since the original was gone.

DontCareDX11. It’s not even the real one, I had to recreate it since the original was gone.

In this project, I would be able to do fast interations on creating the base setup of the project. In here I basically started off with:

  1. Setting up a DX11 Present Hook. This would serve as a place to initially render some debug overlays, and use as an update loop.
  2. Create hooks for input. I need to catch keyboard, mouse, and window events. The window events were necessary for raw input (PS5 controllers) and optimisations (foreshadowing!)

I created some project/library separations because I felt like some of the code would be really useful in offspring projects.

  • GameOverlay: My static library for creating an overlay for the game. It would also handle assertions and error logging (to file and screen) and communication with the application that launched it through IPC, for hot-reloading and initial settings such as the working directory.
  • ControllerManager: A library to handle both Raw Input and XInput controllers.
  • LeagueController: The main DLL, anything that would be specifically for this project would go in here.
  • OverlayInjector: Does what it says on the tin! But also communicated and managed the settings and environment

Getting into game..

I’ll skip over the parts that are kind of fuzzy (mind you, none of this was a serious project at the point, just something I was playing with a bit) and sort of boring. I’ve set up Raw Input to map PS4/PS5 controllers and XInput with listeners so that you could see whenever a controller appeared, disappeared, or sent input. It would update in the DX11 Present Hook I made, and I would output the information in a nice Dear ImGui overlay over the Unity game I made.

After these inputs were done, I would have to output keyboard and mouse inputs in the game. So finally I was ready to inject my application into the League of Legends game binary itself. After some fixes to make that work, I would find out that my ancient method of SendInputting my key inputs to League of Legends no longer worked! The inputs that I would get from the window handler also didn’t work like they did in the Unity project, but that wasn’t as bad, as I would only use those for the debug overlay.

This meant that I also could not use the Unity project anymore, as I would have to use the API that League of Legends uses for mouse and keyboard, DirectInput. If you’re not familiar with this API, there is one function here that is of interest here if you’re injecting, IDirectInputDevice8::GetDeviceData. For a device you’ve initialised, you can retrieve buffered data that the device has sent to the computer. When we hook, we can intercept all calls to this function, read it, and inject our own if we have any.

static HRESULT WINAPI HookedGetDeviceData(IDirectInputDevice8* device, DWORD objectData, LPDIDEVICEOBJECTDATA deviceObjectData, LPDWORD inOut, DWORD flags)
{
	GameOverlay_LogInfoOnce(g_inputLog, "Received GetDeviceData call.");
	DWORD arraySize = *inOut; // Remember how large our array is

	HRESULT result = g_originalGetDeviceData(device, objectData, deviceObjectData, inOut, flags);
	if (result != DI_OK && result != DI_BUFFEROVERFLOW)
	{
		GameOverlay_LogError(g_inputLog, "Original GetDeviceData failed: %u", result);
		return result;
	}

	for (DWORD i = 0; i < *inOut; i++)
	{
		DIDEVICEOBJECTDATA& data = deviceObjectData[i];

		// Collect information about timestamps, input indices and app data

		// Read all the input, forward them to our systems (overlay, debug keys)
		bool isDown = (data.dwData & 0x80) != 0;
		switch (inputDevice->type)
		{
		case InputType::Keyboard:
		{
			UINT virtualKey = MapVirtualKeyEx(data.dwOfs, MAPVK_VSC_TO_VK, 0);
			if (isDown)
				Input::OnKeyDown(virtualKey);
			else
				Input::OnKeyUp(virtualKey);
			break;
		}

		case InputType::Mouse:
		{
			if (data.dwOfs == DIMOFS_BUTTON0 || data.dwOfs == DIMOFS_BUTTON1 || data.dwOfs == DIMOFS_BUTTON2)
			{
				if (isDown)
					Input::OnMouseDown(data.dwOfs - DIMOFS_BUTTON0);
				else
					Input::OnMouseUp(data.dwOfs - DIMOFS_BUTTON0);
			}
			break;
		}
		}
	}

	// Use the collected information about previous inputs to inject our own inputs
	if (GetMouseDevice() != nullptr && device == GetMouseDevice())
	{
		const std::vector<MouseInputRequest>& inputRequests = GetMouseInputRequests();
		for (const MouseInputRequest& input : inputRequests)
		{
			if (*inOut >= arraySize)
				break;
			DIDEVICEOBJECTDATA& data = deviceObjectData[*inOut]; // End of the array 

			// Fake data
			data.dwOfs = DIMOFS_BUTTON0 + input.button;
			data.uAppData = GetAppData();
			data.dwSequence = GetAndIncSequenceIndex();
			data.dwTimeStamp = CalculateNow();
			if (input.down)
				data.dwData |= 0x80;

			*inOut += 1;
		}

		ClearSentMouseInputRequests();
	}

	return result;
}

Note, I modified this code to hide some implementation details. It might not be correct!

This piece of code allows me to send the mouse and keyboard inputs as if they were League of Legends’ own. Neat! Redoing my old project with this setup, with some hardcoded ranges, and I basically had my old project back up. However, it wasn’t exactly correct.

The unending struggle to make it feel correct

If you happened to have watch the Tantan video above, you’ll see some issues that arise from not being able to know what’s happening in the game:

1. You don’t know where the center of the character is, where your mouse needs to be

This would need to know where the character is on screen. Tantan decided to guess the location and speed based on on-screen data, but I felt like that would require a bit too much upkeep. Additionally, the camera is slightly offset differently based on what side of the map you’re playing from! Tantan solves this by having a boolean option.

This is where I had to start reading game memory, I needed to know where the local player GameObject was at all times. I knew it was a 2D game in all ways but rendered on screen, so the position of the player would be at his feet, where the aiming of the spells would also originate from.

2. You don’t know what the range of the character’s abilities are.

You need to restrict the aim range to the maximum distance that the character could aim to, to avoid having the imprecision of aiming for the whole screen. Tantan used hardcoded offsets in a Lua script, here, that would have to be chosen at the start of the game.. I felt like that would be too much for the player to set up. And I knew the game would have to know about these.

3. The game updates every 2 weeks.

So, to solve these, I get the LocalPlayer data. But I can’t just read the same memory address every time the game loads, because every 2 weeks, the game updates, making the locations of these addresses completely different.

The solution: x86 pattern recognition and game content parsing

To solve 1 and 2, you need to read your own GameObject. From this object, I initially need a couple of things:

  1. The name of the champion you’re playing.
  2. Its position
  3. The range of the basic attack (punch or shoot)

You can go to any shady game hacking forum and find the addresses for these game objects, as well as the member data of the structs. The location of the member data generally stays the same, but could change whenever Riot Games would change their game object. These would be offsets into the object itself. Every time Riot Games builds the game though, the LocalPlayer singleton object is in a completely new location. Hackers would go back to these forums and copy-paste the new addresses, but that’s not something I wanted to do every 2 weeks.

The way the posters of these memory addresses on the forum do it is with their own patterns, which they enter in IDA, an interactive debugger, and search for a string such as the following:

IDAs binary string search

IDA’s binary string search

This string is x86 bytecode where certain parts have been replaced with question marks. The question marks here mean “anything can be in this position”. For the code above, these are bytes used for a relative address. Starting the search would result in the following screen:

IDAs binary string search result

The x86 assembly search result

The arrow here points to the piece of code in question! The dword_313D244 here is the variable that signifies the memory address relative to the base of the executable image. Meaning that if I would to use this address in my code, it would contain the data I was looking for!

The issue here is clear, that’s a lot of manual work for 1 address. If I would have to do that for the list of things mentioned at the start of this sub-section, I would have to search for 4 of these patterns every 2 weeks! And that’s just to get started on the project. As of right now, I’ve got 49 of these patterns to run!

So, the answer is to automate this. During runtime of the application, I check if the game has these pattern search results cached. If not, we generate them. I recreated the pattern search that IDA uses here.

void* GetByOperand(Address& relativeAddress, const std::initializer_list<u8>& pattern, u8 ignoreCase, int initialInstructionIndex, int initialOperandIndex, void* inBegin, void* inEnd)
{
	if (relativeAddress != 0)
	{
		GameOverlay_LogDebug(g_offsetLog, "GetByOperand ignored: %p is a valid relative address\n", relativeAddress);
		return true;
	}

	u8* current = (u8*)inBegin;
	u8* end = (u8*)inEnd;
	while (current < end)
	{
		// Copy these numbers (since we're changing them)
		int instructionIndex = initialInstructionIndex;
		int operandIndex = initialOperandIndex;

		u8* locationInCode = (u8*)FindPattern(current, end, pattern, ignoreCase);
		if (locationInCode == nullptr)
		{
			GameOverlay_LogDebug(g_offsetLog, "GetByOperand failed: Unable to find array of bytes: %s\n", PrintBytePattern(pattern, ignoreCase).c_str());
			return true;
		}

		std::vector<Instruction> instructions = Instruction::FromPattern(locationInCode, pattern.size());

		GameOverlay_LogDebug(g_offsetLog, "GetByOperand found a result at: %p\n", locationInCode);
		GameOverlay_LogDebug(g_offsetLog, "GetByOperand: %s %s\n", instructions[0].mnemonic, instructions[0].operands);

		int instructionCount = (int)instructions.size();
		while (instructionIndex < 0)
			instructionIndex += instructionCount;

		if (instructionIndex >= instructionCount)
		{
			GameOverlay_LogDebug(g_offsetLog, "GetByOperand is ignoring 0x%p: Not enough instructions. Instruction Count: %u, Needed: %u, Pattern: %s\n", locationInCode, instructionCount, instructionIndex, PrintBytePattern(pattern, ignoreCase).c_str());
			current = locationInCode + 1;
			continue;
		}

		Instruction& instruction = instructions[instructionIndex];
		int opCount = (int)instruction.GetOperandCount();
		while (operandIndex < 0)
			operandIndex += opCount;

		if (operandIndex >= opCount)
		{
			GameOverlay_LogDebug(g_offsetLog, "GetByOperand is ignoring 0x%p: Not enough operands. Operand Count: %u, Needed: %u, Pattern: %s\n", locationInCode, opCount, operandIndex, PrintBytePattern(pattern, ignoreCase).c_str());
			current = locationInCode + 1;
			continue;
		}

		const Operand* ops = instruction.GetOperands();
		const Operand& operand = ops[operandIndex];
		GameOverlay_LogDebug(g_offsetLog, "GetByOperand: Getting the data for %s %s (type = %d, %x, %x)\n", instructions[0].mnemonic, instructions[0].operands, operand.type, operand.mem.disp, operand.imm);

		Address offset = operand.type == X86_OP_IMM ? operand.imm : operand.mem.disp;
		relativeAddress = offset - (Address)lpBaseOfDll;
		return true;
	}

	return false;
}

Yes I know the parameter code formatting is inconsistent! If I weren’t the only developer, I would’ve been cleaner here, I swear. Also again, I’ve modified the code to make it slightly smaller.

The code above is a bit much, I know, but the gist is that it does 3 things:

  1. Go over the game memory and find the pattern.
  2. If a result is found, assemble the code using Capstone (Thanks Capstone!)
  3. Get the operand requested and convert it to a relative offset.

And I can use that directly! If it were a float containing the seconds we’ve spent in the game simulation, I would just have to convert it to a float*. Simple as that. One of the perks of being inside the game process.

Now to the second problem, every character has spells that you can cast (with Q, W, E, R), which do not necessarily have the same range as the basic attacks. I already dynamically load some spell information (name, cooldown) from the game memory, but not the ranges. Another problem with these is that they could change during gameplay or in certain conditions. There are over 150 champions in this game, and they all have 4 unique abilities! Unfortunately, all the scripts dictating these ranges are server-sided, only leaving hints for the client to figure out.

So I decided to re-use an old system I used for the League of Legends game model renderer, one that reads their archive files and bin structures. I then find the ranges for all champion abilities in the current game (because there’s are two champions that can steal enemy abilities, and a dozen that can “transform”). Another developer worked on a library that can determine all ability ranges for the current game environment, so I could also dynamically determine those thanks to that.


Tristana’s ranges increase when she levels up.

The unending struggle to make it feel good

Now the game worked, it ran, and you could control the character. Though, even though it technically worked, and it technically was fine, far better than any public generic controller solution, and on par with Tantan’s (if you don’t count the manual labour), but I still had the mouse issue. I can’t cast spells in a different direction than the movement direction, because of the two mouse positions. I needed something better. I needed to make sure that it felt good to play.

Issuing my own commands

So, instead of moving with the mouse, I had to move using internally called functions. Of course, the hackers had a solution for this, so I implemented it, and it worked great! The game did ratelimit you on these, and would disconnect you if you went over. So using my knowledge of making a ratelimiter for HTTP APIs, I made a mix between a burst setup (sending as fast as possible) and a spread one (spread allowed messages over amount of time measured). This made it feel fast, fluid and didn’t disconnect you from the game!

It was great! Until I walked into a literal wall.


Ah, my mortal enemy, intentional game design.

If you click on the wrong side of the wall, or beyond that, League of Legends will start pathing you around the object. This is exactly what you want for a mouse, since you want to the point you’re clicking on, but for us, that’s a direction instead. So you’d aim to the north, and walk to the west instead!

This was confusing, and additionally because the direction I input was a vector that was quite long, it would also cause precision issues due to the game pathing you through the obstacles along the way. Instead, what I wanted it to do was to make the character hug the wall, like you would do in any other game.

So for this issue, I decided to dive into the old game data I used to have for the League of Legends server emulator. One of the parts I worked on was to parse the navgrid, and to make it useful for game code to call upon it. This navgrid basically translates down to a 2D image containing points that are passable or not, as well as some other useful information. Using a 2D raytracing function, you can “raycast” into this image from your own position, into the target direction and get the nearest impassable point.


After a bit of tweaking with the numbers, we can correctly walk up to walls. A monumental achievement.

Of course, this would’ve been very hard to do if we didn’t have live information of what was happening behind the scenes, so I drew the navgrid to an image, and displayed it in my overlay debug window:


Circles are drawn around GameObjects, and a purple one around the mouse position in world space. The red line is my aim ray.

Make the difference in ranges not feel too jarring.

To aim at an enemy, you hold your right stick in the correct direction, and then you cast the spell. Sounds simple enough right? There was a slight oversight I made here, some skill ranges are vastly different. Volibear can cast storms and bite someone, for instance. The bite has to be close to Volibear, while the storm can be near the edge of your screen. Let me demonstrate what holding the right analog stick looked in a direction, at bite-range looked like when using your combo:


That was a so-called near-hit.

When trying to aim at a specific enemy, you basically want to remember where you aimed last time, for as long as you’re aiming in that direction. So that’s one of the cool things it now checks for:


That’s better.

Make the player feel like they can aim

So at this point it’s fairly playable, although clicking entities is sort of hard. Especially since the hitbox of some spells are very tiny, and are cast by directly clicking the opponent. This is really easy to do with a mouse, but when you’re directionally aiming a large range with a controller this becomes really difficult. So I added aim-assist, which isn’t that interesting, it sets the mouse position over the enemy when it gets close.. right?

This part required quite a bit of iteration. Partially because I’ve never properly worked on aim assist for controllers before, partially because it just didn’t feel right half the time. When do you release aim assist? How do you need to to hold the analog stick to keep aiming? There’s a surprising amount of work that went into this. Every time I look back at the code for this, I feel like it’s a very alien behaviour. You hold the stick at a certain spot after aim assist? What if you stay in range, but the target is completely on the other side of the character? Honestly, that sounds so dumb, it sounds like the wrong implementation but it feels good. It feels correct. I don’t think I’ve ever worked on a system where saying the implementation out loud just sounds so wrong, but when you use it, it instinctively works perfectly fine.

Which is what I thought, until I tried playing champions with slow spells against moving targets. Here I found a completely new problem: I hard locked-on, aimed 100% perfectly at the feet and you could just cast the spell there. For everything I regularly played this felt fine, but I thought I’d pick up an older champion where the spell takes 250ms to launch after 100ms of casting. The opponent gets 350 millisecond to get out of a small area for that and I could not for the life of me hit a spell on anyone unless they were intentionally standing still.

So I had to make a new concept, a soft lock-on. This system was even more bizarre, because it doesn’t really lock on, it just slows down aiming once you’re in the nearby area of a valid target, and then slightly adjusts your aim in the direction you’re moving in, and they’re moving in. You’d basically have to aim yourself again, but small corrections would be helped by the system itself. It would linearly interpolate into regular movement, so it looked like it wasn’t assisting aim at all, unless you really looked closely. Now I was able to hit spells again, and it felt good.

Aim assist must be the least scientifically provable thing I worked on, ever. It’s sort of jarring, but I’m glad I had the experience of trying. I should’ve investigated this topic a bit more beforehand, though.

Hard lock-on still had a place, precise lock-on was necessary for target selection spells. Luckily, these are clearly identifyable using the static champion data I was already loading. So all target-selection spells would use the precise version, and the others would just be suggestive.

Other small feel-good changes

If you’re not aiming at all, but are moving, you cast spells in the direction you’re moving. This is done so that you can still play while drinking tea, or whatever. I found out that dash spells have a specific field to indicate they’re dashes, so any dash always use the movement stick. This allows you to keep dashing away while still aiming towards your primary target.


Makes sense, right?

I wanted potions to be restricted to a single button, so I also made a script that based on your max health, would use the best potion. I do this by checking which item spells ids (more on item spells in the next paragraph) in our inventory are potions, and checking which would give me the most health for the current situation.


A riveting video of me pressing right on the DPad.

Fixing the lack of controller buttons

One of the controllers main downsides is that you don’t have as many keys as you do on a PC. You’d have to sacrifice keys to key modifiers, or something similar.

League has the unique problem that you can dynamically change the keybinding of an item spell by moving it to a different item slot. If you buy yourself a Zhonya’s Hourglass, you get a spell bound to the key for the respective item index. Item in slot 1? Means that you press 1 to cast this spell. I can’t really, nor really don’t want to move items around during gameplay with a controller, but I still want the custom item key bindings. Here’s our first non-debug rendering task!

  1. First, I detect if there’s a new spell for this item in your inventory. Let’s say we got a new “Item_2403” spell sitting in our inventory.
  2. Every frame, the game checks if there are any spells unbound. We look into the game files and check if it’s a valid, castable spell. If so, and while in the safety of your base, you will get a prompt to bind this key.

Minion Dematerializer

  1. It gets all the localised name data from the game data too.
  2. Once you press an unused key, for the remainder of the game this key will be used to activate this item for as long as it occupies place in your inventory.

Optimisation stage

Funnily enough, the things I thought would be the most problematic were nowhere near the slowest parts of my application. I had set myself the goal that I would get this application to run below 250μs. This would mean that League of Legends had plenty of time rendering the game, and I would be no obstruction to the game internals.

Some examples of what I thought would be slow:

  1. DetermineNearestObject(): This function I call twice per frame, once to determine the closest champion, and once for anything else. I use this to get the nearest one to the mouse, and I was going to optimise it later. I use some terribly slow things, string comparison to determine object type, for instance, going over every single object in the game. Turns out, this is fine.
  2. Lua. I use some Lua scripts, and a wrapper called sol to wrap them. Turns out just swapping to LuaJIT was trivial, and it ran incredibly fast relative to the non-JIT version.

Some things I did not expect:

Luckily these are easily fixed by just checking a window message. Another unsuspected problem was RawInput APIs/XInput being slow? I never thought it would take as much time as it did. I moved all the new device processing to another thread, since it didn’t need to be processed every frame. All in all, optimising my application took an afternoon. Pretty proud of myself, apparently I can still make decently-optimised applications!

Some final attributions

I’ve never finished a personal project as closely as I did this one. I loved working on it. This project wouldn’t have become this amazing without the help of some amazing friends. Thank you all.

  • First and foremost: Wildbook, for helping me making this possible. Simply possible, I couldn’t have wished for a better source of League of Legends internals knowledge, Lua knowledge, and good ideas. If only you were a better League of Legends player.
  • SkinSpotlights: For anything Wildbook wasn’t sure of, or when he didn’t respond in time.
  • LxWing: For the static data library containing over 600 spell range formulas. you’re insane, why did you make this, what in god’s name got into you
  • moonshadow565: For showing me that even in League of Legends’ data format, pointers can be null.
  • PapaPoli: For being brave enough to trust that my mod wasn’t a virus, and playtesting it.

Thanks to the following open-source projects:

  • CDragon: For being an open platform where I can search exactly what internal data I was looking for. Thanks for keeping it up and running all these years.
  • Dear ImGui: For an absolutely perfect debug UI.
  • Sokol: For making me able to focus less on graphics APIs and shaders.
  • Capstone: For making parsing x86 bytecode trivial and fast.
  • Lua, using Sol: Used for quick iteration times when the DLL wouldn’t hot-reload. Such a fun language to write in/for (not so much debug though)
  • Tracy: For being a fine profiler.

Contact

If you want to talk to me, feel free to message me over on LinkedIn:

https://www.linkedin.com/in/querijn-heijmans-4232a54b/