mirror of
https://github.com/minetest/minetest.git
synced 2024-11-26 01:23:46 +01:00
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:
parent
cc8e7a569e
commit
6874c358ea
@ -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
|
||||
|
@ -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")
|
||||
|
37
games/devtest/mods/testentities/observers.lua
Normal file
37
games/devtest/mods/testentities/observers.lua
Normal file
@ -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 <cmath>
|
||||
#include <string>
|
||||
#include "mtevent.h"
|
||||
#include "collision.h"
|
||||
#include "nodedef.h"
|
||||
|
@ -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;
|
||||
|
@ -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<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)
|
||||
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),
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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"<<std::endl;
|
||||
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);
|
||||
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,
|
||||
std::vector<ServerActiveObject *> &result,
|
||||
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,
|
||||
f32 player_radius, const std::set<u16> ¤t_objects,
|
||||
void ActiveObjectMgr::getAddedActiveObjectsAroundPos(
|
||||
const v3f &player_pos, const std::string &player_name,
|
||||
f32 radius, f32 player_radius,
|
||||
const std::set<u16> ¤t_objects,
|
||||
std::vector<u16> &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())
|
||||
|
@ -38,15 +38,18 @@ public:
|
||||
bool registerObject(std::unique_ptr<ServerActiveObject> obj) override;
|
||||
void removeObject(u16 id) override;
|
||||
|
||||
void invalidateActiveObjectObserverCaches();
|
||||
|
||||
void getObjectsInsideRadius(const v3f &pos, float radius,
|
||||
std::vector<ServerActiveObject *> &result,
|
||||
std::function<bool(ServerActiveObject *obj)> include_obj_cb);
|
||||
void getObjectsInArea(const aabb3f &box,
|
||||
std::vector<ServerActiveObject *> &result,
|
||||
std::function<bool(ServerActiveObject *obj)> include_obj_cb);
|
||||
|
||||
void getAddedActiveObjectsAroundPos(v3f player_pos, f32 radius,
|
||||
f32 player_radius, const std::set<u16> ¤t_objects,
|
||||
void getAddedActiveObjectsAroundPos(
|
||||
const v3f &player_pos, const std::string &player_name,
|
||||
f32 radius, f32 player_radius,
|
||||
const std::set<u16> ¤t_objects,
|
||||
std::vector<u16> &added_objects);
|
||||
};
|
||||
} // namespace server
|
||||
|
@ -18,11 +18,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
*/
|
||||
|
||||
#include "serveractiveobject.h"
|
||||
#include <fstream>
|
||||
#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<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 <unordered_set>
|
||||
#include <optional>
|
||||
#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<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:
|
||||
// Cached intersection of m_observers of this object and all its parents.
|
||||
std::optional<Observers> m_effective_observers;
|
||||
|
||||
virtual void onMarkedForDeactivation() {}
|
||||
virtual void onMarkedForRemoval() {}
|
||||
|
||||
|
@ -1711,6 +1711,11 @@ u16 ServerEnvironment::addActiveObject(std::unique_ptr<ServerActiveObject> 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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,6 +277,8 @@ public:
|
||||
*/
|
||||
u16 addActiveObject(std::unique_ptr<ServerActiveObject> object);
|
||||
|
||||
void invalidateActiveObjectObserverCaches();
|
||||
|
||||
/*
|
||||
Find out what new objects have been added to
|
||||
inside a radius around a position
|
||||
|
@ -175,12 +175,12 @@ void TestServerActiveObjectMgr::testGetAddedActiveObjectsAroundPos()
|
||||
|
||||
std::vector<u16> result;
|
||||
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);
|
||||
|
||||
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();
|
||||
|
Loading…
Reference in New Issue
Block a user