Experimental-ish rollback functionality
authorPerttu Ahola <celeron55@gmail.com>
Thu, 26 Jul 2012 19:06:45 +0000 (22:06 +0300)
committerPerttu Ahola <celeron55@gmail.com>
Thu, 26 Jul 2012 23:27:18 +0000 (02:27 +0300)
20 files changed:
builtin/chatcommands.lua
builtin/privileges.lua
doc/lua_api.txt
src/CMakeLists.txt
src/content_abm.cpp
src/content_cao.cpp
src/content_cso.cpp
src/environment.cpp
src/environment.h
src/gamedef.h
src/inventorymanager.cpp
src/map.cpp
src/map.h
src/rollback.cpp [new file with mode: 0644]
src/rollback.h [new file with mode: 0644]
src/rollback_interface.cpp [new file with mode: 0644]
src/rollback_interface.h [new file with mode: 0644]
src/scriptapi.cpp
src/server.cpp
src/server.h

index da9e6c78c740ac22a7af1c4b573c42b2816436ad..49ae8c633771ec04227db7067b781b9c1a2b36a2 100644 (file)
@@ -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 = "[<range>] [<seconds>]",
+       description = "check who has last touched a node or near it, "..
+                       "max. <seconds> 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 = "<player name> [<seconds>] | :liquid [<seconds>]",
+       description = "revert actions of a player; default for <seconds> 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,
+})
+
index 6cb42c103c2dfe53c4a0ffd7f93a6ce4853a3eb8..9ec09d7f6a8c0698dc1c601bd05ed0f81024d808 100644 (file)
@@ -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")
 
index d47281b2df75123ae6bb396c418dcd7c5ecc4acc..46ea3a86eaa0b66292253c293d950b72f34771ca 100644 (file)
@@ -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:<name>", also "liquid".
+minetest.rollback_revert_actions_by(actor, seconds) -> bool, log messages
+^ Revert latest actions of someone
+^ actor: "player:<name>", also "liquid".
+
 Defaults for the on_* item definition functions:
 (These return the leftover itemstack)
 minetest.item_place_node(itemstack, placer, pointed_thing)
index e369b9623f869d391c01aa3ef47402c5a2d90258..43d7f241a9e6557a088c1ccf452aaea7341631cf 100644 (file)
@@ -153,6 +153,8 @@ configure_file(
 )
 
 set(common_SRCS
+       rollback_interface.cpp
+       rollback.cpp
        genericobject.cpp
        voxelalgorithms.cpp
        sound.cpp
index edadfe99e41927e69786392a7982e06c781d239e..12dbeea8e99a80a65d20df3f28b3abbf5e2e1f53 100644 (file)
@@ -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<<")"
 
index 58ff130f2b9c8b448c7de22f751e9ad11f8e48b4..aa5c2d674039ab054fbbe3f71dd5292c27de0c5e 100644 (file)
@@ -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;
index 6f7ecbe503da17b724bccdca1a980ec0c50a127f..666f17734c8c1b7cd0615b92b574d9a11610d8ba 100644 (file)
@@ -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)
index 02ca1f71baf84aeb414d558e009cf053a6652cba..9390101e7578ccf80f4ff0247faef79a3f2080da 100644 (file)
@@ -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";
index 0e4b85e06ef286e8aaa7e83e6e1f229714599ea5..6c52b003d7b34fb47286c64b3612d12d4d176e7c 100644 (file)
@@ -33,11 +33,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include <set>
 #include "irrlichttypes_extrabloated.h"
 #include "player.h"
-#include "map.h"
 #include <ostream>
 #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; }
index 88c15be78501f9a50bd004852c99c92d35b200fe..87918d726c4fa7ca05547a6f4e4582a3cfe5aee6 100644 (file)
@@ -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
index 06978fbb9e39216de4aa2a56f71d4f772dd59611..85668d6455f6dd344a9872d5ca8a3153e0e3f66d 100644 (file)
@@ -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="<<to_i
                        <<std::endl;
 
