Browse Source

Split servermap.cpp/h off from map.cpp/h

sfan5 1 month ago
parent
commit
dfba79f8ff
12 changed files with 1433 additions and 1378 deletions
  1. 1 0
      src/CMakeLists.txt
  2. 1 1
      src/emerge.cpp
  3. 1 1
      src/emerge.h
  4. 0 1197
      src/map.cpp
  5. 1 174
      src/map.h
  6. 1 1
      src/mapgen/treegen.cpp
  7. 1 1
      src/network/clientpackethandler.cpp
  8. 1 1
      src/server.cpp
  9. 0 1
      src/serverenvironment.cpp
  10. 1 1
      src/serverenvironment.h
  11. 1230 0
      src/servermap.cpp
  12. 195 0
      src/servermap.h

+ 1 - 0
src/CMakeLists.txt

@@ -417,6 +417,7 @@ set(common_SRCS
 	serialization.cpp
 	server.cpp
 	serverenvironment.cpp
+	servermap.cpp
 	settings.cpp
 	staticobject.cpp
 	terminal_chat_console.cpp

+ 1 - 1
src/emerge.cpp

@@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "irrlicht_changes/printing.h"
 #include "filesys.h"
 #include "log.h"
-#include "map.h"
+#include "servermap.h"
 #include "mapblock.h"
 #include "mapgen/mg_biome.h"
 #include "mapgen/mg_ore.h"

+ 1 - 1
src/emerge.h

@@ -39,7 +39,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 class EmergeThread;
 class NodeDefManager;
 class Settings;
-
+class MapSettingsManager;
 class BiomeManager;
 class OreManager;
 class DecorationManager;

+ 0 - 1197
src/map.cpp

@@ -20,13 +20,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #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"
@@ -34,29 +31,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "util/directiontables.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 "irrlicht_changes/printing.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
@@ -478,446 +453,6 @@ 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)
-	{ }
-};
-
-static s8 get_max_liquid_level(NodeNeighbor nb, s8 current_max_node_level)
-{
-	s8 max_node_level = current_max_node_level;
-	u8 nb_liquid_level = (nb.n.param2 & LIQUID_LEVEL_MASK);
-	switch (nb.t) {
-		case NEIGHBOR_UPPER:
-			if (nb_liquid_level + WATER_DROP_BOOST > current_max_node_level) {
-				max_node_level = LIQUID_LEVEL_MAX;
-				if (nb_liquid_level + WATER_DROP_BOOST < LIQUID_LEVEL_MAX)
-					max_node_level = nb_liquid_level + WATER_DROP_BOOST;
-			} else if (nb_liquid_level > current_max_node_level) {
-				max_node_level = nb_liquid_level;
-			}
-			break;
-		case NEIGHBOR_LOWER:
-			break;
-		case NEIGHBOR_SAME_LEVEL:
-			if ((nb.n.param2 & LIQUID_FLOW_DOWN_MASK) != LIQUID_FLOW_DOWN_MASK &&
-					nb_liquid_level > 0 && nb_liquid_level - 1 > max_node_level)
-				max_node_level = nb_liquid_level - 1;
-			break;
-	}
-	return max_node_level;
-}
-
-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::vector<v3s16> must_reflow;
-
-	std::vector<std::pair<v3s16, MapNode> > changed_nodes;
-
-	std::vector<v3s16> check_for_falling;
-
-	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;
-			case LiquidType_END:
-				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;
-		bool floating_node_above = 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);
-			if (nt == NEIGHBOR_UPPER && cfnb.floats)
-				floating_node_above = true;
-			switch (cfnb.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
-							if (nb.t == NEIGHBOR_LOWER)
-								flowing_down = true;
-							else
-								ignored_sources = true;
-						}
-					}
-					break;
-				case LIQUID_SOURCE:
-					// if this node is not (yet) of a liquid type, choose the first liquid type we encounter
-					if (liquid_kind == CONTENT_AIR)
-						liquid_kind = cfnb.liquid_alternative_flowing_id;
-					if (cfnb.liquid_alternative_flowing_id != liquid_kind) {
-						neutrals[num_neutrals++] = nb;
-					} else {
-						// Do not count bottom source, it will screw things up
-						if(nt != NEIGHBOR_LOWER)
-							sources[num_sources++] = nb;
-					}
-					break;
-				case LIQUID_FLOWING:
-					if (nb.t != NEIGHBOR_SAME_LEVEL ||
-						(nb.n.param2 & LIQUID_FLOW_DOWN_MASK) != LIQUID_FLOW_DOWN_MASK) {
-						// if this node is not (yet) of a liquid type, choose the first liquid type we encounter
-						// but exclude falling liquids on the same level, they cannot flow here anyway
-
-						// used to determine if the neighbor can even flow into this node
-						s8 max_level_from_neighbor = get_max_liquid_level(nb, -1);
-						u8 range = m_nodedef->get(cfnb.liquid_alternative_flowing_id).liquid_range;
-
-						if (liquid_kind == CONTENT_AIR &&
-								max_level_from_neighbor >= (LIQUID_LEVEL_MAX + 1 - range))
-							liquid_kind = cfnb.liquid_alternative_flowing_id;
-					}
-					if (cfnb.liquid_alternative_flowing_id != liquid_kind) {
-						neutrals[num_neutrals++] = nb;
-					} else {
-						flows[num_flows++] = nb;
-						if (nb.t == NEIGHBOR_LOWER)
-							flowing_down = true;
-					}
-					break;
-				case LiquidType_END:
-					break;
-			}
-		}
-
-		/*
-			decide on the type (and possibly level) of the current node
-		 */
-		content_t new_node_content;
-		s8 new_node_level = -1;
-		s8 max_node_level = -1;
-
-		u8 range = m_nodedef->get(liquid_kind).liquid_range;
-		if (range > LIQUID_LEVEL_MAX + 1)
-			range = LIQUID_LEVEL_MAX + 1;
-
-		if ((num_sources >= 2 && m_nodedef->get(liquid_kind).liquid_renewable) || liquid_type == LIQUID_SOURCE) {
-			// liquid_kind will be set to either the flowing alternative of the node (if it's a liquid)
-			// or the flowing alternative of the first of the surrounding sources (if it's air), so
-			// it's perfectly safe to use liquid_kind here to determine the new node content.
-			new_node_content = m_nodedef->get(liquid_kind).liquid_alternative_source_id;
-		} else if (num_sources >= 1 && sources[0].t != NEIGHBOR_LOWER) {
-			// liquid_kind is set properly, see above
-			max_node_level = new_node_level = LIQUID_LEVEL_MAX;
-			if (new_node_level >= (LIQUID_LEVEL_MAX + 1 - range))
-				new_node_content = liquid_kind;
-			else
-				new_node_content = floodable_node;
-		} else if (ignored_sources && liquid_level >= 0) {
-			// Maybe there are neighboring sources that aren't loaded yet
-			// so prevent flowing away.
-			new_node_level = liquid_level;
-			new_node_content = liquid_kind;
-		} else {
-			// no surrounding sources, so get the maximum level that can flow into this node
-			for (u16 i = 0; i < num_flows; i++) {
-				max_node_level = get_max_liquid_level(flows[i], max_node_level);
-			}
-
-			u8 viscosity = m_nodedef->get(liquid_kind).liquid_viscosity;
-			if (viscosity > 1 && max_node_level != liquid_level) {
-				// amount to gain, limited by viscosity
-				// must be at least 1 in absolute value
-				s8 level_inc = max_node_level - liquid_level;
-				if (level_inc < -viscosity || level_inc > viscosity)
-					new_node_level = liquid_level + level_inc/viscosity;
-				else if (level_inc < 0)
-					new_node_level = liquid_level - 1;
-				else if (level_inc > 0)
-					new_node_level = liquid_level + 1;
-				if (new_node_level != max_node_level)
-					must_reflow.push_back(p0);
-			} else {
-				new_node_level = max_node_level;
-			}
-
-			if (max_node_level >= (LIQUID_LEVEL_MAX + 1 - range))
-				new_node_content = liquid_kind;
-			else
-				new_node_content = floodable_node;
-
-		}
-
-		/*
-			check if anything has changed. if not, just continue with the next node.
-		 */
-		if (new_node_content == n0.getContent() &&
-				(m_nodedef->get(n0.getContent()).liquid_type != LIQUID_FLOWING ||
-				((n0.param2 & LIQUID_LEVEL_MASK) == (u8)new_node_level &&
-				((n0.param2 & LIQUID_FLOW_DOWN_MASK) == LIQUID_FLOW_DOWN_MASK)
-				== flowing_down)))
-			continue;
-
-		/*
-			check if there is a floating node above that needs to be updated.
-		 */
-		if (floating_node_above && new_node_content == CONTENT_AIR)
-			check_for_falling.push_back(p0);
-
-		/*
-			update the current node
-		 */
-		MapNode n00 = n0;
-		//bool flow_down_enabled = (flowing_down && ((n0.param2 & LIQUID_FLOW_DOWN_MASK) != LIQUID_FLOW_DOWN_MASK));
-		if (m_nodedef->get(new_node_content).liquid_type == LIQUID_FLOWING) {
-			// set level to last 3 bits, flowing down bit to 4th bit
-			n0.param2 = (flowing_down ? LIQUID_FLOW_DOWN_MASK : 0x00) | (new_node_level & LIQUID_LEVEL_MASK);
-		} else {
-			// set the liquid level and flow bits to 0
-			n0.param2 &= ~(LIQUID_LEVEL_MASK | LIQUID_FLOW_DOWN_MASK);
-		}
-
-		// change the node.
-		n0.setContent(new_node_content);
-
-		// on_flood() the node
-		if (floodable_node != CONTENT_AIR) {
-			if (env->getScriptIface()->node_on_flood(p0, n00, n0))
-				continue;
-		}
-
-		// Ignore light (because calling voxalgo::update_lighting_nodes)
-		ContentLightingFlags f0 = m_nodedef->getLightingFlags(n0);
-		n0.setLight(LIGHTBANK_DAY, 0, f0);
-		n0.setLight(LIGHTBANK_NIGHT, 0, f0);
-
-		// Find out whether there is a suspect for this action
-		std::string suspect;
-		if (m_gamedef->rollback())
-			suspect = m_gamedef->rollback()->getSuspect(p0, 83, 1);
-
-		if (m_gamedef->rollback() && !suspect.empty()) {
-			// Blame suspect
-			RollbackScopeActor rollback_scope(m_gamedef->rollback(), suspect, true);
-			// Get old node for rollback
-			RollbackNode rollback_oldnode(this, p0, m_gamedef);
-			// Set node
-			setNode(p0, n0);
-			// Report
-			RollbackNode rollback_newnode(this, p0, m_gamedef);
-			RollbackAction action;
-			action.setSetNode(p0, rollback_oldnode, rollback_newnode);
-			m_gamedef->rollback()->reportAction(action);
-		} else {
-			// Set node
-			setNode(p0, n0);
-		}
-
-		v3s16 blockpos = getNodeBlockPos(p0);
-		MapBlock *block = getBlockNoCreateNoEx(blockpos);
-		if (block != NULL) {
-			modified_blocks[blockpos] =  block;
-			changed_nodes.emplace_back(p0, n00);
-		}
-
-		/*
-			enqueue neighbors for update if necessary
-		 */
-		switch (m_nodedef->get(n0.getContent()).liquid_type) {
-			case LIQUID_SOURCE:
-			case LIQUID_FLOWING:
-				// make sure source flows into all neighboring nodes
-				for (u16 i = 0; i < num_flows; i++)
-					if (flows[i].t != NEIGHBOR_UPPER)
-						m_transforming_liquid.push_back(flows[i].p);
-				for (u16 i = 0; i < num_airs; i++)
-					if (airs[i].t != NEIGHBOR_UPPER)
-						m_transforming_liquid.push_back(airs[i].p);
-				break;
-			case LIQUID_NONE:
-				// this flow has turned to air; neighboring flows might need to do the same
-				for (u16 i = 0; i < num_flows; i++)
-					m_transforming_liquid.push_back(flows[i].p);
-				break;
-			case LiquidType_END:
-				break;
-		}
-	}
-	//infostream<<"Map::transformLiquids(): loopcount="<<loopcount<<std::endl;
-
-	for (const auto &iter : must_reflow)
-		m_transforming_liquid.push_back(iter);
-
-	voxalgo::update_lighting_nodes(this, changed_nodes, modified_blocks);
-
-	for (const v3s16 &p : check_for_falling) {
-		env->getScriptIface()->check_for_falling(p);
-	}
-
-	env->getScriptIface()->on_liquid_transformed(changed_nodes);
-
-	/* ----------------------------------------------------------------------
-	 * Manage the queue so that it does not grow indefinitely
-	 */
-	u16 time_until_purge = g_settings->getU16("liquid_queue_purge_time");
-
-	if (time_until_purge == 0)
-		return; // Feature disabled
-
-	time_until_purge *= 1000;	// seconds -> milliseconds
-
-	u64 curr_time = porting::getTimeMs();
-	u32 prev_unprocessed = m_unprocessed_count;
-	m_unprocessed_count = m_transforming_liquid.size();
-
-	// if unprocessed block count is decreasing or stable
-	if (m_unprocessed_count <= prev_unprocessed) {
-		m_queue_size_timer_started = false;
-	} else {
-		if (!m_queue_size_timer_started)
-			m_inc_trending_up_start_time = curr_time;
-		m_queue_size_timer_started = true;
-	}
-
-	// Account for curr_time overflowing
-	if (m_queue_size_timer_started && m_inc_trending_up_start_time > curr_time)
-		m_queue_size_timer_started = false;
-
-	/* If the queue has been growing for more than liquid_queue_purge_time seconds
-	 * and the number of unprocessed blocks is still > liquid_loop_max then we
-	 * cannot keep up; dump the oldest blocks from the queue so that the queue
-	 * has liquid_loop_max items in it
-	 */
-	if (m_queue_size_timer_started
-			&& curr_time - m_inc_trending_up_start_time > time_until_purge
-			&& m_unprocessed_count > liquid_loop_max) {
-
-		size_t dump_qty = m_unprocessed_count - liquid_loop_max;
-
-		infostream << "transformLiquids(): DUMPING " << dump_qty
-		           << " blocks from the queue" << std::endl;
-
-		while (dump_qty--)
-			m_transforming_liquid.pop_front();
-
-		m_queue_size_timer_started = false; // optimistically assume we can keep up now
-		m_unprocessed_count = m_transforming_liquid.size();
-	}
-}
-
 std::vector<v3s16> Map::findNodesWithMetadata(v3s16 p1, v3s16 p2)
 {
 	std::vector<v3s16> positions_with_meta;
@@ -1225,738 +760,6 @@ bool Map::isBlockOccluded(v3s16 pos_relative, v3s16 cam_pos_nodes, bool simple_c
 	return true;
 }
 
-/*
-	ServerMap
-*/
-ServerMap::ServerMap(const std::string &savedir, IGameDef *gamedef,
-		EmergeManager *emerge, MetricsBackend *mb):
-	Map(gamedef),
-	settings_mgr(savedir + DIR_DELIM + "map_meta.txt"),
-	m_emerge(emerge)
-{
-	verbosestream<<FUNCTION_NAME<<std::endl;
-
-	// Tell the EmergeManager about our MapSettingsManager
-	emerge->map_settings_mgr = &settings_mgr;
-
-	/*
-		Try to load map; if not found, create a new one.
-	*/
-
-	// Determine which database backend to use
-	std::string conf_path = savedir + DIR_DELIM + "world.mt";
-	Settings conf;
-	bool succeeded = conf.readConfigFile(conf_path.c_str());
-	if (!succeeded || !conf.exists("backend")) {
-		// fall back to sqlite3
-		conf.set("backend", "sqlite3");
-	}
-	std::string backend = conf.get("backend");
-	dbase = createDatabase(backend, savedir, conf);
-	if (conf.exists("readonly_backend")) {
-		std::string readonly_dir = savedir + DIR_DELIM + "readonly";
-		dbase_ro = createDatabase(conf.get("readonly_backend"), readonly_dir, conf);
-	}
-	if (!conf.updateConfigFile(conf_path.c_str()))
-		errorstream << "ServerMap::ServerMap(): Failed to update world.mt!" << std::endl;
-
-	m_savedir = savedir;
-	m_map_saving_enabled = false;
-
-	m_save_time_counter = mb->addCounter(
-		"minetest_map_save_time", "Time spent saving blocks (in microseconds)");
-	m_save_count_counter = mb->addCounter(
-		"minetest_map_saved_blocks", "Number of blocks saved");
-	m_loaded_blocks_gauge = mb->addGauge(
-		"minetest_map_loaded_blocks", "Number of loaded blocks");
-
-	m_map_compression_level = rangelim(g_settings->getS16("map_compression_level_disk"), -1, 9);
-
-	try {
-		// If directory exists, check contents and load if possible
-		if (fs::PathExists(m_savedir)) {
-			// If directory is empty, it is safe to save into it.
-			if (fs::GetDirListing(m_savedir).empty()) {
-				infostream<<"ServerMap: Empty save directory is valid."
-						<<std::endl;
-				m_map_saving_enabled = true;
-			}
-			else
-			{
-
-				if (settings_mgr.loadMapMeta()) {
-					infostream << "ServerMap: Metadata loaded from "
-						<< savedir << std::endl;
-				} else {
-					infostream << "ServerMap: Metadata could not be loaded "
-						"from " << savedir << ", assuming valid save "
-						"directory." << std::endl;
-				}
-
-				m_map_saving_enabled = true;
-				// Map loaded, not creating new one
-				return;
-			}
-		}
-		// If directory doesn't exist, it is safe to save to it
-		else{
-			m_map_saving_enabled = true;
-		}
-	}
-	catch(std::exception &e)
-	{
-		warningstream<<"ServerMap: Failed to load map from "<<savedir
-				<<", exception: "<<e.what()<<std::endl;
-		infostream<<"Please remove the map or fix it."<<std::endl;
-		warningstream<<"Map saving will be disabled."<<std::endl;
-	}
-}
-
-ServerMap::~ServerMap()
-{
-	verbosestream<<FUNCTION_NAME<<std::endl;
-
-	try
-	{
-		if (m_map_saving_enabled) {
-			// Save only changed parts
-			save(MOD_STATE_WRITE_AT_UNLOAD);
-			infostream << "ServerMap: Saved map to " << m_savedir << std::endl;
-		} else {
-			infostream << "ServerMap: Map not saved" << std::endl;
-		}
-	}
-	catch(std::exception &e)
-	{
-		errorstream << "ServerMap: Failed to save map to " << m_savedir
-				 << ", exception: " << e.what() << std::endl;
-	}
-
-	/*
-		Close database if it was opened
-	*/
-	delete dbase;
-	delete dbase_ro;
-
-	deleteDetachedBlocks();
-}
-
-MapgenParams *ServerMap::getMapgenParams()
-{
-	// getMapgenParams() should only ever be called after Server is initialized
-	assert(settings_mgr.mapgen_params != NULL);
-	return settings_mgr.mapgen_params;
-}
-
-u64 ServerMap::getSeed()
-{
-	return getMapgenParams()->seed;
-}
-
-bool ServerMap::blockpos_over_mapgen_limit(v3s16 p)
-{
-	const s16 mapgen_limit_bp = rangelim(
-		getMapgenParams()->mapgen_limit, 0, MAX_MAP_GENERATION_LIMIT) /
-		MAP_BLOCKSIZE;
-	return p.X < -mapgen_limit_bp ||
-		p.X >  mapgen_limit_bp ||
-		p.Y < -mapgen_limit_bp ||
-		p.Y >  mapgen_limit_bp ||
-		p.Z < -mapgen_limit_bp ||
-		p.Z >  mapgen_limit_bp;
-}
-
-bool ServerMap::initBlockMake(v3s16 blockpos, BlockMakeData *data)
-{
-	s16 csize = getMapgenParams()->chunksize;
-	v3s16 bpmin = EmergeManager::getContainingChunk(blockpos, csize);
-	v3s16 bpmax = bpmin + v3s16(1, 1, 1) * (csize - 1);
-
-	if (!m_chunks_in_progress.insert(bpmin).second)
-		return false;
-
-	bool enable_mapgen_debug_info = m_emerge->enable_mapgen_debug_info;
-	EMERGE_DBG_OUT("initBlockMake(): " << bpmin << " - " << bpmax);
-
-	v3s16 extra_borders(1, 1, 1);
-	v3s16 full_bpmin = bpmin - extra_borders;
-	v3s16 full_bpmax = bpmax + extra_borders;
-
-	// Do nothing if not inside mapgen limits (+-1 because of neighbors)
-	if (blockpos_over_mapgen_limit(full_bpmin) ||
-			blockpos_over_mapgen_limit(full_bpmax))
-		return false;
-
-	data->seed = getSeed();
-	data->blockpos_min = bpmin;
-	data->blockpos_max = bpmax;
-	data->nodedef = m_nodedef;
-
-	/*
-		Create the whole area of this and the neighboring blocks
-	*/
-	for (s16 x = full_bpmin.X; x <= full_bpmax.X; x++)
-	for (s16 z = full_bpmin.Z; z <= full_bpmax.Z; z++) {
-		v2s16 sectorpos(x, z);
-		// Sector metadata is loaded from disk if not already loaded.
-		MapSector *sector = createSector(sectorpos);
-		FATAL_ERROR_IF(sector == NULL, "createSector() failed");
-
-		for (s16 y = full_bpmin.Y; y <= full_bpmax.Y; y++) {
-			v3s16 p(x, y, z);
-
-			MapBlock *block = emergeBlock(p, false);
-			if (block == NULL) {
-				block = createBlock(p);
-
-				// Block gets sunlight if this is true.
-				// Refer to the map generator heuristics.
-				bool ug = m_emerge->isBlockUnderground(p);
-				block->setIsUnderground(ug);
-			}
-		}
-	}
-
-	/*
-		Now we have a big empty area.
-
-		Make a ManualMapVoxelManipulator that contains this and the
-		neighboring blocks
-	*/
-
-	data->vmanip = new MMVManip(this);
-	data->vmanip->initialEmerge(full_bpmin, full_bpmax);
-
-	// Data is ready now.
-	return true;
-}
-
-void ServerMap::finishBlockMake(BlockMakeData *data,
-	std::map<v3s16, MapBlock*> *changed_blocks)
-{
-	v3s16 bpmin = data->blockpos_min;
-	v3s16 bpmax = data->blockpos_max;
-
-	bool enable_mapgen_debug_info = m_emerge->enable_mapgen_debug_info;
-	EMERGE_DBG_OUT("finishBlockMake(): " << bpmin << " - " << bpmax);
-
-	/*
-		Blit generated stuff to map
-		NOTE: blitBackAll adds nearly everything to changed_blocks
-	*/
-	data->vmanip->blitBackAll(changed_blocks);
-
-	EMERGE_DBG_OUT("finishBlockMake: changed_blocks.size()="
-		<< changed_blocks->size());
-
-	/*
-		Copy transforming liquid information
-	*/
-	while (data->transforming_liquid.size()) {
-		m_transforming_liquid.push_back(data->transforming_liquid.front());
-		data->transforming_liquid.pop_front();
-	}
-
-	for (auto &changed_block : *changed_blocks) {
-		MapBlock *block = changed_block.second;
-		if (!block)
-			continue;
-		/*
-			Update is air cache of the MapBlocks
-		*/
-		block->expireIsAirCache();
-		/*
-			Set block as modified
-		*/
-		block->raiseModified(MOD_STATE_WRITE_NEEDED,
-			MOD_REASON_EXPIRE_IS_AIR);
-	}
-
-	/*
-		Set central blocks as generated
-	*/
-	for (s16 x = bpmin.X; x <= bpmax.X; x++)
-	for (s16 z = bpmin.Z; z <= bpmax.Z; z++)
-	for (s16 y = bpmin.Y; y <= bpmax.Y; y++) {
-		MapBlock *block = getBlockNoCreateNoEx(v3s16(x, y, z));
-		if (!block)
-			continue;
-
-		block->setGenerated(true);
-	}
-
-	/*
-		Save changed parts of map
-		NOTE: Will be saved later.
-	*/
-	//save(MOD_STATE_WRITE_AT_UNLOAD);
-	m_chunks_in_progress.erase(bpmin);
-}
-
-MapSector *ServerMap::createSector(v2s16 p2d)
-{
-	/*
-		Check if it exists already in memory
-	*/
-	MapSector *sector = getSectorNoGenerate(p2d);
-	if (sector)
-		return sector;
-
-	/*
-		Do not create over max mapgen limit
-	*/
-	if (blockpos_over_max_limit(v3s16(p2d.X, 0, p2d.Y)))
-		throw InvalidPositionException("createSector(): pos over max mapgen limit");
-
-	/*
-		Generate blank sector
-	*/
-	sector = new MapSector(this, p2d, m_gamedef);
-
-	/*
-		Insert to container
-	*/
-	m_sectors[p2d] = sector;
-
-	return sector;
-}
-
-MapBlock * ServerMap::createBlock(v3s16 p)
-{
-	v2s16 p2d(p.X, p.Z);
-	s16 block_y = p.Y;
-
-	/*
-		This will create or load a sector if not found in memory.
-	*/
-	MapSector *sector;
-	try {
-		sector = createSector(p2d);
-	} catch (InvalidPositionException &e) {
-		infostream<<"createBlock: createSector() failed"<<std::endl;
-		throw e;
-	}
-
-	/*
-		Try to get a block from the sector
-	*/
-
-	MapBlock *block = sector->getBlockNoCreateNoEx(block_y);
-	if (block)
-		return block;
-
-	// Create blank
-	try {
-		block = sector->createBlankBlock(block_y);
-	} catch (InvalidPositionException &e) {
-		infostream << "createBlock: createBlankBlock() failed" << std::endl;
-		throw e;
-	}
-
-	return block;
-}
-
-MapBlock * ServerMap::emergeBlock(v3s16 p, bool create_blank)
-{
-	{
-		MapBlock *block = getBlockNoCreateNoEx(p);
-		if (block)
-			return block;
-	}
-
-	{
-		MapBlock *block = loadBlock(p);
-		if(block)
-			return block;
-	}
-
-	if (create_blank) {
-		try {
-			MapSector *sector = createSector(v2s16(p.X, p.Z));
-			return sector->createBlankBlock(p.Y);
-		} catch (InvalidPositionException &e) {}
-	}
-
-	return NULL;
-}
-
-MapBlock *ServerMap::getBlockOrEmerge(v3s16 p3d, bool generate)
-{
-	MapBlock *block = getBlockNoCreateNoEx(p3d);
-	if (block == NULL)
-		m_emerge->enqueueBlockEmerge(PEER_ID_INEXISTENT, p3d, generate);
-
-	return block;
-}
-
-bool ServerMap::isBlockInQueue(v3s16 pos)
-{
-	return m_emerge && m_emerge->isBlockInQueue(pos);
-}
-
-void ServerMap::addNodeAndUpdate(v3s16 p, MapNode n,
-		std::map<v3s16, MapBlock*> &modified_blocks,
-		bool remove_metadata)
-{
-	Map::addNodeAndUpdate(p, n, modified_blocks, remove_metadata);
-
-	/*
-		Add neighboring liquid nodes and this node to transform queue.
-		(it's vital for the node itself to get updated last, if it was removed.)
-	 */
-
-	for (const v3s16 &dir : g_7dirs) {
-		v3s16 p2 = p + dir;
-
-		bool is_valid_position;
-		MapNode n2 = getNode(p2, &is_valid_position);
-		if(is_valid_position &&
-				(m_nodedef->get(n2).isLiquid() ||
-				n2.getContent() == CONTENT_AIR))
-			m_transforming_liquid.push_back(p2);
-	}
-}
-
-// 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;
-
-	MMVManip *vm = mg->vm;
-	if (!vm)
-		return;
-
-	if (!vm->m_area.contains(pos))
-		return;
-
-	s32 idx = vm->m_area.index(pos);
-	vm->m_data[idx] = getNode(pos);
-	vm->m_flags[idx] &= ~VOXELFLAG_NO_DATA;
-
-	vm->m_is_dirty = true;
-}
-
-void ServerMap::reportMetrics(u64 save_time_us, u32 saved_blocks, u32 all_blocks)
-{
-	m_loaded_blocks_gauge->set(all_blocks);
-	m_save_time_counter->increment(save_time_us);
-	m_save_count_counter->increment(saved_blocks);
-}
-
-void ServerMap::save(ModifiedState save_level)
-{
-	if (!m_map_saving_enabled) {
-		warningstream<<"Not saving map, saving disabled."<<std::endl;
-		return;
-	}
-
-	const auto start_time = porting::getTimeUs();
-
-	if(save_level == MOD_STATE_CLEAN)
-		infostream<<"ServerMap: Saving whole map, this can take time."
-				<<std::endl;
-
-	if (m_map_metadata_changed || save_level == MOD_STATE_CLEAN) {
-		if (settings_mgr.saveMapMeta())
-			m_map_metadata_changed = false;
-	}
-
-	// Profile modified reasons
-	Profiler modprofiler;
-
-	u32 block_count = 0;
-	u32 block_count_all = 0; // Number of blocks in memory
-
-	// Don't do anything with sqlite unless something is really saved
-	bool save_started = false;
-
-	for (auto &sector_it : m_sectors) {
-		MapSector *sector = sector_it.second;
-
-		MapBlockVect blocks;
-		sector->getBlocks(blocks);
-
-		for (MapBlock *block : blocks) {
-			block_count_all++;
-
-			if(block->getModified() >= (u32)save_level) {
-				// Lazy beginSave()
-				if(!save_started) {
-					beginSave();
-					save_started = true;
-				}
-
-				modprofiler.add(block->getModifiedReasonString(), 1);
-
-				saveBlock(block);
-				block_count++;
-			}
-		}
-	}
-
-	if(save_started)
-		endSave();
-
-	/*
-		Only print if something happened or saved whole map
-	*/
-	if(save_level == MOD_STATE_CLEAN
-			|| block_count != 0) {
-		infostream << "ServerMap: Written: "
-				<< block_count << " blocks"
-				<< ", " << block_count_all << " blocks in memory."
-				<< std::endl;
-		PrintInfo(infostream); // ServerMap/ClientMap:
-		infostream<<"Blocks modified by: "<<std::endl;
-		modprofiler.print(infostream);
-	}
-
-	const auto end_time = porting::getTimeUs();
-	reportMetrics(end_time - start_time, block_count, block_count_all);
-}
-
-void ServerMap::listAllLoadableBlocks(std::vector<v3s16> &dst)
-{
-	dbase->listAllLoadableBlocks(dst);
-	if (dbase_ro)
-		dbase_ro->listAllLoadableBlocks(dst);
-}
-
-void ServerMap::listAllLoadedBlocks(std::vector<v3s16> &dst)
-{
-	for (auto &sector_it : m_sectors) {
-		MapSector *sector = sector_it.second;
-
-		MapBlockVect blocks;
-		sector->getBlocks(blocks);
-
-		for (MapBlock *block : blocks) {
-			v3s16 p = block->getPos();
-			dst.push_back(p);
-		}
-	}
-}
-
-MapDatabase *ServerMap::createDatabase(
-	const std::string &name,
-	const std::string &savedir,
-	Settings &conf)
-{
-	if (name == "sqlite3")
-		return new MapDatabaseSQLite3(savedir);
-	if (name == "dummy")
-		return new Database_Dummy();
-	#if USE_LEVELDB
-	if (name == "leveldb")
-		return new Database_LevelDB(savedir);
-	#endif
-	#if USE_REDIS
-	if (name == "redis")
-		return new Database_Redis(conf);
-	#endif
-	#if USE_POSTGRESQL
-	if (name == "postgresql") {
-		std::string connect_string;
-		conf.getNoEx("pgsql_connection", connect_string);
-		return new MapDatabasePostgreSQL(connect_string);
-	}
-	#endif
-
-	throw BaseException(std::string("Database backend ") + name + " not supported.");
-}
-
-void ServerMap::beginSave()
-{
-	dbase->beginSave();
-}
-
-void ServerMap::endSave()
-{
-	dbase->endSave();
-}
-
-bool ServerMap::saveBlock(MapBlock *block)
-{
-	return saveBlock(block, dbase, m_map_compression_level);
-}
-
-bool ServerMap::saveBlock(MapBlock *block, MapDatabase *db, int compression_level)
-{
-	v3s16 p3d = block->getPos();
-
-	// Format used for writing
-	u8 version = SER_FMT_VER_HIGHEST_WRITE;
-
-	/*
-		[0] u8 serialization version
-		[1] data
-	*/
-	std::ostringstream o(std::ios_base::binary);
-	o.write((char*) &version, 1);
-	block->serialize(o, version, true, compression_level);
-
-	// FIXME: zero copy possible in c++20 or with custom rdbuf
-	bool ret = db->saveBlock(p3d, o.str());
-	if (ret) {
-		// We just wrote it to the disk so clear modified flag
-		block->resetModified();
-	}
-	return ret;
-}
-
-void ServerMap::loadBlock(std::string *blob, v3s16 p3d, MapSector *sector, bool save_after_load)
-{
-	try {
-		std::istringstream is(*blob, std::ios_base::binary);
-
-		u8 version = SER_FMT_VER_INVALID;
-		is.read((char*)&version, 1);
-
-		if(is.fail())
-			throw SerializationError("ServerMap::loadBlock(): Failed"
-					" to read MapBlock version");
-
-		MapBlock *block = nullptr;
-		std::unique_ptr<MapBlock> block_created_new;
-		block = sector->getBlockNoCreateNoEx(p3d.Y);
-		if (!block) {
-			block_created_new = sector->createBlankBlockNoInsert(p3d.Y);
-			block = block_created_new.get();
-		}
-
-		{
-		ScopeProfiler sp(g_profiler, "ServerMap: deSer block", SPT_AVG);
-		// Read basic data
-		block->deSerialize(is, version, true);
-		}
-
-		// If it's a new block, insert it to the map
-		if (block_created_new) {
-			sector->insertBlock(std::move(block_created_new));
-			ReflowScan scanner(this, m_emerge->ndef);
-			scanner.scan(block, &m_transforming_liquid);
-		}
-
-		/*
-			Save blocks loaded in old format in new format
-		*/
-
-		//if(version < SER_FMT_VER_HIGHEST_READ || save_after_load)
-		// Only save if asked to; no need to update version
-		if(save_after_load)
-			saveBlock(block);
-
-		// We just loaded it from, so it's up-to-date.
-		block->resetModified();
-	}
-	catch(SerializationError &e)
-	{
-		errorstream<<"Invalid block data in database"
-				<<" ("<<p3d.X<<","<<p3d.Y<<","<<p3d.Z<<")"
-				<<" (SerializationError): "<<e.what()<<std::endl;
-
-		// TODO: Block should be marked as invalid in memory so that it is
-		// not touched but the game can run
-
-		if(g_settings->getBool("ignore_world_load_errors")){
-			errorstream<<"Ignoring block load error. Duck and cover! "
-					<<"(ignore_world_load_errors)"<<std::endl;
-		} else {
-			throw SerializationError("Invalid block data in database");
-		}
-	}
-}
-
-MapBlock* ServerMap::loadBlock(v3s16 blockpos)
-{
-	ScopeProfiler sp(g_profiler, "ServerMap: load block", SPT_AVG);
-	bool created_new = (getBlockNoCreateNoEx(blockpos) == NULL);
-
-	v2s16 p2d(blockpos.X, blockpos.Z);
-
-	std::string ret;
-	dbase->loadBlock(blockpos, &ret);
-	if (!ret.empty()) {
-		loadBlock(&ret, blockpos, createSector(p2d), false);
-	} else if (dbase_ro) {
-		dbase_ro->loadBlock(blockpos, &ret);
-		if (!ret.empty()) {
-			loadBlock(&ret, blockpos, createSector(p2d), false);
-		}
-	} else {
-		return NULL;
-	}
-
-	MapBlock *block = getBlockNoCreateNoEx(blockpos);
-	if (created_new && (block != NULL)) {
-		std::map<v3s16, MapBlock*> modified_blocks;
-		// Fix lighting if necessary
-		voxalgo::update_block_border_lighting(this, block, modified_blocks);
-		if (!modified_blocks.empty()) {
-			//Modified lighting, send event
-			MapEditEvent event;
-			event.type = MEET_OTHER;
-			event.setModifiedBlocks(modified_blocks);
-			dispatchEvent(event);
-		}
-	}
-	return block;
-}
-
-bool ServerMap::deleteBlock(v3s16 blockpos)
-{
-	if (!dbase->deleteBlock(blockpos))
-		return false;
-
-	MapBlock *block = getBlockNoCreateNoEx(blockpos);
-	if (block) {
-		v2s16 p2d(blockpos.X, blockpos.Z);
-		MapSector *sector = getSectorNoGenerate(p2d);
-		if (!sector)
-			return false;
-		// It may not be safe to delete the block from memory at the moment
-		// (pointers to it could still be in use)
-		m_detached_blocks.push_back(sector->detachBlock(block));
-	}
-
-	return true;
-}
-
-void ServerMap::deleteDetachedBlocks()
-{
-	for (const auto &block : m_detached_blocks) {
-		assert(block->isOrphan());
-		(void)block; // silence unused-variable warning in release builds
-	}
-
-	m_detached_blocks.clear();
-}
-
-void ServerMap::step()
-{
-	// Delete from memory blocks removed by deleteBlocks() only when pointers
-	// to them are (probably) no longer in use
-	deleteDetachedBlocks();
-}
-
-void ServerMap::PrintInfo(std::ostream &out)
-{
-	out<<"ServerMap: ";
-}
-
-bool ServerMap::repairBlockLight(v3s16 blockpos,
-	std::map<v3s16, MapBlock *> *modified_blocks)
-{
-	MapBlock *block = emergeBlock(blockpos, false);
-	if (!block || !block->isGenerated())
-		return false;
-	voxalgo::repair_block_light(this, block, modified_blocks);
-	return true;
-}
-
 MMVManip::MMVManip(Map *map):
 		VoxelManipulator(),
 		m_map(map)

