Balatro" (If this at all sounds interesting to you, you should take a look; it's great.)
After hearing that it was built by a single person, I was curious about how it came together, and how I could mod it.
Looking at the game's files, I found a README file belonging to Löve, which advertises itself as a free game engine.
In a nutshell (and with a tiny bit of ignorance), Löve can be thought of as a lightweight wrapper around SDL.
So, I started looking into how Löve builds binaries, and found that they just append an archive to the end of the executable, which contains all the game's assets and code.
The game's executable basically reads the archive from the end of its own file, and then executes the Lua code contained within.
The game is more or less single file, which makes distribution and installation easy, but isn't so great for modding.
This leaves us with two options:
I believe going with the second approach is more interesting, so let's take a look at how we can do that.
I wrote a small tool wich I called lurk, that basically acts as a toolkit to introspect and modify Lua applications at runtime. It allows you to inspect the Lua state, modify it, and even inject new code into the running application.
A shared library is loaded into the application that utilizes lua, during startup. The library waits for the Lua library to be loaded, then continues to hook into the relevant functions responsible for managing the Lua State.
Effectively, the same thing could be done by replacing the Lua library with a custom one, but that would only work in scenarios where lua is dynamically linked, which we want to avoid.
The tool has 3 parameters that can be specified:
As of right now, there is no way to distinguish between different Lua states, so the library will instrument all Lua states that are created by the application.
For more detailed building and running instructions, please refer to the README of the project.
The first step in modifying the game is to dump the scripts that are being loaded into the Lua state, as otherwise we don't really know what we're looking at or dealing with.
This can be done by running the game with the lurk
library loaded, and specifying a directory to dump the scripts to, like this:
monitor.exe -s "<Game dir>\\Balatro.exe" -a "lurk.dll" -p set_parameters "dump {gamesrc/}"
Opening main.Lua
in a text editor, we indeed see the usual Löve structure of defining love.run()
and so on. (I cannot share the source code here, as it is copyrighted, but feel free to dump it yourself.)
love.update()
is the main loop of the game; let's attempt to hook this.
We can do this by creating a new Lua script, which will be loaded into the game using the lurk
library.
monitor.exe -s "<Game dir>\\Balatro.exe" -a "lurk.dll" -p "load {hook.Lua}"
The script will override the love.update()
function to add our own logic.
-- hook.Lua
local o_update = nil
function update_hook(dt)
print("inside update")
return o_update(dt)
end
function periodic()
if love and love.update and love.update ~= update_hook then
o_update = love.update
love.update = update_hook
end
end
Doing this confirms that we've successfully replaced the love.update()
function with our own implementation.
Now that we have a hook into the game's main loop, we can start modifying the game state. For example, we can modify the player's money amount by adding the following code to our hook.Lua
script:
function update_hook(dt)
+ if G.HUD and G.GAME and G.GAME.dollars then
+ G.GAME.dollars = 100000
+ end
This will set the player's money to 100,000 every frame, effectively making the player rich.
With lurk being quite small, clocking in at 400 lines of code (with most of it being boilerplate), it is surprisingly easy to introspect and modify Lua applications at runtime, if you have the patience to build your own tools.