diff --git a/doc/lua_api.txt b/doc/lua_api.txt
index 86c64f9be..46b789526 100644
--- a/doc/lua_api.txt
+++ b/doc/lua_api.txt
@@ -2099,6 +2099,8 @@ methods:
   ^ To be used only by a VoxelManip object from minetest.get_mapgen_object
   ^ (p1, p2) is the area in which lighting is set; defaults to the whole area if left out
 - update_liquids():  Update liquid flow
+- was_modified(): Returns true or false if the data in the voxel manipulator had been modified since
+  the last read from map, due to a call to minetest.set_data() on the loaded area elsewhere
 
 VoxelArea: A helper class for voxel areas
 - Can be created via VoxelArea:new{MinEdge=pmin, MaxEdge=pmax}
diff --git a/src/environment.cpp b/src/environment.cpp
index 24a498aa9..c8af72f6b 100644
--- a/src/environment.cpp
+++ b/src/environment.cpp
@@ -778,19 +778,26 @@ bool ServerEnvironment::setNode(v3s16 p, const MapNode &n)
 {
 	INodeDefManager *ndef = m_gamedef->ndef();
 	MapNode n_old = m_map->getNodeNoEx(p);
+
 	// Call destructor
-	if(ndef->get(n_old).has_on_destruct)
+	if (ndef->get(n_old).has_on_destruct)
 		m_script->node_on_destruct(p, n_old);
+
 	// Replace node
-	bool succeeded = m_map->addNodeWithEvent(p, n);
-	if(!succeeded)
+	if (!m_map->addNodeWithEvent(p, n))
 		return false;
+
+	// Update active VoxelManipulator if a mapgen thread
+	m_map->updateVManip(p);
+
 	// Call post-destructor
-	if(ndef->get(n_old).has_after_destruct)
+	if (ndef->get(n_old).has_after_destruct)
 		m_script->node_after_destruct(p, n_old);
+
 	// Call constructor
-	if(ndef->get(n).has_on_construct)
+	if (ndef->get(n).has_on_construct)
 		m_script->node_on_construct(p, n);
+
 	return true;
 }
 
@@ -798,24 +805,36 @@ bool ServerEnvironment::removeNode(v3s16 p)
 {
 	INodeDefManager *ndef = m_gamedef->ndef();
 	MapNode n_old = m_map->getNodeNoEx(p);
+
 	// Call destructor
-	if(ndef->get(n_old).has_on_destruct)
+	if (ndef->get(n_old).has_on_destruct)
 		m_script->node_on_destruct(p, n_old);
+
 	// Replace with air
 	// This is slightly optimized compared to addNodeWithEvent(air)
-	bool succeeded = m_map->removeNodeWithEvent(p);
-	if(!succeeded)
+	if (!m_map->removeNodeWithEvent(p))
 		return false;
+
+	// Update active VoxelManipulator if a mapgen thread
+	m_map->updateVManip(p);
+
 	// Call post-destructor
-	if(ndef->get(n_old).has_after_destruct)
+	if (ndef->get(n_old).has_after_destruct)
 		m_script->node_after_destruct(p, n_old);
+
 	// Air doesn't require constructor
 	return true;
 }
 
 bool ServerEnvironment::swapNode(v3s16 p, const MapNode &n)
 {
-	return m_map->addNodeWithEvent(p, n, false);
+	if (!m_map->addNodeWithEvent(p, n, false))
+		return false;
+
+	// Update active VoxelManipulator if a mapgen thread
+	m_map->updateVManip(p);
+
+	return true;
 }
 
 std::set<u16> ServerEnvironment::getObjectsInsideRadius(v3f pos, float radius)
diff --git a/src/map.cpp b/src/map.cpp
index 147bd3584..236972ae9 100644
--- a/src/map.cpp
+++ b/src/map.cpp
@@ -2757,6 +2757,28 @@ MapBlock *ServerMap::getBlockOrEmerge(v3s16 p3d)
 void ServerMap::prepareBlock(MapBlock *block) {
 }
 
