Fixed humans winning on round end when no humans present, removed classes file cvar and made it use the config module, replaced SetFailState with LogMessageFormatted with fail flag, made the base config manipulator function.
This commit is contained in:
parent
8da309e4f4
commit
0404230fc8
331
src/zr/config.inc
Normal file
331
src/zr/config.inc
Normal file
@ -0,0 +1,331 @@
|
||||
/*
|
||||
* ============================================================================
|
||||
*
|
||||
* Zombie:Reloaded
|
||||
*
|
||||
* File: config.inc
|
||||
* Description: Config API and executing.
|
||||
*
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* @section List of config files under this modules control.
|
||||
*/
|
||||
#define CONFIG_FILE_MODELS 0
|
||||
#define CONFIG_FILE_DOWNLOADS 1
|
||||
#define CONFIG_FILE_PLAYERCLASSES 2
|
||||
#define CONFIG_FILE_WEAPONS 3
|
||||
#define CONFIG_FILE_WEAPONGROUPS 4
|
||||
#define CONFIG_FILE_HITGROUPS 5
|
||||
/**
|
||||
* @endsection
|
||||
*/
|
||||
|
||||
/**
|
||||
* @section Config file flags.
|
||||
*/
|
||||
#define CONFIG_FILE_FLAG_MODELS 1
|
||||
#define CONFIG_FILE_FLAG_DOWNLOADS 2
|
||||
#define CONFIG_FILE_FLAG_PLAYERCLASSES 4
|
||||
#define CONFIG_FILE_FLAG_WEAPONS 8
|
||||
#define CONFIG_FILE_FLAG_WEAPONGROUPS 16
|
||||
#define CONFIG_FILE_FLAG_HITGROUPS 32
|
||||
/**
|
||||
* @endsection
|
||||
*/
|
||||
|
||||
/**
|
||||
* The max length of a config/value string.
|
||||
*/
|
||||
#define CONFIG_OPTION_MAX_LENGTH 32
|
||||
|
||||
enum ConfigKeyvalueAction
|
||||
{
|
||||
Create, /** Create a key. */
|
||||
Delete, /** Delete a key. */
|
||||
Set, /** Modify setting of a key. */
|
||||
Get, /** Get setting of a key. */
|
||||
}
|
||||
|
||||
/**
|
||||
* @section Global data handle initializations.
|
||||
*/
|
||||
new Handle:arrayModelsList = INVALID_HANDLE;
|
||||
new Handle:kvClassData = INVALID_HANDLE;
|
||||
new Handle:kvWeapons = INVALID_HANDLE;
|
||||
new Handle:kvWeaponGroups = INVALID_HANDLE;
|
||||
new Handle:kvHitgroups = INVALID_HANDLE;
|
||||
/**
|
||||
* Load plugin configs.
|
||||
*/
|
||||
ConfigLoad()
|
||||
{
|
||||
decl String:mapconfig[PLATFORM_MAX_PATH];
|
||||
|
||||
// Get map name and format into config path.
|
||||
GetCurrentMap(mapconfig, sizeof(mapconfig));
|
||||
Format(mapconfig, sizeof(mapconfig), "sourcemod/zombiereloaded/%s.cfg", mapconfig);
|
||||
|
||||
// Prepend cfg to path.
|
||||
decl String:path[PLATFORM_MAX_PATH];
|
||||
Format(path, sizeof(path), "cfg/%s", mapconfig);
|
||||
|
||||
// File doesn't exist, then stop.
|
||||
if (!FileExists(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute config file.
|
||||
ServerCommand("exec %s", mapconfig);
|
||||
|
||||
// Log action.
|
||||
if (LogCheckFlag(LOG_CORE_EVENTS))
|
||||
{
|
||||
LogMessageFormatted(-1, "", "", "Executed map config file: %s.", LOG_FORMAT_TYPE_SIMPLE, mapconfig);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load config file.
|
||||
*
|
||||
* @param file The cvar define of the path to the file.
|
||||
* @return True if the file exists, false if not.
|
||||
*/
|
||||
bool:ConfigGetFilePath(CvarsList:cvar, String:path[])
|
||||
{
|
||||
// Get cvar's path.
|
||||
decl String:filepath[PLATFORM_MAX_PATH];
|
||||
GetConVarString(g_hCvarsList[cvar], filepath, sizeof(filepath));
|
||||
|
||||
// Build full path in return string.
|
||||
BuildPath(Path_SM, path, PLATFORM_MAX_PATH, filepath);
|
||||
|
||||
return FileExists(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates, deletes, sets, or gets any key/setting of any ZR config keyvalue file in memory.
|
||||
* Only use when interacting with a command or manipulating single keys/values,
|
||||
* using this function everywhere would be EXTREMELY inefficient.
|
||||
*
|
||||
* @param config Config index of config to modify. (see CONFIG_FILE_* defines)
|
||||
* @param action Action to perform on keyvalue tree. (see enum ConfigKeyvalueAction)
|
||||
* @param keys Array containing keys to traverse into.
|
||||
* @param keysMax The size of the 'keys' array.
|
||||
* @param setting (Optional) The name of the setting to modify.
|
||||
* @param value (Optional) The new value to set.
|
||||
* @param maxlen (Optional) The maxlength of the gotten value.
|
||||
* @return True if the change was made successfully, false otherwise.
|
||||
*/
|
||||
bool:ConfigKeyvalueTreeSetting(config, ConfigKeyvalueAction:action = Create, const String:keys[][], keysMax, const String:setting[] = "", String:value[] = "", maxlen = 0)
|
||||
{
|
||||
// Retrieve handle of the keyvalue tree.
|
||||
new Handle:hConfig = ConfigGetFileHandle(config);
|
||||
|
||||
// If handle is invalid, then stop.
|
||||
if (hConfig == INVALID_HANDLE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rewind keyvalue tree.
|
||||
KvRewind(hConfig);
|
||||
|
||||
// x = keys index.
|
||||
// Traverse into the keygroup, stop if it fails.
|
||||
for (new x = 0; x < keysMax; x++)
|
||||
{
|
||||
// If key is empty, then break the loop.
|
||||
if (!keys[x][0])
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Try to jump to next level in the transversal stack, create key if specified.
|
||||
new bool:exists = KvJumpToKey(hConfig, keys[x], (action == Create));
|
||||
|
||||
// If exists is false, then stop.
|
||||
if (!exists)
|
||||
{
|
||||
// Key doesn't exist.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
switch(action)
|
||||
{
|
||||
case Create:
|
||||
{
|
||||
if (!setting[0] || !value[0])
|
||||
{
|
||||
// We created the key already, so return true.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Set new value.
|
||||
KvSetString(hConfig, setting, value);
|
||||
}
|
||||
case Delete:
|
||||
{
|
||||
// Return deletion result.
|
||||
return KvDeleteKey(hConfig, setting);
|
||||
}
|
||||
case Set:
|
||||
{
|
||||
// Set new value.
|
||||
KvSetString(hConfig, setting, value);
|
||||
}
|
||||
case Get:
|
||||
{
|
||||
// Get current value.
|
||||
KvGetString(hConfig, setting, value, maxlen);
|
||||
}
|
||||
}
|
||||
|
||||
// We successfully set or got the value.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return handle to array or keygroup for globally stored data.
|
||||
*
|
||||
* @param configindex Index of the config. (see CONFIG_FILE_* defines)
|
||||
*/
|
||||
Handle:ConfigGetFileHandle(config)
|
||||
{
|
||||
switch(config)
|
||||
{
|
||||
case CONFIG_FILE_MODELS:
|
||||
{
|
||||
// Return model list array handle.
|
||||
return arrayModelsList;
|
||||
}
|
||||
case CONFIG_FILE_DOWNLOADS:
|
||||
{
|
||||
// We don't store download data.
|
||||
return INVALID_HANDLE;
|
||||
}
|
||||
case CONFIG_FILE_PLAYERCLASSES:
|
||||
{
|
||||
// Return class config keyvalue file handle.
|
||||
return kvClassData;
|
||||
}
|
||||
case CONFIG_FILE_WEAPONS:
|
||||
{
|
||||
// Return weapon config keyvalue file handle.
|
||||
return kvWeapons;
|
||||
}
|
||||
case CONFIG_FILE_WEAPONGROUPS:
|
||||
{
|
||||
// Return weapon groups config keyvalue file handle.
|
||||
return kvWeaponGroups;
|
||||
}
|
||||
case CONFIG_FILE_HITGROUPS:
|
||||
{
|
||||
// Return hitgroups config keyvalue file handle.
|
||||
return kvHitgroups;
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid config index.
|
||||
return INVALID_HANDLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through a file and store each line in an array.
|
||||
*
|
||||
* @param path Path to the file to iterate through.
|
||||
* @return The handle of the array, don't forget to call CloseHandle
|
||||
* on it when finished!
|
||||
*/
|
||||
Handle:ConfigLinesToArray(const String:path[])
|
||||
{
|
||||
new Handle:arrayLines = CreateArray(PLATFORM_MAX_PATH);
|
||||
decl String:line[PLATFORM_MAX_PATH];
|
||||
|
||||
// Open file.
|
||||
new Handle:hFile = OpenFile(path, "r");
|
||||
|
||||
// If file couldn't be opened, then stop.
|
||||
if (hFile == INVALID_HANDLE)
|
||||
{
|
||||
return INVALID_HANDLE;
|
||||
}
|
||||
|
||||
while(!IsEndOfFile(hFile))
|
||||
{
|
||||
// Get current line text.
|
||||
ReadFileLine(hFile, line, sizeof(line));
|
||||
|
||||
// If line contains a ";", then stop.
|
||||
if (StrContains(line, ";") > -1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Cut out comments at the end of a line.
|
||||
if (StrContains(line, "//") > -1)
|
||||
{
|
||||
SplitString(line, "//", line, sizeof(line));
|
||||
}
|
||||
|
||||
// Trim off whitespace.
|
||||
TrimString(line);
|
||||
|
||||
// If line is empty, then stop.
|
||||
if (!line[0])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Push line into array.
|
||||
PushArrayString(arrayLines, line);
|
||||
}
|
||||
|
||||
// Close file handle.
|
||||
CloseHandle(hFile);
|
||||
|
||||
// Return array handle.
|
||||
return arrayLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts string of "yes" or "no" to a boolean value.
|
||||
*
|
||||
* @param option "yes" or "no" string to be converted.
|
||||
* @return True if string is "yes", false otherwise.
|
||||
*/
|
||||
bool:ConfigSettingToBool(const String:option[])
|
||||
{
|
||||
// If option is equal to "yes," then return true.
|
||||
if (StrEqual(option, "yes", false))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Option isn't "yes."
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts boolean value to "yes" or "no".
|
||||
*
|
||||
* @param bOption True/false value to be converted to "yes"/"no", respectively.
|
||||
* @param option Variable to store "yes" or "no" in.
|
||||
* @param maxlen Max length of return string, (can't be more than 4)
|
||||
*/
|
||||
ConfigBoolToSetting(bool:bOption, String:option[], maxlen)
|
||||
{
|
||||
// If option is true, then copy "yes" to return string.
|
||||
if (bOption)
|
||||
{
|
||||
strcopy(option, maxlen, "yes");
|
||||
}
|
||||
// If option is false, then copy "no" to return string.
|
||||
else
|
||||
{
|
||||
strcopy(option, maxlen, "no");
|
||||
}
|
||||
}
|
@ -35,7 +35,6 @@ enum CvarsList
|
||||
Handle:CVAR_CLASSES_DEFAULT_ZOMBIE,
|
||||
Handle:CVAR_CLASSES_DEFAULT_HUMAN,
|
||||
Handle:CVAR_CLASSES_DEFAULT_ADMIN,
|
||||
Handle:CVAR_CLASSES_FILE,
|
||||
Handle:CVAR_WEAPONS,
|
||||
Handle:CVAR_WEAPONS_RESTRICT,
|
||||
Handle:CVAR_WEAPONS_ZMARKET_BUYZONE,
|
||||
@ -222,8 +221,6 @@ CvarsCreate()
|
||||
// Old Desc:
|
||||
g_hCvarsList[CVAR_CLASSES_DEFAULT_ADMIN] = CreateConVar("zr_classes_default_admin", "random", "");
|
||||
// Old Desc: Default admin-only class selected for admins when they connect. Use \"random\" to select a random class, or blank to use class config defaults.
|
||||
g_hCvarsList[CVAR_CLASSES_FILE] = CreateConVar("zr_classes_file", "configs/zr/playerclasses.txt", "");
|
||||
// Old Desc: Class data file to read from, in Valves key/values format. The path is relative to the \"sourcemod\" folder.
|
||||
|
||||
// ===========================
|
||||
// Weapons (core)
|
||||
|
@ -10,9 +10,10 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Array to store keyvalue data.
|
||||
* Keyvalue handle to store hitgroups data.
|
||||
*
|
||||
* @redir config.inc
|
||||
*/
|
||||
new Handle:kvHitgroups = INVALID_HANDLE;
|
||||
|
||||
/**
|
||||
* @section Player hitgroup values.
|
||||
@ -155,7 +156,7 @@ bool:HitgroupsCanDamageHitgroup(hitgroup)
|
||||
KvGetString(kvHitgroups, "damage", damage, sizeof(damage), "yes");
|
||||
|
||||
// Return hitgroup's damage setting.
|
||||
return ZRConfigSettingToBool(damage);
|
||||
return ConfigSettingToBool(damage);
|
||||
}
|
||||
} while (KvGotoNextKey(kvHitgroups));
|
||||
}
|
||||
|
@ -21,8 +21,9 @@
|
||||
|
||||
/**
|
||||
* Array that stores a list of validated models.
|
||||
*
|
||||
* @redir config.inc
|
||||
*/
|
||||
new Handle:arrayModelsList = INVALID_HANDLE;
|
||||
|
||||
ModelsLoad()
|
||||
{
|
||||
|
@ -233,7 +233,11 @@ enum ClassAttributes
|
||||
Float:class_jump_distance
|
||||
}
|
||||
|
||||
new Handle:kvClassData;
|
||||
/**
|
||||
* Keyvalue handle to store class data.
|
||||
*
|
||||
* @redir config.inc
|
||||
*/
|
||||
|
||||
/**
|
||||
* The original class data. This array only changed when class data is loaded.
|
||||
@ -302,23 +306,30 @@ ClassLoad()
|
||||
}
|
||||
kvClassData = CreateKeyValues("classes");
|
||||
|
||||
decl String:classfile[PLATFORM_MAX_PATH];
|
||||
GetConVarString(g_hCvarsList[CVAR_CLASSES_FILE], classfile, sizeof(classfile));
|
||||
// Get weapons config path.
|
||||
decl String:pathclasses[PLATFORM_MAX_PATH];
|
||||
new bool:exists = ConfigGetFilePath(CVAR_CONFIG_PATH_PLAYERCLASSES, pathclasses);
|
||||
|
||||
// Try to load the class configuration file.
|
||||
decl String:path[PLATFORM_MAX_PATH];
|
||||
BuildPath(Path_SM, path, sizeof(path), classfile);
|
||||
|
||||
if (!FileToKeyValues(kvClassData, path))
|
||||
// If file doesn't exist, then log and stop.
|
||||
if (!exists)
|
||||
{
|
||||
SetFailState("Could not load class data file (\"%s\"). Check path in zr_classes_file in the configuration file.", path);
|
||||
// Log failure.
|
||||
if (LogCheckFlag(LOG_CORE_EVENTS, LOG_MODULE_WEAPONS))
|
||||
{
|
||||
LogMessageFormatted(-1, "Classes", "Config Validation", "Missing playerclasses config file: %s", LOG_FORMAT_TYPE_FATALERROR, pathclasses);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Put file data into memory.
|
||||
FileToKeyValues(kvClassData, pathclasses);
|
||||
|
||||
// Try to find the first class.
|
||||
KvRewind(kvClassData);
|
||||
if (!KvGotoFirstSubKey(kvClassData))
|
||||
{
|
||||
SetFailState("Cannot find any classes in \"%s\".", path);
|
||||
LogMessageFormatted(-1, "Classes", "Config Validation", "Can't find any classes in %s", LOG_FORMAT_TYPE_FATALERROR, pathclasses);
|
||||
}
|
||||
|
||||
decl String:name[64];
|
||||
@ -401,7 +412,7 @@ ClassLoad()
|
||||
ClassData[ClassCount][class_enabled] = false;
|
||||
if (LogCheckFlag(LOG_CORE_EVENTS, LOG_MODULE_CLASSES))
|
||||
{
|
||||
LogMessageFormatted(-1, "Classes", "Load", "Warning: Invalid class at index %d, disabled class. Class error flags: %d.", LOG_FORMAT_TYPE_ERROR, ClassCount, ClassErrorFlags);
|
||||
LogMessageFormatted(-1, "Classes", "Config Validation", "Warning: Invalid class at index %d, disabled class. Class error flags: %d.", LOG_FORMAT_TYPE_ERROR, ClassCount, ClassErrorFlags);
|
||||
}
|
||||
}
|
||||
|
||||
@ -412,13 +423,13 @@ ClassLoad()
|
||||
// Validate team requirements.
|
||||
if (!ClassValidateTeamRequirements())
|
||||
{
|
||||
SetFailState("The class configuration doesn't match the team requirements.");
|
||||
LogMessageFormatted(-1, "Classes", "Config Validation", "The class configuration doesn't match the team requirements.", LOG_FORMAT_TYPE_FATALERROR);
|
||||
}
|
||||
|
||||
// Validate team default requirements.
|
||||
if (!ClassValidateTeamDefaults())
|
||||
{
|
||||
SetFailState("Couldn't find a default class for one or more teams. At least one class per team must be marked as default.");
|
||||
LogMessageFormatted(-1, "Classes", "Config Validation", "Couldn't find a default class for one or more teams. At least one class per team must be marked as default.", LOG_FORMAT_TYPE_FATALERROR);
|
||||
}
|
||||
|
||||
// Cache class data.
|
||||
|
@ -252,7 +252,7 @@ RoundEndOutcome:RoundEndReasonToOutcome(reason)
|
||||
public Action:RoundEndTimer(Handle:timer)
|
||||
{
|
||||
// If there aren't clients on both teams, then stop.
|
||||
if (ZRTeamHasClients())
|
||||
if (!ZRTeamHasClients())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
@ -413,7 +413,7 @@ WeaponsMenuMarket(client)
|
||||
decl String:togglebuyzone[64];
|
||||
|
||||
decl String:curSetting[8];
|
||||
ZRBoolToConfigSetting(GetConVarBool(g_hCvarsList[CVAR_WEAPONS_ZMARKET_BUYZONE]), curSetting, sizeof(curSetting));
|
||||
ConfigBoolToSetting(GetConVarBool(g_hCvarsList[CVAR_WEAPONS_ZMARKET_BUYZONE]), curSetting, sizeof(curSetting));
|
||||
|
||||
Format(togglebuyzone, sizeof(togglebuyzone), "%t", "Weapons menu market toggle buyzone", curSetting);
|
||||
|
||||
|
@ -12,9 +12,10 @@
|
||||
new Handle:gRestrictedWeapons = INVALID_HANDLE;
|
||||
|
||||
/**
|
||||
* Array to store keyvalue data.
|
||||
* Keyvalue handle to store weapon groups data.
|
||||
*
|
||||
* @redir config.inc
|
||||
*/
|
||||
new Handle:kvWeaponGroups = INVALID_HANDLE;
|
||||
|
||||
/**
|
||||
* Array that stores the "HookID" to be later unhooked on player disconnect.
|
||||
@ -125,7 +126,7 @@ RestrictDefaultRestrictions()
|
||||
decl String:restrict[8];
|
||||
KvGetString(kvWeapons, "restrict", restrict, sizeof(restrict), "no");
|
||||
|
||||
if (ZRConfigSettingToBool(restrict))
|
||||
if (ConfigSettingToBool(restrict))
|
||||
{
|
||||
new WpnRestrictQuery:output = RestrictRestrict(weapon, display);
|
||||
RestrictPrintRestrictOutput(0, output, display, true);
|
||||
|
@ -27,18 +27,19 @@
|
||||
*/
|
||||
enum WeaponsType
|
||||
{
|
||||
Type_Invalid = -1,
|
||||
Type_Primary = 0,
|
||||
Type_Secondary = 1,
|
||||
Type_Melee = 2,
|
||||
Type_Projectile = 3,
|
||||
Type_Explosive = 4,
|
||||
Type_Invalid = -1, /** Invalid weapon (slot). */
|
||||
Type_Primary = 0, /** Primary weapon slot. */
|
||||
Type_Secondary = 1, /** Secondary weapon slot. */
|
||||
Type_Melee = 2, /** Melee (knife) weapon slot. */
|
||||
Type_Projectile = 3, /** Projectile (grenades, flashbangs, etc) weapon slot. */
|
||||
Type_Explosive = 4, /** Explosive (c4) weapon slot. */
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Array to store keyvalue data.
|
||||
* Keyvalue handle to store weapon data.
|
||||
*
|
||||
* @redir config.inc
|
||||
*/
|
||||
new Handle:kvWeapons = INVALID_HANDLE;
|
||||
|
||||
#include "zr/weapons/restrict"
|
||||
#include "zr/weapons/markethandler"
|
||||
@ -263,7 +264,7 @@ bool:WeaponsIsWeaponMenu(const String:weapon[])
|
||||
KvGetString(kvWeapons, "menu", menu, sizeof(menu), "yes");
|
||||
|
||||
// Return weapon's setting.
|
||||
return ZRConfigSettingToBool(menu);
|
||||
return ConfigSettingToBool(menu);
|
||||
}
|
||||
} while (KvGotoNextKey(kvWeapons));
|
||||
}
|
||||
|
@ -24,45 +24,6 @@ new dxLevel[MAXPLAYERS + 1];
|
||||
*/
|
||||
new bool:g_bZombieSpawned;
|
||||
|
||||
/**
|
||||
* Converts string of "yes" or "no" to a boolean value.
|
||||
*
|
||||
* @param option "yes" or "no" string to be converted.
|
||||
* @return True if string is "yes", false otherwise.
|
||||
*/
|
||||
bool:ZRConfigSettingToBool(const String:option[])
|
||||
{
|
||||
// If option is equal to "yes," then return true.
|
||||
if (StrEqual(option, "yes", false))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Option isn't "yes."
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts boolean value to "yes" or "no".
|
||||
*
|
||||
* @param bOption True/false value to be converted to "yes"/"no", respectively.
|
||||
* @param option Variable to store "yes" or "no" in.
|
||||
* @param maxlen Max length of return string, (can't be more than 4)
|
||||
*/
|
||||
ZRBoolToConfigSetting(bool:bOption, String:option[], maxlen)
|
||||
{
|
||||
// If option is true, then copy "yes" to return string.
|
||||
if (bOption)
|
||||
{
|
||||
strcopy(option, maxlen, "yes");
|
||||
}
|
||||
// If option is false, then copy "no" to return string.
|
||||
else
|
||||
{
|
||||
strcopy(option, maxlen, "no");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an array populated with eligible clients to be zombie.
|
||||
*
|
||||
|
Loading…
Reference in New Issue
Block a user