////////////////////////////////////////////////////////////////////// // 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 . ////////////////////////////////////////////////////////////////////// #include "main.h" #include "lua_api_app.h" #include "lua_script_manager.h" #include "../gui.h" #include "../editor.h" #include "../map.h" #include "../brush.h" #include "../action.h" #include "../tile.h" #include "../selection.h" #include "../house.h" #include "../items.h" #include "../raw_brush.h" #include #include #include #include #include #include #include #include #include #include #include namespace LuaAPI { // ============================================================================ // Transaction System for Undo/Redo Support // ============================================================================ // Transaction implementation // Helper to sync map metadata when swapping tiles static void updateTileMetadata(Editor* editor, Tile* tile, bool adding) { if (!editor || !tile) { return; } Map &map = editor->getMap(); if (adding) { if (tile->spawnMonster) { map.spawnsMonster.addSpawnMonster(tile); } if (tile->getHouseID()) { House* h = map.houses.getHouse(tile->getHouseID()); if (h) { h->addTile(tile); } } } else { if (tile->spawnMonster) { map.spawnsMonster.removeSpawnMonster(tile); } if (tile->getHouseID()) { House* h = map.houses.getHouse(tile->getHouseID()); if (h) { h->removeTile(tile); } } // Also clean up selection if removing if (tile->isSelected()) { editor->getSelection().start(Selection::INTERNAL); editor->getSelection().removeInternal(tile); editor->getSelection().finish(Selection::INTERNAL); } } } class LuaTransaction { bool active = false; Editor* editor = nullptr; std::unique_ptr batch; std::unique_ptr action; std::unordered_map> originalTiles; uint64_t positionKey(const Position &pos) const { return (static_cast(pos.x) << 32) | (static_cast(pos.y) << 16) | static_cast(pos.z); } public: static LuaTransaction &getInstance() { static LuaTransaction instance; return instance; } LuaTransaction() = default; void begin(Editor* ed) { if (active) { throw sol::error("Transaction already in progress"); } editor = ed; if (!editor) { throw sol::error("No editor or action queue available"); } active = true; batch.reset(editor->getActionQueue()->createBatch(ACTION_LUA_SCRIPT)); action.reset(editor->getActionQueue()->createAction(ACTION_LUA_SCRIPT)); originalTiles.clear(); } void commit() { if (!active) { return; } // Process each modified tile for (auto &[key, originalTile] : originalTiles) { Position pos = originalTile->getPosition(); Tile* modifiedTile = editor->getMap().getTile(pos); if (modifiedTile) { Tile* modifiedCopy = modifiedTile->deepCopy(editor->getMap()); std::unique_ptr swappedOut(editor->getMap().swapTile(pos, originalTile.release())); updateTileMetadata(editor, swappedOut.get(), false); updateTileMetadata(editor, editor->getMap().getTile(pos), true); action->addChange(new Change(modifiedCopy)); } } // Clear - ownership has been transferred originalTiles.clear(); if (!action->empty()) { batch->addAndCommitAction(action.release()); editor->addBatch(batch.release()); editor->getMap().doChange(); g_gui.RefreshView(); } cleanup(); } void rollback() { if (!active) { return; } for (auto &[key, originalTile] : originalTiles) { if (originalTile) { Position pos = originalTile->getPosition(); std::unique_ptr modifiedTile(editor->getMap().swapTile(pos, originalTile.release())); updateTileMetadata(editor, modifiedTile.get(), false); updateTileMetadata(editor, editor->getMap().getTile(pos), true); } } originalTiles.clear(); cleanup(); } void markTileModified(Tile* tile) { if (!active || !tile) { return; } Position pos = tile->getPosition(); uint64_t key = positionKey(pos); // Only snapshot the tile once per transaction (first time it's modified) if (!originalTiles.contains(key)) { originalTiles[key].reset(tile->deepCopy(editor->getMap())); } } bool isActive() const { return active; } Editor* getEditor() const { return editor; } private: void cleanup() { active = false; editor = nullptr; batch.reset(); action.reset(); } }; // Global accessor for tile modification tracking (used by lua_api_tile.cpp) void markTileForUndo(Tile* tile) { if (LuaTransaction::getInstance().isActive()) { LuaTransaction::getInstance().markTileModified(tile); } } // ============================================================================ // Helper Functions // ============================================================================ struct AlertOptions { std::string title = "Script"; std::string message; std::vector buttons; }; static AlertOptions parseAlertOptions(sol::this_state ts, sol::object arg) { sol::state_view lua(ts); AlertOptions opts; if (arg.is()) { opts.message = arg.as(); opts.buttons.emplace_back("OK"); return opts; } if (!arg.is()) { sol::function tostring = lua["tostring"]; opts.message = tostring(arg); opts.buttons.emplace_back("OK"); return opts; } sol::table tbl = arg.as(); if (tbl["title"].valid()) { opts.title = tbl["title"].get(); } if (tbl["text"].valid()) { opts.message = tbl["text"].get(); } if (tbl["buttons"].valid()) { sol::table btns = tbl["buttons"]; for (size_t i = 1; i <= btns.size(); ++i) { if (!btns[i].valid()) { continue; } opts.buttons.push_back(btns[i].get()); } } if (opts.buttons.empty()) { opts.buttons.emplace_back("OK"); } return opts; } static long getDialogStyle(const std::vector &buttons) { long style = wxCENTRE; if (buttons.size() == 1) { return style | wxOK; } if (buttons.size() >= 3) { return style | wxYES_NO | wxCANCEL; } if (buttons.size() != 2) { return style | wxOK; } std::string btn1 = buttons[0]; std::string btn2 = buttons[1]; std::transform(btn1.begin(), btn1.end(), btn1.begin(), ::tolower); std::transform(btn2.begin(), btn2.end(), btn2.begin(), ::tolower); if ((btn1 == "ok" && btn2 == "cancel") || (btn1 == "cancel" && btn2 == "ok")) { return style | wxOK | wxCANCEL; } if ((btn1 == "yes" && btn2 == "no") || (btn1 == "no" && btn2 == "yes")) { return style | wxYES_NO; } return style | wxOK | wxCANCEL; } static int mapDialogResult(int result, size_t buttonCount) { switch (result) { case wxID_OK: case wxID_YES: return 1; case wxID_NO: return 2; case wxID_CANCEL: return buttonCount >= 3 ? 3 : 2; default: return 0; } } static int showAlert(sol::this_state ts, sol::object arg) { AlertOptions opts = parseAlertOptions(ts, arg); long style = getDialogStyle(opts.buttons); wxWindow* parent = g_gui.root; wxMessageDialog dlg(parent, wxString(opts.message), wxString(opts.title), style); return mapDialogResult(dlg.ShowModal(), opts.buttons.size()); } // Check if a map is currently open static bool hasMap() { Editor* editor = g_gui.GetCurrentEditor(); return editor != nullptr; } // Refresh the map view static void refresh() { g_gui.RefreshView(); } // Get the current Map object static Map* getMap() { Editor* editor = g_gui.GetCurrentEditor(); if (!editor) { return nullptr; } return &editor->getMap(); } // Get the current Selection object static Selection* getSelection() { Editor* editor = g_gui.GetCurrentEditor(); if (!editor) { return nullptr; } return &editor->getSelection(); } static void setClipboard(const std::string &text) { if (wxTheClipboard->Open()) { wxTheClipboard->SetData(new wxTextDataObject(text)); wxTheClipboard->Close(); } } // Transaction function with undo/redo support static void transaction(const std::string &name, sol::function func) { Editor* editor = g_gui.GetCurrentEditor(); if (!editor) { throw sol::error("No map open"); } LuaTransaction &trans = LuaTransaction::getInstance(); try { trans.begin(editor); func(); trans.commit(); g_gui.RefreshView(); } catch (const sol::error &) { trans.rollback(); throw; // Re-throw to show error to user } catch (const std::exception &e) { trans.rollback(); throw sol::error(std::string("Transaction failed: ") + e.what()); } } static sol::object getBorders(sol::this_state ts) { sol::state_view lua(ts); sol::table bordersTable = lua.create_table(); return bordersTable; } static std::string getDataDirectory() { return GUI::GetDataDirectory().ToStdString(); } static bool saveStorageData(const std::string &path, sol::this_state ts2, sol::object first, sol::object second) { sol::state_view lua(ts2); std::string content; sol::object data = (second.valid() && !second.is()) ? second : first; if (!data.valid() || data.is()) { return false; } if (data.is()) { content = data.as(); } else { sol::table json = lua["json"]; if (!json.valid() || !json["encode_pretty"].valid()) { return false; } try { sol::function encode = json["encode_pretty"]; sol::protected_function_result result = encode(data); if (!result.valid()) { return false; } content = result.get(); } catch (const sol::error &) { return false; } } std::ofstream file(path, std::ios::trunc); if (!file.is_open()) { return false; } file << content; file.close(); return true; } static sol::table storageForScript(sol::this_state ts, const std::string &name) { sol::state_view lua(ts); sol::table storage = lua.create_table(); std::string scriptDir = "."; if (lua["SCRIPT_DIR"].valid()) { scriptDir = lua["SCRIPT_DIR"]; } std::string filename = name; // Security check: Prevent path traversal and absolute paths if (filename.contains("..") || std::filesystem::path(filename).is_absolute()) { std::println("[Lua Security] Blocked unsafe path in app.storage: {}", filename); return lua.create_table(); } if (!filename.contains('.')) { filename += ".json"; } std::string path = scriptDir + "/" + filename; storage["path"] = path; storage["load"] = [path](sol::this_state ts2, sol::object) -> sol::object { sol::state_view lua(ts2); std::ifstream file(path); if (!file.is_open()) { return sol::make_object(lua, sol::lua_nil); } std::stringstream buffer; buffer << file.rdbuf(); file.close(); std::string content = buffer.str(); if (content.empty()) { return sol::make_object(lua, sol::lua_nil); } sol::table json = lua["json"]; if (!json.valid() || !json["decode"].valid()) { return sol::make_object(lua, sol::lua_nil); } try { sol::function decode = json["decode"]; sol::protected_function_result result = decode(content); if (!result.valid()) { return sol::make_object(lua, sol::lua_nil); } sol::object decoded = result; return sol::make_object(lua, decoded); } catch (const sol::error &) { return sol::make_object(lua, sol::lua_nil); } }; storage["save"] = [path](sol::this_state ts2, sol::object first, sol::object second) { return saveStorageData(path, ts2, first, second); }; storage["clear"] = [path](sol::object) { return std::remove(path.c_str()) == 0; }; return storage; } // ============================================================================ // Register App API // ============================================================================ static sol::object appPropertyGetter(sol::this_state ts, sol::table self, std::string key) { sol::state_view lua(ts); if (key == "map") { Map* map = getMap(); if (map) { return sol::make_object(lua, map); } return sol::lua_nil; } if (key == "selection") { Selection* sel = getSelection(); if (sel) { return sol::make_object(lua, sel); } return sol::lua_nil; } if (key == "borders") { return getBorders(ts); } if (key == "editor") { Editor* editor = g_gui.GetCurrentEditor(); if (editor) { return sol::make_object(lua, editor); } return sol::lua_nil; } if (key == "brush") { Brush* b = g_gui.GetCurrentBrush(); if (b) { return sol::make_object(lua, b); } return sol::lua_nil; } if (key == "brushSize") { return sol::make_object(lua, g_gui.GetBrushSize()); } if (key == "brushShape") { return sol::make_object(lua, g_gui.GetBrushShape() == BRUSHSHAPE_CIRCLE ? "circle" : "square"); } if (key == "brushVariation") { return sol::make_object(lua, g_gui.GetBrushVariation()); } if (key == "spawnTime") { return sol::make_object(lua, 0); } return sol::lua_nil; } static void appPropertySetter(sol::this_state ts, sol::table self, std::string key, sol::object value) { if (key == "brushSize") { if (value.is()) { g_gui.SetBrushSize(value.as()); } } else if (key == "brushShape") { if (value.is()) { std::string s = value.as(); if (s == "circle") { g_gui.SetBrushShape(BRUSHSHAPE_CIRCLE); } else if (s == "square") { g_gui.SetBrushShape(BRUSHSHAPE_SQUARE); } } } else if (key == "brushVariation") { if (value.is()) { g_gui.SetBrushVariation(value.as()); } } } static bool handleRegisterShow(sol::variadic_args va) { std::string label; std::string overlayId; bool enabled = true; sol::function ontoggle; if (va.size() == 2 && va[0].is() && va[1].is()) { label = va[0].as(); overlayId = va[1].as(); } else if (va.size() >= 3) { if (va[0].is()) { if (va[1].is()) { label = va[1].as(); } if (va[2].is()) { overlayId = va[2].as(); } } else if (va[0].is() && va[1].is()) { label = va[0].as(); overlayId = va[1].as(); } } if (va.size() >= 3 && va[va.size() - 1].is()) { sol::table opts = va[va.size() - 1].as(); enabled = opts.get_or(std::string("enabled"), enabled); if (opts["ontoggle"].valid()) { ontoggle = opts["ontoggle"]; } } else if (va.size() >= 3 && va[va.size() - 1].is()) { enabled = va[va.size() - 1].as(); } if (label.empty() || overlayId.empty()) { return false; } return g_luaScripts.getOverlayManager().registerMapOverlayShow(label, overlayId, enabled, ontoggle); } static void setupMapView(sol::table &app) { sol::state_view lua(app.lua_state()); sol::table mapView = lua.create_table(); mapView["addOverlay"] = [](sol::variadic_args va) -> bool { if (va.size() == 2 && va[0].is() && va[1].is()) { return g_luaScripts.getOverlayManager().addMapOverlay(va[0].as(), va[1].as()); } if (va.size() == 3 && va[1].is() && va[2].is()) { return g_luaScripts.getOverlayManager().addMapOverlay(va[1].as(), va[2].as()); } return false; }; mapView["removeOverlay"] = [](sol::variadic_args va) -> bool { if (va.size() == 1 && va[0].is()) { return g_luaScripts.getOverlayManager().removeMapOverlay(va[0].as()); } if (va.size() == 2 && va[1].is()) { return g_luaScripts.getOverlayManager().removeMapOverlay(va[1].as()); } return false; }; mapView["setEnabled"] = [](sol::variadic_args va) -> bool { if (va.size() == 2 && va[0].is() && va[1].is()) { return g_luaScripts.getOverlayManager().setMapOverlayEnabled(va[0].as(), va[1].as()); } if (va.size() == 3 && va[1].is() && va[2].is()) { return g_luaScripts.getOverlayManager().setMapOverlayEnabled(va[1].as(), va[2].as()); } return false; }; mapView["registerShow"] = handleRegisterShow; app["mapView"] = mapView; } static void editorGoToHistory(Editor* editor, int targetIndex) { if (!editor || !editor->getActionQueue()) { return; } int current = editor->getActionQueue()->getCurrentIndex(); int target = targetIndex; if (target < 0) { target = 0; } if (target > (int)editor->getActionQueue()->size()) { target = (int)editor->getActionQueue()->size(); } int diff = target - current; if (diff > 0) { for (int i = 0; i < diff; ++i) { if (editor->canRedo()) { editor->redo(); } } } else if (diff < 0) { for (int i = 0; i < -diff; ++i) { if (editor->canUndo()) { editor->undo(); } } } g_gui.RefreshView(); } static void registerEditorUsertype(sol::state &lua) { lua.new_usertype( "Editor", sol::no_constructor, "undo", [](Editor* editor) { if (editor && editor->canUndo()) { editor->undo(); g_gui.RefreshView(); } }, "redo", [](Editor* editor) { if (editor && editor->canRedo()) { editor->redo(); g_gui.RefreshView(); } }, "canUndo", [](Editor* editor) -> bool { return editor && editor->canUndo(); }, "canRedo", [](Editor* editor) -> bool { return editor && editor->canRedo(); }, "historyIndex", sol::property([](Editor* editor) -> int { if (editor && editor->getActionQueue()) { return (int)editor->getActionQueue()->getCurrentIndex(); } return 0; }), "historySize", sol::property([](Editor* editor) -> int { if (editor && editor->getActionQueue()) { return (int)editor->getActionQueue()->size(); } return 0; }), "getHistory", [](Editor* editor, sol::this_state ts) -> sol::table { sol::state_view lua(ts); sol::table history = lua.create_table(); if (editor && editor->getActionQueue()) { size_t size = editor->getActionQueue()->size(); for (size_t i = 0; i < size; ++i) { sol::table input = lua.create_table(); input["index"] = (int)(i + 1); const BatchAction* ba = editor->getActionQueue()->getAction(i); input["name"] = ba ? ba->getLabel().ToStdString() : std::string(""); history[i + 1] = input; } } return history; }, "goToHistory", editorGoToHistory ); } void registerApp(sol::state &lua) { sol::table app = lua.create_named_table("app"); app["version"] = __RME_VERSION__; app["apiVersion"] = 1; app["alert"] = showAlert; app["hasMap"] = hasMap; app["refresh"] = refresh; app["transaction"] = transaction; app["setClipboard"] = setClipboard; app["getDataDirectory"] = getDataDirectory; app["addContextMenu"] = [](const std::string &label, sol::function callback) { g_luaScripts.registerContextMenuItem(label, callback); }; app["selectRaw"] = [](int itemId) { if (g_items[itemId].id != 0) { ItemType &it = g_items[itemId]; if (it.raw_brush) { g_gui.SelectBrush(it.raw_brush, TILESET_RAW); } } }; app["setCameraPosition"] = [](int x, int y, int z) { g_gui.SetScreenCenterPosition(Position(x, y, z)); }; app["storage"] = storageForScript; app["yield"] = []() { if (wxTheApp) { wxTheApp->Yield(true); } }; app["sleep"] = [](int milliseconds) { if (milliseconds > 0 && milliseconds <= 10000) { std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds)); } }; app["getTime"] = []() -> long { return g_gui.gfx.getElapsedTime(); }; sol::table events = lua.create_table(); events["on"] = [](sol::this_state ts, sol::table self, const std::string &eventName, sol::function callback) -> int { return g_luaScripts.addEventListener(eventName, callback); }; events["off"] = [](sol::this_state ts, sol::table self, int listenerId) { return g_luaScripts.removeEventListener(listenerId); }; app["events"] = events; sol::table mt = lua.create_table(); mt[sol::meta_function::index] = appPropertyGetter; mt[sol::meta_function::new_index] = appPropertySetter; sol::table keyboard = lua.create_table(); keyboard["isCtrlDown"] = []() -> bool { return wxGetKeyState(WXK_CONTROL); }; keyboard["isShiftDown"] = []() -> bool { return wxGetKeyState(WXK_SHIFT); }; keyboard["isAltDown"] = []() -> bool { return wxGetKeyState(WXK_ALT); }; app["keyboard"] = keyboard; app["copy"] = []() { g_gui.DoCopy(); }; app["cut"] = []() { g_gui.DoCut(); }; app["paste"] = []() { g_gui.DoPaste(); }; setupMapView(app); app[sol::metatable_key] = mt; registerEditorUsertype(lua); } } // namespace LuaAPI