mirror of
https://github.com/opentibiabr/remeres-map-editor
synced 2026-08-15 18:26:04 -04:00
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.
370 lines
10 KiB
C++
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;
|
|
}
|
|
}
|