tibia-rme/source/lua/lua_engine.cpp
Humberto Gontijo c136a75263
fix: make macOS build with modern sol2 and wxGL (#201)
Bug Fixes:
- Improved Lua scripting: Lua-visible nil is now consistent for missing/empty values across storage, JSON parsing, item/map/tile APIs, dialogs, app properties, and overlay hover.
- Improved macOS graphics reliability (OpenGL context/linking) and safer background/thread handling for streaming and SQLite bootstrap.
- Fixed client asset loading paths and improved SQLite migration input paths.

UI Improvements:
- Replaced “Missing …” modal dialogs with inline text boxes.
- Made “Client directory” editable; added macOS app-picker browse flow.

CI:
- Added compile-scoped macOS builds via reusable workflow and a new build-macos job.
2026-07-29 19:44:25 -03:00

370 lines
10 KiB
C++

//////////////////////////////////////////////////////////////////////
// This file is part of Remere's Map Editor
//////////////////////////////////////////////////////////////////////
// Remere's Map Editor is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Remere's Map Editor is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//////////////////////////////////////////////////////////////////////
#include "main.h"
#include "lua_engine.h"
#include <fstream>
#include <sstream>
LuaEngine::~LuaEngine() {
try {
shutdown();
} catch (const std::exception &e) {
std::cerr << "[LuaEngine] Critical exception during destructor shutdown: "
<< e.what() << std::endl;
} catch (...) {
std::cerr << "[LuaEngine] Unknown critical exception during destructor shutdown." << std::endl;
}
}
bool LuaEngine::initialize() {
if (initialized) {
return true;
}
try {
lua.open_libraries(
sol::lib::base,
sol::lib::package,
sol::lib::coroutine,
sol::lib::string,
sol::lib::table,
sol::lib::math,
sol::lib::utf8,
sol::lib::os
);
setupSandbox();
registerBaseLibraries();
initialized = true;
return true;
} catch (const sol::error &e) {
lastError = std::string("Failed to initialize Lua engine: ") + e.what();
return false;
} catch (const std::exception &e) {
lastError = std::string("Failed to initialize Lua engine: ") + e.what();
return false;
} catch (...) {
lastError = "Failed to initialize Lua engine: Unknown error";
return false;
}
}
void LuaEngine::shutdown() {
if (!initialized) {
return;
}
printCallback = nullptr;
lua = sol::state();
initialized = false;
}
std::string LuaEngine::sanitizeScriptPath(const std::string &filename) {
if (currentScriptDir.empty()) {
throw sol::error("Script directory not set. Cannot resolve relative path.");
}
if (filename.contains(":") || (filename.size() > 0 && (filename[0] == '/' || filename[0] == '\\'))) {
throw sol::error("Absolute paths are not allowed. Use paths relative to the script.");
}
std::string cleanFilename = filename;
if (cleanFilename.starts_with("./") || cleanFilename.starts_with(".\\")) {
cleanFilename = cleanFilename.substr(2);
}
// Validate '..' per path segment
size_t pos = 0;
size_t lastSep = 0;
while (pos <= cleanFilename.length()) {
if (pos == cleanFilename.length() || cleanFilename[pos] == '/' || cleanFilename[pos] == '\\') {
std::string segment = cleanFilename.substr(lastSep, pos - lastSep);
if (segment == "..") {
throw sol::error("Directory traversal ('..') is not allowed.");
}
lastSep = pos + 1;
}
pos++;
}
return cleanFilename;
}
void LuaEngine::setupSandbox() {
if (lua["os"].valid()) {
lua["os"]["execute"] = sol::lua_nil;
lua["os"]["exit"] = sol::lua_nil;
lua["os"]["remove"] = sol::lua_nil;
lua["os"]["rename"] = sol::lua_nil;
lua["os"]["tmpname"] = sol::lua_nil;
lua["os"]["getenv"] = sol::lua_nil;
lua["os"]["setlocale"] = sol::lua_nil;
}
lua["io"] = sol::lua_nil;
if (lua["package"].valid()) {
lua["package"]["loadlib"] = sol::lua_nil;
lua["package"]["path"] = sol::lua_nil;
lua["package"]["cpath"] = sol::lua_nil;
lua["package"]["searchpath"] = sol::lua_nil;
if (lua["package"]["searchers"].valid()) {
sol::table searchers = lua["package"]["searchers"];
searchers[2] = sol::lua_nil; // Disable Lua file searcher
searchers[3] = sol::lua_nil;
searchers[4] = sol::lua_nil;
}
}
lua["dofile"] = [this](const std::string &filename, sol::this_state s) {
std::string cleanFilename = this->sanitizeScriptPath(filename);
std::string fullPath = this->currentScriptDir + "/" + cleanFilename;
return this->executeFile(fullPath);
};
lua["loadfile"] = [this](const std::string &filename, sol::this_state s) -> sol::object {
sol::state_view lua(s);
std::string cleanFilename = this->sanitizeScriptPath(filename);
std::string fullPath = this->currentScriptDir + "/" + cleanFilename;
// Extract directory for the chunk
std::string chunkDir;
size_t lastSlash = fullPath.find_last_of("/\\");
if (lastSlash != std::string::npos) {
chunkDir = fullPath.substr(0, lastSlash);
} else {
chunkDir = ".";
}
// Load the file
sol::load_result loaded = lua.load_file(fullPath);
if (!loaded.valid()) {
sol::error err = loaded;
throw err;
}
// C++ wrapper managing currentScriptDir without exposing LuaEngine
sol::protected_function chunk = loaded;
std::string capturedDir = chunkDir;
auto wrapper = [this, chunk, capturedDir](sol::variadic_args va) -> sol::object {
std::string savedDir = this->currentScriptDir;
this->currentScriptDir = capturedDir;
struct Guard {
LuaEngine* e;
std::string d;
~Guard() noexcept {
try {
e->currentScriptDir = d;
} catch (const std::exception &err) {
std::cerr << "[LuaEngine] Guard failed to restore script directory: "
<< err.what() << std::endl;
e->currentScriptDir.clear();
}
}
} g { this, savedDir };
sol::protected_function_result result = chunk(va);
if (!result.valid()) {
sol::error err = result;
throw err;
}
// Return first value (compatible with standard loadfile)
if (result.return_count() > 0) {
return result[0];
}
return sol::lua_nil;
};
return sol::make_object(this->lua, wrapper);
};
try {
lua.script(R"(
local old_load = load
_G.load = function(chunk, chunkname, mode, env)
if mode and mode ~= "t" then
error("Secure Mode: Binary chunks are disabled.")
end
return old_load(chunk, chunkname, "t", env)
end
)");
} catch (const std::exception &e) {
lastError = std::string("Failed to install secure load patch: ") + e.what();
throw;
} catch (...) {
lastError = "Failed to install secure load patch: Unknown error";
throw;
}
}
void LuaEngine::handlePrint(sol::variadic_args va) {
std::ostringstream oss;
bool first = true;
for (auto v : va) {
if (!first) {
oss << "\t";
}
first = false;
sol::state_view lua(v.lua_state());
sol::function tostring = lua["tostring"];
std::string str = tostring(v);
oss << str;
}
std::string output = oss.str();
if (printCallback) {
printCallback(output);
} else {
std::cout << "[Lua] " << output << std::endl;
}
}
void LuaEngine::registerBaseLibraries() {
lua["print"] = [this](sol::variadic_args va) {
handlePrint(va);
};
}
void LuaEngine::setPrintCallback(PrintCallback callback) {
printCallback = callback;
lua["print"] = [this](sol::variadic_args va) {
handlePrint(va);
};
}
bool LuaEngine::executeFile(const std::string &filepath) {
if (!initialized) {
lastError = "Lua engine not initialized";
return false;
}
// Save current script directory for RAII restoration
std::string savedScriptDir = currentScriptDir;
// RAII guard to restore currentScriptDir on all exit paths
struct ScopeGuard {
LuaEngine* engine;
std::string savedDir;
ScopeGuard(LuaEngine* e, std::string dir) :
engine(e), savedDir(std::move(dir)) { }
ScopeGuard(const ScopeGuard &) = delete;
ScopeGuard &operator=(const ScopeGuard &) = delete;
ScopeGuard(ScopeGuard &&) = delete;
ScopeGuard &operator=(ScopeGuard &&) = delete;
~ScopeGuard() noexcept {
try {
engine->currentScriptDir = savedDir;
} catch (const std::exception &err) {
std::cerr << "[LuaEngine] ScopeGuard failed to restore script directory: "
<< err.what() << std::endl;
engine->currentScriptDir.clear();
} catch (...) {
std::cerr << "[LuaEngine] ScopeGuard failed to restore script directory: Unknown fatal error" << std::endl;
engine->currentScriptDir.clear();
}
}
};
ScopeGuard guard { this, savedScriptDir };
try {
std::string scriptDir;
size_t lastSlash = filepath.find_last_of("/\\");
if (lastSlash != std::string::npos) {
scriptDir = filepath.substr(0, lastSlash);
} else {
scriptDir = ".";
}
currentScriptDir = scriptDir;
sol::load_result loaded = lua.load_file(filepath);
if (!loaded.valid()) {
sol::error err = loaded;
lastError = std::string("Failed to load script '") + filepath + "': " + err.what();
return false;
}
sol::protected_function script = loaded;
sol::protected_function_result result = script();
if (!result.valid()) {
sol::error err = result;
lastError = std::string("Error executing script '") + filepath + "': " + err.what();
return false;
}
return true;
} catch (const sol::error &e) {
lastError = std::string("Exception executing script '") + filepath + "': " + e.what();
return false;
} catch (const std::exception &e) {
lastError = std::string("Exception executing script '") + filepath + "': " + e.what();
return false;
} catch (...) {
lastError = std::string("Unknown exception executing script '") + filepath + "'";
return false;
}
}
bool LuaEngine::executeString(const std::string &code, const std::string &chunkName) {
if (!initialized) {
lastError = "Lua engine not initialized";
return false;
}
try {
sol::load_result loaded = lua.load(code, chunkName, sol::load_mode::text);
if (!loaded.valid()) {
sol::error err = loaded;
lastError = std::string("Failed to load code: ") + err.what();
return false;
}
sol::protected_function script = loaded;
sol::protected_function_result result = script();
if (!result.valid()) {
sol::error err = result;
lastError = std::string("Error executing code: ") + err.what();
return false;
}
return true;
} catch (const sol::error &e) {
lastError = std::string("Exception executing code: ") + e.what();
return false;
} catch (const std::exception &e) {
lastError = std::string("Exception executing code: ") + e.what();
return false;
} catch (...) {
lastError = "Unknown exception executing code";
return false;
}
}