From 6d1ff128a6f231389f6135336c8873450624b178 Mon Sep 17 00:00:00 2001
From: iThorgrim <125808072+iThorgrim@users.noreply.github.com>
Date: Fri, 29 Aug 2025 14:53:09 +0200
Subject: [PATCH] feat(ElunaFileWatcher): Add file watcher and autoreload
(#286)
---
conf/mod_eluna.conf.dist | 16 +++
src/LuaEngine/ElunaFileWatcher.cpp | 216 +++++++++++++++++++++++++++++
src/LuaEngine/ElunaFileWatcher.h | 43 ++++++
src/LuaEngine/LuaEngine.cpp | 16 +++
src/LuaEngine/LuaEngine.h | 2 +
5 files changed, 293 insertions(+)
create mode 100644 src/LuaEngine/ElunaFileWatcher.cpp
create mode 100644 src/LuaEngine/ElunaFileWatcher.h
diff --git a/conf/mod_eluna.conf.dist b/conf/mod_eluna.conf.dist
index dea70a7..2047d02 100644
--- a/conf/mod_eluna.conf.dist
+++ b/conf/mod_eluna.conf.dist
@@ -38,6 +38,20 @@
# "/usr/local/lib/lua/%s/?.so;/usr/lib/x86_64-linux-gnu/lua/%s/?.so;/usr/local/lib/lua/%s/loadall.so;"
# Default: ""
#
+# Eluna.AutoReload
+# Description: Enable or disable automatic reloading of Lua scripts when files are modified.
+# This feature watches the script directory for changes and automatically
+# triggers a reload when .lua files are added, modified, or deleted.
+# Useful for development but should be disabled in production environments.
+# Default: false - (disabled)
+# true - (enabled)
+#
+# Eluna.AutoReloadInterval
+# Description: Sets the interval in seconds between file system checks for auto-reload.
+# Lower values provide faster detection but use more CPU resources.
+# Higher values reduce CPU usage but increase detection delay.
+# Default: 1 - (check every 1 second)
+#
# Eluna.BytecodeCache
# Description: Enable or disable bytecode caching for improved performance.
# When enabled, Lua/MoonScript files are compiled to bytecode and cached in memory.
@@ -52,6 +66,8 @@ Eluna.ScriptPath = "lua_scripts"
Eluna.PlayerAnnounceReload = false
Eluna.RequirePaths = ""
Eluna.RequireCPaths = ""
+Eluna.AutoReload = false
+Eluna.AutoReloadInterval = 1
Eluna.BytecodeCache = true
###################################################################################################
diff --git a/src/LuaEngine/ElunaFileWatcher.cpp b/src/LuaEngine/ElunaFileWatcher.cpp
new file mode 100644
index 0000000..729f8ae
--- /dev/null
+++ b/src/LuaEngine/ElunaFileWatcher.cpp
@@ -0,0 +1,216 @@
+/*
+* Copyright (C) 2010 - 2016 Eluna Lua Engine
+* This program is free software licensed under GPL version 3
+* Please see the included DOCS/LICENSE.md for more information
+*/
+
+#include "ElunaFileWatcher.h"
+#include "LuaEngine.h"
+#include "ElunaUtility.h"
+#include
+
+ElunaFileWatcher::ElunaFileWatcher() : running(false), checkInterval(1)
+{
+}
+
+ElunaFileWatcher::~ElunaFileWatcher()
+{
+ StopWatching();
+}
+
+void ElunaFileWatcher::StartWatching(const std::string& scriptPath, uint32 intervalSeconds)
+{
+ if (running.load())
+ {
+ ELUNA_LOG_DEBUG("[ElunaFileWatcher]: Already watching files");
+ return;
+ }
+
+ if (scriptPath.empty())
+ {
+ ELUNA_LOG_ERROR("[ElunaFileWatcher]: Cannot start watching - script path is empty");
+ return;
+ }
+
+ watchPath = scriptPath;
+ checkInterval = intervalSeconds;
+ running.store(true);
+
+ ScanDirectory(watchPath);
+
+ watcherThread = std::thread(&ElunaFileWatcher::WatchLoop, this);
+
+ ELUNA_LOG_INFO("[ElunaFileWatcher]: Started watching '{}' (interval: {}s)", watchPath, checkInterval);
+}
+
+void ElunaFileWatcher::StopWatching()
+{
+ if (!running.load())
+ return;
+
+ running.store(false);
+
+ if (watcherThread.joinable())
+ watcherThread.join();
+
+ fileTimestamps.clear();
+
+ ELUNA_LOG_INFO("[ElunaFileWatcher]: Stopped watching files");
+}
+
+void ElunaFileWatcher::WatchLoop()
+{
+ while (running.load())
+ {
+ try
+ {
+ CheckForChanges();
+ }
+ catch (const std::exception& e)
+ {
+ ELUNA_LOG_ERROR("[ElunaFileWatcher]: Error during file watching: {}", e.what());
+ }
+
+ std::this_thread::sleep_for(std::chrono::seconds(checkInterval));
+ }
+}
+
+bool ElunaFileWatcher::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");
+}
+
+void ElunaFileWatcher::ScanDirectory(const std::string& path)
+{
+ try
+ {
+ boost::filesystem::path dir(path);
+
+ if (!boost::filesystem::exists(dir) || !boost::filesystem::is_directory(dir))
+ return;
+
+ boost::filesystem::directory_iterator end_iter;
+
+ for (boost::filesystem::directory_iterator dir_iter(dir); dir_iter != end_iter; ++dir_iter)
+ {
+ std::string fullpath = dir_iter->path().generic_string();
+
+ if (boost::filesystem::is_directory(dir_iter->status()))
+ {
+ ScanDirectory(fullpath);
+ }
+ else if (boost::filesystem::is_regular_file(dir_iter->status()))
+ {
+ std::string filename = dir_iter->path().filename().generic_string();
+
+ if (IsWatchedFileType(filename))
+ {
+ fileTimestamps[fullpath] = boost::filesystem::last_write_time(dir_iter->path());
+ }
+ }
+ }
+ }
+ catch (const std::exception& e)
+ {
+ ELUNA_LOG_ERROR("[ElunaFileWatcher]: Error scanning directory '{}': {}", path, e.what());
+ }
+}
+
+void ElunaFileWatcher::CheckForChanges()
+{
+ bool hasChanges = false;
+
+ try
+ {
+ boost::filesystem::path dir(watchPath);
+
+ if (!boost::filesystem::exists(dir) || !boost::filesystem::is_directory(dir))
+ return;
+
+ boost::filesystem::directory_iterator end_iter;
+
+ for (boost::filesystem::directory_iterator dir_iter(dir); dir_iter != end_iter; ++dir_iter)
+ {
+ if (ShouldReloadFile(dir_iter->path().generic_string()))
+ hasChanges = true;
+ }
+
+ for (auto it = fileTimestamps.begin(); it != fileTimestamps.end();)
+ {
+ if (!boost::filesystem::exists(it->first))
+ {
+ ELUNA_LOG_DEBUG("[ElunaFileWatcher]: File deleted: {}", it->first);
+ it = fileTimestamps.erase(it);
+ hasChanges = true;
+ }
+ else
+ {
+ ++it;
+ }
+ }
+ }
+ catch (const std::exception& e)
+ {
+ ELUNA_LOG_ERROR("[ElunaFileWatcher]: Error checking for changes: {}", e.what());
+ return;
+ }
+
+ if (hasChanges)
+ {
+ ELUNA_LOG_INFO("[ElunaFileWatcher]: Lua script changes detected - triggering reload");
+ Eluna::ReloadEluna();
+
+ ScanDirectory(watchPath);
+ }
+}
+
+bool ElunaFileWatcher::ShouldReloadFile(const std::string& filepath)
+{
+ try
+ {
+ boost::filesystem::path file(filepath);
+
+ if (boost::filesystem::is_directory(file))
+ {
+ boost::filesystem::directory_iterator end_iter;
+
+ for (boost::filesystem::directory_iterator dir_iter(file); dir_iter != end_iter; ++dir_iter)
+ {
+ if (ShouldReloadFile(dir_iter->path().generic_string()))
+ return true;
+ }
+ return false;
+ }
+
+ if (!boost::filesystem::is_regular_file(file))
+ return false;
+
+ std::string filename = file.filename().generic_string();
+
+ if (!IsWatchedFileType(filename)) return false;
+
+ auto currentTime = boost::filesystem::last_write_time(file);
+ auto it = fileTimestamps.find(filepath);
+
+ if (it == fileTimestamps.end())
+ {
+ ELUNA_LOG_DEBUG("[ElunaFileWatcher]: New file detected: {}", filepath);
+ fileTimestamps[filepath] = currentTime;
+ return true;
+ }
+
+ if (it->second != currentTime)
+ {
+ ELUNA_LOG_DEBUG("[ElunaFileWatcher]: File modified: {}", filepath);
+ it->second = currentTime;
+ return true;
+ }
+ }
+ catch (const std::exception& e)
+ {
+ ELUNA_LOG_ERROR("[ElunaFileWatcher]: Error checking file '{}': {}", filepath, e.what());
+ }
+
+ return false;
+}
diff --git a/src/LuaEngine/ElunaFileWatcher.h b/src/LuaEngine/ElunaFileWatcher.h
new file mode 100644
index 0000000..fec29ba
--- /dev/null
+++ b/src/LuaEngine/ElunaFileWatcher.h
@@ -0,0 +1,43 @@
+/*
+* Copyright (C) 2010 - 2016 Eluna Lua Engine
+* This program is free software licensed under GPL version 3
+* Please see the included DOCS/LICENSE.md for more information
+*/
+
+#ifndef ELUNA_FILE_WATCHER_H
+#define ELUNA_FILE_WATCHER_H
+
+#include
+#include
+#include