From fc65d2de69355370312f5fac09d825e512b43169 Mon Sep 17 00:00:00 2001 From: slymscfr-tech Date: Sun, 28 Jun 2026 04:33:25 +0000 Subject: [PATCH 1/2] feat(LuaEngine): Add manifest-based script loading system (JSON, FiveM-inspired) Add a manifest-based script loading system that auto-detects a root manifest.json in the script folder. When found, modules are loaded in the order specified by the root manifest, and files within each module are loaded in the order specified by the module manifest. Key changes: - New ALEManifest.h/.cpp: JSON manifest parser using fkYAML - LuaEngine.cpp: auto-detect manifest mode in LoadScriptPaths(), new LoadScriptPathsFromManifest(), skip sort in RunScripts() - LuaEngine.h: add lua_manifest_mode flag and manifest loader decl - ALEFileWatcher.cpp: watch .json files for auto-reload - mod_ale.conf.dist: document auto-detected manifest mode - CMakeLists.txt: link fkYAML to modules target Behavior: - Auto-detected: if lua_scripts/manifest.json exists -> manifest mode - Retrocompatible: no manifest.json -> legacy recursive scanning - .ext files are NOT loaded in manifest mode --- conf/mod_ale.conf.dist | 26 +++ src/LuaEngine/ALEFileWatcher.cpp | 3 +- src/LuaEngine/ALEManifest.cpp | 280 +++++++++++++++++++++++++++++++ src/LuaEngine/ALEManifest.h | 80 +++++++++ src/LuaEngine/LuaEngine.cpp | 74 +++++++- src/LuaEngine/LuaEngine.h | 5 + 6 files changed, 462 insertions(+), 6 deletions(-) create mode 100644 src/LuaEngine/ALEManifest.cpp create mode 100644 src/LuaEngine/ALEManifest.h 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/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. From 35576c75b956f4d4b12168ba75f9cb89adc68079 Mon Sep 17 00:00:00 2001 From: slymscfr-tech Date: Sun, 28 Jun 2026 04:41:46 +0000 Subject: [PATCH 2/2] docs: Add manifest system documentation Add dedicated docs/MANIFEST.md covering the manifest-based script loading system. Update README.md, docs/USAGE.md, and docs/IMPL_DETAILS.md with references to the new manifest system documentation. --- README.md | 2 + docs/IMPL_DETAILS.md | 31 +++++ docs/MANIFEST.md | 292 +++++++++++++++++++++++++++++++++++++++++++ docs/USAGE.md | 32 +++++ 4 files changed, 357 insertions(+) create mode 100644 docs/MANIFEST.md 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/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* + +[![Discord](https://img.shields.io/badge/Discord-Join%20Us-7289DA?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/ZKSVREE7) +[![AzerothCore](https://img.shields.io/badge/AzerothCore-Integrated-darkgreen?style=for-the-badge)](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: