Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Using the Enshrouded Mod Loader Proxy DLL (recommended)
  2. Using the Enshrouded Mod Manager CLI

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.dll proxy, make sure to remove it from your game directory to avoid conflicts.

Installation

  1. Extract the contents of the downloaded archive.
  2. Copy the dbghelp.dll file to the root directory of your Enshrouded installation.
  3. Create a mods directory in the root directory of your Enshrouded installation if it doesn't already exist.
  4. Place the mods you want to use in the mods directory.
  5. For linux users, follow the additional instructions in the Linux Users section.
  6. (Optional) Modify the eml.json configuration file.
    • Useful for enabling the console or export capabilities.
  7. 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 to true, a console window will be opened alongside the game for debugging purposes.
  • use_export_flag (boolean, default: false): If set to true, the export capability 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:

  1. Open a terminal or command prompt.

  2. Navigate to the directory where you extracted the emm.exe binary (or put it in your PATH)

  3. 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 patch and export capabilities, you would run:

    emm.exe run -g <game-dir> --export --patch
    
  4. 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 separate README.md file.

  • 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 a LICENSE file 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 the image module 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 two u8 values together while ensuring the result is still a u8. 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 the game.guid.hash function which computes the hash of a GUID.

Other useful modules include:

  • io: Provides functions for file system operations. This is especially useful when using the export capability to save data to the export directory. All operations are confined to either the export directory 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.lua definition 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::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 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 a Type object) 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 an Image object.
  • image.decode_texture: Decodes image data in a GPU format into an Image object.

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 an Image object into regular image formats such as PNG or JPEG.
  • image.encode_texture: Encodes an Image object 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 a keen::ContentHash object to a GUID string.
  • to_content_hash: Converts a GUID string to a keen::ContentHash object.

Hasher

The hasher module provides functions to compute hash values for data.

