diff --git a/src/xrScriptEngine/script_engine.cpp b/src/xrScriptEngine/script_engine.cpp index e4271648808..9ce1406e353 100644 --- a/src/xrScriptEngine/script_engine.cpp +++ b/src/xrScriptEngine/script_engine.cpp @@ -781,6 +781,79 @@ struct luajit } }; +// Custom `require` loader resolving modules through the engine file system. +// Unlike the default package.path loader, it also finds scripts packed inside game archives, +// with loose gamedata files taking priority over archived ones, +// consistently with how the engine resolves every other game asset. +// +// Example: +// `local example = require("scripts.folder.file")` imports `gamedata\scripts\folder\file.script` +static int gamedata_module_loader(lua_State* L) +{ + pcstr moduleName = lua_tostring(L, 1); + + string_path fileName; + xr_strcpy(fileName, moduleName); + for (char* c = fileName; *c; ++c) + { + if (*c == '.') + *c = _DELIMITER; + } + xr_strcat(fileName, ".script"); + + string_path filePath; + FS.update_path(filePath, "$game_data$", fileName); + + IReader* reader = FS.r_open(filePath); + if (!reader) + { + // Not an error: report the candidate path and let the remaining searchers try + lua_pushfstring(L, "\n\tno file '%s' in engine file system", filePath); + return 1; + } + + string_path chunkName; + strconcat(sizeof(chunkName), chunkName, "@", filePath); + + const int errorCode = luaL_loadbuffer(L, static_cast(reader->pointer()), reader->length(), chunkName); + FS.r_close(reader); + + if (errorCode) + { + return lua_error(L); + } + + return 1; +} + +// Adds gamedata folder as module root for lua `require` and allows usage of built-in lua module system. +// +// Example: +// `local example = require("scripts.folder.file")` tries to import `gamedata\scripts\folder\file.script` +static void setup_gamedata_module_loading(lua_State* L) +{ + // Engine file system loader, resolves modules packed inside game archives as well. + // Registered after package.preload (index 1), before the default package.path loader: + // table.insert(package.loaders, 2, gamedata_module_loader) + lua_getglobal(L, "table"); + lua_getfield(L, -1, "insert"); + lua_remove(L, -2); + lua_getglobal(L, "package"); + lua_getfield(L, -1, "loaders"); + lua_remove(L, -2); + lua_pushinteger(L, 2); + lua_pushcfunction(L, gamedata_module_loader); + lua_call(L, 3, 0); + + string_path gamedataPath; + string_path packagePath; + + FS.update_path(gamedataPath, "$game_data$", "?.script;"); + xr_sprintf(packagePath, "package.path = package.path .. [[%s]]", gamedataPath); + + luaL_dostring(L, packagePath); +} + void CScriptEngine::init(export_func exporter, bool loadGlobalNamespace) { ZoneScoped; @@ -851,20 +924,7 @@ void CScriptEngine::init(export_func exporter, bool loadGlobalNamespace) luaL_dostring(lua(), mathRandom); } - // Adds gamedata folder as module root for lua `require` and allows usage of built-in lua module system. - // Notes: - // - Does not resolve files inside archived game files - // Example: - // `local example = require("scripts.folder.file")` tries to import `gamedata\scripts\folder\file.script` - { - string_path gamedataPath; - string_path packagePath; - - FS.update_path(gamedataPath, "$game_data$", "?.script;"); - xr_sprintf(packagePath, "package.path = package.path .. [[%s]]", gamedataPath); - - luaL_dostring(lua(), packagePath); - } + setup_gamedata_module_loading(lua()); // XXX nitrocaster: with vanilla scripts, '-nojit' option requires script profiler to be disabled. The reason // is that lua hooks somehow make 'super' global unavailable (is's used all over the vanilla scripts).