diff --git a/builtin/game/misc_s.lua b/builtin/game/misc_s.lua index 67a0ec684..93d2bafa8 100644 --- a/builtin/game/misc_s.lua +++ b/builtin/game/misc_s.lua @@ -8,10 +8,9 @@ -- Misc. API functions -- +-- This must match the implementation in src/script/common/c_converter.h function core.hash_node_position(pos) - return (pos.z + 32768) * 65536 * 65536 - + (pos.y + 32768) * 65536 - + pos.x + 32768 + return (pos.z + 0x8000) * 0x100000000 + (pos.y + 0x8000) * 0x10000 + (pos.x + 0x8000) end diff --git a/builtin/game/register.lua b/builtin/game/register.lua index f0a6cb49a..cc58cad31 100644 --- a/builtin/game/register.lua +++ b/builtin/game/register.lua @@ -633,6 +633,17 @@ core.registered_on_player_inventory_actions, core.register_on_player_inventory_a core.registered_allow_player_inventory_actions, core.register_allow_player_inventory_action = make_registration() core.registered_on_rightclickplayers, core.register_on_rightclickplayer = make_registration() core.registered_on_liquid_transformed, core.register_on_liquid_transformed = make_registration() +core.registered_on_mapblocks_changed, core.register_on_mapblocks_changed = make_registration() + +core.register_on_mods_loaded(function() + core.after(0, function() + setmetatable(core.registered_on_mapblocks_changed, { + __newindex = function() + error("on_mapblocks_changed callbacks must be registered at load time") + end, + }) + end) +end) -- -- Compatibility for on_mapgen_init() diff --git a/builtin/profiler/instrumentation.lua b/builtin/profiler/instrumentation.lua index f80314b32..80f1c66af 100644 --- a/builtin/profiler/instrumentation.lua +++ b/builtin/profiler/instrumentation.lua @@ -43,6 +43,7 @@ local register_functions = { register_on_item_eat = 0, register_on_punchplayer = 0, register_on_player_hpchange = 0, + register_on_mapblocks_changed = 0, } --- diff --git a/doc/lua_api.txt b/doc/lua_api.txt index 6f2f6c307..b41189e9c 100644 --- a/doc/lua_api.txt +++ b/doc/lua_api.txt @@ -5372,6 +5372,16 @@ Call these functions only at load time! * `pos_list` is an array of all modified positions. * `node_list` is an array of the old node that was previously at the position with the corresponding index in pos_list. +* `minetest.register_on_mapblocks_changed(function(modified_blocks, modified_block_count))` + * Called soon after any nodes or node metadata have been modified. No + modifications will be missed, but there may be false positives. + * Will never be called more than once per server step. + * `modified_blocks` is the set of modified mapblock position hashes. These + are in the same format as those produced by `minetest.hash_node_position`, + and can be converted to positions with `minetest.get_position_from_hash`. + The set is a table where the keys are hashes and the values are `true`. + * `modified_block_count` is the number of entries in the set. + * Note: callbacks must be registered at mod load time. Setting-related --------------- diff --git a/games/devtest/mods/unittests/misc.lua b/games/devtest/mods/unittests/misc.lua index 3f327edec..10af80585 100644 --- a/games/devtest/mods/unittests/misc.lua +++ b/games/devtest/mods/unittests/misc.lua @@ -147,3 +147,33 @@ local function test_mapgen_edges(cb) minetest.emerge_area(max_edge, max_edge:add(1), emerge_block, max_finished) end unittests.register("test_mapgen_edges", test_mapgen_edges, {map=true, async=true}) + +local finish_test_on_mapblocks_changed +minetest.register_on_mapblocks_changed(function(modified_blocks, modified_block_count) + if finish_test_on_mapblocks_changed then + finish_test_on_mapblocks_changed(modified_blocks, modified_block_count) + finish_test_on_mapblocks_changed = nil + end +end) +local function test_on_mapblocks_changed(cb, player, pos) + local bp1 = (pos / minetest.MAP_BLOCKSIZE):floor() + local bp2 = bp1:add(1) + for _, bp in ipairs({bp1, bp2}) do + -- Make a modification in the block. + local p = bp * minetest.MAP_BLOCKSIZE + minetest.load_area(p) + local meta = minetest.get_meta(p) + meta:set_int("test_on_mapblocks_changed", meta:get_int("test_on_mapblocks_changed") + 1) + end + finish_test_on_mapblocks_changed = function(modified_blocks, modified_block_count) + if modified_block_count < 2 then + return cb("Expected at least two mapblocks to be recorded as modified") + end + if not modified_blocks[minetest.hash_node_position(bp1)] or + not modified_blocks[minetest.hash_node_position(bp2)] then + return cb("The expected mapblocks were not recorded as modified") + end + cb() + end +end +unittests.register("test_on_mapblocks_changed", test_on_mapblocks_changed, {map=true, async=true}) diff --git a/src/script/common/c_converter.h b/src/script/common/c_converter.h index 2af726d16..f1e4e47ec 100644 --- a/src/script/common/c_converter.h +++ b/src/script/common/c_converter.h @@ -121,3 +121,12 @@ void warn_if_field_exists(lua_State *L, int table, size_t write_array_slice_float(lua_State *L, int table_index, float *data, v3u16 data_size, v3u16 slice_offset, v3u16 slice_size); + +// This must match the implementation in builtin/game/misc_s.lua +// Note that this returns a floating point result as Lua integers are 32-bit +inline lua_Number hash_node_position(v3s16 pos) +{ + return (((s64)pos.Z + 0x8000L) << 32) + | (((s64)pos.Y + 0x8000L) << 16) + | ((s64)pos.X + 0x8000L); +} diff --git a/src/script/cpp_api/s_env.cpp b/src/script/cpp_api/s_env.cpp index e49113405..3cbb13cd2 100644 --- a/src/script/cpp_api/s_env.cpp +++ b/src/script/cpp_api/s_env.cpp @@ -299,3 +299,36 @@ void ScriptApiEnv::on_liquid_transformed( runCallbacks(2, RUN_CALLBACKS_MODE_FIRST); } + +void ScriptApiEnv::on_mapblocks_changed(const std::unordered_set &set) +{ + SCRIPTAPI_PRECHECKHEADER + + // Get core.registered_on_mapblocks_changed + lua_getglobal(L, "core"); + lua_getfield(L, -1, "registered_on_mapblocks_changed"); + luaL_checktype(L, -1, LUA_TTABLE); + lua_remove(L, -2); + + // Convert the set to a set of position hashes + lua_createtable(L, 0, set.size()); + for(const v3s16 &p : set) { + lua_pushnumber(L, hash_node_position(p)); + lua_pushboolean(L, true); + lua_rawset(L, -3); + } + lua_pushinteger(L, set.size()); + + runCallbacks(2, RUN_CALLBACKS_MODE_FIRST); +} + +bool ScriptApiEnv::has_on_mapblocks_changed() +{ + SCRIPTAPI_PRECHECKHEADER + + // Get core.registered_on_mapblocks_changed + lua_getglobal(L, "core"); + lua_getfield(L, -1, "registered_on_mapblocks_changed"); + luaL_checktype(L, -1, LUA_TTABLE); + return lua_objlen(L, -1) > 0; +} diff --git a/src/script/cpp_api/s_env.h b/src/script/cpp_api/s_env.h index 9a50a01cc..bc4c4cd4d 100644 --- a/src/script/cpp_api/s_env.h +++ b/src/script/cpp_api/s_env.h @@ -22,6 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "cpp_api/s_base.h" #include "irr_v3d.h" #include "mapnode.h" +#include #include class ServerEnvironment; @@ -48,5 +49,11 @@ public: // Called after liquid transform changes void on_liquid_transformed(const std::vector> &list); + // Called after mapblock changes + void on_mapblocks_changed(const std::unordered_set &set); + + // Determines whether there are any on_mapblocks_changed callbacks + bool has_on_mapblocks_changed(); + void initializeEnvironment(ServerEnvironment *env); }; diff --git a/src/serverenvironment.cpp b/src/serverenvironment.cpp index 9dd5ba621..aa6ba2f96 100644 --- a/src/serverenvironment.cpp +++ b/src/serverenvironment.cpp @@ -381,6 +381,18 @@ void ActiveBlockList::update(std::vector &active_players, m_list = std::move(newlist); } +/* + OnMapblocksChangedReceiver +*/ + +void OnMapblocksChangedReceiver::onMapEditEvent(const MapEditEvent &event) +{ + assert(receiving); + for (const v3s16 &p : event.modified_blocks) { + modified_blocks.insert(p); + } +} + /* ServerEnvironment */ @@ -476,6 +488,11 @@ void ServerEnvironment::init() m_player_database = openPlayerDatabase(player_backend_name, m_path_world, conf); m_auth_database = openAuthDatabase(auth_backend_name, m_path_world, conf); + + if (m_map && m_script->has_on_mapblocks_changed()) { + m_map->addEventReceiver(&m_on_mapblocks_changed_receiver); + m_on_mapblocks_changed_receiver.receiving = true; + } } ServerEnvironment::~ServerEnvironment() @@ -1570,6 +1587,14 @@ void ServerEnvironment::step(float dtime) // Send outdated detached inventories m_server->sendDetachedInventories(PEER_ID_INEXISTENT, true); + // Notify mods of modified mapblocks + if (m_on_mapblocks_changed_receiver.receiving && + !m_on_mapblocks_changed_receiver.modified_blocks.empty()) { + std::unordered_set modified_blocks; + std::swap(modified_blocks, m_on_mapblocks_changed_receiver.modified_blocks); + m_script->on_mapblocks_changed(modified_blocks); + } + const auto end_time = porting::getTimeUs(); m_step_time_counter->increment(end_time - start_time); } diff --git a/src/serverenvironment.h b/src/serverenvironment.h index 5c4b23f40..bb40a33ce 100644 --- a/src/serverenvironment.h +++ b/src/serverenvironment.h @@ -21,7 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "activeobject.h" #include "environment.h" -#include "mapnode.h" +#include "map.h" #include "settings.h" #include "server/activeobjectmgr.h" #include "util/numeric.h" @@ -30,9 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include class IGameDef; -class ServerMap; struct GameParams; -class MapBlock; class RemotePlayer; class PlayerDatabase; class AuthDatabase; @@ -193,6 +191,16 @@ public: std::set m_forceloaded_list; }; +/* + ServerEnvironment::m_on_mapblocks_changed_receiver +*/ +struct OnMapblocksChangedReceiver : public MapEventReceiver { + std::unordered_set modified_blocks; + bool receiving = false; + + void onMapEditEvent(const MapEditEvent &event) override; +}; + /* Operation mode for ServerEnvironment::clearObjects() */ @@ -455,6 +463,8 @@ private: Server *m_server; // Active Object Manager server::ActiveObjectMgr m_ao_manager; + // on_mapblocks_changed map event receiver + OnMapblocksChangedReceiver m_on_mapblocks_changed_receiver; // World path const std::string m_path_world; // Outgoing network message buffer for active objects