Enshrouded Mod Loader (EML)
EML is a lua-based mod loader and framework for the game Enshrouded.
As of right now, EML is in (somewhat) active development and is not yet complete. Currently it only supports asset modifications before the game starts, but runtime modifications are planned for the future.
If you need help, have questions, or want to discuss modding in general, feel free to join the Discord server.
Usage Instructions
This document provides instructions on how to use the Enshrouded Mod Loader to load and manage mods for the game Enshrouded.
There are currently two ways to use EML:
- Using the Enshrouded Mod Loader Proxy DLL (recommended)
- Using the Enshrouded Mod Manager CLI
Using the Proxy DLL (Recommended)
This is the recommended way to use EML as it provides a seamless experience for loading mods directly when launching the game. This works for both client and server installations of Enshrouded.
Prerequisites
- Ensure you have a legal copy of the game Enshrouded installed on your system.
- Download the latest version of the dbghelp.dll binary.
- If you used EML before with the
dbghelp.dllproxy, make sure to remove it from your game directory to avoid conflicts.
Installation
- Extract the contents of the downloaded archive.
- Copy the
dbghelp.dllfile to the root directory of your Enshrouded installation. - Create a
modsdirectory in the root directory of your Enshrouded installation if it doesn't already exist. - Place the mods you want to use in the
modsdirectory. - For linux users, follow the additional instructions in the Linux Users section.
- (Optional) Modify the
eml.jsonconfiguration file.- Useful for enabling the console or export capabilities.
- Launch the game.
IMPORTANT: If you used EML before with the dbghelp.dll proxy, make sure to remove it from your game directory to avoid conflicts.
Linux Users
Using dbghelp-proxy with Wine/Proton or on Steam requires some additional setup.
For clients on Steam, change the launch options (Enshrouded > Properties > General > Launch Options) to:
WINEDLLOVERRIDES="dbghelp=native,builtin" %command%
For servers or standalone execution, run the game executable with:
WINEDLLOVERRIDES="dbghelp=native,builtin" wine path/to/enshrouded.exe
or with Proton:
WINEDLLOVERRIDES="dbghelp=native,builtin" proton run path/to/enshrouded.exe
Troubleshooting
If you encounter issues on the client, try dinput8.dll instead of dbghelp.dll.
eml.json Configuration
This file is created automatically when you launch the game for the first time with the proxy DLL.
enable_console(boolean, default: false): If set totrue, a console window will be opened alongside the game for debugging purposes.use_export_flag(boolean, default: false): If set totrue, theexportcapability will be enabled when launching the game. This is useful if you want to let mods export stuff when using the proxy DLL.export_directory(string, default: "export"): The directory where exported files will be saved. This path is relative to the game directory.
Using the CLI
This method allows you to load mods using the command line interface.
Additionally, it can also be used to run mods with the export capability.
Prerequisites
- Ensure you have a legal copy of the game Enshrouded installed on your system.
- Download the latest version of the emm.exe binary.
- A terminal or command prompt of your choice.
- Basic knowledge of command line usage.
Loading Mods
To load mods using the CLI, follow these steps:
-
Open a terminal or command prompt.
-
Navigate to the directory where you extracted the
emm.exebinary (or put it in your PATH) -
Run the following command to run the mods:
emm.exe run -g <game-dir> [OPTIONS]By default, this will only validate the mods and not actually run them. To actually run the mods, you need to pass feature flags for the capabilities you want to enable.
For example, to enable the
patchandexportcapabilities, you would run:emm.exe run -g <game-dir> --export --patch -
Launch the game.
Development Guide
Welcome to the EML Guide! This guide is designed to help you understand how to create mods with the latest version of EML.
This guide assumes you have a basic understanding of Lua programming.
Setup
This document provides instructions on how to set up a modding environment for the game Enshrouded using the Enshrouded Mod Loader (EML).
Prerequisites
- Ensure you have a legal copy of the game Enshrouded installed on your system.
- Download the latest version of the Enshrouded Mod Manager CLI from here.
- A text editor or IDE of your choice, preferably with lua_ls (e.g., Visual Studio Code).
- Basic knowledge of Lua programming.
Creating a Mod
The mod manager cli can be used to create a new mod template.
emm.exe create -g <game-dir>
You will be prompted to enter a few details about your mod which will be placed
within the mod.json manifest file.
After the command completes, a new directory named after your mod id will be
created in the mods directory of your game installation.
It will also generate definition files for the Lua language server to provide
autocompletion and type checking. They are located in <game-dir>/.cache/lua.
Mod Structure
<mod-id>/
├── mod.json
├── README.md
├── .luarc.json
└── src/
└── mod.lua
mod.json: The manifest file containing metadata about your mod.README.md: A markdown file where you can provide a detailed description of your mod..luarc.json: Configuration file for the Lua language server. If you don't use lua_ls or use another language server, you can safely delete this file.src/mod.lua: The main Lua script file where you will write your mod's code
Mod Manifest
The mod.json file contains metadata about your mod and is essential for EML to
recognize and load it. Below is a detailed explanation of each field in the manifest:
-
id(string, required): A unique identifier for your mod. The id may only contain lowercase letters, numbers, hyphens (-), and underscores (_). It must start with a letter. -
name(string, required): The display name of your mod. This is what users will see in the mod manager. -
version(string, required): The version of your mod, following semantic versioning. -
capabilities(string list, required): A list of capabilities that your mod requires. Valid capabilities include:patch(lua): Allows the mod to patch game data.export(lua): Allows the mod to export arbitrary data into an export directory.runtime(WIP): Allows the mod to run code at runtime.
-
description(string, optional): A brief description of what your mod does. A detailed description can be provided in a separateREADME.mdfile. -
authors(string list, optional): A list of authors who contributed to the mod. -
license(string, optional): The license under which your mod is released (e.g., MIT, GPL-3.0). You may also include aLICENSEfile in your mod directory. -
icon(string, optional): Path to an icon file (e.g.,icon.png) that represents your mod. This icon will be displayed in the mod manager. -
dependencies(WIP): A list of other mods that your mod depends on.
Writing Your Mod
The main script file for your mod is located at src/mod.lua. This is the
entry point for your mod's code.
Refer to the definition files in <game-dir>/.cache/lua for available
functions and types provided by EML and the game or check the examples
in the examples directory of the repository.
Getting Started
Before you begin, make sure to setup your development environment as described here.
This guide only covers the patch and export capabilities. The runtime capability is still a work in progress and cannot be used yet.
Hello World
Now that you have your environment set up, you should see a mod.lua file in the src directory of your mod.
By default it contains a simple Hello World example like this:
print("Hello from the default mod!")
To run your mod refer to the usage guide.
As soon as you run EML either via the CLI or the proxy DLL, you should see the message in the console output.
To see a console window when using the proxy DLL, you need to enable it in the eml.json configuration file as described here.
Definition Files
As soon as you run EML for the first time, it will generate definition files for the Lua language server to provide autocompletion and type checking, but they may also be used to just lookup available functions and types (especially for the game data structures). When either the game or EML is updated, it will regenerate the definition files on the next run to ensure they are up to date.
These files are located in the .cache/lua directory of your game installation and there are currently two sets of definition files:
base.lua: Contains definitions (including documentation) for the EML API.types.lua: Contains auto generated type definitions for game data structures.
If you generated a new mod using the CLI, it will also create a .luarc.json file in your mod directory to configure the Lua language server to use these definition files.
If you want to develop your mod in a different directory, make sure to adjust the paths in the .luarc.json file accordingly or else you won't get any autocompletion or type checking.
EML API Structure
The EML API is divided into several globally accessable modules.
The most important ones are:
game: The main module to access and modify game data and assets.loader: Used to access information about the mod loader and loaded mods. For example, you can check which mods are currently loaded or what capabilities are enabled.buffer: Provides a buffer type to work with binary data. Since content assets are stored in an abitrary binary format, this module is essential for reading and modifying them. There are also some modules (more coming soon) that do the parsing for you, like theimagemodule for reading and writing images.
Helpful modules when working with game assets:
image: A module for reading and writing images in various formats. It provides an image type that can be used to manipulate image data. You can convert game textures to PNG (or some other format) and vice versa.integer: Provides functions for integers of various sizes. There are a lot of game data structures that use fixed size integers (e.g.,u8,u16, etc.) and this module provides functions to work with them more comfortably. For example, adding twou8values together while ensuring the result is still au8. If you're unfamiliar with these types, see the Type Aliases section below.hasher: Provides functions to compute hashes using various algorithms that are used by the game. This is mainly useful when working with game assets, as they are often identified by their hash values. There is also thegame.guid.hashfunction which computes the hash of a GUID.
Other useful modules include:
io: Provides functions for file system operations. This is especially useful when using theexportcapability to save data to the export directory. All operations are confined to either theexportdirectory or the mod's own directory to prevent mods from accessing arbitrary files on the user's system.
For more detailed information about the available modules and their functions, refer to the API Reference.
Type Aliases
If you have already looked at the definition files, you may have noticed that there are a lot of these u8, i32, f32, etc. types.
These are type aliases for fixed size integers and floating point numbers.
You should always make sure that the values match the boundaries of the specified type, or else it will raise an error.
For example, a u8 can only hold values from 0 to 255, so trying to assign a value of 300 to a u8 variable will result in an error.
The naming convention for these integer types is as follows:
u: Unsigned integer followed by the number of bits (e.g.,u8,u16,u32,u64).i: Signed integer followed by the number of bits (e.g.,i8,i16,i32,i64).f: Floating point number followed by the number of bits (e.g.,f16,f32,f64).
Accessing Game Data
To access game data, you will primarily use the game.assets module.
But before we dive into the details, let's first understand how game data is organized.
Game Data
There are two distinct types of game data:
-
Resources: These are rather small data files that contain typed binary data. They are referenced by a GUID, a qualified type name and a part index. The part index of a resource is used to distinguish between multiple resources with the same guid and type that belong together. The way how part indices are used is specific to the type of resource. For example, the voxel chunks of a scene are made up of multiple parts where all have the same type and guid, but have different part indices that specify their position.
They are provided as lua table-like userdata objects. The structure of these objects is defined in the
types.luadefinition file. -
Content Assets: These are blobs that contain arbitrary binary data like images, audio, models, voxels, etc. They are always referenced by resources via a field with the
keen::ContentHashtype. Content assets are not parsed by EML, but instead provided as raw binary data as aBufferobject. There are also some helper modules like theimagemodule to work with specific content types.
Accessing Resources
To access resources, you can use one of the game.assets.get_resource* functions.
If you know the exact guid, type and part index of a resource, you can use the game.assets.get_resource function like this:
local game_scene = game.assets.get_resource("509feadb-4c60-425f-9c7c-deeefd9b6920", "keen::SceneResource", 0)
print("Guid: " .. game_scene.guid)
print("Type: " .. game_scene.type)
print("Part Index: " .. game_scene.part_index)
print("Data: " .. tostring(game_scene.data))
As you can see, it doesn't return the actual data directly, but instead a Resource object that contains some metadata about the resource (like its guid, type, part index, etc.) and the actual data is stored in the data field of the resource object.
Note: The Resource object can be assigned everywhere where a Guid or ObjectReference is expected, so you can pass it directly to functions and fields that expect these types without having to extract the guid manually.
But there are also cases where you don't know the exact guid or type of a resource or are just unsure if it changes between game versions.
In these cases, you can use the either game.assets.get_resources_by_type or game.assets.get_all_resources functions to get a list of resources that match certain criteria.
As an example, to get all resources of a certain type, you can use the game.assets.get_resources_by_type function like this:
local items = game.assets.get_resources_by_type("keen::ItemInfo")
for _, item in ipairs(items) do
local item_data = item.data -- Access the actual data of the resource
print(item_data.itemId.value, item_data.debugName)
end
And if you don't know what types of resources are available, you can use the game.assets.get_resource_types function to get a list of all available resource types.
It is primarily useful for exploration and debugging purposes.
local resource_types = game.assets.get_resource_types()
for _, resource_type in ipairs(resource_types) do
print(resource_type)
end
As a last resort, there is also the game.assets.get_all_resources function that returns all resources in the game.
It is not recommended to use this function to filter resources by type or guid, as it is less efficient than using the other functions.
Modifying Resources
To modify game data, you can simply change the fields of the resource's data object or the data field itself to overwrite the entire resource.
Since resources are provided as lua table-like userdata objects, you can access and modify their fields just like you would with a regular lua table.
Modifications to resources are checked for validity, so make sure to only assign valid values to fields.
If you try to assign an invalid value, it will raise an error.
Important: If you assign a reference value (i.e., a table or a userdata) to a field, it will create a deep copy of the value. So subsequent modifications to the original value will not affect the resource's data.
Creating a Simple Patch
Now that you have a basic understanding of how to set up your environment and run your mod, let's create a simple patch that modifies some game data.
Open the mod.lua file in your mod's src directory and replace the existing code with the following:
---@type keen.BalancingTable
local BalancingTable = game.assets.get_resources_by_type("keen::BalancingTable")[1].data
BalancingTable.playerBaseStamina = 500
The first line (the ---@type annotation) is a type hint that tells the Lua language server what type the BalancingTable variable has.
This is not strictly necessary, but it helps with autocompletion and type checking.
The second line retrieves the first resource of type keen::BalancingTable and accesses its data field to get the actual balancing table data.
And in the last line, we modify the playerBaseStamina field to set the player's base stamina to 500.
Exporting Data
To export data from the game, you'll have to use the export capability.
Make sure to enable it in your eml.json configuration file if you're using the proxy DLL.
Now, let's modify our mod.lua file to export the string representation of the balancing table to a file.
---@type keen.BalancingTable
local BalancingTable = game.assets.get_resources_by_type("keen::BalancingTable")[1].data
io.export("balancing_table.txt", tostring(BalancingTable))
When you run your mod now, it will create a file named balancing_table.txt in the export directory of your game installation (unless you changed it).
If you specify a subdirectory in the file name, it will create the necessary directories automatically.
You can also export a Buffer object directly to export its binary data.
Important: The export function can only write files to the export directory. Files that already exist will be overwritten without warning!
Next Steps
Now that you have created a simple patch, you start looking into all the resource types that are available. But most things have not been documented yet, so you'll need to figure stuff out by yourself what certain things do. The best way is to just try things out and see what happens.
When it comes to content assets, i highly suggest exporting the binary data with the export capability and inspecting it with external tools such as ImHex for binary analysis.
Be aware that this requires some knowledge about binary formats and reverse engineering, so it may not be suitable for everyone.
Check out the References section for stuff that has already been documented.
Everything that has not been covered in this guide yet is documented in the API Reference.
API
This section provides detailed information about the api provided by EML including code examples and usage guidelines.
There is also documentation available in the definition file that is created when running EML for the first time. In some cases, it may be more up-to-date than what is provided here.
Asset Management
Asset management in EML is currently accessible through the game.assets module.
It provides functions to access and manipulate game assets before the game starts.
Resource
These are rather small data files that contain typed binary data. They are referenced by a GUID, a qualified type name and a part index. The part index of a resource is used to distinguish between multiple resources with the same guid and type that belong together. The way how part indices are used is specific to the type of resource. For example, the voxel chunks of a scene are made up of multiple parts where all have the same type and guid, but have different part indices that specify their position.
They are provided as lua table-like userdata objects.
The structure of these objects is defined in the types.lua definition file.
Accessing Resources
To access resources, you can use one of the game.assets.get_resource* functions.
Each of these functions will return either a single or a list of Resource objects.
Check AssetManager in the base.lua definition file for more information about the individual functions.
Resource Object
Resource objects contain some metadata fields as well as the actual data of the resource stored in the data field.
When accessing the data field, it will return a typed userdata object depending on the type of the resource.
It acts like a regular lua table, but with some additional functionality.
When modifying the data of a resource (i.e., changing fields or adding/removing entries in arrays), these changes will be reflected in the resource itself.
It can also be assigned everywhere where a Guid or ObjectReference is expected, so you can pass it directly to functions and fields that expect these types without having to extract the guid manually.
Important: If you assign a reference value (i.e., a table or a userdata) to a field, it will create a deep copy of the value. So subsequent modifications to the original value will not affect the resource's data.
Creating Resources
You can create new resources using the game.assets.create_resource function.
There are two variants of this function. One for creating a new resource with a new guid, and one for creating additional parts for a given resource.
The first one takes two arguments:
value: A value that is compatible with the specified type.type: The qualified type name (or aTypeobject) of the resource to create.
It returns a new Resource object that contains the specified data with a newly generated guid and a part index of 0.
The second one takes two additional arguments:
guid: The guid of the resource to create a new part for.part_index: The part index of the new resource part.
As described in the documentation comment in the definition file, this function should be called after creating a new resource with the first variant to create additional parts of the same resource. If the resulting resource is not unique (i.e., there is already a resource with the same guid, type and part index), it will raise an error.
Check AssetManager in the base.lua definition file for more information about these functions.
Content
These are blobs that contain arbitrary binary data like images, audio, models, voxels, etc.
They are always referenced by resources via a field with the keen::ContentHash type.
Content assets are not parsed by EML, but instead provided as raw binary data as a Buffer object.
There are also some helper modules like the image module to work with specific content types.
Accessing Content
To access content assets, you first need a keen::ContentHash value that references the content asset you want to access.
You can find these values in resource data structures.
But you can also just use the guid of a content asset if you know it.
Now, to actually get the content asset, you can use the game.assets.get_content function like this:
local content_hash = some_resource.data.textureHash -- Assume this is a keen::ContentHash value
local content_asset = game.assets.get_content(content_hash)
print("Guid: " .. content_asset.guid)
print("Size: " .. content_asset.size)
print("Data: " .. tostring(content_asset:read_data()))
This function returns a Content object that contains some metadata about the content asset (like its guid and size) and a method to read the actual binary data as a read-only Buffer object.
Note: The Content object can be assigned everywhere where a Guid or keen::ContentHash is expected, so you can pass it directly to functions and fields that expect these types without having to extract the guid manually.
Content Object
A content object contains some metadata about the content asset such as its guid and size but also a method to read the actual binary data.
Calling the read_data method will return a read-only Buffer object that contains the binary data of the content asset.
In comparison to resources, content assets are immutable since content hashes are unique, so the data for a given content hash is always the same and never changes. Because of that, you will have to instead create new content assets and update the old references.
Creating Content
To create new content assets, you can use the game.assets.create_content function.
This function takes a Buffer object containing the binary data of the content asset and returns a new Content object that can be assigned to resource fields.
local buf = ... -- Assume this is a Buffer object containing the binary data of the content asset
local content_asset = game.assets.create_content(buf)
print("Guid: " .. content_asset.guid)
print("Size: " .. content_asset.size)
Binary Format
Every content asset is stored differently depending on the context where it is used.
For example, image content are referenced in keen::UiTextureResource which has information about the format, size, etc. of the image data.
Without it, its very hard to interpret the raw binary data of a content asset.
Currently, EML does only provide the image module as a helper to work with image content assets.
That is, because most content formats have not been figured out just yet.
So you will have to reverse engineer the binary formats on your own if you want to work with other content types.
When doing so, i highly suggest exporting the binary data with the export capability and inspecting it with external tools such as ImHex for binary analysis.
Be aware that this requires some knowledge about binary formats and reverse engineering, so it may not be suitable for everyone.
Loader
The loader module provides information about the current environment in which the mod is currently running in.
is_client: A boolean value indicating whether the mod is running in a client environment.is_server: A boolean value indicating whether the mod is running in a server environment.features: See Features.has_mod(mod_id): Checks if a mod with the specified ID is loaded.
Features
The features table contains boolean flags indicating the availability of certain features in the current environment.
patch: Indicates whether data patching is supported.export: Indicates whether data exporting is supported.
Buffer
A Buffer is a (mutable or immutable) sequence of bytes for reading and writing binary data.
Buffers act like a FIFO (first-in, first-out) stream where data is read from the front and written to the back. To achieve this, it has two 0-based byte offsets:
- head: The position where data is read from (next byte to read).
- tail: The position where data is written to (next byte to write).
That means that bytes are read in the order they were written, unless you modify the head or tail positions manually.
Reading or writing bytes advances the respective offset by the number of bytes read or written. If head should ever exceeds tail, a read error is raised.
Additionally, buffers have a capacity which is the total allocated size of the buffer in bytes. It is automatically increased as needed when writing but the amount of growth may vary. It is not to be confused with the actual size; it is simply the amount of space the buffer can work with without needing to reallocate memory. The actual size is determined by the difference between tail and head.
Buffers are primarily used to work with raw binary data such as Content assets.
But they may also be used to work with embedded binary data in resources.
Creating Buffers
There are two ways to create new buffers:
buffer.create: Creates a new empty mutable buffer with an optional initial capacity.buffer.wrap: Creates a new mutable buffer from a given string.
local buf1 = buffer.create(128) -- Create an empty buffer with an initial capacity
local buf2 = buffer.wrap("Hello, World!") -- Create a buffer from a string
Reading and Writing Data
Buffers provide various methods to read and write different types of data.
Refer to the base.lua definition file for a complete list of available methods.
Here are some examples of reading and writing data:
local buf = buffer.create() -- Create a new empty buffer
buf:write_u8(42) -- Write a byte (0x2A)
buf:write_string("Hello") -- Write a string ("Hello")
assert(buf:tail() == 6) -- Tail is now at position 6
assert(buf:head() == 0) -- Head is still at position 0
local a = buf:read_u8() -- Read a byte (0x2A)
local str = buf:read_string(5) -- Read a string ("Hello")
assert(a == 42) -- a is 42
assert(str == "Hello") -- str is "Hello"
assert(buf:head() == 6) -- Head is now at position 6
assert(buf:tail() == 6) -- Tail is still at position 6
Reading and Writing raw Resources
In some cases, you may come across resources that are still in their raw binary format.
In such cases, you can use the read_resource and write_resource methods to read and write these resources directly from/to a buffer.
This is currently the case for translations which are resources stored as a content asset.
Image
The image module provides functions for encoding and decoding image data in various formats.
It supports common image formats such as PNG and JPEG, but also gpu image formats like R8G8B8A8_UNORM, BCn, etc.
Decoding Images
There are two separate functions to decode image data for different use cases:
image.decode: Decodes regular image data such as PNG or JPEG into anImageobject.image.decode_texture: Decodes image data in a GPU format into anImageobject.
Both functions take a Buffer object containing the binary image data as input and return an Image object representing the decoded image.
However decode_texture also requires the width, height, format and mip level of the image to decode it properly.
local png_buffer = io.read("image.png")
local png_image = image.decode(png_buffer)
local texture_buffer = io.read("texture.dat")
local texture_image = image.decode_texture(texture_buffer, 256, 256, "R8G8B8A8_UNORM")
Encoding Images
Similar to decoding, there are two separate functions to encode image data for different use cases:
image.encode: Encodes anImageobject into regular image formats such as PNG or JPEG.image.encode_texture: Encodes anImageobject into a GPU image format.
Both functions take an Image object as input and return a Buffer object containing the encoded binary image data.
Note: Encoding to GPU formats may be a slow process depending on the format and size of the image. Especially for block-compressed formats, it may take several seconds to encode a single image.
local image = ... -- Assume this is an Image object
local png_buffer = image.encode(image, "PNG")
io.export("output.png", png_buffer) -- Export the PNG image
local texture_buffer = image.encode_texture(image, "R8G8B8A8_UNORM")
Converting Images
With encoding and decoding functions available, you can easily convert images between different formats.
For example, to extract a texture from a resource and convert it to a PNG image, you can do the following:
local texture_resource = ... -- Assume this is a keen::UiTextureResource
local content = game.assets.get_content(texture_resource.data)
-- Decode the texture data into an Image object
local texture_image = image.decode_texture(
content:read_data(),
texture_resource.width,
texture_resource.height,
texture_resource.format
)
-- Encode the Image object into a PNG image
local png_buffer = image.encode(texture_image, "PNG")
io.export("texture.png", png_buffer) -- Export as PNG
This also works the other way around, so you can convert a PNG image into a GPU texture format:
local png_buffer = io.read("input.png")
local image = image.decode(png_buffer)
local texture_buffer = image.encode_texture(image, "R8G8B8A8_UNORM")
local texture_resource = ... -- Assume this is a keen::UiTextureResource
texture_resource.width = image.width
texture_resource.height = image.height
texture_resource.format = "R8G8B8A8_UNORM"
texture_resource.data = game.assets.create_content(texture_buffer)
Creating Images
You can also create new Image objects from scratch by using the image.create function.
It takes the width and height of the image and returns a new empty Image object where all pixels are initialized to transparent black.
local img = image.create(128, 128) -- Create a new 128x128 image
Image Object
An Image object represents a 2D image with pixel data stored in RGBA format.
It provides some basic methods to manipulate the image data, such as getting and setting pixel colors.
Check the Image definition in the base.lua definition file for a complete list of available methods and properties.
Types
The game.types module provides access to extracted type metadata from the game's binary.
You can use it to query information about types, their fields, inheritance relationships, etc.
Check TypeRegistry in the base.lua definition file for more information about the available fields and functions.
IO
The io module provides functions for reading and exporting files and data.
Note: All io operations (except io.export) are currently only supported for files within the mod's own directory.
Additionally, all operations are subject to io errors which may be raised if something goes wrong (e.g. file not found, permission denied, etc.).
Reading Files
You can use the io.read or io.read_to_string functions to read a file and get its contents as a Buffer object or a string respectively.
This can be useful to load configurations, binary data, or any other type of file your mod needs to work with.
local contents = io.read_to_string("path/to/file.txt")
-- Outputs the content of the file.
print(contents)
Note: Reading is currently only supported for files within the mod's own directory.
Exporting Files
You can use the io.export function to write data to a file relative to the specified export directory.
The export capability must be enabled for this function to work, otherwise an error will be raised.
-- Exports "Hello, World!" to "<export-dir>/greetings/hello.txt"
io.export("greetings/hello.txt", "Hello, World!")
File System
If you have multiple files or need to work with directories, you can use the following functions:
io.list_files: Lists all files in a given directory. (non-recursive)io.exists: Checks if a file or directory exists.io.is_file: Checks if a given path is a file.io.is_directory: Checks if a given path is a directory.
There are also a few helper functions to work with file paths:
io.name: Gets the name of a file (with extension) or directory from a given path.io.name_without_extension: Gets the name of a file without its extension from a given path.io.extension: Gets the extension of a file from a given path.io.parent: Gets the parent directory of a given path or nil if there is none.io.join: Joins multiple path segments into a single path.
Check the base.lua definition file for more information about these functions (including examples).
Utilities
This section covers smaller and niche modules that provide various utility functions for different purposes. These modules may not fit into the main categories but are still useful for mod development.
Guid Helper
The game.guid module provides utility functions to work with GUID strings.
hash: Computes a hash value from a GUID string.from_content_hash: Converts akeen::ContentHashobject to a GUID string.to_content_hash: Converts a GUID string to akeen::ContentHashobject.
Hasher
The hasher module provides functions to compute hash values for data.
Currently supported hash algorithms are:
fnv1a32: FNV-1a 32-bit hashcrc32: CRC-32/ISO-HDLC checksumcrc64: CRC-64/ECMA-182 checksum
Integer
The integer module provides functions for working with fixed-sized integers.
It covers all standard sizes from 8-bit to 64-bit, both signed and unsigned.
Each type has the same set of fields:
MAX: The maximum representable value for the integer type.MIN: The minimum representable value for the integer type.BITS: The number of bits used to represent the integer type.
Each type also has the following utility functions:
parse(string): Parses a string and returns the corresponding integer value. If the string is not a valid representation of the integer type, nil is returned.truncate(value): Truncates the bits of a given number to fit within the bounds of the integer type.clamp(value): Clamps a given number to fit within the bounds of the integer type. If the number is less thanMIN,MINis returned. If the number is greater thanMAX,MAXis returned.is_valid(value): Checks if a given number is within the bounds of the integer type.to_string(value): Converts a given integer value to its string representation.
Besides these utility functions, each type has a bunch of functions for various operations on the integer type.
I will only cover a few important things here, check the base.lua definition file for more information about all available functions.
Besides the regular arithmetic functions like add, sub, mul, div,
there is also a checked, saturating, wrapping, and overflowing variant for each arithmetic operation.
checked_*(a, b): Performs the operation and returns nil if an overflow occurs.saturating_*(a, b): Performs the operation and clamps the result to the bounds of the integer type if an overflow occurs.wrapping_*(a, b): Performs the operation and wraps around the result if an overflow. This is the default behavior for integer operations.overflowing_*(a, b): Performs the operation and returns a tuple containing the result and a boolean indicating whether an overflow occurred. If an overflow occurs, the result is wrapped around.
Resources
This section covers the various resource types available in the game.
For content format details, check the content section. It will be updated and expanded over time as more resources are documented.
Scene
A Scene is a container for all the game objects, lights, and other elements that make up an environment in the game.
Each scene is composed of multiple different objects with the same GUID (this list may not be complete):
keen::SceneResourcekeen::FogVoxelMappingResourcekeen::RenderModelChunkGridResourcekeen::RenderModelChunkModelResourcekeen::SceneCinematicListkeen::SceneEntityChunkResourcekeen::SceneRandomLootResourcekeen::VolumetricFog3ModelResourcekeen::VoxelTemperatureResourcekeen::VoxelWorldChunkResourcekeen::VoxelWorldFog3Resourcekeen::VoxelWorldResourcekeen::WaterChunkResource
keen::SceneResource
This object contains mostly static information about the scene, such as models, lights, bounds, and other elements. Check the type definition for more details.
Chunks
Note: This section is incomplete and may contain inaccurate information.
The scene contains several chunked resources that describe the terrain/fog voxels, water or entities. There are 4 chunked resources in total:
keen::VoxelWorldChunkResource: Contains the terrain and fog voxel data.keen::RenderModelChunkModelResource: Most likely contains the low-detail model for the terrain.keen::WaterChunkResource: Contains water data.keen::SceneEntityChunkResource: Contains entities placed in the scene.
Voxels
Each chunk of voxels is stored in a keen::VoxelWorldChunkResource object.
And for both the terrain and fog voxels, there is a separate keen::VoxelWorldResource object that contains:
type(terrain or fog): Indicates whether the voxel data represents terrain or fog.tileCount(x, z): The number ofkeen::VoxelWorldChunkResourcechunks in the x and z dimensions. There is no y dimension, as the height seems to be determined by the voxel data itself.origin(x, y, z): The position of the chunk grid's origin in world space.lowLODData: Most likely a lower-resolution representation of the voxel data for rendering at a distance.materialGuids(max 256): Most likely a list of material guids used for the voxels.
There are other fields, but their purpose is currently unknown. Check the type definition for more details.
To access the respective keen::VoxelWorldChunkResource objects, you will need to know the part indices of the keen::VoxelWorldChunkResource you want to access.
All keen::VoxelWorldChunkResource objects in the scene will have the same GUID,
where the part index corresponds to flattened chunk coordinates + amount of chunks of previous keen::VoxelWorldResources.
For example, if you have a terrain and fog voxel world with 2x2 chunks, the part indices for the terrain chunks will be 0, 1, 2, and 3, while the part indices for the fog chunks will be 4, 5, 6, and 7.
Now, to access the voxel data of a chunk, you will need to access the content that is referenced by the highLODData field in the keen::VoxelWorldChunkResource.
The binary format of the voxel data is currently unknown, but zeroing out the data will result in an empty chunk.
Water
The water data is stored in keen::WaterChunkResource objects.
Entities
The entities placed in the scene are stored in keen::SceneEntityChunkResource objects.
Each chunk contains a list of template references, models and entity spawns.
The amount of chunks is determined by the entityChunkCount (x, z) field in the keen::SceneResource.
Content
This section covers the various binary content formats used in the game. It will be updated and expanded over time as more content formats are documented.
Image
The game primarily uses gpu-ready packed/compressed texture formats for images.
A list of all supported formats can be found in the keen::PixelFormat enum in the type definitions.
All formats follow Vulkan's Format Spec
The most commonly used formats are:
R8G8B8A8_UNORM: 32-bit RGBA format for color textures with alpha (e.g. sprites, UI elements)BC7_SRGB_BLOCK: Compressed format for color textures with optional alpha (e.g. albedo/diffuse maps)BC5_UNORM_BLOCK: Compressed format for normal mapsBC1_RGB_UNORM_BLOCK: Compressed format for material parameters (e.g. roughness, metallic, ambient occlusion)BC4_UNORM_BLOCK: Compressed format for single-channel textures (e.g. masks)BC6H_UFLOAT_BLOCK: Compressed format for HDR textures (e.g. emissive maps)
Game File Documentation
The game consists of:
- One
.kfcfile - One
.kfc_resourcesfile - Multiple
.datfiles (containers). The number of.datfiles is always a power of two and they always need to be present even if empty.
There are two different kinds of assets:
- Resource: typed binary data (type metadata defined in the types section and bundled inside the executable).
Resources can be referenced by
ObjectReference<T>typed fields. They are stored in the.kfc_resourcesfile (after the header) and are 16-byte aligned. - Content: opaque blobs (images, audio, models, voxels, etc.).
Content assets are referenced by resources via a
keen::ContentHashtyped field. They are stored in the.datfiles and are 4096-byte aligned.
KFC File Format Specification
OUTDATED: This format is based on KFC2. The lastest game version uses KFC3 which is not documented yet.
Everything is little-endian unless otherwise noted.
The format of resources is defined in the resource section.
Header
| Name | Type | Size (bytes) | Description |
|---|---|---|---|
| magic | uint32 | 4 | File signature: 4B 46 43 32 ("KFC2") |
| size | uint32 | 4 | Size in bytes of the header area. |
| unknown | uint32 | 4 | Always 12 |
| padding | uint32 | 4 | Padding? Always 0 |
| version | KFCLocation | 8 | Points to uint8[count] containing the version string. |
| containers | KFCLocation | 8 | Points to ContainerInfo[count] describing .dat files. |
| unused0 | KFCLocation | 8 | Unused, always null location. |
| unused1 | KFCLocation | 8 | Unused, always null location. |
| resource_locations | KFCLocation | 8 | Points to ResourceLocation[count] describing where resources are stored within this file. |
| resource_indices | KFCLocation | 8 | Points to uint32[count] mapping ResourceBundleEntry::index to an index in resource_keys. See Resource Bundles. |
| content_buckets | KFCLocation | 8 | Points to StaticMapBucket[count] for content static map. |
| content_keys | KFCLocation | 8 | Points to ContentHash[count] for content static map. |
| content_values | KFCLocation | 8 | Points to ContentEntry[count] for content static map. |
| resource_buckets | KFCLocation | 8 | Points to StaticMapBucket[count] for resources static map. |
| resource_keys | KFCLocation | 8 | Points to ResourceId[count] for resources static map. |
| resource_values | KFCLocation | 8 | Points to ResourceEntry[count] for resources static map. |
| resource_bundle_buckets | KFCLocation | 8 | Points to StaticMapBucket[count] for resource bundles static map. |
| resource_bundle_keys | KFCLocation | 8 | Points to uint32[count] for resource bundles static map. (the internal hash of the resource type) |
| resource_bundle_values | KFCLocation | 8 | Points to ResourceBundleEntry[count] for resource bundles static map. |
Addtional notes:
versionis a non-null-terminated ASCII string.resource_indices.count==resource_keys.count==resource_values.count
KFCLocation
| Name | Type | Size (bytes) | Description |
|---|---|---|---|
| relative_offset | uint32 | 4 | The amount of bytes between the offset of this field and the start of the data it is pointing to. |
| count | uint32 | 4 | The number of entries of the type being pointed to. |
To get the absolute file offset of the data, add relative_offset to the file offset of the relative_offset field itself.
For example, if the relative_offset field is at file offset 0x20 and its value is 0x100, the data starts at file offset 0x120.
ContainerInfo
| Name | Type | Size (bytes) | Description |
|---|---|---|---|
| size | uint64 | 8 | Total size of the .dat container file in bytes. |
| count | uint64 | 8 | Number of contents in this container. |
While the size is a uint64, ContentEntry uses a uint32 for the offset and size of each content,
so no single content can be larger than 4 GiB.
ResourceLocation
| Name | Type | Size (bytes) | Description |
|---|---|---|---|
| offset | uint32 | 4 | Offset to where the resources start in this file. (absolute) |
| size | uint32 | 4 | Total size of all resources in bytes. |
| count | uint32 | 4 | Number of resources. |
There is currently always exactly one ResourceLocation entry.
It may work with multiple entries, but this has not been tested yet.
StaticMapBucket
| Name | Type | Size (bytes) | Description |
|---|---|---|---|
| index | uint32 | 4 | Start index into the map's key/value arrays. |
| count | uint32 | 4 | Number of entries in this bucket. (linear probe range) |
See Static Map for details.
ContentHash
A content hash is used to reference content assets within .dat files.
Here is how it is structured:
| Name | Type | Size (bytes) | Description |
|---|---|---|---|
| size | uint32 | 4 | The size of the content. |
| hash0 | uint32 | 4 | First part of the hash. |
| hash1 | uint32 | 4 | Second part of the hash. |
| hash2 | uint32 | 4 | Third part of the hash. |
The hash of the content is computed with a custom algorithm which produces a 128-bit hash. The first 4 bytes of the result is then replaced with the size of the content.
The size field is used for determining the size of the content and there is no other way to get it.
Since content with the same data have the same ContentHash, you don't need to store the same content multiple times.
ContentEntry
| Name | Type | Size (bytes) | Description |
|---|---|---|---|
| offset | uint32 | 4 | Offset inside the referenced .dat file where the content starts. |
| flags | uint16 | 2 | Currently unused, always 0. |
| container_index | uint16 | 2 | Index into the containers array. |
| padding | uint8[8] | 8 | Padding to make the struct 16 bytes long. Always 0. |
Content is always aligned to 4096 bytes inside the .dat files.
The size of the content is not stored here, but in the ContentHash.
ResourceId
A resource ID is used to reference resources within the .kfc file.
It is a completely unique identifier for each resource.
Here is how it is structured:
| Name | Type | Size (bytes) | Description |
|---|---|---|---|
| hash | uint32[4] | 16 | The hash of the resource. |
| type_hash | uint32 | 4 | The qualified hash of the resource type. |
| part_index | uint32 | 4 | The index of the part if there are multiple instances of this resource. |
| reserved0 | uint32 | 4 | Reserved, always 0. |
| reserved1 | uint32 | 4 | Reserved, always 0. |
The hash of the resource id is a randomly generated UUIDv4.
ResourceEntry
| Name | Type | Size (bytes) | Description |
|---|---|---|---|
| offset | uint32 | 4 | Offset inside the resource location where the resource starts. |
| size | uint32 | 4 | Size of the resource in bytes. |
The absolute offset of the resource can be calculated by adding the offset to the offset of the ResourceLocation.
ResourceBundleEntry
| Name | Type | Size (bytes) | Description |
|---|---|---|---|
| internal_hash | uint32 | 4 | The internal hash of the resource type. |
| index | uint32 | 4 | Index into the resource_keys array. |
| count | uint32 | 4 | Number of resources of this type. |
Static Map
Static maps consists of three arrays: buckets, keys and values.
There are always N buckets, where N is a power of two, and the key and value arrays must always have the same length.
Here is how to look up a key in a static map in pseudocode:
hash = hash_function(key) % buckets.count
bucket = buckets[hash]
for i in 0 until bucket.count:
idx = bucket.index + i
if keys[idx] == key:
return values[idx]
return not_found
Hash Functions
| Key Type | Hash Function |
|---|---|
| ContentHash | hash0 field of the ContentHash (precomputed) |
| ResourceId | A seeded FNV-1a32 over a 64-bit word formed from type_hash (low 32 bits) and part_index (high 32 bits) with hash[0] as the seed. |
| uint32 | The uint32 value itself. |
Note: The 64-bit word data = type_hash | (part_index << 32) is serialized in little-endian byte order and then fed, byte-by-byte, into FNV-1a32.
FNV-1a32
Here is the pseudocode for FNV-1a32 used by the game:
function fnv1a32(data: byte[]) -> uint32:
return fnv1a32_with_seed(data, 0x811C9DC5)
function fnv1a32_with_seed(data: byte[], seed: uint32) -> uint32:
hash = seed
for byte in data:
hash = hash XOR byte
hash = hash * 0x01000193
return hash
Resource Bundles
Resource bundles are a collection of resources of the same type. You can use them to efficiently look up all resources of a specific type.
Here is how to get all resources of a specific type in pseudocode:
bundle = resource_bundles.get(type_hash)
bundle_resources = []
for i in 0 until bundle.count:
idx = bundle.index + i
resource_index = resource_indices[idx]
resource_id = resource_keys[resource_index]
resource_entry = resource_values[resource_index]
bundle_resources.append((resource_id, resource_entry))
return bundle_resources
Resource Format Specification
This document describes the format of resources stored inside the .kfc_resources file.
Resources are typed binary objects. The concrete structure of types (fields, type hashes, field offsets, alignments, etc.) is defined in the types section.
General Notes
- Everything is little-endian unless otherwise noted.
- Make sure to zero out any padding bytes when serializing.
Primitive Types
| Name | Ordinal | Description |
|---|---|---|
none | 0x00 | No data |
bool | 0x01 | 1 byte, values: 0x00 (false), 0x01 (true) |
uint8 | 0x02 | 1 byte unsigned integer |
sint8 | 0x03 | 1 byte signed integer |
uint16 | 0x04 | 2 byte unsigned integer |
sint16 | 0x05 | 2 byte signed integer |
uint32 | 0x06 | 4 byte unsigned integer |
sint32 | 0x07 | 4 byte signed integer |
uint64 | 0x08 | 8 byte unsigned integer |
sint64 | 0x09 | 8 byte signed integer |
float32 | 0x0A | 4 byte IEEE 754 floating point number |
float64 | 0x0B | 8 byte IEEE 754 floating point number |
enum | 0x0C | stored using the enum's inner_type (see Enum) |
bitmask8 | 0x0D | 1 byte bitmask (up to 8 flags) |
bitmask16 | 0x0E | 2 byte bitmask (up to 16 flags) |
bitmask32 | 0x0F | 4 byte bitmask (up to 32 flags) |
bitmask64 | 0x10 | 8 byte bitmask (up to 64 flags) |
typedef | 0x11 | stored using the typedef's inner_type (see Typedef) |
struct | 0x12 | see Struct |
static_array | 0x13 | a fixed-size array (see Static Array) |
ds_array | 0x14 | unknown/not used |
ds_string | 0x15 | unknown/not used |
ds_optional | 0x16 | unknown/not used |
ds_variant | 0x17 | unknown/not used |
blob_array | 0x18 | a variable-size array (see Blob Array) |
blob_string | 0x19 | a variable-size string (see Blob String) |
blob_optional | 0x1A | an optional value (see Blob Optional) |
blob_variant | 0x1B | a variant of the base type (see Blob Variant) |
object_reference | 0x1C | 16 byte GUID referencing another resource |
guid | 0x1D | 16 byte ContentHash |
Enum
- An
enumis stored using itsinner_type(in the type metadata). - The
inner_typeis always a primitive integer type (uint8,sint8,uint16,sint16,uint32,sint32,uint64,sint64).
Struct
- A
structis a composite type consisting of multiple fields which is essentially a concatenation of its fields' serialized bytes. - If a struct inherits from a base struct (namely, has a
inner_type), the base struct's fields are serialized first (parents recursively up the chain). - Each field is serialized without its keys, just its value.
Note: Each field has a field_offset in the type metadata which can be used to locate the field's value inside the struct instead of recomputing padding/alignment yourself.
Typedef
- A
typedefis an alias for another type (theinner_type). - It can be resolved by simply serializing the
inner_typerecursively until a non-typedef type is reached.
Static Array
- A
static_arrayis a fixed-size array of elements of the same type. - The number of elements is
field_count(in the type metadata). - The element type is
inner_type(in the type metadata). - Elements are stored contiguously, directly inline.
Blob Array
- Out-of-line, data is stored as a blob.
- Layout:
- 4 byte
uint32relative offset (0 if empty) - 4 byte
uint32count (0 if empty)
- 4 byte
- The element type is
inner_type(in the type metadata). - Elements are stored contiguously, at the given blob offset.
- IMPORTANT: blob rules apply, see Blob Rules.
Blob String
- Out-of-line, data is stored as a blob (non-null-terminated).
- Layout:
- 4 byte
uint32relative offset (0 if empty) - 4 byte
uint32length in bytes (0 if empty)
- 4 byte
- Characters are stored as bytes at the given blob offset.
- IMPORTANT: blob rules apply, see Blob Rules.
Blob Optional
- Out-of-line, data is stored as a blob.
- Layout:
- 4 byte
uint32relative offset (0 if null)
- 4 byte
- The inner type is
inner_type(in the type metadata). - The value is stored at the given blob offset if not null.
- IMPORTANT: blob rules apply, see Blob Rules.
Blob Variant
- Out-of-line, data is stored as a blob.
- Layout:
- 4 byte
uint32qualified type hash of the stored variant (0 if no variant is specified) - 4 byte
uint32relative offset (0 if no variant is specified) - 4 byte
uint32blob size in bytes (0 if no variant is specified)
- 4 byte
- The base type is
inner_type(in the type metadata). - IMPORTANT: blob rules apply, see Blob Rules.
Blob Rules
Blob types (blob_array, blob_string, blob_optional, blob_variant) are placed out-of-line after the fixed-size base struct.
They need to be properly spaced and aligned according to their type metadata.
Because of this, when serializing a blob value you must manage this process yourself. Feel free to implement this in a way that makes sense for you, but here is how the game seems to do it:
- Set a
blob_offsetto the size of the base struct. - Serialize everything in order. For each blob field, do the following:
- (BlobVariant only) Write the qualified type hash.
- Align
blob_offsetto the blob data's alignment specified by the type metadata for the blob's data. - Compute
relative_offset = blob_offset - stream.position, where:stream.positionis the absolute position of therelative_offsetfield itself. (i.e. the position where therelative_offsetwill be written)
- Write the
relative_offset. - Write the
count/length/sizefield if applicable. (blob_array,blob_string,blob_variant) - Then write the blob data at the current
blob_offset. - After writing the blob data, increment
blob_offsetby the size of the written blob data. - And finally align
blob_offsetagain to the blob data's alignment.
- Continue with the next field until all fields are serialized.
Types
WIP :(
kfc-parser (deprecated)
This project has been deprecated and will no longer receive additional features but will (for the foreseeable future) continue to receive bug fixes.
The kfc-parser is a command-line interface (CLI) for working with the .kfc format used by Enshrouded.
It allows users to unpack, repack, and restore game files, as well as disassemble and assemble impact programs.
Usage
Unpacking and Repacking
To unpack game files, use the unpack command.
kfc-parser.exe unpack -g <game-dir> -o <output-dir> [OPTIONS]
To repack unpacked files, use the repack command.
It will repack all .json files in the input directory which have a
qualified guid name (e.g. 82706b40-61b1-4b8f-8b23-dcec6971bda1_9398e747_0.json).
The hash between the two underscores (9398e747 in this case) is used to determine the file type.
kfc-parser.exe repack -g <game-dir> -i <input-dir> [OPTIONS]
Restoring Original Game Files
To restore the original game files, use the restore command.
kfc-parser.exe restore -g <game-dir>
Impact CLI
The impact sub command can be used to convert an impact program into
a more manageable format and vice versa.
The disassemble command will convert an impact program into a .impact and .shutdown.impact
file which will contain the program's bytecode in text format and a .data.json file which will
contain the program's data such as variables, etc.
kfc-parser.exe impact disassemble -i <input-file-name>
To convert the disassembled files back into an impact program, use the assemble command.
The input-file-name should be the shared name of the disassembled files as follows:
<input-file-name>.impact<input-file-name>.shutdown.impact<input-file-name>.data.json
kfc-parser.exe impact assemble -i <input-file-name> [OPTIONS]
Extracting Reflection Data
To extract reflection data from the enshrouded executable, use the extract-types command.
Note: This is automatically executed when unpacking or repacking files.
kfc-parser.exe extract-types [OPTIONS]