+ 1 - 174
src/map.h

@@ -20,10 +20,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #pragma once
 
 #include <iostream>
-#include <sstream>
 #include <set>
 #include <map>
-#include <list>
 
 #include "irrlichttypes_bloated.h"
 #include "mapblock.h"
@@ -31,32 +29,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "constants.h"
 #include "voxel.h"
 #include "modifiedstate.h"
-#include "util/container.h"
-#include "util/metricsbackend.h"
 #include "util/numeric.h"
 #include "nodetimer.h"
-#include "map_settings_manager.h"
 #include "debug.h"
 
-class Settings;
-class MapDatabase;
-class ClientMap;
 class MapSector;
-class ServerMapSector;
-class MapBlock;
 class NodeMetadata;
 class IGameDef;
 class IRollbackManager;
-class EmergeManager;
-class MetricsBackend;
-class ServerEnvironment;
-struct BlockMakeData;
 
 /*
 	MapEditEvent
 */
 
-enum MapEditEventType{
+enum MapEditEventType {
 	// Node added (changed from air or something else to something)
 	MEET_ADDNODE,
 	// Node removed (changed to air)
@@ -335,165 +321,6 @@ protected:
 		u32 needed_count);
 };
 
-/*
-	ServerMap
-
-	This is the only map class that is able to generate map.
-*/
-
-class ServerMap : public Map
-{
-public:
-	/*
-		savedir: directory to which map data should be saved
-	*/
-	ServerMap(const std::string &savedir, IGameDef *gamedef, EmergeManager *emerge, MetricsBackend *mb);
-	~ServerMap();
-
-	/*
-		Get a sector from somewhere.
-		- Check memory
-		- Check disk (doesn't load blocks)
-		- Create blank one
-	*/
-	MapSector *createSector(v2s16 p);
-
-	/*
-		Blocks are generated by using these and makeBlock().
-	*/
-	bool blockpos_over_mapgen_limit(v3s16 p);
-	bool initBlockMake(v3s16 blockpos, BlockMakeData *data);
-	void finishBlockMake(BlockMakeData *data,
-		std::map<v3s16, MapBlock*> *changed_blocks);
-
-	/*
-		Get a block from somewhere.
-		- Memory
-		- Create blank
-	*/
-	MapBlock *createBlock(v3s16 p);
-
-	/*
-		Forcefully get a block from somewhere.
-		- Memory
-		- Load from disk
-		- Create blank filled with CONTENT_IGNORE
-
-	*/
-	MapBlock *emergeBlock(v3s16 p, bool create_blank=true) override;
-
-	/*
-		Try to get a block.
-		If it does not exist in memory, add it to the emerge queue.
-		- Memory
-		- Emerge Queue (deferred disk or generate)
-	*/
-	MapBlock *getBlockOrEmerge(v3s16 p3d, bool generate);
-
-	bool isBlockInQueue(v3s16 pos);
-
-	void addNodeAndUpdate(v3s16 p, MapNode n,
-			std::map<v3s16, MapBlock*> &modified_blocks,
-			bool remove_metadata) override;
-
-	/*
-		Database functions
-	*/
-	static MapDatabase *createDatabase(const std::string &name, const std::string &savedir, Settings &conf);
-
-	// Call these before and after saving of blocks
-	void beginSave() override;
-	void endSave() override;
-
-	void save(ModifiedState save_level) override;
-	void listAllLoadableBlocks(std::vector<v3s16> &dst);
-	void listAllLoadedBlocks(std::vector<v3s16> &dst);
-
-	MapgenParams *getMapgenParams();
-
-	bool saveBlock(MapBlock *block) override;
-	static bool saveBlock(MapBlock *block, MapDatabase *db, int compression_level = -1);
-	MapBlock* loadBlock(v3s16 p);
-	// Database version
-	void loadBlock(std::string *blob, v3s16 p3d, MapSector *sector, bool save_after_load=false);
-
-	// Blocks are removed from the map but not deleted from memory until
-	// deleteDetachedBlocks() is called, since pointers to them may still exist
-	// when deleteBlock() is called.
-	bool deleteBlock(v3s16 blockpos) override;
-
-	void deleteDetachedBlocks();
-
-	void step();
-
-	void updateVManip(v3s16 pos);
-
-	// For debug printing
-	void PrintInfo(std::ostream &out) override;
-
-	bool isSavingEnabled(){ return m_map_saving_enabled; }
-
-	u64 getSeed();
-
-	/*!
-	 * Fixes lighting in one map block.
-	 * May modify other blocks as well, as light can spread
-	 * out of the specified block.
-	 * Returns false if the block is not generated (so nothing
-	 * changed), true otherwise.
-	 */
-	bool repairBlockLight(v3s16 blockpos,
-		std::map<v3s16, MapBlock *> *modified_blocks);
-
-	void transformLiquids(std::map<v3s16, MapBlock*> & modified_blocks,
-			ServerEnvironment *env);
-
-	void transforming_liquid_add(v3s16 p);
-
-	MapSettingsManager settings_mgr;
-
-protected:
-
-	void reportMetrics(u64 save_time_us, u32 saved_blocks, u32 all_blocks) override;
-
-private:
-	friend class ModApiMapgen; // for m_transforming_liquid
-
-	// Emerge manager
-	EmergeManager *m_emerge;
-
-	std::string m_savedir;
-	bool m_map_saving_enabled;
-
-	int m_map_compression_level;
-
-	std::set<v3s16> m_chunks_in_progress;
-
-	// used by deleteBlock() and deleteDetachedBlocks()
-	std::vector<std::unique_ptr<MapBlock>> m_detached_blocks;
-
-	// Queued transforming water nodes
-	UniqueQueue<v3s16> m_transforming_liquid;
-	f32 m_transforming_liquid_loop_count_multiplier = 1.0f;
-	u32 m_unprocessed_count = 0;
-	u64 m_inc_trending_up_start_time = 0; // milliseconds
-	bool m_queue_size_timer_started = false;
-
-	/*
-		Metadata is re-written on disk only if this is true.
-		This is reset to false when written on disk.
-	*/
-	bool m_map_metadata_changed = true;
-	MapDatabase *dbase = nullptr;
-	MapDatabase *dbase_ro = nullptr;
-
-	// Map metrics
-	MetricGaugePtr m_loaded_blocks_gauge;
-	MetricCounterPtr m_save_time_counter;
-	MetricCounterPtr m_save_count_counter;
-};
-
-
 #define VMANIP_BLOCK_DATA_INEXIST     1
 #define VMANIP_BLOCK_CONTAINS_CIGNORE 2
 

