So, I came in trying to fix gameobject LoS. So I restructured some stuff.
Then it turned out that gameobject LoS is already fixed. So all this does, really, is restructure some stuff. And remove the hack from Sapphiron because I could.
This commit is contained in:
1 parent
9ba9fa8664
commit
9057cae219
11 files changed
+71
-117
No files matched your search
File renamed without changes.
@@ -1345,20 +1345,6 @@ bool WorldObject::_IsWithinDist(WorldObject const* obj, float dist2compare, bool
|
||||
return distsq < maxdist * maxdist;
|
||||
}
|
||||
|
||||
bool WorldObject::IsWithinLOSInMap(const WorldObject* obj, VMAP::ModelIgnoreFlags ignoreFlags) const
|
||||
{
|
||||
if (!IsInMap(obj))
|
||||
return false;
|
||||
|
||||
float x, y, z;
|
||||
if (obj->GetTypeId() == TYPEID_PLAYER)
|
||||
obj->GetPosition(x, y, z);
|
||||
else
|
||||
obj->GetHitSpherePointFor(GetPosition(), x, y, z);
|
||||
|
||||
return IsWithinLOS(x, y, z, ignoreFlags);
|
||||
}
|
||||
|
||||
float WorldObject::GetDistance(const WorldObject* obj) const
|
||||
{
|
||||
float d = GetExactDist(obj) - GetObjectSize() - obj->GetObjectSize();
|
||||
@@ -1433,12 +1419,17 @@ bool WorldObject::IsWithinDistInMap(WorldObject const* obj, float dist2compare,
|
||||
return obj && IsInMap(obj) && IsInPhase(obj) && _IsWithinDist(obj, dist2compare, is3D);
|
||||
}
|
||||
|
||||
bool WorldObject::IsWithinLOS(float ox, float oy, float oz, VMAP::ModelIgnoreFlags ignoreFlags) const
|
||||
Position WorldObject::GetHitSpherePointFor(Position const& dest) const
|
||||
{
|
||||
G3D::Vector3 vThis(GetPositionX(), GetPositionY(), GetPositionZ());
|
||||
G3D::Vector3 vObj(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||
G3D::Vector3 contactPoint = vThis + (vObj - vThis).directionOrZero() * std::min(dest.GetExactDist(GetPosition()), GetObjectSize());
|
||||
|
||||
return Position(contactPoint.x, contactPoint.y, contactPoint.z, GetAngle(contactPoint.x, contactPoint.y));
|
||||
}
|
||||
|
||||
bool WorldObject::IsWithinLOS(float ox, float oy, float oz, LineOfSightChecks checks, VMAP::ModelIgnoreFlags ignoreFlags) const
|
||||
{
|
||||
/*float x, y, z;
|
||||
GetPosition(x, y, z);
|
||||
VMAP::IVMapManager* vMapManager = VMAP::VMapFactory::createOrGetVMapManager();
|
||||
return vMapManager->isInLineOfSight(GetMapId(), x, y, z+2.0f, ox, oy, oz+2.0f);*/
|
||||
if (IsInWorld())
|
||||
{
|
||||
float x, y, z;
|
||||
@@ -1447,19 +1438,24 @@ bool WorldObject::IsWithinLOS(float ox, float oy, float oz, VMAP::ModelIgnoreFla
|
||||
else
|
||||
GetHitSpherePointFor({ ox, oy, oz }, x, y, z);
|
||||
|
||||
return GetMap()->isInLineOfSight(x, y, z + 2.0f, ox, oy, oz + 2.0f, GetPhases(), ignoreFlags);
|
||||
return GetMap()->isInLineOfSight(x, y, z + 2.0f, ox, oy, oz + 2.0f, GetPhases(), checks, ignoreFlags);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Position WorldObject::GetHitSpherePointFor(Position const& dest) const
|
||||
bool WorldObject::IsWithinLOSInMap(const WorldObject* obj, LineOfSightChecks checks, VMAP::ModelIgnoreFlags ignoreFlags) const
|
||||
{
|
||||
G3D::Vector3 vThis(GetPositionX(), GetPositionY(), GetPositionZ());
|
||||
G3D::Vector3 vObj(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||
G3D::Vector3 contactPoint = vThis + (vObj - vThis).directionOrZero() * std::min(dest.GetExactDist(GetPosition()), GetObjectSize());
|
||||
if (!IsInMap(obj))
|
||||
return false;
|
||||
|
||||
return Position(contactPoint.x, contactPoint.y, contactPoint.z, GetAngle(contactPoint.x, contactPoint.y));
|
||||
float x, y, z;
|
||||
if (obj->GetTypeId() == TYPEID_PLAYER)
|
||||
obj->GetPosition(x, y, z);
|
||||
else
|
||||
obj->GetHitSpherePointFor(GetPosition(), x, y, z);
|
||||
|
||||
return IsWithinLOS(x, y, z, checks, ignoreFlags);
|
||||
}
|
||||
|
||||
void WorldObject::GetHitSpherePointFor(Position const& dest, float& x, float& y, float& z) const
|
||||
|
||||
@@ -526,8 +526,8 @@ class TC_GAME_API WorldObject : public Object, public WorldLocation
|
||||
// use only if you will sure about placing both object at same map
|
||||
bool IsWithinDist(WorldObject const* obj, float dist2compare, bool is3D = true) const;
|
||||
bool IsWithinDistInMap(WorldObject const* obj, float dist2compare, bool is3D = true) const;
|
||||
bool IsWithinLOS(float x, float y, float z, VMAP::ModelIgnoreFlags ignoreFlags = VMAP::ModelIgnoreFlags::Nothing) const;
|
||||
bool IsWithinLOSInMap(WorldObject const* obj, VMAP::ModelIgnoreFlags ignoreFlags = VMAP::ModelIgnoreFlags::Nothing) const;
|
||||
bool IsWithinLOS(float x, float y, float z, LineOfSightChecks checks = LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags ignoreFlags = VMAP::ModelIgnoreFlags::Nothing) const;
|
||||
bool IsWithinLOSInMap(WorldObject const* obj, LineOfSightChecks checks = LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags ignoreFlags = VMAP::ModelIgnoreFlags::Nothing) const;
|
||||
Position GetHitSpherePointFor(Position const& dest) const;
|
||||
void GetHitSpherePointFor(Position const& dest, float& x, float& y, float& z) const;
|
||||
bool GetDistanceOrder(WorldObject const* obj1, WorldObject const* obj2, bool is3D = true) const;
|
||||
|
||||
@@ -2625,10 +2625,15 @@ float Map::GetWaterLevel(float x, float y) const
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool Map::isInLineOfSight(float x1, float y1, float z1, float x2, float y2, float z2, std::set<uint32> const& phases, VMAP::ModelIgnoreFlags ignoreFlags) const
|
||||
bool Map::isInLineOfSight(float x1, float y1, float z1, float x2, float y2, float z2, std::set<uint32> const& phases, LineOfSightChecks checks, VMAP::ModelIgnoreFlags ignoreFlags) const
|
||||
{
|
||||
return VMAP::VMapFactory::createOrGetVMapManager()->isInLineOfSight(GetId(), x1, y1, z1, x2, y2, z2, ignoreFlags)
|
||||
&& _dynamicTree.isInLineOfSight({ x1, y1, z1 }, { x2, y2, z2 }, phases);
|
||||
if ((checks & LINEOFSIGHT_CHECK_VMAP)
|
||||
&& !VMAP::VMapFactory::createOrGetVMapManager()->isInLineOfSight(GetId(), x1, y1, z1, x2, y2, z2, ignoreFlags))
|
||||
return false;
|
||||
if (sWorld->getBoolConfig(CONFIG_CHECK_GOBJECT_LOS) && (checks & LINEOFSIGHT_CHECK_GOBJECT)
|
||||
&& !_dynamicTree.isInLineOfSight({x1, y1, z1}, {x2, y2, z2}, phases))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Map::getObjectHitPos(std::set<uint32> const& phases, float x1, float y1, float z1, float x2, float y2, float z2, float& rx, float& ry, float& rz, float modifyDist)
|
||||
|
||||
@@ -175,6 +175,14 @@ struct PositionFullTerrainStatus
|
||||
Optional<LiquidData> liquidInfo;
|
||||
};
|
||||
|
||||
enum LineOfSightChecks
|
||||
{
|
||||
LINEOFSIGHT_CHECK_VMAP = 0x1, // check static floor layout data
|
||||
LINEOFSIGHT_CHECK_GOBJECT = 0x2, // check dynamic game object data
|
||||
|
||||
LINEOFSIGHT_ALL_CHECKS = (LINEOFSIGHT_CHECK_VMAP | LINEOFSIGHT_CHECK_GOBJECT)
|
||||
};
|
||||
|
||||
class TC_GAME_API GridMap
|
||||
{
|
||||
uint32 _flags;
|
||||
@@ -515,7 +523,7 @@ class TC_GAME_API Map : public GridRefManager<NGridType>
|
||||
|
||||
float GetWaterOrGroundLevel(std::set<uint32> const& phases, float x, float y, float z, float* ground = nullptr, bool swim = false) const;
|
||||
float GetHeight(std::set<uint32> const& phases, float x, float y, float z, bool vmap = true, float maxSearchDist = DEFAULT_HEIGHT_SEARCH) const;
|
||||
bool isInLineOfSight(float x1, float y1, float z1, float x2, float y2, float z2, std::set<uint32> const& phases, VMAP::ModelIgnoreFlags ignoreFlags) const;
|
||||
bool isInLineOfSight(float x1, float y1, float z1, float x2, float y2, float z2, std::set<uint32> const& phases, LineOfSightChecks checks, VMAP::ModelIgnoreFlags ignoreFlags) const;
|
||||
void Balance() { _dynamicTree.balance(); }
|
||||
void RemoveGameObjectModel(const GameObjectModel& model) { _dynamicTree.remove(model); }
|
||||
void InsertGameObjectModel(const GameObjectModel& model) { _dynamicTree.insert(model); }
|
||||
|
||||
@@ -1927,7 +1927,7 @@ void Spell::SearchChainTargets(std::list<WorldObject*>& targets, uint32 chainTar
|
||||
if (Unit* unit = (*itr)->ToUnit())
|
||||
{
|
||||
uint32 deficit = unit->GetMaxHealth() - unit->GetHealth();
|
||||
if ((deficit > maxHPDeficit || foundItr == tempTargets.end()) && target->IsWithinDist(unit, jumpRadius) && target->IsWithinLOSInMap(unit, VMAP::ModelIgnoreFlags::M2))
|
||||
if ((deficit > maxHPDeficit || foundItr == tempTargets.end()) && target->IsWithinDist(unit, jumpRadius) && target->IsWithinLOSInMap(unit, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::M2))
|
||||
{
|
||||
foundItr = itr;
|
||||
maxHPDeficit = deficit;
|
||||
@@ -1942,10 +1942,10 @@ void Spell::SearchChainTargets(std::list<WorldObject*>& targets, uint32 chainTar
|
||||
{
|
||||
if (foundItr == tempTargets.end())
|
||||
{
|
||||
if ((!isBouncingFar || target->IsWithinDist(*itr, jumpRadius)) && target->IsWithinLOSInMap(*itr, VMAP::ModelIgnoreFlags::M2))
|
||||
if ((!isBouncingFar || target->IsWithinDist(*itr, jumpRadius)) && target->IsWithinLOSInMap(*itr, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::M2))
|
||||
foundItr = itr;
|
||||
}
|
||||
else if (target->GetDistanceOrder(*itr, *foundItr) && target->IsWithinLOSInMap(*itr, VMAP::ModelIgnoreFlags::M2))
|
||||
else if (target->GetDistanceOrder(*itr, *foundItr) && target->IsWithinLOSInMap(*itr, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::M2))
|
||||
foundItr = itr;
|
||||
}
|
||||
}
|
||||
@@ -5150,7 +5150,7 @@ SpellCastResult Spell::CheckCast(bool strict, uint32* param1 /*= nullptr*/, uint
|
||||
if (DynamicObject* dynObj = m_caster->GetDynObject(m_triggeredByAuraSpell->Id))
|
||||
losTarget = dynObj;
|
||||
|
||||
if (!m_spellInfo->HasAttribute(SPELL_ATTR2_CAN_TARGET_NOT_IN_LOS) && !DisableMgr::IsDisabledFor(DISABLE_TYPE_SPELL, m_spellInfo->Id, NULL, SPELL_DISABLE_LOS) && !target->IsWithinLOSInMap(losTarget, VMAP::ModelIgnoreFlags::M2))
|
||||
if (!m_spellInfo->HasAttribute(SPELL_ATTR2_CAN_TARGET_NOT_IN_LOS) && !DisableMgr::IsDisabledFor(DISABLE_TYPE_SPELL, m_spellInfo->Id, NULL, SPELL_DISABLE_LOS) && !target->IsWithinLOSInMap(losTarget, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::M2))
|
||||
return SPELL_FAILED_LINE_OF_SIGHT;
|
||||
}
|
||||
}
|
||||
@@ -5162,7 +5162,7 @@ SpellCastResult Spell::CheckCast(bool strict, uint32* param1 /*= nullptr*/, uint
|
||||
float x, y, z;
|
||||
m_targets.GetDstPos()->GetPosition(x, y, z);
|
||||
|
||||
if (!m_spellInfo->HasAttribute(SPELL_ATTR2_CAN_TARGET_NOT_IN_LOS) && !DisableMgr::IsDisabledFor(DISABLE_TYPE_SPELL, m_spellInfo->Id, NULL, SPELL_DISABLE_LOS) && !m_caster->IsWithinLOS(x, y, z, VMAP::ModelIgnoreFlags::M2))
|
||||
if (!m_spellInfo->HasAttribute(SPELL_ATTR2_CAN_TARGET_NOT_IN_LOS) && !DisableMgr::IsDisabledFor(DISABLE_TYPE_SPELL, m_spellInfo->Id, NULL, SPELL_DISABLE_LOS) && !m_caster->IsWithinLOS(x, y, z, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::M2))
|
||||
return SPELL_FAILED_LINE_OF_SIGHT;
|
||||
}
|
||||
|
||||
@@ -6972,7 +6972,7 @@ bool Spell::CheckEffectTarget(Unit const* target, uint32 eff, Position const* lo
|
||||
default: // normal case
|
||||
{
|
||||
if (losPosition)
|
||||
return target->IsWithinLOS(losPosition->GetPositionX(), losPosition->GetPositionY(), losPosition->GetPositionZ(), VMAP::ModelIgnoreFlags::M2);
|
||||
return target->IsWithinLOS(losPosition->GetPositionX(), losPosition->GetPositionY(), losPosition->GetPositionZ(), LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::M2);
|
||||
else
|
||||
{
|
||||
// Get GO cast coordinates if original caster -> GO
|
||||
@@ -6981,7 +6981,7 @@ bool Spell::CheckEffectTarget(Unit const* target, uint32 eff, Position const* lo
|
||||
caster = m_caster->GetMap()->GetGameObject(m_originalCasterGUID);
|
||||
if (!caster)
|
||||
caster = m_caster;
|
||||
if (target != m_caster && !target->IsWithinLOSInMap(caster, VMAP::ModelIgnoreFlags::M2))
|
||||
if (target != m_caster && !target->IsWithinLOSInMap(caster, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::M2))
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1406,6 +1406,9 @@ void World::LoadConfigSettings(bool reload)
|
||||
// Allow 5-man parties to use raid warnings
|
||||
m_bool_configs[CONFIG_CHAT_PARTY_RAID_WARNINGS] = sConfigMgr->GetBoolDefault("PartyRaidWarnings", false);
|
||||
|
||||
// Whether to use LoS from game objects
|
||||
m_bool_configs[CONFIG_CHECK_GOBJECT_LOS] = sConfigMgr->GetBoolDefault("CheckGameObjectLoS", true);
|
||||
|
||||
// call ScriptMgr if we're reloading the configuration
|
||||
if (reload)
|
||||
sScriptMgr->OnConfigLoad(reload);
|
||||
|
||||
@@ -186,6 +186,7 @@ enum WorldBoolConfigs
|
||||
CONFIG_HOTSWAP_BUILD_FILE_RECREATION_ENABLED,
|
||||
CONFIG_HOTSWAP_INSTALL_ENABLED,
|
||||
CONFIG_HOTSWAP_PREFIX_CORRECTION_ENABLED,
|
||||
CONFIG_CHECK_GOBJECT_LOS,
|
||||
BOOL_CONFIG_VALUE_COUNT
|
||||
};
|
||||
|
||||
|
||||
@@ -1122,8 +1122,15 @@ public:
|
||||
static bool HandleDebugLoSCommand(ChatHandler* handler, char const* /*args*/)
|
||||
{
|
||||
if (Unit* unit = handler->getSelectedUnit())
|
||||
handler->PSendSysMessage("Unit %s (GuidLow: %u) is %sin LoS", unit->GetName().c_str(), unit->GetGUID().GetCounter(), handler->GetSession()->GetPlayer()->IsWithinLOSInMap(unit) ? "" : "not ");
|
||||
return true;
|
||||
{
|
||||
Player* player = handler->GetSession()->GetPlayer();
|
||||
handler->PSendSysMessage("Checking LoS %s -> %s:", player->GetName().c_str(), unit->GetName().c_str());
|
||||
handler->PSendSysMessage(" VMAP LoS: %s", player->IsWithinLOSInMap(unit, LINEOFSIGHT_CHECK_VMAP) ? "clear" : "obstructed");
|
||||
handler->PSendSysMessage(" GObj LoS: %s", player->IsWithinLOSInMap(unit, LINEOFSIGHT_CHECK_GOBJECT) ? "clear" : "obstructed");
|
||||
handler->PSendSysMessage("%s is %sin line of sight of %s.", unit->GetName().c_str(), (player->IsWithinLOSInMap(unit) ? "" : "not "), player->GetName().c_str());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool HandleDebugSetAuraStateCommand(ChatHandler* handler, char const* args)
|
||||
|
||||
@@ -340,6 +340,7 @@ class boss_sapphiron : public CreatureScript
|
||||
case EVENT_EXPLOSION:
|
||||
DoCastAOE(SPELL_FROST_BREATH);
|
||||
DoCastAOE(SPELL_FROST_BREATH_ANTICHEAT);
|
||||
instance->DoRemoveAurasDueToSpellOnPlayers(SPELL_ICEBOLT);
|
||||
events.ScheduleEvent(EVENT_LAND, Seconds(3) + Milliseconds(500), 0, PHASE_FLIGHT);
|
||||
return;
|
||||
case EVENT_LAND:
|
||||
@@ -505,81 +506,6 @@ class spell_sapphiron_icebolt : public SpellScriptLoader
|
||||
}
|
||||
};
|
||||
|
||||
// @hack Hello, developer from the future! How has your day been?
|
||||
// Anyway, this is, as you can undoubtedly see, a hack to emulate line of sight checks on a spell that abides line of sight anyway.
|
||||
// In the current core, line of sight is not properly checked for people standing behind an ice block. This is not a good thing and kills people.
|
||||
// Thus, we have this hack to check for ice block LoS in a "safe" way. Kind of. It's inaccurate, but in a good way (tends to save people when it shouldn't in edge cases).
|
||||
// If LoS handling is better in whatever the current revision is when you read this, please get rid of the hack. Thanks!
|
||||
class spell_sapphiron_frost_breath : public SpellScriptLoader
|
||||
{
|
||||
public:
|
||||
spell_sapphiron_frost_breath() : SpellScriptLoader("spell_sapphiron_frost_breath") { }
|
||||
|
||||
class spell_sapphiron_frost_breath_SpellScript : public SpellScript
|
||||
{
|
||||
PrepareSpellScript(spell_sapphiron_frost_breath_SpellScript);
|
||||
|
||||
bool Validate(SpellInfo const* /*spell*/) override
|
||||
{
|
||||
return !!sSpellMgr->GetSpellInfo(SPELL_FROST_BREATH);
|
||||
}
|
||||
|
||||
void HandleTargets(std::list<WorldObject*>& targetList)
|
||||
{
|
||||
std::list<GameObject*> blocks;
|
||||
if(GetCaster())
|
||||
GetCaster()->GetGameObjectListWithEntryInGrid(blocks, GO_ICEBLOCK, 200.0f);
|
||||
|
||||
std::vector<Unit*> toRemove;
|
||||
toRemove.reserve(3);
|
||||
std::list<WorldObject*>::iterator it = targetList.begin();
|
||||
while (it != targetList.end())
|
||||
{
|
||||
Unit* target = (*it)->ToUnit();
|
||||
if (!target)
|
||||
{
|
||||
it = targetList.erase(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (target->HasAura(SPELL_ICEBOLT))
|
||||
{
|
||||
it = targetList.erase(it);
|
||||
toRemove.push_back(target);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
for (GameObject* block : blocks)
|
||||
if (block->IsInBetween(GetCaster(), target, 2.0f) && GetCaster()->GetExactDist2d(block) + 5 >= GetCaster()->GetExactDist2d(target))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
if (found)
|
||||
{
|
||||
it = targetList.erase(it);
|
||||
continue;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
|
||||
for (Unit* block : toRemove)
|
||||
block->RemoveAura(SPELL_ICEBOLT);
|
||||
}
|
||||
|
||||
void Register() override
|
||||
{
|
||||
OnObjectAreaTargetSelect += SpellObjectAreaTargetSelectFn(spell_sapphiron_frost_breath_SpellScript::HandleTargets, EFFECT_0, TARGET_UNIT_DEST_AREA_ENEMY);
|
||||
}
|
||||
};
|
||||
|
||||
SpellScript* GetSpellScript() const override
|
||||
{
|
||||
return new spell_sapphiron_frost_breath_SpellScript();
|
||||
}
|
||||
};
|
||||
|
||||
class spell_sapphiron_summon_blizzard : public SpellScriptLoader
|
||||
{
|
||||
public:
|
||||
@@ -641,7 +567,6 @@ void AddSC_boss_sapphiron()
|
||||
new go_sapphiron_birth();
|
||||
new spell_sapphiron_change_blizzard_target();
|
||||
new spell_sapphiron_icebolt();
|
||||
new spell_sapphiron_frost_breath();
|
||||
new spell_sapphiron_summon_blizzard();
|
||||
new achievement_the_hundred_club();
|
||||
}
|
||||
@@ -400,6 +400,15 @@ vmap.enableIndoorCheck = 1
|
||||
|
||||
DetectPosCollision = 1
|
||||
|
||||
#
|
||||
# CheckGameObjectLoS
|
||||
# Description: Include dynamic game objects (doors, chests etc.) in line of sight checks.
|
||||
# This increases CPU usage somewhat.
|
||||
# Default: 1 - (Enabled)
|
||||
# 0 - (Disabled, may break some boss encounters)
|
||||
|
||||
CheckGameObjectLoS = 1
|
||||
|
||||
#
|
||||
# TargetPosRecalculateRange
|
||||
# Description: Max distance from movement target point (+moving unit size) and targeted
|
||||
@@ -3998,4 +4007,4 @@ Metric.ConnectionInfo = "127.0.0.1;8086;worldserver"
|
||||
|
||||
Metric.OverallStatusInterval = 1
|
||||
|
||||
###################################################################################################
|
||||
###################################################################################################
|
||||
Reference in new issue
Block a user