Using IsaacScript in Lua
A library is a collection of helper functions and features that make writing your program easier. Instead of sticking a ton of low-level code in your program, you can instead call a single library function, abstracting away all the complexity and keeping your program nice and clean. In the Isaac modding scene, libraries are really great because the standard API is quite limited.
The IsaacScript framework contains two Binding of Isaac: Repentance libraries:
isaac-typescript-definitions
- A leveled-up version of the vanilla enums with many bug fixes and community contributed additions for everything that the developers forgot to include. You can learn more about every enum by reading the documentation.isaacscript-common
- Helper functions and features that abstract away much of the complexity in working with the Isaac API. It's the biggest and most advanced Isaac library ever written. You can learn more about every function and feature by reading the documentation.
If you are writing your mod in TypeScript, then using these libraries is effortless - you can just start typing the names of the enums or functions, and the auto-complete will automatically import them (and include them in your final bundled mod).
If you are writing your mod in Lua, then you can also leverage the power of these libraries by downloading the isaacscript-common.lua
file and placing it alongside your Lua code. Read on for the specifics.
Note that for Lua users, both isaac-typescript-definitions
and isaacscript-common
are combined into one library bundle, so all you need is one file. From here on out, both libraries will be simply referred to as "the library".
Automatic Installation (Optional)
If you have Python installed on your computer and you know what a terminal/shell is, then the easiest way to install the library is to use the isaacscript-lua
Python tool. Otherwise, you can use the manual installation (see below).
- In a terminal, navigate to the directory of your mod.
- Install the tool:
pip install isaacscript-lua --upgrade
- Install the Lua library:
isaacscript-lua install
If you get ERROR: Could not find a version that satisfies the requirement isaacscript-lua (from versions: none)
, that means that your version of Python is too old. Make sure that you have Python version 3.10 or later installed.
Manual Installation
Here, "installing" is a loose term. All you really need to do is to put the isaacscript-common.lua
file inside your mod. (This is essentially the same thing as copy-pasting code snippets directly into your mod, except that the library code lives in its own dedicated file.)
Step 1 - Download the Library
Download it from the npm registry: https://unpkg.com/isaacscript-common@latest/dist/isaacscript-common.lua
Use right-click + save link as.
(Note that by specifying "latest" as the version in the URL, the website will redirect us to the numbers that correspond to the latest version.)
It will save to a file called "isaacscript-common.lua".
Step 2 - Put It In Your Mod
- Create a subdirectory in your mod called the same thing as you mod.
- For example, if your mod is located at
C:\Repositories\revelations
, create a subdirectory calledC:\Repositories\revelations\revelations
.
- For example, if your mod is located at
- Create a subdirectory in the new directory called
lib
.- For example, if your mod is located at
C:\Repositories\revelations
, create a subdirectory calledC:\Repositories\revelations\revelations\lib
.
- For example, if your mod is located at
- Put the "isaacscript-common.lua" file in the
lib
directory.- For example, if your mod is located at
C:\Repositories\revelations
, it would go toC:\Repositories\revelations\revelations\lib\isaacscript-common.lua
.
- For example, if your mod is located at
In other words, your file structure should look something like this:
my-mod/
|── main.lua (the Lua entry point to your mod)
|── metadata.xml (the Steam Workshop file)
└── my-mod/ (a subdirectory with the same name as your mod for the purpose of preventing namespace conflicts)
└── lib/ (a subdirectory that contains 3rd-party library code)
└── isaacscript-common.lua
(Creating a directory of the same name is necessary because in the Isaac environment, we must namespace all of our code. As the documentation explains, using include
does not obviate the need to do this because we need the ability to access the library in more than one Lua file. Having to namespace code is one of the disadvantages of using Lua instead of TypeScript.)
Basic Usage
Import the Library
At the top of the Lua file where you want to use the library, use the following import statement:
local isc = require("my-mod.lib.isaacscript-common")
Note that:
- You must replace "my-mod" with the name of your mod, which corresponds to the namespaced directory from the previous step.
- The period in the
require
invocation is a directory separator. (It is conventional in Lua to use a period instead of a slash.) - You must repeat this import statement in every Lua file where you use the library. (One disadvantage of using Lua over TypeScript is that you don't have automatic imports.)
Using Pure Functions
Most functions in the library are exported from the root. For example:
local isc = require("my-mod.lib.isaacscript-common")
local function anyPlayerHasSadOnion()
return isc:anyPlayerHasCollectible(CollectibleType.COLLECTIBLE_SAD_ONION)
end
if anyPlayerHasSadOnion() then
print("One or more players has a Sad Onion.")
end
Note that similar to most Lua libraries, you must use a colon (instead of a period) when invoking functions, since the library is an exported module.
Using Enums
As previously mentioned, the enums from isaac-typescript-definitions
are also exported from isaacscript-common
for your use.
For example, there is no vanilla GaperVariant
enum, but in IsaacScript, there is:
if entity.Type == EntityType.ENTITY_GAPER and entity.Variant === isc.GaperVariant.FLAMING_GAPER then
print("This is a Flaming Gaper!")
end
One of the advantages of the improved enums is that they don't have the pointless prefix. So you could slightly improve the previous code snippet like this:
if entity.Type == isc.EntityType.GAPER and entity.Variant === isc.GaperVariant.FLAMING_GAPER then
print("This is a Flaming Gaper!")
end
As a general safety practice, you should always use the library enums over the vanilla enums because the vanilla enums are not safe - they can be modified by other mods and deleted.
Using Custom Callbacks
Like any good library, importing anything in isaacscript-common
will not cause any code to be executed in your mod. Most of its functions are pure functions.
However, in order for the custom callbacks and the extra features to work, some code does need to be executed. This is because these features need to track when certain things happen in-game. In order to enable this functionality, you must upgrade your mod with the upgradeMod
function.
For example:
local isc = require("my-mod.lib.isaacscript-common")
local modVanilla = RegisterMod("Foo", 1)
local mod = isc:upgradeMod(modVanilla)
-- Register normal callbacks.
mod:AddCallback(ModCallbacks.MC_USE_ITEM, function(collectibleType)
Isaac.DebugString("MC_USE_ITEM fired - item was: " .. tostring(collectibleType))
end)
-- Register custom callbacks.
mod:AddCallbackCustom(isc.ModCallbackCustom.POST_ITEM_PICKUP, function(player, pickingUpItem)
Isaac.DebugString("POST_ITEM_PICKUP fired - item was: " .. tostring(pickupUpItem.subType))
end)
Using Extra Features
Some helper functions rely on stateful tracking (like isPlayerUsingPony
) or store data about what you want to do for later (like setHotkey
). These fall under the category of "extra features". Since they are non-pure, you are only able to access them if you upgrade your mod. However, this is slightly different than upgrading your mod for custom callbacks.
Instead of activating every feature when you upgrade your mod, the standard library keeps things blazing fast by only activating the specific features that you need. Thus, when you upgrade your mod, you have to tell the library which features you want by passing them as the second argument to the upgradeMod
function.
For example:
local isc = require("my-mod.lib.isaacscript-common")
local modVanilla = RegisterMod("Foo", 1)
local features = { -- An array of features.
isc.ISCFeature.PONY_DETECTION,
}
local mod = isc:upgradeMod(modVanilla, features)
mod:AddCallback(ModCallbacks.MC_POST_PEFFECT_UPDATE, function (player)
local usingPony = mod:isPlayerUsingPony(player);
Isaac.DebugString("Is this player using a Pony? " .. tostring(usingPony));
end)
Here, by specifying that we want the "pony detection" feature, extra methods are added on to the mod object. Then, we can proceed to use those methods throughout our mod.
Updating the Library
The isaac-typescript-definitions
and isaacscript-common
packages change frequently with bug fixes and new features. You can read about the latest changes on the change log or by reading the commits to the monorepo. Subsequently, it is a good idea to keep your library version up to date.
- The version of your downloaded library can be found in a comment at the top of your
isaacscript-common.lua
file. - The latest version of the
isaacscript-common
library can be found on the npm page (below the title at the top of the page).
To update, you can manually download the latest Lua file again from the links above. Or, if you have the isaacscript-common
Python tool installed, you can simply run: isaacscript-common update
Using JavaScript/TypeScript Array Methods in Lua
In JavaScript/TypeScript, arrays come preloaded with a bunch of useful methods such as map
and filter
, which allow you to write functional, high-level code.
Many of these array methods are now provided as convenience functions inside of isaacscript-common
specifically for Lua users who don't have these methods natively.
If you find yourself using a for
loop to iterate over an array - stop. Become a better programmer and use a higher-order function instead.
Steam Workshop
You might be wondering why isaacscript-common
is not offered on the Steam Workshop. Having Isaac libraries live on the workshop is a poor design choice, as it forces end-users to subscribe to an extra thing. When someone wants to play your mod, they should only have to subscribe to one thing - your mod.
Furthermore, having the library logic bundled with the mod preserves backwards compatibility and ensures that library is tightly-coupled to the mod logic that is using it. It also allows the mod author to be in complete control of when they update to the latest version, if ever. This also allows the upstream library to make breaking changes and stay clean without having to worry about having perpetual technical debt.
The IsaacScript Stage Library
The IsaacScript standard library contains the ability to create custom stages, which was inspired by StageAPI and aims to improve upon it.
Note that some of the custom stage functions (such as e.g. setCustomStage
) cannot be used in Lua, since they require a compiler to generate the custom stage metadata. For advanced users, you could manually prepare the metadata, but at that point you would probably be better off using StageAPI, since nothing you write is going to be type safe anyway.
File Size
The file size of the library is around 2 megabytes, so using the library will increase the total size of your mod by that amount. With that said, the file size of your mod is mostly irrelevant for a few reasons. This is a common misconception by newer programmers, so it is worth taking a few minutes to explain why this is.
Running Time
A library's run-time is defined as the time it takes for its code to execute while the player is inside of the game, actively playing. Just because a library has a large file size does not mean that it takes a long time to execute. Obviously, not all lines of code are created equal, and different kinds of code will take varying amounts of time to execute.
As previously mentioned, isaacscript-common
will not cause any code to be executed in your mod by default. Thus, it has no run-time cost whatsoever if you are just using enums and pure functions.
On the other hand, if you explicitly upgrade your mod, some extra code is executed using some vanilla callbacks. However, the code is extremely efficient such that callback code is only ever executed if you are actually using the specific callback. Thus, thousands of copies of the standard library can run simultaneously without ever measuring a run-time performance penalty.
Loading Time
Loading time is defined as the time it takes for the game to load the Lua code when the game first boots. Just because a library has a large file size does not mean that it takes a long time for the game to load it.
In general, we care about loading time a lot less than the run-time, because it only happens when the user first launches the game. And it is largely invisible to the end-user playing the mod.
With that said, loading the library has been measured to take around 4 milliseconds. Thus, you could load the library around 250 times in a row before an end-user would ever even start to notice. So, even if you are passionate enough to care about this, the library is around three orders of magnitude too small for it to matter.
Download Time
Download time is defined as the time it takes to download the mod over Steam. This is probably the place where the size of your mod matters the most.
With that said, we care about download time even less than loading time, because it only happens once on the initial mod download. (It would also happen if you upload a mod patch post-release.) Obviously, this is going to happen a lot less and matter a lot less than the previous two sections.
In 2022, the average internet speed is 64.7 Mbps. This means that on average, users will be able to download the library in just 0.3 seconds. (Average speed also increases every year, so this number will be even smaller by the time you are reading this.)
How much does 0.3 seconds matter? That depends on your much you value sub-second optimizations that will only happen once-per-user over the lifetime of your product. For most sane people, this will be near-zero.
Other Assets
Note that for many mods, the size of your assets (e.g. sprite files, sound files, music files, room files) will vastly outweigh the size of all of your code. So even if the file size of your code did matter (which it doesn't), it would quickly become more important to spend time and energy on reducing the file size of the assets instead of worrying about optimizing the code.
In order to further drive home this point home, consider that the most popular Repentance mod of all time is Fiend Folio, which clocks in at around 581 megabytes. That is several orders of magnitude larger than the standard library, and yet virtually no-one in the Isaac ecosystem cares.
Minification
It is possible to reduce the file size of the library by using a Lua minifier. However, it is not recommended to do this, because it will not improve the run-time speed of your mod. (The whole point of minification is to reduce the file size of the mod, but doing that is near-pointless, as the previous four sections have established.)
Furthermore, minification is actively harmful since it will obfuscate the line numbers of your run-time errors. (Run-time errors are mostly non-existent if you use TypeScript, but they happen a ton in Lua.)
TypeScript
If you find the IsaacScript standard library useful, you should consider using it in a TypeScript mod. TypeScript has the advantage of auto-complete, auto-importing, and the compiler preventing you from ever misusing anything in the library. Taken together, it makes for a dream-like Isaac development experience.
For more information, see the list of features. (If you don't know how to program in TypeScript, then you can learn in around 30 minutes.)