+       /*
+               Record rollback information
+       */
+       if(!ignore_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);
+               }
+               // 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)
index dc1f45068cbe77bc4058e517ac9f14bf0cc3d34f..734122105e80e854f0bc0cf496d9a2add52ae013 100644 (file)
@@ -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<v3s16, MapBlock*> & a_blocks,
 void Map::addNodeAndUpdate(v3s16 p, MapNode n,
                core::map<v3s16, MapBlock*> &modified_blocks)
 {
-       INodeDefManager *nodemgr = m_gamedef->ndef();
+       INodeDefManager *ndef = m_gamedef->ndef();
 
        /*PrintInfo(m_dout);
        m_dout<<DTIME<<"Map::addNodeAndUpdate(): p=("
                        <<p.X<<","<<p.Y<<","<<p.Z<<")"<<std::endl;*/
-
+       
        /*
                From this node to nodes underneath:
                If lighting is sunlight (1.0), unlight neighbours and
@@ -950,6 +951,11 @@ void Map::addNodeAndUpdate(v3s16 p, MapNode n,
 
        bool node_under_sunlight = true;
        core::map<v3s16, bool> 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<v3s16, MapBlock*> &modified_blocks)
 {
-       INodeDefManager *nodemgr = m_gamedef->ndef();
+       INodeDefManager *ndef = m_gamedef->ndef();
 
        /*PrintInfo(m_dout);
        m_dout<<DTIME<<"Map::removeNodeAndUpdate(): p=("
@@ -1128,6 +1145,11 @@ void Map::removeNodeAndUpdate(v3s16 p,
        // Node will be replaced with this
        content_t replace_material = CONTENT_AIR;
 
+       /*
+               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,
                there will be no sunlight going down.
@@ -1135,7 +1157,7 @@ void Map::removeNodeAndUpdate(v3s16 p,
        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)
@@ -1157,7 +1179,7 @@ void Map::removeNodeAndUpdate(v3s16 p,
                        Unlight neighbors (in case the node is a light source)
                */
                unLightNeighbors(bank, p,
-                               getNode(p).getLight(bank, nodemgr),
+                               getNode(p).getLight(bank, ndef),
                                light_sources, modified_blocks);
        }
 
