Modding: Using CMake
Fundamentals |
---|
Basics • Data.wak • Getting started • Lua Scripting • Useful Tools |
Guides |
Audio • Enemies • Environments (Fog of War) • Image Emitters • Materials • Perks • Special Behaviors • Spells • Spritesheets • Steam Workshop • Using CMake |
Components/Entities |
Component Documentation • Enums • List of all tags • Special Tags • Tags System • Update Order |
Lua Scripting |
Lua API • Utility Scripts |
Other Information |
Enemy Information Table • Magic Numbers • Sound Events • Spell IDs • Perk IDs • Material 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:
- https://cmake.org/install/
- https://ninja-build.org/ (Not required but it's the generator used in this guide)
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 useproject
lists some basic info about the projectinstall(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 include7Z
(.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.
clipboard.cpp |
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <cstring>
/**
* Set the clipboard to the provided text.
* @return true if the clipboard change was successful.
*/
extern "C" __declspec(dllexport)
bool set_clipboard(const char* text)
{
bool success = false;
bool clipboard_opened = false;
HGLOBAL global_data = nullptr;
// Need a window for the clipboard API, it's never made visible
HWND clipboard_window = CreateWindowA(
"Message", nullptr, 0, 0, 0, 0, 0, nullptr, nullptr, nullptr, nullptr);
if (!clipboard_window)
goto cleanup;
if (!(clipboard_opened = OpenClipboard(clipboard_window)) || !EmptyClipboard())
goto cleanup;
std::size_t data_length = std::strlen(text) + 1;
global_data = GlobalAlloc(GMEM_MOVEABLE, data_length);
if (!global_data)
goto cleanup;
void* data = GlobalLock(global_data);
if (!data)
goto cleanup;
std::memcpy(data, text, data_length);
if (!GlobalUnlock(global_data))
goto cleanup;
if (SetClipboardData(CF_TEXT, global_data)) {
global_data = nullptr; // System now manages the data
success = true;
}
cleanup:
if (global_data)
GlobalFree(global_data);
if (clipboard_opened)
CloseClipboard();
if (clipboard_window)
DestroyWindow(clipboard_window);
return success;
}
|
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:
- https://github.com/dextercd/Noita-CMake-Example - The example project used throughout this guide
- https://github.com/dextercd/Noita-Minidump - Building C++ code and adding the DLL into the mod folder
- https://github.com/dextercd/Noita-Shutdown - Generating sprite images using Python
- https://github.com/dextercd/Noita-Synchronise-Expansive-Worlds - Building C and C++ code and building documentation files
- https://github.com/dextercd/Noita-Component-Explorer - Generating Lua code from templates
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.
- Reference Documentation: https://cmake.org/cmake/help/latest/
- Great book on CMake: https://crascit.com/professional-cmake/