diff --git a/doc/lua_api.md b/doc/lua_api.md index 74728a018..5713e8696 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -7986,6 +7986,29 @@ 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 as governed by server settings. This is the default. + * `observers` is a "set" of player names: `{[player name] = true, [other player name] = true, ...}` + * A set is a table where the keys are the elements of the set (in this case, player names) + and the values are all `true`. + * Attachments: The *effective observers* of an object are made up of + all players who can observe the object *and* are also effective observers + of its parent object (if there is one). + * Players are automatically added to their own observer sets. + Players **must** effectively observe themselves. + * Object activation and deactivation are unaffected by observability. + * Attached sounds do not work correctly and thus should not be used + on objects with managed observers yet. +* `get_observers()`: + * throws an error if the object is invalid + * returns `nil` if the observers are unmanaged + * returns a table with all observer names as keys and `true` values (a "set") otherwise +* `get_effective_observers()`: + * Like `get_observers()`, but returns the "effective" observers, taking into account attachments + * Time complexity: O(nm) + * n: number of observers of the involved entities + * m: number of ancestors along the attachment chain * `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 9ab54f5ab..659febe20 100644 --- a/games/devtest/mods/testentities/init.lua +++ b/games/devtest/mods/testentities/init.lua @@ -1,4 +1,5 @@ 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") dofile(minetest.get_modpath("testentities").."/pointable.lua") diff --git a/games/devtest/mods/testentities/observers.lua b/games/devtest/mods/testentities/observers.lua new file mode 100644 index 000000000..6dbbeba42 --- /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/client/localplayer.cpp b/src/client/localplayer.cpp index 2329dd2c0..aa335e90e 100644 --- a/src/client/localplayer.cpp +++ b/src/client/localplayer.cpp @@ -19,7 +19,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "localplayer.h" #include -#include #include "mtevent.h" #include "collision.h" #include "nodedef.h" diff --git a/src/player.cpp b/src/player.cpp index 021a96db6..e55de5937 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "porting.h" // strlcpy -Player::Player(const std::string name, IItemDefManager *idef): +Player::Player(const std::string &name, IItemDefManager *idef): inventory(idef) { m_name = name; diff --git a/src/script/lua_api/l_object.cpp b/src/script/lua_api/l_object.cpp index c6d4bb71a..89974fb0e 100644 --- a/src/script/lua_api/l_object.cpp +++ b/src/script/lua_api/l_object.cpp @@ -27,6 +27,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "common/c_converter.h" #include "common/c_content.h" #include "log.h" +#include "player.h" +#include "server/serveractiveobject.h" #include "tool.h" #include "remoteplayer.h" #include "server.h" @@ -36,6 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "server/player_sao.h" #include "server/serverinventorymgr.h" #include "server/unit_sao.h" +#include "util/string.h" using object_t = ServerActiveObject::object_t; @@ -837,6 +840,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)) { + sao->m_observers.reset(); + return 0; + } + + std::unordered_set observer_names; + lua_pushnil(L); + while (lua_next(L, 2) != 0) { + std::string name = readParam(L, -2); + if (name.empty()) + throw LuaError("Observer name is empty"); + if (name.size() > PLAYERNAME_SIZE) + throw LuaError("Observer name is too long"); + if (!string_allowed(name, PLAYERNAME_ALLOWED_CHARS)) + throw LuaError("Observer name contains invalid characters"); + if (!lua_toboolean(L, -1)) // falsy value? + throw LuaError("Values in the `observers` table need to be true"); + observer_names.insert(std::move(name)); + lua_pop(L, 1); // pop value, keep key + } + + RemotePlayer *player = getplayer(ref); + if (player != nullptr) { + observer_names.insert(player->getName()); + } + + sao->m_observers = std::move(observer_names); + return 0; +} + +template +static int get_observers(lua_State *L, F observer_getter) +{ + ObjectRef *ref = ObjectRef::checkObject(L, 1); + ServerActiveObject *sao = ObjectRef::getobject(ref); + if (sao == nullptr) + throw LuaError("invalid ObjectRef"); + + const auto observers = observer_getter(sao); + if (!observers) { + lua_pushnil(L); + return 1; + } + // Push set of observers {[name] = true} + lua_createtable(L, 0, observers->size()); + for (auto &name : *observers) { + lua_pushboolean(L, true); + lua_setfield(L, -2, name.c_str()); + } + return 1; +} + +// get_observers(self) +int ObjectRef::l_get_observers(lua_State *L) +{ + NO_MAP_LOCK_REQUIRED; + return get_observers(L, [](auto sao) { return sao->m_observers; }); +} + +// get_effective_observers(self) +int ObjectRef::l_get_effective_observers(lua_State *L) +{ + NO_MAP_LOCK_REQUIRED; + return get_observers(L, [](auto sao) { + // The cache may be outdated, so we always have to recalculate. + return sao->recalculateEffectiveObservers(); + }); +} + // is_player(self) int ObjectRef::l_is_player(lua_State *L) { @@ -2676,6 +2758,9 @@ 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(ObjectRef, get_effective_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 3e6d01201..ace19e1f0 100644 --- a/src/script/lua_api/l_object.h +++ b/src/script/lua_api/l_object.h @@ -163,6 +163,15 @@ private: // 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); + + // get_effective_observers(self) + static int l_get_effective_observers(lua_State *L); + // is_player(self) static int l_is_player(lua_State *L); diff --git a/src/server.cpp b/src/server.cpp index f6268402d..0b0786209 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -116,6 +116,8 @@ void *ServerThread::run() m_server->setAsyncFatalError(e.what()); } catch (LuaError &e) { m_server->setAsyncFatalError(e); + } catch (ModError &e) { + m_server->setAsyncFatalError(e.what()); } float dtime = 0.0f; @@ -142,6 +144,8 @@ void *ServerThread::run() m_server->setAsyncFatalError(e.what()); } catch (LuaError &e) { m_server->setAsyncFatalError(e); + } catch (ModError &e) { + m_server->setAsyncFatalError(e.what()); } dtime = 1e-6f * (porting::getTimeUs() - t0); @@ -781,6 +785,12 @@ void Server::AsyncRunStep(float dtime, bool initial_step) //infostream<<"Server: Checking added and deleted active objects"<invalidateActiveObjectObserverCaches(); + { ClientInterface::AutoLock clientlock(m_clients); const RemoteClientMap &clients = m_clients.getClientList(); diff --git a/src/server/activeobjectmgr.cpp b/src/server/activeobjectmgr.cpp index 983fc0d95..c6f1010ea 100644 --- a/src/server/activeobjectmgr.cpp +++ b/src/server/activeobjectmgr.cpp @@ -118,6 +118,16 @@ void ActiveObjectMgr::removeObject(u16 id) } } +void ActiveObjectMgr::invalidateActiveObjectObserverCaches() +{ + for (auto &active_object : m_active_objects.iter()) { + ServerActiveObject *obj = active_object.second.get(); + if (!obj) + continue; + obj->invalidateEffectiveObservers(); + } +} + void ActiveObjectMgr::getObjectsInsideRadius(const v3f &pos, float radius, std::vector &result, std::function include_obj_cb) @@ -153,15 +163,18 @@ void ActiveObjectMgr::getObjectsInArea(const aabb3f &box, } } -void ActiveObjectMgr::getAddedActiveObjectsAroundPos(v3f player_pos, f32 radius, - f32 player_radius, const std::set ¤t_objects, +void ActiveObjectMgr::getAddedActiveObjectsAroundPos( + const v3f &player_pos, const std::string &player_name, + f32 radius, f32 player_radius, + const std::set ¤t_objects, std::vector &added_objects) { /* Go through the object list, - discard removed/deactivated objects, - discard objects that are too far away, - - discard objects that are found in current_objects. + - discard objects that are found in current_objects, + - discard objects that are not observed by the player. - add remaining objects to added_objects */ for (auto &ao_it : m_active_objects.iter()) { @@ -183,6 +196,9 @@ void ActiveObjectMgr::getAddedActiveObjectsAroundPos(v3f player_pos, f32 radius, } else if (distance_f > radius) continue; + if (!object->isEffectivelyObservedBy(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 dab795e8c..82c0ab3ad 100644 --- a/src/server/activeobjectmgr.h +++ b/src/server/activeobjectmgr.h @@ -38,15 +38,18 @@ public: bool registerObject(std::unique_ptr obj) override; void removeObject(u16 id) override; + void invalidateActiveObjectObserverCaches(); + void getObjectsInsideRadius(const v3f &pos, float radius, std::vector &result, std::function include_obj_cb); void getObjectsInArea(const aabb3f &box, std::vector &result, std::function include_obj_cb); - - void getAddedActiveObjectsAroundPos(v3f player_pos, f32 radius, - f32 player_radius, const std::set ¤t_objects, + void getAddedActiveObjectsAroundPos( + const v3f &player_pos, const std::string &player_name, + f32 radius, f32 player_radius, + const std::set ¤t_objects, std::vector &added_objects); }; } // namespace server diff --git a/src/server/serveractiveobject.cpp b/src/server/serveractiveobject.cpp index fb09464cf..c660b5a69 100644 --- a/src/server/serveractiveobject.cpp +++ b/src/server/serveractiveobject.cpp @@ -18,11 +18,9 @@ with this program; if not, write to the Free Software Foundation, Inc., */ #include "serveractiveobject.h" -#include #include "inventory.h" #include "inventorymanager.h" #include "constants.h" // BS -#include "log.h" ServerActiveObject::ServerActiveObject(ServerEnvironment *env, v3f pos): ActiveObject(0), @@ -95,3 +93,48 @@ InventoryLocation ServerActiveObject::getInventoryLocation() const { return InventoryLocation(); } + +void ServerActiveObject::invalidateEffectiveObservers() +{ + m_effective_observers.reset(); +} + +using Observers = ServerActiveObject::Observers; + +const Observers &ServerActiveObject::getEffectiveObservers() +{ + if (m_effective_observers) // cached + return *m_effective_observers; + + auto parent = getParent(); + if (parent == nullptr) + return *(m_effective_observers = m_observers); + auto parent_observers = parent->getEffectiveObservers(); + if (!parent_observers) // parent is unmanaged + return *(m_effective_observers = m_observers); + if (!m_observers) // we are unmanaged + return *(m_effective_observers = parent_observers); + // Set intersection between parent_observers and m_observers + // Avoid .clear() to free the allocated memory. + m_effective_observers = std::unordered_set(); + for (const auto &observer_name : *m_observers) { + if (parent_observers->count(observer_name) > 0) + (*m_effective_observers)->insert(observer_name); + } + return *m_effective_observers; +} + +const Observers& ServerActiveObject::recalculateEffectiveObservers() +{ + // Invalidate final observers for this object and all of its parents. + for (auto obj = this; obj != nullptr; obj = obj->getParent()) + obj->invalidateEffectiveObservers(); + // getEffectiveObservers will now be forced to recalculate. + return getEffectiveObservers(); +} + +bool ServerActiveObject::isEffectivelyObservedBy(const std::string &player_name) +{ + auto effective_observers = getEffectiveObservers(); + return !effective_observers || effective_observers->count(player_name) > 0; +} diff --git a/src/server/serveractiveobject.h b/src/server/serveractiveobject.h index a94c2df9c..d734b8469 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" @@ -236,7 +237,25 @@ public: */ v3s16 m_static_block = v3s16(1337,1337,1337); + // Names of players to whom the object is to be sent, not considering parents. + using Observers = std::optional>; + Observers m_observers; + + /// Invalidate final observer cache. This needs to be done whenever + /// the observers of this object or any of its ancestors may have changed. + void invalidateEffectiveObservers(); + /// Cache `m_effective_observers` with the names of all observers, + /// also indirect observers (object attachment chain). + const Observers &getEffectiveObservers(); + /// Force a recalculation of final observers (including all parents). + const Observers &recalculateEffectiveObservers(); + /// Whether the object is sent to `player_name` + bool isEffectivelyObservedBy(const std::string &player_name); + protected: + // Cached intersection of m_observers of this object and all its parents. + std::optional m_effective_observers; + virtual void onMarkedForDeactivation() {} virtual void onMarkedForRemoval() {} diff --git a/src/serverenvironment.cpp b/src/serverenvironment.cpp index d19019fff..24e4a587e 100644 --- a/src/serverenvironment.cpp +++ b/src/serverenvironment.cpp @@ -1711,6 +1711,11 @@ u16 ServerEnvironment::addActiveObject(std::unique_ptr objec return id; } +void ServerEnvironment::invalidateActiveObjectObserverCaches() +{ + m_ao_manager.invalidateActiveObjectObserverCaches(); +} + /* Finds out what new objects have been added to inside a radius around a position @@ -1726,8 +1731,13 @@ 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); + if (!playersao->isEffectivelyObservedBy(playersao->getPlayer()->getName())) + throw ModError("Player does not observe itself"); + + m_ao_manager.getAddedActiveObjectsAroundPos( + playersao->getBasePosition(), playersao->getPlayer()->getName(), + radius_f, player_radius_f, + current_objects, added_objects); } /* @@ -1744,13 +1754,20 @@ void ServerEnvironment::getRemovedActiveObjects(PlayerSAO *playersao, s16 radius if (player_radius_f < 0) player_radius_f = 0; + + const std::string &player_name = playersao->getPlayer()->getName(); + + if (!playersao->isEffectivelyObservedBy(player_name)) + throw ModError("Player does not observe itself"); + /* Go through current_objects; object is removed if: - object is not found in m_active_objects (this is actually an error condition; objects should be removed only after all clients have been informed about removal), or - object is to be removed or deactivated, or - - object is too far away + - object is too far away, or + - object is marked as not observable by the client */ for (u16 id : current_objects) { ServerActiveObject *object = getActiveObject(id); @@ -1768,14 +1785,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 in_range = 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.emplace_back(false, id); + if (!in_range || !object->isEffectivelyObservedBy(player_name)) + removed_objects.emplace_back(false, id); // out of range or not observed anymore } } diff --git a/src/serverenvironment.h b/src/serverenvironment.h index e02bd86b2..7a388d21c 100644 --- a/src/serverenvironment.h +++ b/src/serverenvironment.h @@ -277,6 +277,8 @@ public: */ u16 addActiveObject(std::unique_ptr object); + void invalidateActiveObjectObserverCaches(); + /* Find out what new objects have been added to inside a radius around a position diff --git a/src/unittest/test_serveractiveobjectmgr.cpp b/src/unittest/test_serveractiveobjectmgr.cpp index 7f0ca84cb..cbfd1b859 100644 --- a/src/unittest/test_serveractiveobjectmgr.cpp +++ b/src/unittest/test_serveractiveobjectmgr.cpp @@ -175,12 +175,12 @@ void TestServerActiveObjectMgr::testGetAddedActiveObjectsAroundPos() std::vector 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.clear(); 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();