mirror of
https://github.com/jmarshall23/DoomRTX.git
synced 2026-08-16 16:20:42 +02:00
Fixes for the bots.
This commit is contained in:
@@ -9,6 +9,143 @@ idCVar bot_itemsfile( "bot_itemsfile", "items.c", CVAR_CHEAT, "" );
|
||||
|
||||
idBotGoalManager botGoalManager;
|
||||
|
||||
|
||||
static bool BotGoalItemNameContains( const iteminfo_t* itemInfo, const char* needle ) {
|
||||
if( itemInfo == NULL || needle == NULL ) {
|
||||
return false;
|
||||
}
|
||||
return strstr( itemInfo->classname.c_str(), needle ) != NULL || strstr( itemInfo->name.c_str(), needle ) != NULL;
|
||||
}
|
||||
|
||||
static bool BotGoalHasUsableWeapon( const int* inventory ) {
|
||||
if( inventory == NULL ) {
|
||||
return false;
|
||||
}
|
||||
return ( inventory[INVENTORY_ROCKETLAUNCHER] > 0 && inventory[INVENTORY_ROCKETS] > 0 ) ||
|
||||
( inventory[INVENTORY_PLASMAGUN] > 0 && inventory[INVENTORY_CELLS] > 0 ) ||
|
||||
( inventory[INVENTORY_SHOTGUN] > 0 && inventory[INVENTORY_SHELLS] > 0 ) ||
|
||||
( inventory[INVENTORY_MACHINEGUN] > 0 && inventory[INVENTORY_BULLETS] > 0 );
|
||||
}
|
||||
|
||||
static bool BotGoalHasPowerWeapon( const int* inventory ) {
|
||||
if( inventory == NULL ) {
|
||||
return false;
|
||||
}
|
||||
return ( inventory[INVENTORY_ROCKETLAUNCHER] > 0 && inventory[INVENTORY_ROCKETS] > 0 ) ||
|
||||
( inventory[INVENTORY_PLASMAGUN] > 0 && inventory[INVENTORY_CELLS] > 0 ) ||
|
||||
( inventory[INVENTORY_SHOTGUN] > 0 && inventory[INVENTORY_SHELLS] > 0 );
|
||||
}
|
||||
|
||||
static bool BotGoalHasOnlyPistolOrDryWeapons( const int* inventory ) {
|
||||
if( inventory == NULL ) {
|
||||
return true;
|
||||
}
|
||||
const bool ownsNonPistolWeapon = inventory[INVENTORY_MACHINEGUN] > 0 ||
|
||||
inventory[INVENTORY_SHOTGUN] > 0 ||
|
||||
inventory[INVENTORY_PLASMAGUN] > 0 ||
|
||||
inventory[INVENTORY_ROCKETLAUNCHER] > 0;
|
||||
if( !ownsNonPistolWeapon ) {
|
||||
return true;
|
||||
}
|
||||
if( !BotGoalHasUsableWeapon( inventory ) ) {
|
||||
return true;
|
||||
}
|
||||
const bool onlyMachinegun = inventory[INVENTORY_MACHINEGUN] > 0 &&
|
||||
inventory[INVENTORY_SHOTGUN] <= 0 &&
|
||||
inventory[INVENTORY_PLASMAGUN] <= 0 &&
|
||||
inventory[INVENTORY_ROCKETLAUNCHER] <= 0;
|
||||
return onlyMachinegun && inventory[INVENTORY_BULLETS] < 25;
|
||||
}
|
||||
|
||||
static float BotGoalCombatItemBias( const iteminfo_t* itemInfo, const int* inventory ) {
|
||||
if( itemInfo == NULL || inventory == NULL ) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const bool desperate = BotGoalHasOnlyPistolOrDryWeapons( inventory );
|
||||
const bool lacksPowerWeapon = !BotGoalHasPowerWeapon( inventory );
|
||||
float bias = 0.0f;
|
||||
|
||||
// Weapons: route through these during a fight, especially if the bot is stuck
|
||||
// on pistol/machinegun or is out of usable ammo.
|
||||
if( BotGoalItemNameContains( itemInfo, "weapon_rocketlauncher" ) || BotGoalItemNameContains( itemInfo, "rocketlauncher" ) ) {
|
||||
if( inventory[INVENTORY_ROCKETLAUNCHER] <= 0 || desperate ) {
|
||||
bias += desperate ? 9000.0f : 4200.0f;
|
||||
}
|
||||
else if( inventory[INVENTORY_ROCKETS] <= 3 ) {
|
||||
bias += 2200.0f;
|
||||
}
|
||||
}
|
||||
else if( BotGoalItemNameContains( itemInfo, "weapon_plasmagun" ) || BotGoalItemNameContains( itemInfo, "plasmagun" ) ) {
|
||||
if( inventory[INVENTORY_PLASMAGUN] <= 0 || desperate ) {
|
||||
bias += desperate ? 8200.0f : 3800.0f;
|
||||
}
|
||||
else if( inventory[INVENTORY_CELLS] <= 18 ) {
|
||||
bias += 1800.0f;
|
||||
}
|
||||
}
|
||||
else if( BotGoalItemNameContains( itemInfo, "weapon_shotgun" ) || BotGoalItemNameContains( itemInfo, "shotgun" ) ) {
|
||||
if( inventory[INVENTORY_SHOTGUN] <= 0 || desperate || lacksPowerWeapon ) {
|
||||
bias += desperate ? 7600.0f : 3200.0f;
|
||||
}
|
||||
else if( inventory[INVENTORY_SHELLS] <= 6 ) {
|
||||
bias += 1600.0f;
|
||||
}
|
||||
}
|
||||
else if( BotGoalItemNameContains( itemInfo, "weapon_machinegun" ) || BotGoalItemNameContains( itemInfo, "machinegun" ) ) {
|
||||
if( inventory[INVENTORY_MACHINEGUN] <= 0 || desperate ) {
|
||||
bias += desperate ? 6200.0f : 1800.0f;
|
||||
}
|
||||
else if( inventory[INVENTORY_BULLETS] <= 30 ) {
|
||||
bias += 1200.0f;
|
||||
}
|
||||
}
|
||||
else if( BotGoalItemNameContains( itemInfo, "ammo_rockets" ) || BotGoalItemNameContains( itemInfo, "rocket_ammo" ) ) {
|
||||
if( inventory[INVENTORY_ROCKETLAUNCHER] > 0 && inventory[INVENTORY_ROCKETS] <= 5 ) {
|
||||
bias += inventory[INVENTORY_ROCKETS] <= 1 ? 4200.0f : 2400.0f;
|
||||
}
|
||||
}
|
||||
else if( BotGoalItemNameContains( itemInfo, "ammo_cells" ) || BotGoalItemNameContains( itemInfo, "cells" ) ) {
|
||||
if( inventory[INVENTORY_PLASMAGUN] > 0 && inventory[INVENTORY_CELLS] <= 25 ) {
|
||||
bias += inventory[INVENTORY_CELLS] <= 8 ? 3600.0f : 2000.0f;
|
||||
}
|
||||
}
|
||||
else if( BotGoalItemNameContains( itemInfo, "ammo_shells" ) || BotGoalItemNameContains( itemInfo, "shells" ) ) {
|
||||
if( inventory[INVENTORY_SHOTGUN] > 0 && inventory[INVENTORY_SHELLS] <= 10 ) {
|
||||
bias += inventory[INVENTORY_SHELLS] <= 3 ? 3200.0f : 1700.0f;
|
||||
}
|
||||
}
|
||||
else if( BotGoalItemNameContains( itemInfo, "ammo_clip" ) || BotGoalItemNameContains( itemInfo, "bullets" ) ) {
|
||||
if( inventory[INVENTORY_MACHINEGUN] > 0 && inventory[INVENTORY_BULLETS] <= 45 ) {
|
||||
bias += inventory[INVENTORY_BULLETS] <= 10 ? 2800.0f : 1300.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// Health/armor should also interrupt a fight, but only when it is tactically
|
||||
// worth the detour. This still lets fuzzy item weights handle normal roaming.
|
||||
if( BotGoalItemNameContains( itemInfo, "health" ) ) {
|
||||
if( inventory[INVENTORY_HEALTH] < 35 ) {
|
||||
bias += 5200.0f;
|
||||
}
|
||||
else if( inventory[INVENTORY_HEALTH] < 65 ) {
|
||||
bias += 2600.0f;
|
||||
}
|
||||
else if( inventory[INVENTORY_HEALTH] < 95 ) {
|
||||
bias += 900.0f;
|
||||
}
|
||||
}
|
||||
if( BotGoalItemNameContains( itemInfo, "armor" ) ) {
|
||||
if( inventory[INVENTORY_ARMOR] < 25 ) {
|
||||
bias += 2400.0f;
|
||||
}
|
||||
else if( inventory[INVENTORY_ARMOR] < 75 ) {
|
||||
bias += 1100.0f;
|
||||
}
|
||||
}
|
||||
|
||||
return bias;
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
idBotGoalManager::idBotGoalManager
|
||||
@@ -261,24 +398,131 @@ itemconfig_t* idBotGoalManager::LoadItemConfig( char* filename )
|
||||
idBotGoalManager::ItemWeightIndex
|
||||
========================
|
||||
*/
|
||||
int* idBotGoalManager::ItemWeightIndex( weightconfig_t* iwc, itemconfig_t* ic )
|
||||
{
|
||||
int* index, i;
|
||||
/*
|
||||
================
|
||||
Bot_ItemWeightAlias
|
||||
|
||||
//initialize item weight index
|
||||
index = new int[( sizeof( int ) * ic->numiteminfo )]; // jmarshall: Get this off of the heap!
|
||||
Some itemconfig entries come from Quake 3 / Team Arena style bot data, but the
|
||||
Doom 3 weight config usually does not contain every one of those classnames.
|
||||
|
||||
for( i = 0; i < ic->numiteminfo; i++ )
|
||||
{
|
||||
index[i] = botFuzzyWeightManager.FindFuzzyWeight( iwc, ( char* )ic->iteminfo[i].classname.c_str() );
|
||||
if( index[i] < 0 )
|
||||
{
|
||||
common->Warning( "item info %d \"%s\" has no fuzzy weight\r\n", i, ic->iteminfo[i].classname.c_str() );
|
||||
The item weight index must still point at a valid fuzzy weight, so we alias
|
||||
unsupported / missing items to the closest useful existing Doom 3 weight.
|
||||
================
|
||||
*/
|
||||
static const char* Bot_ItemWeightAlias(const char* classname) {
|
||||
if (!classname || !classname[0]) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct itemWeightAlias_t {
|
||||
const char* missingName;
|
||||
const char* fallbackName;
|
||||
};
|
||||
|
||||
static const itemWeightAlias_t aliases[] = {
|
||||
// Missing weapon names commonly seen in Q3 bot item configs.
|
||||
{ "weapon_fists", "weapon_pistol" },
|
||||
{ "weapon_grapplinghook", "weapon_chainsaw" },
|
||||
{ "weapon_grenadelauncher", "weapon_rocketlauncher" },
|
||||
{ "weapon_lightning", "weapon_plasmagun" },
|
||||
{ "weapon_machinegun", "weapon_machinegun" },
|
||||
{ "weapon_railgun", "weapon_rocketlauncher" },
|
||||
{ "weapon_nailgun", "weapon_chaingun" },
|
||||
{ "weapon_prox_launcher", "weapon_rocketlauncher" },
|
||||
|
||||
// Team Arena / CTF objective entities. These are not normal pickup goals.
|
||||
// Give them a valid low-risk fallback so the index is never invalid.
|
||||
{ "team_redobelisk", "item_armor_shard" },
|
||||
{ "team_blueobelisk", "item_armor_shard" },
|
||||
{ "team_neutralobelisk", "item_armor_shard" },
|
||||
|
||||
{ NULL, NULL }
|
||||
};
|
||||
|
||||
for (int i = 0; aliases[i].missingName != NULL; i++) {
|
||||
if (idStr::Icmp(classname, aliases[i].missingName) == 0) {
|
||||
return aliases[i].fallbackName;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
================
|
||||
idBotGoalManager::ItemWeightIndex
|
||||
================
|
||||
*/
|
||||
int* idBotGoalManager::ItemWeightIndex(weightconfig_t* iwc, itemconfig_t* ic) {
|
||||
int* index;
|
||||
int i;
|
||||
|
||||
if (iwc == NULL || ic == NULL || ic->numiteminfo <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// initialize item weight index
|
||||
index = new int[ic->numiteminfo]; // jmarshall: Get this off of the heap!
|
||||
|
||||
for (i = 0; i < ic->numiteminfo; i++) {
|
||||
const char* classname = ic->iteminfo[i].classname.c_str();
|
||||
|
||||
index[i] = botFuzzyWeightManager.FindFuzzyWeight(iwc, (char*)classname);
|
||||
|
||||
if (index[i] < 0) {
|
||||
const char* aliasName = Bot_ItemWeightAlias(classname);
|
||||
|
||||
if (aliasName != NULL) {
|
||||
index[i] = botFuzzyWeightManager.FindFuzzyWeight(iwc, (char*)aliasName);
|
||||
|
||||
if (index[i] >= 0) {
|
||||
common->Printf(
|
||||
"Bot item weight alias: item info %d \"%s\" using \"%s\"\n",
|
||||
i,
|
||||
classname,
|
||||
aliasName
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Last-resort fallback. This keeps the bot goal system from storing
|
||||
// invalid fuzzy weight indexes even if the expected alias is missing.
|
||||
static const char* safeFallbacks[] = {
|
||||
"item_armor_shard",
|
||||
"item_armor",
|
||||
"ammo_clip",
|
||||
"weapon_pistol",
|
||||
"weapon_shotgun",
|
||||
"weapon_machinegun",
|
||||
NULL
|
||||
};
|
||||
|
||||
for (int j = 0; safeFallbacks[j] != NULL; j++) {
|
||||
index[i] = botFuzzyWeightManager.FindFuzzyWeight(iwc, (char*)safeFallbacks[j]);
|
||||
if (index[i] >= 0) {
|
||||
common->Printf(
|
||||
"Bot item weight fallback: item info %d \"%s\" using \"%s\"\n",
|
||||
i,
|
||||
classname,
|
||||
safeFallbacks[j]
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (index[i] < 0) {
|
||||
common->Warning(
|
||||
"item info %d \"%s\" has no fuzzy weight and no fallback weight\r\n",
|
||||
i,
|
||||
classname
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
/*
|
||||
========================
|
||||
idBotGoalManager::InitLevelItemHeap
|
||||
@@ -490,6 +734,11 @@ void idBotGoalManager::InitLevelItems( void )
|
||||
//for (ent = AAS_NextBSPEntity(0); ent; ent = AAS_NextBSPEntity(ent))
|
||||
for( int idx = 0; idx < gameLocal.num_entities; idx++ )
|
||||
{
|
||||
if( gameLocal.entities[idx] == NULL )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
idItem* ent = gameLocal.entities[idx]->Cast<idItem>();
|
||||
|
||||
if( ent == nullptr )
|
||||
@@ -606,6 +855,12 @@ void idBotGoalManager::BotGoalName( int number, char* name, int size )
|
||||
{
|
||||
levelitem_t* li;
|
||||
|
||||
if( name == NULL || size <= 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
name[0] = '\0';
|
||||
|
||||
if( !itemconfig )
|
||||
{
|
||||
return;
|
||||
@@ -615,8 +870,7 @@ void idBotGoalManager::BotGoalName( int number, char* name, int size )
|
||||
{
|
||||
if( li->number == number )
|
||||
{
|
||||
name[size - 1] = '\0';
|
||||
strcpy( name, itemconfig->iteminfo[li->iteminfo].name );
|
||||
idStr::Copynz( name, itemconfig->iteminfo[li->iteminfo].name, size );
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -858,7 +1112,7 @@ int idBotGoalManager::BotGetLevelItemGoal( int index, char* name, bot_goal_t* go
|
||||
{
|
||||
// goal->areanum = li->goalareanum;
|
||||
goal->origin = li->goalorigin;
|
||||
goal->entitynum = li->item->entityNumber;
|
||||
goal->entitynum = li->item != NULL ? li->item->entityNumber : 0;
|
||||
goal->mins = itemconfig->iteminfo[li->iteminfo].mins;
|
||||
goal->maxs = itemconfig->iteminfo[li->iteminfo].maxs;
|
||||
goal->number = li->number;
|
||||
@@ -953,9 +1207,14 @@ void idBotGoalManager::BotFindEntityForLevelItem( levelitem_t* li )
|
||||
}
|
||||
for( int idx = 0; idx < gameLocal.num_entities; idx++ )
|
||||
{
|
||||
if( gameLocal.entities[idx] == NULL )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
idItem* ent = gameLocal.entities[idx]->Cast<idItem>();
|
||||
|
||||
if( ent == NULL )
|
||||
if( ent == nullptr )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -1032,6 +1291,11 @@ void idBotGoalManager::UpdateEntityItems( void )
|
||||
//
|
||||
for( int idx = 0; idx < gameLocal.num_entities; idx++ )
|
||||
{
|
||||
if( gameLocal.entities[idx] == NULL )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
idItem* ent = gameLocal.entities[idx]->Cast<idItem>();
|
||||
|
||||
if( ent == nullptr )
|
||||
@@ -1430,17 +1694,25 @@ int idBotGoalManager::BotChooseLTGItem( int goalstate, idVec3 origin, int* inven
|
||||
}
|
||||
//get the fuzzy weight function for this item
|
||||
iteminfo = &ic->iteminfo[li->iteminfo];
|
||||
const float tacticalBias = BotGoalCombatItemBias( iteminfo, inventory );
|
||||
weightnum = gs->itemweightindex[iteminfo->number];
|
||||
if( weightnum < 0 )
|
||||
{
|
||||
continue;
|
||||
if( tacticalBias <= 0.0f )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
weight = 0.0f;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
#ifdef UNDECIDEDFUZZY
|
||||
weight = FuzzyWeightUndecided( inventory, gs->itemweightconfig, weightnum );
|
||||
weight = FuzzyWeightUndecided( inventory, gs->itemweightconfig, weightnum );
|
||||
#else
|
||||
weight = botFuzzyWeightManager.FuzzyWeight( inventory, gs->itemweightconfig, weightnum );
|
||||
weight = botFuzzyWeightManager.FuzzyWeight( inventory, gs->itemweightconfig, weightnum );
|
||||
#endif //UNDECIDEDFUZZY
|
||||
}
|
||||
weight += tacticalBias;
|
||||
#ifdef DROPPEDWEIGHT
|
||||
//HACK: to make dropped items more attractive
|
||||
if( li->timeout )
|
||||
@@ -1517,7 +1789,7 @@ int idBotGoalManager::BotChooseLTGItem( int goalstate, idVec3 origin, int* inven
|
||||
VectorCopy( iteminfo->mins, goal.mins );
|
||||
VectorCopy( iteminfo->maxs, goal.maxs );
|
||||
// goal.areanum = bestitem->goalareanum;
|
||||
goal.entitynum = bestitem->item->entityNumber;
|
||||
goal.entitynum = bestitem->item != NULL ? bestitem->item->entityNumber : 0;
|
||||
goal.number = bestitem->number;
|
||||
goal.flags = GFL_ITEM;
|
||||
if( bestitem->timeout )
|
||||
@@ -1656,17 +1928,25 @@ int idBotGoalManager::BotChooseNBGItem( int goalstate, idVec3 origin, int* inven
|
||||
}
|
||||
//get the fuzzy weight function for this item
|
||||
iteminfo = &ic->iteminfo[li->iteminfo];
|
||||
const float tacticalBias = BotGoalCombatItemBias( iteminfo, inventory );
|
||||
weightnum = gs->itemweightindex[iteminfo->number];
|
||||
if( weightnum < 0 )
|
||||
{
|
||||
continue;
|
||||
if( tacticalBias <= 0.0f )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
weight = 0.0f;
|
||||
}
|
||||
//
|
||||
else
|
||||
{
|
||||
#ifdef UNDECIDEDFUZZY
|
||||
weight = FuzzyWeightUndecided( inventory, gs->itemweightconfig, weightnum );
|
||||
weight = FuzzyWeightUndecided( inventory, gs->itemweightconfig, weightnum );
|
||||
#else
|
||||
weight = botFuzzyWeightManager.FuzzyWeight( inventory, gs->itemweightconfig, weightnum );
|
||||
weight = botFuzzyWeightManager.FuzzyWeight( inventory, gs->itemweightconfig, weightnum );
|
||||
#endif //UNDECIDEDFUZZY
|
||||
}
|
||||
weight += tacticalBias;
|
||||
#ifdef DROPPEDWEIGHT
|
||||
//HACK: to make dropped items more attractive
|
||||
if( li->timeout )
|
||||
@@ -1709,8 +1989,11 @@ int idBotGoalManager::BotChooseNBGItem( int goalstate, idVec3 origin, int* inven
|
||||
t = gameLocal.TravelTimeToGoal( li->goalorigin, ltg->origin );
|
||||
// jmarshall end
|
||||
} //end if
|
||||
//if the travel back is possible and doesn't take too long
|
||||
if( t <= ltg_time )
|
||||
// For ordinary detours, keep the original "on the way" check. For tactical
|
||||
// combat pickups such as a missing weapon, ammo, or low-health pickup, allow
|
||||
// a short off-route move because surviving the fight is more important than
|
||||
// staying glued to the enemy/chase goal.
|
||||
if( tacticalBias > 0.0f || t <= ltg_time )
|
||||
{
|
||||
bestweight = weight;
|
||||
bestitem = li;
|
||||
@@ -1730,7 +2013,7 @@ int idBotGoalManager::BotChooseNBGItem( int goalstate, idVec3 origin, int* inven
|
||||
VectorCopy( iteminfo->mins, goal.mins );
|
||||
VectorCopy( iteminfo->maxs, goal.maxs );
|
||||
// goal.areanum = bestitem->goalareanum;
|
||||
goal.entitynum = bestitem->item->entityNumber;
|
||||
goal.entitynum = bestitem->item != NULL ? bestitem->item->entityNumber : 0;
|
||||
goal.number = bestitem->number;
|
||||
goal.flags = GFL_ITEM;
|
||||
if( bestitem->timeout )
|
||||
@@ -1818,7 +2101,7 @@ int idBotGoalManager::BotItemGoalInVisButNotVisible( int viewer, idVec3 eye, idA
|
||||
return false;
|
||||
}
|
||||
|
||||
VectorAdd( goal->mins, goal->mins, middle );
|
||||
VectorAdd( goal->mins, goal->maxs, middle );
|
||||
VectorScale( middle, 0.5, middle );
|
||||
VectorAdd( goal->origin, middle, middle );
|
||||
|
||||
@@ -1926,11 +2209,13 @@ void idBotGoalManager::BotFreeItemWeights( int goalstate )
|
||||
if( gs->itemweightconfig )
|
||||
{
|
||||
botFuzzyWeightManager.FreeWeightConfig( gs->itemweightconfig );
|
||||
gs->itemweightconfig = NULL;
|
||||
}
|
||||
if( gs->itemweightindex )
|
||||
{
|
||||
delete[] gs->itemweightindex;
|
||||
gs->itemweightindex = NULL;
|
||||
}
|
||||
// jmarshall - eval
|
||||
//if (gs->itemweightindex)
|
||||
// FreeMemory(gs->itemweightindex);
|
||||
// jmarshall end
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user