How to Write Lua Scripts for Video Games with the BizHawk Emulator

July 10, 2020
Written by
Sam Agnew
Twilion
Reviewed by
Diane Phan
Twilion

Copy of Generic Blog Header 1.png

BizHawk is a multi-system emulator beloved by the Tool Assisted Speedrun community for its recording/playback and debugging tools, as well as Lua scripting functionality that can be used for a variety of purposes.

While there is basic documentation describing some of the functions available in these scripts, the lack of working code samples might make it difficult for some to get started. Let's walk through some of the Lua scripting features the BizHawk emulator provides, and have some fun with real examples.

Setting up the BizHawk emulator

BizHawk runs on multiple operating systems, but Lua scripting is only available on the Windows versions. If you are on Mac or Linux, you can use Wine, although setup might be a bit of an involved process depending on which version of which operating system you're using.

There are installation instructions in the README of the project's repository, including an installer that takes care of the prerequisites. Download, unzip, run this installation tool, and then download the corresponding version of the emulator from TASVideos. Now you can run the EmuHawk executable, and load up your favorite video game ROM!

The original Super Mario Bros for the Nintendo Entertainment System is a widely popular classic game so we will be referencing it throughout the post for our code examples.

Super Mario Bros

Getting started with Lua scripting

Now that you have the emulator running, open up the Lua scripting window via Tools -> Lua Console. Here you can open scripts, control their execution, and view their output. In the text editor of your choice, create a file called hello.lua and add the following line of code to it:

console.log('Hello World!')

Run this code by clicking Script -> Open Script in the Lua Console and navigating to where you saved the file. You should see "Hello World!" printed in the “Outputs” tab.

For most emulator scripts, it's desirable to have a main execution loop that runs continuously until it's stopped, executing code before each frame of the game is rendered. There are two ways you can do this. One is to write an infinite loop using emu.frameadvance at the end of each iteration, and the other is to register a function to event.onframestart. Let's take a look at both of these.

Replace the code in hello.lua with the following, which will write "Hello World!" to the screen for every frame of the game, rather than the console:

while true do
    gui.text(50, 50, 'Hello World!')
    emu.frameadvance()
end

You can double click your script in the Lua console to toggle it on and off, and each time you restart it, your new code will load if you have made changes and saved the file. Do this and you should see "Hello World!" pop onto the screen and stay there because it is being printed on every frame. Personally, I am excited to write an infinite loop that's actually useful, as an act of vindication for all of the ones I've written by mistake.

Hello World

If you don't like the way infinite loops look, let's try doing it more functionally using event.onframestart with the following code that has the same behavior:

function gameloop()
    gui.text(50, 50, 'Hello World!')
end

event.onframestart(gameloop)

With that out of the way, let's move onto writing code to interact with the game itself.

Reading from and writing to memory

BizHawk provides many useful developer tools to gain insight into the games you are playing. These utilities come in handy when doing speedruns or hacking old games. One of them is a “hex editor”, a tool that allows you to view and edit the game's RAM in real time. The RAM is displayed in the form of 4 digit (for NES games) hexadecimal addresses and 2 digit hexadecimal values.

Hex Editor

Open BizHawk's hex editor by clicking Tools -> Hex Editor. All of the two digit hex numbers you see represent a value in the game’s RAM at a specific location. You can change any value to see what happens in-game. You can also search for specific values using Ctrl-F or clicking Edit -> Find. For example, the hex editor can be used to find the bytes corresponding to the timer in Super Mario Bros and to change the time to zero to kill Mario:

Editing the timer to zero

Cleverly editing memory addresses can result in some interesting gameplay modifications. In this case, we killed Mario by editing the values in the memory addresses, 07F8-07FA to zero. But this can also be done programmatically in a Lua script.

Create a new script with the following code, which will log the values of the timer to the console, and then change them to zero to kill Mario:

console.log(memory.readbyte(0x07F8) .. memory.readbyte(0x07F9) .. memory.readbyte(0x07FA))

memory.writebyte(0x07F8, 0)
memory.writebyte(0x07F9, 0)
memory.writebyte(0x07FA, 0)

In Lua, 0x before a number means that you are referring to a hexadecimal value, and .. is an operator for string concatenation. As you can see in this code you can read the values of hex addresses in the game's memory with memory.readbyte and write to them with memory.writebyte.