+ 1 - 1
src/mapgen/treegen.cpp

@@ -23,7 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include <stack>
 #include "util/pointer.h"
 #include "util/numeric.h"
-#include "map.h"
+#include "servermap.h"
 #include "mapblock.h"
 #include "nodedef.h"
 #include "treegen.h"

+ 1 - 1
src/network/clientpackethandler.cpp

@@ -25,7 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "chatmessage.h"
 #include "client/clientmedia.h"
 #include "log.h"
-#include "map.h"
+#include "servermap.h"
 #include "mapsector.h"
 #include "client/minimap.h"
 #include "modchannels.h"

+ 1 - 1
src/server.cpp

@@ -26,7 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "network/serveropcodes.h"
 #include "server/ban.h"
 #include "environment.h"
-#include "map.h"
+#include "servermap.h"
 #include "threading/mutex_auto_lock.h"
 #include "constants.h"
 #include "voxel.h"

+ 0 - 1
src/serverenvironment.cpp

@@ -27,7 +27,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "nodedef.h"
 #include "nodemetadata.h"
 #include "gamedef.h"
-#include "map.h"
 #include "porting.h"
 #include "profiler.h"
 #include "raycast.h"

+ 1 - 1
src/serverenvironment.h

@@ -25,7 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 
 #include "activeobject.h"
 #include "environment.h"
