diff --git a/README.md b/README.md
index e0f56aa58e..af91b9be26 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,7 @@ ALE is a powerful, AzerothCore-specific implementation of a Lua scripting engine
### Key Features
- **Native AzerothCore Integration**: Built specifically for AzerothCore's architecture
- **Enhanced API**: Extended functionality for AzerothCore, beyond the original Eluna specification.
+- **Manifest-Based Loading**: FiveM-inspired manifest system for explicit control over module and file load order (see [Manifest System](docs/MANIFEST.md))
- **Community-Driven Development**: Actively maintained with community contributions
## ⚠️ Compatibility Notice
@@ -83,6 +84,7 @@ make -j$(nproc)
### Getting Started
- **[Installation Guide](docs/INSTALL.md)** - Complete installation and setup instructions
- **[Usage Guide](docs/USAGE.md)** - Learn how to write your first Lua scripts
+- **[Manifest System](docs/MANIFEST.md)** - Manifest-based script loading with explicit load order control
- **[Implementation Details](docs/IMPL_DETAILS.md)** - Advanced features and technical details
### Advanced Topics
diff --git a/conf/mod_ale.conf.dist b/conf/mod_ale.conf.dist
index 3eca4cc50d..7b81ab3cf0 100644
--- a/conf/mod_ale.conf.dist
+++ b/conf/mod_ale.conf.dist
@@ -70,6 +70,32 @@ ALE.AutoReload = false
ALE.AutoReloadInterval = 1
ALE.BytecodeCache = true
+###################################################################################################
+# MANIFEST MODE (auto-detected)
+#
+# When a manifest.json file is found in the script folder (lua_scripts/),
+# ALE automatically switches to manifest-based loading (inspired by FiveM).
+# No config option needed - just create the manifest file.
+#
+# In manifest mode:
+# - .ext extension files are NOT loaded
+# - Modules are loaded in the order specified by the root manifest
+# - Files within each module are loaded in the order specified by the module manifest
+# - If no manifest.json exists, legacy recursive scanning is used
+#
+# Root manifest (lua_scripts/manifest.json):
+# {
+# "modules": ["module_a", "module_b"]
+# }
+#
+# Module manifest (lua_scripts/module_a/manifest.json):
+# {
+# "files": ["init.lua", "handlers/player.lua"]
+# }
+#
+# If a module has no manifest.json, all supported files in that
+# directory are loaded recursively (legacy behavior within the module).
+
###################################################################################################
# LOGGING SYSTEM SETTINGS
#
diff --git a/docs/IMPL_DETAILS.md b/docs/IMPL_DETAILS.md
index 7c9dd02f6f..5af819edd1 100644
--- a/docs/IMPL_DETAILS.md
+++ b/docs/IMPL_DETAILS.md
@@ -17,6 +17,7 @@
- [Configuration](#-configuration)
- [Script Management](#-script-management)
+- [Manifest System](#-manifest-system)
- [Advanced Features](#-advanced-features)
- [Database Integration](#-database-integration)
- [Performance Tips](#-performance-tips)
@@ -73,6 +74,9 @@ Files with `.ext` extension load before standard `.lua` files:
> [!TIP]
> Instead of using `.ext`, prefer the standard Lua `require()` function for better maintainability.
+> [!NOTE]
+> In manifest mode, `.ext` files are **not loaded**. See [Manifest System](MANIFEST.md) for details.
+
#### Using Require
The entire script folder structure is added to Lua's require path:
@@ -87,6 +91,33 @@ require("helpers")
**Note:** Omit the `.lua` extension when using `require()`.
+## 📦 Manifest System
+
+ALE supports an optional manifest-based script loading system, inspired by FiveM's resource system. This is **auto-detected**: if a `manifest.json` file exists in your script folder, manifest mode is activated automatically.
+
+### Key Differences from Legacy Mode
+
+| Feature | Legacy Mode | Manifest Mode |
+|---------|-------------|---------------|
+| File discovery | Recursive scan | Only files listed in manifests |
+| Load order | Alphabetical by path | Order defined in manifests |
+| `.ext` files | Loaded first | Not loaded |
+| Detection | Always | Auto-detected via `manifest.json` |
+
+### How It Works
+
+1. A root `manifest.json` in the script folder lists modules to load (in order)
+2. Each module can have its own `manifest.json` listing files to load (in order)
+3. If a module has no manifest, its files are scanned recursively
+
+### Retrocompatibility
+
+- No `manifest.json` → legacy mode (fully backward compatible)
+- Gradual migration: add a root manifest first, then add module manifests as needed
+
+> [!TIP]
+> For complete documentation including examples, migration guide, and edge cases, see **[Manifest System](MANIFEST.md)**.
+
## 🎯 Advanced Features
### Automatic Type Conversion
diff --git a/docs/MANIFEST.md b/docs/MANIFEST.md
new file mode 100644
index 0000000000..d619d4bd2d
--- /dev/null
+++ b/docs/MANIFEST.md
@@ -0,0 +1,292 @@
+
+
+# 📦 ALE Manifest System
+
+*Manifest-based script loading inspired by FiveM's resource system*
+
+[](https://discord.com/invite/ZKSVREE7)
+[](http://www.azerothcore.org/)
+
+---
+
+
+> [!IMPORTANT]
+> The manifest system is **auto-detected**. If a `manifest.json` file exists in your script folder, ALE automatically switches to manifest mode. No config option needed.
+
+## 📋 Table of Contents
+
+- [Overview](#-overview)
+- [How It Works](#-how-it-works)
+- [Root Manifest](#-root-manifest)
+- [Module Manifest](#-module-manifest)
+- [Load Order](#-load-order)
+- [Retrocompatibility](#-retrocompatibility)
+- [Differences from Legacy Mode](#-differences-from-legacy-mode)
+- [Examples](#-examples)
+
+## 🚀 Overview
+
+ALE supports two script loading modes:
+
+| Mode | Trigger | Behavior |
+|------|---------|----------|
+| **Manifest mode** | `manifest.json` exists in script folder | Loads only modules and files specified in manifests, in the defined order |
+| **Legacy mode** | No `manifest.json` found | Recursively scans all folders, loads all `.lua`/`.ext`/`.moon` files, sorted alphabetically |
+
+The manifest system is inspired by [FiveM's `fxmanifest.lua`](https://docs.fivem.net/docs/scripting-reference/resource-manifest/) resource system. It gives you explicit control over:
+
+- **Which modules** to load (not everything in the folder)
+- **In what order** modules are loaded
+- **Which files** within each module to load
+- **In what order** files are loaded
+
+This solves a common problem: in legacy mode, file loading order is determined by alphabetical sorting of file paths, which means a file in `handlers/` loads before `init.lua` (because `h` < `i`). With manifests, you control the order explicitly.
+
+## ⚙️ How It Works
+
+```
+lua_scripts/
+├── manifest.json ← Root manifest (lists modules to load, in order)
+├── my_module/
+│ ├── manifest.json ← Module manifest (lists files to load, in order)
+│ ├── init.lua
+│ ├── utils/
+│ │ └── helpers.lua
+│ └── handlers/
+│ ├── player.lua
+│ └── creature.lua
+└── another_module/
+ ├── manifest.json
+ └── main.lua
+```
+
+1. ALE checks if `manifest.json` exists in the script folder (configured via `ALE.ScriptPath`, default: `lua_scripts`)
+2. If found, manifest mode is activated
+3. The root manifest is parsed to get the ordered list of modules
+4. For each module, its `manifest.json` is parsed to get the ordered list of files
+5. Files are loaded in the exact order specified
+
+## 📄 Root Manifest
+
+The root manifest (`lua_scripts/manifest.json`) lists the modules to load, in order.
+
+```json
+{
+ "modules": [
+ "my_module",
+ "another_module",
+ "subfolder/third_module"
+ ]
+}
+```
+
+### Fields
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `modules` | `string[]` | Yes | Ordered list of module directory names (relative to the script folder) |
+
+### Module Names
+
+- Module names are **relative paths** from the script folder
+- Nested paths are supported: `"utils/library"` refers to `lua_scripts/utils/library/`
+- Order matters: modules are loaded top-to-bottom
+
+## 📦 Module Manifest
+
+Each module can have its own `manifest.json` listing the files to load, in order.
+
+```json
+{
+ "files": [
+ "init.lua",
+ "utils/helpers.lua",
+ "handlers/player.lua",
+ "handlers/creature.lua"
+ ]
+}
+```
+
+### Fields
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `files` | `string[]` | Yes | Ordered list of file paths (relative to the module directory) |
+
+### File Paths
+
+- File paths are **relative to the module directory**
+- Nested paths are supported: `"handlers/player.lua"` refers to `my_module/handlers/player.lua`
+- Only the files listed are loaded - other files in the module directory are ignored
+- Order matters: files are loaded top-to-bottom
+
+### No Module Manifest (Fallback)
+
+If a module directory does **not** contain a `manifest.json`, ALE falls back to recursive scanning within that module. This means:
+
+- All supported files (`.lua`, `.moon`, `.out`) in the module and its subdirectories are loaded
+- Files are NOT sorted (they are loaded in directory iteration order)
+- This is useful for simple modules with a single file or when order doesn't matter
+
+> [!TIP]
+> For modules with dependencies between files (e.g., `init.lua` must load before handlers), always use a module manifest to guarantee load order.
+
+## 🔢 Load Order
+
+The load order is fully deterministic and controlled by the manifests:
+
+```
+Root manifest modules[0] → module manifest files[0], files[1], ...
+Root manifest modules[1] → module manifest files[0], files[1], ...
+...
+```
+
+### Example
+
+With this root manifest:
+```json
+{ "modules": ["combat", "quests"] }
+```
+
+And `combat/manifest.json`:
+```json
+{ "files": ["init.lua", "handlers/player.lua"] }
+```
+
+The load order is:
+1. `combat/init.lua`
+2. `combat/handlers/player.lua`
+3. `quests/...` (first file from quests module)
+
+### Why Load Order Matters
+
+In legacy mode, files are sorted alphabetically by full path. This means:
+
+| Legacy order | Manifest order |
+|---|---|
+| `combat/handlers/player.lua` (h) | `combat/init.lua` (1st) |
+| `combat/init.lua` (i) | `combat/handlers/player.lua` (2nd) |
+
+With legacy mode, `handlers/player.lua` loads **before** `init.lua`, which breaks if `player.lua` depends on variables set up by `init.lua`. The manifest system solves this.
+
+## 🔄 Retrocompatibility
+
+The manifest system is fully retrocompatible:
+
+- **No `manifest.json`** → Legacy mode is used (recursive scan, alphabetical sort, `.ext` files loaded)
+- **`manifest.json` present** → Manifest mode is used automatically
+- **Module without `manifest.json`** → That module falls back to recursive scanning
+
+You can migrate gradually:
+1. Start with legacy mode (no manifest)
+2. Create a root `manifest.json` listing your existing folders as modules
+3. Add module manifests one by one to control file order within each module
+
+## ⚠️ Differences from Legacy Mode
+
+| Feature | Legacy Mode | Manifest Mode |
+|---------|-------------|---------------|
+| File discovery | Recursive scan of all folders | Only files listed in manifests |
+| Load order | Alphabetical by file path | Order defined in manifests |
+| `.ext` files | Loaded (before `.lua` files) | **NOT loaded** |
+| `.lua` files | Loaded | Loaded |
+| `.moon` files | Loaded | Loaded |
+| `.out` files | Loaded | Loaded |
+| `.dll`/`.so` files | Loaded | Loaded |
+| Hidden files/dirs | Skipped | Skipped |
+| `require()` paths | All subdirectories added | Only module directories and file subdirectories added |
+| Auto-reload | Watches `.lua`/`.ext`/`.moon` | Watches `.lua`/`.ext`/`.moon`/`.json` |
+
+> [!WARNING]
+> If you are using `.ext` extension files and want to switch to manifest mode, convert them to regular `.lua` files and list them in your module manifest instead.
+
+## 📝 Examples
+
+### Single Module
+
+**`lua_scripts/manifest.json`:**
+```json
+{
+ "modules": ["my_scripts"]
+}
+```
+
+**`lua_scripts/my_scripts/manifest.json`:**
+```json
+{
+ "files": [
+ "config.lua",
+ "main.lua"
+ ]
+}
+```
+
+### Multiple Modules with Dependencies
+
+**`lua_scripts/manifest.json`:**
+```json
+{
+ "modules": [
+ "core_library",
+ "combat_system",
+ "quest_system",
+ "economy"
+ ]
+}
+```
+
+This ensures `core_library` loads first, then `combat_system`, etc.
+
+### Module with Nested Directories
+
+**`lua_scripts/combat_system/manifest.json`:**
+```json
+{
+ "files": [
+ "init.lua",
+ "utils/damage_calculator.lua",
+ "handlers/player_handler.lua",
+ "handlers/creature_handler.lua"
+ ]
+}
+```
+
+### Module Without Manifest (Fallback Scanning)
+
+If `lua_scripts/simple_scripts/` has no `manifest.json`, all supported files in that directory and its subdirectories will be loaded automatically.
+
+### Gradual Migration
+
+1. **Before** (legacy mode - everything loads automatically):
+```
+lua_scripts/
+├── lib/
+│ └── utils.lua
+├── combat/
+│ ├── init.lua
+│ └── handlers.lua
+└── quests/
+ └── main.lua
+```
+
+2. **Step 1** - Add root manifest (modules load in order, but files within each module still scan recursively):
+```json
+{
+ "modules": ["lib", "combat", "quests"]
+}
+```
+
+3. **Step 2** - Add module manifest to `combat/` to control file order:
+```json
+{
+ "files": ["init.lua", "handlers.lua"]
+}
+```
+
+---
+
+
+Developed with ❤️ by the AzerothCore and ALE community
+
+[⬆ Back to Top](#-ale-manifest-system)
+
diff --git a/docs/USAGE.md b/docs/USAGE.md
index 8232bfd467..44a2e30367 100644
--- a/docs/USAGE.md
+++ b/docs/USAGE.md
@@ -19,6 +19,7 @@
- [Your First Script](#-your-first-script)
- [Lua Basics](#-lua-basics)
- [ALE Basics](#-ale-basics)
+- [Manifest Mode](#-manifest-mode)
- [Script Reloading](#-script-reloading)
- [Getting Help](#-getting-help)
@@ -231,6 +232,37 @@ local function OnChat(event, player, msg, type, lang)
end
```
+## 📦 Manifest Mode
+
+ALE supports a manifest-based script loading system, inspired by FiveM's resource system. When a `manifest.json` file is found in your script folder, ALE automatically switches to manifest mode.
+
+### Why Use Manifests?
+
+In legacy mode, files are loaded in alphabetical order by path. This can break dependencies - for example, `handlers/player.lua` loads before `init.lua` because `h` comes before `i` alphabetically.
+
+Manifest mode gives you explicit control over load order.
+
+### Quick Example
+
+**`lua_scripts/manifest.json`** (root manifest):
+```json
+{
+ "modules": ["my_module"]
+}
+```
+
+**`lua_scripts/my_module/manifest.json`** (module manifest):
+```json
+{
+ "files": ["init.lua", "handlers/player.lua"]
+}
+```
+
+This ensures `init.lua` loads before `handlers/player.lua`.
+
+> [!TIP]
+> For the complete manifest system documentation including retrocompatibility, fallback behavior, and migration guide, see **[Manifest System](MANIFEST.md)**.
+
## 🔄 Script Reloading
For quick testing during development, you can reload scripts without restarting:
diff --git a/src/LuaEngine/ALEFileWatcher.cpp b/src/LuaEngine/ALEFileWatcher.cpp
index aa3cd37a36..ff12c7502a 100644
--- a/src/LuaEngine/ALEFileWatcher.cpp
+++ b/src/LuaEngine/ALEFileWatcher.cpp
@@ -78,7 +78,8 @@ void ALEFileWatcher::WatchLoop()
bool ALEFileWatcher::IsWatchedFileType(const std::string& filename) {
return (filename.length() >= 4 && filename.substr(filename.length() - 4) == ".lua") ||
(filename.length() >= 4 && filename.substr(filename.length() - 4) == ".ext") ||
- (filename.length() >= 5 && filename.substr(filename.length() - 5) == ".moon");
+ (filename.length() >= 5 && filename.substr(filename.length() - 5) == ".moon") ||
+ (filename.length() >= 5 && filename.substr(filename.length() - 5) == ".json");
}
void ALEFileWatcher::ScanDirectory(const std::string& path)
diff --git a/src/LuaEngine/ALEManifest.cpp b/src/LuaEngine/ALEManifest.cpp
new file mode 100644
index 0000000000..53028fbc45
--- /dev/null
+++ b/src/LuaEngine/ALEManifest.cpp
@@ -0,0 +1,280 @@
+/*
+* Copyright (C) 2010 - 2025 Eluna Lua Engine
+* This program is free software licensed under GPL version 3
+* Please see the included DOCS/LICENSE.md for more information
+*/
+
+#include "ALEManifest.h"
+#include "ALEUtility.h"
+
+#include
+
+#include
+#include
+#include
+
+namespace fs = boost::filesystem;
+
+bool ALEManifest::HasRootManifest(std::string const& scriptPath)
+{
+ fs::path manifestPath = fs::path(scriptPath) / "manifest.json";
+ return fs::exists(manifestPath) && fs::is_regular_file(manifestPath);
+}
+
+std::vector ALEManifest::LoadRootManifest(
+ std::string const& scriptPath,
+ std::string& requirePath,
+ std::string& requireCPath)
+{
+ std::vector modules;
+
+ fs::path manifestFile = fs::path(scriptPath) / "manifest.json";
+
+ FILE* file = std::fopen(manifestFile.string().c_str(), "r");
+ if (!file)
+ {
+ ALE_LOG_ERROR("[ALE Manifest]: Failed to open root manifest `{}`", manifestFile.string());
+ return modules;
+ }
+
+ fkyaml::node root;
+ try
+ {
+ root = fkyaml::node::deserialize(file);
+ }
+ catch (const std::exception& e)
+ {
+ ALE_LOG_ERROR("[ALE Manifest]: Failed to parse root manifest: {}", e.what());
+ std::fclose(file);
+ return modules;
+ }
+
+ std::fclose(file);
+
+ if (!root.contains("modules"))
+ {
+ ALE_LOG_ERROR("[ALE Manifest]: Root manifest is missing `modules` array");
+ return modules;
+ }
+
+ fkyaml::node modulesNode = root["modules"];
+ if (!modulesNode.is_sequence())
+ {
+ ALE_LOG_ERROR("[ALE Manifest]: `modules` must be an array in root manifest");
+ return modules;
+ }
+
+ // Add root directory to require path
+ requirePath +=
+ scriptPath + "/?.lua;" +
+ scriptPath + "/?.moon;" +
+ scriptPath + "/?.out;";
+ requireCPath +=
+ scriptPath + "/?.dll;" +
+ scriptPath + "/?.so;";
+
+ for (auto const& moduleEntry : modulesNode.as_seq())
+ {
+ std::string moduleName = moduleEntry.get_value();
+
+ fs::path moduleDir = fs::path(scriptPath) / moduleName;
+
+ if (!fs::exists(moduleDir) || !fs::is_directory(moduleDir))
+ {
+ ALE_LOG_ERROR("[ALE Manifest]: Module directory `{}` not found, skipping", moduleDir.string());
+ continue;
+ }
+
+ std::string modulePath = moduleDir.generic_string();
+
+ ALE_LOG_INFO("[ALE Manifest]: Loading module `{}`", moduleName);
+
+ ModuleManifest mod;
+ mod.name = moduleName;
+ mod.fullPath = modulePath;
+ mod.scripts = LoadModuleManifest(modulePath, moduleName, requirePath, requireCPath);
+
+ modules.push_back(std::move(mod));
+ }
+
+ ALE_LOG_INFO("[ALE Manifest]: Loaded {} module(s) from root manifest", modules.size());
+ return modules;
+}
+
+std::vector ALEManifest::LoadModuleManifest(
+ std::string const& modulePath,
+ std::string const& moduleName,
+ std::string& requirePath,
+ std::string& requireCPath)
+{
+ std::vector scripts;
+
+ fs::path moduleManifestFile = fs::path(modulePath) / "manifest.json";
+
+ // Add module directory to require path
+ requirePath +=
+ modulePath + "/?.lua;" +
+ modulePath + "/?.moon;" +
+ modulePath + "/?.out;";
+ requireCPath +=
+ modulePath + "/?.dll;" +
+ modulePath + "/?.so;";
+
+ if (!fs::exists(moduleManifestFile) || !fs::is_regular_file(moduleManifestFile))
+ {
+ // No module manifest — load all supported files from this directory recursively (legacy behavior within module)
+ ALE_LOG_DEBUG("[ALE Manifest]: No manifest.json in module `{}`, scanning directory recursively", moduleName);
+
+ // Recursively scan the module directory for supported files
+ std::function scanDir = [&](fs::path const& dir)
+ {
+ fs::directory_iterator endIter;
+ for (fs::directory_iterator it(dir); it != endIter; ++it)
+ {
+ std::string fullpath = it->path().generic_string();
+
+ // Skip hidden files/dirs
+ std::string name = it->path().filename().generic_string();
+ if (!name.empty() && name[0] == '.')
+ continue;
+
+ if (fs::is_directory(it->status()))
+ {
+ // Add subdirectory to require path
+ requirePath += fullpath + "/?.lua;" + fullpath + "/?.moon;" + fullpath + "/?.out;";
+ requireCPath += fullpath + "/?.dll;" + fullpath + "/?.so;";
+ scanDir(it->path());
+ }
+ else if (fs::is_regular_file(it->status()))
+ {
+ std::string filename = it->path().filename().generic_string();
+ std::size_t extDot = filename.find_last_of('.');
+ if (extDot == std::string::npos)
+ continue;
+
+ std::string ext = filename.substr(extDot);
+ if (IsSupportedExtension(ext))
+ scripts.push_back(BuildScriptEntry(filename, modulePath));
+ }
+ }
+ };
+
+ scanDir(fs::path(modulePath));
+ return scripts;
+ }
+
+ // Parse the module manifest
+ FILE* file = std::fopen(moduleManifestFile.string().c_str(), "r");
+ if (!file)
+ {
+ ALE_LOG_ERROR("[ALE Manifest]: Failed to open module manifest `{}`", moduleManifestFile.string());
+ return scripts;
+ }
+
+ fkyaml::node root;
+ try
+ {
+ root = fkyaml::node::deserialize(file);
+ }
+ catch (const std::exception& e)
+ {
+ ALE_LOG_ERROR("[ALE Manifest]: Failed to parse module manifest `{}`: {}", moduleManifestFile.string(), e.what());
+ std::fclose(file);
+ return scripts;
+ }
+
+ std::fclose(file);
+
+ if (!root.contains("files"))
+ {
+ ALE_LOG_ERROR("[ALE Manifest]: Module manifest `{}` is missing `files` array", moduleName);
+ return scripts;
+ }
+
+ fkyaml::node filesNode = root["files"];
+ if (!filesNode.is_sequence())
+ {
+ ALE_LOG_ERROR("[ALE Manifest]: `files` must be an array in module manifest `{}`", moduleName);
+ return scripts;
+ }
+
+ for (auto const& fileEntry : filesNode.as_seq())
+ {
+ std::string relativePath = fileEntry.get_value();
+
+ // Validate extension
+ std::size_t extDot = relativePath.find_last_of('.');
+ if (extDot == std::string::npos)
+ {
+ ALE_LOG_ERROR("[ALE Manifest]: File `{}` in module `{}` has no extension, skipping", relativePath, moduleName);
+ continue;
+ }
+
+ std::string ext = relativePath.substr(extDot);
+ if (!IsSupportedExtension(ext))
+ {
+ ALE_LOG_ERROR("[ALE Manifest]: File `{}` in module `{}` has unsupported extension `{}`, skipping", relativePath, moduleName, ext);
+ continue;
+ }
+
+ // Check that the file actually exists
+ fs::path fullPath = fs::path(modulePath) / relativePath;
+ if (!fs::exists(fullPath) || !fs::is_regular_file(fullPath))
+ {
+ ALE_LOG_ERROR("[ALE Manifest]: File `{}` not found in module `{}`, skipping", relativePath, moduleName);
+ continue;
+ }
+
+ // Add subdirectory paths to require path if the file is in a subdirectory
+ fs::path fileDir = fullPath.parent_path();
+ if (fileDir != fs::path(modulePath))
+ {
+ std::string dirPath = fileDir.generic_string();
+ requirePath += dirPath + "/?.lua;" + dirPath + "/?.moon;" + dirPath + "/?.out;";
+ requireCPath += dirPath + "/?.dll;" + dirPath + "/?.so;";
+ }
+
+ scripts.push_back(BuildScriptEntry(relativePath, modulePath));
+ }
+
+ ALE_LOG_DEBUG("[ALE Manifest]: Loaded {} file(s) from module `{}`", scripts.size(), moduleName);
+ return scripts;
+}
+
+LuaScript ALEManifest::BuildScriptEntry(
+ std::string const& relativePath,
+ std::string const& modulePath)
+{
+ LuaScript script;
+
+ // Extract filename and extension
+ fs::path filePath(relativePath);
+ std::string filename = filePath.filename().generic_string();
+
+ std::size_t extDot = filename.find_last_of('.');
+ if (extDot != std::string::npos)
+ {
+ script.fileext = filename.substr(extDot);
+ script.filename = filename.substr(0, extDot);
+ }
+ else
+ {
+ script.fileext = "";
+ script.filename = filename;
+ }
+
+ // Full absolute path
+ fs::path fullPath = fs::path(modulePath) / relativePath;
+ script.filepath = fullPath.generic_string();
+
+ // Module path (directory containing the file)
+ script.modulepath = fullPath.parent_path().generic_string();
+
+ return script;
+}
+
+bool ALEManifest::IsSupportedExtension(std::string const& ext)
+{
+ // In manifest mode, .ext files are NOT supported
+ return ext == ".lua" || ext == ".moon" || ext == ".dll" || ext == ".so" || ext == ".out";
+}
diff --git a/src/LuaEngine/ALEManifest.h b/src/LuaEngine/ALEManifest.h
new file mode 100644
index 0000000000..f80340bfec
--- /dev/null
+++ b/src/LuaEngine/ALEManifest.h
@@ -0,0 +1,80 @@
+/*
+* Copyright (C) 2010 - 2025 Eluna Lua Engine
+* This program is free software licensed under GPL version 3
+* Please see the included DOCS/LICENSE.md for more information
+*/
+
+#ifndef ALE_MANIFEST_H
+#define ALE_MANIFEST_H
+
+#include "LuaEngine.h"
+#include
+#include
+
+/*
+ * Manifest-based script loading system, inspired by FiveM's fxmanifest.lua.
+ *
+ * Root manifest (lua_scripts/manifest.json):
+ * {
+ * "modules": [
+ * "my_module",
+ * "another_module/subdir"
+ * ]
+ * }
+ *
+ * Module manifest (lua_scripts/my_module/manifest.json):
+ * {
+ * "files": [
+ * "init.lua",
+ * "handlers/player_handler.lua",
+ * "handlers/creature_handler.lua"
+ * ]
+ * }
+ *
+ * When manifest mode is enabled:
+ * - Only modules listed in the root manifest are loaded, in the specified order.
+ * - Only files listed in each module's manifest are loaded, in the specified order.
+ * - .ext extension files are NOT loaded (unlike legacy mode).
+ * - If no root manifest exists, falls back to legacy recursive scanning.
+ */
+
+struct ModuleManifest
+{
+ std::string name; // Module directory name (relative to lua_scripts/)
+ std::string fullPath; // Absolute path to the module directory
+ std::vector scripts; // Ordered list of scripts to load
+};
+
+class ALEManifest
+{
+public:
+ // Returns true if a root manifest exists at the given script path.
+ static bool HasRootManifest(std::string const& scriptPath);
+
+ // Loads the root manifest and all module manifests.
+ // Returns a vector of ModuleManifest in the order specified by the root manifest.
+ // Also populates requirePath/requireCPath for Lua's require() function.
+ static std::vector LoadRootManifest(
+ std::string const& scriptPath,
+ std::string& requirePath,
+ std::string& requireCPath);
+
+private:
+ // Parses a single module's manifest.json and returns the ordered list of scripts.
+ static std::vector LoadModuleManifest(
+ std::string const& modulePath,
+ std::string const& moduleName,
+ std::string& requirePath,
+ std::string& requireCPath);
+
+ // Builds a LuaScript entry from a file path relative to the module directory.
+ static LuaScript BuildScriptEntry(
+ std::string const& relativePath,
+ std::string const& modulePath);
+
+ // Checks if a file extension is supported in manifest mode.
+ // In manifest mode, .ext files are excluded.
+ static bool IsSupportedExtension(std::string const& ext);
+};
+
+#endif // ALE_MANIFEST_H
diff --git a/src/LuaEngine/LuaEngine.cpp b/src/LuaEngine/LuaEngine.cpp
index c647d689a5..b781c756f5 100644
--- a/src/LuaEngine/LuaEngine.cpp
+++ b/src/LuaEngine/LuaEngine.cpp
@@ -15,6 +15,7 @@
#include "ALEUtility.h"
#include "ALECreatureAI.h"
#include "ALEInstanceAI.h"
+#include "ALEManifest.h"
#if AC_PLATFORM == AC_PLATFORM_WINDOWS
#define ALE_WINDOWS
@@ -46,6 +47,7 @@ ALE::ScriptList ALE::lua_extensions;
std::string ALE::lua_folderpath;
std::string ALE::lua_requirepath;
std::string ALE::lua_requirecpath;
+bool ALE::lua_manifest_mode = false;
ALE* ALE::GALE = NULL;
bool ALE::reload = false;
bool ALE::initialized = false;
@@ -132,7 +134,19 @@ void ALE::LoadScriptPaths()
lua_requirepath.clear();
lua_requirecpath.clear();
- GetScripts(lua_folderpath);
+ // Auto-detect manifest mode: if a root manifest.json exists, use manifest-based loading.
+ // Otherwise, fall back to legacy recursive scanning.
+ if (ALEManifest::HasRootManifest(lua_folderpath))
+ {
+ lua_manifest_mode = true;
+ ALE_LOG_INFO("[ALE]: Manifest mode enabled - root manifest.json found");
+ LoadScriptPathsFromManifest();
+ }
+ else
+ {
+ lua_manifest_mode = false;
+ GetScripts(lua_folderpath);
+ }
// append our custom require paths and cpaths if the config variables are not empty
if (!lua_path_extra.empty())
@@ -151,6 +165,46 @@ void ALE::LoadScriptPaths()
ALE_LOG_DEBUG("[ALE]: Loaded {} scripts in {} ms", lua_scripts.size() + lua_extensions.size(), ALEUtil::GetTimeDiff(oldMSTime));
}
+void ALE::LoadScriptPathsFromManifest()
+{
+ uint32 oldMSTime = ALEUtil::GetCurrTime();
+
+ // Load the root manifest and all module manifests.
+ // The require path/cpath are built by the manifest loader as it processes modules.
+ std::vector modules = ALEManifest::LoadRootManifest(
+ lua_folderpath, lua_requirepath, lua_requirecpath);
+
+ // Populate lua_scripts in the order defined by the manifests.
+ // In manifest mode, .ext files are not loaded, so lua_extensions stays empty.
+ for (auto const& mod : modules)
+ {
+ for (auto const& script : mod.scripts)
+ {
+ lua_scripts.push_back(script);
+ }
+ }
+
+ // Append custom require paths from config
+ const std::string& lua_path_extra = static_cast(ALEConfig::GetInstance().GetRequirePath());
+ const std::string& lua_cpath_extra = static_cast(ALEConfig::GetInstance().GetRequireCPath());
+
+ if (!lua_path_extra.empty())
+ lua_requirepath += lua_path_extra;
+
+ if (!lua_cpath_extra.empty())
+ lua_requirecpath += lua_cpath_extra;
+
+ // Erase last ;
+ if (!lua_requirepath.empty())
+ lua_requirepath.erase(lua_requirepath.end() - 1);
+
+ if (!lua_requirecpath.empty())
+ lua_requirecpath.erase(lua_requirecpath.end() - 1);
+
+ ALE_LOG_INFO("[ALE]: Loaded {} scripts from {} module(s) in {} ms",
+ lua_scripts.size(), modules.size(), ALEUtil::GetTimeDiff(oldMSTime));
+}
+
void ALE::_ReloadALE()
{
LOCK_ALE;
@@ -686,10 +740,20 @@ void ALE::RunScripts()
ClearTimestampCache();
ScriptList scripts;
- lua_extensions.sort(ScriptPathComparator);
- lua_scripts.sort(ScriptPathComparator);
- scripts.insert(scripts.end(), lua_extensions.begin(), lua_extensions.end());
- scripts.insert(scripts.end(), lua_scripts.begin(), lua_scripts.end());
+ if (lua_manifest_mode)
+ {
+ // In manifest mode, scripts are already in the correct order - do NOT sort.
+ // Extensions are not loaded in manifest mode.
+ scripts.insert(scripts.end(), lua_scripts.begin(), lua_scripts.end());
+ }
+ else
+ {
+ // Legacy mode: sort alphabetically, extensions first.
+ lua_extensions.sort(ScriptPathComparator);
+ lua_scripts.sort(ScriptPathComparator);
+ scripts.insert(scripts.end(), lua_extensions.begin(), lua_extensions.end());
+ scripts.insert(scripts.end(), lua_scripts.begin(), lua_scripts.end());
+ }
std::unordered_map loaded; // filename, path
diff --git a/src/LuaEngine/LuaEngine.h b/src/LuaEngine/LuaEngine.h
index a21640d0e4..7d52b1cfb0 100644
--- a/src/LuaEngine/LuaEngine.h
+++ b/src/LuaEngine/LuaEngine.h
@@ -133,6 +133,11 @@ class ALE_GAME_API ALE
// lua path variable for require() function
static std::string lua_requirepath;
static std::string lua_requirecpath;
+ // Whether manifest mode is active (root manifest.json found)
+ static bool lua_manifest_mode;
+
+ // Manifest-based script loading
+ static void LoadScriptPathsFromManifest();
// A counter for lua event stacks that occur (see event_level).
// This is used to determine whether an object belongs to the current call stack or not.