Currently supported hash algorithms are:

  • fnv1a32: FNV-1a 32-bit hash
  • crc32: CRC-32/ISO-HDLC checksum
  • crc64: 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 than MIN, MIN is returned. If the number is greater than MAX, MAX is 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::SceneResource
  • keen::FogVoxelMappingResource
  • keen::RenderModelChunkGridResource
  • keen::RenderModelChunkModelResource
  • keen::SceneCinematicList
  • keen::SceneEntityChunkResource
  • keen::SceneRandomLootResource
  • keen::VolumetricFog3ModelResource
  • keen::VoxelTemperatureResource
  • keen::VoxelWorldChunkResource
  • keen::VoxelWorldFog3Resource
  • keen::VoxelWorldResource
  • keen::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 of keen::VoxelWorldChunkResource chunks 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 maps
  • BC1_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 .kfc file
  • One .kfc_resources file
  • Multiple .dat files (containers). The number of .dat files 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_resources file (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::ContentHash typed field. They are stored in the .dat files 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.

NameTypeSize (bytes)Description
magicuint324File signature: 4B 46 43 32 ("KFC2")
sizeuint324Size in bytes of the header area.
unknownuint324Always 12
paddinguint324Padding? Always 0
versionKFCLocation8Points to uint8[count] containing the version string.
containersKFCLocation8Points to ContainerInfo[count] describing .dat files.
unused0KFCLocation8Unused, always null location.
unused1KFCLocation8Unused, always null location.
resource_locationsKFCLocation8Points to ResourceLocation[count] describing where resources are stored within this file.
resource_indicesKFCLocation8Points to uint32[count] mapping ResourceBundleEntry::index to an index in resource_keys. See Resource Bundles.
content_bucketsKFCLocation8Points to StaticMapBucket[count] for content static map.
content_keysKFCLocation8Points to ContentHash[count] for content static map.
content_valuesKFCLocation8Points to ContentEntry[count] for content static map.
resource_bucketsKFCLocation8Points to StaticMapBucket[count] for resources static map.
resource_keysKFCLocation8Points to ResourceId[count] for resources static map.
resource_valuesKFCLocation8Points to ResourceEntry[count] for resources static map.
resource_bundle_bucketsKFCLocation8Points to StaticMapBucket[count] for resource bundles static map.
resource_bundle_keysKFCLocation8Points to uint32[count] for resource bundles static map. (the internal hash of the resource type)
resource_bundle_valuesKFCLocation8Points to ResourceBundleEntry[count] for resource bundles static map.

Addtional notes:

  • version is a non-null-terminated ASCII string.
  • resource_indices.count == resource_keys.count == resource_values.count

KFCLocation

NameTypeSize (bytes)Description
relative_offsetuint324The amount of bytes between the offset of this field and the start of the data it is pointing to.
countuint324The 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

NameTypeSize (bytes)Description
sizeuint648Total size of the .dat container file in bytes.
countuint648Number 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

NameTypeSize (bytes)Description
offsetuint324Offset to where the resources start in this file. (absolute)
sizeuint324Total size of all resources in bytes.
countuint324Number of resources.

There is currently always exactly one ResourceLocation entry. It may work with multiple entries, but this has not been tested yet.

StaticMapBucket

NameTypeSize (bytes)Description
indexuint324Start index into the map's key/value arrays.
countuint324Number 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:

NameTypeSize (bytes)Description
sizeuint324The size of the content.
hash0uint324First part of the hash.
hash1uint324Second part of the hash.
hash2uint324Third 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

NameTypeSize (bytes)Description
offsetuint324Offset inside the referenced .dat file where the content starts.
flagsuint162Currently unused, always 0.
container_indexuint162Index into the containers array.
paddinguint8[8]8Padding 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:

NameTypeSize (bytes)Description
hashuint32[4]16The hash of the resource.
type_hashuint324The qualified hash of the resource type.
part_indexuint324The index of the part if there are multiple instances of this resource.
reserved0uint324Reserved, always 0.
reserved1uint324Reserved, always 0.

The hash of the resource id is a randomly generated UUIDv4.

ResourceEntry

NameTypeSize (bytes)Description
offsetuint324Offset inside the resource location where the resource starts.
sizeuint324Size 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

NameTypeSize (bytes)Description
internal_hashuint324The internal hash of the resource type.
indexuint324Index into the resource_keys array.
countuint324Number 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 TypeHash Function
ContentHashhash0 field of the ContentHash (precomputed)
ResourceIdA 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.
uint32The 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

See FNV hash on Wikipedia.

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

NameOrdinalDescription
none0x00No data
bool0x011 byte, values: 0x00 (false), 0x01 (true)
uint80x021 byte unsigned integer
sint80x031 byte signed integer
uint160x042 byte unsigned integer
sint160x052 byte signed integer
uint320x064 byte unsigned integer
sint320x074 byte signed integer
uint640x088 byte unsigned integer
sint640x098 byte signed integer
float320x0A4 byte IEEE 754 floating point number
float640x0B8 byte IEEE 754 floating point number
enum0x0Cstored using the enum's inner_type (see Enum)
bitmask80x0D1 byte bitmask (up to 8 flags)
bitmask160x0E2 byte bitmask (up to 16 flags)
bitmask320x0F4 byte bitmask (up to 32 flags)
bitmask640x108 byte bitmask (up to 64 flags)
typedef0x11stored using the typedef's inner_type (see Typedef)
struct0x12see Struct
static_array0x13a fixed-size array (see Static Array)
ds_array0x14unknown/not used
ds_string0x15unknown/not used
ds_optional0x16unknown/not used
ds_variant0x17unknown/not used
blob_array0x18a variable-size array (see Blob Array)
blob_string0x19a variable-size string (see Blob String)
blob_optional0x1Aan optional value (see Blob Optional)
blob_variant0x1Ba variant of the base type (see Blob Variant)
object_reference0x1C16 byte GUID referencing another resource
guid0x1D16 byte ContentHash

Enum

  • An enum is stored using its inner_type (in the type metadata).
  • The inner_type is always a primitive integer type (uint8, sint8, uint16, sint16, uint32, sint32, uint64, sint64).

Struct

  • A struct is 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 typedef is an alias for another type (the inner_type).
  • It can be resolved by simply serializing the inner_type recursively until a non-typedef type is reached.

Static Array

  • A static_array is 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 uint32 relative offset (0 if empty)
    • 4 byte uint32 count (0 if empty)
  • 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 uint32 relative offset (0 if empty)
    • 4 byte uint32 length in bytes (0 if empty)
  • 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 uint32 relative offset (0 if null)
  • 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 uint32 qualified type hash of the stored variant (0 if no variant is specified)
    • 4 byte uint32 relative offset (0 if no variant is specified)
    • 4 byte uint32 blob size in bytes (0 if no variant is specified)
  • 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:

  1. Set a blob_offset to the size of the base struct.
  2. Serialize everything in order. For each blob field, do the following:
    • (BlobVariant only) Write the qualified type hash.
    • Align blob_offset to the blob data's alignment specified by the type metadata for the blob's data.
    • Compute relative_offset = blob_offset - stream.position, where:
      • stream.position is the absolute position of the relative_offset field itself. (i.e. the position where the relative_offset will be written)
    • Write the relative_offset.
    • Write the count/length/size field 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_offset by the size of the written blob data.
    • And finally align blob_offset again to the blob data's alignment.
  3. 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]