Browse Source

Lua on each mapgen thread (#13092)

sfan5 2 months ago
parent
commit
3cac17d23e

+ 1 - 0
.luacheckrc

@@ -17,6 +17,7 @@ read_globals = {
 	"VoxelArea",
 	"profiler",
 	"Settings",
+	"PerlinNoise", "PerlinNoiseMap",
 
 	string = {fields = {"split", "trim"}},
 	table  = {fields = {"copy", "getn", "indexof", "insert_all"}},

+ 61 - 0
builtin/emerge/env.lua

@@ -0,0 +1,61 @@
+-- Reimplementations of some environment function on vmanips, since this is
+-- what the emerge environment operates on
+
+-- core.vmanip = <VoxelManip> -- set by C++
+
+function core.set_node(pos, node)
+	return core.vmanip:set_node_at(pos, node)
+end
+
+function core.bulk_set_node(pos_list, node)
+	local vm = core.vmanip
+	local set_node_at = vm.set_node_at
+	for _, pos in ipairs(pos_list) do
+		if not set_node_at(vm, pos, node) then
+			return false
+		end
+	end
+	return true
+end
+
+core.add_node = core.set_node
+
+-- we don't deal with metadata currently
+core.swap_node = core.set_node
+
+function core.remove_node(pos)
+	return core.vmanip:set_node_at(pos, {name="air"})
+end
+
+function core.get_node(pos)
+	return core.vmanip:get_node_at(pos)
+end
+
+function core.get_node_or_nil(pos)
+	local node = core.vmanip:get_node_at(pos)
+	return node.name ~= "ignore" and node
+end
+
+function core.get_perlin(seed, octaves, persist, spread)
+	local params
+	if type(seed) == "table" then
+		params = table.copy(seed)
+	else
+		assert(type(seed) == "number")
+		params = {
+			seed = seed,
+			octaves = octaves,
+			persist = persist,
+			spread = {x=spread, y=spread, z=spread},
+		}
+	end
+	params.seed = core.get_seed(params.seed) -- add mapgen seed
+	return PerlinNoise(params)
+end
+
+
+function core.get_perlin_map(params, size)
+	local params2 = table.copy(params)
+	params2.seed = core.get_seed(params.seed) -- add mapgen seed
+	return PerlinNoiseMap(params2, size)
+end

+ 21 - 0
builtin/emerge/init.lua

@@ -0,0 +1,21 @@
+local gamepath = core.get_builtin_path() .. "game" .. DIR_DELIM
+local commonpath = core.get_builtin_path() .. "common" .. DIR_DELIM
+local epath = core.get_builtin_path() .. "emerge" .. DIR_DELIM
+
+local builtin_shared = {}
+
+-- Import parts shared with "game" environment
+dofile(gamepath .. "constants.lua")
+assert(loadfile(commonpath .. "item_s.lua"))(builtin_shared)
+dofile(gamepath .. "misc_s.lua")
+dofile(gamepath .. "features.lua")
+dofile(gamepath .. "voxelarea.lua")
+
+-- Now for our own stuff
+assert(loadfile(commonpath .. "register.lua"))(builtin_shared)
+assert(loadfile(epath .. "register.lua"))(builtin_shared)
+dofile(epath .. "env.lua")
+
+builtin_shared.cache_content_ids()
+
+core.log("info", "Initialized emerge Lua environment")

+ 54 - 0
builtin/emerge/register.lua

@@ -0,0 +1,54 @@
+local builtin_shared = ...
+
+-- Copy all the registration tables over
+do
+	local all = assert(core.transferred_globals)
+	core.transferred_globals = nil
+
+	all.registered_nodes = {}
+	all.registered_craftitems = {}
+	all.registered_tools = {}
+	for k, v in pairs(all.registered_items) do
+		-- Disable further modification
+		setmetatable(v, {__newindex = {}})
+		-- Reassemble the other tables
+		if v.type == "node" then
+			getmetatable(v).__index = all.nodedef_default
+			all.registered_nodes[k] = v
+		elseif v.type == "craft" then
+			getmetatable(v).__index = all.craftitemdef_default
+			all.registered_craftitems[k] = v
+		elseif v.type == "tool" then
+			getmetatable(v).__index = all.tooldef_default
+			all.registered_tools[k] = v
+		else
+			getmetatable(v).__index = all.noneitemdef_default
+		end
+	end
+
+	for k, v in pairs(all) do
+		core[k] = v
+	end
+end
+
+-- For tables that are indexed by item name:
+-- If table[X] does not exist, default to table[core.registered_aliases[X]]
+local alias_metatable = {
+	__index = function(t, name)
+		return rawget(t, core.registered_aliases[name])
+	end
+}
+setmetatable(core.registered_items, alias_metatable)
+setmetatable(core.registered_nodes, alias_metatable)
+setmetatable(core.registered_craftitems, alias_metatable)
+setmetatable(core.registered_tools, alias_metatable)
+
+--
+-- Callbacks
+--
+
+local make_registration = builtin_shared.make_registration
+
+core.registered_on_mods_loaded, core.register_on_mods_loaded = make_registration()
+core.registered_on_generateds, core.register_on_generated = make_registration()
+core.registered_on_shutdown, core.register_on_shutdown = make_registration()

+ 5 - 2
builtin/game/misc.lua

@@ -237,8 +237,8 @@ end
 core.dynamic_media_callbacks = {}
 
 
--- Transfer of certain globals into async environment
--- see builtin/async/game.lua for the other side
+-- Transfer of certain globals into seconday Lua environments
+-- see builtin/async/game.lua or builtin/emerge/register.lua for the unpacking
 
 local function copy_filtering(t, seen)
 	if type(t) == "userdata" or type(t) == "function" then
@@ -261,6 +261,9 @@ function core.get_globals_to_transfer()
 	local all = {
 		registered_items = copy_filtering(core.registered_items),
 		registered_aliases = core.registered_aliases,
+		registered_biomes = core.registered_biomes,
+		registered_ores = core.registered_ores,
+		registered_decorations = core.registered_decorations,
 
 		nodedef_default = copy_filtering(core.nodedef_default),
 		craftitemdef_default = copy_filtering(core.craftitemdef_default),

+ 4 - 4
builtin/init.lua

@@ -31,8 +31,6 @@ minetest = core
 
 -- Load other files
 local scriptdir = core.get_builtin_path()
-local gamepath = scriptdir .. "game" .. DIR_DELIM
-local clientpath = scriptdir .. "client" .. DIR_DELIM
 local commonpath = scriptdir .. "common" .. DIR_DELIM
 local asyncpath = scriptdir .. "async" .. DIR_DELIM
 
@@ -42,7 +40,7 @@ dofile(commonpath .. "serialize.lua")
 dofile(commonpath .. "misc_helpers.lua")
 
 if INIT == "game" then
-	dofile(gamepath .. "init.lua")
+	dofile(scriptdir .. "game" .. DIR_DELIM .. "init.lua")
 	assert(not core.get_http_api)
 elseif INIT == "mainmenu" then
 	local mm_script = core.settings:get("main_menu_script")
@@ -67,7 +65,9 @@ elseif INIT == "async"  then
 elseif INIT == "async_game" then
 	dofile(asyncpath .. "game.lua")
 elseif INIT == "client" then
-	dofile(clientpath .. "init.lua")
+	dofile(scriptdir .. "client" .. DIR_DELIM .. "init.lua")
+elseif INIT == "emerge" then
+	dofile(scriptdir .. "emerge" .. DIR_DELIM .. "init.lua")
 else
 	error(("Unrecognized builtin initialization type %s!"):format(tostring(INIT)))
 end

+ 109 - 25
doc/lua_api.md

@@ -4679,6 +4679,7 @@ differences:
   into it; it's not necessary to call `VoxelManip:read_from_map()`.
   Note that the region of map it has loaded is NOT THE SAME as the `minp`, `maxp`
   parameters of `on_generated()`. Refer to `minetest.get_mapgen_object` docs.
+  Once you're done you still need to call `VoxelManip:write_to_map()`
 
 * The `on_generated()` callbacks of some mods may place individual nodes in the
   generated area using non-VoxelManip map modification methods. Because the
@@ -4875,10 +4876,10 @@ Mapgen objects
 ==============
 
 A mapgen object is a construct used in map generation. Mapgen objects can be
-used by an `on_generate` callback to speed up operations by avoiding
+used by an `on_generated` callback to speed up operations by avoiding
 unnecessary recalculations, these can be retrieved using the
 `minetest.get_mapgen_object()` function. If the requested Mapgen object is
-unavailable, or `get_mapgen_object()` was called outside of an `on_generate()`
+unavailable, or `get_mapgen_object()` was called outside of an `on_generated`
 callback, `nil` is returned.
 
 The following Mapgen objects are currently available:
@@ -4910,12 +4911,14 @@ generated chunk by the current mapgen.
 
 ### `gennotify`
 
-Returns a table mapping requested generation notification types to arrays of
-positions at which the corresponding generated structures are located within
-the current chunk. To enable the capture of positions of interest to be recorded
-call `minetest.set_gen_notify()` first.
+Returns a table. You need to announce your interest in a specific
+field by calling `minetest.set_gen_notify()` *before* map generation happens.
 
-Possible fields of the returned table are:
+* key = string: generation notification type
+* value = list of positions (usually)
+   * Exceptions are denoted in the listing below.
+
+Available generation notification types:
 
 * `dungeon`: bottom center position of dungeon rooms
 * `temple`: as above but for desert temples (mgv6 only)
@@ -4923,7 +4926,12 @@ Possible fields of the returned table are:
 * `cave_end`
 * `large_cave_begin`
 * `large_cave_end`
-* `decoration#id` (see below)
+* `custom`: data originating from [Mapgen environment] (Lua API)
+   * This is a table.
+   * key = user-defined ID (string)
+   * value = arbitrary Lua value
+* `decoration#id`: decorations
+  * (see below)
 
 Decorations have a key in the format of `"decoration#id"`, where `id` is the
 numeric unique decoration ID as returned by `minetest.get_decoration_id()`.
@@ -5587,8 +5595,10 @@ Call these functions only at load time!
 * `minetest.register_on_punchnode(function(pos, node, puncher, pointed_thing))`
     * Called when a node is punched
 * `minetest.register_on_generated(function(minp, maxp, blockseed))`
-    * Called after generating a piece of world. Modifying nodes inside the area
-      is a bit faster than usual.
+    * Called after generating a piece of world between `minp` and `maxp`.
+    * **Avoid using this** whenever possible. As with other callbacks this blocks
+      the main thread and introduces noticable latency.
+      Consider [Mapgen environment] for an alternative.
 * `minetest.register_on_newplayer(function(ObjectRef))`
     * Called when a new player enters the world for the first time
 * `minetest.register_on_punchplayer(function(player, hitter, time_from_last_punch, tool_capabilities, dir, damage))`
@@ -6004,20 +6014,18 @@ Environment access
 * `minetest.get_voxel_manip([pos1, pos2])`
     * Return voxel manipulator object.
     * Loads the manipulator from the map if positions are passed.
-* `minetest.set_gen_notify(flags, {deco_ids})`
+* `minetest.set_gen_notify(flags, [deco_ids], [custom_ids])`
     * Set the types of on-generate notifications that should be collected.
-    * `flags` is a flag field with the available flags:
-        * dungeon
-        * temple
-        * cave_begin
-        * cave_end
-        * large_cave_begin
-        * large_cave_end
-        * decoration
-    * The second parameter is a list of IDs of decorations which notification
+    * `flags`: flag field, see [`gennotify`] for available generation notification types.
+    * The following parameters are optional:
+    * `deco_ids` is a list of IDs of decorations which notification
       is requested for.
+    * `custom_ids` is a list of user-defined IDs (strings) which are
+      requested. By convention these should be the mod name with an optional
+      colon and specifier added, e.g. `"default"` or `"default:dungeon_loot"`
 * `minetest.get_gen_notify()`
-    * Returns a flagstring and a table with the `deco_id`s.
+    * Returns a flagstring, a table with the `deco_id`s and a table with
+      user-defined IDs.
 * `minetest.get_decoration_id(decoration_name)`
     * Returns the decoration ID number for the provided decoration name string,
       or `nil` on failure.
@@ -6573,6 +6581,86 @@ Variables:
     * with all functions and userdata values replaced by `true`, calling any
       callbacks here is obviously not possible
 
+Mapgen environment
+------------------
+
+The engine runs the map generator on separate threads, each of these also has
+a Lua environment. Its primary purpose is to allow mods to operate on newly
+generated parts of the map to e.g. generate custom structures.
+Internally it is referred to as "emerge environment".
+
+Refer to [Async environment] for the usual disclaimer on what environment isolation entails.
+
+The map generator threads, which also contain the above mentioned Lua environment,
+are initialized after all mods have been loaded by the server. After that the
+registered scripts (not all mods!) - see below - are run during initialization of
+the mapgen environment. After that only callbacks happen. The mapgen env
+does not have a global step or timer.
+
+* `minetest.register_mapgen_script(path)`:
+    * Register a path to a Lua file to be imported when a mapgen environment
+      is initialized. Run in order of registration.
+
+### List of APIs exclusive to the mapgen env
+
+* `minetest.register_on_generated(function(vmanip, minp, maxp, blockseed))`
+    * Called after the engine mapgen finishes a chunk but before it is written to
+      the map.
+    * Chunk data resides in `vmanip`. Other parts of the map are not accessible.
+      The area of the chunk if comprised of `minp` and `maxp`, note that is smaller
+      than the emerged area of the VoxelManip.
+      Note: calling `read_from_map()` or `write_to_map()` on the VoxelManipulator object
+      is not necessary and is disallowed.
+    * `blockseed`: 64-bit seed number used for this chunk
+* `minetest.save_gen_notify(id, data)`
+    * Saves data for retrieval using the gennotify mechanism (see [Mapgen objects]).
+    * Data is bound to the chunk that is currently being processed, so this function
+      only makes sense inside the `on_generated` callback.
+    * `id`: user-defined ID (a string)
+      By convention these should be the mod name with an optional
+      colon and specifier added, e.g. `"default"` or `"default:dungeon_loot"`
+    * `data`: any Lua object (will be serialized, no userdata allowed)
+    * returns `true` if the data was remembered. That is if `minetest.set_gen_notify`
+      was called with the same user-defined ID before.
+
+### List of APIs available in the mapgen env
+
+Classes:
+* `AreaStore`
+* `ItemStack`
+* `PerlinNoise`
+* `PerlinNoiseMap`
+* `PseudoRandom`
+* `PcgRandom`
+* `SecureRandom`
+* `VoxelArea`
+* `VoxelManip`
+    * only given by callbacks; cannot access rest of map
+* `Settings`
+
+Functions:
+* Standalone helpers such as logging, filesystem, encoding,
+  hashing or compression APIs
+* `minetest.request_insecure_environment` (same restrictions apply)
+* `minetest.get_biome_id`, `get_biome_name`, `get_heat`, `get_humidity`,
+  `get_biome_data`, `get_mapgen_object`, `get_mapgen_params`, `get_mapgen_edges`,
+  `get_mapgen_setting`, `get_noiseparams`, `get_decoration_id` and more
+* `minetest.get_node`, `set_node`, `find_node_near`, `find_nodes_in_area`,
+  `spawn_tree` and similar
+    * these only operate on the current chunk (if inside a callback)
+
+Variables:
+* `minetest.settings`
+* `minetest.registered_items`, `registered_nodes`, `registered_tools`,
+  `registered_craftitems` and `registered_aliases`
+    * with all functions and userdata values replaced by `true`, calling any
+      callbacks here is obviously not possible
+* `minetest.registered_biomes`, `registered_ores`, `registered_decorations`
+
+Note that node metadata does not exist in the mapgen env, we suggest deferring
+setting any metadata you need to the `on_generated` callback in the regular env.
+You can use the gennotify mechanism to transfer this information.
+
 Server
 ------
 
@@ -7081,10 +7169,6 @@ Global tables
     * Map of registered decoration definitions, indexed by the `name` field.
     * If `name` is nil, the key is the object handle returned by
       `minetest.register_decoration`.
-* `minetest.registered_schematics`
-    * Map of registered schematic definitions, indexed by the `name` field.
-    * If `name` is nil, the key is the object handle returned by
-      `minetest.register_schematic`.
 * `minetest.registered_chatcommands`
     * Map of registered chat command definitions, indexed by name
 * `minetest.registered_privileges`

+ 32 - 0
games/devtest/mods/unittests/inside_mapgen_env.lua

@@ -0,0 +1,32 @@
+core.log("info", "Hello World")
+
+local function do_tests()
+	assert(core == minetest)
+	-- stuff that should not be here
+	assert(not core.get_player_by_name)
+	assert(not core.object_refs)
+	-- stuff that should be here
+	assert(core.register_on_generated)
+	assert(core.get_node)
+	assert(core.spawn_tree)
+	assert(ItemStack)
+	local meta = ItemStack():get_meta()
+	assert(type(meta) == "userdata")
+	assert(type(meta.set_tool_capabilities) == "function")
+	assert(core.registered_items[""])
+	assert(core.save_gen_notify)
+	-- alias handling
+	assert(core.registered_items["unittests:steel_ingot_alias"].name ==
+		"unittests:steel_ingot")
+	-- fallback to item defaults
+	assert(core.registered_items["unittests:description_test"].on_place == true)
+end
+
+-- there's no (usable) communcation path between mapgen and the regular env
+-- so we just run the test unconditionally
+do_tests()
+
+core.register_on_generated(function(vm, pos1, pos2, blockseed)
+	local n = tonumber(core.get_mapgen_setting("chunksize")) * 16 - 1
+	assert(pos2:subtract(pos1) == vector.new(n, n, n))
+end)

+ 30 - 0
games/devtest/mods/unittests/misc.lua

@@ -1,3 +1,6 @@
+core.register_mapgen_script(core.get_modpath(core.get_current_modname()) ..
+	DIR_DELIM .. "inside_mapgen_env.lua")
+
 local function test_pseudo_random()
 	-- We have comprehensive unit tests in C++, this is just to make sure the API code isn't messing up
 	local gen1 = PseudoRandom(13)
@@ -204,3 +207,30 @@ local function test_on_mapblocks_changed(cb, player, pos)
 	end
 end
 unittests.register("test_on_mapblocks_changed", test_on_mapblocks_changed, {map=true, async=true})
+
+local function test_gennotify_api()
+	local DECO_ID = 123
+	local UD_ID = "unittests:dummy"
+
+	-- the engine doesn't check if the id is actually valid, maybe it should
+	core.set_gen_notify({decoration=true}, {DECO_ID})
+
+	core.set_gen_notify({custom=true}, nil, {UD_ID})
+
+	local flags, deco, custom = core.get_gen_notify()
+	local function ff(flag)
+		return (" " .. flags .. " "):match("[ ,]" .. flag .. "[ ,]") ~= nil
+	end
+	assert(ff("decoration"), "'decoration' flag missing")
+	assert(ff("custom"), "'custom' flag missing")
+	assert(table.indexof(deco, DECO_ID) > 0)
+	assert(table.indexof(custom, UD_ID) > 0)
+
+	core.set_gen_notify({decoration=false, custom=false})
+
+	flags, deco, custom = core.get_gen_notify()
+	assert(not ff("decoration") and not ff("custom"))
+	assert(#deco == 0, "deco ids not empty")
+	assert(#custom == 0, "custom ids not empty")
+end
+unittests.register("test_gennotify_api", test_gennotify_api)

+ 72 - 78
src/emerge.cpp

@@ -19,19 +19,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 */
 
 
-#include "emerge.h"
+#include "emerge_internal.h"
 
 #include <iostream>
-#include <queue>
 
 #include "util/container.h"
-#include "util/thread.h"
-#include "threading/event.h"
-
 #include "config.h"
 #include "constants.h"
 #include "environment.h"
 #include "irrlicht_changes/printing.h"
+#include "filesys.h"
 #include "log.h"
 #include "map.h"
 #include "mapblock.h"
@@ -42,76 +39,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "nodedef.h"
 #include "profiler.h"
 #include "scripting_server.h"
+#include "scripting_emerge.h"
 #include "server.h"
 #include "settings.h"
 #include "voxel.h"
 
-class EmergeThread : public Thread {
-public:
-	bool enable_mapgen_debug_info;
-	int id;
-
-	EmergeThread(Server *server, int ethreadid);
-	~EmergeThread() = default;
-
-	void *run();
-	void signal();
-
-	// Requires queue mutex held
-	bool pushBlock(const v3s16 &pos);
-
-	void cancelPendingItems();
-
-protected:
-
-	void runCompletionCallbacks(
-		const v3s16 &pos, EmergeAction action,
-		const EmergeCallbackList &callbacks);
-
-private:
-	Server *m_server;
-	ServerMap *m_map;
-	EmergeManager *m_emerge;
-	Mapgen *m_mapgen;
-
-	Event m_queue_event;
-	std::queue<v3s16> m_block_queue;
-
-	bool popBlockEmerge(v3s16 *pos, BlockEmergeData *bedata);
-
-	EmergeAction getBlockOrStartGen(
-		const v3s16 &pos, bool allow_gen, MapBlock **block, BlockMakeData *data);
-	MapBlock *finishGen(v3s16 pos, BlockMakeData *bmdata,
-		std::map<v3s16, MapBlock *> *modified_blocks);
-
-	friend class EmergeManager;
-};
-
-class MapEditEventAreaIgnorer
-{
-public:
-	MapEditEventAreaIgnorer(VoxelArea *ignorevariable, const VoxelArea &a):
-		m_ignorevariable(ignorevariable)
-	{
-		if(m_ignorevariable->getVolume() == 0)
-			*m_ignorevariable = a;
-		else
-			m_ignorevariable = NULL;
-	}
-
-	~MapEditEventAreaIgnorer()
-	{
-		if(m_ignorevariable)
-		{
-			assert(m_ignorevariable->getVolume() != 0);
-			*m_ignorevariable = VoxelArea();
-		}
-	}
-
-private:
-	VoxelArea *m_ignorevariable;
-};
-
 EmergeParams::~EmergeParams()
 {
 	infostream << "EmergeParams: destroying " << this << std::endl;
@@ -131,6 +63,7 @@ EmergeParams::EmergeParams(EmergeManager *parent, const BiomeGen *biomegen,
 	enable_mapgen_debug_info(parent->enable_mapgen_debug_info),
 	gen_notify_on(parent->gen_notify_on),
 	gen_notify_on_deco_ids(&parent->gen_notify_on_deco_ids),
+	gen_notify_on_custom(&parent->gen_notify_on_custom),
 	biomemgr(biomemgr->clone()), oremgr(oremgr->clone()),
 	decomgr(decomgr->clone()), schemmgr(schemmgr->clone())
 {
@@ -518,9 +451,10 @@ EmergeThread::EmergeThread(Server *server, int ethreadid) :
 	enable_mapgen_debug_info(false),
 	id(ethreadid),
 	m_server(server),
-	m_map(NULL),
-	m_emerge(NULL),
-	m_mapgen(NULL)
+	m_map(nullptr),
+	m_emerge(nullptr),
+	m_mapgen(nullptr),
+	m_trans_liquid(nullptr)
 {
 	m_name = "Emerge-" + itos(ethreadid);
 }
@@ -641,13 +575,13 @@ MapBlock *EmergeThread::finishGen(v3s16 pos, BlockMakeData *bmdata,
 				 v3s16(1,1,1) * (MAP_BLOCKSIZE - 1);
 
 	// Ignore map edit events, they will not need to be sent
-	// to anybody because the block hasn't been sent to anybody
+	// to anyone because the block hasn't been sent yet.
 	MapEditEventAreaIgnorer ign(
 		&m_server->m_ignore_map_edit_events_area,
 		VoxelArea(minp, maxp));
 
 	/*
-		Run Lua on_generated callbacks
+		Run Lua on_generated callbacks in the server environment
 	*/
 	try {
 		m_server->getScriptIface()->environment_OnGenerated(
@@ -674,6 +608,36 @@ MapBlock *EmergeThread::finishGen(v3s16 pos, BlockMakeData *bmdata,
 }
 
 
+bool EmergeThread::initScripting()
+{
+	m_script = std::make_unique<EmergeScripting>(this);
+
+	try {
+		m_script->loadMod(Server::getBuiltinLuaPath() + DIR_DELIM + "init.lua",
+			BUILTIN_MOD_NAME);
+		m_script->checkSetByBuiltin();
+	} catch (const ModError &e) {
+		errorstream << "Execution of mapgen base environment failed." << std::endl;
+		m_server->setAsyncFatalError(e.what());
+		return false;
+	}
+
+	const auto &list = m_server->m_mapgen_init_files;
+	try {
+		for (auto &it : list)
+			m_script->loadMod(it.second, it.first);
+
+		m_script->on_mods_loaded();
+	} catch (const ModError &e) {
+		errorstream << "Failed to load mod script inside mapgen environment." << std::endl;
+		m_server->setAsyncFatalError(e.what());
+		return false;
+	}
+
+	return true;
+}
+
+
 void *EmergeThread::run()
 {
 	BEGIN_DEBUG_EXCEPTION_HANDLER
@@ -686,6 +650,11 @@ void *EmergeThread::run()
 	m_mapgen = m_emerge->m_mapgens[id];
 	enable_mapgen_debug_info = m_emerge->enable_mapgen_debug_info;
 
+	if (!initScripting()) {
+		m_script.reset();
+		stop(); // do not enter main loop
+	}
+
 	try {
 	while (!stopRequested()) {
 		BlockEmergeData bedata;
@@ -706,6 +675,9 @@ void *EmergeThread::run()
 
 		action = getBlockOrStartGen(pos, allow_gen, &block, &bmdata);
 		if (action == EMERGE_GENERATED) {
+			bool error = false;
+			m_trans_liquid = &bmdata.transforming_liquid;
+
 			{
 				ScopeProfiler sp(g_profiler,
 					"EmergeThread: Mapgen::makeChunk", SPT_AVG);
@@ -713,9 +685,24 @@ void *EmergeThread::run()
 				m_mapgen->makeChunk(&bmdata);
 			}
 
-			block = finishGen(pos, &bmdata, &modified_blocks);
-			if (!block)
+			{
+				ScopeProfiler sp(g_profiler,
+					"EmergeThread: Lua on_generated", SPT_AVG);
+
+				try {
+					m_script->on_generated(&bmdata);
+				} catch (const LuaError &e) {
+					m_server->setAsyncFatalError(e);
+					error = true;
+				}
+			}
+
+			if (!error)
+				block = finishGen(pos, &bmdata, &modified_blocks);
+			if (!block || error)
 				action = EMERGE_ERRORED;
+
+			m_trans_liquid = nullptr;
 		}
 
 		runCompletionCallbacks(pos, action, bedata.callbacks);
@@ -752,6 +739,13 @@ void *EmergeThread::run()
 		m_server->setAsyncFatalError(err.str());
 	}
 
+	try {
+		if (m_script)
+			m_script->on_shutdown();
+	} catch (const ModError &e) {
+		m_server->setAsyncFatalError(e.what());
+	}
+
 	cancelPendingItems();
 
 	END_DEBUG_EXCEPTION_HANDLER

+ 7 - 0
src/emerge.h

@@ -107,6 +107,7 @@ public:
 
 	u32 gen_notify_on;
 	const std::set<u32> *gen_notify_on_deco_ids; // shared
+	const std::set<std::string> *gen_notify_on_custom; // shared
 
 	BiomeGen *biomegen;
 	BiomeManager *biomemgr;
@@ -114,6 +115,11 @@ public:
 	DecorationManager *decomgr;
 	SchematicManager *schemmgr;
 
+	inline GenerateNotifier createNotifier() const {
+		return GenerateNotifier(gen_notify_on, gen_notify_on_deco_ids,
+			gen_notify_on_custom);
+	}
+
 private:
 	EmergeParams(EmergeManager *parent, const BiomeGen *biomegen,
 		const BiomeManager *biomemgr,
@@ -134,6 +140,7 @@ public:
 	// Generation Notify
 	u32 gen_notify_on = 0;
 	std::set<u32> gen_notify_on_deco_ids;
+	std::set<std::string> gen_notify_on_custom;
 
 	// Parameters passed to mapgens owned by ServerMap
 	// TODO(hmmmm): Remove this after mapgen helper methods using them

+ 115 - 0
src/emerge_internal.h

@@ -0,0 +1,115 @@
+/*
+Minetest
+Copyright (C) 2010-2013 kwolekr, Ryan Kwolek <kwolekr@minetest.net>
+
+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
+
+/******************************************************************/
+/* may only be included by emerge.cpp or emerge scripting related */
+/******************************************************************/
+
+#include "emerge.h"
+
+#include <queue>
+
+#include "util/thread.h"
+#include "threading/event.h"
+
+class Server;
+class ServerMap;
+class Mapgen;
+
+class EmergeManager;
+class EmergeScripting;
+
+class EmergeThread : public Thread {
+public:
+	bool enable_mapgen_debug_info;
+	int id;
+
+	EmergeThread(Server *server, int ethreadid);
+	~EmergeThread() = default;
+
+	void *run();
+	void signal();
+
+	// Requires queue mutex held
+	bool pushBlock(const v3s16 &pos);
+
+	void cancelPendingItems();
+
+	EmergeManager *getEmergeManager() { return m_emerge; }
+	Mapgen *getMapgen() { return m_mapgen; }
+
+protected:
+
+	void runCompletionCallbacks(
+		const v3s16 &pos, EmergeAction action,
+		const EmergeCallbackList &callbacks);
+
+private:
+	Server *m_server;
+	ServerMap *m_map;
+	EmergeManager *m_emerge;
+	Mapgen *m_mapgen;
+
+	std::unique_ptr<EmergeScripting> m_script;
+	// read from scripting:
+	UniqueQueue<v3s16> *m_trans_liquid; //< non-null only when generating a mapblock
+
+	Event m_queue_event;
+	std::queue<v3s16> m_block_queue;
+
+	bool initScripting();
+
+	bool popBlockEmerge(v3s16 *pos, BlockEmergeData *bedata);
+
+	EmergeAction getBlockOrStartGen(
+		const v3s16 &pos, bool allow_gen, MapBlock **block, BlockMakeData *data);
+	MapBlock *finishGen(v3s16 pos, BlockMakeData *bmdata,
+		std::map<v3s16, MapBlock *> *modified_blocks);
+
+	friend class EmergeManager;
+	friend class EmergeScripting;
+	friend class ModApiMapgen;
+};
+
+// Scoped helper to set Server::m_ignore_map_edit_events_area
+class MapEditEventAreaIgnorer
+{
+public:
+	MapEditEventAreaIgnorer(VoxelArea *ignorevariable, const VoxelArea &a):
+		m_ignorevariable(ignorevariable)
+	{
+		if (m_ignorevariable->getVolume() == 0)
+			*m_ignorevariable = a;
+		else
+			m_ignorevariable = nullptr;
+	}
+
+	~MapEditEventAreaIgnorer()
+	{
+		if (m_ignorevariable) {
+			assert(m_ignorevariable->getVolume() != 0);
+			*m_ignorevariable = VoxelArea();
+		}
+	}
+
+private:
+	VoxelArea *m_ignorevariable;
+};

+ 42 - 12
src/mapgen/mapgen.cpp

@@ -70,6 +70,7 @@ FlagDesc flagdesc_gennotify[] = {
 	{"large_cave_begin", 1 << GENNOTIFY_LARGECAVE_BEGIN},
 	{"large_cave_end",   1 << GENNOTIFY_LARGECAVE_END},
 	{"decoration",       1 << GENNOTIFY_DECORATION},
+	{"custom",           1 << GENNOTIFY_CUSTOM},
 	{NULL,               0}
 };
 
@@ -108,7 +109,7 @@ static_assert(
 ////
 
 Mapgen::Mapgen(int mapgenid, MapgenParams *params, EmergeParams *emerge) :
-	gennotify(emerge->gen_notify_on, emerge->gen_notify_on_deco_ids)
+	gennotify(emerge->createNotifier())
 {
 	id           = mapgenid;
 	water_level  = params->water_level;
@@ -980,39 +981,67 @@ void MapgenBasic::generateDungeons(s16 max_stone_y)
 ////
 
 GenerateNotifier::GenerateNotifier(u32 notify_on,
-	const std::set<u32> *notify_on_deco_ids)
+	const std::set<u32> *notify_on_deco_ids,
+	const std::set<std::string> *notify_on_custom)
 {
 	m_notify_on = notify_on;
 	m_notify_on_deco_ids = notify_on_deco_ids;
+	m_notify_on_custom = notify_on_custom;
 }
 
 
-bool GenerateNotifier::addEvent(GenNotifyType type, v3s16 pos, u32 id)
+bool GenerateNotifier::addEvent(GenNotifyType type, v3s16 pos)
 {
-	if (!(m_notify_on & (1 << type)))
+	assert(type != GENNOTIFY_DECORATION && type != GENNOTIFY_CUSTOM);
+	if (!shouldNotifyOn(type))
 		return false;
 
-	if (type == GENNOTIFY_DECORATION &&
-		m_notify_on_deco_ids->find(id) == m_notify_on_deco_ids->cend())
+	GenNotifyEvent gne;
+	gne.type = type;
+	gne.pos  = pos;
+	m_notify_events.emplace_back(std::move(gne));
+	return true;
+}
+
+
+bool GenerateNotifier::addDecorationEvent(v3s16 pos, u32 id)
+{
+	if (!shouldNotifyOn(GENNOTIFY_DECORATION))
+		return false;
+	// check if data relating to this decoration was requested
+	assert(m_notify_on_deco_ids);
+	if (m_notify_on_deco_ids->find(id) == m_notify_on_deco_ids->cend())
 		return false;
 
 	GenNotifyEvent gne;
-	gne.type = type;
+	gne.type = GENNOTIFY_DECORATION;
 	gne.pos  = pos;
 	gne.id   = id;
-	m_notify_events.push_back(gne);
+	m_notify_events.emplace_back(std::move(gne));
+	return true;
+}
+
+
+bool GenerateNotifier::setCustom(const std::string &key, const std::string &value)
+{
+	if (!shouldNotifyOn(GENNOTIFY_CUSTOM))
+		return false;
+	// check if this key was requested to be saved
+	assert(m_notify_on_custom);
+	if (m_notify_on_custom->count(key) == 0)
+		return false;
 
+	m_notify_custom[key] = value;
 	return true;
 }
 
 
 void GenerateNotifier::getEvents(
-	std::map<std::string, std::vector<v3s16> > &event_map)
+	std::map<std::string, std::vector<v3s16>> &event_map) const
 {
-	std::list<GenNotifyEvent>::iterator it;
+	for (auto &gn : m_notify_events) {
+		assert(gn.type != GENNOTIFY_CUSTOM); // never stored in this list
 
-	for (it = m_notify_events.begin(); it != m_notify_events.end(); ++it) {
-		GenNotifyEvent &gn = *it;
 		std::string name = (gn.type == GENNOTIFY_DECORATION) ?
 			"decoration#"+ itos(gn.id) :
 			flagdesc_gennotify[gn.type].name;
@@ -1025,6 +1054,7 @@ void GenerateNotifier::getEvents(
 void GenerateNotifier::clearEvents()
 {
 	m_notify_events.clear();
+	m_notify_custom.clear();
 }
 
 

+ 22 - 10
src/mapgen/mapgen.h

@@ -76,29 +76,41 @@ enum GenNotifyType {
 	GENNOTIFY_LARGECAVE_BEGIN,
 	GENNOTIFY_LARGECAVE_END,
 	GENNOTIFY_DECORATION,
+	GENNOTIFY_CUSTOM, // user-defined data
 	NUM_GENNOTIFY_TYPES
 };
 
-struct GenNotifyEvent {
-	GenNotifyType type;
-	v3s16 pos;
-	u32 id;
-};
-
 class GenerateNotifier {
 public:
+	struct GenNotifyEvent {
+		GenNotifyType type;
+		v3s16 pos;
+		u32 id; // for GENNOTIFY_DECORATION
+	};
+
 	// Use only for temporary Mapgen objects with no map generation!
 	GenerateNotifier() = default;
-	GenerateNotifier(u32 notify_on, const std::set<u32> *notify_on_deco_ids);
-
-	bool addEvent(GenNotifyType type, v3s16 pos, u32 id=0);
-	void getEvents(std::map<std::string, std::vector<v3s16> > &event_map);
+	// normal constructor
+	GenerateNotifier(u32 notify_on, const std::set<u32> *notify_on_deco_ids,
+		const std::set<std::string> *notify_on_custom);
+
+	bool addEvent(GenNotifyType type, v3s16 pos);
+	bool addDecorationEvent(v3s16 pos, u32 deco_id);
+	bool setCustom(const std::string &key, const std::string &value);
+	void getEvents(std::map<std::string, std::vector<v3s16>> &map) const;
+	const StringMap &getCustomData() const { return m_notify_custom; }
 	void clearEvents();
 
 private:
 	u32 m_notify_on = 0;
 	const std::set<u32> *m_notify_on_deco_ids = nullptr;
+	const std::set<std::string> *m_notify_on_custom = nullptr;
 	std::list<GenNotifyEvent> m_notify_events;
+	StringMap m_notify_custom;
+
+	inline bool shouldNotifyOn(GenNotifyType type) const {
+		return m_notify_on & (1 << type);
+	}
 };
 
 // Order must match the order of 'static MapgenDesc g_reg_mapgens[]' in mapgen.cpp

+ 3 - 5
src/mapgen/mg_decoration.cpp

@@ -236,8 +236,7 @@ size_t Decoration::placeDeco(Mapgen *mg, u32 blockseed, v3s16 nmin, v3s16 nmax)
 
 						v3s16 pos(x, y, z);
 						if (generate(mg->vm, &ps, pos, false))
-							mg->gennotify.addEvent(
-									GENNOTIFY_DECORATION, pos, index);
+							mg->gennotify.addDecorationEvent(pos, index);
 					}
 				}
 
@@ -249,8 +248,7 @@ size_t Decoration::placeDeco(Mapgen *mg, u32 blockseed, v3s16 nmin, v3s16 nmax)
 
 						v3s16 pos(x, y, z);
 						if (generate(mg->vm, &ps, pos, true))
-							mg->gennotify.addEvent(
-									GENNOTIFY_DECORATION, pos, index);
+							mg->gennotify.addDecorationEvent(pos, index);
 					}
 				}
 			} else { // Heightmap decorations
@@ -273,7 +271,7 @@ size_t Decoration::placeDeco(Mapgen *mg, u32 blockseed, v3s16 nmin, v3s16 nmax)
 
 				v3s16 pos(x, y, z);
 				if (generate(mg->vm, &ps, pos, false))
-					mg->gennotify.addEvent(GENNOTIFY_DECORATION, pos, index);
+					mg->gennotify.addDecorationEvent(pos, index);
 			}
 		}
 	}

+ 1 - 0
src/script/CMakeLists.txt

@@ -5,6 +5,7 @@ add_subdirectory(lua_api)
 # Used by server and client
 set(common_SCRIPT_SRCS
 	${CMAKE_CURRENT_SOURCE_DIR}/scripting_server.cpp
+	${CMAKE_CURRENT_SOURCE_DIR}/scripting_emerge.cpp
 	${common_SCRIPT_COMMON_SRCS}
 	${common_SCRIPT_CPP_API_SRCS}
 	${common_SCRIPT_LUA_API_SRCS}

+ 1 - 0
src/script/cpp_api/CMakeLists.txt

@@ -5,6 +5,7 @@ set(common_SCRIPT_CPP_API_SRCS
 	${CMAKE_CURRENT_SOURCE_DIR}/s_env.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/s_inventory.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/s_item.cpp
+	${CMAKE_CURRENT_SOURCE_DIR}/s_mapgen.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/s_modchannels.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/s_node.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/s_nodemeta.cpp

+ 8 - 1
src/script/cpp_api/s_base.h

@@ -61,13 +61,15 @@ enum class ScriptingType: u8 {
 	Async, // either mainmenu (client) or ingame (server)
 	Client,
 	MainMenu,
-	Server
+	Server,
+	Emerge
 };
 
 class Server;
 #ifndef SERVER
 class Client;
 #endif
+class EmergeThread;
 class IGameDef;
 class Environment;
 class GUIEngine;
@@ -158,6 +160,9 @@ protected:
 	void setGuiEngine(GUIEngine* guiengine) { m_guiengine = guiengine; }
 #endif
 
+	EmergeThread* getEmergeThread() { return m_emerge; }
+	void setEmergeThread(EmergeThread *emerge) { m_emerge = emerge; }
+
 	void objectrefGetOrCreate(lua_State *L, ServerActiveObject *cobj);
 
 	void pushPlayerHPChangeReason(lua_State *L, const PlayerHPChangeReason& reason);
@@ -180,5 +185,7 @@ private:
 #ifndef SERVER
 	GUIEngine      *m_guiengine = nullptr;
 #endif
+	EmergeThread   *m_emerge = nullptr;
+
 	ScriptingType  m_type;
 };

+ 76 - 0
src/script/cpp_api/s_mapgen.cpp

@@ -0,0 +1,76 @@
+/*
+Minetest
+Copyright (C) 2022 sfan5 <sfan5@live.de>
+
+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 "cpp_api/s_mapgen.h"
+#include "cpp_api/s_internal.h"
+#include "common/c_converter.h"
+#include "lua_api/l_vmanip.h"
+#include "emerge.h"
+
+void ScriptApiMapgen::on_mods_loaded()
+{
+	SCRIPTAPI_PRECHECKHEADER
+
+	// Get registered shutdown hooks
+	lua_getglobal(L, "core");
+	lua_getfield(L, -1, "registered_on_mods_loaded");
+	// Call callbacks
+	runCallbacks(0, RUN_CALLBACKS_MODE_FIRST);
+}
+
+void ScriptApiMapgen::on_shutdown()
+{
+	SCRIPTAPI_PRECHECKHEADER
+
+	// Get registered shutdown hooks
+	lua_getglobal(L, "core");
+	lua_getfield(L, -1, "registered_on_shutdown");
+	// Call callbacks
+	runCallbacks(0, RUN_CALLBACKS_MODE_FIRST);
+}
+
+void ScriptApiMapgen::on_generated(BlockMakeData *bmdata)
+{
+	SCRIPTAPI_PRECHECKHEADER
+
+	v3s16 minp = bmdata->blockpos_min * MAP_BLOCKSIZE;
+	v3s16 maxp = bmdata->blockpos_max * MAP_BLOCKSIZE +
+				 v3s16(1,1,1) * (MAP_BLOCKSIZE - 1);
+
+	LuaVoxelManip::create(L, bmdata->vmanip, true);
+	const int vmanip = lua_gettop(L);
+
+	// Store vmanip globally (used by helpers)
+	lua_getglobal(L, "core");
+	lua_pushvalue(L, vmanip);
+	lua_setfield(L, -2, "vmanip");
+
+	// Call callbacks
+	lua_getfield(L, -1, "registered_on_generateds");
+	lua_pushvalue(L, vmanip);
+	push_v3s16(L, minp);
+	push_v3s16(L, maxp);
+	lua_pushnumber(L, bmdata->seed);
+	runCallbacks(4, RUN_CALLBACKS_MODE_FIRST);
+	lua_pop(L, 1); // return val
+
+	// Unset core.vmanip again
+	lua_pushnil(L);
+	lua_setfield(L, -2, "vmanip");
+}

+ 40 - 0
src/script/cpp_api/s_mapgen.h

@@ -0,0 +1,40 @@
+/*
+Minetest
+Copyright (C) 2022 sfan5 <sfan5@live.de>
+
+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 "cpp_api/s_base.h"
+
+struct BlockMakeData;
+
+/*
+ * Note that this is the class defining the functions called inside the emerge
+ * Lua state, not the server one.
+ */
+
+class ScriptApiMapgen : virtual public ScriptApiBase
+{
+public:
+
+	void on_mods_loaded();
+	void on_shutdown();
+
+	// Called after generating a piece of map before writing it to the map
+	void on_generated(BlockMakeData *bmdata);
+};

+ 5 - 0
src/script/lua_api/l_base.cpp

@@ -75,6 +75,11 @@ GUIEngine *ModApiBase::getGuiEngine(lua_State *L)
 }
 #endif
 
+EmergeThread *ModApiBase::getEmergeThread(lua_State *L)
+{
+	return getScriptApiBase(L)->getEmergeThread();
+}
+
 std::string ModApiBase::getCurrentModPath(lua_State *L)
 {
 	lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_CURRENT_MOD_NAME);

+ 2 - 1
src/script/lua_api/l_base.h

@@ -34,7 +34,7 @@ extern "C" {
 class Client;
 class GUIEngine;
 #endif
-
+class EmergeThread;
 class ScriptApiBase;
 class Server;
 class Environment;
@@ -49,6 +49,7 @@ public:
 	static Client*          getClient(lua_State *L);
 	static GUIEngine*       getGuiEngine(lua_State *L);
 	#endif // !SERVER
+	static EmergeThread*    getEmergeThread(lua_State *L);
 
 	static IGameDef*        getGameDef(lua_State *L);
 

+ 188 - 2
src/script/lua_api/l_env.cpp

@@ -34,7 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "daynightratio.h"
 #include "util/pointedthing.h"
 #include "mapgen/treegen.h"
-#include "emerge.h"
+#include "emerge_internal.h"
 #include "pathfinder.h"
 #include "face_position_cache.h"
 #include "remoteplayer.h"
@@ -241,7 +241,7 @@ void LuaEmergeAreaCallback(v3s16 blockpos, EmergeAction action, void *param)
 		delete state;
 }
 
-// Exported functions
+/* Exported functions */
 
 // set_node(pos, node)
 // pos = {x=num, y=num, z=num}
@@ -1538,3 +1538,189 @@ void ModApiEnv::InitializeClient(lua_State *L, int top)
 	API_FCT(line_of_sight);
 	API_FCT(raycast);
 }
+
+#define GET_VM_PTR               \
+	MMVManip *vm = getVManip(L); \
+	if (!vm)                     \
+		return 0
+
+// get_node_max_level(pos)
+int ModApiEnvVM::l_get_node_max_level(lua_State *L)
+{
+	GET_VM_PTR;
+
+	v3s16 pos = read_v3s16(L, 1);
+	MapNode n = vm->getNodeNoExNoEmerge(pos);
+	lua_pushnumber(L, n.getMaxLevel(getGameDef(L)->ndef()));
+	return 1;
+}
+
+// get_node_level(pos)
+int ModApiEnvVM::l_get_node_level(lua_State *L)
+{
+	GET_VM_PTR;
+
+	v3s16 pos = read_v3s16(L, 1);
+	MapNode n = vm->getNodeNoExNoEmerge(pos);
+	lua_pushnumber(L, n.getLevel(getGameDef(L)->ndef()));
+	return 1;
+}
+
+// set_node_level(pos, level)
+int ModApiEnvVM::l_set_node_level(lua_State *L)
+{
+	GET_VM_PTR;
+
+	v3s16 pos = read_v3s16(L, 1);
+	u8 level = 1;
+	if (lua_isnumber(L, 2))
+		level = lua_tonumber(L, 2);
+	MapNode n = vm->getNodeNoExNoEmerge(pos);
+	lua_pushnumber(L, n.setLevel(getGameDef(L)->ndef(), level));
+	vm->setNodeNoEmerge(pos, n);
+	return 1;
+}
+
+// add_node_level(pos, level)
+int ModApiEnvVM::l_add_node_level(lua_State *L)
+{
+	GET_VM_PTR;
+
+	v3s16 pos = read_v3s16(L, 1);
+	u8 level = 1;
+	if (lua_isnumber(L, 2))
+		level = lua_tonumber(L, 2);
+	MapNode n = vm->getNodeNoExNoEmerge(pos);
+	lua_pushnumber(L, n.addLevel(getGameDef(L)->ndef(), level));
+	vm->setNodeNoEmerge(pos, n);
+	return 1;
+}
+
+// find_node_near(pos, radius, nodenames, [search_center])
+int ModApiEnvVM::l_find_node_near(lua_State *L)
+{
+	GET_VM_PTR;
+
+	const NodeDefManager *ndef = getGameDef(L)->ndef();
+
+	v3s16 pos = read_v3s16(L, 1);
+	int radius = luaL_checkinteger(L, 2);
+	std::vector<content_t> filter;
+	collectNodeIds(L, 3, ndef, filter);
+	int start_radius = (lua_isboolean(L, 4) && readParam<bool>(L, 4)) ? 0 : 1;
+
+	auto getNode = [&vm] (v3s16 p) -> MapNode {
+		return vm->getNodeNoExNoEmerge(p);
+	};
+	return findNodeNear(L, pos, radius, filter, start_radius, getNode);
+}
+
+// find_nodes_in_area(minp, maxp, nodenames, [grouped])
+int ModApiEnvVM::l_find_nodes_in_area(lua_State *L)
+{
+	GET_VM_PTR;
+
+	const NodeDefManager *ndef = getGameDef(L)->ndef();
+
+	v3s16 minp = read_v3s16(L, 1);
+	v3s16 maxp = read_v3s16(L, 2);
+	sortBoxVerticies(minp, maxp);
+
+	checkArea(minp, maxp);
+	// avoid the loop going out-of-bounds
+	{
+		VoxelArea cropped = VoxelArea(minp, maxp).intersect(vm->m_area);
+		minp = cropped.MinEdge;
+		maxp = cropped.MaxEdge;
+	}
+
+	std::vector<content_t> filter;
+	collectNodeIds(L, 3, ndef, filter);
+
+	bool grouped = lua_isboolean(L, 4) && readParam<bool>(L, 4);
+
+	auto iterate = [&] (auto callback) {
+		for (s16 z = minp.Z; z <= maxp.Z; z++)
+		for (s16 y = minp.Y; y <= maxp.Y; y++) {
+			u32 vi = vm->m_area.index(minp.X, y, z);
+			for (s16 x = minp.X; x <= maxp.X; x++) {
+				v3s16 pos(x, y, z);
+				MapNode n = vm->m_data[vi];
+				if (!callback(pos, n))
+					return;
+				++vi;
+			}
+		}
+	};
+	return findNodesInArea(L, ndef, filter, grouped, iterate);
+}
+
+// find_nodes_in_area_under_air(minp, maxp, nodenames)
+int ModApiEnvVM::l_find_nodes_in_area_under_air(lua_State *L)
+{
+	GET_VM_PTR;
+
+	const NodeDefManager *ndef = getGameDef(L)->ndef();
+
+	v3s16 minp = read_v3s16(L, 1);
+	v3s16 maxp = read_v3s16(L, 2);
+	sortBoxVerticies(minp, maxp);
+	checkArea(minp, maxp);
+
+	std::vector<content_t> filter;
+	collectNodeIds(L, 3, ndef, filter);
+
+	auto getNode = [&vm] (v3s16 p) -> MapNode {
+		return vm->getNodeNoExNoEmerge(p);
+	};
+	return findNodesInAreaUnderAir(L, minp, maxp, filter, getNode);
+}
+
+// spawn_tree(pos, treedef)
+int ModApiEnvVM::l_spawn_tree(lua_State *L)
+{
+	GET_VM_PTR;
+
+	const NodeDefManager *ndef = getGameDef(L)->ndef();
+
+	v3s16 p0 = read_v3s16(L, 1);
+
+	treegen::TreeDef tree_def;
+	if (!read_tree_def(L, 2, ndef, tree_def))
+		return 0;
+
+	treegen::error e;
+	if ((e = treegen::make_ltree(*vm, p0, ndef, tree_def)) != treegen::SUCCESS) {
+		if (e == treegen::UNBALANCED_BRACKETS) {
+			throw LuaError("spawn_tree(): closing ']' has no matching opening bracket");
+		} else {
+			throw LuaError("spawn_tree(): unknown error");
+		}
+	}
+
+	lua_pushboolean(L, true);
+	return 1;
+}
+
+MMVManip *ModApiEnvVM::getVManip(lua_State *L)
+{
+	auto emerge = getEmergeThread(L);
+	if (emerge)
+		return emerge->getMapgen()->vm;
+	return nullptr;
+}
+
+void ModApiEnvVM::InitializeEmerge(lua_State *L, int top)
+{
+	// other, more trivial functions are in builtin/emerge/env.lua
+	API_FCT(get_node_max_level);
+	API_FCT(get_node_level);
+	API_FCT(set_node_level);
+	API_FCT(add_node_level);
+	API_FCT(find_node_near);
+	API_FCT(find_nodes_in_area);
+	API_FCT(find_nodes_in_area_under_air);
+	API_FCT(spawn_tree);
+}
+
+#undef GET_VM_PTR

+ 38 - 0
src/script/lua_api/l_env.h

@@ -243,6 +243,44 @@ public:
 	static void InitializeClient(lua_State *L, int top);
 };
 
+/*
+ * Duplicates of certain env APIs that operate not on the global
+ * map but on a VoxelManipulator. This is for emerge scripting. 
+ */
+class ModApiEnvVM : public ModApiEnvBase {
+private:
+
+	// get_node_max_level(pos)
+	static int l_get_node_max_level(lua_State *L);
+
+	// get_node_level(pos)
+	static int l_get_node_level(lua_State *L);
+
+	// set_node_level(pos)
+	static int l_set_node_level(lua_State *L);
+
+	// add_node_level(pos)
+	static int l_add_node_level(lua_State *L);
+
+	// find_node_near(pos, radius, nodenames, [search_center])
+	static int l_find_node_near(lua_State *L);
+
+	// find_nodes_in_area(minp, maxp, nodenames, [grouped])
+	static int l_find_nodes_in_area(lua_State *L);
+
+	// find_surface_nodes_in_area(minp, maxp, nodenames)
+	static int l_find_nodes_in_area_under_air(lua_State *L);
+
+	// spawn_tree(pos, treedef)
+	static int l_spawn_tree(lua_State *L);
+
+	// Helper: get the vmanip we're operating on
+	static MMVManip *getVManip(lua_State *L);
+
+public:
+	static void InitializeEmerge(lua_State *L, int top);
+};
+
 class LuaABM : public ActiveBlockModifier {
 private:
 	int m_id;

+ 202 - 44
src/script/lua_api/l_mapgen.cpp

@@ -26,7 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "util/serialize.h"
 #include "server.h"
 #include "environment.h"
-#include "emerge.h"
+#include "emerge_internal.h"
 #include "mapgen/mg_biome.h"
 #include "mapgen/mg_ore.h"
 #include "mapgen/mg_decoration.h"
@@ -482,7 +482,7 @@ int ModApiMapgen::l_get_biome_id(lua_State *L)
 
 	const char *biome_str = luaL_checkstring(L, 1);
 
-	const BiomeManager *bmgr = getServer(L)->getEmergeManager()->getBiomeManager();
+	const BiomeManager *bmgr = getEmergeManager(L)->getBiomeManager();
 	if (!bmgr)
 		return 0;
 
@@ -504,7 +504,7 @@ int ModApiMapgen::l_get_biome_name(lua_State *L)
 
 	int biome_id = luaL_checkinteger(L, 1);
 
-	const BiomeManager *bmgr = getServer(L)->getEmergeManager()->getBiomeManager();
+	const BiomeManager *bmgr = getEmergeManager(L)->getBiomeManager();
 	if (!bmgr)
 		return 0;
 
@@ -523,8 +523,7 @@ int ModApiMapgen::l_get_heat(lua_State *L)
 
 	v3s16 pos = read_v3s16(L, 1);
 
-	const BiomeGen *biomegen = getServer(L)->getEmergeManager()->getBiomeGen();
-
+	const BiomeGen *biomegen = getBiomeGen(L);
 	if (!biomegen || biomegen->getType() != BIOMEGEN_ORIGINAL)
 		return 0;
 
@@ -544,8 +543,7 @@ int ModApiMapgen::l_get_humidity(lua_State *L)
 
 	v3s16 pos = read_v3s16(L, 1);
 
-	const BiomeGen *biomegen = getServer(L)->getEmergeManager()->getBiomeGen();
-
+	const BiomeGen *biomegen = getBiomeGen(L);
 	if (!biomegen || biomegen->getType() != BIOMEGEN_ORIGINAL)
 		return 0;
 
@@ -565,7 +563,7 @@ int ModApiMapgen::l_get_biome_data(lua_State *L)
 
 	v3s16 pos = read_v3s16(L, 1);
 
-	const BiomeGen *biomegen = getServer(L)->getEmergeManager()->getBiomeGen();
+	const BiomeGen *biomegen = getBiomeGen(L);
 	if (!biomegen)
 		return 0;
 
@@ -607,8 +605,7 @@ int ModApiMapgen::l_get_mapgen_object(lua_State *L)
 
 	enum MapgenObject mgobj = (MapgenObject)mgobjint;
 
-	EmergeManager *emerge = getServer(L)->getEmergeManager();
-	Mapgen *mg = emerge->getCurrentMapgen();
+	Mapgen *mg = getMapgen(L);
 	if (!mg)
 		throw LuaError("Must only be called in a mapgen thread!");
 
@@ -683,8 +680,7 @@ int ModApiMapgen::l_get_mapgen_object(lua_State *L)
 		return 1;
 	}
 	case MGOBJ_GENNOTIFY: {
-		std::map<std::string, std::vector<v3s16> >event_map;
-
+		std::map<std::string, std::vector<v3s16>> event_map;
 		mg->gennotify.getEvents(event_map);
 
 		lua_createtable(L, 0, event_map.size());
@@ -699,6 +695,24 @@ int ModApiMapgen::l_get_mapgen_object(lua_State *L)
 			lua_setfield(L, -2, it->first.c_str());
 		}
 
+		// push user-defined data
+		auto &custom_map = mg->gennotify.getCustomData();
+
+		lua_createtable(L, 0, custom_map.size());
+		lua_getglobal(L, "core");
+		lua_getfield(L, -1, "deserialize");
+		lua_remove(L, -2); // remove 'core'
+		for (const auto &it : custom_map) {
+			lua_pushvalue(L, -1); // deserialize func
+			lua_pushlstring(L, it.second.c_str(), it.second.size());
+			lua_pushboolean(L, true);
+			lua_call(L, 2, 1);
+
+			lua_setfield(L, -3, it.first.c_str()); // put into table
+		}
+		lua_pop(L, 1); // remove func
+		lua_setfield(L, -2, "custom"); // put into top-level table
+
 		return 1;
 	}
 	}
@@ -728,6 +742,31 @@ int ModApiMapgen::l_get_spawn_level(lua_State *L)
 }
 
 
+// get_seed([add])
+int ModApiMapgen::l_get_seed(lua_State *L)
+{
+	NO_MAP_LOCK_REQUIRED;
+
+	// This exists to
+	// 1. not duplicate the truncation logic from Mapgen::Mapgen() once more
+	// 2. because I don't trust myself to do it correctly in Lua
+
+	auto *emerge = getEmergeManager(L);
+	if (!emerge || !emerge->mgparams)
+		return 0;
+
+	int add = 0;
+	if (lua_isnumber(L, 1))
+		add = luaL_checkint(L, 1);
+
+	s32 seed = (s32)emerge->mgparams->seed;
+	seed += add;
+
+	lua_pushinteger(L, seed);
+	return 1;
+}
+
+
 int ModApiMapgen::l_get_mapgen_params(lua_State *L)
 {
 	NO_MAP_LOCK_REQUIRED;
@@ -737,8 +776,8 @@ int ModApiMapgen::l_get_mapgen_params(lua_State *L)
 
 	std::string value;
 
-	MapSettingsManager *settingsmgr =
-		getServer(L)->getEmergeManager()->map_settings_mgr;
+	const MapSettingsManager *settingsmgr =
+		getEmergeManager(L)->map_settings_mgr;
 
 	lua_newtable(L);
 
@@ -810,7 +849,8 @@ int ModApiMapgen::l_get_mapgen_edges(lua_State *L)
 {
 	NO_MAP_LOCK_REQUIRED;
 
-	MapSettingsManager *settingsmgr = getServer(L)->getEmergeManager()->map_settings_mgr;
+	const MapSettingsManager *settingsmgr =
+		getEmergeManager(L)->map_settings_mgr;
 
 	// MapSettingsManager::makeMapgenParams cannot be used here because it would
 	// make mapgen settings immutable from then on. Mapgen settings should stay
@@ -846,8 +886,8 @@ int ModApiMapgen::l_get_mapgen_setting(lua_State *L)
 	NO_MAP_LOCK_REQUIRED;
 
 	std::string value;
-	MapSettingsManager *settingsmgr =
-		getServer(L)->getEmergeManager()->map_settings_mgr;
+	const MapSettingsManager *settingsmgr =
+		getEmergeManager(L)->map_settings_mgr;
 
 	const char *name = luaL_checkstring(L, 1);
 	if (!settingsmgr->getMapSetting(name, &value))
@@ -863,8 +903,8 @@ int ModApiMapgen::l_get_mapgen_setting_noiseparams(lua_State *L)
 	NO_MAP_LOCK_REQUIRED;
 
 	NoiseParams np;
-	MapSettingsManager *settingsmgr =
-		getServer(L)->getEmergeManager()->map_settings_mgr;
+	const MapSettingsManager *settingsmgr =
+		getEmergeManager(L)->map_settings_mgr;
 
 	const char *name = luaL_checkstring(L, 1);
 	if (!settingsmgr->getMapSettingNoiseParams(name, &np))
@@ -964,7 +1004,7 @@ int ModApiMapgen::l_get_noiseparams(lua_State *L)
 }
 
 
-// set_gen_notify(flags, {deco_id_table})
+// set_gen_notify(flags, {deco_ids}, {custom_ids})
 int ModApiMapgen::l_set_gen_notify(lua_State *L)
 {
 	NO_MAP_LOCK_REQUIRED;
@@ -986,11 +1026,26 @@ int ModApiMapgen::l_set_gen_notify(lua_State *L)
 		}
 	}
 
+	if (lua_istable(L, 3)) {
+		lua_pushnil(L);
+		while (lua_next(L, 3)) {
+			emerge->gen_notify_on_custom.insert(readParam<std::string>(L, -1));
+			lua_pop(L, 1);
+		}
+	}
+
+	// Clear sets if relevant flag disabled
+	if ((emerge->gen_notify_on & (1 << GENNOTIFY_DECORATION)) == 0)
+		emerge->gen_notify_on_deco_ids.clear();
+	if ((emerge->gen_notify_on & (1 << GENNOTIFY_CUSTOM)) == 0)
+		emerge->gen_notify_on_custom.clear();
+
 	return 0;
 }
 
 
 // get_gen_notify()
+// returns flagstring, {deco_ids}, {custom_ids})
 int ModApiMapgen::l_get_gen_notify(lua_State *L)
 {
 	NO_MAP_LOCK_REQUIRED;
@@ -999,13 +1054,43 @@ int ModApiMapgen::l_get_gen_notify(lua_State *L)
 	push_flags_string(L, flagdesc_gennotify, emerge->gen_notify_on,
 		emerge->gen_notify_on);
 
-	lua_newtable(L);
+	lua_createtable(L, emerge->gen_notify_on_deco_ids.size(), 0);
 	int i = 1;
-	for (u32 gen_notify_on_deco_id : emerge->gen_notify_on_deco_ids) {
-		lua_pushnumber(L, gen_notify_on_deco_id);
+	for (u32 id : emerge->gen_notify_on_deco_ids) {
+		lua_pushnumber(L, id);
 		lua_rawseti(L, -2, i++);
 	}
-	return 2;
+
+	lua_createtable(L, emerge->gen_notify_on_custom.size(), 0);
+	int j = 1;
+	for (const auto &id : emerge->gen_notify_on_custom) {
+		lua_pushstring(L, id.c_str());
+		lua_rawseti(L, -2, j++);
+	}
+
+	return 3;
+}
+
+
+// save_gen_notify(custom_id, data) [in emerge thread]
+int ModApiMapgen::l_save_gen_notify(lua_State *L)
+{
+	auto *emerge = getEmergeThread(L);
+
+	std::string key = readParam<std::string>(L, 1);
+
+	lua_getglobal(L, "core");
+	lua_getfield(L, -1, "serialize");
+	lua_remove(L, -2); // remove 'core'
+	lua_pushvalue(L, 2);
+	lua_call(L, 1, 1);
+	std::string val = readParam<std::string>(L, -1);
+	lua_pop(L, 1);
+
+	bool set = emerge->getMapgen()->gennotify.setCustom(key, val);
+
+	lua_pushboolean(L, set);
+	return 1;
 }
 
 
@@ -1020,8 +1105,7 @@ int ModApiMapgen::l_get_decoration_id(lua_State *L)
 		return 0;
 
 	const DecorationManager *dmgr =
-		getServer(L)->getEmergeManager()->getDecorationManager();
-
+		getEmergeManager(L)->getDecorationManager();
 	if (!dmgr)
 		return 0;
 
@@ -1452,20 +1536,26 @@ int ModApiMapgen::l_clear_registered_schematics(lua_State *L)
 }
 
 
-// generate_ores(vm, p1, p2, [ore_id])
+// generate_ores(vm, p1, p2)
 int ModApiMapgen::l_generate_ores(lua_State *L)
 {
 	NO_MAP_LOCK_REQUIRED;
 
-	EmergeManager *emerge = getServer(L)->getEmergeManager();
+	auto *emerge = getEmergeManager(L);
 	if (!emerge || !emerge->mgparams)
 		return 0;
 
+	OreManager *oremgr;
+	if (auto mg = getMapgen(L))
+		oremgr = mg->m_emerge->oremgr;
+	else
+		oremgr = emerge->oremgr;
+
 	Mapgen mg;
 	// Intentionally truncates to s32, see Mapgen::Mapgen()
 	mg.seed = (s32)emerge->mgparams->seed;
 	mg.vm   = checkObject<LuaVoxelManip>(L, 1)->vm;
-	mg.ndef = getServer(L)->getNodeDefManager();
+	mg.ndef = emerge->ndef;
 
 	v3s16 pmin = lua_istable(L, 2) ? check_v3s16(L, 2) :
 			mg.vm->m_area.MinEdge + v3s16(1,1,1) * MAP_BLOCKSIZE;
@@ -1475,26 +1565,32 @@ int ModApiMapgen::l_generate_ores(lua_State *L)
 
 	u32 blockseed = Mapgen::getBlockSeed(pmin, mg.seed);
 
-	emerge->oremgr->placeAllOres(&mg, blockseed, pmin, pmax);
+	oremgr->placeAllOres(&mg, blockseed, pmin, pmax);
 
 	return 0;
 }
 
 
-// generate_decorations(vm, p1, p2, [deco_id])
+// generate_decorations(vm, p1, p2)
 int ModApiMapgen::l_generate_decorations(lua_State *L)
 {
 	NO_MAP_LOCK_REQUIRED;
 
-	EmergeManager *emerge = getServer(L)->getEmergeManager();
+	auto *emerge = getEmergeManager(L);
 	if (!emerge || !emerge->mgparams)
 		return 0;
 
+	DecorationManager *decomgr;
+	if (auto mg = getMapgen(L))
+		decomgr = mg->m_emerge->decomgr;
+	else
+		decomgr = emerge->decomgr;
+
 	Mapgen mg;
 	// Intentionally truncates to s32, see Mapgen::Mapgen()
 	mg.seed = (s32)emerge->mgparams->seed;
 	mg.vm   = checkObject<LuaVoxelManip>(L, 1)->vm;
-	mg.ndef = getServer(L)->getNodeDefManager();
+	mg.ndef = emerge->ndef;
 
 	v3s16 pmin = lua_istable(L, 2) ? check_v3s16(L, 2) :
 			mg.vm->m_area.MinEdge + v3s16(1,1,1) * MAP_BLOCKSIZE;
@@ -1504,7 +1600,7 @@ int ModApiMapgen::l_generate_decorations(lua_State *L)
 
 	u32 blockseed = Mapgen::getBlockSeed(pmin, mg.seed);
 
-	emerge->decomgr->placeAllDecos(&mg, blockseed, pmin, pmax);
+	decomgr->placeAllDecos(&mg, blockseed, pmin, pmax);
 
 	return 0;
 }
@@ -1629,7 +1725,11 @@ int ModApiMapgen::l_place_schematic_on_vmanip(lua_State *L)
 {
 	NO_MAP_LOCK_REQUIRED;
 
-	SchematicManager *schemmgr = getServer(L)->getEmergeManager()->schemmgr;
+	SchematicManager *schemmgr;
+	if (auto mg = getMapgen(L))
+		schemmgr = mg->m_emerge->schemmgr;
+	else
+		schemmgr = getServer(L)->getEmergeManager()->schemmgr;
 
 	//// Read VoxelManip object
 	MMVManip *vm = checkObject<LuaVoxelManip>(L, 1)->vm;
@@ -1677,7 +1777,7 @@ int ModApiMapgen::l_serialize_schematic(lua_State *L)
 {
 	NO_MAP_LOCK_REQUIRED;
 
-	const SchematicManager *schemmgr = getServer(L)->getEmergeManager()->getSchematicManager();
+	const SchematicManager *schemmgr = getEmergeManager(L)->getSchematicManager();
 
 	//// Read options
 	bool use_comments = getboolfield_default(L, 3, "lua_use_comments", false);
@@ -1727,8 +1827,7 @@ int ModApiMapgen::l_read_schematic(lua_State *L)
 {
 	NO_MAP_LOCK_REQUIRED;
 
-	const SchematicManager *schemmgr =
-		getServer(L)->getEmergeManager()->getSchematicManager();
+	const SchematicManager *schemmgr = getEmergeManager(L)->getSchematicManager();
 	const NodeDefManager *ndef = getGameDef(L)->ndef();
 
 	//// Read options
@@ -1806,17 +1905,22 @@ int ModApiMapgen::l_read_schematic(lua_State *L)
 
 int ModApiMapgen::update_liquids(lua_State *L, MMVManip *vm)
 {
-	GET_ENV_PTR;
+	UniqueQueue<v3s16> *trans_liquid;
+	if (auto emerge = getEmergeThread(L)) {
+		trans_liquid = emerge->m_trans_liquid;
+	} else {
+		GET_ENV_PTR;
+		trans_liquid = &env->getServerMap().m_transforming_liquid;
+	}
+	assert(trans_liquid);
 
-	ServerMap *map = &(env->getServerMap());
-	const NodeDefManager *ndef = getServer(L)->getNodeDefManager();
+	const NodeDefManager *ndef = getGameDef(L)->ndef();
 
 	Mapgen mg;
 	mg.vm   = vm;
 	mg.ndef = ndef;
 
-	mg.updateLiquid(&map->m_transforming_liquid,
-		vm->m_area.MinEdge, vm->m_area.MaxEdge);
+	mg.updateLiquid(trans_liquid, vm->m_area.MinEdge, vm->m_area.MaxEdge);
 	return 0;
 }
 
@@ -1824,7 +1928,7 @@ int ModApiMapgen::calc_lighting(lua_State *L, MMVManip *vm,
 		v3s16 pmin, v3s16 pmax, bool propagate_shadow)
 {
 	const NodeDefManager *ndef = getGameDef(L)->ndef();
-	EmergeManager *emerge = getServer(L)->getEmergeManager();
+	auto emerge = getEmergeManager(L);
 
 	assert(vm->m_area.contains(VoxelArea(pmin, pmax)));
 
@@ -1850,6 +1954,35 @@ int ModApiMapgen::set_lighting(lua_State *L, MMVManip *vm,
 	return 0;
 }
 
+const EmergeManager *ModApiMapgen::getEmergeManager(lua_State *L)
+{
+	auto emerge = getEmergeThread(L);
+	if (emerge)
+		return emerge->getEmergeManager();
+	return getServer(L)->getEmergeManager();
+}
+
+const BiomeGen *ModApiMapgen::getBiomeGen(lua_State *L)
+{
+	// path 1: we're in the emerge environment
+	auto emerge = getEmergeThread(L);
+	if (emerge)
+		return emerge->getMapgen()->m_emerge->biomegen;
+	// path 2: we're in the server environment
+	auto manager = getServer(L)->getEmergeManager();
+	return manager->getBiomeGen();
+}
+
+Mapgen *ModApiMapgen::getMapgen(lua_State *L)
+{
+	// path 1
+	auto emerge = getEmergeThread(L);
+	if (emerge)
+		return emerge->getMapgen();
+	// path 2
+	return getServer(L)->getEmergeManager()->getCurrentMapgen();
+}
+
 void ModApiMapgen::Initialize(lua_State *L, int top)
 {
 	API_FCT(get_biome_id);
@@ -1891,3 +2024,28 @@ void ModApiMapgen::Initialize(lua_State *L, int top)
 	API_FCT(serialize_schematic);
 	API_FCT(read_schematic);
 }
+
+void ModApiMapgen::InitializeEmerge(lua_State *L, int top)
+{
+	API_FCT(get_biome_id);
+	API_FCT(get_biome_name);
+	API_FCT(get_heat);
+	API_FCT(get_humidity);
+	API_FCT(get_biome_data);
+	API_FCT(get_mapgen_object);
+
+	API_FCT(get_seed);
+	API_FCT(get_mapgen_params);
+	API_FCT(get_mapgen_edges);
+	API_FCT(get_mapgen_setting);
+	API_FCT(get_mapgen_setting_noiseparams);
+	API_FCT(get_noiseparams);
+	API_FCT(get_decoration_id);
+	API_FCT(save_gen_notify);
+
+	API_FCT(generate_ores);
+	API_FCT(generate_decorations);
+	API_FCT(place_schematic_on_vmanip);
+	API_FCT(serialize_schematic);
+	API_FCT(read_schematic);
+}

+ 20 - 1
src/script/lua_api/l_mapgen.h

@@ -25,6 +25,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 typedef u16 biome_t;  // copy from mg_biome.h to avoid an unnecessary include
 
 class MMVManip;
+class BiomeManager;
+class BiomeGen;
+class Mapgen;
 
 class ModApiMapgen : public ModApiBase
 {
@@ -68,6 +71,9 @@ private:
 	// get_mapgen_edges([mapgen_limit[, chunksize]])
 	static int l_get_mapgen_edges(lua_State *L);
 
+	// get_seed([add])
+	static int l_get_seed(lua_State *L);
+
 	// get_mapgen_setting(name)
 	static int l_get_mapgen_setting(lua_State *L);
 
@@ -86,12 +92,15 @@ private:
 	// get_noiseparam_defaults(name)
 	static int l_get_noiseparams(lua_State *L);
 
-	// set_gen_notify(flags, {deco_id_table})
+	// set_gen_notify(flags, {deco_ids}, {ud_ids})
 	static int l_set_gen_notify(lua_State *L);
 
 	// get_gen_notify()
 	static int l_get_gen_notify(lua_State *L);
 
+	// save_gen_notify(ud_id, data)
+	static int l_save_gen_notify(lua_State *L);
+
 	// get_decoration_id(decoration_name)
 	// returns the decoration ID as used in gennotify
 	static int l_get_decoration_id(lua_State *L);
@@ -158,8 +167,18 @@ private:
 	static int set_lighting(lua_State *L, MMVManip *vm,
 			v3s16 pmin, v3s16 pmax, u8 light);
 
+	// Helpers
+
+	// get a read-only(!) EmergeManager
+	static const EmergeManager *getEmergeManager(lua_State *L);
+	// get the thread-local or global BiomeGen (still read-only)
+	static const BiomeGen *getBiomeGen(lua_State *L);
+	// get the thread-local mapgen
+	static Mapgen *getMapgen(lua_State *L);
+
 public:
 	static void Initialize(lua_State *L, int top);
+	static void InitializeEmerge(lua_State *L, int top);
 
 	static struct EnumString es_BiomeTerrainType[];
 	static struct EnumString es_DecorationType[];

+ 21 - 0
src/script/lua_api/l_server.cpp

@@ -667,6 +667,25 @@ int ModApiServer::l_register_async_dofile(lua_State *L)
 	return 1;
 }
 
+// register_mapgen_script(path)
+int ModApiServer::l_register_mapgen_script(lua_State *L)
+{
+	NO_MAP_LOCK_REQUIRED;
+
+	std::string path = readParam<std::string>(L, 1);
+	CHECK_SECURE_PATH(L, path.c_str(), false);
+
+	// Find currently running mod name (only at init time)
+	lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_CURRENT_MOD_NAME);
+	if (!lua_isstring(L, -1))
+		return 0;
+	std::string modname = readParam<std::string>(L, -1);
+
+	getServer(L)->m_mapgen_init_files.emplace_back(modname, path);
+	lua_pushboolean(L, true);
+	return 1;
+}
+
 // serialize_roundtrip(value)
 // Meant for unit testing the packer from Lua
 int ModApiServer::l_serialize_roundtrip(lua_State *L)
@@ -730,6 +749,8 @@ void ModApiServer::Initialize(lua_State *L, int top)
 	API_FCT(do_async_callback);
 	API_FCT(register_async_dofile);
 	API_FCT(serialize_roundtrip);
+
+	API_FCT(register_mapgen_script);
 }
 
 void ModApiServer::InitializeAsync(lua_State *L, int top)

+ 3 - 0
src/script/lua_api/l_server.h

@@ -118,6 +118,9 @@ private:
 	// register_async_dofile(path)
 	static int l_register_async_dofile(lua_State *L);
 
+	// register_mapgen_script(path)
+	static int l_register_mapgen_script(lua_State *L);
+
 	// serialize_roundtrip(obj)
 	static int l_serialize_roundtrip(lua_State *L);
 

+ 13 - 7
src/script/lua_api/l_vmanip.cpp

@@ -48,6 +48,9 @@ int LuaVoxelManip::l_read_from_map(lua_State *L)
 	if (vm->isOrphan())
 		return 0;
 
+	if (getEmergeThread(L))
+		throw LuaError("VoxelManip:read_from_map called in mapgen environment");
+
 	v3s16 bp1 = getNodeBlockPos(check_v3s16(L, 2));
 	v3s16 bp2 = getNodeBlockPos(check_v3s16(L, 3));
 	sortBoxVerticies(bp1, bp2);
@@ -110,14 +113,18 @@ int LuaVoxelManip::l_set_data(lua_State *L)
 
 int LuaVoxelManip::l_write_to_map(lua_State *L)
 {
-	GET_ENV_PTR;
-
 	LuaVoxelManip *o = checkObject<LuaVoxelManip>(L, 1);
 	bool update_light = !lua_isboolean(L, 2) || readParam<bool>(L, 2);
 
 	if (o->vm->isOrphan())
 		return 0;
 
+	// This wouldn't work anyway as we have no env ptr, but it's still unsafe.
+	if (getEmergeThread(L))
+		throw LuaError("VoxelManip:write_to_map called in mapgen environment");
+
+	GET_ENV_PTR;
+
 	ServerMap *map = &(env->getServerMap());
 
 	std::map<v3s16, MapBlock*> modified_blocks;
@@ -154,9 +161,8 @@ int LuaVoxelManip::l_set_node_at(lua_State *L)
 	v3s16 pos        = check_v3s16(L, 2);
 	MapNode n        = readnode(L, 3);
 
-	o->vm->setNodeNoEmerge(pos, n);
-
-	return 0;
+	lua_pushboolean(L, o->vm->setNodeNoEmerge(pos, n));
+	return 1;
 }
 
 int LuaVoxelManip::l_update_liquids(lua_State *L)
@@ -193,8 +199,8 @@ int LuaVoxelManip::l_set_lighting(lua_State *L)
 {
 	LuaVoxelManip *o = checkObject<LuaVoxelManip>(L, 1);
 	if (!o->is_mapgen_vm) {
-		warningstream << "VoxelManip:set_lighting called for a non-mapgen "
-			"VoxelManip object" << std::endl;
+		log_deprecated(L, "set_lighting called for a non-mapgen "
+			"VoxelManip object");
 		return 0;
 	}
 

+ 93 - 0
src/script/scripting_emerge.cpp

@@ -0,0 +1,93 @@
+/*
+Minetest
+Copyright (C) 2022 sfan5 <sfan5@live.de>
+
+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 "scripting_emerge.h"
+#include "emerge_internal.h"
+#include "server.h"
+#include "settings.h"
+#include "cpp_api/s_internal.h"
+#include "common/c_packer.h"
+#include "lua_api/l_areastore.h"
+#include "lua_api/l_base.h"
+#include "lua_api/l_craft.h"
+#include "lua_api/l_env.h"
+#include "lua_api/l_item.h"
+#include "lua_api/l_itemstackmeta.h"
+#include "lua_api/l_mapgen.h"
+#include "lua_api/l_noise.h"
+#include "lua_api/l_server.h"
+#include "lua_api/l_util.h"
+#include "lua_api/l_vmanip.h"
+#include "lua_api/l_settings.h"
+
+extern "C" {
+#include <lualib.h>
+}
+
+EmergeScripting::EmergeScripting(EmergeThread *parent):
+		ScriptApiBase(ScriptingType::Emerge)
+{
+	setGameDef(parent->m_server);
+	setEmergeThread(parent);
+
+	SCRIPTAPI_PRECHECKHEADER
+
+	if (g_settings->getBool("secure.enable_security"))
+		initializeSecurity();
+
+	lua_getglobal(L, "core");
+	int top = lua_gettop(L);
+
+	InitializeModApi(L, top);
+
+	auto *data = ModApiBase::getServer(L)->m_lua_globals_data.get();
+	assert(data);
+	script_unpack(L, data);
+	lua_setfield(L, top, "transferred_globals");
+
+	lua_pop(L, 1);
+
+	// Push builtin initialization type
+	lua_pushstring(L, "emerge");
+	lua_setglobal(L, "INIT");
+}
+
+void EmergeScripting::InitializeModApi(lua_State *L, int top)
+{
+	// Register reference classes (userdata)
+	ItemStackMetaRef::Register(L);
+	LuaAreaStore::Register(L);
+	LuaItemStack::Register(L);
+	LuaPerlinNoise::Register(L);
+	LuaPerlinNoiseMap::Register(L);
+	LuaPseudoRandom::Register(L);
+	LuaPcgRandom::Register(L);
+	LuaSecureRandom::Register(L);
+	LuaVoxelManip::Register(L);
+	LuaSettings::Register(L);
+
+	// Initialize mod api modules
+	ModApiCraft::InitializeAsync(L, top);
+	ModApiEnvVM::InitializeEmerge(L, top);
+	ModApiItem::InitializeAsync(L, top);
+	ModApiMapgen::InitializeEmerge(L, top);
+	ModApiServer::InitializeAsync(L, top);
+	ModApiUtil::InitializeAsync(L, top);
+	// TODO ^ these should also be renamed to InitializeRO or such
+}

+ 37 - 0
src/script/scripting_emerge.h

@@ -0,0 +1,37 @@
+/*
+Minetest
+Copyright (C) 2022 sfan5 <sfan5@live.de>
+
+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 "cpp_api/s_base.h"
+#include "cpp_api/s_mapgen.h"
+#include "cpp_api/s_security.h"
+
+class EmergeThread;
+
+class EmergeScripting:
+		virtual public ScriptApiBase,
+		public ScriptApiMapgen,
+		public ScriptApiSecurity
+{
+public:
+	EmergeScripting(EmergeThread *parent);
+
+private:
+	void InitializeModApi(lua_State *L, int top);
+};

+ 2 - 0
src/server.h

@@ -418,6 +418,8 @@ public:
 
 	// Lua files registered for init of async env, pair of modname + path
 	std::vector<std::pair<std::string, std::string>> m_async_init_files;
+	// Identical but for mapgen env
+	std::vector<std::pair<std::string, std::string>> m_mapgen_init_files;
 
 	// Data transferred into other Lua envs at init time
 	std::unique_ptr<PackedValue> m_lua_globals_data;