The idea of digging around a game's memory may seem daunting at first, but it turns out that for many games, people on the internet have already done this for you! Here is a RAM map for Super Mario Bros. Try playing around with these memory addresses and values for yourself and see what happens.

Controller input - playing the game with code

Aside from manipulating a game's memory, you can also programmatically enter button presses with joypad.set and read input with joypad.get. The button names are different depending on each video game console, so here is a useful chart with string values of button names for each system.

Controller inputs are represented by a table in Lua, which is similar to a dictionary in Python or an object in JavaScript. For instance, joypad.set({A=true}, 1) would press the A button on player 1's controller.

Using Mario as an example again, run a Lua script with the following code to make Mario run to the right indefinitely at full speed:

while true do
    joypad.set({Right=true, B=true}, 1)
    emu.frameadvance()
end

Running into a Goomba

This is great and while running full speed into a Goomba might be funny, it is not a good recipe for success. What if we combined all of the things we learned to write some code that can run through the entire first level of Super Mario Bros? With the information from the RAM map for the game, we can write code to read values from the game's memory to make decisions on when to jump.

Create a file called level1.lua with the following code:

-- A table of indexes of pit locations in the level, as well as
-- two other spots that aren't pits where jumping also helps.
local pits = {[0x1C]=true, [0x24]=true, [0x30]=true, [0x34]=true, [0x3C]=true, [0x54]=true}

while true do
        -- Our button inputs, which will continuously move to the right by default.
        -- Add to this table to jump depending on the situation.
        local input = {Right=true}

        -- Mario's current position
        local mario = memory.readbyte(0x0086)

        -- The first enemy's (on the screen) current position
        local enemy = memory.readbyte(0x0087)

        -- Where we are in the level
        local level_index = memory.readbyte(0x072C)

        -- If Mario is colliding with an object (like a pipe), hold jump for 20 frames.
        if memory.readbyte(0x0490) == 0xFE then
            input.A = true
            for i=1, 20, 1 do
                joypad.set(input, 1)
                emu.frameadvance()
            end

            -- For some reason the above strategy isn't working in these two specific spots.
            if level_index == 0x52 or level_index == 0x60 then
                joypad.set({A=true})
                emu.frameadvance()
            end
        end

        -- If Mario is close to an enemy, jump.
        -- 38 is just a number of pixels that I found works well from experimentation.
        if math.abs(mario - enemy) < 38 then
            input.A = true
        end

        -- If Mario is near a pit, jump.
        if pits[memory.readbyte(0x072C)] then
            -- For some pits, it works better to slow down a bit before jumping.
            -- Advancing a few frames with no button input will do this.
            for i=1, 3, 1 do
                emu.frameadvance()
            end

            input.A = true

            -- Hold A for 20 frames to clear any gaps.
            for i=1, 20, 1 do
                joypad.set(input, 1)
                emu.frameadvance()
            end
        end

        -- Set the button inputs and advance the frame, restarting the loop.
        joypad.set(input, 1)
        emu.frameadvance()
end

I left comments for each section to explain the code, but the basic strategy is to continuously move to the right while jumping in these scenarios:

  • Whenever an enemy gets too close
  • When there is a pit nearby
  • If Mario is colliding with an obstacle

jumping whenever an enemy gets too close, whenever there is a pit nearby, or whenever Mario is colliding with an obstacle. There are values in memory that represent all of these things, at least accurately enough for this example.

Start the game at World 1-1, and run this code to see Mario complete the level! (If you let it keep running he will likely get to World 1-2 and lose to the second Goomba).

Completing World 1-1 programmatically

It's not the most sophisticated or efficient way to beat the level, but it gets the job done. This was only the first level, so now the rest is up to you. Try improving on the script to beat the next level!

Level Complete!

We've covered some decent ground here to get you started writing with Lua scripts for video games, but BizHawk's Lua API has some other features as well. With the event module you can register functions to events, such as saving or loading save states, or writing to certain memory locations. There is also a comm module that provides some communications functionality such as socket connections and HTTP requests. These are all described in the BizHawk Lua Functions documentation on TASVideos.

I cannot wait to see what kind emulator extensions you build. Drop me a line if you have any questions or if you just want to show off your hack.