R-Type
Distributed multiplayer game engine in C++
Loading...
Searching...
No Matches
LuaEngine.cpp
Go to the documentation of this file.
1/*
2** EPITECH PROJECT, 2025
3** RTYPE
4** File description:
5** LuaEngine implementation
6*/
7
8#include "LuaEngine.hpp"
9#include <filesystem>
10#include <unordered_set>
11
19
20namespace scripting {
21 LuaEngine::LuaEngine(const std::string &scriptPath)
22 : _scriptPath(scriptPath), _world(nullptr), _bindingsInitialized(false) {
23 _lua.open_libraries(sol::lib::base, sol::lib::package, sol::lib::math, sol::lib::table,
24 sol::lib::string);
25
26 LOG_INFO("LuaEngine initialized with script path: " + scriptPath);
27 LOG_WARNING("Lua bindings not yet initialized. Call setWorld() before executing scripts.");
28 }
29
31 if (world == nullptr) {
32 LOG_ERROR("Attempted to set null world in LuaEngine");
33 throw std::invalid_argument("World cannot be nullptr");
34 }
35
36 _world = world;
37
41 LOG_INFO("Lua bindings initialized successfully");
42 } else {
43 LOG_WARNING("World updated in LuaEngine - bindings already initialized");
44 }
45 }
46
48 // Enregistrer tous les composants et obtenir le helper
49 auto &helper = bindings::bindComponents(_lua, _world);
50
51 // Utiliser le helper pour g\u00e9n\u00e9rer automatiquement les bindings Entity
53
54 // Bindings globaux (world, createEntity, etc.)
56 // Bindings spécifiques au serveur (spawn, random, etc.)
58 }
59
60 bool LuaEngine::loadScript(const std::string &scriptPath) {
61 std::scoped_lock lock(_luaMutex);
62
63 namespace fs = std::filesystem;
64
65 // scriptPath is typically like: "test_movement.lua"
66 // _scriptPath is typically like: "server/Scripting/scripts/"
67 const auto base = fs::path(_scriptPath);
68 const fs::path candidate = base / scriptPath;
69
70 auto tryLoad = [&](const fs::path &p) -> bool {
71 if (!fs::exists(p)) {
72 return false;
73 }
74 try {
75 // Load the script file
76 const sol::load_result loadResult = _lua.load_file(p.string());
77 if (!loadResult.valid()) {
78 const sol::error err = loadResult;
79 LOG_ERROR("Lua error loading " + p.string() + ": " + std::string(err.what()));
80 return false;
81 }
82
83 const sol::protected_function scriptFunc = loadResult;
84
85 // Execute in global environment
86 sol::protected_function_result result = scriptFunc();
87 if (!result.valid()) {
88 const sol::error err = result;
89 LOG_ERROR("Lua error executing " + p.string() + ": " + std::string(err.what()));
90 return false;
91 }
92
93 // Create a table that captures the state after script execution
94 // Copy only the essential functions (onUpdate, onInit, etc.) not everything
95 sol::table scriptTable = _lua.create_table();
96 sol::table globals = _lua.globals();
97
98 // Copy only known script functions to avoid deep copy issues
99 if (globals["onUpdate"].valid()) {
100 scriptTable["onUpdate"] = globals["onUpdate"];
101 }
102 if (globals["onInit"].valid()) {
103 scriptTable["onInit"] = globals["onInit"];
104 }
105 if (globals["onDestroy"].valid()) {
106 scriptTable["onDestroy"] = globals["onDestroy"];
107 }
108 if (globals["onGameStart"].valid()) {
109 scriptTable["onGameStart"] = globals["onGameStart"];
110 }
111
112 _scriptCache[scriptPath] = scriptTable;
113 LOG_INFO("Loaded Lua script: " + scriptPath + " (" + p.string() + ")");
114 return true;
115 } catch (const sol::error &e) {
116 LOG_ERROR("Lua error loading " + p.string() + ": " + std::string(e.what()));
117 return false;
118 }
119 };
120
121 // 1) Try as-is relative to current working directory
122 if (tryLoad(candidate)) {
123 return true;
124 }
125
126 // 2) Fallback: if CLion runs from a build directory, try going up a few levels
127 // and re-appending server/Scripting/scripts.
128 // This keeps things simple without requiring extra CMake configuration.
129 fs::path cwd = fs::current_path();
130 for (int up = 0; up < 6; ++up) {
131 fs::path probeRoot = cwd;
132 for (int i = 0; i < up; ++i) {
133 probeRoot = probeRoot.parent_path();
134 }
135 fs::path probe = probeRoot / "server" / "Scripting" / "scripts" / scriptPath;
136 if (tryLoad(probe)) {
137 return true;
138 }
139 }
140
141 // Avoid log spam: only print the "not found" error once per script name.
142 static std::unordered_set<std::string> loggedMissing;
143 if (!loggedMissing.contains(scriptPath)) {
144 loggedMissing.insert(scriptPath);
145 LOG_ERROR("Lua script not found: " + candidate.string());
146 LOG_ERROR(" - cwd: " + cwd.string());
147 LOG_ERROR(" - scriptPath: " + scriptPath);
148 LOG_ERROR(" - basePath: " + base.string());
149 }
150 return false;
151 }
152
153 void LuaEngine::executeOnGameStart(const std::string &scriptPath, ecs::wrapper::Entity entity) {
154 std::lock_guard<std::recursive_mutex> lock(_luaMutex);
155
156 if (!_world || !_bindingsInitialized) {
157 LOG_ERROR("LuaEngine not properly initialized. Call setWorld() first.");
158 return;
159 }
160
161 if (_scriptCache.find(scriptPath) == _scriptCache.end()) {
162 if (!loadScript(scriptPath)) {
163 return;
164 }
165 }
166
167 try {
168 // Retrieve the specific script's environment from cache
169 sol::table script = _scriptCache[scriptPath];
170
171 // Always call Lua through a protected function to avoid hard crashes on script errors.
172 sol::optional<sol::function> onGameStartOpt = script["onGameStart"];
173
174 if (!onGameStartOpt) {
175 LOG_WARNING("Script " + scriptPath + " has no onGameStart function");
176 return;
177 }
178
179 sol::protected_function onGameStart = onGameStartOpt.value();
180
181 sol::protected_function_result result = onGameStart(entity);
182 if (!result.valid()) {
183 sol::error err = result;
184 LOG_ERROR("Lua runtime error in " + scriptPath + ": " + std::string(err.what()));
185 }
186 } catch (const sol::error &e) {
187 LOG_ERROR("Lua runtime error in " + scriptPath + ": " + std::string(e.what()));
188 } catch (const std::exception &e) {
189 LOG_ERROR("C++ exception in executeOnGameStart: " + std::string(e.what()));
190 }
191 }
192
193 void LuaEngine::executeUpdate(const std::string &scriptPath, ecs::wrapper::Entity entity,
194 float deltaTime) {
195 std::lock_guard<std::recursive_mutex> lock(_luaMutex);
196
197 if (!_world || !_bindingsInitialized) {
198 LOG_ERROR("LuaEngine not properly initialized. Call setWorld() first.");
199 return;
200 }
201
202 // wave_manager uses cached shared state (not per-entity)
203 bool isWaveManager = (scriptPath.find("wave_manager") != std::string::npos);
204
205 if (isWaveManager) {
206 // Use cached version for wave_manager (shared state, not per-entity)
207 if (_scriptCache.find(scriptPath) == _scriptCache.end()) {
208 if (!loadScript(scriptPath)) {
209 return;
210 }
211 }
212
213 try {
214 sol::table script = _scriptCache[scriptPath];
215 sol::optional<sol::function> onUpdateOpt = script["onUpdate"];
216
217 if (!onUpdateOpt) {
218 LOG_WARNING("Script " + scriptPath + " has no onUpdate function");
219 return;
220 }
221
222 sol::protected_function onUpdate = onUpdateOpt.value();
223 sol::protected_function_result result = onUpdate(entity, deltaTime);
224 if (!result.valid()) {
225 sol::error err = result;
226 LOG_ERROR("Lua runtime error in " + scriptPath + ": " + std::string(err.what()));
227 }
228 } catch (const sol::error &e) {
229 LOG_ERROR("Lua runtime error in " + scriptPath + ": " + std::string(e.what()));
230 } catch (const std::exception &e) {
231 LOG_ERROR("C++ exception in executeUpdate: " + std::string(e.what()));
232 }
233 return; // Exit here for wave_manager
234 }
235
236 // For entity scripts (enemy_*): use simple cached script execution
237 // No complex per-entity environments - rely on local variables in Lua
238
239 // Use cached version (same as wave_manager)
240 if (_scriptCache.find(scriptPath) == _scriptCache.end()) {
241 if (!loadScript(scriptPath)) {
242 return;
243 }
244 }
245
246 try {
247 sol::table script = _scriptCache[scriptPath];
248
249 // Validate that the table is still valid
250 if (!script.valid()) {
251 LOG_ERROR("Cached script table is invalid for: " + scriptPath);
252 _scriptCache.erase(scriptPath); // Remove invalid cache
253 return;
254 }
255
256 sol::optional<sol::function> onUpdateOpt = script["onUpdate"];
257
258 if (!onUpdateOpt) {
259 LOG_WARNING("Script " + scriptPath + " has no onUpdate function");
260 return;
261 }
262
263 sol::protected_function onUpdate = onUpdateOpt.value();
264
265 // Validate function before calling
266 if (!onUpdate.valid()) {
267 LOG_ERROR("onUpdate function is invalid for: " + scriptPath);
268 return;
269 }
270
271 sol::protected_function_result result = onUpdate(entity, deltaTime);
272 if (!result.valid()) {
273 sol::error err = result;
274 LOG_ERROR("Lua runtime error in " + scriptPath + ": " + std::string(err.what()));
275 }
276 } catch (const sol::error &e) {
277 LOG_ERROR("Lua runtime error in " + scriptPath + ": " + std::string(e.what()));
278 } catch (const std::exception &e) {
279 LOG_ERROR("C++ exception in executeUpdate: " + std::string(e.what()));
280 }
281 }
282
283 template <typename... Args>
284 void LuaEngine::callFunction(const std::string &scriptPath, const std::string &functionName,
285 Args &&...args) {
286 std::lock_guard<std::recursive_mutex> lock(_luaMutex);
287
288 if (!_world || !_bindingsInitialized) {
289 LOG_ERROR("LuaEngine not properly initialized. Call setWorld() first.");
290 return;
291 }
292
293 if (_scriptCache.find(scriptPath) == _scriptCache.end()) {
294 if (!loadScript(scriptPath)) {
295 return;
296 }
297 }
298
299 try {
300 sol::table script = _scriptCache[scriptPath];
301 sol::optional<sol::function> func = script[functionName];
302
303 if (func) {
304 func.value()(std::forward<Args>(args)...);
305 } else {
306 LOG_WARNING("Function " + functionName + " not found in " + scriptPath);
307 }
308 } catch (const sol::error &e) {
309 LOG_ERROR("Lua error calling " + functionName + ": " + std::string(e.what()));
310 }
311 }
312
313 void LuaEngine::registerGameStartCallback(sol::function callback) {
314 std::scoped_lock lock(_luaMutex);
315 _gameStartCallbacks.push_back(callback);
316 LOG_DEBUG("Registered game start callback (total: " + std::to_string(_gameStartCallbacks.size()) +
317 ")");
318 }
319
320 void LuaEngine::fireGameStartCallbacks(const std::string &roomId) {
321 std::scoped_lock lock(_luaMutex);
322
323 if (_gameStartCallbacks.empty()) {
324 LOG_DEBUG("No game start callbacks registered");
325 return;
326 }
327
328 LOG_INFO("Firing " + std::to_string(_gameStartCallbacks.size()) +
329 " game start callback(s) for room: " + roomId);
330
331 for (auto &callback : _gameStartCallbacks) {
332 try {
333 sol::protected_function_result result = callback(roomId);
334 if (!result.valid()) {
335 sol::error err = result;
336 LOG_ERROR("Lua error in game start callback: " + std::string(err.what()));
337 }
338 } catch (const sol::error &e) {
339 LOG_ERROR("Lua exception in game start callback: " + std::string(e.what()));
340 } catch (const std::exception &e) {
341 LOG_ERROR("C++ exception in game start callback: " + std::string(e.what()));
342 }
343 }
344 }
345
346 void LuaEngine::cleanupEntity(uint32_t entityId) {
347 std::lock_guard<std::recursive_mutex> lock(_luaMutex);
348
349 auto it = _entityScriptCache.find(entityId);
350 if (it != _entityScriptCache.end()) {
351 LOG_DEBUG("Cleaning up script cache for entity " + std::to_string(entityId));
352 _entityScriptCache.erase(it);
353 }
354 }
355
356} // namespace scripting
#define LOG_INFO(...)
Definition Logger.hpp:181
#define LOG_DEBUG(...)
Definition Logger.hpp:180
#define LOG_ERROR(...)
Definition Logger.hpp:183
#define LOG_WARNING(...)
Definition Logger.hpp:182
High-level ECS manager providing clean server-side API.
Definition ECSWorld.hpp:122
High-level entity wrapper providing fluent interface.
Definition ECSWorld.hpp:33
void executeOnGameStart(const std::string &scriptPath, ecs::wrapper::Entity entity)
Execute onGameStart function for an entity's script.
void registerGameStartCallback(sol::function callback)
Register a Lua callback to be called when the game starts.
void cleanupEntity(uint32_t entityId)
Clean up script cache for a destroyed entity.
std::string _scriptPath
void executeUpdate(const std::string &scriptPath, ecs::wrapper::Entity entity, float deltaTime)
Execute onUpdate function for an entity's script.
LuaEngine(const std::string &scriptPath="server/Scripting/scripts/")
Constructor with scripts directory path.
Definition LuaEngine.cpp:21
std::recursive_mutex _luaMutex
void setWorld(ecs::wrapper::ECSWorld *world)
Set the ECS world for entity operations.
Definition LuaEngine.cpp:30
std::unordered_map< uint32_t, std::unordered_map< std::string, sol::table > > _entityScriptCache
void callFunction(const std::string &scriptPath, const std::string &functionName, Args &&...args)
Call a specific Lua function.
bool loadScript(const std::string &scriptPath)
Load and cache a Lua script.
Definition LuaEngine.cpp:60
ecs::wrapper::ECSWorld * _world
std::unordered_map< std::string, sol::table > _scriptCache
std::vector< sol::function > _gameStartCallbacks
void fireGameStartCallbacks(const std::string &roomId)
Fire all registered game start callbacks.
void bindEntity(sol::state &lua, ecs::wrapper::ECSWorld *world, ComponentBindingHelper &helper)
Bind Entity wrapper class and operations to Lua.
void bindWorld(sol::state &lua, ecs::wrapper::ECSWorld *world)
Bind ECS world operations to Lua.
void bindServerGame(sol::state &lua, ecs::wrapper::ECSWorld *world, LuaEngine *engine)
Bind server-specific game logic functions to Lua.
ComponentBindingHelper & bindComponents(sol::state &lua, ecs::wrapper::ECSWorld *world)
Bind ECS component types to Lua.