aboutsummaryrefslogtreecommitdiff
path: root/src/rollback_interface.cpp
blob: 808b07fed9545a7b5032d54e6898a6ca29e2a6df (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
/*
Minetest
Copyright (C) 2013 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::getPosition(v3s16 *dst) const
{
	switch(type){
	case RollbackAction::TYPE_SET_NODE:
		if(dst) *dst = p;
		return true;
	case RollbackAction::TYPE_MODIFY_INVENTORY_STACK: {
		InventoryLocation loc;
		loc.deSerialize(inventory_location);
		if(loc.type != InventoryLocation::NODEMETA)
			return false;
		if(dst) *dst = loc.p;
		return true; }
	default:
		return false;
	}
}

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);
						if(!map->setNodeMetadata(p, meta)){
							delete meta;
							infostream<<"RollbackAction::applyRevert(): "
									<<"setNodeMetadata failed at "
									<<PP(p)<<" for "<<n_old.name<<std::endl;
							return false;
						}
					}
					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;
}

>1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062
/*
Minetest
Copyright (C) 2010-2013 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 "map.h"
#include "mapsector.h"
#include "mapblock.h"
#include "filesys.h"
#include "voxel.h"
#include "voxelalgorithms.h"
#include "porting.h"
#include "serialization.h"
#include "nodemetadata.h"
#include "settings.h"
#include "log.h"
#include "profiler.h"
#include "nodedef.h"
#include "gamedef.h"
#include "util/directiontables.h"
#include "util/basic_macros.h"
#include "rollback_interface.h"
#include "environment.h"
#include "reflowscan.h"
#include "emerge.h"
#include "mapgen/mapgen_v6.h"
#include "mapgen/mg_biome.h"
#include "config.h"
#include "server.h"
#include "database/database.h"
#include "database/database-dummy.h"
#include "database/database-sqlite3.h"
#include "script/scripting_server.h"
#include <deque>
#include <queue>
#if USE_LEVELDB
#include "database/database-leveldb.h"
#endif
#if USE_REDIS
#include "database/database-redis.h"
#endif
#if USE_POSTGRESQL
#include "database/database-postgresql.h"
#endif


/*
	Map
*/

Map::Map(IGameDef *gamedef):
	m_gamedef(gamedef),
	m_nodedef(gamedef->ndef())
{
}

Map::~Map()
{
	/*
		Free all MapSectors
	*/
	for (auto &sector : m_sectors) {
		delete sector.second;
	}
}

void Map::addEventReceiver(MapEventReceiver *event_receiver)
{
	m_event_receivers.insert(event_receiver);
}

void Map::removeEventReceiver(MapEventReceiver *event_receiver)
{
	m_event_receivers.erase(event_receiver);
}

void Map::dispatchEvent(const MapEditEvent &event)
{
	for (MapEventReceiver *event_receiver : m_event_receivers) {
		event_receiver->onMapEditEvent(event);
	}
}

MapSector * Map::getSectorNoGenerateNoLock(v2s16 p)
{
	if(m_sector_cache != NULL && p == m_sector_cache_p){
		MapSector * sector = m_sector_cache;
		return sector;
	}

	auto n = m_sectors.find(p);

	if (n == m_sectors.end())
		return NULL;

	MapSector *sector = n->second;

	// Cache the last result
	m_sector_cache_p = p;
	m_sector_cache = sector;

	return sector;
}

MapSector * Map::getSectorNoGenerate(v2s16 p)
{
	return getSectorNoGenerateNoLock(p);
}

MapBlock * Map::getBlockNoCreateNoEx(v3s16 p3d)
{
	v2s16 p2d(p3d.X, p3d.Z);
	MapSector * sector = getSectorNoGenerate(p2d);
	if(sector == NULL)
		return NULL;
	MapBlock *block = sector->getBlockNoCreateNoEx(p3d.Y);
	return block;
}

MapBlock * Map::getBlockNoCreate(v3s16 p3d)
{
	MapBlock *block = getBlockNoCreateNoEx(p3d);
	if(block == NULL)
		throw InvalidPositionException();
	return block;
}

bool Map::isValidPosition(v3s16 p)
{
	v3s16 blockpos = getNodeBlockPos(p);
	MapBlock *block = getBlockNoCreateNoEx(blockpos);
	return (block != NULL);
}

// Returns a CONTENT_IGNORE node if not found
MapNode Map::getNode(v3s16 p, bool *is_valid_position)
{
	v3s16 blockpos = getNodeBlockPos(p);
	MapBlock *block = getBlockNoCreateNoEx(blockpos);
	if (block == NULL) {
		if (is_valid_position != NULL)
			*is_valid_position = false;
		return {CONTENT_IGNORE};
	}

	v3s16 relpos = p - blockpos*MAP_BLOCKSIZE;
	bool is_valid_p;
	MapNode node = block->getNodeNoCheck(relpos, &is_valid_p);
	if (is_valid_position != NULL)
		*is_valid_position = is_valid_p;
	return node;
}

static void set_node_in_block(MapBlock *block, v3s16 relpos, MapNode n)
{
	// Never allow placing CONTENT_IGNORE, it causes problems
	if(n.getContent() == CONTENT_IGNORE){
		const NodeDefManager *nodedef = block->getParent()->getNodeDefManager();
		v3s16 blockpos = block->getPos();
		v3s16 p = blockpos * MAP_BLOCKSIZE + relpos;
		bool temp_bool;
		errorstream<<"Not allowing to place CONTENT_IGNORE"
				<<" while trying to replace \""
				<<nodedef->get(block->getNodeNoCheck(relpos, &temp_bool)).name
				<<"\" at "<<PP(p)<<" (block "<<PP(blockpos)<<")"<<std::endl;
		return;
	}
	block->setNodeNoCheck(relpos, n);
}

// throws InvalidPositionException if not found
void Map::setNode(v3s16 p, MapNode & n)
{
	v3s16 blockpos = getNodeBlockPos(p);
	MapBlock *block = getBlockNoCreate(blockpos);
	v3s16 relpos = p - blockpos*MAP_BLOCKSIZE;
	set_node_in_block(block, relpos, n);
}

void Map::addNodeAndUpdate(v3s16 p, MapNode n,
		std::map<v3s16, MapBlock*> &modified_blocks,
		bool remove_metadata)
{
	// Collect old node for rollback
	RollbackNode rollback_oldnode(this, p, m_gamedef);

	v3s16 blockpos = getNodeBlockPos(p);
	MapBlock *block = getBlockNoCreate(blockpos);
	if (block->isDummy())
		throw InvalidPositionException();
	v3s16 relpos = p - blockpos * MAP_BLOCKSIZE;

	// This is needed for updating the lighting
	MapNode oldnode = block->getNodeUnsafe(relpos);

	// Remove node metadata
	if (remove_metadata) {
		removeNodeMetadata(p);
	}

	// Set the node on the map
	const ContentFeatures &cf = m_nodedef->get(n);
	const ContentFeatures &oldcf = m_nodedef->get(oldnode);
	if (cf.lightingEquivalent(oldcf)) {
		// No light update needed, just copy over the old light.
		n.setLight(LIGHTBANK_DAY, oldnode.getLightRaw(LIGHTBANK_DAY, oldcf), cf);
		n.setLight(LIGHTBANK_NIGHT, oldnode.getLightRaw(LIGHTBANK_NIGHT, oldcf), cf);
		set_node_in_block(block, relpos, n);

		modified_blocks[blockpos] = block;
	} else {
		// Ignore light (because calling voxalgo::update_lighting_nodes)
		n.setLight(LIGHTBANK_DAY, 0, cf);
		n.setLight(LIGHTBANK_NIGHT, 0, cf);
		set_node_in_block(block, relpos, n);

		// Update lighting
		std::vector<std::pair<v3s16, MapNode> > oldnodes;
		oldnodes.emplace_back(p, oldnode);
		voxalgo::update_lighting_nodes(this, oldnodes, modified_blocks);

		for (auto &modified_block : modified_blocks) {
			modified_block.second->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);
	}
}

void Map::removeNodeAndUpdate(v3s16 p,
		std::map<v3s16, MapBlock*> &modified_blocks)
{
	addNodeAndUpdate(p, MapNode(CONTENT_AIR), modified_blocks, true);
}

bool Map::addNodeWithEvent(v3s16 p, MapNode n, bool remove_metadata)
{
	MapEditEvent event;
	event.type = remove_metadata ? MEET_ADDNODE : MEET_SWAPNODE;
	event.p = p;
	event.n = n;

	bool succeeded = true;
	try{
		std::map<v3s16, MapBlock*> modified_blocks;
		addNodeAndUpdate(p, n, modified_blocks, remove_metadata);

		// Copy modified_blocks to event
		for (auto &modified_block : modified_blocks) {
			event.modified_blocks.insert(modified_block.first);
		}
	}
	catch(InvalidPositionException &e){
		succeeded = false;
	}

	dispatchEvent(event);

	return succeeded;
}

bool Map::removeNodeWithEvent(v3s16 p)
{
	MapEditEvent event;
	event.type = MEET_REMOVENODE;
	event.p = p;

	bool succeeded = true;
	try{
		std::map<v3s16, MapBlock*> modified_blocks;
		removeNodeAndUpdate(p, modified_blocks);

		// Copy modified_blocks to event
		for (auto &modified_block : modified_blocks) {
			event.modified_blocks.insert(modified_block.first);
		}
	}
	catch(InvalidPositionException &e){
		succeeded = false;
	}

	dispatchEvent(event);

	return succeeded;
}

struct TimeOrderedMapBlock {
	MapSector *sect;
	MapBlock *block;

	TimeOrderedMapBlock(MapSector *sect, MapBlock *block) :
		sect(sect),
		block(block)
	{}

	bool operator<(const TimeOrderedMapBlock &b) const
	{
		return block->getUsageTimer() < b.block->getUsageTimer();
	};
};

/*
	Updates usage timers
*/
void Map::timerUpdate(float dtime, float unload_timeout, s32 max_loaded_blocks,
		std::vector<v3s16> *unloaded_blocks)
{
	bool save_before_unloading = maySaveBlocks();

	// Profile modified reasons
	Profiler modprofiler;

	std::vector<v2s16> sector_deletion_queue;
	u32 deleted_blocks_count = 0;
	u32 saved_blocks_count = 0;
	u32 block_count_all = 0;

	const auto start_time = porting::getTimeUs();
	beginSave();

	// If there is no practical limit, we spare creation of mapblock_queue
	if (max_loaded_blocks < 0) {
		for (auto &sector_it : m_sectors) {
			MapSector *sector = sector_it.second;

			bool all_blocks_deleted = true;

			MapBlockVect blocks;
			sector->getBlocks(blocks);

			for (MapBlock *block : blocks) {
				block->incrementUsageTimer(dtime);

				if (block->refGet() == 0
						&& block->getUsageTimer() > unload_timeout) {
					v3s16 p = block->getPos();

					// Save if modified
					if (block->getModified() != MOD_STATE_CLEAN
							&& save_before_unloading) {
						modprofiler.add(block->getModifiedReasonString(), 1);
						if (!saveBlock(block))
							continue;
						saved_blocks_count++;
					}

					// Delete from memory
					sector->deleteBlock(block);

					if (unloaded_blocks)
						unloaded_blocks->push_back(p);

					deleted_blocks_count++;
				} else {
					all_blocks_deleted = false;
					block_count_all++;
				}
			}

			// Delete sector if we emptied it
			if (all_blocks_deleted) {
				sector_deletion_queue.push_back(sector_it.first);
			}
		}
	} else {
		std::priority_queue<TimeOrderedMapBlock> mapblock_queue;
		for (auto &sector_it : m_sectors) {
			MapSector *sector = sector_it.second;

			MapBlockVect blocks;
			sector->getBlocks(blocks);

			for (MapBlock *block : blocks) {
				block->incrementUsageTimer(dtime);
				mapblock_queue.push(TimeOrderedMapBlock(sector, block));
			}
		}
		block_count_all = mapblock_queue.size();

		// Delete old blocks, and blocks over the limit from the memory
		while (!mapblock_queue.empty() && ((s32)mapblock_queue.size() > max_loaded_blocks
				|| mapblock_queue.top().block->getUsageTimer() > unload_timeout)) {
			TimeOrderedMapBlock b = mapblock_queue.top();
			mapblock_queue.pop();

			MapBlock *block = b.block;

			if (block->refGet() != 0)
				continue;

			v3s16 p = block->getPos();

			// Save if modified
			if (block->getModified() != MOD_STATE_CLEAN && save_before_unloading) {
				modprofiler.add(block->getModifiedReasonString(), 1);
				if (!saveBlock(block))
					continue;
				saved_blocks_count++;
			}

			// Delete from memory
			b.sect->deleteBlock(block);

			if (unloaded_blocks)
				unloaded_blocks->push_back(p);

			deleted_blocks_count++;
			block_count_all--;
		}

		// Delete empty sectors
		for (auto &sector_it : m_sectors) {
			if (sector_it.second->empty()) {
				sector_deletion_queue.push_back(sector_it.first);
			}
		}
	}

	endSave();
	const auto end_time = porting::getTimeUs();

	reportMetrics(end_time - start_time, saved_blocks_count, block_count_all);

	// Finally delete the empty sectors
	deleteSectors(sector_deletion_queue);

	if(deleted_blocks_count != 0)
	{
		PrintInfo(infostream); // ServerMap/ClientMap:
		infostream<<"Unloaded "<<deleted_blocks_count
				<<" blocks from memory";
		if(save_before_unloading)
			infostream<<", of which "<<saved_blocks_count<<" were written";
		infostream<<", "<<block_count_all<<" blocks in memory";
		infostream<<"."<<std::endl;
		if(saved_blocks_count != 0){
			PrintInfo(infostream); // ServerMap/ClientMap:
			infostream<<"Blocks modified by: "<<std::endl;
			modprofiler.print(infostream);
		}
	}
}

void Map::unloadUnreferencedBlocks(std::vector<v3s16> *unloaded_blocks)
{
	timerUpdate(0.0, -1.0, 0, unloaded_blocks);
}

void Map::deleteSectors(std::vector<v2s16> &sectorList)
{
	for (v2s16 j : sectorList) {
		MapSector *sector = m_sectors[j];
		// If sector is in sector cache, remove it from there
		if(m_sector_cache == sector)
			m_sector_cache = NULL;
		// Remove from map and delete
		m_sectors.erase(j);
		delete sector;
	}
}

void Map::PrintInfo(std::ostream &out)
{
	out<<"Map: ";
}

#define WATER_DROP_BOOST 4

const static v3s16 liquid_6dirs[6] = {
	// order: upper before same level before lower
	v3s16( 0, 1, 0),
	v3s16( 0, 0, 1),
	v3s16( 1, 0, 0),
	v3s16( 0, 0,-1),
	v3s16(-1, 0, 0),
	v3s16( 0,-1, 0)
};

enum NeighborType : u8 {
	NEIGHBOR_UPPER,
	NEIGHBOR_SAME_LEVEL,
	NEIGHBOR_LOWER
};

struct NodeNeighbor {
	MapNode n;
	NeighborType t;
	v3s16 p;

	NodeNeighbor()
		: n(CONTENT_AIR), t(NEIGHBOR_SAME_LEVEL)
	{ }

	NodeNeighbor(const MapNode &node, NeighborType n_type, const v3s16 &pos)
		: n(node),
		  t(n_type),
		  p(pos)
	{ }
};

void ServerMap::transforming_liquid_add(v3s16 p) {
        m_transforming_liquid.push_back(p);
}

void ServerMap::transformLiquids(std::map<v3s16, MapBlock*> &modified_blocks,
		ServerEnvironment *env)
{
	u32 loopcount = 0;
	u32 initial_size = m_transforming_liquid.size();

	/*if(initial_size != 0)
		infostream<<"transformLiquids(): initial_size="<<initial_size<<std::endl;*/

	// list of nodes that due to viscosity have not reached their max level height
	std::deque<v3s16> must_reflow;

	std::vector<std::pair<v3s16, MapNode> > changed_nodes;

	u32 liquid_loop_max = g_settings->getS32("liquid_loop_max");
	u32 loop_max = liquid_loop_max;

	while (m_transforming_liquid.size() != 0)
	{
		// This should be done here so that it is done when continue is used
		if (loopcount >= initial_size || loopcount >= loop_max)
			break;
		loopcount++;

		/*
			Get a queued transforming liquid node
		*/
		v3s16 p0 = m_transforming_liquid.front();
		m_transforming_liquid.pop_front();

		MapNode n0 = getNode(p0);

		/*
			Collect information about current node
		 */
		s8 liquid_level = -1;
		// The liquid node which will be placed there if
		// the liquid flows into this node.
		content_t liquid_kind = CONTENT_IGNORE;
		// The node which will be placed there if liquid
		// can't flow into this node.
		content_t floodable_node = CONTENT_AIR;
		const ContentFeatures &cf = m_nodedef->get(n0);
		LiquidType liquid_type = cf.liquid_type;
		switch (liquid_type) {
			case LIQUID_SOURCE:
				liquid_level = LIQUID_LEVEL_SOURCE;
				liquid_kind = cf.liquid_alternative_flowing_id;
				break;
			case LIQUID_FLOWING:
				liquid_level = (n0.param2 & LIQUID_LEVEL_MASK);
				liquid_kind = n0.getContent();
				break;
			case LIQUID_NONE:
				// if this node is 'floodable', it *could* be transformed
				// into a liquid, otherwise, continue with the next node.
				if (!cf.floodable)
					continue;
				floodable_node = n0.getContent();
				liquid_kind = CONTENT_AIR;
				break;
		}

		/*
			Collect information about the environment
		 */
		NodeNeighbor sources[6]; // surrounding sources
		int num_sources = 0;
		NodeNeighbor flows[6]; // surrounding flowing liquid nodes
		int num_flows = 0;
		NodeNeighbor airs[6]; // surrounding air
		int num_airs = 0;
		NodeNeighbor neutrals[6]; // nodes that are solid or another kind of liquid
		int num_neutrals = 0;
		bool flowing_down = false;
		bool ignored_sources = false;
		for (u16 i = 0; i < 6; i++) {
			NeighborType nt = NEIGHBOR_SAME_LEVEL;
			switch (i) {
				case 0:
					nt = NEIGHBOR_UPPER;
					break;
				case 5:
					nt = NEIGHBOR_LOWER;
					break;
				default:
					break;
			}
			v3s16 npos = p0 + liquid_6dirs[i];
			NodeNeighbor nb(getNode(npos), nt, npos);
			const ContentFeatures &cfnb = m_nodedef->get(nb.n);
			switch (m_nodedef->get(nb.n.getContent()).liquid_type) {
				case LIQUID_NONE:
					if (cfnb.floodable) {
						airs[num_airs++] = nb;
						// if the current node is a water source the neighbor
						// should be enqueded for transformation regardless of whether the
						// current node changes or not.
						if (nb.t != NEIGHBOR_UPPER && liquid_type != LIQUID_NONE)
							m_transforming_liquid.push_back(npos);
						// if the current node happens to be a flowing node, it will start to flow down here.
						if (nb.t == NEIGHBOR_LOWER)
							flowing_down = true;
					} else {
						neutrals[num_neutrals++] = nb;
						if (nb.n.getContent() == CONTENT_IGNORE) {
							// If node below is ignore prevent water from
							// spreading outwards and otherwise prevent from
							// flowing away as ignore node might be the source