Hello! Welcome to the final module of our course. In the last lesson, we built a powerful API that allows mods to define new conversation packs with custom logic using Lua functions. However, our setup was a bit rigid, as we had to manually load a single, hardcoded Lua file.
Today, we're going to build a proper modding system. The learning outcome for this lesson is to implement a mod loader that reads and executes Lua scripts from a mods folder on startup. This will transform our project from running a single script to automatically discovering and loading an entire ecosystem of mods, which is a cornerstone of the extensible game we aim to build.
1. A Scalable Mod Directory Structure
Before we write any code, we need to decide on a standard file structure for our mods. A clean, predictable structure makes it easy for both our loader to find the mods and for players to manage them.
A common and effective pattern is to give each mod its own folder inside a main mods directory. Within each mod's folder, a specific file, like main.lua or init.lua, serves as the entry point that our game will execute.
Here is an example of the structure we'll use:
game_root/
├── mods/
│ ├── friendly_chat_pack/
│ │ ├── main.lua
│ │ └── another_file.lua -- (optional, for larger mods)
│ └── simple_trait_mod/
│ └── main.lua
└── ... (your Rust project files)
This structure is organized and scalable. For a more advanced example of how complex games organize mods, including DLCs, consider this file layout:

Your Task:
- Create a
modsdirectory in the root of your project if you haven't already. - Inside
mods, create a new folder namedfriendly_chat_pack. - Move the code from your existing
conversations.luafile into a new file atmods/friendly_chat_pack/main.lua. - Create another folder inside
modscalledcore_tweaks. - Inside
core_tweaks, create amain.luafile with the following content:-- mods/core_tweaks/main.lua print('[Lua] core_tweaks mod is loading!') local observant_trait = { id = "observant", name = "Observant", description = "Notices the little things." } game.register_trait(observant_trait) print('[Lua] Registered new trait: observant')
2. Building the Mod Loader in Rust
Now, let's update our Rust code to automatically scan this new directory structure. We will modify the initialize_and_run_mods function to iterate through the mods directory, identify each mod folder, and execute its main.lua script.
We'll use Rust's standard library for file system operations, primarily std::fs.
Conceptually, loading a script from a file is a common task when embedding a scripting language. While this video uses C++, it provides a great overview of the fundamental principles. Pay attention to how it transitions from executing a hardcoded string to loading and running an entire .lua file.
Watch the section from 14:28 to 16:29. This part demonstrates loading a script file named 'videoexample.lua'. This is the core idea we are about to implement in Rust, but for multiple files found by scanning a directory.
The article "Embedding Lua in sqleibniz with Rust" provides a fantastic, modern Rust example of loading and executing a Lua configuration file. We will use its approach as a model for our mod loader.
Embedding Lua in sqleibniz with Rust - xnacly
This article demonstrates a robust way to load, execute, and parse a Lua file in Rust using mlua. We are interested in the function that performs the loading and execution.
Focus on the configuration function in the Rust code block under the 'Putting it all together' section. Notice how it uses fs::read_to_string to get the file content, and then lua.load(...).set_name(...).exec() to run it. The .set_name() part is especially useful as it provides better error messages that include the filename.
Now, let's implement this in src/modding.rs. We'll replace the hardcoded logic in initialize_and_run_mods.
src/modding.rs (Updated initialize_and_run_mods)
// At the top, make sure you have `use std::fs;`
pub fn initialize_and_run_mods() -> mlua::Result<()> {
// --- Part 1: Initialize Lua and Expose API ---
let lua = mlua::Lua::new();
let globals = lua.globals();
globals.set("game", game_module(&lua)?)?;
println!("[Rust] Lua environment initialized and 'game' API exposed.");
// --- Part 2: Scan and Load Mods ---
println!("[Rust] --- Starting Mod Scan ---");
let mods_dir = "./mods";
// Read the contents of the 'mods' directory
match fs::read_dir(mods_dir) {
Ok(entries) => {
for entry in entries {
let entry = entry.expect("Failed to read directory entry");
let path = entry.path();
// We only care about subdirectories
if path.is_dir() {
let mod_name = path.file_name().unwrap().to_str().unwrap();
let main_script_path = path.join("main.lua");
if main_script_path.exists() {
println!("[Rust] Found mod '{}', executing main.lua...", mod_name);
// Read the script content
let script_content = fs::read_to_string(&main_script_path)
.expect("Could not read main.lua");
// Execute the script
lua.load(&script_content)
.set_name(main_script_path.to_str().unwrap()) // For better error messages
.exec()?;
}
}
}
},
Err(e) => eprintln!("[Rust] WARN: Could not read mods directory '{}': {}", mods_dir, e),
}
println!("[Rust] --- Mod Scan Complete ---");
// --- Part 3: Verify Results (from previous lesson) ---
// We can keep this to test that our conversation pack loaded correctly.
check_interaction_availability(&lua)?;
Ok(())
}
If you run your project now, you should see the output from both mods! The mod loader successfully finds and executes both main.lua files.
3. Handling Multi-File Mods with require
Our loader works for simple mods, but what if a modder wants to split their code into multiple files for better organization?
For example, a mod might look like this:
mods/advanced_mod/main.luamods/advanced_mod/utils.lua
And main.lua might try to load utils.lua using require("utils").
If you try this, it will fail. By default, Lua doesn't know to look inside the mods/advanced_mod/ directory. It searches a predefined set of paths stored in a global variable called package.path.
To solve this, we must tell Lua where to find each mod's files. The solution is to dynamically add the mod's directory to package.path before we execute its main.lua.
Load Lua-files by relative path - Stack Overflow
This Stack Overflow discussion provides the exact solutions we need for managing relative paths in Lua.
Read the accepted answer by 'Seth Carnegie'. The key takeaway is the line package.path = './mylib/?.lua;' .. package.path. This shows how to prepend a new search path to Lua's existing package path. The ? is a placeholder for the module name.
Let's apply this fix. In our Rust loop, right before executing main.lua, we will execute a small Lua snippet to update package.path.
src/modding.rs (Updated loop inside initialize_and_run_mods)
// ... inside the if path.is_dir() block ...
if main_script_path.exists() {
println!("[Rust] Found mod '{}', executing main.lua...", mod_name);
// --- NEW: Set up package.path for this mod ---
if let Some(mod_path_str) = path.to_str() {
// We need to normalize path separators for Lua
let mod_lua_path = format!("{}/?.lua", mod_path_str.replace('\\', "/"));
let set_path_script = format!("package.path = '{}..' .. package.path", mod_lua_path);
lua.load(&set_path_script).exec()?;
}
// --- END NEW ---
// Read the script content
let script_content = fs::read_to_string(&main_script_path)
.expect("Could not read main.lua");
// Execute the script
lua.load(&script_content)
.set_name(main_script_path.to_str().unwrap())
.exec()?;
}
// ...
Your Task (to test this):
- Create a new mod folder
mods/path_test. - Create
mods/path_test/utils.lua:-- mods/path_test/utils.lua print('[Lua] utils.lua has been required!') local M = {} function M.get_message() return "It works!" end return M - Create
mods/path_test/main.lua:-- mods/path_test/main.lua print('[Lua] path_test mod is loading!') -- This will now work because of our package.path modification local utils = require("utils") print('[Lua] path_test message: ' .. utils.get_message())
Run your project one last time. You should now see the output from all three mods, including the successful require call in path_test. Your mod loader is now robust and ready for modders!
Conclusion
Congratulations! You have successfully built a dynamic mod loader. This is a massive step forward, turning a single-script system into a fully-fledged modding platform.
Key Takeaways:
- A standardized directory structure (e.g.,
mods/<mod_name>/main.lua) is essential for an automatic mod loader. - Rust's
std::fsmodule provides the tools (read_dir,path,is_dir) needed to scan directories and find mod files. - You can execute Lua code from a string using
lua.load(&code).exec(). Using.set_name()provides clearer error reporting. - To support multi-file mods that use
require, you must dynamically update Lua'spackage.pathto include each mod's directory before executing its entry script.
Preview of the Next Lesson:
Our mod loader is functional, but it's naive. What happens if both core_tweaks and another mod try to register a trait with the id = "observant"? The last one loaded would win, silently overwriting the first. In our next lesson, we will address this by implementing basic conflict detection for mods that register content with the same ID, making our modding system safer and more predictable.