From: Perttu Ahola Date: Thu, 26 Jul 2012 19:06:45 +0000 (+0300) Subject: Experimental-ish rollback functionality X-Git-Url: http://81.2.79.47:8989/gitweb/?a=commitdiff_plain;h=0190f9b077dcb2b8cb41c622dd91ffc1e04dacac;p=zefram%2Fminetest%2Fminetest_engine.git Experimental-ish rollback functionality --- diff --git a/builtin/chatcommands.lua b/builtin/chatcommands.lua index da9e6c78..49ae8c63 100644 --- a/builtin/chatcommands.lua +++ b/builtin/chatcommands.lua @@ -496,3 +496,75 @@ minetest.register_chatcommand("pulverize", { end, }) +-- Key = player name +minetest.rollback_punch_callbacks = {} + +minetest.register_on_punchnode(function(pos, node, puncher) + local name = puncher:get_player_name() + if minetest.rollback_punch_callbacks[name] then + minetest.rollback_punch_callbacks[name](pos, node, puncher) + minetest.rollback_punch_callbacks[name] = nil + end +end) + +minetest.register_chatcommand("rollback_check", { + params = "[] []", + description = "check who has last touched a node or near it, ".. + "max. ago (default range=0, seconds=86400=24h)", + privs = {rollback=true}, + func = function(name, param) + local range, seconds = string.match(param, "(%d+) *(%d*)") + range = tonumber(range) or 0 + seconds = tonumber(seconds) or 86400 + minetest.chat_send_player(name, "Punch a node (limits set: range=".. + dump(range).." seconds="..dump(seconds).."s)") + minetest.rollback_punch_callbacks[name] = function(pos, node, puncher) + local name = puncher:get_player_name() + local actor, act_p, act_seconds = + minetest.rollback_get_last_node_actor(pos, range, seconds) + if actor == "" then + minetest.chat_send_player(name, "Nobody has touched the ".. + "specified location in "..dump(seconds).." seconds") + return + end + local nodedesc = "this node" + if act_p.x ~= pos.x or act_p.y ~= pos.y or act_p.z ~= pos.z then + nodedesc = minetest.pos_to_string(act_p) + end + minetest.chat_send_player(name, "Last actor on "..nodedesc.." was ".. + actor..", "..dump(act_seconds).."s ago") + end + end, +}) + +minetest.register_chatcommand("rollback", { + params = " [] | :liquid []", + description = "revert actions of a player; default for is 60", + privs = {rollback=true}, + func = function(name, param) + local target_name, seconds = string.match(param, ":([^ ]+) *(%d*)") + if not target_name then + local player_name = nil; + player_name, seconds = string.match(param, "([^ ]+) *(%d*)") + if not player_name then + minetest.chat_send_player(name, "Invalid parameters. See /help rollback and /help rollback_check") + return + end + target_name = "player:"..player_name + end + seconds = tonumber(seconds) or 60 + minetest.chat_send_player(name, "Reverting actions of ".. + dump(target_name).." since "..dump(seconds).." seconds.") + local success, log = minetest.rollback_revert_actions_by( + target_name, seconds) + for _,line in ipairs(log) do + minetest.chat_send_player(name, line) + end + if success then + minetest.chat_send_player(name, "Reverting actions succeeded.") + else + minetest.chat_send_player(name, "Reverting actions FAILED.") + end + end, +}) + diff --git a/builtin/privileges.lua b/builtin/privileges.lua index 6cb42c10..9ec09d7f 100644 --- a/builtin/privileges.lua +++ b/builtin/privileges.lua @@ -44,5 +44,5 @@ minetest.register_privilege("fast", { description = "Can walk fast using the fast_move mode", give_to_singleplayer = false, }) - +minetest.register_privilege("rollback", "Can use the rollback functionality") diff --git a/doc/lua_api.txt b/doc/lua_api.txt index d47281b2..46ea3a86 100644 --- a/doc/lua_api.txt +++ b/doc/lua_api.txt @@ -889,6 +889,14 @@ minetest.get_craft_recipe(output) -> input stack 5, stack 6, stack 7, stack 8, stack 9 } ^ input.items = nil if no recipe found +Rollbacks: +minetest.rollback_get_last_node_actor(p, range, seconds) -> actor, p, seconds +^ Find who has done something to a node, or near a node +^ actor: "player:", also "liquid". +minetest.rollback_revert_actions_by(actor, seconds) -> bool, log messages +^ Revert latest actions of someone +^ actor: "player:", also "liquid". + Defaults for the on_* item definition functions: (These return the leftover itemstack) minetest.item_place_node(itemstack, placer, pointed_thing) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e369b962..43d7f241 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -153,6 +153,8 @@ configure_file( ) set(common_SRCS + rollback_interface.cpp + rollback.cpp genericobject.cpp voxelalgorithms.cpp sound.cpp diff --git a/src/content_abm.cpp b/src/content_abm.cpp index edadfe99..12dbeea8 100644 --- a/src/content_abm.cpp +++ b/src/content_abm.cpp @@ -26,6 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "settings.h" #include "mapblock.h" // For getNodeBlockPos #include "mapgen.h" // For mapgen::make_tree +#include "map.h" #define PP(x) "("<<(x).X<<","<<(x).Y<<","<<(x).Z<<")" diff --git a/src/content_cao.cpp b/src/content_cao.cpp index 58ff130f..aa5c2d67 100644 --- a/src/content_cao.cpp +++ b/src/content_cao.cpp @@ -39,6 +39,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "util/numeric.h" // For IntervalLimiter #include "util/serialize.h" #include "util/mathconstants.h" +#include "map.h" class Settings; struct ToolCapabilities; diff --git a/src/content_cso.cpp b/src/content_cso.cpp index 6f7ecbe5..666f1773 100644 --- a/src/content_cso.cpp +++ b/src/content_cso.cpp @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "environment.h" #include "gamedef.h" #include "log.h" +#include "map.h" static void setBillboardTextureMatrix(scene::IBillboardSceneNode *bill, float txs, float tys, int col, int row) diff --git a/src/environment.cpp b/src/environment.cpp index 02ca1f71..9390101e 100644 --- a/src/environment.cpp +++ b/src/environment.cpp @@ -349,6 +349,17 @@ ServerEnvironment::~ServerEnvironment() } } +Map & ServerEnvironment::getMap() +{ + return *m_map; +} + +ServerMap & ServerEnvironment::getServerMap() +{ + return *m_map; +} + + void ServerEnvironment::serializePlayers(const std::string &savedir) { std::string players_path = savedir + "/players"; diff --git a/src/environment.h b/src/environment.h index 0e4b85e0..6c52b003 100644 --- a/src/environment.h +++ b/src/environment.h @@ -33,11 +33,12 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include "irrlichttypes_extrabloated.h" #include "player.h" -#include "map.h" #include #include "activeobject.h" #include "util/container.h" #include "util/numeric.h" +#include "mapnode.h" +#include "mapblock.h" class Server; class ServerEnvironment; @@ -46,6 +47,8 @@ class ServerActiveObject; typedef struct lua_State lua_State; class ITextureSource; class IGameDef; +class Map; +class ServerMap; class ClientMap; class Environment @@ -191,11 +194,9 @@ public: IBackgroundBlockEmerger *emerger); ~ServerEnvironment(); - Map & getMap() - { return *m_map; } + Map & getMap(); - ServerMap & getServerMap() - { return *m_map; } + ServerMap & getServerMap(); lua_State* getLua() { return m_lua; } diff --git a/src/gamedef.h b/src/gamedef.h index 88c15be7..87918d72 100644 --- a/src/gamedef.h +++ b/src/gamedef.h @@ -29,6 +29,7 @@ class ICraftDefManager; class ITextureSource; class ISoundManager; class MtEventManager; +class IRollbackReportSink; /* An interface for fetching game-global definitions like tool and @@ -54,6 +55,10 @@ public: // Only usable on the client virtual ISoundManager* getSoundManager()=0; virtual MtEventManager* getEventManager()=0; + + // Only usable on the server, and NOT thread-safe. It is usable from the + // environment thread. + virtual IRollbackReportSink* getRollbackReportSink(){return NULL;} // Used on the client virtual bool checkLocalPrivilege(const std::string &priv) @@ -66,6 +71,7 @@ public: ITextureSource* tsrc(){return getTextureSource();} ISoundManager* sound(){return getSoundManager();} MtEventManager* event(){return getEventManager();} + IRollbackReportSink* rollback(){return getRollbackReportSink();} }; #endif diff --git a/src/inventorymanager.cpp b/src/inventorymanager.cpp index 06978fbb..85668d64 100644 --- a/src/inventorymanager.cpp +++ b/src/inventorymanager.cpp @@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "main.h" // for g_settings #include "settings.h" #include "craftdef.h" +#include "rollback_interface.h" #define PP(x) "("<<(x).X<<","<<(x).Y<<","<<(x).Z<<")" @@ -199,6 +200,14 @@ void IMoveAction::apply(InventoryManager *mgr, ServerActiveObject *player, IGame return; } + /* + Do not handle rollback if both inventories are that of the same player + */ + bool ignore_rollback = ( + from_inv.type == InventoryLocation::PLAYER && + to_inv.type == InventoryLocation::PLAYER && + from_inv.name == to_inv.name); + /* Collect information of endpoints */ @@ -343,6 +352,41 @@ void IMoveAction::apply(InventoryManager *mgr, ServerActiveObject *player, IGame <<" i="<rollback(); + + // If source is not infinite, record item take + if(!src_can_take_count != -1){ + RollbackAction action; + std::string loc; + { + std::ostringstream os(std::ios::binary); + from_inv.serialize(os); + loc = os.str(); + } + action.setModifyInventoryStack(loc, from_list, from_i, false, + src_item.getItemString()); + rollback->reportAction(action); + } + // If destination is not infinite, record item put + if(!dst_can_put_count != -1){ + RollbackAction action; + std::string loc; + { + std::ostringstream os(std::ios::binary); + to_inv.serialize(os); + loc = os.str(); + } + action.setModifyInventoryStack(loc, to_list, to_i, true, + src_item.getItemString()); + rollback->reportAction(action); + } + } + /* Report move to endpoints */ @@ -405,7 +449,7 @@ void IMoveAction::apply(InventoryManager *mgr, ServerActiveObject *player, IGame L, from_inv.p, from_list, from_i, src_item, player); } } - + mgr->setInventoryModified(from_inv); if(inv_from != inv_to) mgr->setInventoryModified(to_inv); @@ -488,6 +532,11 @@ void IDropAction::apply(InventoryManager *mgr, ServerActiveObject *player, IGame return; } + /* + Do not handle rollback if inventory is player's + */ + bool ignore_src_rollback = (from_inv.type == InventoryLocation::PLAYER); + /* Collect information of endpoints */ @@ -575,6 +624,28 @@ void IDropAction::apply(InventoryManager *mgr, ServerActiveObject *player, IGame scriptapi_nodemeta_inventory_on_take( L, from_inv.p, from_list, from_i, src_item, player); } + + /* + Record rollback information + */ + if(!ignore_src_rollback) + { + IRollbackReportSink *rollback = gamedef->rollback(); + + // If source is not infinite, record item take + if(!src_can_take_count != -1){ + RollbackAction action; + std::string loc; + { + std::ostringstream os(std::ios::binary); + from_inv.serialize(os); + loc = os.str(); + } + action.setModifyInventoryStack(loc, from_list, from_i, + false, src_item.getItemString()); + rollback->reportAction(action); + } + } } void IDropAction::clientApply(InventoryManager *mgr, IGameDef *gamedef) diff --git a/src/map.cpp b/src/map.cpp index dc1f4506..73412210 100644 --- a/src/map.cpp +++ b/src/map.cpp @@ -32,6 +32,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "nodedef.h" #include "gamedef.h" #include "util/directiontables.h" +#include "rollback_interface.h" #define PP(x) "("<<(x).X<<","<<(x).Y<<","<<(x).Z<<")" @@ -932,12 +933,12 @@ void Map::updateLighting(core::map & a_blocks, void Map::addNodeAndUpdate(v3s16 p, MapNode n, core::map &modified_blocks) { - INodeDefManager *nodemgr = m_gamedef->ndef(); + INodeDefManager *ndef = m_gamedef->ndef(); /*PrintInfo(m_dout); m_dout< light_sources; + + /* + Collect old node for rollback + */ + RollbackNode rollback_oldnode(this, p, m_gamedef); /* If there is a node at top and it doesn't have sunlight, @@ -960,7 +966,7 @@ void Map::addNodeAndUpdate(v3s16 p, MapNode n, try{ MapNode topnode = getNode(toppos); - if(topnode.getLight(LIGHTBANK_DAY, nodemgr) != LIGHT_SUN) + if(topnode.getLight(LIGHTBANK_DAY, ndef) != LIGHT_SUN) node_under_sunlight = false; } catch(InvalidPositionException &e) @@ -980,7 +986,7 @@ void Map::addNodeAndUpdate(v3s16 p, MapNode n, { enum LightBank bank = banks[i]; - u8 lightwas = getNode(p).getLight(bank, nodemgr); + u8 lightwas = getNode(p).getLight(bank, ndef); // Add the block of the added node to modified_blocks v3s16 blockpos = getNodeBlockPos(p); @@ -997,16 +1003,16 @@ void Map::addNodeAndUpdate(v3s16 p, MapNode n, // light again into this. unLightNeighbors(bank, p, lightwas, light_sources, modified_blocks); - n.setLight(bank, 0, nodemgr); + n.setLight(bank, 0, ndef); } /* If node lets sunlight through and is under sunlight, it has sunlight too. */ - if(node_under_sunlight && nodemgr->get(n).sunlight_propagates) + if(node_under_sunlight && ndef->get(n).sunlight_propagates) { - n.setLight(LIGHTBANK_DAY, LIGHT_SUN, nodemgr); + n.setLight(LIGHTBANK_DAY, LIGHT_SUN, ndef); } /* @@ -1028,7 +1034,7 @@ void Map::addNodeAndUpdate(v3s16 p, MapNode n, TODO: This could be optimized by mass-unlighting instead of looping */ - if(node_under_sunlight && !nodemgr->get(n).sunlight_propagates) + if(node_under_sunlight && !ndef->get(n).sunlight_propagates) { s16 y = p.Y - 1; for(;; y--){ @@ -1044,12 +1050,12 @@ void Map::addNodeAndUpdate(v3s16 p, MapNode n, break; } - if(n2.getLight(LIGHTBANK_DAY, nodemgr) == LIGHT_SUN) + if(n2.getLight(LIGHTBANK_DAY, ndef) == LIGHT_SUN) { unLightNeighbors(LIGHTBANK_DAY, - n2pos, n2.getLight(LIGHTBANK_DAY, nodemgr), + n2pos, n2.getLight(LIGHTBANK_DAY, ndef), light_sources, modified_blocks); - n2.setLight(LIGHTBANK_DAY, 0, nodemgr); + n2.setLight(LIGHTBANK_DAY, 0, ndef); setNode(n2pos, n2); } else @@ -1078,6 +1084,17 @@ void Map::addNodeAndUpdate(v3s16 p, MapNode n, block->expireDayNightDiff(); } + /* + Report for rollback + */ + if(m_gamedef->rollback()) + { + RollbackNode rollback_newnode(this, p, m_gamedef); + RollbackAction action; + action.setSetNode(p, rollback_oldnode, rollback_newnode); + m_gamedef->rollback()->reportAction(action); + } + /* Add neighboring liquid nodes and the node itself if it is liquid (=water node was added) to transform queue. @@ -1099,7 +1116,7 @@ void Map::addNodeAndUpdate(v3s16 p, MapNode n, v3s16 p2 = p + dirs[i]; MapNode n2 = getNode(p2); - if(nodemgr->get(n2).isLiquid() || n2.getContent() == CONTENT_AIR) + if(ndef->get(n2).isLiquid() || n2.getContent() == CONTENT_AIR) { m_transforming_liquid.push_back(p2); } @@ -1115,7 +1132,7 @@ void Map::addNodeAndUpdate(v3s16 p, MapNode n, void Map::removeNodeAndUpdate(v3s16 p, core::map &modified_blocks) { - INodeDefManager *nodemgr = m_gamedef->ndef(); + INodeDefManager *ndef = m_gamedef->ndef(); /*PrintInfo(m_dout); m_dout<expireDayNightDiff(); } + /* + Report for rollback + */ + if(m_gamedef->rollback()) + { + RollbackNode rollback_newnode(this, p, m_gamedef); + RollbackAction action; + action.setSetNode(p, rollback_oldnode, rollback_newnode); + m_gamedef->rollback()->reportAction(action); + } + /* Add neighboring liquid nodes and this node to transform queue. (it's vital for the node itself to get updated last.) @@ -1275,7 +1308,7 @@ void Map::removeNodeAndUpdate(v3s16 p, v3s16 p2 = p + dirs[i]; MapNode n2 = getNode(p2); - if(nodemgr->get(n2).isLiquid() || n2.getContent() == CONTENT_AIR) + if(ndef->get(n2).isLiquid() || n2.getContent() == CONTENT_AIR) { m_transforming_liquid.push_back(p2); } @@ -1588,6 +1621,11 @@ void Map::transformLiquids(core::map & modified_blocks) DSTACK(__FUNCTION_NAME); //TimeTaker timer("transformLiquids()"); + /* + If something goes wrong, liquids are to blame + */ + RollbackScopeActor rollback_scope(m_gamedef->rollback(), "liquid"); + u32 loopcount = 0; u32 initial_size = m_transforming_liquid.size(); @@ -1791,7 +1829,22 @@ void Map::transformLiquids(core::map & modified_blocks) n0.param2 = ~(LIQUID_LEVEL_MASK | LIQUID_FLOW_DOWN_MASK); } n0.setContent(new_node_content); + + // Get old node for rollback + RollbackNode rollback_oldnode(this, p0, m_gamedef); + + // Set node setNode(p0, n0); + + // Report for rollback + if(m_gamedef->rollback()) + { + RollbackNode rollback_newnode(this, p0, m_gamedef); + RollbackAction action; + action.setSetNode(p0, rollback_oldnode, rollback_newnode); + m_gamedef->rollback()->reportAction(action); + } + v3s16 blockpos = getNodeBlockPos(p0); MapBlock *block = getBlockNoCreateNoEx(blockpos); if(block != NULL) { diff --git a/src/map.h b/src/map.h index b561e5e7..30cf626b 100644 --- a/src/map.h +++ b/src/map.h @@ -44,6 +44,7 @@ class ServerMapSector; class MapBlock; class NodeMetadata; class IGameDef; +class IRollbackReportSink; namespace mapgen{ struct BlockMakeData; @@ -169,7 +170,7 @@ public: void removeEventReceiver(MapEventReceiver *event_receiver); // event shall be deleted by caller after the call. void dispatchEvent(MapEditEvent *event); - + // On failure returns NULL MapSector * getSectorNoGenerateNoExNoLock(v2s16 p2d); // Same as the above (there exists no lock anymore) @@ -336,7 +337,7 @@ protected: IGameDef *m_gamedef; core::map m_event_receivers; - + core::map m_sectors; // Be sure to set this to NULL when the cached sector is deleted diff --git a/src/rollback.cpp b/src/rollback.cpp new file mode 100644 index 00000000..3fe79105 --- /dev/null +++ b/src/rollback.cpp @@ -0,0 +1,293 @@ +/* +Minetest-c55 +Copyright (C) 2012 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program 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 Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "rollback.h" +#include +#include +#include +#include "log.h" +#include "mapnode.h" +#include "gamedef.h" +#include "nodedef.h" +#include "util/serialize.h" +#include "util/string.h" +#include "strfnd.h" +#include "inventorymanager.h" // deserializing InventoryLocations + +#define PP(x) "("<<(x).X<<","<<(x).Y<<","<<(x).Z<<")" + +class RollbackManager: public IRollbackManager +{ +public: + // IRollbackManager interface + + void reportAction(const RollbackAction &action_) + { + // Ignore if not important + if(!action_.isImportant(m_gamedef)) + return; + RollbackAction action = action_; + action.unix_time = time(0); + action.actor = m_current_actor; + infostream<<"RollbackManager::reportAction():" + <<" time="<::const_iterator + i = m_action_todisk_buffer.begin(); + i != m_action_todisk_buffer.end(); i++) + { + // Do not save stuff that does not have an actor + if(i->actor == "") + continue; + of<unix_time; + of<<" "; + of<actor); + of<<" "; + std::string action_s = i->toString(); + of<= 100) + flush(); + } + + bool readFile(std::list &dst) + { + // Load whole file to memory + std::ifstream f(m_filepath.c_str(), std::ios::in); + if(!f.good()){ + errorstream<<"RollbackManager::readFile(): Could not open " + <<"file for reading: \""< getEntriesSince(int first_time) + { + infostream<<"RollbackManager::getEntriesSince("< action_buffer; + // Use the latest buffer if it is long enough + if(!m_action_latest_buffer.empty() && + m_action_latest_buffer.begin()->unix_time <= first_time){ + action_buffer = m_action_latest_buffer; + } + else + { + // Save all remaining stuff + flush(); + // Load whole file to memory + bool good = readFile(action_buffer); + if(!good){ + errorstream<<"RollbackManager::getEntriesSince(): Failed to" + <<" open file; using data in memory."< action_buffer = getEntriesSince(first_time); + + std::list result; + + for(std::list::const_reverse_iterator + i = action_buffer.rbegin(); + i != action_buffer.rend(); i++) + { + if(i->unix_time < first_time) + break; + + // Find position of action or continue + v3s16 action_p; + + if(i->type == RollbackAction::TYPE_SET_NODE) + { + action_p = i->p; + } + else if(i->type == RollbackAction::TYPE_MODIFY_INVENTORY_STACK) + { + InventoryLocation loc; + loc.deSerialize(i->inventory_location); + if(loc.type != InventoryLocation::NODEMETA) + continue; + action_p = loc.p; + } + else + continue; + + if(range == 0){ + if(action_p != p) + continue; + } else { + if(abs(action_p.X - p.X) > range || + abs(action_p.Y - p.Y) > range || + abs(action_p.Z - p.Z) > range) + continue; + } + + if(act_p) + *act_p = action_p; + if(act_seconds) + *act_seconds = cur_time - i->unix_time; + return i->actor; + } + return ""; + } + + std::list getRevertActions(const std::string &actor_filter, + int seconds) + { + infostream<<"RollbackManager::getRevertActions("< action_buffer = getEntriesSince(first_time); + + std::list result; + + for(std::list::const_reverse_iterator + i = action_buffer.rbegin(); + i != action_buffer.rend(); i++) + { + if(i->unix_time < first_time) + break; + if(i->actor != actor_filter) + continue; + const RollbackAction &action = *i; + /*infostream<<"RollbackManager::revertAction(): Should revert" + <<" time="< + +class IGameDef; + +class IRollbackManager: public IRollbackReportSink +{ +public: + // IRollbackReportManager + virtual void reportAction(const RollbackAction &action) = 0; + virtual std::string getActor() = 0; + virtual void setActor(const std::string &actor) = 0; + + virtual ~IRollbackManager(){} + virtual void flush() = 0; + // Get last actor that did something to position p, but not further than + // in history + virtual std::string getLastNodeActor(v3s16 p, int range, int seconds, + v3s16 *act_p, int *act_seconds) = 0; + // Get actions to revert of history made by + virtual std::list getRevertActions(const std::string &actor, + int seconds) = 0; +}; + +IRollbackManager *createRollbackManager(const std::string &filepath, IGameDef *gamedef); + +#endif diff --git a/src/rollback_interface.cpp b/src/rollback_interface.cpp new file mode 100644 index 00000000..e15fe3da --- /dev/null +++ b/src/rollback_interface.cpp @@ -0,0 +1,398 @@ +/* +Minetest-c55 +Copyright (C) 2012 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program 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 Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "rollback_interface.h" +#include +#include "util/serialize.h" +#include "util/string.h" +#include "util/numeric.h" +#include "map.h" +#include "gamedef.h" +#include "nodedef.h" +#include "nodemetadata.h" +#include "exceptions.h" +#include "log.h" +#include "inventorymanager.h" +#include "inventory.h" +#include "mapblock.h" + +#define PP(x) "("<<(x).X<<","<<(x).Y<<","<<(x).Z<<")" + +RollbackNode::RollbackNode(Map *map, v3s16 p, IGameDef *gamedef) +{ + INodeDefManager *ndef = gamedef->ndef(); + MapNode n = map->getNodeNoEx(p); + name = ndef->get(n).name; + param1 = n.param1; + param2 = n.param2; + NodeMetadata *metap = map->getNodeMetadata(p); + if(metap){ + std::ostringstream os(std::ios::binary); + metap->serialize(os); + meta = os.str(); + } +} + +std::string RollbackAction::toString() const +{ + switch(type){ + case TYPE_SET_NODE: { + std::ostringstream os(std::ios::binary); + os<<"[set_node"; + os<<" "; + os<<"("<ndef(); + // Both are of the same name, so a single definition is needed + const ContentFeatures &def = ndef->get(n_old.name); + // If the type is flowing liquid, action is not important + if(def.liquid_type == LIQUID_FLOWING) + return false; + // Otherwise action is important + return true; } + default: + return true; + } +} + +bool RollbackAction::applyRevert(Map *map, InventoryManager *imgr, IGameDef *gamedef) const +{ + try{ + switch(type){ + case TYPE_NOTHING: + return true; + case TYPE_SET_NODE: { + INodeDefManager *ndef = gamedef->ndef(); + // Make sure position is loaded from disk + map->emergeBlock(getContainerPos(p, MAP_BLOCKSIZE), false); + // Check current node + MapNode current_node = map->getNodeNoEx(p); + std::string current_name = ndef->get(current_node).name; + // If current node not the new node, it's bad + if(current_name != n_new.name) + return false; + /*// If current node not the new node and not ignore, it's bad + if(current_name != n_new.name && current_name != "ignore") + return false;*/ + // Create rollback node + MapNode n(ndef, n_old.name, n_old.param1, n_old.param2); + // Set rollback node + try{ + if(!map->addNodeWithEvent(p, n)){ + infostream<<"RollbackAction::applyRevert(): " + <<"AddNodeWithEvent failed at " + <getNodeMetadata(p); + if(n_old.meta != ""){ + if(!meta){ + meta = new NodeMetadata(gamedef); + map->setNodeMetadata(p, meta); + } + std::istringstream is(n_old.meta, std::ios::binary); + meta->deSerialize(is); + } else { + map->removeNodeMetadata(p); + } + // NOTE: This same code is in scriptapi.cpp + // Inform other things that the metadata has changed + v3s16 blockpos = getContainerPos(p, MAP_BLOCKSIZE); + MapEditEvent event; + event.type = MEET_BLOCK_NODE_METADATA_CHANGED; + event.p = blockpos; + map->dispatchEvent(&event); + // Set the block to be saved + MapBlock *block = map->getBlockNoCreateNoEx(blockpos); + if(block) + block->raiseModified(MOD_STATE_WRITE_NEEDED, + "NodeMetaRef::reportMetadataChange"); + }catch(InvalidPositionException &e){ + infostream<<"RollbackAction::applyRevert(): " + <<"InvalidPositionException: "<idef()); + Inventory *inv = imgr->getInventory(loc); + if(!inv){ + infostream<<"RollbackAction::applyRevert(): Could not get " + "inventory at "<getList(inventory_list); + if(!list){ + infostream<<"RollbackAction::applyRevert(): Could not get " + "inventory list \""<getSize() <= inventory_index){ + infostream<<"RollbackAction::applyRevert(): List index " + <getItem(inventory_index).name != stack.name) + return false; + list->takeItem(inventory_index, stack.count); + } else { + list->addItem(inventory_index, stack); + } + // Inventory was modified; send to clients + imgr->setInventoryModified(loc); + return true; } + default: + errorstream<<"RollbackAction::applyRevert(): type not handled" + < + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program 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 Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#ifndef ROLLBACK_INTERFACE_HEADER +#define ROLLBACK_INTERFACE_HEADER + +#include "irr_v3d.h" +#include +#include +#include "exceptions.h" + +class Map; +class IGameDef; +struct MapNode; +class InventoryManager; + +struct RollbackNode +{ + std::string name; + int param1; + int param2; + std::string meta; + + bool operator==(const RollbackNode &other) + { + return (name == other.name && param1 == other.param1 && + param2 == other.param2 && meta == other.meta); + } + bool operator!=(const RollbackNode &other) + { + return !(*this == other); + } + + RollbackNode(): + param1(0), + param2(0) + {} + + RollbackNode(Map *map, v3s16 p, IGameDef *gamedef); +}; + +struct RollbackAction +{ + enum Type{ + TYPE_NOTHING, + TYPE_SET_NODE, + TYPE_MODIFY_INVENTORY_STACK, + } type; + + int unix_time; + std::string actor; + + v3s16 p; + RollbackNode n_old; + RollbackNode n_new; + + std::string inventory_location; + std::string inventory_list; + u32 inventory_index; + bool inventory_add; + std::string inventory_stack; + + RollbackAction(): + type(TYPE_NOTHING) + {} + + void setSetNode(v3s16 p_, const RollbackNode &n_old_, + const RollbackNode &n_new_) + { + type = TYPE_SET_NODE; + p = p_; + n_old = n_old_; + n_new = n_new_; + } + + void setModifyInventoryStack(const std::string &inventory_location_, + const std::string &inventory_list_, int index_, + bool add_, const std::string &inventory_stack_) + { + type = TYPE_MODIFY_INVENTORY_STACK; + inventory_location = inventory_location_; + inventory_list = inventory_list_; + inventory_index = index_; + inventory_add = add_; + inventory_stack = inventory_stack_; + } + + // String should not contain newlines or nulls + std::string toString() const; + void fromStream(std::istream &is) throw(SerializationError); + + // Eg. flowing water level changes are not important + bool isImportant(IGameDef *gamedef) const; + + bool applyRevert(Map *map, InventoryManager *imgr, IGameDef *gamedef) const; +}; + +class IRollbackReportSink +{ +public: + virtual ~IRollbackReportSink(){} + virtual void reportAction(const RollbackAction &action) = 0; + virtual std::string getActor() = 0; + virtual void setActor(const std::string &actor) = 0; +}; + +class RollbackScopeActor +{ +public: + RollbackScopeActor(IRollbackReportSink *sink, const std::string &actor): + m_sink(sink) + { + if(m_sink){ + m_actor_was = m_sink->getActor(); + m_sink->setActor(actor); + } + } + ~RollbackScopeActor() + { + if(m_sink){ + m_sink->setActor(m_actor_was); + } + } +private: + IRollbackReportSink *m_sink; + std::string m_actor_was; +}; + +#endif diff --git a/src/scriptapi.cpp b/src/scriptapi.cpp index e1d91854..61488abb 100644 --- a/src/scriptapi.cpp +++ b/src/scriptapi.cpp @@ -47,6 +47,7 @@ extern "C" { #include "daynightratio.h" #include "noise.h" // PseudoRandom for LuaPseudoRandom #include "util/pointedthing.h" +#include "rollback.h" static void stackDump(lua_State *L, std::ostream &o) { @@ -2106,6 +2107,7 @@ private: static void reportMetadataChange(NodeMetaRef *ref) { + // NOTE: This same code is in rollback_interface.cpp // Inform other things that the metadata has changed v3s16 blockpos = getNodeBlockPos(ref->m_p); MapEditEvent event; @@ -4853,6 +4855,55 @@ static int l_get_craft_recipe(lua_State *L) return 1; } +// rollback_get_last_node_actor(p, range, seconds) -> actor, p, seconds +static int l_rollback_get_last_node_actor(lua_State *L) +{ + v3s16 p = read_v3s16(L, 1); + int range = luaL_checknumber(L, 2); + int seconds = luaL_checknumber(L, 3); + Server *server = get_server(L); + IRollbackManager *rollback = server->getRollbackManager(); + v3s16 act_p; + int act_seconds = 0; + std::string actor = rollback->getLastNodeActor(p, range, seconds, &act_p, &act_seconds); + lua_pushstring(L, actor.c_str()); + push_v3s16(L, act_p); + lua_pushnumber(L, act_seconds); + return 3; +} + +// rollback_revert_actions_by(actor, seconds) -> bool, log messages +static int l_rollback_revert_actions_by(lua_State *L) +{ + std::string actor = luaL_checkstring(L, 1); + int seconds = luaL_checknumber(L, 2); + Server *server = get_server(L); + IRollbackManager *rollback = server->getRollbackManager(); + std::list actions = rollback->getRevertActions(actor, seconds); + std::list log; + bool success = server->rollbackRevertActions(actions, &log); + // Push boolean result + lua_pushboolean(L, success); + // Get the table insert function and push the log table + lua_getglobal(L, "table"); + lua_getfield(L, -1, "insert"); + int table_insert = lua_gettop(L); + lua_newtable(L); + int table = lua_gettop(L); + for(std::list::const_iterator i = log.begin(); + i != log.end(); i++) + { + lua_pushvalue(L, table_insert); + lua_pushvalue(L, table); + lua_pushstring(L, i->c_str()); + if(lua_pcall(L, 2, 0, 0)) + script_error(L, "error: %s", lua_tostring(L, -1)); + } + lua_remove(L, -2); // Remove table + lua_remove(L, -2); // Remove insert + return 2; +} + static const struct luaL_Reg minetest_f [] = { {"debug", l_debug}, {"log", l_log}, @@ -4880,6 +4931,8 @@ static const struct luaL_Reg minetest_f [] = { {"notify_authentication_modified", l_notify_authentication_modified}, {"get_craft_result", l_get_craft_result}, {"get_craft_recipe", l_get_craft_recipe}, + {"rollback_get_last_node_actor", l_rollback_get_last_node_actor}, + {"rollback_revert_actions_by", l_rollback_revert_actions_by}, {NULL, NULL} }; diff --git a/src/server.cpp b/src/server.cpp index 21c936e6..a868a042 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -54,6 +54,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "util/string.h" #include "util/pointedthing.h" #include "util/mathconstants.h" +#include "rollback.h" #define PP(x) "("<<(x).X<<","<<(x).Y<<","<<(x).Z<<")" @@ -934,6 +935,8 @@ Server::Server( m_env(NULL), m_con(PROTOCOL_ID, 512, CONNECTION_TIMEOUT, this), m_banmanager(path_world+DIR_DELIM+"ipban.txt"), + m_rollback(NULL), + m_rollback_sink_enabled(true), m_lua(NULL), m_itemdef(createItemDefManager()), m_nodedef(createNodeDefManager()), @@ -973,6 +976,10 @@ Server::Server( infostream<<"- config: "<getName()); + /* Note: Always set inventory not sent, to repair cases where the client made a bad prediction. @@ -2949,6 +2961,12 @@ void Server::ProcessData(u8 *data, u32 datasize, u16 peer_id) return; } + /* + If something goes wrong, this player is to blame + */ + RollbackScopeActor rollback_scope(m_rollback, + std::string("player:")+player->getName()); + /* 0: start digging or punch object */ @@ -3204,8 +3222,23 @@ void Server::ProcessData(u8 *data, u32 datasize, u16 peer_id) fields[fieldname] = fieldvalue; } + // If something goes wrong, this player is to blame + RollbackScopeActor rollback_scope(m_rollback, + std::string("player:")+player->getName()); + + // Check the target node for rollback data; leave others unnoticed + RollbackNode rn_old(&m_env->getMap(), p, this); + scriptapi_node_on_receive_fields(m_lua, p, formname, fields, playersao); + + // Report rollback data + RollbackNode rn_new(&m_env->getMap(), p, this); + if(rollback() && rn_new != rn_old){ + RollbackAction action; + action.setSetNode(p, rn_old, rn_new); + rollback()->reportAction(action); + } } else if(command == TOSERVER_INVENTORY_FIELDS) { @@ -4522,6 +4555,73 @@ Inventory* Server::createDetachedInventory(const std::string &name) return inv; } +class BoolScopeSet +{ +public: + BoolScopeSet(bool *dst, bool val): + m_dst(dst) + { + m_orig_state = *m_dst; + *m_dst = val; + } + ~BoolScopeSet() + { + *m_dst = m_orig_state; + } +private: + bool *m_dst; + bool m_orig_state; +}; + +// actions: time-reversed list +// Return value: success/failure +bool Server::rollbackRevertActions(const std::list &actions, + std::list *log) +{ + infostream<<"Server::rollbackRevertActions(len="<getMap()); + // Disable rollback report sink while reverting + BoolScopeSet rollback_scope_disable(&m_rollback_sink_enabled, false); + + // Fail if no actions to handle + if(actions.empty()){ + log->push_back("Nothing to do."); + return false; + } + + int num_tried = 0; + int num_failed = 0; + + for(std::list::const_iterator + i = actions.begin(); + i != actions.end(); i++) + { + const RollbackAction &action = *i; + num_tried++; + bool success = action.applyRevert(map, this, this); + if(!success){ + num_failed++; + std::ostringstream os; + os<<"Revert of step ("<push_back(os.str()); + }else{ + std::ostringstream os; + os<<"Succesfully reverted step ("<push_back(os.str()); + } + } + + infostream<<"Map::rollbackRevertActions(): "< // Needed for rollbackRevertActions() struct LuaState; typedef struct lua_State lua_State; @@ -44,6 +46,7 @@ class IWritableNodeDefManager; class IWritableCraftDefManager; class EventManager; class PlayerSAO; +class IRollbackManager; class ServerError : public std::exception { @@ -543,6 +546,13 @@ public: // Envlock and conlock should be locked when using Lua lua_State *getLua(){ return m_lua; } + + // Envlock should be locked when using the rollback manager + IRollbackManager *getRollbackManager(){ return m_rollback; } + // actions: time-reversed list + // Return value: success/failure + bool rollbackRevertActions(const std::list &actions, + std::list *log); // IGameDef interface // Under envlock @@ -553,6 +563,7 @@ public: virtual u16 allocateUnknownNodeId(const std::string &name); virtual ISoundManager* getSoundManager(); virtual MtEventManager* getEventManager(); + virtual IRollbackReportSink* getRollbackReportSink(); IWritableItemDefManager* getWritableItemDefManager(); IWritableNodeDefManager* getWritableNodeDefManager(); @@ -720,6 +731,10 @@ private: // Bann checking BanManager m_banmanager; + // Rollback manager (behind m_env_mutex) + IRollbackManager *m_rollback; + bool m_rollback_sink_enabled; + // Scripting // Envlock and conlock should be locked when using Lua lua_State *m_lua;