From 8af793777cab0ae24fafb6292670abe15d7ca6d5 Mon Sep 17 00:00:00 2001 From: Lars Mueller Date: Sun, 12 Nov 2023 15:28:29 +0100 Subject: [PATCH] Allow managing object observers --- doc/lua_api.md | 14 ++++ games/devtest/mods/testentities/init.lua | 1 + games/devtest/mods/testentities/observers.lua | 37 +++++++++ src/script/lua_api/l_object.cpp | 81 +++++++++++++++++++ src/script/lua_api/l_object.h | 6 ++ src/server/activeobjectmgr.cpp | 9 ++- src/server/activeobjectmgr.h | 7 +- src/server/serveractiveobject.cpp | 5 ++ src/server/serveractiveobject.h | 8 ++ src/serverenvironment.cpp | 21 ++--- src/unittest/test_serveractiveobjectmgr.cpp | 4 +- 11 files changed, 177 insertions(+), 16 deletions(-) create mode 100644 games/devtest/mods/testentities/observers.lua diff --git a/doc/lua_api.md b/doc/lua_api.md index ab4ff52cc9eb5..e868877d87888 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -7583,6 +7583,20 @@ child will follow movement and rotation of that bone. * `get_bone_overrides()`: returns all bone overrides as table `{[bonename] = override, ...}` * `set_properties(object property table)` * `get_properties()`: returns a table of all object properties +* `set_observers(observers)`: sets observers (players this object is sent to) + * If `observers` is `nil`, the object's observers are "unmanaged": + The object is sent to all players (in proximity). This is the default. + * `observers` is a "set" of player names: `{[player name] = true, [other player name] = true, ...}` + * A player is an observer if and only if the player's name is a key in the `observers` table. + * The values are irrelevant. To prevent the mistake of expecting `[player name] = false` + to "uninstall" a player as an observer, they must be truthy. + * If players are managed, they always need to have themselves as observers. + * Attachments: + * If an object's observers are managed, the observers of all children need to be managed too. + * The observers of children need to be a subset of the observers of parents. +* `get_observers()`: + * returns `nil` if the observers are unmanaged + * returns a table with all observer names as keys and `true` values (a "set") otherwise * `is_player()`: returns true for players, false otherwise * `get_nametag_attributes()` * returns a table with the attributes of the nametag of an object diff --git a/games/devtest/mods/testentities/init.lua b/games/devtest/mods/testentities/init.lua index 6e0f8acb8d312..0341646fe7484 100644 --- a/games/devtest/mods/testentities/init.lua +++ b/games/devtest/mods/testentities/init.lua @@ -1,3 +1,4 @@ dofile(minetest.get_modpath("testentities").."/visuals.lua") +dofile(minetest.get_modpath("testentities").."/observers.lua") dofile(minetest.get_modpath("testentities").."/selectionbox.lua") dofile(minetest.get_modpath("testentities").."/armor.lua") diff --git a/games/devtest/mods/testentities/observers.lua b/games/devtest/mods/testentities/observers.lua new file mode 100644 index 0000000000000..6dbbeba422bf4 --- /dev/null +++ b/games/devtest/mods/testentities/observers.lua @@ -0,0 +1,37 @@ +local function player_names_excluding(exclude_player_name) + local player_names = {} + for _, player in ipairs(minetest.get_connected_players()) do + player_names[player:get_player_name()] = true + end + player_names[exclude_player_name] = nil + return player_names +end + +minetest.register_entity("testentities:observable", { + initial_properties = { + visual = "sprite", + textures = { "testentities_sprite.png" }, + static_save = false, + infotext = "Punch to set observers to anyone but you" + }, + on_activate = function(self) + self.object:set_armor_groups({punch_operable = 1}) + assert(self.object:get_observers() == nil) + -- Using a value of `false` in the table should error. + assert(not pcall(self.object, self.object.set_observers, self.object, {test = false})) + end, + on_punch = function(self, puncher) + local puncher_name = puncher:get_player_name() + local observers = player_names_excluding(puncher_name) + self.object:set_observers(observers) + local got_observers = self.object:get_observers() + for name in pairs(observers) do + assert(got_observers[name]) + end + for name in pairs(got_observers) do + assert(observers[name]) + end + self.object:set_properties({infotext = "Excluding " .. puncher_name}) + return true + end +}) diff --git a/src/script/lua_api/l_object.cpp b/src/script/lua_api/l_object.cpp index be091a6e0ef81..090ec416901a4 100644 --- a/src/script/lua_api/l_object.cpp +++ b/src/script/lua_api/l_object.cpp @@ -816,6 +816,85 @@ int ObjectRef::l_get_properties(lua_State *L) return 1; } +// set_observers(self, observers) +int ObjectRef::l_set_observers(lua_State *L) +{ + GET_ENV_PTR; + ObjectRef *ref = checkObject(L, 1); + ServerActiveObject *sao = getobject(ref); + if (sao == nullptr) + throw LuaError("Invalid ObjectRef"); + + // Reset object to "unmanaged" (sent to everyone)? + if (lua_isnoneornil(L, 2)) { + ServerActiveObject *parent = sao->getParent(); + if (parent != nullptr && !parent->m_observer_names.has_value()) + throw LuaError("Child observers need to be managed if parent observers are managed"); + sao->m_observer_names.reset(); + return 0; + } + + std::unordered_set observer_names; + lua_pushnil(L); + while (lua_next(L, 2) != 0) { + observer_names.insert(readParam(L, -2)); + if (!lua_toboolean(L, -1)) // falsy value? + throw LuaError("Values in the `observers` table need to be truthy"); + lua_pop(L, 1); // pop value, keep key + } + + RemotePlayer *player = getplayer(ref); + if (player != nullptr) { + if (observer_names.find(player->getName()) == observer_names.end()) + throw LuaError("Players need to observe themselves"); + } + + // Check attachments + ServerActiveObject *parent = sao->getParent(); + if (parent != nullptr) { + for (auto name : observer_names) { + if (!parent->isObservedBy(name)) + throw LuaError("Child observers not a subset of parent observers"); + } + } + const std::unordered_set child_ids = sao->getAttachmentChildIds(); + for (auto child_id : child_ids) { + ServerActiveObject *child = env->getActiveObject(child_id); + if (!child->m_observer_names.has_value()) + throw LuaError("Child observers need to be managed if parent observers are managed"); + for (auto name : child->m_observer_names.value()) { + if (!sao->isObservedBy(name)) + throw LuaError("Child observers not a subset of parent observers"); + } + } + sao->m_observer_names = observer_names; + return 0; +} + +// get_observers(self) +int ObjectRef::l_get_observers(lua_State *L) +{ + NO_MAP_LOCK_REQUIRED; + ObjectRef *ref = checkObject(L, 1); + ServerActiveObject *sao = getobject(ref); + if (sao == nullptr) + throw LuaError("invalid ObjectRef"); + + const auto &observer_names = sao->m_observer_names; + if (!observer_names.has_value()) { + lua_pushnil(L); + return 1; + } + + // Push set of observers {[name] = true} + lua_createtable(L, 0, observer_names.value().size()); + for (auto name : observer_names.value()) { + lua_pushboolean(L, true); + lua_setfield(L, -2, name.c_str()); + } + return 1; +} + // is_player(self) int ObjectRef::l_is_player(lua_State *L) { @@ -2625,6 +2704,8 @@ luaL_Reg ObjectRef::methods[] = { luamethod(ObjectRef, get_properties), luamethod(ObjectRef, set_nametag_attributes), luamethod(ObjectRef, get_nametag_attributes), + luamethod(ObjectRef, set_observers), + luamethod(ObjectRef, get_observers), luamethod_aliased(ObjectRef, set_velocity, setvelocity), luamethod_aliased(ObjectRef, add_velocity, add_player_velocity), diff --git a/src/script/lua_api/l_object.h b/src/script/lua_api/l_object.h index 4465a823f02c3..4dd282a60c694 100644 --- a/src/script/lua_api/l_object.h +++ b/src/script/lua_api/l_object.h @@ -157,6 +157,12 @@ class ObjectRef : public ModApiBase { // get_properties(self) static int l_get_properties(lua_State *L); + // set_observers(self, observers) + static int l_set_observers(lua_State *L); + + // get_observers(self) + static int l_get_observers(lua_State *L); + // is_player(self) static int l_is_player(lua_State *L); diff --git a/src/server/activeobjectmgr.cpp b/src/server/activeobjectmgr.cpp index 1b3376ae6f5eb..11c8781df96bd 100644 --- a/src/server/activeobjectmgr.cpp +++ b/src/server/activeobjectmgr.cpp @@ -156,8 +156,10 @@ void ActiveObjectMgr::getObjectsInArea(const aabb3f &box, } } -void ActiveObjectMgr::getAddedActiveObjectsAroundPos(const v3f &player_pos, f32 radius, - f32 player_radius, std::set ¤t_objects, +void ActiveObjectMgr::getAddedActiveObjectsAroundPos( + const v3f &player_pos, const std::string &player_name, + f32 radius, f32 player_radius, + std::set ¤t_objects, std::queue &added_objects) { /* @@ -186,6 +188,9 @@ void ActiveObjectMgr::getAddedActiveObjectsAroundPos(const v3f &player_pos, f32 } else if (distance_f > radius) continue; + if (!object->isObservedBy(player_name)) + continue; + // Discard if already on current_objects auto n = current_objects.find(id); if (n != current_objects.end()) diff --git a/src/server/activeobjectmgr.h b/src/server/activeobjectmgr.h index 5d333c2328b1f..48f7782935820 100644 --- a/src/server/activeobjectmgr.h +++ b/src/server/activeobjectmgr.h @@ -44,9 +44,10 @@ class ActiveObjectMgr final : public ::ActiveObjectMgr void getObjectsInArea(const aabb3f &box, std::vector &result, std::function include_obj_cb); - - void getAddedActiveObjectsAroundPos(const v3f &player_pos, f32 radius, - f32 player_radius, std::set ¤t_objects, + void getAddedActiveObjectsAroundPos( + const v3f &player_pos, const std::string &player_name, + f32 radius, f32 player_radius, + std::set ¤t_objects, std::queue &added_objects); }; } // namespace server diff --git a/src/server/serveractiveobject.cpp b/src/server/serveractiveobject.cpp index fb09464cfbb48..4dc301b8dbe4d 100644 --- a/src/server/serveractiveobject.cpp +++ b/src/server/serveractiveobject.cpp @@ -95,3 +95,8 @@ InventoryLocation ServerActiveObject::getInventoryLocation() const { return InventoryLocation(); } + +bool ServerActiveObject::isObservedBy(const std::string &player_name) const +{ + return !m_observer_names.has_value() || m_observer_names.value().count(player_name) > 0; +} diff --git a/src/server/serveractiveobject.h b/src/server/serveractiveobject.h index a42096f7bd394..5728c8ba6ef5b 100644 --- a/src/server/serveractiveobject.h +++ b/src/server/serveractiveobject.h @@ -21,6 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include +#include #include "irrlichttypes_bloated.h" #include "activeobject.h" #include "itemgroup.h" @@ -235,6 +236,13 @@ class ServerActiveObject : public ActiveObject */ v3s16 m_static_block = v3s16(1337,1337,1337); + bool isObservedBy(const std::string &player_name) const; + + /* + Names of players to whom the object is to be sent + */ + std::optional> m_observer_names; + protected: virtual void onMarkedForDeactivation() {} virtual void onMarkedForRemoval() {} diff --git a/src/serverenvironment.cpp b/src/serverenvironment.cpp index a715ec8015910..2f2eb20d250ec 100644 --- a/src/serverenvironment.cpp +++ b/src/serverenvironment.cpp @@ -1701,8 +1701,10 @@ void ServerEnvironment::getAddedActiveObjects(PlayerSAO *playersao, s16 radius, if (player_radius_f < 0.0f) player_radius_f = 0.0f; - m_ao_manager.getAddedActiveObjectsAroundPos(playersao->getBasePosition(), radius_f, - player_radius_f, current_objects, added_objects); + m_ao_manager.getAddedActiveObjectsAroundPos( + playersao->getBasePosition(), playersao->getPlayer()->getName(), + radius_f, player_radius_f, + current_objects, added_objects); } /* @@ -1719,6 +1721,9 @@ void ServerEnvironment::getRemovedActiveObjects(PlayerSAO *playersao, s16 radius if (player_radius_f < 0) player_radius_f = 0; + + const std::string player_name = playersao->getPlayer()->getName(); + /* Go through current_objects; object is removed if: - object is not found in m_active_objects (this is actually an @@ -1743,14 +1748,12 @@ void ServerEnvironment::getRemovedActiveObjects(PlayerSAO *playersao, s16 radius } f32 distance_f = object->getBasePosition().getDistanceFrom(playersao->getBasePosition()); - if (object->getType() == ACTIVEOBJECT_TYPE_PLAYER) { - if (distance_f <= player_radius_f || player_radius_f == 0) - continue; - } else if (distance_f <= radius_f) - continue; + bool inRange = object->getType() == ACTIVEOBJECT_TYPE_PLAYER + ? distance_f <= player_radius_f || player_radius_f == 0 + : distance_f <= radius_f; - // Object is no longer visible - removed_objects.push(id); + if (!inRange || !object->isObservedBy(player_name)) + removed_objects.push(id); // out of range or not observed anymore } } diff --git a/src/unittest/test_serveractiveobjectmgr.cpp b/src/unittest/test_serveractiveobjectmgr.cpp index 3e57eb99a7b06..f23074341ce88 100644 --- a/src/unittest/test_serveractiveobjectmgr.cpp +++ b/src/unittest/test_serveractiveobjectmgr.cpp @@ -175,12 +175,12 @@ void TestServerActiveObjectMgr::testGetAddedActiveObjectsAroundPos() std::queue result; std::set cur_objects; - saomgr.getAddedActiveObjectsAroundPos(v3f(), 100, 50, cur_objects, result); + saomgr.getAddedActiveObjectsAroundPos(v3f(), "singleplayer", 100, 50, cur_objects, result); UASSERTCMP(int, ==, result.size(), 1); result = std::queue(); cur_objects.clear(); - saomgr.getAddedActiveObjectsAroundPos(v3f(), 740, 50, cur_objects, result); + saomgr.getAddedActiveObjectsAroundPos(v3f(), "singleplayer", 740, 50, cur_objects, result); UASSERTCMP(int, ==, result.size(), 2); saomgr.clear();