-#include "map.h"
+#include "servermap.h"
 #include "settings.h"
 #include "server/activeobjectmgr.h"
 #include "util/numeric.h"

+ 1230 - 0
src/servermap.cpp

@@ -0,0 +1,1230 @@
+/*
+Minetest
+Copyright (C) 2010-2024 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 "filesys.h"
+#include "voxel.h"
+#include "voxelalgorithms.h"
+#include "porting.h"
+#include "serialization.h"
+#include "settings.h"
+#include "log.h"
+#include "profiler.h"
+#include "gamedef.h"
+#include "util/directiontables.h"
+#include "rollback_interface.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 "irrlicht_changes/printing.h"
+#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
+
+/*
+	ServerMap	
+*/
+
+ServerMap::ServerMap(const std::string &savedir, IGameDef *gamedef,
+		EmergeManager *emerge, MetricsBackend *mb):
+	Map(gamedef),
+	settings_mgr(savedir + DIR_DELIM + "map_meta.txt"),
+	m_emerge(emerge)
+{
+	verbosestream<<FUNCTION_NAME<<std::endl;
+
+	// Tell the EmergeManager about our MapSettingsManager
+	emerge->map_settings_mgr = &settings_mgr;
+
+	/*
+		Try to load map; if not found, create a new one.
+	*/
+
+	// Determine which database backend to use
+	std::string conf_path = savedir + DIR_DELIM + "world.mt";
+	Settings conf;
+	bool succeeded = conf.readConfigFile(conf_path.c_str());
+	if (!succeeded || !conf.exists("backend")) {
+		// fall back to sqlite3
+		conf.set("backend", "sqlite3");
+	}
+	std::string backend = conf.get("backend");
+	dbase = createDatabase(backend, savedir, conf);
+	if (conf.exists("readonly_backend")) {
+		std::string readonly_dir = savedir + DIR_DELIM + "readonly";
+		dbase_ro = createDatabase(conf.get("readonly_backend"), readonly_dir, conf);
+	}
+	if (!conf.updateConfigFile(conf_path.c_str()))
+		errorstream << "ServerMap::ServerMap(): Failed to update world.mt!" << std::endl;
+
+	m_savedir = savedir;
+	m_map_saving_enabled = false;
+
+	m_save_time_counter = mb->addCounter(
+		"minetest_map_save_time", "Time spent saving blocks (in microseconds)");
+	m_save_count_counter = mb->addCounter(
+		"minetest_map_saved_blocks", "Number of blocks saved");
+	m_loaded_blocks_gauge = mb->addGauge(
+		"minetest_map_loaded_blocks", "Number of loaded blocks");
+
+	m_map_compression_level = rangelim(g_settings->getS16("map_compression_level_disk"), -1, 9);
+
+	try {
+		// If directory exists, check contents and load if possible
+		if (fs::PathExists(m_savedir)) {
+			// If directory is empty, it is safe to save into it.
+			if (fs::GetDirListing(m_savedir).empty()) {
+				infostream<<"ServerMap: Empty save directory is valid."
+						<<std::endl;
+				m_map_saving_enabled = true;
+			}
+			else
+			{
+
+				if (settings_mgr.loadMapMeta()) {
+					infostream << "ServerMap: Metadata loaded from "
+						<< savedir << std::endl;
+				} else {
+					infostream << "ServerMap: Metadata could not be loaded "
+						"from " << savedir << ", assuming valid save "
+						"directory." << std::endl;
+				}
+
+				m_map_saving_enabled = true;
+				// Map loaded, not creating new one
+				return;
+			}
+		}
+		// If directory doesn't exist, it is safe to save to it
+		else{
+			m_map_saving_enabled = true;
+		}
+	}
+	catch(std::exception &e)
+	{
+		warningstream<<"ServerMap: Failed to load map from "<<savedir
+				<<", exception: "<<e.what()<<std::endl;
+		infostream<<"Please remove the map or fix it."<<std::endl;
+		warningstream<<"Map saving will be disabled."<<std::endl;
+	}
+}
+
+ServerMap::~ServerMap()
+{
+	verbosestream<<FUNCTION_NAME<<std::endl;
+
+	try
+	{
+		if (m_map_saving_enabled) {
+			// Save only changed parts
+			save(MOD_STATE_WRITE_AT_UNLOAD);
+			infostream << "ServerMap: Saved map to " << m_savedir << std::endl;
+		} else {
+			infostream << "ServerMap: Map not saved" << std::endl;
+		}
+	}
+	catch(std::exception &e)
+	{
+		errorstream << "ServerMap: Failed to save map to " << m_savedir
+				 << ", exception: " << e.what() << std::endl;
+	}
+
+	/*
+		Close database if it was opened
+	*/
+	delete dbase;
+	delete dbase_ro;
+
+	deleteDetachedBlocks();
+}
+
+MapgenParams *ServerMap::getMapgenParams()
+{
+	// getMapgenParams() should only ever be called after Server is initialized
+	assert(settings_mgr.mapgen_params != NULL);
+	return settings_mgr.mapgen_params;
+}
+
+u64 ServerMap::getSeed()
+{
+	return getMapgenParams()->seed;
+}
+
+bool ServerMap::blockpos_over_mapgen_limit(v3s16 p)
+{
+	const s16 mapgen_limit_bp = rangelim(
+		getMapgenParams()->mapgen_limit, 0, MAX_MAP_GENERATION_LIMIT) /
+		MAP_BLOCKSIZE;
+	return p.X < -mapgen_limit_bp ||
+		p.X >  mapgen_limit_bp ||
+		p.Y < -mapgen_limit_bp ||
+		p.Y >  mapgen_limit_bp ||
+		p.Z < -mapgen_limit_bp ||
+		p.Z >  mapgen_limit_bp;
+}
+
+bool ServerMap::initBlockMake(v3s16 blockpos, BlockMakeData *data)
+{
+	s16 csize = getMapgenParams()->chunksize;
+	v3s16 bpmin = EmergeManager::getContainingChunk(blockpos, csize);
+	v3s16 bpmax = bpmin + v3s16(1, 1, 1) * (csize - 1);
+
+	if (!m_chunks_in_progress.insert(bpmin).second)
+		return false;
+
+	bool enable_mapgen_debug_info = m_emerge->enable_mapgen_debug_info;
+	EMERGE_DBG_OUT("initBlockMake(): " << bpmin << " - " << bpmax);
+
+	v3s16 extra_borders(1, 1, 1);
+	v3s16 full_bpmin = bpmin - extra_borders;
+	v3s16 full_bpmax = bpmax + extra_borders;
+
+	// Do nothing if not inside mapgen limits (+-1 because of neighbors)
+	if (blockpos_over_mapgen_limit(full_bpmin) ||
+			blockpos_over_mapgen_limit(full_bpmax))
+		return false;
+
+	data->seed = getSeed();
+	data->blockpos_min = bpmin;
+	data->blockpos_max = bpmax;
+	data->nodedef = m_nodedef;
+
+	/*
+		Create the whole area of this and the neighboring blocks
+	*/
+	for (s16 x = full_bpmin.X; x <= full_bpmax.X; x++)
+	for (s16 z = full_bpmin.Z; z <= full_bpmax.Z; z++) {
+		v2s16 sectorpos(x, z);
+		// Sector metadata is loaded from disk if not already loaded.
+		MapSector *sector = createSector(sectorpos);
+		FATAL_ERROR_IF(sector == NULL, "createSector() failed");
+
+		for (s16 y = full_bpmin.Y; y <= full_bpmax.Y; y++) {
+			v3s16 p(x, y, z);
+
+			MapBlock *block = emergeBlock(p, false);
+			if (block == NULL) {
+				block = createBlock(p);
+
+				// Block gets sunlight if this is true.
+				// Refer to the map generator heuristics.
+				bool ug = m_emerge->isBlockUnderground(p);
+				block->setIsUnderground(ug);
+			}
+		}
+	}
+
+	/*
+		Now we have a big empty area.
+
+		Make a ManualMapVoxelManipulator that contains this and the
+		neighboring blocks
+	*/
+
+	data->vmanip = new MMVManip(this);
+	data->vmanip->initialEmerge(full_bpmin, full_bpmax);
+
+	// Data is ready now.
+	return true;
+}
+
+void ServerMap::finishBlockMake(BlockMakeData *data,
+	std::map<v3s16, MapBlock*> *changed_blocks)
+{
+	v3s16 bpmin = data->blockpos_min;
+	v3s16 bpmax = data->blockpos_max;
+
+	bool enable_mapgen_debug_info = m_emerge->enable_mapgen_debug_info;
+	EMERGE_DBG_OUT("finishBlockMake(): " << bpmin << " - " << bpmax);
+
+	/*
+		Blit generated stuff to map
+		NOTE: blitBackAll adds nearly everything to changed_blocks
+	*/
+	data->vmanip->blitBackAll(changed_blocks);
+
+	EMERGE_DBG_OUT("finishBlockMake: changed_blocks.size()="
+		<< changed_blocks->size());
+
+	/*
+		Copy transforming liquid information
+	*/
+	while (data->transforming_liquid.size()) {
+		m_transforming_liquid.push_back(data->transforming_liquid.front());
+		data->transforming_liquid.pop_front();
+	}
+
+	for (auto &changed_block : *changed_blocks) {
+		MapBlock *block = changed_block.second;
+		if (!block)
+			continue;
+		/*
+			Update is air cache of the MapBlocks
+		*/
+		block->expireIsAirCache();
+		/*
+			Set block as modified
+		*/
+		block->raiseModified(MOD_STATE_WRITE_NEEDED,
+			MOD_REASON_EXPIRE_IS_AIR);
+	}
+
+	/*
+		Set central blocks as generated
+	*/
+	for (s16 x = bpmin.X; x <= bpmax.X; x++)
+	for (s16 z = bpmin.Z; z <= bpmax.Z; z++)
+	for (s16 y = bpmin.Y; y <= bpmax.Y; y++) {
+		MapBlock *block = getBlockNoCreateNoEx(v3s16(x, y, z));
+		if (!block)
+			continue;
+
+		block->setGenerated(true);
+	}
+
+	/*
+		Save changed parts of map
+		NOTE: Will be saved later.
+	*/
+	//save(MOD_STATE_WRITE_AT_UNLOAD);
+	m_chunks_in_progress.erase(bpmin);
+}
+
+MapSector *ServerMap::createSector(v2s16 p2d)
+{
+	/*
+		Check if it exists already in memory
+	*/
+	MapSector *sector = getSectorNoGenerate(p2d);
+	if (sector)
+		return sector;
+
+	/*
+		Do not create over max mapgen limit
+	*/
+	if (blockpos_over_max_limit(v3s16(p2d.X, 0, p2d.Y)))
+		throw InvalidPositionException("createSector(): pos over max mapgen limit");
+
+	/*
+		Generate blank sector
+	*/
+	sector = new MapSector(this, p2d, m_gamedef);
+
+	/*
+		Insert to container
+	*/
+	m_sectors[p2d] = sector;
+
+	return sector;
+}
+
+MapBlock * ServerMap::createBlock(v3s16 p)
+{
+	v2s16 p2d(p.X, p.Z);
+	s16 block_y = p.Y;
+
+	/*
+		This will create or load a sector if not found in memory.
+	*/
+	MapSector *sector;
+	try {
+		sector = createSector(p2d);
+	} catch (InvalidPositionException &e) {
+		infostream<<"createBlock: createSector() failed"<<std::endl;
+		throw e;
+	}
+
+	/*
+		Try to get a block from the sector
+	*/
+
+	MapBlock *block = sector->getBlockNoCreateNoEx(block_y);
+	if (block)
+		return block;
+
+	// Create blank
+	try {
+		block = sector->createBlankBlock(block_y);
+	} catch (InvalidPositionException &e) {
+		infostream << "createBlock: createBlankBlock() failed" << std::endl;
+		throw e;
+	}
+
+	return block;
+}
+
+MapBlock * ServerMap::emergeBlock(v3s16 p, bool create_blank)
+{
+	{
+		MapBlock *block = getBlockNoCreateNoEx(p);
+		if (block)
+			return block;
+	}
+
+	{
+		MapBlock *block = loadBlock(p);
+		if(block)
+			return block;
+	}
+
+	if (create_blank) {
+		try {
+			MapSector *sector = createSector(v2s16(p.X, p.Z));
+			return sector->createBlankBlock(p.Y);
+		} catch (InvalidPositionException &e) {}
+	}
+
+	return NULL;
+}
+
+MapBlock *ServerMap::getBlockOrEmerge(v3s16 p3d, bool generate)
+{
+	MapBlock *block = getBlockNoCreateNoEx(p3d);
+	if (block == NULL)
+		m_emerge->enqueueBlockEmerge(PEER_ID_INEXISTENT, p3d, generate);
+
+	return block;
+}
+
+bool ServerMap::isBlockInQueue(v3s16 pos)
+{
+	return m_emerge && m_emerge->isBlockInQueue(pos);
+}
+
+void ServerMap::addNodeAndUpdate(v3s16 p, MapNode n,
+		std::map<v3s16, MapBlock*> &modified_blocks,
+		bool remove_metadata)
+{
+	Map::addNodeAndUpdate(p, n, modified_blocks, remove_metadata);
+
+	/*
+		Add neighboring liquid nodes and this node to transform queue.
+		(it's vital for the node itself to get updated last, if it was removed.)
+	 */
+
+	for (const v3s16 &dir : g_7dirs) {
+		v3s16 p2 = p + dir;
+
+		bool is_valid_position;
+		MapNode n2 = getNode(p2, &is_valid_position);
+		if(is_valid_position &&
+				(m_nodedef->get(n2).isLiquid() ||
+				n2.getContent() == CONTENT_AIR))
+			m_transforming_liquid.push_back(p2);
+	}
+}
+
+// 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;
+
+	MMVManip *vm = mg->vm;
+	if (!vm)
+		return;
+
+	if (!vm->m_area.contains(pos))
+		return;
+
+	s32 idx = vm->m_area.index(pos);
+	vm->m_data[idx] = getNode(pos);
+	vm->m_flags[idx] &= ~VOXELFLAG_NO_DATA;
+
+	vm->m_is_dirty = true;
+}
+
+void ServerMap::reportMetrics(u64 save_time_us, u32 saved_blocks, u32 all_blocks)
+{
+	m_loaded_blocks_gauge->set(all_blocks);
+	m_save_time_counter->increment(save_time_us);
+	m_save_count_counter->increment(saved_blocks);
+}
+
+void ServerMap::save(ModifiedState save_level)
+{
+	if (!m_map_saving_enabled) {
+		warningstream<<"Not saving map, saving disabled."<<std::endl;
+		return;
+	}
+
+	const auto start_time = porting::getTimeUs();
+
+	if(save_level == MOD_STATE_CLEAN)
+		infostream<<"ServerMap: Saving whole map, this can take time."
+				<<std::endl;
+
+	if (m_map_metadata_changed || save_level == MOD_STATE_CLEAN) {
+		if (settings_mgr.saveMapMeta())
+			m_map_metadata_changed = false;
+	}
+
+	// Profile modified reasons
+	Profiler modprofiler;
+
+	u32 block_count = 0;
+	u32 block_count_all = 0; // Number of blocks in memory
+
+	// Don't do anything with sqlite unless something is really saved
+	bool save_started = false;
+
+	for (auto &sector_it : m_sectors) {
+		MapSector *sector = sector_it.second;
+
+		MapBlockVect blocks;
+		sector->getBlocks(blocks);
+
+		for (MapBlock *block : blocks) {
+			block_count_all++;
+
+			if(block->getModified() >= (u32)save_level) {
+				// Lazy beginSave()
+				if(!save_started) {
+					beginSave();
+					save_started = true;
+				}
+
+				modprofiler.add(block->getModifiedReasonString(), 1);
+
+				saveBlock(block);
+				block_count++;
+			}
+		}
+	}
+
+	if(save_started)
+		endSave();
+
+	/*
+		Only print if something happened or saved whole map
+	*/
+	if(save_level == MOD_STATE_CLEAN
+			|| block_count != 0) {
+		infostream << "ServerMap: Written: "
+				<< block_count << " blocks"
+				<< ", " << block_count_all << " blocks in memory."
+				<< std::endl;
+		PrintInfo(infostream); // ServerMap/ClientMap:
+		infostream<<"Blocks modified by: "<<std::endl;
+		modprofiler.print(infostream);
+	}
+
+	const auto end_time = porting::getTimeUs();
+	reportMetrics(end_time - start_time, block_count, block_count_all);
+}
+
+void ServerMap::listAllLoadableBlocks(std::vector<v3s16> &dst)
+{
+	dbase->listAllLoadableBlocks(dst);
+	if (dbase_ro)
+		dbase_ro->listAllLoadableBlocks(dst);
+}
+
+void ServerMap::listAllLoadedBlocks(std::vector<v3s16> &dst)
+{
+	for (auto &sector_it : m_sectors) {
+		MapSector *sector = sector_it.second;
+
+		MapBlockVect blocks;
+		sector->getBlocks(blocks);
+
+		for (MapBlock *block : blocks) {
+			v3s16 p = block->getPos();
+			dst.push_back(p);
+		}
+	}
+}
+
+MapDatabase *ServerMap::createDatabase(
+	const std::string &name,
+	const std::string &savedir,
+	Settings &conf)
+{
+	if (name == "sqlite3")
+		return new MapDatabaseSQLite3(savedir);
+	if (name == "dummy")
+		return new Database_Dummy();
+	#if USE_LEVELDB
+	if (name == "leveldb")
+		return new Database_LevelDB(savedir);
+	#endif
+	#if USE_REDIS
+	if (name == "redis")
+		return new Database_Redis(conf);
+	#endif
+	#if USE_POSTGRESQL
+	if (name == "postgresql") {
+		std::string connect_string;
+		conf.getNoEx("pgsql_connection", connect_string);
+		return new MapDatabasePostgreSQL(connect_string);
+	}
+	#endif
+
+	throw BaseException(std::string("Database backend ") + name + " not supported.");
+}
+
+void ServerMap::beginSave()
+{
+	dbase->beginSave();
+}
+
+void ServerMap::endSave()
+{
+	dbase->endSave();
+}
+
+bool ServerMap::saveBlock(MapBlock *block)
+{
+	return saveBlock(block, dbase, m_map_compression_level);
+}
+
+bool ServerMap::saveBlock(MapBlock *block, MapDatabase *db, int compression_level)
+{
+	v3s16 p3d = block->getPos();
+
+	// Format used for writing
+	u8 version = SER_FMT_VER_HIGHEST_WRITE;
+
+	/*
+		[0] u8 serialization version
+		[1] data
+	*/
+	std::ostringstream o(std::ios_base::binary);
+	o.write((char*) &version, 1);
+	block->serialize(o, version, true, compression_level);
+
+	// FIXME: zero copy possible in c++20 or with custom rdbuf
+	bool ret = db->saveBlock(p3d, o.str());
+	if (ret) {
+		// We just wrote it to the disk so clear modified flag
+		block->resetModified();
+	}
+	return ret;
+}
+
+void ServerMap::loadBlock(std::string *blob, v3s16 p3d, MapSector *sector, bool save_after_load)
+{
+	try {
+		std::istringstream is(*blob, std::ios_base::binary);
+
+		u8 version = SER_FMT_VER_INVALID;
+		is.read((char*)&version, 1);
+
+		if(is.fail())
+			throw SerializationError("ServerMap::loadBlock(): Failed"
+					" to read MapBlock version");
+
+		MapBlock *block = nullptr;
+		std::unique_ptr<MapBlock> block_created_new;
+		block = sector->getBlockNoCreateNoEx(p3d.Y);
+		if (!block) {
+			block_created_new = sector->createBlankBlockNoInsert(p3d.Y);
+			block = block_created_new.get();
+		}
+
+		{
+		ScopeProfiler sp(g_profiler, "ServerMap: deSer block", SPT_AVG);
+		// Read basic data
+		block->deSerialize(is, version, true);
+		}
+
+		// If it's a new block, insert it to the map
+		if (block_created_new) {
+			sector->insertBlock(std::move(block_created_new));
+			ReflowScan scanner(this, m_emerge->ndef);
+			scanner.scan(block, &m_transforming_liquid);
+		}
+
+		/*
+			Save blocks loaded in old format in new format
+		*/
+
+		//if(version < SER_FMT_VER_HIGHEST_READ || save_after_load)
+		// Only save if asked to; no need to update version
+		if(save_after_load)
+			saveBlock(block);
+
+		// We just loaded it from, so it's up-to-date.
+		block->resetModified();
+	}
+	catch(SerializationError &e)
+	{
+		errorstream<<"Invalid block data in database"
+				<<" ("<<p3d.X<<","<<p3d.Y<<","<<p3d.Z<<")"
+				<<" (SerializationError): "<<e.what()<<std::endl;
+
+		// TODO: Block should be marked as invalid in memory so that it is
+		// not touched but the game can run
+
+		if(g_settings->getBool("ignore_world_load_errors")){
+			errorstream<<"Ignoring block load error. Duck and cover! "
+					<<"(ignore_world_load_errors)"<<std::endl;
+		} else {
+			throw SerializationError("Invalid block data in database");
+		}
+	}
+}
+
+MapBlock* ServerMap::loadBlock(v3s16 blockpos)
+{
+	ScopeProfiler sp(g_profiler, "ServerMap: load block", SPT_AVG);
+	bool created_new = (getBlockNoCreateNoEx(blockpos) == NULL);
+
+	v2s16 p2d(blockpos.X, blockpos.Z);
+
+	std::string ret;
+	dbase->loadBlock(blockpos, &ret);
+	if (!ret.empty()) {
+		loadBlock(&ret, blockpos, createSector(p2d), false);
+	} else if (dbase_ro) {
+		dbase_ro->loadBlock(blockpos, &ret);
+		if (!ret.empty()) {
+			loadBlock(&ret, blockpos, createSector(p2d), false);
+		}
+	} else {
+		return NULL;
+	}
+
+	MapBlock *block = getBlockNoCreateNoEx(blockpos);
+	if (created_new && (block != NULL)) {
+		std::map<v3s16, MapBlock*> modified_blocks;
+		// Fix lighting if necessary
+		voxalgo::update_block_border_lighting(this, block, modified_blocks);
+		if (!modified_blocks.empty()) {
+			//Modified lighting, send event
+			MapEditEvent event;
+			event.type = MEET_OTHER;
+			event.setModifiedBlocks(modified_blocks);
+			dispatchEvent(event);
+		}
+	}
+	return block;
+}
+
+bool ServerMap::deleteBlock(v3s16 blockpos)
+{
+	if (!dbase->deleteBlock(blockpos))
+		return false;
+
+	MapBlock *block = getBlockNoCreateNoEx(blockpos);
+	if (block) {
+		v2s16 p2d(blockpos.X, blockpos.Z);
+		MapSector *sector = getSectorNoGenerate(p2d);
+		if (!sector)
+			return false;
+		// It may not be safe to delete the block from memory at the moment
+		// (pointers to it could still be in use)
+		m_detached_blocks.push_back(sector->detachBlock(block));
+	}
+
+	return true;
+}
+
+void ServerMap::deleteDetachedBlocks()
+{
+	for (const auto &block : m_detached_blocks) {
+		assert(block->isOrphan());
+		(void)block; // silence unused-variable warning in release builds
+	}
+
+	m_detached_blocks.clear();
+}
+
+void ServerMap::step()
+{
+	// Delete from memory blocks removed by deleteBlocks() only when pointers
+	// to them are (probably) no longer in use
+	deleteDetachedBlocks();
+}
+
+void ServerMap::PrintInfo(std::ostream &out)
+{
+	out<<"ServerMap: ";
+}
+
+bool ServerMap::repairBlockLight(v3s16 blockpos,
+	std::map<v3s16, MapBlock *> *modified_blocks)
+{
+	MapBlock *block = emergeBlock(blockpos, false);
+	if (!block || !block->isGenerated())
+		return false;
+	voxalgo::repair_block_light(this, block, modified_blocks);
+	return true;
+}
+
+/*
+	Liquids
+*/
+
+#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)
+	{ }
+};
+
+static s8 get_max_liquid_level(NodeNeighbor nb, s8 current_max_node_level)
+{
+	s8 max_node_level = current_max_node_level;
+	u8 nb_liquid_level = (nb.n.param2 & LIQUID_LEVEL_MASK);
+	switch (nb.t) {
+		case NEIGHBOR_UPPER:
+			if (nb_liquid_level + WATER_DROP_BOOST > current_max_node_level) {
+				max_node_level = LIQUID_LEVEL_MAX;
+				if (nb_liquid_level + WATER_DROP_BOOST < LIQUID_LEVEL_MAX)
+					max_node_level = nb_liquid_level + WATER_DROP_BOOST;
+			} else if (nb_liquid_level > current_max_node_level) {
+				max_node_level = nb_liquid_level;
+			}
+			break;
+		case NEIGHBOR_LOWER:
+			break;
+		case NEIGHBOR_SAME_LEVEL:
+			if ((nb.n.param2 & LIQUID_FLOW_DOWN_MASK) != LIQUID_FLOW_DOWN_MASK &&
+					nb_liquid_level > 0 && nb_liquid_level - 1 > max_node_level)
+				max_node_level = nb_liquid_level - 1;
+			break;
+	}
+	return max_node_level;
+}
+
+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::vector<v3s16> must_reflow;
+
+	std::vector<std::pair<v3s16, MapNode> > changed_nodes;
+
+	std::vector<v3s16> check_for_falling;
+
+	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;
+			case LiquidType_END:
+				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;
+		bool floating_node_above = 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);
+			if (nt == NEIGHBOR_UPPER && cfnb.floats)
+				floating_node_above = true;
+			switch (cfnb.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
+							if (nb.t == NEIGHBOR_LOWER)
+								flowing_down = true;
+							else
+								ignored_sources = true;
+						}
+					}
+					break;
+				case LIQUID_SOURCE:
+					// if this node is not (yet) of a liquid type, choose the first liquid type we encounter
+					if (liquid_kind == CONTENT_AIR)
+						liquid_kind = cfnb.liquid_alternative_flowing_id;
+					if (cfnb.liquid_alternative_flowing_id != liquid_kind) {
+						neutrals[num_neutrals++] = nb;
+					} else {
+						// Do not count bottom source, it will screw things up
+						if(nt != NEIGHBOR_LOWER)
+							sources[num_sources++] = nb;
+					}
+					break;
+				case LIQUID_FLOWING:
+					if (nb.t != NEIGHBOR_SAME_LEVEL ||
+						(nb.n.param2 & LIQUID_FLOW_DOWN_MASK) != LIQUID_FLOW_DOWN_MASK) {
+						// if this node is not (yet) of a liquid type, choose the first liquid type we encounter
+						// but exclude falling liquids on the same level, they cannot flow here anyway
+
+						// used to determine if the neighbor can even flow into this node
+						s8 max_level_from_neighbor = get_max_liquid_level(nb, -1);
+						u8 range = m_nodedef->get(cfnb.liquid_alternative_flowing_id).liquid_range;
+
+						if (liquid_kind == CONTENT_AIR &&
+								max_level_from_neighbor >= (LIQUID_LEVEL_MAX + 1 - range))
+							liquid_kind = cfnb.liquid_alternative_flowing_id;
+					}
+					if (cfnb.liquid_alternative_flowing_id != liquid_kind) {
+						neutrals[num_neutrals++] = nb;
+					} else {
+						flows[num_flows++] = nb;
+						if (nb.t == NEIGHBOR_LOWER)
+							flowing_down = true;
+					}
+					break;
+				case LiquidType_END:
+					break;
+			}
+		}
+
+		/*
+			decide on the type (and possibly level) of the current node
+		 */
+		content_t new_node_content;
+		s8 new_node_level = -1;
+		s8 max_node_level = -1;
+
+		u8 range = m_nodedef->get(liquid_kind).liquid_range;
+		if (range > LIQUID_LEVEL_MAX + 1)
+			range = LIQUID_LEVEL_MAX + 1;
+
+		if ((num_sources >= 2 && m_nodedef->get(liquid_kind).liquid_renewable) || liquid_type == LIQUID_SOURCE) {
+			// liquid_kind will be set to either the flowing alternative of the node (if it's a liquid)
+			// or the flowing alternative of the first of the surrounding sources (if it's air), so
+			// it's perfectly safe to use liquid_kind here to determine the new node content.
+			new_node_content = m_nodedef->get(liquid_kind).liquid_alternative_source_id;
+		} else if (num_sources >= 1 && sources[0].t != NEIGHBOR_LOWER) {
+			// liquid_kind is set properly, see above
+			max_node_level = new_node_level = LIQUID_LEVEL_MAX;
+			if (new_node_level >= (LIQUID_LEVEL_MAX + 1 - range))
+				new_node_content = liquid_kind;
+			else
+				new_node_content = floodable_node;
+		} else if (ignored_sources && liquid_level >= 0) {
+			// Maybe there are neighboring sources that aren't loaded yet
+			// so prevent flowing away.
+			new_node_level = liquid_level;
+			new_node_content = liquid_kind;
+		} else {
+			// no surrounding sources, so get the maximum level that can flow into this node
+			for (u16 i = 0; i < num_flows; i++) {
+				max_node_level = get_max_liquid_level(flows[i], max_node_level);
+			}
+
+			u8 viscosity = m_nodedef->get(liquid_kind).liquid_viscosity;
+			if (viscosity > 1 && max_node_level != liquid_level) {
+				// amount to gain, limited by viscosity
+				// must be at least 1 in absolute value
+				s8 level_inc = max_node_level - liquid_level;
+				if (level_inc < -viscosity || level_inc > viscosity)
+					new_node_level = liquid_level + level_inc/viscosity;
+				else if (level_inc < 0)
+					new_node_level = liquid_level - 1;
+				else if (level_inc > 0)
+					new_node_level = liquid_level + 1;
+				if (new_node_level != max_node_level)
+					must_reflow.push_back(p0);
+			} else {
+				new_node_level = max_node_level;
+			}
+
+			if (max_node_level >= (LIQUID_LEVEL_MAX + 1 - range))
+				new_node_content = liquid_kind;
+			else
+				new_node_content = floodable_node;
+
+		}
+
+		/*
+			check if anything has changed. if not, just continue with the next node.
+		 */
+		if (new_node_content == n0.getContent() &&
+				(m_nodedef->get(n0.getContent()).liquid_type != LIQUID_FLOWING ||
+				((n0.param2 & LIQUID_LEVEL_MASK) == (u8)new_node_level &&
+				((n0.param2 & LIQUID_FLOW_DOWN_MASK) == LIQUID_FLOW_DOWN_MASK)
+				== flowing_down)))
+			continue;
+
+		/*
+			check if there is a floating node above that needs to be updated.
+		 */
+		if (floating_node_above && new_node_content == CONTENT_AIR)
+			check_for_falling.push_back(p0);
+
+		/*
+			update the current node
+		 */
+		MapNode n00 = n0;
+		//bool flow_down_enabled = (flowing_down && ((n0.param2 & LIQUID_FLOW_DOWN_MASK) != LIQUID_FLOW_DOWN_MASK));
+		if (m_nodedef->get(new_node_content).liquid_type == LIQUID_FLOWING) {
+			// set level to last 3 bits, flowing down bit to 4th bit
+			n0.param2 = (flowing_down ? LIQUID_FLOW_DOWN_MASK : 0x00) | (new_node_level & LIQUID_LEVEL_MASK);
+		} else {
+			// set the liquid level and flow bits to 0
+			n0.param2 &= ~(LIQUID_LEVEL_MASK | LIQUID_FLOW_DOWN_MASK);
+		}
+
+		// change the node.
+		n0.setContent(new_node_content);
+
+		// on_flood() the node
+		if (floodable_node != CONTENT_AIR) {
+			if (env->getScriptIface()->node_on_flood(p0, n00, n0))
+				continue;
+		}
+
+		// Ignore light (because calling voxalgo::update_lighting_nodes)
+		ContentLightingFlags f0 = m_nodedef->getLightingFlags(n0);
+		n0.setLight(LIGHTBANK_DAY, 0, f0);
+		n0.setLight(LIGHTBANK_NIGHT, 0, f0);
+
+		// Find out whether there is a suspect for this action
+		std::string suspect;
+		if (m_gamedef->rollback())
+			suspect = m_gamedef->rollback()->getSuspect(p0, 83, 1);
+
+		if (m_gamedef->rollback() && !suspect.empty()) {
+			// Blame suspect
+			RollbackScopeActor rollback_scope(m_gamedef->rollback(), suspect, true);
+			// Get old node for rollback
+			RollbackNode rollback_oldnode(this, p0, m_gamedef);
+			// Set node
+			setNode(p0, n0);
+			// Report
+			RollbackNode rollback_newnode(this, p0, m_gamedef);
+			RollbackAction action;
+			action.setSetNode(p0, rollback_oldnode, rollback_newnode);
+			m_gamedef->rollback()->reportAction(action);
+		} else {
+			// Set node
+			setNode(p0, n0);
+		}
+
+		v3s16 blockpos = getNodeBlockPos(p0);
+		MapBlock *block = getBlockNoCreateNoEx(blockpos);
+		if (block != NULL) {
+			modified_blocks[blockpos] =  block;
+			changed_nodes.emplace_back(p0, n00);
+		}
+
+		/*
+			enqueue neighbors for update if necessary
+		 */
+		switch (m_nodedef->get(n0.getContent()).liquid_type) {
+			case LIQUID_SOURCE:
+			case LIQUID_FLOWING:
+				// make sure source flows into all neighboring nodes
+				for (u16 i = 0; i < num_flows; i++)
+					if (flows[i].t != NEIGHBOR_UPPER)
+						m_transforming_liquid.push_back(flows[i].p);
+				for (u16 i = 0; i < num_airs; i++)
+					if (airs[i].t != NEIGHBOR_UPPER)
+						m_transforming_liquid.push_back(airs[i].p);
+				break;
+			case LIQUID_NONE:
+				// this flow has turned to air; neighboring flows might need to do the same
+				for (u16 i = 0; i < num_flows; i++)
+					m_transforming_liquid.push_back(flows[i].p);
+				break;
+			case LiquidType_END:
+				break;
+		}
+	}
+	//infostream<<"Map::transformLiquids(): loopcount="<<loopcount<<std::endl;
+
+	for (const auto &iter : must_reflow)
+		m_transforming_liquid.push_back(iter);
+
+	voxalgo::update_lighting_nodes(this, changed_nodes, modified_blocks);
+
+	for (const v3s16 &p : check_for_falling) {
+		env->getScriptIface()->check_for_falling(p);
+	}
+
+	env->getScriptIface()->on_liquid_transformed(changed_nodes);
+
+	/* ----------------------------------------------------------------------
+	 * Manage the queue so that it does not grow indefinitely
+	 */
+	u16 time_until_purge = g_settings->getU16("liquid_queue_purge_time");
+
+	if (time_until_purge == 0)
+		return; // Feature disabled
+
+	time_until_purge *= 1000;	// seconds -> milliseconds
+
+	u64 curr_time = porting::getTimeMs();
+	u32 prev_unprocessed = m_unprocessed_count;
+	m_unprocessed_count = m_transforming_liquid.size();
+
+	// if unprocessed block count is decreasing or stable
+	if (m_unprocessed_count <= prev_unprocessed) {
+		m_queue_size_timer_started = false;
+	} else {
+		if (!m_queue_size_timer_started)
+			m_inc_trending_up_start_time = curr_time;
+		m_queue_size_timer_started = true;
+	}
+
+	// Account for curr_time overflowing
+	if (m_queue_size_timer_started && m_inc_trending_up_start_time > curr_time)
+		m_queue_size_timer_started = false;
+
+	/* If the queue has been growing for more than liquid_queue_purge_time seconds
+	 * and the number of unprocessed blocks is still > liquid_loop_max then we
+	 * cannot keep up; dump the oldest blocks from the queue so that the queue
+	 * has liquid_loop_max items in it
+	 */
+	if (m_queue_size_timer_started
+			&& curr_time - m_inc_trending_up_start_time > time_until_purge
+			&& m_unprocessed_count > liquid_loop_max) {
+
+		size_t dump_qty = m_unprocessed_count - liquid_loop_max;
+
+		infostream << "transformLiquids(): DUMPING " << dump_qty
+		           << " blocks from the queue" << std::endl;
+
+		while (dump_qty--)
+			m_transforming_liquid.pop_front();
+
+		m_queue_size_timer_started = false; // optimistically assume we can keep up now
+		m_unprocessed_count = m_transforming_liquid.size();
+	}
+}