+// N.B.  This requires no synchronization, since data will not be modified unless
+// the VoxelManipulator being updated belongs to the same thread.
+void ServerMap::updateVManip(v3s16 pos)
+{
+	Mapgen *mg = m_emerge->getCurrentMapgen();
+	if (!mg)
+		return;
+
+	ManualMapVoxelManipulator *vm = mg->vm;
+	if (!vm)
+		return;
+
+	if (!vm->m_area.contains(pos))
+		return;
+
+	s32 idx = vm->m_area.index(pos);
+	vm->m_data[idx] = getNodeNoEx(pos);
+	vm->m_flags[idx] &= ~VOXELFLAG_NO_DATA;
+
+	vm->m_is_dirty = true;
+}
+
 s16 ServerMap::findGroundLevel(v2s16 p2d)
 {
 #if 0
@@ -3523,6 +3545,7 @@ void ServerMap::PrintInfo(std::ostream &out)
 
 ManualMapVoxelManipulator::ManualMapVoxelManipulator(Map *map):
 		VoxelManipulator(),
+		m_is_dirty(false),
 		m_create_area(false),
 		m_map(map)
 {
@@ -3617,6 +3640,8 @@ void ManualMapVoxelManipulator::initialEmerge(v3s16 blockpos_min,
 
 		m_loaded_blocks[p] = flags;
 	}
+
+	m_is_dirty = false;
 }
 
 void ManualMapVoxelManipulator::blitBackAll(
diff --git a/src/map.h b/src/map.h
index 9b505d8e6..1847c7ac7 100644
--- a/src/map.h
+++ b/src/map.h
@@ -493,6 +493,8 @@ public:
 	// Database version
 	void loadBlock(std::string *blob, v3s16 p3d, MapSector *sector, bool save_after_load=false);
 
+	void updateVManip(v3s16 pos);
+
 	// For debug printing
 	virtual void PrintInfo(std::ostream &out);
 
@@ -550,6 +552,8 @@ public:
 	void blitBackAll(std::map<v3s16, MapBlock*> * modified_blocks,
 			bool overwrite_generated = true);
 
+	bool m_is_dirty;
+
 protected:
 	bool m_create_area;
 	Map *m_map;
diff --git a/src/script/lua_api/l_vmanip.cpp b/src/script/lua_api/l_vmanip.cpp
index f3c1fa67a..2a3cfcafa 100644
--- a/src/script/lua_api/l_vmanip.cpp
+++ b/src/script/lua_api/l_vmanip.cpp
@@ -304,6 +304,18 @@ int LuaVoxelManip::l_update_map(lua_State *L)
 	return 0;
 }
 
+int LuaVoxelManip::l_was_modified(lua_State *L)
+{
+	NO_MAP_LOCK_REQUIRED;
+
+	LuaVoxelManip *o = checkobject(L, 1);
+	ManualMapVoxelManipulator *vm = o->vm;
+
+	lua_pushboolean(L, vm->m_is_dirty);
+
+	return 1;
+}
+
 LuaVoxelManip::LuaVoxelManip(ManualMapVoxelManipulator *mmvm, bool is_mg_vm)
 {
 	this->vm           = mmvm;
@@ -396,5 +408,6 @@ const luaL_reg LuaVoxelManip::methods[] = {
 	luamethod(LuaVoxelManip, set_light_data),
 	luamethod(LuaVoxelManip, get_param2_data),
 	luamethod(LuaVoxelManip, set_param2_data),
+	luamethod(LuaVoxelManip, was_modified),
 	{0,0}
 };
diff --git a/src/script/lua_api/l_vmanip.h b/src/script/lua_api/l_vmanip.h
index fc7ef30e6..70468b344 100644
--- a/src/script/lua_api/l_vmanip.h
+++ b/src/script/lua_api/l_vmanip.h
@@ -58,6 +58,8 @@ private:
 	static int l_get_param2_data(lua_State *L);
 	static int l_set_param2_data(lua_State *L);
 
+	static int l_was_modified(lua_State *L);
+
 public:
 	LuaVoxelManip(ManualMapVoxelManipulator *mmvm, bool is_mapgen_vm);
 	LuaVoxelManip(Map *map);