lovely-injector: runtime Lua modding for LÖVE games without repatching the binary
A runtime lua injector for games built with LÖVE
At a glance
- What is it?
- Lovely is a runtime Lua injector for LÖVE 2D games that detours the in-process Lua API instead of patching the executable, so mods can be installed, updated and removed repeatedly without reinstalling the game. The cost is a patch format the project still calls unstable, and a per-platform setup step.
- Who is it for?
- Lovely suits players who want to keep and update mods for LÖVE based games such as Balatro without repatching a game binary each time, and who are comfortable copying a library file into a game directory.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 30 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Detouring the Lua API instead of patching the executable
Lovely is a runtime Lua injector for LÖVE 2D. It embeds code into a running LÖVE game rather than modifying the shipped executable, and the project contrasts that with executable patchers explicitly. The stated benefit is that mods can be installed, updated and removed over and over again without a partial or total game reinstallation, and the mechanism named for this is in-process Lua API detouring combined with an easy to use patch system.
That is the whole idea, and it explains the shape of the rest of the project. The injector sits inside the game process and intercepts Lua calls, and a patch file then says where in the game's Lua source a payload should be injected. The game files on disk are untouched, so swapping a mod means changing a mod folder, and removing one means deleting it.
The implementation is Rust. The repository root holds a `Cargo.toml` that defines a workspace whose members are everything under `crates/`, and it patches one crates.io dependency, `dobby-sys`, to a local path at `crates/dobby-sys`. The patching of a native hooking library is consistent with the detouring approach the project describes. There is also a `rust-toolchain.toml` and a `flake.nix`, so the build has a pinned toolchain and a Nix development path.
For the audience, the target is players of LÖVE based games, with Balatro used throughout the documentation as the worked example. The mods are Lua code, and the injector is the part that gets installed into the game directory.
Linux native needs the love binary and two copied files
The Linux path is the simplest of the three because it does not go through a compatibility layer. You install LÖVE with the distribution's package manager, which the documentation gives as `sudo pacman -S love` on SteamOS and Arch based distributions, and `sudo apt install love` on Ubuntu and Debian based ones.
Then you take the Linux release archive, `x86_64-unknown-linux-gnu.tar.gz`, and copy `liblovely.so` and `run_lovely_linux.sh` into the game directory. The game directory is found the Steam way, by right-clicking the game, hovering Manage and selecting Browse local files.
Mods go in the mod directory, which is deliberately not the game directory. For Balatro on Linux that is `~/.local/share/love/Mods`, and the folder may need to be created by hand.
The game is then started by executing `./run_lovely_linux.sh` from a terminal inside the game directory, and the documentation notes the script can be copied into a directory of your choice instead.
That last point is worth pausing on. Launching through a shell script rather than through Steam means the game no longer starts from the library view, and for a Steam Deck user that changes how the game is started at all. It is also what makes the injector observable, because you can see its output in the terminal rather than having it swallowed by the game launcher.
Windows through Proton needs a DLL override
On Windows the injector is a DLL, and the release archive for it is `lovely-x86_64-pc-windows-msvc.zip`. You copy `winmm.dll` into the game directory, and if you are upgrading from an older version of Lovely you are told to delete the old `version.dll` from that directory first.
The step that trips people up is the launch option. On Steam Deck, Proton or Wine, the game's launch options have to be set to `WINEDLLOVERRIDES="winmm=n,b" %command%`. The documentation marks that line as applying only to Steam Deck, Proton and Wine, and the game is then run through Steam as usual. The override tells Wine to load the bundled `winmm.dll` rather than its own, which is how the injector gets into the process.
The Windows mod directory for Balatro is `%AppData%/Balatro/Mods`, and the Steam Deck variant is buried under the Proton prefix in `steamapps/compatdata/2379780/pfx/drive_c/users/steamuser/AppData/Roaming/Balatro/Mods`. A Flatpak Steam install moves that whole path under `~/.var/app/com.valvesoftware.Steam/`, which is a separate location you have to remember to create.
The macOS path differs in a more fundamental way. There the release archive contains `liblovely.dylib` and `run_lovely_macos.sh`, both copied into the game directory, and the game has to be started either by dragging the script onto Terminal or by running `sh run_lovely_macos.sh`. The documentation explains that you cannot run the game through Steam on Mac because of a bug within the Steam client, so the script is the only route.
Patches come in pattern, regex and copy flavours
A patch file defines where and how code injection happens inside the game process. The format is TOML, either in a `lovely.toml` file or under `lovely/*.toml`, and the documentation gives a complete example.
The file starts with a manifest and a table of variables that are substituted into payloads:
[manifest]
version = "1.0.0"
priority = 0
# Define a var substitution rule. This searches for lines that contain {{lovely:var_name}}
# (var_name from this example, it can really be anything) and replaces each match with the
# provided value.
# This example would transform print('{{lovely:var_name}}') to print('Hello world!').
#
# USEFUL: For when you want to reduce the complexity of repetitive injections, eg. embedding
# release version numbers in multiple locations.
[vars]
var_name = "Hello world!"The `pattern` patch injects lines before, after or at a line matching a literal pattern:
[[patches]]
[patches.pattern]
target = "game.lua"
pattern = "self.SPEEDFACTOR = 1"
position = "after"
payload = '''
initSteamodded()
print('{{lovely:var_name}}')
'''
match_indent = trueThe `regex` patch does the same thing with regular expressions and capture groups, and it can inject into a capture group rather than a whole line:
[patches.regex]
target = "tag.lua"
pattern = "(?<indent>[\t ]*)if (?<cond>_context.type == 'eval' then)"
position = 'at'
line_prepend = '$indent'
payload = '''The payload in that example is the block of Lua that gets spliced in:
local obj = SMODS.Tags[self.key]
local res
if obj and obj.apply and type(obj.apply) == 'function' then
res = obj.apply(self, _context)
end
if res then
return res
elseif $cond
'''The documentation is direct about the regex patch: regex is not efficient, so use the pattern patch unless it is absolutely necessary. It also warns that the regex patch does not trim whitespace from each line, which the author says to take into account when designing a pattern.
The third kind is `copy`, which appends or prepends the contents of files onto a target, and which the documentation points at for the case where you only care about getting your code into the game and nothing else:
[[patches]]
[patches.copy]
target = "main.lua"
position = "append"
sources = [
"core/core.lua",
"core/deck.lua",
"core/joker.lua",
"core/sprite.lua",
"debug/debug.lua",
"loader/loader.lua",
]
payload = "-- I'm extra code that isn't worth an extra file"The example patch set in the Steamodded repository is pointed to as a good complex example of what the format can express.
The format is unstable and mods need their own folder
Two constraints are stated in bold and deserve to be read before anything else.
The first is that the patch format is unstable and prone to change until Lovely is out of early development. That is the project's own words, and it has a practical consequence. A mod that works today may not parse tomorrow, and a mod author who targets the current format is writing against a moving target. The mitigation is implicit in the versioning: the manifest carries a `version` field and a `priority` value, so a loader can reason about ordering, but the format behind those fields is still changing.
The second is the rule about mod layout: mods with Lovely patch files, meaning `lovely.toml` or files under `lovely/*.toml`, must be installed into their own folder, or their own zip file, inside the mod directory. No exceptions are allowed. The reason is structural rather than stylistic, since a patch file describes where to inject code relative to the game's own Lua files, and two mods injecting into the same target need a deterministic order and a way to be removed cleanly.
The Mac mod directory is `/Users/$USER/Library/Application Support/Balatro/Mods`, and the documentation has a practical tip for finding it, pressing `Shift-Command-.` in Finder to reveal hidden files.
The other thing worth planning for is the launch model. On Linux and macOS the game is started by a script, which means the injector is always in the chain and the game starts the same way every time. On Windows the equivalent is the DLL override in the Steam launch options. In all three cases the game directory gains a file that was not shipped by the publisher, and a Steam update that replaces the game directory can remove it.
Licence, releases and what the Rust workspace contains
The licence is MIT, recorded in a `LICENSE.md` file at the repository root. That is a permissive licence with no copyleft obligation attached to the injector or to patches you write, which matters here because mods are distributed as files that other players copy into their own game folders.
On releases, the version trail is v0.10.0 on 2026-09-23, v0.9.0 on 2026-01-21 and v0.8.0 on 2025-06-07. The repository is not archived and the last push was on 2026-09-23, the same day as the newest tag, so the project is being worked on but the numbered versions are still pre-1.0, which is consistent with the early development caveat attached to the patch format.
The workspace layout is thin at the top level. `crates/` holds the members, `Cargo.lock` and `Cargo.toml` describe the build, `flake.lock` and `flake.nix` cover the Nix development environment, `rust-toolchain.toml` pins the Rust toolchain, and there is a `.gitmodules` file for the submodules the build expects. The `Cargo.toml` also carries a `[patch.crates-io]` section pointing `dobby-sys` at the local `crates/dobby-sys` path, which is how the project keeps its hooking dependency under its own control rather than taking whatever version crates.io serves.
The practical upgrade path is therefore a file replacement. On Windows that means copying the new `winmm.dll` over the old one and removing a stale `version.dll`, on macOS replacing `liblovely.dylib` and the run script, and on Linux replacing `liblovely.so` and `run_lovely_linux.sh`. Your mods stay where they are, which is the whole point of the runtime approach.
Editorial conclusion
Lovely suits players who want to keep and update mods for LÖVE based games such as Balatro without repatching a game binary each time, and who are comfortable copying a library file into a game directory. It does not suit someone who needs a stable patch format, since the project states the format is unstable and prone to change until Lovely leaves early development, and it does not suit a Mac player who wants to launch through Steam, because the Steam client bug means the game has to be started with run_lovely_macos.sh. Before installing, note that mods with lovely.toml files must sit in their own folder inside the mod directory, and that the last push was on 2026-09-23 with version 0.10.0 released the same day.
Frequently asked questions
What is Lovely and what does it do to a LÖVE game?
Lovely is a runtime Lua injector that embeds code into a LÖVE 2D game while it runs, using in-process Lua API detouring. Because it does not patch the executable, mods can be installed, updated and removed repeatedly without reinstalling the game.
How do I install Lovely on Linux natively?
Install `love` with your package manager, then copy `liblovely.so` and `run_lovely_linux.sh` from the `x86_64-unknown-linux-gnu.tar.gz` release into the game directory. Mods go into the mod directory, `~/.local/share/love/Mods` for Balatro, and the game starts with `./run_lovely_linux.sh`.
What Steam launch options does Lovely need on Windows?
On Steam Deck, Proton and Wine the launch options must be set to `WINEDLLOVERRIDES="winmm=n,b" %command%`, so Wine loads the bundled `winmm.dll` copied into the game directory. The game is then run through Steam as usual.
Where must a mod with a lovely.toml file be placed?
In its own folder, or its own zip file, inside the mod directory. Mods carrying patch files in `lovely.toml` or `lovely/*.toml` must be installed that way, with no exceptions.
Can I run a Lovely modded game through Steam on macOS?
No. The documentation states that a bug within the Steam client means the game cannot be run through Steam on Mac, so you have to start it with `run_lovely_macos.sh` in Terminal instead.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ethangreen-dev-lovely-injector)Was this useful?