From 10d67aa82a8541792e31bdc8a55481b7fe89a474 Mon Sep 17 00:00:00 2001 From: Celtic Minstrel Date: Sun, 23 Apr 2023 13:43:41 -0400 Subject: [PATCH] Implement a new Lua API to the undo system --- data/lua/diversion.lua | 26 +++++++--------- data/lua/on_event.lua | 2 +- data/lua/wml-tags.lua | 14 ++++++++- src/actions/undo_action.cpp | 43 +++++++++++++++++++++++---- src/actions/undo_action.hpp | 3 ++ src/game_events/action_wml.cpp | 9 ------ src/scripting/game_lua_kernel.cpp | 40 ++++++++++++++++++++++++- src/scripting/game_lua_kernel.hpp | 2 ++ src/synced_context.cpp | 10 +++++++ src/synced_context.hpp | 15 ++++++++-- utils/emmylua/wesnoth/game_events.lua | 11 ++++++- 11 files changed, 139 insertions(+), 36 deletions(-) diff --git a/data/lua/diversion.lua b/data/lua/diversion.lua index 566e3bf7859..f6bb465f9cc 100644 --- a/data/lua/diversion.lua +++ b/data/lua/diversion.lua @@ -1,7 +1,6 @@ local _ = wesnoth.textdomain 'wesnoth-help' local T = wml.tag -local on_event = wesnoth.require("on_event") local u_pos_filter = function(u_id) @@ -37,8 +36,7 @@ local u_pos_filter = function(u_id) end end - -local status_anim_update = function(is_undo) +local function status_anim_update(is_undo) local ec = wesnoth.current.event_context local changed_something = false @@ -88,20 +86,16 @@ local status_anim_update = function(is_undo) } end end - if changed_something and not is_undo then - wesnoth.wml_actions.on_undo { - wml.tag.on_undo_diversion { - } - } + if not is_undo then + wesnoth.game_events.set_undoable(true) + if changed_something then + wesnoth.game_events.add_undo_actions(function(_) + status_anim_update(true) + end) + end end end -function wesnoth.wml_actions.on_undo_diversion(cfg) - status_anim_update(true) -end - -on_event("moveto, die, recruit, recall", function() - status_anim_update() - +wesnoth.game_events.add_repeating("moveto, die, recruit, recall", function(_) + status_anim_update(false) end) - diff --git a/data/lua/on_event.lua b/data/lua/on_event.lua index 0a219bdf13a..6e5cc506906 100644 --- a/data/lua/on_event.lua +++ b/data/lua/on_event.lua @@ -51,5 +51,5 @@ local function on_event(eventname, priority, fcn) end end -core_on_event = on_event +core_on_event = wesnoth.deprecate_api('on_event', 'wesnoth.game_events.add_repeating', 1, nil, on_event) return on_event diff --git a/data/lua/wml-tags.lua b/data/lua/wml-tags.lua index 8d2e1eceb16..29b089f27d5 100644 --- a/data/lua/wml-tags.lua +++ b/data/lua/wml-tags.lua @@ -645,7 +645,11 @@ function wml_actions.put_to_recall_list(cfg) end function wml_actions.allow_undo(cfg) - wesnoth.allow_undo() + wesnoth.game_events.set_undoable(true) +end + +function wml_actions.disallow_undo(cfg) + wesnoth.game_events.set_undoable(false) end function wml_actions.allow_end_turn(cfg) @@ -1025,3 +1029,11 @@ function wml_actions.progress_achievement(cfg) wesnoth.achievements.progress(cfg.content_for, cfg.id, cfg.amount, tonumber(cfg.limit) or 999999999) end + +function wml_actions.on_undo(cfg) + if cfg.delayed_variable_substitution then + wesnoth.game_events.add_undo_actions(wml.literal(cfg)); + else + wesnoth.game_events.add_undo_actions(wml.parsed(cfg)); + end +end diff --git a/src/actions/undo_action.cpp b/src/actions/undo_action.cpp index e7b18327689..02d0f4a194b 100644 --- a/src/actions/undo_action.cpp +++ b/src/actions/undo_action.cpp @@ -29,6 +29,27 @@ namespace actions { +undo_event::undo_event(int fcn_idx, const config& args, const game_events::queued_event& ctx) + : lua_idx(fcn_idx) + , commands(args) + , data(ctx.data) + , loc1(ctx.loc1) + , loc2(ctx.loc2) + , filter_loc1(ctx.loc1.filter_loc()) + , filter_loc2(ctx.loc2.filter_loc()) + , uid1(), uid2() +{ + unit_const_ptr u1 = ctx.loc1.get_unit(), u2 = ctx.loc2.get_unit(); + if(u1) { + id1 = u1->id(); + uid1 = u1->underlying_id(); + } + if(u2) { + id2 = u2->id(); + uid2 = u2->underlying_id(); + } +} + undo_event::undo_event(const config& cmds, const game_events::queued_event& ctx) : commands(cmds) , data(ctx.data) @@ -68,8 +89,12 @@ undo_action::undo_action() , unit_id_diff(synced_context::get_unit_id_diff()) { auto& undo = synced_context::get_undo_commands(); - auto command_transformer = [](const std::pair& p) { - return undo_event(p.first, p.second); + auto command_transformer = [](const synced_context::event_info& p) { + if(p.lua_.has_value()) { + return undo_event(*p.lua_, p.cmds_, p.evt_); + } else { + return undo_event(p.cmds_, p.evt_); + } }; std::transform(undo.begin(), undo.end(), std::back_inserter(umc_commands_undo), command_transformer); undo.clear(); @@ -115,7 +140,11 @@ namespace { scoped_weapon_info w2("second_weapon", e.data.optional_child("second")); game_events::queued_event q(tag, "", map_location(x1, y1, wml_loc()), map_location(x2, y2, wml_loc()), e.data); - resources::lua_kernel->run_wml_action("command", vconfig(e.commands), q); + if(e.lua_idx.has_value()) { + resources::lua_kernel->run_wml_event(*e.lua_idx, vconfig(e.commands), q); + } else { + resources::lua_kernel->run_wml_action("command", vconfig(e.commands), q); + } sound::commit_music_changes(); x1 = oldx1; y1 = oldy1; @@ -142,7 +171,7 @@ void undo_action::write(config & cfg) const void undo_action::read_event_vector(event_vector& vec, const config& cfg, const std::string& tag) { for(auto c : cfg.child_range(tag)) { - vec.emplace_back(c.child_or_empty("filter"), c.child_or_empty("filter_second"), c.child_or_empty("filter_weapons"), c.child_or_empty("command")); + vec.emplace_back(c.child_or_empty("filter"), c.child_or_empty("filter_second"), c.child_or_empty("data"), c.child_or_empty("command")); } } @@ -150,10 +179,14 @@ void undo_action::write_event_vector(const event_vector& vec, config& cfg, const { for(const auto& evt : vec) { + if(evt.lua_idx.has_value()) { + // TODO: Log warning that this cannot be serialized + continue; + } config& entry = cfg.add_child(tag); config& first = entry.add_child("filter"); config& second = entry.add_child("filter_second"); - entry.add_child("filter_weapons", evt.data); + entry.add_child("data", evt.data); entry.add_child("command", evt.commands); // First location first["filter_x"] = evt.filter_loc1.wml_x(); diff --git a/src/actions/undo_action.hpp b/src/actions/undo_action.hpp index 7b8170379d7..3224797b6b3 100644 --- a/src/actions/undo_action.hpp +++ b/src/actions/undo_action.hpp @@ -15,6 +15,7 @@ #pragma once +#include #include "vision.hpp" #include "map/location.hpp" #include "units/ptr.hpp" @@ -26,10 +27,12 @@ namespace actions { class undo_list; struct undo_event { + std::optional lua_idx; config commands, data; map_location loc1, loc2, filter_loc1, filter_loc2; std::size_t uid1, uid2; std::string id1, id2; + undo_event(int fcn_idx, const config& args, const game_events::queued_event& ctx); undo_event(const config& cmds, const game_events::queued_event& ctx); undo_event(const config& first, const config& second, const config& weapons, const config& cmds); }; diff --git a/src/game_events/action_wml.cpp b/src/game_events/action_wml.cpp index 53b895f0178..222fe22ca93 100644 --- a/src/game_events/action_wml.cpp +++ b/src/game_events/action_wml.cpp @@ -910,13 +910,4 @@ WML_HANDLER_FUNCTION(unit,, cfg) } -WML_HANDLER_FUNCTION(on_undo, event_info, cfg) -{ - if(cfg["delayed_variable_substitution"].to_bool(false)) { - synced_context::add_undo_commands(cfg.get_config(), event_info); - } else { - synced_context::add_undo_commands(cfg.get_parsed_config(), event_info); - } -} - } // end namespace game_events diff --git a/src/scripting/game_lua_kernel.cpp b/src/scripting/game_lua_kernel.cpp index efa2b1b5927..23801925aa9 100644 --- a/src/scripting/game_lua_kernel.cpp +++ b/src/scripting/game_lua_kernel.cpp @@ -3985,6 +3985,23 @@ static std::string read_event_name(lua_State* L, int idx) } } +/** + * Add undo actions for the current active event + * Arg 1: Either a table of ActionWML or a function to call + * Arg 2: (optional) If Arg 1 is a function, this is a WML table that will be passed to it + */ +int game_lua_kernel::intf_add_undo_actions(lua_State *L) +{ + config cfg; + if(luaW_toconfig(L, 1, cfg)) { + synced_context::add_undo_commands(cfg, get_event_info()); + } else { + luaW_toconfig(L, 2, cfg); + synced_context::add_undo_commands(save_wml_event(1), cfg, get_event_info()); + } + return 0; +} + /** Add a new event handler * Arg 1: Table of options. * name: Event to handle, as a string or list of strings @@ -4073,9 +4090,25 @@ int game_lua_kernel::intf_add_event(lua_State *L) return 0; } +/** + * Upvalue 1: The event function + * Upvalue 2: The undo function + * Arg 1: The event content + */ +int game_lua_kernel::cfun_undoable_event(lua_State* L) +{ + lua_pushvalue(L, lua_upvalueindex(1)); + lua_push(L, 1); + luaW_pcall(L, 1, 0); + synced_context::add_undo_commands(lua_upvalueindex(2), get_event_info()); + return 0; +} + /** Add a new event handler * Arg 1: Event to handle, as a string or list of strings; or menu item ID if this is a menu item * Arg 2: The function to call when the event triggers + * Arg 3: (optional) Event priority + * Arg 4: (optional, non-menu-items only) The function to call when the event is undone * * Lua API: * - wesnoth.game_events.add_repeating @@ -4094,6 +4127,10 @@ int game_lua_kernel::intf_add_event_simple(lua_State *L) if(is_menu_item) { id = name; name = "menu item " + name; + } else if(lua_absindex(L, -1) > 2 && lua_isfunction(L, -1)) { + // If undo is provided as a separate function, link them together into a single function + // The function can be either the 3rd or 4th argument. + lua_pushcclosure(L, &dispatch<&game_lua_kernel::cfun_undoable_event>, 2); } auto new_handler = man.add_event_handler_from_lua(name, id, repeat, priority, is_menu_item); if(new_handler.valid()) { @@ -4913,7 +4950,6 @@ game_lua_kernel::game_lua_kernel(game_state & gs, play_controller & pc, reports { "get_era", &intf_get_era }, { "get_resource", &intf_get_resource }, { "modify_ai", &intf_modify_ai_old }, - { "allow_undo", &dispatch<&game_lua_kernel::intf_allow_undo > }, { "cancel_action", &dispatch<&game_lua_kernel::intf_cancel_action > }, { "log_replay", &dispatch<&game_lua_kernel::intf_log_replay > }, { "log", &dispatch<&game_lua_kernel::intf_log > }, @@ -5281,6 +5317,8 @@ game_lua_kernel::game_lua_kernel(game_state & gs, play_controller & pc, reports { "remove", &dispatch<&game_lua_kernel::intf_remove_event> }, { "fire", &dispatch2<&game_lua_kernel::intf_fire_event, false> }, { "fire_by_id", &dispatch2<&game_lua_kernel::intf_fire_event, true> }, + { "add_undo_actions", &dispatch<&game_lua_kernel::intf_add_undo_actions> }, + { "set_undoable", &dispatch<&game_lua_kernel::intf_allow_undo > }, { nullptr, nullptr } }; lua_getglobal(L, "wesnoth"); diff --git a/src/scripting/game_lua_kernel.hpp b/src/scripting/game_lua_kernel.hpp index b81b8c44b7d..e5419d941fb 100644 --- a/src/scripting/game_lua_kernel.hpp +++ b/src/scripting/game_lua_kernel.hpp @@ -161,6 +161,8 @@ class game_lua_kernel : public lua_kernel_base int intf_add_event_simple(lua_State* L); int intf_add_event_wml(lua_State* L); int intf_add_event(lua_State *L); + int intf_add_undo_actions(lua_State *L); + int cfun_undoable_event(lua_State *L); int intf_remove_event(lua_State *L); int intf_color_adjust(lua_State *L); int intf_get_color_adjust(lua_State *L); diff --git a/src/synced_context.cpp b/src/synced_context.cpp index 05598fe1923..fb73974aecf 100644 --- a/src/synced_context.cpp +++ b/src/synced_context.cpp @@ -358,6 +358,16 @@ void synced_context::add_undo_commands(const config& commands, const game_events undo_commands_.emplace_front(commands, ctx); } +void synced_context::add_undo_commands(int idx, const game_events::queued_event& ctx) +{ + undo_commands_.emplace_front(idx, ctx); +} + +void synced_context::add_undo_commands(int idx, const config& args, const game_events::queued_event& ctx) +{ + undo_commands_.emplace_front(idx, args, ctx); +} + set_scontext_synced_base::set_scontext_synced_base() : new_rng_(synced_context::get_rng_for_action()) , old_rng_(randomness::generator) diff --git a/src/synced_context.hpp b/src/synced_context.hpp index b6634b39359..1c60c7d990d 100644 --- a/src/synced_context.hpp +++ b/src/synced_context.hpp @@ -190,13 +190,24 @@ public: /** If we are in a mp game, ask the server, otherwise generate the answer ourselves. */ static config ask_server_choice(const server_choice&); - typedef std::deque> event_list; + struct event_info { + config cmds_; + std::optional lua_; + game_events::queued_event evt_; + event_info(const config& cmds, game_events::queued_event evt) : cmds_(cmds), evt_(evt) {} + event_info(int lua, game_events::queued_event evt) : lua_(lua), evt_(evt) {} + event_info(int lua, const config& args, game_events::queued_event evt) : cmds_(args), lua_(lua), evt_(evt) {} + }; + + typedef std::deque event_list; static event_list& get_undo_commands() { return undo_commands_; } static void add_undo_commands(const config& commands, const game_events::queued_event& ctx); + static void add_undo_commands(int fcn_idx, const game_events::queued_event& ctx); + static void add_undo_commands(int fcn_idx, const config& args, const game_events::queued_event& ctx); static void reset_undo_commands() { @@ -221,7 +232,7 @@ private: /** Used to restore the unit id manager when undoing. */ static inline int last_unit_id_ = 0; - /** Actions wml to be executed when the current action is undone. */ + /** Actions to be executed when the current action is undone. */ static inline event_list undo_commands_ {}; }; diff --git a/utils/emmylua/wesnoth/game_events.lua b/utils/emmylua/wesnoth/game_events.lua index 0ff05c07ee6..70ac27e020d 100644 --- a/utils/emmylua/wesnoth/game_events.lua +++ b/utils/emmylua/wesnoth/game_events.lua @@ -53,7 +53,8 @@ function wesnoth.game_events.add(opts) end ---@param name string|string[] The event or events to handle ---@param action fun(WML) The function called when the event triggers ---@param priority? number Events execute in order of decreasing priority, and secondarily in order of addition -function wesnoth.game_events.add_repeating(name, action, priority) end +---@param undo_action? fun(WML) The function called if undoing after the event triggers. +function wesnoth.game_events.add_repeating(name, action, priority, undo_action) end ---Add a game event handler triggered from a menu item, bound directly to a Lua function ---@param id string @@ -83,3 +84,11 @@ function wesnoth.game_events.fire_by_id(id, first, second, data) end ---Remove an event handler by ID ---@param id string The event to remove function wesnoth.game_events.remove(id) end + +---Set whether the current event is undoable. +---@param can_undo boolean Whether the event is undoable. +function wesnoth.game_events.set_undoable(can_undo) end + +---Add undo actions for the current event +---@param actions WML|fun(ctx):boolean The undo actions, either as ActionWML or a Lua function. +function wesnoth.game_events.add_undo_actions(actions) end