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* + +[![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: 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.