Modding: Using CMake

From Noita Wiki
Jump to navigation Jump to search
Modding Navigation
Fundamentals
BasicsData.wakGetting startedLua ScriptingUseful Tools
Guides
AudioEnemiesEnvironments (Fog of War) • Image EmittersMaterialsPerksSpecial BehaviorsSpellsSpritesheetsSteam WorkshopUsing CMake
Components/Entities
Component DocumentationEnumsList of all tagsSpecial TagsTags SystemUpdate Order
Lua Scripting
Lua APIUtility Scripts
Other Information
Enemy Information TableMagic NumbersSound EventsSpell IDsPerk IDsMaterial IDs

CMake is a build system primarily aimed at C and C++ development but it can also be used for many other things, including automating steps in your Noita mod development process.

There's not much reason to use it unless you have some advanced requirements, such as:

  • Creating release zip files
  • Downloading files
  • Generating files
  • Compiling native code
  • Running tests

This guide assumes basic experience with the CMD/PowerShell and Noita mod development. Prior experience with CMake is also helpful.

Installation

See:

Make sure CMake and Ninja are in your PATH environment variable.

Basics

Project Start

Let's create a basic mod that uses CMake to automate some steps. The mod name is "HelloMod", this is the project structure:

HelloMod-source/
├── CMakeLists.txt
└── HelloMod
    ├── init.lua
    └── mod.xml

This mod is just for demonstration purposes, the contents of init.lua and mod.xml are not important.

Files that should be copied verbatim to the final mod folder are put in a subdirectory with the same name as the desired mod folder, here it contains init.lua and mod.xml. Files that shouldn't be included in the release or that must be generated/processed in some way are placed outside of this directory.

CMakeLists.txt is used to specify what CMake should do. This is how we define a basic, installable project:

cmake_minimum_required(VERSION 3.24)

project(HelloMod
    VERSION 0.1.0
    DESCRIPTION "Noita example mod"
    HOMEPAGE_URL "https://github.com/dextercd/Noita-CMake-Example"
    LANGUAGES # Empty
)

install(DIRECTORY HelloMod
    DESTINATION .
    COMPONENT HelloMod
)
  • cmake_minimum_required specifies what version of CMake we want to use
  • project lists some basic info about the project
  • install(DIRECTORY ...) makes it so that the directory is copied to the install location when we run the installation step

These are the CMake commands to build and install the mod:

PS Y:\> cmake -G Ninja -DCMAKE_INSTALL_PREFIX="C:\Program Files (x86)\Steam\steamapps\common\Noita\mods" -B Y:\HelloMod-build -S Y:\HelloMod-source
-- Configuring done
-- Generating done
-- Build files have been written to: Y:/HelloMod-build
PS Y:\> cd Y:\HelloMod-build
PS Y:\HelloMod-build> cmake --build .
ninja: no work to do.
PS Y:\HelloMod-build> cmake --install .  --component HelloMod
-- Install configuration: ""
-- Installing: C:/Program Files (x86)/Steam/steamapps/common/Noita/mods/./HelloMod
-- Installing: C:/Program Files (x86)/Steam/steamapps/common/Noita/mods/./HelloMod/init.lua
-- Installing: C:/Program Files (x86)/Steam/steamapps/common/Noita/mods/./HelloMod/mod.xml

With the first CMake command we configure the build, this way it knows where the source directory is and where to put the build files. The rest of this guide assumes you're running the CMake commands from the build directory.

The --build subcommand detects changes made to CMakeLists.txt in order to create updated build files, after this it builds the targets (currently we have none). You can use the --install subcommand to install the files to the CMAKE_INSTALL_PREFIX path that was specified in the configure step. Using your Noita mods folder for this makes a lot of sense when you are developing your mod.

Creating a Package

This basic CMake project was pretty simple to setup, but we didn't gain anything compared to just working directly in our Noita mods folder. Let's use CMake's packaging functionality to do something useful and automate the creation of a mod release zip file.

Packaging is handled by CPack which is included with CMake, using it requires some boilerplate code:

# ... previous CMakeLists.txt content is above here ...

# Packaging

set(CPACK_GENERATOR ZIP)
set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY FALSE)
set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}")
set(CPACK_VERBATIM_VARIABLES TRUE)
include(CPack)
  • CPACK_GENERATOR: list of default package generators to use, in this case only ZIP. Other options include 7Z (.7z), TGZ (.tar.gz), TBZ2 (.tar.bz2), and more.
  • CPACK_INCLUDE_TOPLEVEL_DIRECTORY: by default CPack adds an additional directory in the archive. Here we don't want that.
  • CPACK_PACKAGE_FILE_NAME: normally the system you're building for is included in the package name, e.g. 'Linux' or 'win32'. We don't want this so we change it from the default.
  • CPACK_VERBATIM_VARIABLES: this is false by default for backwards compatibility but in new projects you want to set this to true.

After adding these lines you can use CPack to create a zip file of your mod:

PS Y:\HelloMod-build> cmake --build .
[0/1] Re-running CMake...-- Configuring done
-- Generating done
-- Build files have been written to: Y:/HelloMod-build

ninja: no work to do.
PS Y:\HelloMod-build> cpack
CPack: Create package using ZIP
CPack: Install projects
CPack: - Install project: HelloMod []
CPack: Create package
CPack: - package: Y:/HelloMod-build/HelloMod-0.1.0.zip generated.

Tada! If you want to make a 7Zip archive you just run cpack -G 7Z instead. Archives built this way have the following structure:

Archive:  HelloMod-0.1.0.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  2022-12-29 20:53   HelloMod/
       77  2022-12-28 21:23   HelloMod/init.lua
       80  2022-12-28 21:55   HelloMod/mod.xml