+ 195 - 0
src/servermap.h

@@ -0,0 +1,195 @@
+/*
+Minetest
+Copyright (C) 2010-2024 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.
+*/
+
+#pragma once
+
+#include <vector>
+#include <memory>
+
+#include "map.h"
+#include "util/container.h"
+#include "util/metricsbackend.h"
+#include "map_settings_manager.h"
+
+class Settings;
+class MapDatabase;
+class IRollbackManager;
+class EmergeManager;
+class ServerEnvironment;
+struct BlockMakeData;
+
+class MetricsBackend;
+
+/*
+	ServerMap
+
+	This is the only map class that is able to generate map.
+*/
+
+class ServerMap : public Map
+{
+public:
+	/*
+		savedir: directory to which map data should be saved
+	*/
+	ServerMap(const std::string &savedir, IGameDef *gamedef, EmergeManager *emerge, MetricsBackend *mb);
+	~ServerMap();
+
+	/*
+		Get a sector from somewhere.
+		- Check memory
+		- Check disk (doesn't load blocks)
+		- Create blank one
+	*/
+	MapSector *createSector(v2s16 p);
+
+	/*
+		Blocks are generated by using these and makeBlock().
+	*/
+	bool blockpos_over_mapgen_limit(v3s16 p);
+	bool initBlockMake(v3s16 blockpos, BlockMakeData *data);
+	void finishBlockMake(BlockMakeData *data,
+		std::map<v3s16, MapBlock*> *changed_blocks);
+
+	/*
+		Get a block from somewhere.
+		- Memory
+		- Create blank
+	*/
+	MapBlock *createBlock(v3s16 p);
+
+	/*
+		Forcefully get a block from somewhere.
+		- Memory
+		- Load from disk
+		- Create blank filled with CONTENT_IGNORE
+
+	*/
+	MapBlock *emergeBlock(v3s16 p, bool create_blank=true) override;
+
+	/*
+		Try to get a block.
+		If it does not exist in memory, add it to the emerge queue.
+		- Memory
+		- Emerge Queue (deferred disk or generate)
+	*/
+	MapBlock *getBlockOrEmerge(v3s16 p3d, bool generate);
+
+	bool isBlockInQueue(v3s16 pos);
+
+	void addNodeAndUpdate(v3s16 p, MapNode n,
+			std::map<v3s16, MapBlock*> &modified_blocks,
+			bool remove_metadata) override;
+
+	/*
+		Database functions
+	*/
+	static MapDatabase *createDatabase(const std::string &name, const std::string &savedir, Settings &conf);
+
+	// Call these before and after saving of blocks
+	void beginSave() override;
+	void endSave() override;
+
+	void save(ModifiedState save_level) override;
+	void listAllLoadableBlocks(std::vector<v3s16> &dst);
+	void listAllLoadedBlocks(std::vector<v3s16> &dst);
+
+	MapgenParams *getMapgenParams();
+
+	bool saveBlock(MapBlock *block) override;
+	static bool saveBlock(MapBlock *block, MapDatabase *db, int compression_level = -1);
+	MapBlock* loadBlock(v3s16 p);
+	// Database version
+	void loadBlock(std::string *blob, v3s16 p3d, MapSector *sector, bool save_after_load=false);
+
+	// Blocks are removed from the map but not deleted from memory until
+	// deleteDetachedBlocks() is called, since pointers to them may still exist
+	// when deleteBlock() is called.
+	bool deleteBlock(v3s16 blockpos) override;
+
+	void deleteDetachedBlocks();
+
+	void step();
+
+	void updateVManip(v3s16 pos);
+
+	// For debug printing
+	void PrintInfo(std::ostream &out) override;
+
+	bool isSavingEnabled(){ return m_map_saving_enabled; }
+
+	u64 getSeed();
+
+	/*!
+	 * Fixes lighting in one map block.
+	 * May modify other blocks as well, as light can spread
+	 * out of the specified block.
+	 * Returns false if the block is not generated (so nothing
+	 * changed), true otherwise.
+	 */
+	bool repairBlockLight(v3s16 blockpos,
+		std::map<v3s16, MapBlock *> *modified_blocks);
+
+	void transformLiquids(std::map<v3s16, MapBlock*> & modified_blocks,
+			ServerEnvironment *env);
+
+	void transforming_liquid_add(v3s16 p);
+
+	MapSettingsManager settings_mgr;
+
+protected:
+
+	void reportMetrics(u64 save_time_us, u32 saved_blocks, u32 all_blocks) override;
+
+private:
+	friend class ModApiMapgen; // for m_transforming_liquid
+
+	// Emerge manager
+	EmergeManager *m_emerge;
+
+	std::string m_savedir;
+	bool m_map_saving_enabled;
+
+	int m_map_compression_level;
+
+	std::set<v3s16> m_chunks_in_progress;
+
+	// used by deleteBlock() and deleteDetachedBlocks()
+	std::vector<std::unique_ptr<MapBlock>> m_detached_blocks;
+
+	// Queued transforming water nodes
+	UniqueQueue<v3s16> m_transforming_liquid;
+	f32 m_transforming_liquid_loop_count_multiplier = 1.0f;
+	u32 m_unprocessed_count = 0;
+	u64 m_inc_trending_up_start_time = 0; // milliseconds
+	bool m_queue_size_timer_started = false;
+
+	/*
+		Metadata is re-written on disk only if this is true.
+		This is reset to false when written on disk.
+	*/
+	bool m_map_metadata_changed = true;
+	MapDatabase *dbase = nullptr;
+	MapDatabase *dbase_ro = nullptr;
+
+	// Map metrics
+	MetricGaugePtr m_loaded_blocks_gauge;
+	MetricCounterPtr m_save_time_counter;
+	MetricCounterPtr m_save_count_counter;
+};