@@ -1219,7 +1241,7 @@ void Map::removeNodeAndUpdate(v3s16 p,
                // TODO: Is this needed? Lighting is cleared up there already.
                try{
                        MapNode n = getNode(p);
-                       n.setLight(LIGHTBANK_DAY, 0, nodemgr);
+                       n.setLight(LIGHTBANK_DAY, 0, ndef);
                        setNode(p, n);
                }
                catch(InvalidPositionException &e)
@@ -1254,6 +1276,17 @@ void Map::removeNodeAndUpdate(v3s16 p,
                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 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<v3s16, MapBlock*> & 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<v3s16, MapBlock*> & 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) {
index b561e5e72a09670afd3b35ccfbfa706783031830..30cf626bb7d7aa15b06ea5684483dc6d724d35e3 100644 (file)
--- 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<MapEventReceiver*, bool> m_event_receivers;
-       
+
        core::map<v2s16, MapSector*> 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 (file)
index 0000000..3fe7910
--- /dev/null
@@ -0,0 +1,293 @@
+/*
+Minetest-c55
+Copyright (C) 2012 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 <fstream>
+#include <list>
+#include <sstream>
+#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="<<action.unix_time
+                               <<" actor=\""<<action.actor<<"\""
+                               <<" action="<<action.toString()
+                               <<std::endl;
+               addAction(action);
+       }
+       std::string getActor()
+       {
+               return m_current_actor;
+       }
+       void setActor(const std::string &actor)
+       {
+               m_current_actor = actor;
+       }
+       void flush()
+       {
+               infostream<<"RollbackManager::flush()"<<std::endl;
+               std::ofstream of(m_filepath.c_str(), std::ios::app);
+               if(!of.good()){
+                       errorstream<<"RollbackManager::flush(): Could not open file "
+                                       <<"for appending: \""<<m_filepath<<"\""<<std::endl;
+                       return;
+               }
+               for(std::list<RollbackAction>::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<<i->unix_time;
+                       of<<" ";
+                       of<<serializeJsonString(i->actor);
+                       of<<" ";
+                       std::string action_s = i->toString();
+                       of<<action_s<<std::endl;
+               }
+               m_action_todisk_buffer.clear();
+       }
+       
+       // Other
+
+       RollbackManager(const std::string &filepath, IGameDef *gamedef):
+               m_filepath(filepath),
+               m_gamedef(gamedef)
+       {
+               infostream<<"RollbackManager::RollbackManager("<<filepath<<")"
+                               <<std::endl;
+       }
+       ~RollbackManager()
+       {
+               infostream<<"RollbackManager::~RollbackManager()"<<std::endl;
+               flush();
+       }
+
+       void addAction(const RollbackAction &action)
+       {
+               m_action_todisk_buffer.push_back(action);
+               m_action_latest_buffer.push_back(action);
+
+               // Flush to disk sometimes
+               if(m_action_todisk_buffer.size() >= 100)
+                       flush();
+       }
+       
+       bool readFile(std::list<RollbackAction> &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: \""<<m_filepath<<"\""<<std::endl;
+                       return false;
+               }
+               for(;;){
+                       if(f.eof() || !f.good())
+                               break;
+                       std::string line;
+                       std::getline(f, line);
+                       line = trim(line);
+                       if(line == "")
+                               continue;
+                       std::istringstream is(line);
+                       
+                       try{
+                               std::string action_time_raw;
+                               std::getline(is, action_time_raw, ' ');
+                               std::string action_actor;
+                               try{
+                                       action_actor = deSerializeJsonString(is);
+                               }catch(SerializationError &e){
+                                       errorstream<<"RollbackManager: Error deserializing actor: "
+                                                       <<e.what()<<std::endl;
+                                       throw e;
+                               }
+                               RollbackAction action;
+                               action.unix_time = stoi(action_time_raw);
+                               action.actor = action_actor;
+                               int c = is.get();
+                               if(c != ' '){
+                                       is.putback(c);
+                                       throw SerializationError("readFile(): second ' ' not found");
+                               }
+                               action.fromStream(is);
+                               /*infostream<<"RollbackManager::readFile(): Action from disk: "
+                                               <<action.toString()<<std::endl;*/
+                               dst.push_back(action);
+                       }
+                       catch(SerializationError &e){
+                               errorstream<<"RollbackManager: Error on line: "<<line<<std::endl;
+                               errorstream<<"RollbackManager: ^ error: "<<e.what()<<std::endl;
+                       }
+               }
+               return true;
+       }
+
+       std::list<RollbackAction> getEntriesSince(int first_time)
+       {
+               infostream<<"RollbackManager::getEntriesSince("<<first_time<<")"<<std::endl;
+               // Collect enough data to this buffer
+               std::list<RollbackAction> 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."<<std::endl;
+                               action_buffer = m_action_latest_buffer;
+                       }
+               }
+               return action_buffer;
+       }
+       
+       std::string getLastNodeActor(v3s16 p, int range, int seconds,
+                       v3s16 *act_p, int *act_seconds)
+       {
+               infostream<<"RollbackManager::getLastNodeActor("<<PP(p)
+                               <<", "<<seconds<<")"<<std::endl;
+               // Figure out time
+               int cur_time = time(0);
+               int first_time = cur_time - seconds;
+
+               std::list<RollbackAction> action_buffer = getEntriesSince(first_time);
+               
+               std::list<RollbackAction> result;
+
+               for(std::list<RollbackAction>::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<RollbackAction> getRevertActions(const std::string &actor_filter,
+                       int seconds)
+       {
+               infostream<<"RollbackManager::getRevertActions("<<actor_filter
+                               <<", "<<seconds<<")"<<std::endl;
+               // Figure out time
+               int cur_time = time(0);
+               int first_time = cur_time - seconds;
+               
+               std::list<RollbackAction> action_buffer = getEntriesSince(first_time);
+               
+               std::list<RollbackAction> result;
+
+               for(std::list<RollbackAction>::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="<<action.unix_time
+                                       <<" actor=\""<<action.actor<<"\""
+                                       <<" action="<<action.toString()
+                                       <<std::endl;*/
+                       result.push_back(action);
+               }
+
+               return result;
+       }
+
+private:
+       std::string m_filepath;
+       IGameDef *m_gamedef;
+       std::string m_current_actor;
+       std::list<RollbackAction> m_action_todisk_buffer;
+       std::list<RollbackAction> m_action_latest_buffer;
+};
+
+IRollbackManager *createRollbackManager(const std::string &filepath, IGameDef *gamedef)
+{
+       return new RollbackManager(filepath, gamedef);
+}
+
+
diff --git a/src/rollback.h b/src/rollback.h
new file mode 100644 (file)
index 0000000..b5428c4
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+Minetest-c55
+Copyright (C) 2012 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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_HEADER
+#define ROLLBACK_HEADER
+
+#include <string>
+#include "irr_v3d.h"
+#include "rollback_interface.h"
+#include <list>
+
+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
+       // <seconds> in history
+       virtual std::string getLastNodeActor(v3s16 p, int range, int seconds,
+                       v3s16 *act_p, int *act_seconds) = 0;
+       // Get actions to revert <seconds> of history made by <actor>
+       virtual std::list<RollbackAction> 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 (file)
index 0000000..e15fe3d
--- /dev/null
@@ -0,0 +1,398 @@
+/*
+Minetest-c55
+Copyright (C) 2012 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 <sstream>
+#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<<"("<<itos(p.X)<<","<<itos(p.Y)<<","<<itos(p.Z)<<")";
+               os<<" ";
+               os<<serializeJsonString(n_old.name);
+               os<<" ";
+               os<<itos(n_old.param1);
+               os<<" ";
+               os<<itos(n_old.param2);
+               os<<" ";
+               os<<serializeJsonString(n_old.meta);
+               os<<" ";
+               os<<serializeJsonString(n_new.name);
+               os<<" ";
+               os<<itos(n_new.param1);
+               os<<" ";
+               os<<itos(n_new.param2);
+               os<<" ";
+               os<<serializeJsonString(n_new.meta);
+               os<<"]";
+               return os.str(); }
+       case TYPE_MODIFY_INVENTORY_STACK: {
+               std::ostringstream os(std::ios::binary);
+               os<<"[modify_inventory_stack";
+               os<<" ";
+               os<<serializeJsonString(inventory_location);
+               os<<" ";
+               os<<serializeJsonString(inventory_list);
+               os<<" ";
+               os<<inventory_index;
+               os<<" ";
+               os<<(inventory_add?"add":"remove");
+               os<<" ";
+               os<<serializeJsonString(inventory_stack);
+               os<<"]";
+               return os.str(); }
+       default:
+               return "none";
+       }
+}
+
+void RollbackAction::fromStream(std::istream &is) throw(SerializationError)
+{
+       int c = is.get();
+       if(c != '['){
+               is.putback(c);
+               throw SerializationError("RollbackAction: starting [ not found");
+       }
+       
+       std::string id;
+       std::getline(is, id, ' ');
+       
+       if(id == "set_node")
+       {
+               c = is.get();
+               if(c != '('){
+                       is.putback(c);
+                       throw SerializationError("RollbackAction: starting ( not found");
+               }
+               // Position
+               std::string px_raw;
+               std::string py_raw;
+               std::string pz_raw;
+               std::getline(is, px_raw, ',');
+               std::getline(is, py_raw, ',');
+               std::getline(is, pz_raw, ')');
+               c = is.get();
+               if(c != ' '){
+                       is.putback(c);
+                       throw SerializationError("RollbackAction: after-p ' ' not found");
+               }
+               v3s16 loaded_p(stoi(px_raw), stoi(py_raw), stoi(pz_raw));
+               // Old node
+               std::string old_name;
+               try{
+                       old_name = deSerializeJsonString(is);
+               }catch(SerializationError &e){
+                       errorstream<<"Serialization error in RollbackAction::fromStream(): "
+                                       <<"old_name: "<<e.what()<<std::endl;
+                       throw e;
+               }
+               c = is.get();
+               if(c != ' '){
+                       is.putback(c);
+                       throw SerializationError("RollbackAction: after-old_name ' ' not found");
+               }
+               std::string old_p1_raw;
+               std::string old_p2_raw;
+               std::getline(is, old_p1_raw, ' ');
+               std::getline(is, old_p2_raw, ' ');
+               int old_p1 = stoi(old_p1_raw);
+               int old_p2 = stoi(old_p2_raw);
+               std::string old_meta;
+               try{
+                       old_meta = deSerializeJsonString(is);
+               }catch(SerializationError &e){
+                       errorstream<<"Serialization error in RollbackAction::fromStream(): "
+                                       <<"old_meta: "<<e.what()<<std::endl;
+                       throw e;
+               }
+               c = is.get();
+               if(c != ' '){
+                       is.putback(c);
+                       throw SerializationError("RollbackAction: after-old_meta ' ' not found");
+               }
+               // New node
+               std::string new_name;
+               try{
+                       new_name = deSerializeJsonString(is);
+               }catch(SerializationError &e){
+                       errorstream<<"Serialization error in RollbackAction::fromStream(): "
+                                       <<"new_name: "<<e.what()<<std::endl;
+                       throw e;
+               }
+               c = is.get();
+               if(c != ' '){
+                       is.putback(c);
+                       throw SerializationError("RollbackAction: after-new_name ' ' not found");
+               }
+               std::string new_p1_raw;
+               std::string new_p2_raw;
+               std::getline(is, new_p1_raw, ' ');
+               std::getline(is, new_p2_raw, ' ');
+               int new_p1 = stoi(new_p1_raw);
+               int new_p2 = stoi(new_p2_raw);
+               std::string new_meta;
+               try{
+                       new_meta = deSerializeJsonString(is);
+               }catch(SerializationError &e){
+                       errorstream<<"Serialization error in RollbackAction::fromStream(): "
+                                       <<"new_meta: "<<e.what()<<std::endl;
+                       throw e;
+               }
+               c = is.get();
+               if(c != ']'){
+                       is.putback(c);
+                       throw SerializationError("RollbackAction: after-new_meta ] not found");
+               }
+               // Set values
+               type = TYPE_SET_NODE;
+               p = loaded_p;
+               n_old.name = old_name;
+               n_old.param1 = old_p1;
+               n_old.param2 = old_p2;
+               n_old.meta = old_meta;
+               n_new.name = new_name;
+               n_new.param1 = new_p1;
+               n_new.param2 = new_p2;
+               n_new.meta = new_meta;
+       }
+       else if(id == "modify_inventory_stack")
+       {
+               // Location
+               std::string location;
+               try{
+                       location = deSerializeJsonString(is);
+               }catch(SerializationError &e){
+                       errorstream<<"Serialization error in RollbackAction::fromStream(): "
+                                       <<"location: "<<e.what()<<std::endl;
+                       throw e;
+               }
+               c = is.get();
+               if(c != ' '){
+                       is.putback(c);
+                       throw SerializationError("RollbackAction: after-loc ' ' not found");
+               }
+               // List
+               std::string listname;
+               try{
+                       listname = deSerializeJsonString(is);
+               }catch(SerializationError &e){
+                       errorstream<<"Serialization error in RollbackAction::fromStream(): "
+                                       <<"listname: "<<e.what()<<std::endl;
+                       throw e;
+               }
+               c = is.get();
+               if(c != ' '){
+                       is.putback(c);
+                       throw SerializationError("RollbackAction: after-list ' ' not found");
+               }
+               // Index
+               std::string index_raw;
+               std::getline(is, index_raw, ' ');
+               // add/remove
+               std::string addremove;
+               std::getline(is, addremove, ' ');
+               if(addremove != "add" && addremove != "remove"){
+                       throw SerializationError("RollbackAction: addremove is not add or remove");
+               }
+               // Itemstring
+               std::string stack;
+               try{
+                       stack = deSerializeJsonString(is);
+               }catch(SerializationError &e){
+                       errorstream<<"Serialization error in RollbackAction::fromStream(): "
+                                       <<"stack: "<<e.what()<<std::endl;
+                       throw e;
+               }
+               // Set values
+               type = TYPE_MODIFY_INVENTORY_STACK;
+               inventory_location = location;
+               inventory_list = listname;
+               inventory_index = stoi(index_raw);
+               inventory_add = (addremove == "add");
+               inventory_stack = stack;
+       }
+       else
+       {
+               throw SerializationError("RollbackAction: Unknown id");
+       }
+}
+
+bool RollbackAction::isImportant(IGameDef *gamedef) const
+{
+       switch(type){
+       case TYPE_SET_NODE: {
+               // If names differ, action is always important
+               if(n_old.name != n_new.name)
+                       return true;
+               // If metadata differs, action is always important
+               if(n_old.meta != n_new.meta)
+                       return true;
+               INodeDefManager *ndef = gamedef->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 "
+                                                       <<PP(p)<<" for "<<n_old.name<<std::endl;
+                                       return false;
+                               }
+                               NodeMetadata *meta = map->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: "<<e.what()<<std::endl;
+                               return false;
+                       }
+                       // Success
+                       return true; }
+               case TYPE_MODIFY_INVENTORY_STACK: {
+                       InventoryLocation loc;
+                       loc.deSerialize(inventory_location);
+                       ItemStack stack;
+                       stack.deSerialize(inventory_stack, gamedef->idef());
+                       Inventory *inv = imgr->getInventory(loc);
+                       if(!inv){
+                               infostream<<"RollbackAction::applyRevert(): Could not get "
+                                               "inventory at "<<inventory_location<<std::endl;
+                               return false;
+                       }
+                       InventoryList *list = inv->getList(inventory_list);
+                       if(!list){
+                               infostream<<"RollbackAction::applyRevert(): Could not get "
+                                               "inventory list \""<<inventory_list<<"\" in "
+                                               <<inventory_location<<std::endl;
+                               return false;
+                       }
+                       if(list->getSize() <= inventory_index){
+                               infostream<<"RollbackAction::applyRevert(): List index "
+                                               <<inventory_index<<" too large in "
+                                               <<"inventory list \""<<inventory_list<<"\" in "
+                                               <<inventory_location<<std::endl;
+                       }
+                       // If item was added, take away item, otherwise add removed item
+                       if(inventory_add){
+                               // Silently ignore different current item
+                               if(list->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"
+                                       <<std::endl;
+                       return false;
+               }
+       }catch(SerializationError &e){
+               errorstream<<"RollbackAction::applyRevert(): n_old.name="<<n_old.name
+                               <<", SerializationError: "<<e.what()<<std::endl;
+       }
+       return false;
+}
+
diff --git a/src/rollback_interface.h b/src/rollback_interface.h
new file mode 100644 (file)
index 0000000..0f0a118
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+Minetest-c55
+Copyright (C) 2012 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 <string>
+#include <iostream>
+#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
index e1d9185420235113059c0cce5ef285e5fd7aaffc..61488abb4eb23b14ead3d841452a05dead171566 100644 (file)
@@ -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<RollbackAction> actions = rollback->getRevertActions(actor, seconds);
+       std::list<std::string> 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<std::string>::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}
 };
 
index 21c936e6996dfcec97fe1d3f7d299c487bbc3618..a868a0425600bbda00234efce4a0efd76390cd12 100644 (file)
@@ -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: "<<m_path_config<<std::endl;
        infostream<<"- game:   "<<m_gamespec.path<<std::endl;
 
+       // Create rollback manager
+       std::string rollback_path = m_path_world+DIR_DELIM+"rollback.txt";
+       m_rollback = createRollbackManager(rollback_path, this);
+
        // Add world mod search path
        m_modspaths.push_front(m_path_world + DIR_DELIM + "worldmods");
        // Add addon mod search path
@@ -1049,7 +1056,7 @@ Server::Server(
        
        m_env = new ServerEnvironment(new ServerMap(path_world, this), m_lua,
                        this, this);
-
+       
        // Give environment reference to scripting api
        scriptapi_add_environment(m_lua, m_env);
        
@@ -1152,6 +1159,7 @@ Server::~Server()
        
        // Delete things in the reverse order of creation
        delete m_env;
+       delete m_rollback;
        delete m_event;
        delete m_itemdef;
        delete m_nodedef;
@@ -2481,6 +2489,10 @@ 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());
+
                /*
                        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<RollbackAction> &actions,
+               std::list<std::string> *log)
+{
+       infostream<<"Server::rollbackRevertActions(len="<<actions.size()<<")"<<std::endl;
+       ServerMap *map = (ServerMap*)(&m_env->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<RollbackAction>::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 ("<<num_tried<<") "<<action.toString()<<" failed";
+                       infostream<<"Map::rollbackRevertActions(): "<<os.str()<<std::endl;
+                       if(log)
+                               log->push_back(os.str());
+               }else{
+                       std::ostringstream os;
+                       os<<"Succesfully reverted step ("<<num_tried<<") "<<action.toString();
+                       infostream<<"Map::rollbackRevertActions(): "<<os.str()<<std::endl;
+                       if(log)
+                               log->push_back(os.str());
+               }
+       }
+       
+       infostream<<"Map::rollbackRevertActions(): "<<num_failed<<"/"<<num_tried
+                       <<" failed"<<std::endl;
+
+       // Call it done if less than half failed
+       return num_failed <= num_tried/2;
+}
+
 // IGameDef interface
 // Under envlock
 IItemDefManager* Server::getItemDefManager()
@@ -4552,6 +4652,12 @@ MtEventManager* Server::getEventManager()
 {
        return m_event;
 }
+IRollbackReportSink* Server::getRollbackReportSink()
+{
+       if(!m_rollback_sink_enabled)
+               return NULL;
+       return m_rollback;
+}
 
 IWritableItemDefManager* Server::getWritableItemDefManager()
 {
index 4316bc21fd4a64726c8b43e7ac6fb1cbdf7bcdbd..668d424167a7ed695357dd05f927b3183c965671 100644 (file)
@@ -36,6 +36,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "sound.h"
 #include "util/thread.h"
 #include "util/string.h"
+#include "rollback_interface.h" // Needed for rollbackRevertActions()
+#include <list> // 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<RollbackAction> &actions,
+                       std::list<std::string> *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;