nemu
NEMU is a collection of related small LuaJIT libraries that aim to emulate a portion of the Noita Lua API.
This project does not aim to and will never reimplement any gameplay or actual functionality of the game. Instead, it implements the bare essentials to "load" mods:
- Compatible virtual filesystem (
data.wak
support, Steam Workshop mod path redirection etc.) - Alterations to the Lua standard library (weird
dofile
behavior,dofile_once
etc) Mod*
functions (ModIsEnabled
,ModTextFileSetContent
etc.)
The above features make it possible for external Lua code to load a mod, let it run its init.lua
in full and possibly later inspect changes to engine state or the global environment (for example, ModLuaFileAppend
use can be tracked through a lua_appends
field on the mod_manager
object).
loading modules
In order to make integrating in different Lua environments easier, every module listed below supports making use of a NEMU_LOAD_MODULE
global function to load modules. This function will receive a canonical, forward-slashed path relative to the root directory of the entire nemu project. It should return the result of calling dofile
with a path to the intended file.
example
Loads all enabled mods, executes them and then prints the list of Lua file appends done through the ModLuaFileAppend
API function:
local nemu = {
session = require("session")
mod_manager = require("mod_manager")
vfs = require("vfs")
engine = require("engine")
}
local root = "/home/zatherz/.local/share/Steam/steamapps/common/Noita"
local session = nemu.session.new(root, root .. "/save00", root .. "/../../workshop/content/881100")
local vfs = nemu.vfs.new_noita(session)
local mod_manager = nemu.mod_manager.new(session, vfs)
vfs:import_mods(mod_manager)
print("SESSION: " .. tostring(s))
print("MOD MANAGER: " .. tostring(m))
local engine = nemu.engine.new(session, vfs, mod_manager, {
report_stubs = true
})
mod_manager:load_all(engine)
print("LUA APPENDS")
for to_filename, from_list in pairs(m.lua_appends) do
print("TARGET SCRIPT: " .. to_filename)
for i, from_filename in ipairs(from_list) do
print("- " .. from_filename)
end
end
session.lua
Uses luawak
(from libs/
) internally.
Container for information about a Noita "session" or installation. More specifically - paths to various resources.
-
table
session
-
function
.new(root_path : string, save_path : string[, workshop_content_path : string])
returnsnemu.session
root_path
points to Noita's root directory (the parent ofnoita.exe
)
save_path
points to thesave00
directory (usually in AppData)
workshop_content_path
points to thesteamapps/workshop/content/881100
directory in the Steam installation and is optional, if not given, the session is considered to be non-Steam and workshop support will be disabled in other modules (e.g.vfs.lua
)
-
-
table type
nxml.session
-
function
:expand_root_path(rel_path : string)
returnsstring
expands
rel_path
in the context of the root Noita directory -
function
:expand_save_path(rel_path : string)
returnsstring
expands
rel_path
in the context of the save directory (save00
) -
function
:expand_workshop_path(rel_path ; string)
returnsstring
expands
rel_path
in the context of the game's workshop content directory
if ran on a non-Steam session, it will always returnnil
-
field
root_path
of typestring
-
field
save_path
of typestring
-
field
workshop_content_path
of typestring
nil
if accessed on a non-Steam session
-
vfs.lua
Requires session.lua
.
Noita VFS emulation requires mod_manager.lua
.
Implements a Noita-like virtual file system.
-
table
vfs
-
function
.path_parent(path : string)
returnsstring
utility function to get the parent directory of
path
-
function
.path_append(path : string, part : string)
returnsstring
utility function to combine a relative path to an absolute path, or two relative paths
-
function
.path_normalize(path : string)
returnsstring
removes excess forward slashes from
path
, creating a canonical path that points to the same place -
function
.path_as_child_of(path : string, parent : string)
returnsstring
if
path
is a child ofparent
, then the part relative fromparent
is extracted from it otherwise,nil
is returned -
function
.path_exists(path : string)
returnsstring
returns
true
if a file underpath
exists, orfalse
if not -
function
.new_virtual_file(tostring_info : string, func_impl : table{read : function(nemu.file) -> string}, instance_data : any)
returnsnemu.file
creates a new virtual file, where reading is implemented by the
read
function offunc_impl
which should return the full content of the file as a string
instance_data
can be anything, and is attached onto thenemu.file
under theuserdata
fieldtostring_info
is used when passing thenemu.file
instance totostring()
in order to provide more information about the object -
function
.new_file(real_path : string, vfs_path : string)
returnsnemu.file
creates a new physical file, where
real_path
is the path on disk andvfs_path
is the intended path within the virtual filesystem
content of physical files can be overriden through the:set_virtual_file
function -
function
.new(session : nemu.session)
returnsnemu.vfs
creates a new barebones virtual filesystem from the given
session
note that this function does not apply any kind of special handling ofdata.wak
or Steam Workshop mods -
function
.new_noita(session : nemu.session)
returnsnemu.vfs
creates a new Noita-like virtual filesystem from the given
session
this only registers handlers fordata.wak
overlay - to support Steam Workshop mods, call the:import_mods
functoin on the newly creatednemu.vfs
table
-
-
table type
nemu.vfs
-
function
:add_dir_handler(name : string, dir : string, handler : function(vfs : nemu.vfs, full_path : string) -> nemu.file)
registers a directory handler
handler
with the namename
for all child directories ofdir
after this call, any access to paths that are children ofdir
will be delegated to the provided function, which should return a file ornil
if the file doesn't exist or can't be accessed for other reasons -
function
:open(path : string)
returnsnemu.file
opens and returns the file at
path
within the virtual filesystem
if the file doesn't exist or can't be accessed, an error will be raised with diagnostic info about where the handling of the file access was delegated to -
function
:set_virtual_file(path : string, content : string)
creates a new virtual file filled with
content
atpath
if no file exists at that location
if a file already exists at that location, it will override it (the next:read
on that file will return `content) -
function
:import_mods(mod_manager : nemu.mod_manager)
registers
mods/.../
path handlers for Steam Workshop mods loaded by themod_manager
-
-
table type
nemu.file
-
function
:read()
returnsstring
returns the entire content of the file as a string
-
mod_manager.lua
Requires session.lua
and vfs.lua
, as well as luanxml
(from libs/
) internally.
Executing init scripts requires engine.lua
.
Implements very primitive loading of mods.
-
table
mod_manager
-
function
.new(session : nemu.session, fs : nemu.vfs)
returns `nemu.mod_manager"creates a new mod manager
-
-
table type
nemu.mod_manager
-
function
:load_all(engine : nemu.engine)
runs through the list of loaded and enabled mods, reads all of their
mod.xml
files, executes all of their init scripts using the providedengine
, then marks them as active -
function
:is_enabled(id : string)
returnsboolean
-
function
:get(id : string)
returnstable{enabled : boolean, active : boolean, workshop_id : number, name : string, physical_path : string, display_name : string, description : string, unrestrict : boolean, gamemode : table{...}}
returns a table with details about the mod with ID
id
-
function
:add_lua_append(to : string, from : string)
registers the Lua script at path
from
to be loaded whenever one at pathto
is loaded
backing implementation of theModLuaFileAppend
API function -
function
:set_file_content(filename : string, new_content : string)
overrides the content of the file
filename
within the virtual filesystem withnew_content
backing implementation of theModTextFileSetContent
API function -
function
:get_file_content(filename : string)
reads the content of the file at
filename
within the virtual filesystem, including any overrides done through the use of:set_file_content
backing implementation of theModTextFileGetContent
API function -
function
:who_set_file_content(filename : string)
returns the ID of the mod that last ended up running
:set_file_content
withfilename
provided as the path
backing implementation of theModTextFileWhoSetContent
API function
-
engine.lua
Requires session.lua
, mod_manager.lua
and vfs.lua
. Internally uses luawak
and luanxml
from libs/
. Also has a hard dependency on the LuaJIT FFI library being available.
Implements the fake Noita Lua environment that allows for executing certain scripts.
-
table
engine
-
function
.setcwd(path : string)
sets the current working directory of the process to
path
-
function
.getcwd()
returnsstring
returns the current working directory of the process
-
function
.cstr_trim(str : string)
returnsstring
if
str
contains a null byte, the result will terminate right before it - otherwise, the result will be equivalent tostr
this function is used to emulate C string behavior of certain API functions in the Noita engine, which unintentionally trim strings at the first null byte -
function
.new(session : nemu.session, fs : nemu.vfs, mod_manager : nemu.mod_manager, options : table{...})
returnsnemu.engine
creates a new engine based on the provided
session
,fs
andmod_manager
options
can have the following fields:name : string
- controls the name of the engine in diagnostic messages, defaultNEMU
print_prefix : string
- if set, all text printed usingprint()
orprint_error()
will be prefixed with this stringreport_stubs : boolean
- iftrue
, diagnostic messages will be printed whenever a stubbed (unimplemented) function is called
-
-
table type
nemu.engine
-
function
:diag(text : string)
prints a diagnostic message
-
function
:error(reason : string)
prints a Noita error
note that this is not a Lua error - like in the actual game's API, you cannot catch these errors and they just get printed to the log -
function
:stub()
when called within a function, produces a diagnostic message about a stub function being called with the name of the caller
-
function
:panic(reason : string)
produces an engine panic, which immediately quits the process
this emulates situations where Noita would crash natively, like callingGlobalsSetValue
too early -
function
:create_env(unrestrict : boolean)
returnstable{...}
creates a new environment
ifunrestrict
isfalse
, then the environment will be sandboxed in the same way as Noita's, otherwise it will not be sandboxed at all
the result of this function is intended to be used withsetfenv
, although note that only doing that will likely break (due to the wrong working directory)
use:run_in_root
withsetfenv
to ensure the working directory gets changed and restored correctly (or simply use:execute
) -
function
:run_in_root(f : function())
runs
f
with no arguments with the Noita root directory as the current working directory (for correct path resolution) -
function
:execute(unrestrict : boolean, f : function())
runs
f
under a new environment created by callingself:create_env(unrestrict)
-