Allow managing object observers

-----

Co-authored-by: sfan5 <sfan5@live.de>
Co-authored-by: SmallJoker <SmallJoker@users.noreply.github.com>
This commit is contained in:
Lars Mueller 2023-11-12 15:28:29 +01:00 committed by sfan5
parent cc8e7a569e
commit 6874c358ea
15 changed files with 284 additions and 22 deletions

@ -7986,6 +7986,29 @@ child will follow movement and rotation of that bone.
* `get_bone_overrides()`: returns all bone overrides as table `{[bonename] = override, ...}` * `get_bone_overrides()`: returns all bone overrides as table `{[bonename] = override, ...}`
* `set_properties(object property table)` * `set_properties(object property table)`
* `get_properties()`: returns a table of all object properties * `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 * `is_player()`: returns true for players, false otherwise
* `get_nametag_attributes()` * `get_nametag_attributes()`
* returns a table with the attributes of the nametag of an object * returns a table with the attributes of the nametag of an object

@ -1,4 +1,5 @@
dofile(minetest.get_modpath("testentities").."/visuals.lua") 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").."/selectionbox.lua")
dofile(minetest.get_modpath("testentities").."/armor.lua") dofile(minetest.get_modpath("testentities").."/armor.lua")
dofile(minetest.get_modpath("testentities").."/pointable.lua") dofile(minetest.get_modpath("testentities").."/pointable.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
})

@ -19,7 +19,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "localplayer.h" #include "localplayer.h"
#include <cmath> #include <cmath>
#include <string>
#include "mtevent.h" #include "mtevent.h"
#include "collision.h" #include "collision.h"
#include "nodedef.h" #include "nodedef.h"

@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "porting.h" // strlcpy #include "porting.h" // strlcpy
Player::Player(const std::string name, IItemDefManager *idef): Player::Player(const std::string &name, IItemDefManager *idef):
inventory(idef) inventory(idef)
{ {
m_name = name; m_name = name;

@ -27,6 +27,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "common/c_converter.h" #include "common/c_converter.h"
#include "common/c_content.h" #include "common/c_content.h"
#include "log.h" #include "log.h"
#include "player.h"
#include "server/serveractiveobject.h"
#include "tool.h" #include "tool.h"
#include "remoteplayer.h" #include "remoteplayer.h"
#include "server.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/player_sao.h"
#include "server/serverinventorymgr.h" #include "server/serverinventorymgr.h"
#include "server/unit_sao.h" #include "server/unit_sao.h"
#include "util/string.h"
using object_t = ServerActiveObject::object_t; using object_t = ServerActiveObject::object_t;
@ -837,6 +840,85 @@ int ObjectRef::l_get_properties(lua_State *L)
return 1; return 1;
} }
// set_observers(self, observers)
int ObjectRef::l_set_observers(lua_State *L)
{
GET_ENV_PTR;
ObjectRef *ref = checkObject<ObjectRef>(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<std::string> observer_names;
lua_pushnil(L);
while (lua_next(L, 2) != 0) {
std::string name = readParam<std::string>(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<typename F>
static int get_observers(lua_State *L, F observer_getter)
{
ObjectRef *ref = ObjectRef::checkObject<ObjectRef>(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) // is_player(self)
int ObjectRef::l_is_player(lua_State *L) int ObjectRef::l_is_player(lua_State *L)
{ {
@ -2676,6 +2758,9 @@ luaL_Reg ObjectRef::methods[] = {
luamethod(ObjectRef, get_properties), luamethod(ObjectRef, get_properties),
luamethod(ObjectRef, set_nametag_attributes), luamethod(ObjectRef, set_nametag_attributes),
luamethod(ObjectRef, get_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, set_velocity, setvelocity),
luamethod_aliased(ObjectRef, add_velocity, add_player_velocity), luamethod_aliased(ObjectRef, add_velocity, add_player_velocity),

@ -163,6 +163,15 @@ private:
// get_properties(self) // get_properties(self)
static int l_get_properties(lua_State *L); 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) // is_player(self)
static int l_is_player(lua_State *L); static int l_is_player(lua_State *L);

@ -116,6 +116,8 @@ void *ServerThread::run()
m_server->setAsyncFatalError(e.what()); m_server->setAsyncFatalError(e.what());
} catch (LuaError &e) { } catch (LuaError &e) {
m_server->setAsyncFatalError(e); m_server->setAsyncFatalError(e);
} catch (ModError &e) {
m_server->setAsyncFatalError(e.what());
} }
float dtime = 0.0f; float dtime = 0.0f;
@ -142,6 +144,8 @@ void *ServerThread::run()
m_server->setAsyncFatalError(e.what()); m_server->setAsyncFatalError(e.what());
} catch (LuaError &e) { } catch (LuaError &e) {
m_server->setAsyncFatalError(e); m_server->setAsyncFatalError(e);
} catch (ModError &e) {
m_server->setAsyncFatalError(e.what());
} }
dtime = 1e-6f * (porting::getTimeUs() - t0); 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"<<std::endl; //infostream<<"Server: Checking added and deleted active objects"<<std::endl;
MutexAutoLock envlock(m_env_mutex); MutexAutoLock envlock(m_env_mutex);
// This guarantees that each object recomputes its cache only once per server step,
// unless get_effective_observers is called.
// If we were to update observer sets eagerly in set_observers instead,
// the total costs of calls to set_observers could theoretically be higher.
m_env->invalidateActiveObjectObserverCaches();
{ {
ClientInterface::AutoLock clientlock(m_clients); ClientInterface::AutoLock clientlock(m_clients);
const RemoteClientMap &clients = m_clients.getClientList(); const RemoteClientMap &clients = m_clients.getClientList();

@ -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, void ActiveObjectMgr::getObjectsInsideRadius(const v3f &pos, float radius,
std::vector<ServerActiveObject *> &result, std::vector<ServerActiveObject *> &result,
std::function<bool(ServerActiveObject *obj)> include_obj_cb) std::function<bool(ServerActiveObject *obj)> include_obj_cb)
@ -153,15 +163,18 @@ void ActiveObjectMgr::getObjectsInArea(const aabb3f &box,
} }
} }
void ActiveObjectMgr::getAddedActiveObjectsAroundPos(v3f player_pos, f32 radius, void ActiveObjectMgr::getAddedActiveObjectsAroundPos(
f32 player_radius, const std::set<u16> &current_objects, const v3f &player_pos, const std::string &player_name,
f32 radius, f32 player_radius,
const std::set<u16> &current_objects,
std::vector<u16> &added_objects) std::vector<u16> &added_objects)
{ {
/* /*
Go through the object list, Go through the object list,
- discard removed/deactivated objects, - discard removed/deactivated objects,
- discard objects that are too far away, - 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 - add remaining objects to added_objects
*/ */
for (auto &ao_it : m_active_objects.iter()) { 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) } else if (distance_f > radius)
continue; continue;
if (!object->isEffectivelyObservedBy(player_name))
continue;
// Discard if already on current_objects // Discard if already on current_objects
auto n = current_objects.find(id); auto n = current_objects.find(id);
if (n != current_objects.end()) if (n != current_objects.end())

@ -38,15 +38,18 @@ public:
bool registerObject(std::unique_ptr<ServerActiveObject> obj) override; bool registerObject(std::unique_ptr<ServerActiveObject> obj) override;
void removeObject(u16 id) override; void removeObject(u16 id) override;
void invalidateActiveObjectObserverCaches();
void getObjectsInsideRadius(const v3f &pos, float radius, void getObjectsInsideRadius(const v3f &pos, float radius,
std::vector<ServerActiveObject *> &result, std::vector<ServerActiveObject *> &result,
std::function<bool(ServerActiveObject *obj)> include_obj_cb); std::function<bool(ServerActiveObject *obj)> include_obj_cb);
void getObjectsInArea(const aabb3f &box, void getObjectsInArea(const aabb3f &box,
std::vector<ServerActiveObject *> &result, std::vector<ServerActiveObject *> &result,
std::function<bool(ServerActiveObject *obj)> include_obj_cb); std::function<bool(ServerActiveObject *obj)> include_obj_cb);
void getAddedActiveObjectsAroundPos(
void getAddedActiveObjectsAroundPos(v3f player_pos, f32 radius, const v3f &player_pos, const std::string &player_name,
f32 player_radius, const std::set<u16> &current_objects, f32 radius, f32 player_radius,
const std::set<u16> &current_objects,
std::vector<u16> &added_objects); std::vector<u16> &added_objects);
}; };
} // namespace server } // namespace server

@ -18,11 +18,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
*/ */
#include "serveractiveobject.h" #include "serveractiveobject.h"
#include <fstream>
#include "inventory.h" #include "inventory.h"
#include "inventorymanager.h" #include "inventorymanager.h"
#include "constants.h" // BS #include "constants.h" // BS
#include "log.h"
ServerActiveObject::ServerActiveObject(ServerEnvironment *env, v3f pos): ServerActiveObject::ServerActiveObject(ServerEnvironment *env, v3f pos):
ActiveObject(0), ActiveObject(0),
@ -95,3 +93,48 @@ InventoryLocation ServerActiveObject::getInventoryLocation() const
{ {
return InventoryLocation(); 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<std::string>();
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;
}

@ -21,6 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <cassert> #include <cassert>
#include <unordered_set> #include <unordered_set>
#include <optional>
#include "irrlichttypes_bloated.h" #include "irrlichttypes_bloated.h"
#include "activeobject.h" #include "activeobject.h"
#include "itemgroup.h" #include "itemgroup.h"
@ -236,7 +237,25 @@ public:
*/ */
v3s16 m_static_block = v3s16(1337,1337,1337); 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<std::unordered_set<std::string>>;
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: protected:
// Cached intersection of m_observers of this object and all its parents.
std::optional<Observers> m_effective_observers;
virtual void onMarkedForDeactivation() {} virtual void onMarkedForDeactivation() {}
virtual void onMarkedForRemoval() {} virtual void onMarkedForRemoval() {}

@ -1711,6 +1711,11 @@ u16 ServerEnvironment::addActiveObject(std::unique_ptr<ServerActiveObject> objec
return id; return id;
} }
void ServerEnvironment::invalidateActiveObjectObserverCaches()
{
m_ao_manager.invalidateActiveObjectObserverCaches();
}
/* /*
Finds out what new objects have been added to Finds out what new objects have been added to
inside a radius around a position inside a radius around a position
@ -1726,8 +1731,13 @@ void ServerEnvironment::getAddedActiveObjects(PlayerSAO *playersao, s16 radius,
if (player_radius_f < 0.0f) if (player_radius_f < 0.0f)
player_radius_f = 0.0f; player_radius_f = 0.0f;
m_ao_manager.getAddedActiveObjectsAroundPos(playersao->getBasePosition(), radius_f, if (!playersao->isEffectivelyObservedBy(playersao->getPlayer()->getName()))
player_radius_f, current_objects, added_objects); 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) if (player_radius_f < 0)
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: Go through current_objects; object is removed if:
- object is not found in m_active_objects (this is actually an - object is not found in m_active_objects (this is actually an
error condition; objects should be removed only after all clients error condition; objects should be removed only after all clients
have been informed about removal), or have been informed about removal), or
- object is to be removed or deactivated, 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) { for (u16 id : current_objects) {
ServerActiveObject *object = getActiveObject(id); ServerActiveObject *object = getActiveObject(id);
@ -1768,14 +1785,12 @@ void ServerEnvironment::getRemovedActiveObjects(PlayerSAO *playersao, s16 radius
} }
f32 distance_f = object->getBasePosition().getDistanceFrom(playersao->getBasePosition()); f32 distance_f = object->getBasePosition().getDistanceFrom(playersao->getBasePosition());
if (object->getType() == ACTIVEOBJECT_TYPE_PLAYER) { bool in_range = object->getType() == ACTIVEOBJECT_TYPE_PLAYER
if (distance_f <= player_radius_f || player_radius_f == 0) ? distance_f <= player_radius_f || player_radius_f == 0
continue; : distance_f <= radius_f;
} else if (distance_f <= radius_f)
continue;
// Object is no longer visible if (!in_range || !object->isEffectivelyObservedBy(player_name))
removed_objects.emplace_back(false, id); removed_objects.emplace_back(false, id); // out of range or not observed anymore
} }
} }

@ -277,6 +277,8 @@ public:
*/ */
u16 addActiveObject(std::unique_ptr<ServerActiveObject> object); u16 addActiveObject(std::unique_ptr<ServerActiveObject> object);
void invalidateActiveObjectObserverCaches();
/* /*
Find out what new objects have been added to Find out what new objects have been added to
inside a radius around a position inside a radius around a position

@ -175,12 +175,12 @@ void TestServerActiveObjectMgr::testGetAddedActiveObjectsAroundPos()
std::vector<u16> result; std::vector<u16> result;
std::set<u16> cur_objects; std::set<u16> cur_objects;
saomgr.getAddedActiveObjectsAroundPos(v3f(), 100, 50, cur_objects, result); saomgr.getAddedActiveObjectsAroundPos(v3f(), "singleplayer", 100, 50, cur_objects, result);
UASSERTCMP(int, ==, result.size(), 1); UASSERTCMP(int, ==, result.size(), 1);
result.clear(); result.clear();
cur_objects.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); UASSERTCMP(int, ==, result.size(), 2);
saomgr.clear(); saomgr.clear();