It has a top-level folder with the name we chose, to install the mod the user can open the zip then drag and drop this into their mods folder.

Downloading Files

Sometimes we want to include code or other files from some other project into our mod. A lot of the time the simplest/best solution is to copy the files into your repository, but if there are many files, large files, or binary files (e.g. .dll, .exe) then you may not want to do that. You can instead use CMake to manage these dependencies.

Let's say you want to use EZWand and LuaJIT-MinHook in your mod, this is what you would add to the CMakeLists.txt file:

# ... basic project setup is above here ...

# Dependencies

include(FetchContent)

FetchContent_Declare(EZWand
    URL https://github.com/TheHorscht/EZWand/releases/download/v1.5.0/EZWand.lua
    DOWNLOAD_NO_EXTRACT TRUE
)
FetchContent_MakeAvailable(EZWand)

install(FILES ${ezwand_SOURCE_DIR}/EZWand.lua
    DESTINATION HelloMod/lib/EZWand
    COMPONENT HelloMod
)

FetchContent_Declare(LuaJIT-MinHook
    URL https://github.com/dextercd/LuaJIT-MinHook/releases/download/release-1.1.1/LuaJIT-MinHook-1.1.1.zip
)
FetchContent_MakeAvailable(LuaJIT-MinHook)

install(DIRECTORY ${luajit-minhook_SOURCE_DIR}/
    DESTINATION HelloMod/lib/MinHook
    COMPONENT HelloMod
)

# ... CPack stuff should always come last ...

The FetchContent module is used to download files/archives.

We specify the name and download location of dependencies using FetchContent_Declare. EZWand is a single Lua file, not an archive that must be extracted, so we must specify the DOWNLOAD_NO_EXTRACT TRUE option.

FetchContent_MakeAvailable will do the initial download and perform a redownload whenever we change the URL. (This means updating dependencies should be pretty simple!)

After the FetchContent_MakeAvailable command has run it creates a <lowercaseName>_SOURCE_DIR variable which is the path the files were downloaded to. We use this to install the files we want to include in the mod.

After re-running cmake --build . and cpack we have a zip archive containing these files:

Archive:  HelloMod-0.1.0.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  2022-12-29 21:49   HelloMod/
        0  2022-12-29 21:49   HelloMod/lib/
        0  2022-12-29 21:49   HelloMod/lib/MinHook/
     3346  2022-12-29 21:33   HelloMod/lib/MinHook/minhook.lua
    21504  2022-12-29 21:33   HelloMod/lib/MinHook/luajit_minhook.dll
        0  2022-12-29 21:49   HelloMod/lib/EZWand/
    44068  2022-12-29 20:53   HelloMod/lib/EZWand/EZWand.lua
       77  2022-12-28 21:23   HelloMod/init.lua
       80  2022-12-28 21:55   HelloMod/mod.xml
---------                     -------
    69075                     9 files

You can still use cmake --install . --component HelloMod to place the files in your mod folder during development.

Building C/C++ Code

CMake is primarily a C and C++ build system so it's ideal for building executables and DLLs for your mod. Whether this native code is for testing parts of your mod, generating data, or an integral part of how your mod works, CMake can compile, execute, and package these files for you.

In this guide we will use these facilities to build a DLL with which we can set the contents of the clipboard. The mod will use this to put the world seed in the player's clipboard at the start of a run.

To start you need a C/C++ toolchain installed (most likely some version of VC++). Make sure CMake can find this toolchain, with VC++ you need to make it visible in the environment by launching an 'x86 Native Tools Command Prompt'.

Now we need to tell CMake that our project uses C++, we do this by changing the project command in the CMakeLists.txt file.

 project(HelloMod
     VERSION 0.1.0
     DESCRIPTION "Noita example mod"
     HOMEPAGE_URL "https://github.com/dextercd/Noita-CMake-Example"
+    LANGUAGES CXX
 )

If you run the --build subcommand now it should show info about the compilation tools CMake found.

The project has a clipboard.cpp file in the root directory. This C++ code exports a single function that we can use from Lua which sets the contents of the clipboard using the win32 API.

Now we tell CMake to build this file into a DLL and package it with the rest of the mod. Make sure this code is added above the packaging setup.

# ...

# C++ module

add_library(clipboard MODULE
    clipboard.cpp
)

install(TARGETS clipboard
    LIBRARY DESTINATION HelloMod/dll
    COMPONENT HelloMod
)

# ... CPack stuff should always come last ...

If you build and install the project you should see clipboard.dll in your mod inside of the dll folder.

Now enable request_no_api_restrictions="1" in mod.xml, and change the init.lua file to this:

local ffi = require("ffi")

ffi.cdef([[
bool set_clipboard(const char* text);
]])

local clipboard = ffi.load("mods/HelloMod/dll/clipboard.dll")

function OnWorldInitialized()
    local seed = StatsGetValue("world_seed")
    local text = "Currently playing this seed: " .. seed
    if not clipboard.set_clipboard(text) then
        GamePrint("Couldn't set clipboard!")
    end
end

And that should be everything! You now have a functioning mod that puts the current seed you're playing on in your clipboard. Installing and packaging using CMake should work exactly like before.

Note that this example uses LuaJIT's ffi module instead of a C package that interfaces with Lua using the Lua C API. The latter requires finding and linking against Lua which is a lot more tedious to setup (but definitely possible!)

Project Using CMake

Links to Noita mods that use CMake, perhaps there are some useful snippets that you can reuse:

Resources

This guide is here just to demonstrate some useful techniques, and to have some code that you can copy and paste to get a project started on CMake. It's not a replacement for CMake's documentation or other learning material.