[7776] Completed implementation of CMSG_SPELLCLICK Author: arrai
For vehicles, you have to add the correct SPELL_AURA_CONTROL_VEHICLE spells to
npc_spellclick_spells, otherwise you won't be able to use them
--HG--
branch : trunk
This commit is contained in:
+1
-1
@@ -23,7 +23,7 @@ DROP TABLE IF EXISTS `db_version`;
|
||||
CREATE TABLE `db_version` (
|
||||
`version` varchar(120) default NULL,
|
||||
`creature_ai_version` varchar(120) default NULL,
|
||||
`required_7720_01_mangos_mangos_string` bit(1) default NULL
|
||||
`required_7776_01_mangos_npc_spellclick_spells` bit(1) default NULL
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 ROW_FORMAT=FIXED COMMENT='Used DB version notes';
|
||||
|
||||
--
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE db_version CHANGE COLUMN required_7720_01_mangos_mangos_string required_7776_01_mangos_npc_spellclick_spells bit;
|
||||
|
||||
CREATE TABLE `npc_spellclick_spells` (
|
||||
`npc_entry` INT UNSIGNED NOT NULL COMMENT 'reference to creature_template',
|
||||
`spell_id` INT UNSIGNED NOT NULL COMMENT 'spell which should be casted ',
|
||||
`quest_id` INT UNSIGNED NOT NULL COMMENT 'reference to quest_template',
|
||||
`cast_flags` TINYINT UNSIGNED NOT NULL COMMENT 'first bit defines caster: 1=player, 0=creature; second bit defines target, same mapping as caster bit'
|
||||
) ENGINE = MYISAM DEFAULT CHARSET=utf8;
|
||||
@@ -455,6 +455,7 @@ ChatCommand * ChatHandler::getCommandTable()
|
||||
{ "page_text", SEC_ADMINISTRATOR, true, &ChatHandler::HandleReloadPageTextsCommand, "", NULL },
|
||||
{ "pickpocketing_loot_template", SEC_ADMINISTRATOR, true, &ChatHandler::HandleReloadLootTemplatesPickpocketingCommand,"",NULL},
|
||||
{ "points_of_interest", SEC_ADMINISTRATOR, true, &ChatHandler::HandleReloadPointsOfInterestCommand, "",NULL},
|
||||
{ "npc_spellclick_spells", SEC_ADMINISTRATOR, true, &ChatHandler::HandleReloadSpellClickSpellsCommand, "",NULL},
|
||||
{ "prospecting_loot_template", SEC_ADMINISTRATOR, true, &ChatHandler::HandleReloadLootTemplatesProspectingCommand,"", NULL },
|
||||
{ "quest_mail_loot_template", SEC_ADMINISTRATOR, true, &ChatHandler::HandleReloadLootTemplatesQuestMailCommand, "", NULL },
|
||||
{ "quest_end_scripts", SEC_ADMINISTRATOR, true, &ChatHandler::HandleReloadQuestEndScriptsCommand, "", NULL },
|
||||
|
||||
@@ -374,6 +374,7 @@ class ChatHandler
|
||||
bool HandleReloadNpcVendorCommand(const char* args);
|
||||
bool HandleReloadPageTextsCommand(const char* args);
|
||||
bool HandleReloadPointsOfInterestCommand(const char* args);
|
||||
bool HandleReloadSpellClickSpellsCommand(const char* args);
|
||||
bool HandleReloadQuestAreaTriggersCommand(const char* args);
|
||||
bool HandleReloadQuestEndScriptsCommand(const char* args);
|
||||
bool HandleReloadQuestStartScriptsCommand(const char* args);
|
||||
|
||||
@@ -242,7 +242,7 @@ class TRINITY_DLL_SPEC CreatureAI : public UnitAI
|
||||
|
||||
void OnCharmed(bool apply);
|
||||
|
||||
virtual void SpellClick(Player *player) {}
|
||||
//virtual void SpellClick(Player *player) {}
|
||||
|
||||
// Called at reaching home after evade
|
||||
virtual void JustReachedHome() {}
|
||||
|
||||
+4
-4
@@ -210,7 +210,7 @@ void Group::ConvertToRaid()
|
||||
// update quest related GO states (quest activity dependent from raid membership)
|
||||
for(member_citerator citr = m_memberSlots.begin(); citr != m_memberSlots.end(); ++citr)
|
||||
if(Player* player = objmgr.GetPlayer(citr->guid))
|
||||
player->UpdateForQuestsGO();
|
||||
player->UpdateForQuestWorldObjects();
|
||||
}
|
||||
|
||||
bool Group::AddInvite(Player *player)
|
||||
@@ -304,7 +304,7 @@ bool Group::AddMember(const uint64 &guid, const char* name)
|
||||
|
||||
// quest related GO state dependent from raid memebership
|
||||
if(isRaidGroup())
|
||||
player->UpdateForQuestsGO();
|
||||
player->UpdateForQuestWorldObjects();
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -323,7 +323,7 @@ uint32 Group::RemoveMember(const uint64 &guid, const uint8 &method)
|
||||
{
|
||||
// quest related GO state dependent from raid membership
|
||||
if(isRaidGroup())
|
||||
player->UpdateForQuestsGO();
|
||||
player->UpdateForQuestWorldObjects();
|
||||
|
||||
WorldPacket data;
|
||||
|
||||
@@ -404,7 +404,7 @@ void Group::Disband(bool hideDestroy)
|
||||
|
||||
// quest related GO state dependent from raid membership
|
||||
if(isRaidGroup())
|
||||
player->UpdateForQuestsGO();
|
||||
player->UpdateForQuestWorldObjects();
|
||||
|
||||
if(!player->GetSession())
|
||||
continue;
|
||||
|
||||
@@ -576,6 +576,7 @@ bool ChatHandler::HandleReloadAllNpcCommand(const char* /*args*/)
|
||||
HandleReloadNpcTrainerCommand("a");
|
||||
HandleReloadNpcVendorCommand("a");
|
||||
HandleReloadPointsOfInterestCommand("a");
|
||||
HandleReloadSpellClickSpellsCommand("a");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -922,6 +923,14 @@ bool ChatHandler::HandleReloadPointsOfInterestCommand(const char*)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ChatHandler::HandleReloadSpellClickSpellsCommand(const char*)
|
||||
{
|
||||
sLog.outString( "Re-Loading `npc_spellclick_spells` Table!" );
|
||||
objmgr.LoadNPCSpellClickSpells();
|
||||
SendGlobalSysMessage("DB table `npc_spellclick_spells` reloaded.");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ChatHandler::HandleReloadReservedNameCommand(const char*)
|
||||
{
|
||||
sLog.outString( "Loading ReservedNames... (`reserved_name`)" );
|
||||
|
||||
@@ -1686,29 +1686,6 @@ void WorldSession::HandleSetTaxiBenchmarkOpcode( WorldPacket & recv_data )
|
||||
sLog.outDebug("Client used \"/timetest %d\" command", mode);
|
||||
}
|
||||
|
||||
void WorldSession::HandleSpellClick( WorldPacket & recv_data )
|
||||
{
|
||||
CHECK_PACKET_SIZE(recv_data, 8);
|
||||
|
||||
uint64 guid;
|
||||
recv_data >> guid;
|
||||
|
||||
Creature *creature = NULL;
|
||||
if(IS_VEHICLE_GUID(guid))
|
||||
{
|
||||
Vehicle *vehicle = ObjectAccessor::GetVehicle(guid);
|
||||
if(vehicle)
|
||||
{
|
||||
_player->EnterVehicle(vehicle);
|
||||
creature = vehicle;
|
||||
}
|
||||
}
|
||||
if(!creature)
|
||||
creature = ObjectAccessor::GetCreatureOrPetOrVehicle(*_player, guid);
|
||||
if(creature && creature->IsAIEnabled)
|
||||
creature->AI()->SpellClick(_player);
|
||||
}
|
||||
|
||||
void WorldSession::HandleInspectAchievements( WorldPacket & recv_data )
|
||||
{
|
||||
CHECK_PACKET_SIZE(recv_data, 1);
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "Corpse.h"
|
||||
#include "Player.h"
|
||||
#include "Vehicle.h"
|
||||
#include "SpellAuras.h"
|
||||
#include "MapManager.h"
|
||||
#include "Transports.h"
|
||||
#include "BattleGround.h"
|
||||
|
||||
+9
-2
@@ -526,9 +526,16 @@ void Object::_BuildValuesUpdate(uint8 updatetype, ByteBuffer * data, UpdateMask
|
||||
{
|
||||
if( updateMask->GetBit( index ) )
|
||||
{
|
||||
// remove custom flag before send
|
||||
if( index == UNIT_NPC_FLAGS )
|
||||
*data << uint32(m_uint32Values[ index ] & ~(UNIT_NPC_FLAG_GUARD + UNIT_NPC_FLAG_OUTDOORPVP));
|
||||
{
|
||||
// remove custom flag before sending
|
||||
uint32 appendValue = m_uint32Values[ index ] & ~(UNIT_NPC_FLAG_GUARD + UNIT_NPC_FLAG_OUTDOORPVP);
|
||||
|
||||
if (GetTypeId() == TYPEID_UNIT && !target->canSeeSpellClickOn((Creature*)this))
|
||||
appendValue &= ~UNIT_NPC_FLAG_SPELLCLICK;
|
||||
|
||||
*data << uint32(appendValue);
|
||||
}
|
||||
// FIXME: Some values at server stored in float format but must be sent to client in uint32 format
|
||||
else if(index >= UNIT_FIELD_BASEATTACKTIME && index <= UNIT_FIELD_RANGEDATTACKTIME)
|
||||
{
|
||||
|
||||
@@ -6207,6 +6207,76 @@ void ObjectMgr::LoadPointsOfInterest()
|
||||
sLog.outString(">> Loaded %u Points of Interest definitions", count);
|
||||
}
|
||||
|
||||
void ObjectMgr::LoadNPCSpellClickSpells()
|
||||
{
|
||||
uint32 count = 0;
|
||||
|
||||
mSpellClickInfoMap.clear();
|
||||
|
||||
QueryResult *result = WorldDatabase.Query("SELECT npc_entry, spell_id, quest_id, cast_flags FROM npc_spellclick_spells");
|
||||
|
||||
if(!result)
|
||||
{
|
||||
barGoLink bar(1);
|
||||
|
||||
bar.step();
|
||||
|
||||
sLog.outString();
|
||||
sLog.outErrorDb(">> Loaded 0 spellclick spells. DB table `npc_spellclick_spells` is empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
barGoLink bar(result->GetRowCount());
|
||||
|
||||
do
|
||||
{
|
||||
Field *fields = result->Fetch();
|
||||
bar.step();
|
||||
|
||||
uint32 npc_entry = fields[0].GetUInt32();
|
||||
CreatureInfo const* cInfo = GetCreatureTemplate(npc_entry);
|
||||
if (!cInfo)
|
||||
{
|
||||
sLog.outErrorDb("Table npc_spellclick_spells references unknown creature_template %u. Skipping entry.", npc_entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32 spellid = fields[1].GetUInt32();
|
||||
SpellEntry const *spellinfo = sSpellStore.LookupEntry(spellid);
|
||||
if (!spellinfo)
|
||||
{
|
||||
sLog.outErrorDb("Table npc_spellclick_spells references unknown spellid %u. Skipping entry.", spellid);
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32 quest = fields[2].GetUInt32();
|
||||
|
||||
// quest might be 0 to enable spellclick independent of any quest
|
||||
if (quest)
|
||||
{
|
||||
if(mQuestTemplates.find(quest) == mQuestTemplates.end())
|
||||
{
|
||||
sLog.outErrorDb("Table npc_spellclick_spells references unknown quest %u. Skipping entry.", spellid);
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
uint8 castFlags = fields[3].GetUInt8();
|
||||
SpellClickInfo info;
|
||||
info.spellId = spellid;
|
||||
info.questId = quest;
|
||||
info.castFlags = castFlags;
|
||||
mSpellClickInfoMap.insert(SpellClickInfoMap::value_type(npc_entry, info));
|
||||
++count;
|
||||
} while (result->NextRow());
|
||||
|
||||
delete result;
|
||||
|
||||
sLog.outString();
|
||||
sLog.outString(">> Loaded %u spellclick definitions", count);
|
||||
}
|
||||
|
||||
void ObjectMgr::LoadWeatherZoneChances()
|
||||
{
|
||||
uint32 count = 0;
|
||||
|
||||
@@ -99,6 +99,15 @@ extern ScriptMapMap sGameObjectScripts;
|
||||
extern ScriptMapMap sEventScripts;
|
||||
extern ScriptMapMap sWaypointScripts;
|
||||
|
||||
struct SpellClickInfo
|
||||
{
|
||||
uint32 spellId;
|
||||
uint32 questId;
|
||||
uint8 castFlags;
|
||||
};
|
||||
|
||||
typedef std::multimap<uint32, SpellClickInfo> SpellClickInfoMap;
|
||||
|
||||
struct AreaTrigger
|
||||
{
|
||||
uint32 access_id;
|
||||
@@ -569,6 +578,9 @@ class ObjectMgr
|
||||
void LoadReputationOnKill();
|
||||
void LoadPointsOfInterest();
|
||||
|
||||
SpellClickInfoMap mSpellClickInfoMap;
|
||||
void LoadNPCSpellClickSpells();
|
||||
|
||||
void LoadWeatherZoneChances();
|
||||
void LoadGameTele();
|
||||
|
||||
|
||||
+38
-5
@@ -12562,7 +12562,7 @@ void Player::AddQuest( Quest const *pQuest, Object *questGiver )
|
||||
CastSpell(this,itr->second->spellId,true);
|
||||
}
|
||||
|
||||
UpdateForQuestsGO();
|
||||
UpdateForQuestWorldObjects();
|
||||
}
|
||||
|
||||
void Player::CompleteQuest( uint32 quest_id )
|
||||
@@ -13293,7 +13293,7 @@ void Player::SetQuestStatus( uint32 quest_id, QuestStatus status )
|
||||
if (q_status.uState != QUEST_NEW) q_status.uState = QUEST_CHANGED;
|
||||
}
|
||||
|
||||
UpdateForQuestsGO();
|
||||
UpdateForQuestWorldObjects();
|
||||
}
|
||||
|
||||
// not used in TrinIty, but used in scripting code
|
||||
@@ -13414,7 +13414,7 @@ void Player::ItemAddedQuestCheck( uint32 entry, uint32 count )
|
||||
}
|
||||
}
|
||||
}
|
||||
UpdateForQuestsGO();
|
||||
UpdateForQuestWorldObjects();
|
||||
}
|
||||
|
||||
void Player::ItemRemovedQuestCheck( uint32 entry, uint32 count )
|
||||
@@ -13455,7 +13455,7 @@ void Player::ItemRemovedQuestCheck( uint32 entry, uint32 count )
|
||||
}
|
||||
}
|
||||
}
|
||||
UpdateForQuestsGO();
|
||||
UpdateForQuestWorldObjects();
|
||||
}
|
||||
|
||||
void Player::KilledMonster( uint32 entry, uint64 guid )
|
||||
@@ -18890,7 +18890,7 @@ bool Player::HasQuestForGO(int32 GOId) const
|
||||
return false;
|
||||
}
|
||||
|
||||
void Player::UpdateForQuestsGO()
|
||||
void Player::UpdateForQuestWorldObjects()
|
||||
{
|
||||
if(m_clientGUIDs.empty())
|
||||
return;
|
||||
@@ -18905,6 +18905,24 @@ void Player::UpdateForQuestsGO()
|
||||
if(obj)
|
||||
obj->BuildValuesUpdateBlockForPlayer(&udata,this);
|
||||
}
|
||||
else if(IS_CREATURE_GUID(*itr) || IS_VEHICLE_GUID(*itr))
|
||||
{
|
||||
Creature *obj = ObjectAccessor::GetCreatureOrPetOrVehicle(*this, *itr);
|
||||
if(!obj)
|
||||
continue;
|
||||
// check if this unit requires quest specific flags
|
||||
|
||||
SpellClickInfoMap const& map = objmgr.mSpellClickInfoMap;
|
||||
for(SpellClickInfoMap::const_iterator itr = map.lower_bound(obj->GetEntry()); itr != map.upper_bound(obj->GetEntry()); ++itr)
|
||||
{
|
||||
if(itr->second.questId != 0)
|
||||
{
|
||||
obj->BuildCreateUpdateBlockForPlayer(&udata,this);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
udata.BuildPacket(&packet);
|
||||
GetSession()->SendPacket(&packet);
|
||||
@@ -20455,3 +20473,18 @@ void Player::ResummonPetTemporaryUnSummonedIfAny()
|
||||
|
||||
m_temporaryUnsummonedPetNumber = 0;
|
||||
}
|
||||
|
||||
bool Player::canSeeSpellClickOn(Creature const *c) const
|
||||
{
|
||||
SpellClickInfoMap const& map = objmgr.mSpellClickInfoMap;
|
||||
if(map.empty())
|
||||
return true;
|
||||
|
||||
for(SpellClickInfoMap::const_iterator itr = map.lower_bound(c->GetEntry()); itr != map.upper_bound(c->GetEntry()); ++itr)
|
||||
{
|
||||
if(itr->second.questId == 0 || GetQuestStatus(itr->second.questId) == QUEST_STATUS_INCOMPLETE)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -1160,7 +1160,7 @@ class TRINITY_DLL_SPEC Player : public Unit
|
||||
void ReputationChanged(FactionEntry const* factionEntry );
|
||||
bool HasQuestForItem( uint32 itemid ) const;
|
||||
bool HasQuestForGO(int32 GOId) const;
|
||||
void UpdateForQuestsGO();
|
||||
void UpdateForQuestWorldObjects();
|
||||
bool CanShareQuest(uint32 quest_id) const;
|
||||
|
||||
void SendQuestComplete( uint32 quest_id );
|
||||
@@ -2028,6 +2028,8 @@ class TRINITY_DLL_SPEC Player : public Unit
|
||||
bool HasTitle(CharTitlesEntry const* title) { return HasTitle(title->bit_index); }
|
||||
void SetTitle(CharTitlesEntry const* title);
|
||||
|
||||
//bool isActiveObject() const { return true; }
|
||||
bool canSeeSpellClickOn(Creature const* creature) const;
|
||||
protected:
|
||||
|
||||
/*********************************************************/
|
||||
|
||||
+27
-5
@@ -47,6 +47,7 @@
|
||||
#include "Util.h"
|
||||
#include "GridNotifiers.h"
|
||||
#include "GridNotifiersImpl.h"
|
||||
#include "Vehicle.h"
|
||||
#include "CellImpl.h"
|
||||
|
||||
pAuraHandler AuraHandler[TOTAL_AURAS]=
|
||||
@@ -6726,19 +6727,40 @@ void AuraEffect::HandleArenaPreparation(bool apply, bool Real)
|
||||
m_target->RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PREPARATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Such auras are applied from a caster(=player) to a vehicle.
|
||||
* This has been verified using spell #49256
|
||||
*/
|
||||
void AuraEffect::HandleAuraControlVehicle(bool apply, bool Real)
|
||||
{
|
||||
if(!Real)
|
||||
return;
|
||||
|
||||
if(m_target->GetTypeId() != TYPEID_PLAYER)
|
||||
if(m_target->GetTypeId() != TYPEID_UNIT || !((Creature*)m_target)->isVehicle())
|
||||
return;
|
||||
|
||||
if(Pet *pet = ((Player*)m_target)->GetPet())
|
||||
pet->Remove(PET_SAVE_AS_CURRENT);
|
||||
Unit *caster = GetCaster();
|
||||
if(!caster)
|
||||
return;
|
||||
|
||||
//WorldPacket data(SMSG_ON_CANCEL_EXPECTED_RIDE_VEHICLE_AURA, 0);
|
||||
//((Player*)m_target)->GetSession()->SendPacket(&data);
|
||||
Vehicle *vehicle = dynamic_cast<Vehicle*>(m_target);
|
||||
|
||||
if (apply)
|
||||
{
|
||||
if(caster->GetTypeId() == TYPEID_PLAYER)
|
||||
if(Pet *pet = ((Player*)caster)->GetPet())
|
||||
pet->Remove(PET_SAVE_AS_CURRENT);
|
||||
caster->EnterVehicle(vehicle);
|
||||
}
|
||||
else
|
||||
{
|
||||
SpellEntry const *spell = GetSpellProto();
|
||||
|
||||
// some SPELL_AURA_CONTROL_VEHICLE auras have a dummy effect on the player - remove them
|
||||
caster->RemoveAurasDueToSpell(spell->Id);
|
||||
|
||||
caster->ExitVehicle();
|
||||
}
|
||||
}
|
||||
|
||||
void AuraEffect::HandleAuraConvertRune(bool apply, bool Real)
|
||||
|
||||
@@ -1227,6 +1227,19 @@ void Spell::EffectDummy(uint32 i)
|
||||
m_caster->CastSpell(m_caster, 30452, true, NULL);
|
||||
return;
|
||||
}
|
||||
case 51592: // Pickup Primordial Hatchling
|
||||
{
|
||||
if(!unitTarget || unitTarget->GetTypeId() != TYPEID_UNIT)
|
||||
return;
|
||||
|
||||
Creature* creatureTarget = (Creature*)unitTarget;
|
||||
|
||||
creatureTarget->setDeathState(JUST_DIED);
|
||||
creatureTarget->RemoveCorpse();
|
||||
creatureTarget->SetHealth(0); // just for nice GM-mode view
|
||||
return;
|
||||
|
||||
}
|
||||
case 52308:
|
||||
{
|
||||
switch(i)
|
||||
|
||||
@@ -483,3 +483,30 @@ void WorldSession::HandleSelfResOpcode( WorldPacket & /*recv_data*/ )
|
||||
}
|
||||
}
|
||||
|
||||
void WorldSession::HandleSpellClick( WorldPacket & recv_data )
|
||||
{
|
||||
CHECK_PACKET_SIZE(recv_data, 8);
|
||||
|
||||
uint64 guid;
|
||||
recv_data >> guid;
|
||||
|
||||
Creature *unit = ObjectAccessor::GetCreatureOrPetOrVehicle(*_player, guid);
|
||||
|
||||
if(!unit)
|
||||
return;
|
||||
|
||||
SpellClickInfoMap const& map = objmgr.mSpellClickInfoMap;
|
||||
for(SpellClickInfoMap::const_iterator itr = map.lower_bound(unit->GetEntry()); itr != map.upper_bound(unit->GetEntry()); ++itr)
|
||||
{
|
||||
if(itr->second.questId == 0 || _player->GetQuestStatus(itr->second.questId) == QUEST_STATUS_INCOMPLETE)
|
||||
{
|
||||
Unit *caster = (itr->second.castFlags & 0x1) ? (Unit*)_player : (Unit*)unit;
|
||||
Unit *target = (itr->second.castFlags & 0x2) ? (Unit*)_player : (Unit*)unit;
|
||||
|
||||
caster->CastSpell(target, itr->second.spellId, true);
|
||||
}
|
||||
}
|
||||
|
||||
if(unit->isVehicle())
|
||||
_player->EnterVehicle((Vehicle*)unit);
|
||||
}
|
||||
|
||||
@@ -1293,6 +1293,9 @@ void World::SetInitialWorldSettings()
|
||||
sLog.outString( ">>> Quests Relations loaded" );
|
||||
sLog.outString();
|
||||
|
||||
sLog.outString( "Loading UNIT_NPC_FLAG_SPELLCLICK Data..." );
|
||||
objmgr.LoadNPCSpellClickSpells();
|
||||
|
||||
sLog.outString( "Loading SpellArea Data..." ); // must be after quest load
|
||||
spellmgr.LoadSpellAreas();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user