Fixed unsanitized strings on category addition

Overlooked categories as being unsanitized, as that's the only place
non-numeric values are ever used.  Anyways, it's been fixed for now.

/**
 * Download Preferences
 * Author(s): nosoop
 * File:  downloadprefs.sp
 * Description:	 Allows clients to select categories of files to download.
 * License:	 MIT License
 */

#pragma semicolon 1
#include <sourcemod>

#define PLUGIN_VERSION			"0.7.1"
public Plugin:myinfo = {
	name = "Download Preferences",
	author = "nosoop",
	description = "Client download preferences",
	version = PLUGIN_VERSION,
	url = "http://github.com/nosoop/SM-DownloadPrefs"
}

#define INVALID_DPREFS_ID -1
#define INVALID_DOWNLOAD_CATEGORY -1
#define MAX_SQL_QUERY_LENGTH 512
#define MAX_URL_LENGTH 256

// List of categories that have been registered in this session
#define MAX_DOWNLOAD_PREFERENCES 64
new g_rgiCategories[MAX_DOWNLOAD_PREFERENCES], g_nDownloadPrefs;

// Database name and related handle
#define DATABASE_NAME "downloadprefs"
new Handle:g_hDatabase = INVALID_HANDLE;

// ConVar handles
new Handle:g_hCDownloadURL = INVALID_HANDLE, // sv_downloadurl
	Handle:g_hCDPrefURL = INVALID_HANDLE; // sm_dprefs_downloadurl

// TODO allocate new g_rgDownloadPreferences[MAXPLAYERS][MAX_DOWNLOAD_PREFERENCES];

// TODO perform plugin / category validation

public OnPluginStart() {
	CreateConVar("sm_dprefs_version", PLUGIN_VERSION, _, FCVAR_PLUGIN | FCVAR_NOTIFY);

	g_nDownloadPrefs = 0;
	for (new i = 0; i < MAX_DOWNLOAD_PREFERENCES; i++) {
		g_rgiCategories[i] = INVALID_DOWNLOAD_CATEGORY;
	}

	// Hook to set redirect downloadurl.  If blank, the transmission of sv_downloadurl to the client is not changed.
	g_hCDownloadURL = FindConVar("sv_downloadurl");
	g_hCDPrefURL = CreateConVar("sm_dprefs_downloadurl", "", "Download URL to send to the client.  See README for details.", FCVAR_PLUGIN | FCVAR_SPONLY);

	AutoExecConfig(true);
}

public OnPluginEnd() {
	CloseHandle(g_hDatabase);
}

public APLRes:AskPluginLoad2(Handle:hMySelf, bool:bLate, String:strError[], iMaxErrors) {
	RegPluginLibrary("downloadprefs");

	CreateNative("RegClientDownloadCategory", Native_RegClientDownloadCategory);
	CreateNative("RegClientDownloadFile", Native_RegClientDownloadFile);
	CreateNative("SetClientDownloadPreference", Native_SetClientDownloadPreference);
	CreateNative("GetClientDownloadPreference", Native_GetClientDownloadPreference);
	CreateNative("ClientHasDownloadPreference", Native_ClientHasDownloadPreference);

	// Raw access methods
	CreateNative("SetRawDownloadPreference", Native_SetRawDownloadPreference);
	CreateNative("GetRawDownloadPreference", Native_GetRawDownloadPreference);
	CreateNative("HasRawDownloadPreference", Native_HasRawDownloadPreference);

	// Unstable methods
	CreateNative("RawCategoryInfo", Native_RawCategoryInfo);
	CreateNative("GetActiveCategories", Native_GetActiveCategories);
	CreateNative("CategoryToIdentifier", Native_CategoryToIdentifier);

	g_hDatabase = GetDatabase();

	return APLRes_Success;
}

/**
 * Sends a custom download URL when the client is connecting.
 * This occurs every time the client reconnects (via map change, etc.)
 */
public OnClientAuthorized(client, const String:auth[]) {
	if (!IsFakeClient(client)) {
		SendCustomDownloadURL(client);
	}
}

SendCustomDownloadURL(client) {
	new iSteamID3 = GetSteamAccountID(client);
	new String:sClientDownloadURL[MAX_URL_LENGTH];
	GetConVarString(g_hCDPrefURL, sClientDownloadURL, sizeof(sClientDownloadURL));

	// TODO strip trailing slash off of sm_dprefs_downloadurl?

	if (strlen(sClientDownloadURL) > 0) {
		new String:sSteamID3[32];
		IntToString(iSteamID3, sSteamID3, sizeof(sSteamID3));
		ReplaceString(sClientDownloadURL, sizeof(sClientDownloadURL), "$STEAMID", sSteamID3);
		SendConVarValue(client, g_hCDownloadURL, sClientDownloadURL);
	}
}

/**
 * Resets the download URL to the server-supplied value once the client is fully in-game.
 * This occurs after the client has performed any fast download requests.
 */
public OnClientPostAdminCheck(client) {
	if (!IsFakeClient(client)) {
		new String:sDefaultDownloadURL[MAX_URL_LENGTH];
		GetConVarString(g_hCDownloadURL, sDefaultDownloadURL, sizeof(sDefaultDownloadURL));
		SendConVarValue(client, g_hCDownloadURL, sDefaultDownloadURL);
	}
}

/**
 * Registers a group of files to download.
 * Store the category plus description into the database if nonexistent,
 * returning an ID to the corresponding category.
 */
_:RegClientDownloadCategory(const String:category[], const String:description[], bool:enabled = true) {
	new Handle:hQuery = INVALID_HANDLE, categoryid;
	decl String:sQuery[MAX_SQL_QUERY_LENGTH];

	new String:safeCategory[128];
	SQL_EscapeString(g_hDatabase, category, safeCategory, sizeof(safeCategory));

	// TODO Proper fix for query sanitization and abstraction
	Format(sQuery, sizeof(sQuery), "SELECT categoryid FROM categories WHERE categoryname='%s'",
			safeCategory);
	hQuery = SQL_Query(g_hDatabase, sQuery);

	// Category does not exist; create it.
	if (SQL_GetRowCount(hQuery) == 0 && CloseHandle(hQuery)) {
		new String:error[4];
		new Handle:hStmt = SQL_PrepareQuery(g_hDatabase,
				"INSERT OR REPLACE INTO categories (categoryid, categoryname, categorydesc, enabled) VALUES (NULL, ?, ?, ?)",
				error, sizeof(error));

		SQL_BindParamString(hStmt, 0, category, false);
		SQL_BindParamString(hStmt, 1, description, false);
		SQL_BindParamInt(hStmt, 2, enabled);
		SQL_Execute(hStmt);
		CloseHandle(hStmt);
	}

	Format(sQuery, sizeof(sQuery), "SELECT categoryid FROM categories WHERE categoryname='%s'",
			safeCategory);
	hQuery = SQL_Query(g_hDatabase, sQuery);
	SQL_FetchRow(hQuery);
	categoryid = SQL_FetchInt(hQuery, 0);

	CloseHandle(hQuery);

	return DownloadCategoryAdded(categoryid);
}

public Native_RegClientDownloadCategory(Handle:hPlugin, nParams) {
	decl String:category[512], String:description[512];

	GetNativeString(1, category, sizeof(category));
	GetNativeString(2, description, sizeof(description));
	new bool:bDefault = GetNativeCell(3);

	return RegClientDownloadCategory(category, description, bDefault);
}

/**
 * Registers a file to a category.
 */
RegClientDownloadFile(id, const String:filepath[]) {
	if (!IsValidDownloadCategory(id)) {
		ThrowError("Invalid id %d", id);
	}
	decl String:sQuery[MAX_SQL_QUERY_LENGTH];
	Format(sQuery, sizeof(sQuery),
			"INSERT OR REPLACE INTO files (categoryid, filepath) VALUES (%d, '%s')",
			g_rgiCategories[id], filepath);
	SQL_FastQuery(g_hDatabase, sQuery);
}

public Native_RegClientDownloadFile(Handle:hPlugin, nParams) {
	new id = GetNativeCell(1);
	decl String:filepath[PLATFORM_MAX_PATH]; GetNativeString(2, filepath, sizeof(filepath));

	// TODO perform plugin validation
	RegClientDownloadFile(id, filepath);
}

/**
 * Stores the client's download preference.
 */
SetClientDownloadPreference(client, id, bool:enabled) {
	if (!IsValidDownloadCategory(id)) {
		ThrowError("Invalid id %d", id);
	}
	SetRawDownloadPreference(GetSteamAccountID(client), g_rgiCategories[id], enabled);
}

public Native_SetClientDownloadPreference(Handle:hPlugin, nParams) {
	new client = GetNativeCell(1), id = GetNativeCell(2), bool:enabled = GetNativeCell(3);

	// TODO perform plugin validation
	SetClientDownloadPreference(client, id, enabled);
}

/**
 * Retrieves a client's download preference.  If non-existent, will return the default setting.
 * A client will keep their existing download preference until a map change or reconnect.
 */
bool:GetClientDownloadPreference(client, id) {
	if (!IsValidDownloadCategory(id)) {
		SetFailState("Could not get download preference for ID %d", id);
	}

	return GetRawDownloadPreference(GetSteamAccountID(client), g_rgiCategories[id]);
}

public Native_GetClientDownloadPreference(Handle:hPlugin, nParams) {
	new client = GetNativeCell(1), id = GetNativeCell(2);

	// TODO perform plugin validation
	return GetClientDownloadPreference(client, id);
}

/**
 * Checks whether or not the client has their own download preference set.
 */
bool:ClientHasDownloadPreference(client, id, &any:result = 0) {
	if (!IsValidDownloadCategory(id)) {
		SetFailState("Could not check download preference for %N (ID %d)", client, id);
	}
	return HasRawDownloadPreference(GetSteamAccountID(client), g_rgiCategories[id], result);
}

public Native_ClientHasDownloadPreference(Handle:hPlugin, nParams) {
	new client = GetNativeCell(1), id = GetNativeCell(2), result = GetNativeCellRef(3);
	return ClientHasDownloadPreference(client, id, result);
}

/**
 * Gets the description of the download category.
 */
bool:RawCategoryInfo(categoryid, String:title[], maxTitleLength, String:description[], maxDescLength) {
	decl String:sQuery[MAX_SQL_QUERY_LENGTH];
	new Handle:hQuery = INVALID_HANDLE;
	new bool:bHasRows;

	Format(sQuery, sizeof(sQuery), "SELECT categoryname, categorydesc FROM categories WHERE categoryid=%d",
			categoryid);
	hQuery = SQL_Query(g_hDatabase, sQuery);

	if ((bHasRows = (SQL_GetRowCount(hQuery) > 0))) {
		SQL_FetchRow(hQuery);
		SQL_FetchString(hQuery, 0, title, maxTitleLength);
		SQL_FetchString(hQuery, 1, description, maxDescLength);
	}

	CloseHandle(hQuery);
	return bHasRows;
}

public Native_RawCategoryInfo(Handle:hPlugin, nParams) {
	new categoryid = GetNativeCell(1), maxTitleLength = GetNativeCell(3), maxDescLength = GetNativeCell(5);
	new String:title[maxTitleLength], String:description[maxDescLength];

	new bool:bResult = RawCategoryInfo(categoryid, title, maxTitleLength, description, maxDescLength);

	SetNativeString(2, title, maxTitleLength);
	SetNativeString(3, description, maxDescLength);

	return bResult;
}

/**
 * Adds the specified category to the active category list.
 */
_:DownloadCategoryAdded(categoryid) {
	new bool:bFound = false;

	for (new i = 0; i < g_nDownloadPrefs; i++) {
		bFound |= (categoryid == g_rgiCategories[i]);
		if (bFound) {
			// Category already exists
			return i;
		}
	}

	if (g_nDownloadPrefs < MAX_DOWNLOAD_PREFERENCES) {
		g_rgiCategories[g_nDownloadPrefs] = categoryid;
		return g_nDownloadPrefs++;
	}
	return INVALID_DPREFS_ID;
}

/**
 * Checks if the download category is not invalid.
 */
bool:IsValidDownloadCategory(id) {
	return g_rgiCategories[id] != INVALID_DOWNLOAD_CATEGORY;
}

public Native_GetActiveCategories(Handle:hPlugin, nParams) {
	new size = GetNativeCell(2), start = GetNativeCell(3), nCategories;
	new categoryids[size];

	for (new i = start; i < g_nDownloadPrefs; i++) {
		if (g_rgiCategories[i] != INVALID_DOWNLOAD_CATEGORY) {
			categoryids[nCategories++] = g_rgiCategories[i];
		}
	}

	SetNativeArray(1, categoryids, size);

	return nCategories;
}

/**
 * Provides raw access to update the database for the specified SteamID.
 */
SetRawDownloadPreference(steamid, categoryid, bool:enabled) {
	decl String:sQuery[MAX_SQL_QUERY_LENGTH];

	if (HasRawDownloadPreference(steamid, categoryid)) {
		// Update existing entry.
		Format(sQuery, sizeof(sQuery), "UPDATE downloadprefs SET enabled = %d WHERE sid3 = %d AND categoryid = %d",
				_:enabled, steamid, categoryid);
	} else {
		// Create new entry.
		Format(sQuery, sizeof(sQuery), "INSERT INTO downloadprefs (sid3, categoryid, enabled) VALUES (%d, %d, %d)",
				steamid, categoryid, _:enabled);
	}
	SQL_FastQuery(g_hDatabase, sQuery);
}

public Native_SetRawDownloadPreference(Handle:hPlugin, nParams) {
	new steamid = GetNativeCell(1), categoryid = GetNativeCell(2), bool:enabled = GetNativeCell(3);
	SetRawDownloadPreference(steamid, categoryid, enabled);
}

/**
 * Grants raw access to retrieve the download preference for the specified SteamID and category.
 */
bool:GetRawDownloadPreference(steamid, categoryid) {
	decl String:sQuery[MAX_SQL_QUERY_LENGTH];
	new bool:bPreferenceEnabled;

	if (!HasRawDownloadPreference(steamid, categoryid, bPreferenceEnabled)) {
		// Default preference for category
		Format(sQuery, sizeof(sQuery), "SELECT enabled FROM categories WHERE categoryid=%d",
				categoryid);
		bPreferenceEnabled = bool:SQL_QuerySingleRowInt(g_hDatabase, sQuery);
	}

	return bPreferenceEnabled;
}

public Native_GetRawDownloadPreference(Handle:hPlugin, nParams) {
	new steamid = GetNativeCell(1), categoryid = GetNativeCell(2);
	return GetRawDownloadPreference(steamid, categoryid);
}

/**
 * Checks if a SteamID has a preference set for a category.
 */
bool:HasRawDownloadPreference(steamid, categoryid, &any:result = 0) {
	decl String:sQuery[MAX_SQL_QUERY_LENGTH];

	new bool:bHasRows;
	new Handle:hQuery = INVALID_HANDLE;
	Format(sQuery, sizeof(sQuery), "SELECT enabled FROM downloadprefs WHERE sid3=%d AND categoryid=%d",
			steamid, categoryid);
	hQuery = SQL_Query(g_hDatabase, sQuery);

	if ((bHasRows = (SQL_GetRowCount(hQuery) > 0))) {
		SQL_FetchRow(hQuery);
		result = bool:SQL_FetchInt(hQuery, 0);
	}

	CloseHandle(hQuery);
	return bHasRows;
}

public Native_HasRawDownloadPreference(Handle:hPlugin, nParams) {
	new steamid = GetNativeCell(1), categoryid = GetNativeCell(2), bool:result;

	new bool:response = HasRawDownloadPreference(steamid, categoryid, result);
	SetNativeCellRef(3, result);

	return response;
}

/**
 * Converts a categoryid to an id.
 */
 public Native_CategoryToIdentifier(Handle:hPlugin, nParams) {
	new categoryid = GetNativeCell(1);
	for (new i = 0; i < g_nDownloadPrefs; i++) {
		if (categoryid == g_rgiCategories[i]) {
			// Category already exists
			return i;
		}
	}
	return INVALID_DPREFS_ID;
 }

/**
 * Runs a query, returning the first integer from the first row.
 */
_:SQL_QuerySingleRowInt(Handle:database, const String:query[]) {
	new result;
	new Handle:hQuery = SQL_Query(database, query);

	result = SQL_FetchInt(hQuery, 0);
	CloseHandle(hQuery);

	return result;
}

/**
 * Reads the database configuration.
 */
Handle:GetDatabase() {
	new Handle:hDatabase = INVALID_HANDLE;

	if (SQL_CheckConfig(DATABASE_NAME)) {
		decl String:sErrorBuffer[256];
		if ( (hDatabase = SQL_Connect(DATABASE_NAME, true, sErrorBuffer, sizeof(sErrorBuffer))) == INVALID_HANDLE ) {
			SetFailState("[downloadprefs] Could not connect to database: %s", sErrorBuffer);
		} else {
			return hDatabase;
		}
	} else {
		SetFailState("[downloadprefs] Could not find configuration %s to load database.", DATABASE_NAME);
	}
	return INVALID_HANDLE;
}

/**
 * Flow to check if a download is allowed.
 * When a client is authorized: -> Send custom virtual fastdl directory data
 * When a client downloads a file: ->
 *	  - Get SteamID3 from query string
 *	  - Get 'categoryid' by 'filepath' in table files (filepath obtained in query string),
 *	  - Get 'enabled' by 'sid3' and 'category' in downloadprefs,
 *	  - (If not found, get 'enabled' by 'categoryid' in table categories),
 *	  - If enabled, allow the download, otherwise redirect / 404.
 */

/**
 * Download Preferences
 * Author(s): nosoop
 * File:  downloadprefs.sp
 * Description:	 Allows clients to select categories of files to download.
 * License:	 MIT License
 */

#pragma semicolon 1
#include <sourcemod>

#define PLUGIN_VERSION			"0.7.0"
public Plugin:myinfo = {
	name = "Download Preferences",
	author = "nosoop",
	description = "Client download preferences",
	version = PLUGIN_VERSION,
	url = "http://github.com/nosoop/SM-DownloadPrefs"
}

#define INVALID_DPREFS_ID -1
#define INVALID_DOWNLOAD_CATEGORY -1
#define MAX_SQL_QUERY_LENGTH 512
#define MAX_URL_LENGTH 256

// List of categories that have been registered in this session
#define MAX_DOWNLOAD_PREFERENCES 64
new g_rgiCategories[MAX_DOWNLOAD_PREFERENCES], g_nDownloadPrefs;

// Database name and related handle
#define DATABASE_NAME "downloadprefs"
new Handle:g_hDatabase = INVALID_HANDLE;

// ConVar handles
new Handle:g_hCDownloadURL = INVALID_HANDLE, // sv_downloadurl
	Handle:g_hCDPrefURL = INVALID_HANDLE; // sm_dprefs_downloadurl

// TODO allocate new g_rgDownloadPreferences[MAXPLAYERS][MAX_DOWNLOAD_PREFERENCES];

// TODO perform plugin / category validation

public OnPluginStart() {
	CreateConVar("sm_dprefs_version", PLUGIN_VERSION, _, FCVAR_PLUGIN | FCVAR_NOTIFY);

	g_nDownloadPrefs = 0;
	for (new i = 0; i < MAX_DOWNLOAD_PREFERENCES; i++) {
		g_rgiCategories[i] = INVALID_DOWNLOAD_CATEGORY;
	}

	// Hook to set redirect downloadurl.  If blank, the transmission of sv_downloadurl to the client is not changed.
	g_hCDownloadURL = FindConVar("sv_downloadurl");
	g_hCDPrefURL = CreateConVar("sm_dprefs_downloadurl", "", "Download URL to send to the client.  See README for details.", FCVAR_PLUGIN | FCVAR_SPONLY);

	AutoExecConfig(true);
}

public OnPluginEnd() {
	CloseHandle(g_hDatabase);
}

public APLRes:AskPluginLoad2(Handle:hMySelf, bool:bLate, String:strError[], iMaxErrors) {
	RegPluginLibrary("downloadprefs");

	CreateNative("RegClientDownloadCategory", Native_RegClientDownloadCategory);
	CreateNative("RegClientDownloadFile", Native_RegClientDownloadFile);
	CreateNative("SetClientDownloadPreference", Native_SetClientDownloadPreference);
	CreateNative("GetClientDownloadPreference", Native_GetClientDownloadPreference);
	CreateNative("ClientHasDownloadPreference", Native_ClientHasDownloadPreference);

	// Raw access methods
	CreateNative("SetRawDownloadPreference", Native_SetRawDownloadPreference);
	CreateNative("GetRawDownloadPreference", Native_GetRawDownloadPreference);
	CreateNative("HasRawDownloadPreference", Native_HasRawDownloadPreference);

	// Unstable methods
	CreateNative("RawCategoryInfo", Native_RawCategoryInfo);
	CreateNative("GetActiveCategories", Native_GetActiveCategories);
	CreateNative("CategoryToIdentifier", Native_CategoryToIdentifier);

	g_hDatabase = GetDatabase();

	return APLRes_Success;
}

/**
 * Sends a custom download URL when the client is connecting.
 * This occurs every time the client reconnects (via map change, etc.)
 */
public OnClientAuthorized(client, const String:auth[]) {
	if (!IsFakeClient(client)) {
		SendCustomDownloadURL(client);
	}
}

SendCustomDownloadURL(client) {
	new iSteamID3 = GetSteamAccountID(client);
	new String:sClientDownloadURL[MAX_URL_LENGTH];
	GetConVarString(g_hCDPrefURL, sClientDownloadURL, sizeof(sClientDownloadURL));

	// TODO strip trailing slash off of sm_dprefs_downloadurl?

	if (strlen(sClientDownloadURL) > 0) {
		new String:sSteamID3[32];
		IntToString(iSteamID3, sSteamID3, sizeof(sSteamID3));
		ReplaceString(sClientDownloadURL, sizeof(sClientDownloadURL), "$STEAMID", sSteamID3);
		SendConVarValue(client, g_hCDownloadURL, sClientDownloadURL);
	}
}

/**
 * Resets the download URL to the server-supplied value once the client is fully in-game.
 * This occurs after the client has performed any fast download requests.
 */
public OnClientPostAdminCheck(client) {
	if (!IsFakeClient(client)) {
		new String:sDefaultDownloadURL[MAX_URL_LENGTH];
		GetConVarString(g_hCDownloadURL, sDefaultDownloadURL, sizeof(sDefaultDownloadURL));
		SendConVarValue(client, g_hCDownloadURL, sDefaultDownloadURL);
	}
}

/**
 * Registers a group of files to download.
 * Store the category plus description into the database if nonexistent,
 * returning an ID to the corresponding category.
 */
_:RegClientDownloadCategory(const String:category[], const String:description[], bool:enabled = true) {
	new Handle:hQuery = INVALID_HANDLE, categoryid;
	decl String:sQuery[MAX_SQL_QUERY_LENGTH];

	Format(sQuery, sizeof(sQuery), "SELECT categoryid FROM categories WHERE categoryname='%s'",
			category);
	hQuery = SQL_Query(g_hDatabase, sQuery);

	// Category does not exist; create it.
	if (SQL_GetRowCount(hQuery) == 0) {
		Format(sQuery, sizeof(sQuery), "INSERT OR REPLACE INTO categories (categoryid, categoryname, categorydesc, enabled) VALUES (NULL, '%s', '%s', '%b')",
				category, description, enabled);
		SQL_FastQuery(g_hDatabase, sQuery);
	}
	CloseHandle(hQuery);

	Format(sQuery, sizeof(sQuery), "SELECT categoryid FROM categories WHERE categoryname='%s'",
			category);
	hQuery = SQL_Query(g_hDatabase, sQuery);
	SQL_FetchRow(hQuery);
	categoryid = SQL_FetchInt(hQuery, 0);

	CloseHandle(hQuery);

	return DownloadCategoryAdded(categoryid);
}

public Native_RegClientDownloadCategory(Handle:hPlugin, nParams) {
	decl String:category[512], String:description[512];

	GetNativeString(1, category, sizeof(category));
	GetNativeString(2, description, sizeof(description));
	new bool:bDefault = GetNativeCell(3);

	return RegClientDownloadCategory(category, description, bDefault);
}

/**
 * Registers a file to a category.
 */
RegClientDownloadFile(id, const String:filepath[]) {
	if (!IsValidDownloadCategory(id)) {
		ThrowError("Invalid id %d", id);
	}
	decl String:sQuery[MAX_SQL_QUERY_LENGTH];
	Format(sQuery, sizeof(sQuery),
			"INSERT OR REPLACE INTO files (categoryid, filepath) VALUES (%d, '%s')",
			g_rgiCategories[id], filepath);
	SQL_FastQuery(g_hDatabase, sQuery);
}

public Native_RegClientDownloadFile(Handle:hPlugin, nParams) {
	new id = GetNativeCell(1);
	decl String:filepath[PLATFORM_MAX_PATH]; GetNativeString(2, filepath, sizeof(filepath));

	// TODO perform plugin validation
	RegClientDownloadFile(id, filepath);
}

/**
 * Stores the client's download preference.
 */
SetClientDownloadPreference(client, id, bool:enabled) {
	if (!IsValidDownloadCategory(id)) {
		ThrowError("Invalid id %d", id);
	}
	SetRawDownloadPreference(GetSteamAccountID(client), g_rgiCategories[id], enabled);
}

public Native_SetClientDownloadPreference(Handle:hPlugin, nParams) {
	new client = GetNativeCell(1), id = GetNativeCell(2), bool:enabled = GetNativeCell(3);

	// TODO perform plugin validation
	SetClientDownloadPreference(client, id, enabled);
}

/**
 * Retrieves a client's download preference.  If non-existent, will return the default setting.
 * A client will keep their existing download preference until a map change or reconnect.
 */
bool:GetClientDownloadPreference(client, id) {
	if (!IsValidDownloadCategory(id)) {
		SetFailState("Could not get download preference for ID %d", id);
	}

	return GetRawDownloadPreference(GetSteamAccountID(client), g_rgiCategories[id]);
}

public Native_GetClientDownloadPreference(Handle:hPlugin, nParams) {
	new client = GetNativeCell(1), id = GetNativeCell(2);

	// TODO perform plugin validation
	return GetClientDownloadPreference(client, id);
}

/**
 * Checks whether or not the client has their own download preference set.
 */
bool:ClientHasDownloadPreference(client, id, &any:result = 0) {
	if (!IsValidDownloadCategory(id)) {
		SetFailState("Could not check download preference for %N (ID %d)", client, id);
	}
	return HasRawDownloadPreference(GetSteamAccountID(client), g_rgiCategories[id], result);
}

public Native_ClientHasDownloadPreference(Handle:hPlugin, nParams) {
	new client = GetNativeCell(1), id = GetNativeCell(2), result = GetNativeCellRef(3);
	return ClientHasDownloadPreference(client, id, result);
}

/**
 * Gets the description of the download category.
 */
bool:RawCategoryInfo(categoryid, String:title[], maxTitleLength, String:description[], maxDescLength) {
	decl String:sQuery[MAX_SQL_QUERY_LENGTH];
	new Handle:hQuery = INVALID_HANDLE;
	new bool:bHasRows;

	Format(sQuery, sizeof(sQuery), "SELECT categoryname, categorydesc FROM categories WHERE categoryid=%d",
			categoryid);
	hQuery = SQL_Query(g_hDatabase, sQuery);

	if ((bHasRows = (SQL_GetRowCount(hQuery) > 0))) {
		SQL_FetchRow(hQuery);
		SQL_FetchString(hQuery, 0, title, maxTitleLength);
		SQL_FetchString(hQuery, 1, description, maxDescLength);
	}

	CloseHandle(hQuery);
	return bHasRows;
}

public Native_RawCategoryInfo(Handle:hPlugin, nParams) {
	new categoryid = GetNativeCell(1), maxTitleLength = GetNativeCell(3), maxDescLength = GetNativeCell(5);
	new String:title[maxTitleLength], String:description[maxDescLength];

	new bool:bResult = RawCategoryInfo(categoryid, title, maxTitleLength, description, maxDescLength);

	SetNativeString(2, title, maxTitleLength);
	SetNativeString(3, description, maxDescLength);

	return bResult;
}

/**
 * Adds the specified category to the active category list.
 */
_:DownloadCategoryAdded(categoryid) {
	new bool:bFound = false;

	for (new i = 0; i < g_nDownloadPrefs; i++) {
		bFound |= (categoryid == g_rgiCategories[i]);
		if (bFound) {
			// Category already exists
			return i;
		}
	}

	if (g_nDownloadPrefs < MAX_DOWNLOAD_PREFERENCES) {
		g_rgiCategories[g_nDownloadPrefs] = categoryid;
		return g_nDownloadPrefs++;
	}
	return INVALID_DPREFS_ID;
}

/**
 * Checks if the download category is not invalid.
 */
bool:IsValidDownloadCategory(id) {
	return g_rgiCategories[id] != INVALID_DOWNLOAD_CATEGORY;
}

public Native_GetActiveCategories(Handle:hPlugin, nParams) {
	new size = GetNativeCell(2), start = GetNativeCell(3), nCategories;
	new categoryids[size];

	for (new i = start; i < g_nDownloadPrefs; i++) {
		if (g_rgiCategories[i] != INVALID_DOWNLOAD_CATEGORY) {
			categoryids[nCategories++] = g_rgiCategories[i];
		}
	}

	SetNativeArray(1, categoryids, size);

	return nCategories;
}

/**
 * Provides raw access to update the database for the specified SteamID.
 */
SetRawDownloadPreference(steamid, categoryid, bool:enabled) {
	decl String:sQuery[MAX_SQL_QUERY_LENGTH];

	if (HasRawDownloadPreference(steamid, categoryid)) {
		// Update existing entry.
		Format(sQuery, sizeof(sQuery), "UPDATE downloadprefs SET enabled = %d WHERE sid3 = %d AND categoryid = %d",
				_:enabled, steamid, categoryid);
	} else {
		// Create new entry.
		Format(sQuery, sizeof(sQuery), "INSERT INTO downloadprefs (sid3, categoryid, enabled) VALUES (%d, %d, %d)",
				steamid, categoryid, _:enabled);
	}
	SQL_FastQuery(g_hDatabase, sQuery);
}

public Native_SetRawDownloadPreference(Handle:hPlugin, nParams) {
	new steamid = GetNativeCell(1), categoryid = GetNativeCell(2), bool:enabled = GetNativeCell(3);
	SetRawDownloadPreference(steamid, categoryid, enabled);
}

/**
 * Grants raw access to retrieve the download preference for the specified SteamID and category.
 */
bool:GetRawDownloadPreference(steamid, categoryid) {
	decl String:sQuery[MAX_SQL_QUERY_LENGTH];
	new bool:bPreferenceEnabled;

	if (!HasRawDownloadPreference(steamid, categoryid, bPreferenceEnabled)) {
		// Default preference for category
		Format(sQuery, sizeof(sQuery), "SELECT enabled FROM categories WHERE categoryid=%d",
				categoryid);
		bPreferenceEnabled = bool:SQL_QuerySingleRowInt(g_hDatabase, sQuery);
	}

	return bPreferenceEnabled;
}

public Native_GetRawDownloadPreference(Handle:hPlugin, nParams) {
	new steamid = GetNativeCell(1), categoryid = GetNativeCell(2);
	return GetRawDownloadPreference(steamid, categoryid);
}

/**
 * Checks if a SteamID has a preference set for a category.
 */
bool:HasRawDownloadPreference(steamid, categoryid, &any:result = 0) {
	decl String:sQuery[MAX_SQL_QUERY_LENGTH];

	new bool:bHasRows;
	new Handle:hQuery = INVALID_HANDLE;
	Format(sQuery, sizeof(sQuery), "SELECT enabled FROM downloadprefs WHERE sid3=%d AND categoryid=%d",
			steamid, categoryid);
	hQuery = SQL_Query(g_hDatabase, sQuery);

	if ((bHasRows = (SQL_GetRowCount(hQuery) > 0))) {
		SQL_FetchRow(hQuery);
		result = bool:SQL_FetchInt(hQuery, 0);
	}

	CloseHandle(hQuery);
	return bHasRows;
}

public Native_HasRawDownloadPreference(Handle:hPlugin, nParams) {
	new steamid = GetNativeCell(1), categoryid = GetNativeCell(2), bool:result;

	new bool:response = HasRawDownloadPreference(steamid, categoryid, result);
	SetNativeCellRef(3, result);

	return response;
}

/**
 * Converts a categoryid to an id.
 */
 public Native_CategoryToIdentifier(Handle:hPlugin, nParams) {
	new categoryid = GetNativeCell(1);
	for (new i = 0; i < g_nDownloadPrefs; i++) {
		if (categoryid == g_rgiCategories[i]) {
			// Category already exists
			return i;
		}
	}
	return INVALID_DPREFS_ID;
 }

/**
 * Runs a query, returning the first integer from the first row.
 */
_:SQL_QuerySingleRowInt(Handle:database, const String:query[]) {
	new result;
	new Handle:hQuery = SQL_Query(database, query);

	result = SQL_FetchInt(hQuery, 0);
	CloseHandle(hQuery);

	return result;
}

/**
 * Reads the database configuration.
 */
Handle:GetDatabase() {
	new Handle:hDatabase = INVALID_HANDLE;

	if (SQL_CheckConfig(DATABASE_NAME)) {
		decl String:sErrorBuffer[256];
		if ( (hDatabase = SQL_Connect(DATABASE_NAME, true, sErrorBuffer, sizeof(sErrorBuffer))) == INVALID_HANDLE ) {
			SetFailState("[downloadprefs] Could not connect to database: %s", sErrorBuffer);
		} else {
			return hDatabase;
		}
	} else {
		SetFailState("[downloadprefs] Could not find configuration %s to load database.", DATABASE_NAME);
	}
	return INVALID_HANDLE;
}

/**
 * Flow to check if a download is allowed.
 * When a client is authorized: -> Send custom virtual fastdl directory data
 * When a client downloads a file: ->
 *	  - Get SteamID3 from query string
 *	  - Get 'categoryid' by 'filepath' in table files (filepath obtained in query string),
 *	  - Get 'enabled' by 'sid3' and 'category' in downloadprefs,
 *	  - (If not found, get 'enabled' by 'categoryid' in table categories),
 *	  - If enabled, allow the download, otherwise redirect / 404.
 */


Handle mapchange event for day of defeat

/**
 * vim: set ts=4 :
 * =============================================================================
 * in_game_audio_map_change
 * Plays a song to all clients just before the map ends so they can listen to
 * music while the next map loads.  Also triggers when the next map vote starts
 * when a RockTheVote is called.
 *
 * Copyright 2013 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <in_game_audio>

#define PLUGIN_VERSION "1.8.7"
#define PLUGIN_NAME "In Game Audio Map Change"

public Plugin:myinfo =
{
    name = PLUGIN_NAME,
    author = "CrimsonTautology",
    description = "Play a song during map change",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_in_game_audio"
};

public OnPluginStart()
{
    decl String:mod_name[PLATFORM_MAX_PATH];
    GetGameFolderName(mod_name, sizeof(mod_name));

    LoadTranslations("in_game_audio.phrases");

    CreateConVar("sm_iga_map_change_version", PLUGIN_VERSION, PLUGIN_NAME, FCVAR_PLUGIN | FCVAR_SPONLY | FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DONTRECORD);

    if(StrEqual(mod_name, "tf"))
    {
        HookEventEx("teamplay_game_over", Event_MapChange);
    }else if(StrEqual(mod_name, "dod"))
    {
        HookEventEx("dod_game_over", Event_MapChange);
    }else{
        HookEventEx("game_end", Event_MapChange);
    }
}

public Action:Event_MapChange(Handle:event, const String:name[], bool:dontBroadcast)
{
    //Prevent HTTP calls when noone is online
    if(!CanAnyoneHearIGA()) return Plugin_Continue;

    new String:next_map[64];
    GetNextMap(next_map, sizeof(next_map));
    MapTheme(true, next_map);

    return Plugin_Continue;
}

/**
 * vim: set ts=4 :
 * =============================================================================
 * in_game_audio_map_change
 * Plays a song to all clients just before the map ends so they can listen to
 * music while the next map loads.  Also triggers when the next map vote starts
 * when a RockTheVote is called.
 *
 * Copyright 2013 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <in_game_audio>

#define PLUGIN_VERSION "1.8.7"
#define PLUGIN_NAME "In Game Audio Map Change"

public Plugin:myinfo =
{
    name = PLUGIN_NAME,
    author = "CrimsonTautology",
    description = "Play a song during map change",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_in_game_audio"
};

public OnPluginStart()
{
    decl String:mod_name[PLATFORM_MAX_PATH];
    GetGameFolderName(mod_name, sizeof(mod_name));

    LoadTranslations("in_game_audio.phrases");

    CreateConVar("sm_iga_map_change_version", PLUGIN_VERSION, PLUGIN_NAME, FCVAR_PLUGIN | FCVAR_SPONLY | FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DONTRECORD);

    if(StrEqual(mod_name, "tf"))
    {
        HookEventEx("teamplay_game_over", Event_MapChange);
    }else{
        HookEventEx("game_end", Event_MapChange);
    }
}

public Action:Event_MapChange(Handle:event, const String:name[], bool:dontBroadcast)
{
    //Prevent HTTP calls when noone is online
    if(!CanAnyoneHearIGA()) return Plugin_Continue;

    new String:next_map[64];
    GetNextMap(next_map, sizeof(next_map));
    MapTheme(true, next_map);

    return Plugin_Continue;
}


Change to respawn behind

Change to respawn behind

//(C) 2020 rrrfffrrr <rrrfffrrr@naver.com>

#pragma semicolon 1
#pragma newdecls required

public Plugin myinfo = {
	name		= "[INS] RespawnBots",
	author		= "rrrfffrrr",
	description	= "Handling bot respawn system for coop",
	version		= "2.1.0",
	url			= ""
};

#include <sourcemod>
#include <sdktools>
#include <timers>
//#include <counterattack>
#define TEAM_SPECTATE 1
#define TEAM_SECURITY 2
#define TEAM_INSURGENT 3

ConVar CvarRTime;
ConVar CvarDisplay;
ConVar CvarDisplayDelay;
ConVar CvarDefaultPoint;
ConVar CvarAddPoint;
ConVar CvarDefaultPointCA;
ConVar CvarAddPointCA;
ConVar CvarInfiniteOnCA;
ConVar CvarOnlyAliveCA;
ConVar CvarEnableAdjustSpawnLocation;
ConVar CvarAdjustDistance;

Handle fToggleSpawnZone;
Handle fPointInSpawnZone;
Handle fGetBaseEntity;
Handle fForceRespawn;
Handle cGameConfig;

int iBotRespawnRemain = 0;

Handle tRespawnDelay[MAXPLAYERS+1];
Handle tDisplay;

Handle tOnRespawnFrame;

StringMap hSpawnZone;
StringMapSnapshot hSpawnZoneKeys;

/// Counter attack forward.
/// If you don't use CustomCounterAttack, delete #include <counterattack> in line 17 to disable it (default: disabled)
#if defined _ins_counterattack
public void OnCounterAttackFinished(bool isCustom) {
	ResetRespawnCount();
}
#endif

public void OnPluginStart()
{
	InitSDKCalls();
	InitCvars();
	HookCvars();
	HookEvents();

	UpdateDisplayTimer();
	UpdateRespawnTimer();

	hSpawnZone = new StringMap();
	hSpawnZoneKeys = hSpawnZone.Snapshot();
	BuildSpawnZoneList();
}

public void OnMapStart() {
	tDisplay = INVALID_HANDLE;
	UpdateDisplayTimer();

	tOnRespawnFrame = INVALID_HANDLE;
	UpdateRespawnTimer();

	for(int i = 1; i <= MaxClients; ++i) {
		tRespawnDelay[i] = INVALID_HANDLE;
	}
	BuildSpawnZoneList();
}

void InitSDKCalls() {
	cGameConfig = LoadGameConfigFile("insurgency.games");
	if (cGameConfig == INVALID_HANDLE) {
		SetFailState("Fatal Error: Missing File \"insurgency.games\"!");
	}

	StartPrepSDKCall(SDKCall_Player);
	PrepSDKCall_SetFromConf(cGameConfig, SDKConf_Virtual, "CINSPlayer::ForceRespawn");
	fForceRespawn = EndPrepSDKCall();
	if (fForceRespawn == INVALID_HANDLE) {
		SetFailState("Fatal Error: Unable to find CINSPlayer::ForceRespawn");
	}

	StartPrepSDKCall(SDKCall_Static);
	PrepSDKCall_SetFromConf(cGameConfig, SDKConf_Signature, "CINSSpawnZone::PointInSpawnZone");
	PrepSDKCall_AddParameter(SDKType_Vector, SDKPass_ByRef);
	PrepSDKCall_AddParameter(SDKType_CBaseEntity, SDKPass_Pointer);
	PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Pointer, 0, VENCODE_FLAG_COPYBACK);
	fPointInSpawnZone = EndPrepSDKCall();
	if (fPointInSpawnZone == INVALID_HANDLE) {
		SetFailState("Fatal Error: Unable to find CINSSpawnZone::PointInSpawnZone");
	}
	StartPrepSDKCall(SDKCall_Raw);
	PrepSDKCall_SetFromConf(cGameConfig, SDKConf_Virtual, "CINSSpawnZone::GetBaseEntity");
	PrepSDKCall_SetReturnInfo(SDKType_CBaseEntity, SDKPass_Plain);
	fGetBaseEntity = EndPrepSDKCall();
	if (fGetBaseEntity == INVALID_HANDLE) {
		SetFailState("Fatal Error: Unable to find CINSSpawnZone::GetBaseEntity");
	}

	StartPrepSDKCall(SDKCall_GameRules);
	PrepSDKCall_SetFromConf(cGameConfig, SDKConf_Signature, "CINSRules::ToggleSpawnZone");
	PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
	PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
	fToggleSpawnZone = EndPrepSDKCall();
	if (fToggleSpawnZone == INVALID_HANDLE) {
		SetFailState("Fatal Error: Unable to find CINSRules::ToggleSpawnZone");
	}
}

void InitCvars() {
	CvarRTime = CreateConVar("sm_botrespawn_delay", "1", "How many seconds wait before spawn for insurgent, 0 is disable delay", FCVAR_NOTIFY);

	CvarDefaultPoint = CreateConVar("sm_botrespawn_rp", "5", "Default bots count, 0 is infinity", FCVAR_NOTIFY);
	CvarAddPoint = CreateConVar("sm_botrespawn_rp_add", "5", "Number of bots that each player (rp + players * rp_add)", FCVAR_NOTIFY);

	CvarDefaultPointCA = CreateConVar("sm_botrespawn_carp", "5", "Default bots count when counter attack", FCVAR_NOTIFY);
	CvarAddPointCA = CreateConVar("sm_botrespawn_carp_add", "5", "Number of bots that each player when counter attack (carp + players * carp_add)", FCVAR_NOTIFY);
	CvarInfiniteOnCA = CreateConVar("sm_botrespawn_infinite_ca", "1", "Infinity spawn when counter attack", FCVAR_NOTIFY);

	CvarDisplay = CreateConVar("sm_botrespawn_display", "1", "Display respawn point(0 = don't display, 1 = display)", FCVAR_NOTIFY);
	CvarDisplayDelay = CreateConVar("sm_botrespawn_display_delay", "1", "Display delay(sec)", FCVAR_NOTIFY);

	CvarOnlyAliveCA = CreateConVar("sm_botrespawn_count_alive", "1", "Count only alive players when counter attack.", FCVAR_NOTIFY);

	CvarEnableAdjustSpawnLocation = CreateConVar("sm_botrespawn_adjust", "1", "Adjust spawn location to where player can't see.", FCVAR_NOTIFY);
	CvarAdjustDistance = CreateConVar("sm_botrespawn_adjust_distance", "100", "Adjust spawn location when someone close enough (Include teams)", FCVAR_NOTIFY, true, 50.0);
}

void HookCvars() {
	HookConVarChange(CvarDisplayDelay, UpdateCvarDisplay);
}

void HookEvents() {
	HookEvent("player_spawn", Event_PlayerSpawn);
	HookEvent("player_death", Event_PlayerDeath);
	HookEvent("round_start", Event_BeginRound);
	HookEvent("round_begin", Event_StartRound);
	HookEvent("controlpoint_captured", Event_Captured);
	HookEvent("object_destroyed", Event_Captured);
}

/// Display
void UpdateCvarDisplay(Handle cvar, const char[] oldValue, const char[] newValue) {
	UpdateDisplayTimer();
}

void UpdateDisplayTimer() {
	if (tDisplay != INVALID_HANDLE) {
		KillTimer(tDisplay);
	}

	tDisplay = CreateTimer(CvarDisplayDelay.FloatValue, Timer_DisplayCount,_, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE);
}

Action Timer_DisplayCount(Handle timer) {
	if (CvarDisplay.BoolValue != true) {
		return Plugin_Continue;
	}

	char hint[64];
	if (IsCounterAttack() && CvarInfiniteOnCA.BoolValue)
		Format(hint, sizeof(hint), "Infinite respawn");
	else
		Format(hint, sizeof(hint), "남은 Insurgent : %i", ((iBotRespawnRemain > 0) ? iBotRespawnRemain : 0) + GetFakeClientCount());

	for(int i = 1; i <= MaxClients; i++) {
		if (IsValidPlayer(i) && !IsFakeClient(i)) {
			PrintHintText(i, "%s", hint);
		}
	}
	return Plugin_Continue;
}

/// Respawn tick
void UpdateRespawnTimer() {
	if (tOnRespawnFrame == INVALID_HANDLE) {
		tOnRespawnFrame = CreateTimer(1.0, Timer_RespawnTick,_, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE);
	}
}

Action Timer_RespawnTick(Handle timer) {
	bool IsInfiniteRespawn = (IsCounterAttack() && CvarInfiniteOnCA.BoolValue);
	if (!IsInfiniteRespawn && iBotRespawnRemain <= 0)
		return Plugin_Continue;

	for(int i = 1; i <= MaxClients; ++i) {
		if (IsValidPlayer(i) && IsFakeClient(i) && !IsPlayerAlive(i) && tRespawnDelay[i] == INVALID_HANDLE) {
			if (RespawnPlayer(i)) {
				iBotRespawnRemain -= (IsInfiniteRespawn ? 0 : 1);
				break;
			}
		}
	}

	return Plugin_Continue;
}

Action Event_PlayerSpawn(Event event, const char[] name, bool dontBroadcast) {
	int client = GetClientOfUserId(event.GetInt("userid"));
	if (tRespawnDelay[client] != INVALID_HANDLE) {
		KillTimer(tRespawnDelay[client]);
		tRespawnDelay[client] = INVALID_HANDLE;
	}
	return Plugin_Continue;
}

void ResetRespawnCount() {
	if (IsCounterAttack()) {
		iBotRespawnRemain = CalcRespawnCount(CvarDefaultPointCA, CvarAddPointCA, GetRealClientCount(true, CvarOnlyAliveCA.BoolValue));
	} else {
		iBotRespawnRemain = CalcRespawnCount(CvarDefaultPoint, CvarAddPoint, GetRealClientCount());
	}
}

int CalcRespawnCount(ConVar def, ConVar add, int player) {
	return def.IntValue + player * add.IntValue;
}

Action Event_BeginRound(Event event, const char[] name, bool dontBroadcast) {
	ResetRespawnCount();
	return Plugin_Continue;
}

Action Event_StartRound(Event event, const char[] name, bool dontBroadcast) {
	ResetRespawnCount();
	return Plugin_Continue;
}

Action Event_Captured(Handle event, const char[] name, bool dontBroadcast) {
	ResetRespawnCount();
	return Plugin_Continue;
}

/// Death handler
Action Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast) {
	int client = GetClientOfUserId(event.GetInt("userid"));
	if (!IsValidPlayer(client) || !IsFakeClient(client)) {
		return Plugin_Continue;
	}

	bool isCA = IsCounterAttack();
	bool enableRespawn = (((isCA) ? CvarDefaultPointCA.IntValue : CvarDefaultPoint.IntValue) != 0);	// idk why this code exists... should i delete it?
	if (enableRespawn)
		return Plugin_Continue;

	float delay = CvarRTime.FloatValue;
	if (delay > 0.0) {
		tRespawnDelay[client] = CreateTimer(delay, Timer_RespawnDelay, client, TIMER_FLAG_NO_MAPCHANGE);
	}

	return Plugin_Continue;
}

Action Timer_RespawnDelay(Handle timer, int client) {
	tRespawnDelay[client] = INVALID_HANDLE;
	return Plugin_Continue;
}

/// Utils
bool RespawnPlayer(int client) {
	if (IsValidPlayer(client) && !IsPlayerAlive(client) && GetClientTeam(client) != TEAM_SPECTATE) {
		SDKCall(fForceRespawn, client);
		FixSpawnPoint(client);
		return true;
	}

	return false;
}

bool IsValidPlayer(int client) {
	if (client < 1 || client > MaxClients)
		return false;

	if (!IsClientInGame(client))
		return false;

	return true;
}

int GetRealClientCount(bool inGameOnly = true, bool onlyAlivePlayer = true) {
	int clients = 0;
	for(int i = 1; i <= MaxClients; i++) {
		if (((inGameOnly) ? IsClientInGame(i) : IsClientConnected(i)) && !IsFakeClient(i) && GetClientTeam(i) != TEAM_SPECTATE && ((onlyAlivePlayer) ? IsPlayerAlive(i) : true)) {
			clients++;
		}
	}
	return clients;
}

int GetFakeClientCount(bool inGameOnly = true, bool onlyAlivePlayer = true) {
	int clients = 0;
	for(int i = 1; i <= MaxClients; i++) {
		if (((inGameOnly) ? IsClientInGame(i) : IsClientConnected(i)) && IsFakeClient(i) && ((onlyAlivePlayer) ? IsPlayerAlive(i) : true)) {
			clients++;
		}
	}
	return clients;
}

bool IsCounterAttack() {
	return view_as<bool>(GameRules_GetProp("m_bCounterAttack"));
}

int GetCurrentCPIndex() {
	int res = FindEntityByClassname(-1, "ins_objective_resource");
	if (res == -1)
		return -1;

	int CP = GetEntProp(res, Prop_Send, "m_nActivePushPointIndex");

	return CP;
}

/// Fix spawn location
// This is the main feature of RespawnBots 2.0
void FixSpawnPoint(int client) {
	if (!CvarEnableAdjustSpawnLocation.BoolValue)
		return;

	float pos[3];
	GetClientAbsOrigin(client, pos);
	if (!EnemyInSightOrClose(client, pos))
		return;

	int cp = GetCurrentCPIndex();
	int team = GetClientTeam(client);
	char key[4];
	ArrayList list;
	if (hSpawnZone.GetValue(key, list)) {
		for(int i = 0; i < list.Length; ++i) {
			GetEntPropVector(list.Get(i), Prop_Send, "m_vecOrigin", pos);
			if (!EnemyInSightOrClose(client, pos)) {
				TeleportEntity(client, pos, NULL_VECTOR, NULL_VECTOR);
				return;
			}
		}
	}

	Format(key, sizeof(key), "%s%d", ((team == TEAM_SECURITY) ? "S" : "I"), cp - 2);
	if (hSpawnZone.GetValue(key, list)) {
		for(int i = 0; i < list.Length; ++i) {
			GetEntPropVector(list.Get(i), Prop_Send, "m_vecOrigin", pos);
			if (!EnemyInSightOrClose(client, pos)) {
				TeleportEntity(client, pos, NULL_VECTOR, NULL_VECTOR);
				return;
			}
		}
	}
}

bool EnemyInSightOrClose(int client, float pos[3]) {
	int team = GetClientTeam(client);
	int eteam = (team == TEAM_SECURITY) ? TEAM_INSURGENT : TEAM_SECURITY;
	float minDist = Pow(CvarAdjustDistance.FloatValue, 2.0);
	float org[3];
	for(int i = 1; i <= MaxClients; ++i) {
		if (i != client && IsClientInGame(i) && IsPlayerAlive(i)) {
			GetClientEyePosition(i, org);
			if (GetVectorDistance(pos, org, true) <= minDist) {
				return true;
			}

			if (GetClientTeam(i) == eteam) {
				if (NothingBetweenClient(client, i, pos, org)) {
					return true;
				}
			}
		}
	}

	return false;
}

bool NothingBetweenClient(int client1, int client2, float c1Vec[3], float c2Vec[3]) {
	Handle tr = TR_TraceRayFilterEx(c1Vec, c2Vec, MASK_PLAYERSOLID, RayType_EndPoint, Filter_Caller, client1);
	if (TR_DidHit(tr)) {
		if (TR_GetEntityIndex(tr) == client2) {
			CloseHandle(tr);
			return true;
		}

		CloseHandle(tr);
		return false;
	}
	CloseHandle(tr);
	return true;
}

bool Filter_Caller(int entity, int contentsMask, int client) {
	if (entity == client) {
		return false;
	}

	return true;
}

int FindSpawnZone(int spawnpoint) {
	Address pSpawnZone = Address_Null;
	float absOrigin[3];
	GetEntPropVector(spawnpoint, Prop_Data, "m_vecAbsOrigin", absOrigin);
	SDKCall(fPointInSpawnZone, absOrigin, spawnpoint, pSpawnZone);
	if (pSpawnZone == Address_Null) {
		return -1;
	}
	return SDKCall(fGetBaseEntity, pSpawnZone);
}

void BuildSpawnZoneList() {
	ArrayList listIns;
	ArrayList listSec;
	char key[4];
	for(int i = 0; i < hSpawnZoneKeys.Length; ++i) {
		hSpawnZoneKeys.GetKey(i, key, sizeof(key));
		if (hSpawnZone.GetValue(key, listIns)) {
			delete listIns;
		}
	}
	hSpawnZone.Clear();

	int objective = FindEntityByClassname(-1, "ins_objective_resource");
	if (objective == -1)
		return;
	int numOfSpawnZone = GetEntProp(objective, Prop_Send, "m_iNumControlPoints");
	for(int i = 0; i <= numOfSpawnZone; ++i) {
		SDKCall(fToggleSpawnZone, i, false);
	}

	int point = -1;
	int zone = -1;
	int team = 1;
	for(int i = 0; i <= numOfSpawnZone; ++i) {
		SDKCall(fToggleSpawnZone, i, true);

		listIns = new ArrayList();
		listSec = new ArrayList();
		point = FindEntityByClassname(-1, "ins_spawnpoint");
		while(point != -1) {
			zone = FindSpawnZone(point);
			if (zone != -1) {
				team = GetEntProp(point, Prop_Send, "m_iTeamNum");
				float pos[3];
				GetEntPropVector(point, Prop_Send, "m_vecOrigin", pos);
				if (team == TEAM_SECURITY)
					listSec.Push(point);
				else if (team == TEAM_INSURGENT)
					listIns.Push(point);
			}
			point = FindEntityByClassname(point, "ins_spawnpoint");
		}
		Format(key, sizeof(key), "I%d", i);
		hSpawnZone.SetValue(key, listIns, true);
		Format(key, sizeof(key), "S%d", i);
		hSpawnZone.SetValue(key, listSec, true);

		SDKCall(fToggleSpawnZone, i, false);
	}

	hSpawnZoneKeys = hSpawnZone.Snapshot();
	SDKCall(fToggleSpawnZone, GetCurrentCPIndex(), true);
}

//(C) 2020 rrrfffrrr <rrrfffrrr@naver.com>

#pragma semicolon 1
#pragma newdecls required

public Plugin myinfo = {
	name		= "[INS] RespawnBots",
	author		= "rrrfffrrr",
	description	= "Handling bot respawn system for coop",
	version		= "2.1.0",
	url			= ""
};

#include <sourcemod>
#include <sdktools>
#include <timers>
//#include <counterattack>
#define TEAM_SPECTATE 1
#define TEAM_SECURITY 2
#define TEAM_INSURGENT 3

ConVar CvarRTime;
ConVar CvarDisplay;
ConVar CvarDisplayDelay;
ConVar CvarDefaultPoint;
ConVar CvarAddPoint;
ConVar CvarDefaultPointCA;
ConVar CvarAddPointCA;
ConVar CvarInfiniteOnCA;
ConVar CvarOnlyAliveCA;
ConVar CvarEnableAdjustSpawnLocation;
ConVar CvarAdjustDistance;

Handle fToggleSpawnZone;
Handle fPointInSpawnZone;
Handle fGetBaseEntity;
Handle fForceRespawn;
Handle cGameConfig;

int iBotRespawnRemain = 0;

Handle tRespawnDelay[MAXPLAYERS+1];
Handle tDisplay;

Handle tOnRespawnFrame;

StringMap hSpawnZone;
StringMapSnapshot hSpawnZoneKeys;

/// Counter attack forward.
/// If you don't use CustomCounterAttack, delete #include <counterattack> in line 17 to disable it (default: disabled)
#if defined _ins_counterattack
public void OnCounterAttackFinished(bool isCustom) {
	ResetRespawnCount();
}
#endif

public void OnPluginStart()
{
	InitSDKCalls();
	InitCvars();
	HookCvars();
	HookEvents();

	UpdateDisplayTimer();
	UpdateRespawnTimer();

	hSpawnZone = new StringMap();
	hSpawnZoneKeys = hSpawnZone.Snapshot();
	BuildSpawnZoneList();
}

public void OnMapStart() {
	tDisplay = INVALID_HANDLE;
	UpdateDisplayTimer();

	tOnRespawnFrame = INVALID_HANDLE;
	UpdateRespawnTimer();

	for(int i = 1; i <= MaxClients; ++i) {
		tRespawnDelay[i] = INVALID_HANDLE;
	}
	BuildSpawnZoneList();
}

void InitSDKCalls() {
	cGameConfig = LoadGameConfigFile("insurgency.games");
	if (cGameConfig == INVALID_HANDLE) {
		SetFailState("Fatal Error: Missing File \"insurgency.games\"!");
	}

	StartPrepSDKCall(SDKCall_Player);
	PrepSDKCall_SetFromConf(cGameConfig, SDKConf_Virtual, "CINSPlayer::ForceRespawn");
	fForceRespawn = EndPrepSDKCall();
	if (fForceRespawn == INVALID_HANDLE) {
		SetFailState("Fatal Error: Unable to find CINSPlayer::ForceRespawn");
	}

	StartPrepSDKCall(SDKCall_Static);
	PrepSDKCall_SetFromConf(cGameConfig, SDKConf_Signature, "CINSSpawnZone::PointInSpawnZone");
	PrepSDKCall_AddParameter(SDKType_Vector, SDKPass_ByRef);
	PrepSDKCall_AddParameter(SDKType_CBaseEntity, SDKPass_Pointer);
	PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Pointer, 0, VENCODE_FLAG_COPYBACK);
	fPointInSpawnZone = EndPrepSDKCall();
	if (fPointInSpawnZone == INVALID_HANDLE) {
		SetFailState("Fatal Error: Unable to find CINSSpawnZone::PointInSpawnZone");
	}
	StartPrepSDKCall(SDKCall_Raw);
	PrepSDKCall_SetFromConf(cGameConfig, SDKConf_Virtual, "CINSSpawnZone::GetBaseEntity");
	PrepSDKCall_SetReturnInfo(SDKType_CBaseEntity, SDKPass_Plain);
	fGetBaseEntity = EndPrepSDKCall();
	if (fGetBaseEntity == INVALID_HANDLE) {
		SetFailState("Fatal Error: Unable to find CINSSpawnZone::GetBaseEntity");
	}

	StartPrepSDKCall(SDKCall_GameRules);
	PrepSDKCall_SetFromConf(cGameConfig, SDKConf_Signature, "CINSRules::ToggleSpawnZone");
	PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
	PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
	fToggleSpawnZone = EndPrepSDKCall();
	if (fToggleSpawnZone == INVALID_HANDLE) {
		SetFailState("Fatal Error: Unable to find CINSRules::ToggleSpawnZone");
	}
}

void InitCvars() {
	CvarRTime = CreateConVar("sm_botrespawn_delay", "1", "How many seconds wait before spawn for insurgent, 0 is disable delay", FCVAR_NOTIFY);

	CvarDefaultPoint = CreateConVar("sm_botrespawn_rp", "5", "Default bots count, 0 is infinity", FCVAR_NOTIFY);
	CvarAddPoint = CreateConVar("sm_botrespawn_rp_add", "5", "Number of bots that each player (rp + players * rp_add)", FCVAR_NOTIFY);

	CvarDefaultPointCA = CreateConVar("sm_botrespawn_carp", "5", "Default bots count when counter attack", FCVAR_NOTIFY);
	CvarAddPointCA = CreateConVar("sm_botrespawn_carp_add", "5", "Number of bots that each player when counter attack (carp + players * carp_add)", FCVAR_NOTIFY);
	CvarInfiniteOnCA = CreateConVar("sm_botrespawn_infinite_ca", "1", "Infinity spawn when counter attack", FCVAR_NOTIFY);

	CvarDisplay = CreateConVar("sm_botrespawn_display", "1", "Display respawn point(0 = don't display, 1 = display)", FCVAR_NOTIFY);
	CvarDisplayDelay = CreateConVar("sm_botrespawn_display_delay", "1", "Display delay(sec)", FCVAR_NOTIFY);

	CvarOnlyAliveCA = CreateConVar("sm_botrespawn_count_alive", "1", "Count only alive players when counter attack.", FCVAR_NOTIFY);

	CvarEnableAdjustSpawnLocation = CreateConVar("sm_botrespawn_adjust", "1", "Adjust spawn location to where player can't see.", FCVAR_NOTIFY);
	CvarAdjustDistance = CreateConVar("sm_botrespawn_adjust_distance", "100", "Adjust spawn location when someone close enough (Include teams)", FCVAR_NOTIFY, true, 50.0);
}

void HookCvars() {
	HookConVarChange(CvarDisplayDelay, UpdateCvarDisplay);
}

void HookEvents() {
	HookEvent("player_spawn", Event_PlayerSpawn);
	HookEvent("player_death", Event_PlayerDeath);
	HookEvent("round_start", Event_BeginRound);
	HookEvent("round_begin", Event_StartRound);
	HookEvent("controlpoint_captured", Event_Captured);
	HookEvent("object_destroyed", Event_Captured);
}

/// Display
void UpdateCvarDisplay(Handle cvar, const char[] oldValue, const char[] newValue) {
	UpdateDisplayTimer();
}

void UpdateDisplayTimer() {
	if (tDisplay != INVALID_HANDLE) {
		KillTimer(tDisplay);
	}

	tDisplay = CreateTimer(CvarDisplayDelay.FloatValue, Timer_DisplayCount,_, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE);
}

Action Timer_DisplayCount(Handle timer) {
	if (CvarDisplay.BoolValue != true) {
		return Plugin_Continue;
	}

	char hint[64];
	if (IsCounterAttack() && CvarInfiniteOnCA.BoolValue)
		Format(hint, sizeof(hint), "Infinite respawn");
	else
		Format(hint, sizeof(hint), "남은 Insurgent : %i", ((iBotRespawnRemain > 0) ? iBotRespawnRemain : 0) + GetFakeClientCount());

	for(int i = 1; i <= MaxClients; i++) {
		if (IsValidPlayer(i) && !IsFakeClient(i)) {
			PrintHintText(i, "%s", hint);
		}
	}
	return Plugin_Continue;
}

/// Respawn tick
void UpdateRespawnTimer() {
	if (tOnRespawnFrame == INVALID_HANDLE) {
		tOnRespawnFrame = CreateTimer(1.0, Timer_RespawnTick,_, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE);
	}
}

Action Timer_RespawnTick(Handle timer) {
	bool IsInfiniteRespawn = (IsCounterAttack() && CvarInfiniteOnCA.BoolValue);
	if (!IsInfiniteRespawn && iBotRespawnRemain <= 0)
		return Plugin_Continue;

	for(int i = 1; i <= MaxClients; ++i) {
		if (IsValidPlayer(i) && IsFakeClient(i) && !IsPlayerAlive(i) && tRespawnDelay[i] == INVALID_HANDLE) {
			if (RespawnPlayer(i)) {
				iBotRespawnRemain -= (IsInfiniteRespawn ? 0 : 1);
				break;
			}
		}
	}

	return Plugin_Continue;
}

Action Event_PlayerSpawn(Event event, const char[] name, bool dontBroadcast) {
	int client = GetClientOfUserId(event.GetInt("userid"));
	if (tRespawnDelay[client] != INVALID_HANDLE) {
		KillTimer(tRespawnDelay[client]);
		tRespawnDelay[client] = INVALID_HANDLE;
	}
	return Plugin_Continue;
}

void ResetRespawnCount() {
	if (IsCounterAttack()) {
		iBotRespawnRemain = CalcRespawnCount(CvarDefaultPointCA, CvarAddPointCA, GetRealClientCount(true, CvarOnlyAliveCA.BoolValue));
	} else {
		iBotRespawnRemain = CalcRespawnCount(CvarDefaultPoint, CvarAddPoint, GetRealClientCount());
	}
}

int CalcRespawnCount(ConVar def, ConVar add, int player) {
	return def.IntValue + player * add.IntValue;
}

Action Event_BeginRound(Event event, const char[] name, bool dontBroadcast) {
	ResetRespawnCount();
	return Plugin_Continue;
}

Action Event_StartRound(Event event, const char[] name, bool dontBroadcast) {
	ResetRespawnCount();
	return Plugin_Continue;
}

Action Event_Captured(Handle event, const char[] name, bool dontBroadcast) {
	ResetRespawnCount();
	return Plugin_Continue;
}

/// Death handler
Action Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast) {
	int client = GetClientOfUserId(event.GetInt("userid"));
	if (!IsValidPlayer(client) || !IsFakeClient(client)) {
		return Plugin_Continue;
	}

	bool isCA = IsCounterAttack();
	bool enableRespawn = (((isCA) ? CvarDefaultPointCA.IntValue : CvarDefaultPoint.IntValue) != 0);	// idk why this code exists... should i delete it?
	if (enableRespawn)
		return Plugin_Continue;

	float delay = CvarRTime.FloatValue;
	if (delay > 0.0) {
		tRespawnDelay[client] = CreateTimer(delay, Timer_RespawnDelay, client, TIMER_FLAG_NO_MAPCHANGE);
	}

	return Plugin_Continue;
}

Action Timer_RespawnDelay(Handle timer, int client) {
	tRespawnDelay[client] = INVALID_HANDLE;
	return Plugin_Continue;
}

/// Utils
bool RespawnPlayer(int client) {
	if (IsValidPlayer(client) && !IsPlayerAlive(client) && GetClientTeam(client) != TEAM_SPECTATE) {
		SDKCall(fForceRespawn, client);
		FixSpawnPoint(client);
		return true;
	}

	return false;
}

bool IsValidPlayer(int client) {
	if (client < 1 || client > MaxClients)
		return false;

	if (!IsClientInGame(client))
		return false;

	return true;
}

int GetRealClientCount(bool inGameOnly = true, bool onlyAlivePlayer = true) {
	int clients = 0;
	for(int i = 1; i <= MaxClients; i++) {
		if (((inGameOnly) ? IsClientInGame(i) : IsClientConnected(i)) && !IsFakeClient(i) && GetClientTeam(i) != TEAM_SPECTATE && ((onlyAlivePlayer) ? IsPlayerAlive(i) : true)) {
			clients++;
		}
	}
	return clients;
}

int GetFakeClientCount(bool inGameOnly = true, bool onlyAlivePlayer = true) {
	int clients = 0;
	for(int i = 1; i <= MaxClients; i++) {
		if (((inGameOnly) ? IsClientInGame(i) : IsClientConnected(i)) && IsFakeClient(i) && ((onlyAlivePlayer) ? IsPlayerAlive(i) : true)) {
			clients++;
		}
	}
	return clients;
}

bool IsCounterAttack() {
	return view_as<bool>(GameRules_GetProp("m_bCounterAttack"));
}

int GetCurrentCPIndex() {
	int res = FindEntityByClassname(-1, "ins_objective_resource");
	if (res == -1)
		return -1;

	int CP = GetEntProp(res, Prop_Send, "m_nActivePushPointIndex");

	return CP;
}

/// Fix spawn location
// This is the main feature of RespawnBots 2.0
void FixSpawnPoint(int client) {
	if (!CvarEnableAdjustSpawnLocation.BoolValue)
		return;

	float pos[3];
	GetClientAbsOrigin(client, pos);
	if (!EnemyInSightOrClose(client, pos))
		return;

	int cp = GetCurrentCPIndex();
	int team = GetClientTeam(client);
	char key[4];
	ArrayList list;
	if (hSpawnZone.GetValue(key, list)) {
		for(int i = 0; i < list.Length; ++i) {
			GetEntPropVector(list.Get(i), Prop_Send, "m_vecOrigin", pos);
			if (!EnemyInSightOrClose(client, pos)) {
				TeleportEntity(client, pos, NULL_VECTOR, NULL_VECTOR);
				return;
			}
		}
	}

	Format(key, sizeof(key), "%s%d", ((team == TEAM_SECURITY) ? "S" : "I"), cp + 1);
	if (hSpawnZone.GetValue(key, list)) {
		for(int i = 0; i < list.Length; ++i) {
			GetEntPropVector(list.Get(i), Prop_Send, "m_vecOrigin", pos);
			if (!EnemyInSightOrClose(client, pos)) {
				TeleportEntity(client, pos, NULL_VECTOR, NULL_VECTOR);
				return;
			}
		}
	}
}

bool EnemyInSightOrClose(int client, float pos[3]) {
	int team = GetClientTeam(client);
	int eteam = (team == TEAM_SECURITY) ? TEAM_INSURGENT : TEAM_SECURITY;
	float minDist = Pow(CvarAdjustDistance.FloatValue, 2.0);
	float org[3];
	for(int i = 1; i <= MaxClients; ++i) {
		if (i != client && IsClientInGame(i) && IsPlayerAlive(i)) {
			GetClientEyePosition(i, org);
			if (GetVectorDistance(pos, org, true) <= minDist) {
				return true;
			}

			if (GetClientTeam(i) == eteam) {
				if (NothingBetweenClient(client, i, pos, org)) {
					return true;
				}
			}
		}
	}

	return false;
}

bool NothingBetweenClient(int client1, int client2, float c1Vec[3], float c2Vec[3]) {
	Handle tr = TR_TraceRayFilterEx(c1Vec, c2Vec, MASK_PLAYERSOLID, RayType_EndPoint, Filter_Caller, client1);
	if (TR_DidHit(tr)) {
		if (TR_GetEntityIndex(tr) == client2) {
			CloseHandle(tr);
			return true;
		}

		CloseHandle(tr);
		return false;
	}
	CloseHandle(tr);
	return true;
}

bool Filter_Caller(int entity, int contentsMask, int client) {
	if (entity == client) {
		return false;
	}

	return true;
}

int FindSpawnZone(int spawnpoint) {
	Address pSpawnZone = Address_Null;
	float absOrigin[3];
	GetEntPropVector(spawnpoint, Prop_Data, "m_vecAbsOrigin", absOrigin);
	SDKCall(fPointInSpawnZone, absOrigin, spawnpoint, pSpawnZone);
	if (pSpawnZone == Address_Null) {
		return -1;
	}
	return SDKCall(fGetBaseEntity, pSpawnZone);
}

void BuildSpawnZoneList() {
	ArrayList listIns;
	ArrayList listSec;
	char key[4];
	for(int i = 0; i < hSpawnZoneKeys.Length; ++i) {
		hSpawnZoneKeys.GetKey(i, key, sizeof(key));
		if (hSpawnZone.GetValue(key, listIns)) {
			delete listIns;
		}
	}
	hSpawnZone.Clear();

	int objective = FindEntityByClassname(-1, "ins_objective_resource");
	if (objective == -1)
		return;
	int numOfSpawnZone = GetEntProp(objective, Prop_Send, "m_iNumControlPoints");
	for(int i = 0; i <= numOfSpawnZone; ++i) {
		SDKCall(fToggleSpawnZone, i, false);
	}

	int point = -1;
	int zone = -1;
	int team = 1;
	for(int i = 0; i <= numOfSpawnZone; ++i) {
		SDKCall(fToggleSpawnZone, i, true);

		listIns = new ArrayList();
		listSec = new ArrayList();
		point = FindEntityByClassname(-1, "ins_spawnpoint");
		while(point != -1) {
			zone = FindSpawnZone(point);
			if (zone != -1) {
				team = GetEntProp(point, Prop_Send, "m_iTeamNum");
				float pos[3];
				GetEntPropVector(point, Prop_Send, "m_vecOrigin", pos);
				if (team == TEAM_SECURITY)
					listSec.Push(point);
				else if (team == TEAM_INSURGENT)
					listIns.Push(point);
			}
			point = FindEntityByClassname(point, "ins_spawnpoint");
		}
		Format(key, sizeof(key), "I%d", i);
		hSpawnZone.SetValue(key, listIns, true);
		Format(key, sizeof(key), "S%d", i);
		hSpawnZone.SetValue(key, listSec, true);

		SDKCall(fToggleSpawnZone, i, false);
	}

	hSpawnZoneKeys = hSpawnZone.Snapshot();
	SDKCall(fToggleSpawnZone, GetCurrentCPIndex(), true);
}


Initial fleshing-out/scribbling

Initial fleshing-out/scribbling

#include <sourcemod>

#pragma semicolon 1
#pragma newdecls required

const char[] PLUGIN_NAME    = "Multi-Giveaway";
const char[] PLUGIN_AUTHOR  = "Alex \"ZeroKnight\" George";
const char[] PLUGIN_DESC    = "Ultimate Giveaway plugin. Implements numerous kinds of Giveaways.";
const char[] PLUGIN_VERSION = "0.1.0";
const char[] PLUGIN_URL     = "http:/github.com/ZeroKnight/sm-multi-giveaway";
const char[] PLUGIN_TAG     = "[MG]";

public Plugin myinfo =
{
  name        = PLUGIN_NAME,
  author      = PLUGIN_AUTHOR,
  description = PLUGIN_DESC,
  version     = PLUGIN_VERSION,
  url         = PLUGIN_URL
};

enum MG_GiveawayType { MG_Dice = 1, MG_Number = 2, MG_Kill = 4, MG_All = 7 };

// TODO: Put things like this in a personal core library?
#define CVAR(%1) ConVar cv_%1;
const char[] CVAR_PREFIX = "multi_giveaway";
CVAR("version");
CVAR("flags");
CVAR("enaled_types");
CVAR("player_state");
CVAR("min_conn_time");
CVAR("dice_min");
CVAR("dice_max");
CVAR("dice_show_rolls");
CVAR("dice_rerolls");
CVAR("number_min");
CVAR("number_max");
CVAR("number_show_guesses");

void RegisterConVars()
{
  // XXX: Why is CreateConVar() not overloaded?! Are there even overloads?!
  const char[32] sMG_All; IntToString(MG_All, sMG_All, sizeof(sMG_All));

  cv_version = CreateConVar(
    "multi_givaway_version",
    PLUGIN_VERSION,
    "Plugin version",
    FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY|FCVAR_DONTRECORD);
  cv_flags = CreateConVar(
    "multi_givaway_flags",
    "b",
    "List of admin flags allowed to manage giveaways");
  cv_enabled_types = CreateConVar(
    "multi_giveaway_enabled_types",
    sMG_All,
    "Bit-field (sum of options) of enabled Giveaway types",
    true, 1.0,
    true, MG_All);
  cv_player_state = CreateConVar(
    "multi_giveaway_player_state",
    "7",
    "Bit-field (sum of options) of player states that are valid for giveaway participation: 1 - Spectator, 2 - Dead, 4 - Alive"
    true, 1.0,
    true, 7.0);
  cv_min_conn_time = CreateConVar( // TODO: Allow certain giveaways to override (eg. kill objective)
    "multi_giveaway_min_conn_time",
    "300", // 5 minutes
    "Amount of time in seconds a player must be connected to participate in a giveaway",
    true, 0.0);

  cv_dice_min = CreateConVar(
    "multi_giveaway_dice_min",
    "1",
    "Lowest possible number a player may roll",
    true, 1.0);
  cv_dice_max = CreateConVar(
    "multi_giveaway_dice_max",
    "50",
    "Highest possible number a player may roll");
  cv_dice_show_rolls = CreateConVar(
    "multi_giveaway_dice_show_rolls",
    "0",
    "Who can see other clients' dice rolls: 0 - Nobody, 1 - Admins, 2 - Everyone",
    true, 0.0
    true, 2.0);
  cv_dice_rerolls = CreateConVar(
    "multi_giveaway_dice_reroll",
    "0",
    "Number of 'do-overs' a player gets when rolling. Only the last is counted, for better or for worse");

  cv_number_min = CreateConVar(
    "multi_giveaway_number_min",
    "1",
    "Lower bound of the range of numbers",
    true, 1.0);
  cv_number_max = CreateConVar(
    "multi_giveaway_number_max",
    "100",
    "Upper bound of the range of numbers");
  cv_number_show_guesses = CreateConVar(
    "multi_giveaway_number_show_guesses",
    "2",
    "Who can see other clients' guesses: 0 - Nobody, 1 - Admins, 2 - Everyone",
    true, 0.0,
    true, 2.0);
}

void RegisterCommands()
{
  // XXX: Is there a better way to create aliases?
  // RegConsoleCmd for client commands (ie participation)
  // RegAdminCmd for admin commands (ie (start|stop)ing giveaways
  // methodmap may be helpful

  RegAdminCmd("sm_mg_reload", Command_ReloadConfig, ADMFLAG_CONFIG,
    "Reload configuration file");
  // start/stop/status <type> - style command
  RegAdminCmd("sm_multigiveaway", Command_MultiGiveaway, ADMFLAG_GENERIC,
    "Manage giveaways");
  RegAdminCmd("sm_mg", Command_MultiGiveaway, ADMFLAG_GENERIC,
    "Manage giveaways");
}

void LoadConfig()
{
  // ...
}

// Forwards  ///////////////////////

public void OnPluginStart()
{
  // NOTE:
  // load our config. make use of the Kv*/KeyValue functions
  // for flag cvars, ReadFlagString(), GetUserFlagBits() and & are our friends
  // register event forwards (CreateGlobalForward())? ie kill-objectives

  RegisterCommands();
  RegisterConVars();

  const char[128] translatefile;
  Format(translatefile, sizeof(translatefile), "%s.phrases", PLUGIN_NAME);
  LoadTranslations(translatefile);

  return Plugin_Handled;
}

public void OnClientDisconnect(int client)
{
  // remove client from any current lists, etc
}

// Callbacks ///////////////////////

public Action Command_ReloadConfig(int client, int args)
{
  LoadConfig();
  LogAction(client, -1, "%s configuration reloaded by %L", PLUGIN_NAME, client);
  ReplyToCommand(client, "%s Configuration reloaded!", PLUGIN_TAG);
  return Plugin_Handled;
}

public Action Command_MultiGiveaway(int client, int args)
{
  // sm_mg <start|stop|restart> <dice|number|kill|...> [params]...
  char[32] action, type;

  GetCmdArgString(1, action, sizeof(action));
  GetCmdArgString(2, type, sizeof(type));

  // ...
}


Convert docker 🐳 to \c[WHALE]

Convert docker 🐳 to \c[WHALE]

class Docker { }
class DockerImg { }

constant File $:docker-socket = '/var/run/docker.sock';
constant File $:docker-install-cli-path = '/usr/local/bin/docker';

constant Cmd $:docker is logged-as("\c[WHALE]") = on {
    Spit-Helper { 'docker' }
    Linux {
        Cmd<docker> or (
            if $:docker-socket {
                debug "$_ exists so only installing docker cli";
                Docker.install-cli;
            } else {
                debug "$_ doesn't exist so installing the full Docker package";
                Docker.install;
                'docker';
            }
        )
    }
}

constant $:moby-github = GitHubRepo<moby/moby>;

constant File $?docker-cleanup = ${mktemp};
constant File $?docker-img-cleanup = ${mktemp};

augment Docker {

    static method latest-build-ver~ {
        my $gh-release-url = $:moby-github.latest-release-url;
        die "unable to get latest docker version" unless $gh-release-url;
        $gh-release-url.${sed 's/.*\/v//'};
    }

    static method latest-build-tgz-url-->HTTP {
        my $latest-v = Docker.latest-build-ver;
        "https://download.docker.com/linux/static/stable/x86_64/docker-{$latest-v}.tgz";
    }
    static method install-cli-->File {
        my $url = Docker.latest-build-tgz-url;
        info "Getting latest docker build from $url";
        my $docker-dir = $url.get.extract.cleanup;
        info "Installing to $:docker-install-cli-path";
        $docker-dir.add('docker').move-to($:docker-install-cli-path);
        $:docker-install-cli-path;
    }

    static method install {
        info 'running https://get.docker.com script';
        ${
            sh -c (HTTP<https://get.docker.com>.get)
            >debug('docker-install.sh')
            !>debug('docker-install.sh')
        };
    }

    static method hello-world? {
        ${ $:docker run -i --rm hello-world *>debug('hello-world') };
    }

    method cleanup -->Docker {
        $?docker-cleanup.push($self);
        END {
            $?docker-cleanup.slurp-->List[Docker].remove;
            $?docker-cleanup.remove;
        }
    }

    static method create(
        $from,
        :$name,
        :@args,
        Bool :$mount-socket
    ) -->Docker {
        my $id = ${
            $:docker create -i !>error
            ("--name=$_" if $name)
            ("-v=$:docker-socket:$:docker-socket" if $mount-socket)
            --entrypoint ''
            @args $from sh -c ':;sh'
        }.substr(0,12); # 12 is a sufficient length of hash
        $name || $id;
    }

    method exists? ${ $:docker container inspect $self *>X }
    method Bool    { $self.exists }

    method copy($from, File $to) -->File {
        ${ $:docker cp $from "$self:$to" !>error};
        $to.add($from);
    }

    method copy-from($from,$to) --> File {
        ${ $:docker cp "$self:$from" $to !>error};
        $to;
    }

    method remove? { $self-->List[Docker].remove }

    method running? {
        ${ $:docker inspect -f '{{.State.Status}}' $self } eq 'running';
    }

    method start-sleep {
        debug "Starting docker container $self with sleep";
        'while true; do sleep 1; done'.${ $:docker start $self >X !>error};
    }

    method exec(Str $eval)? {
        if $self.running {
            $eval.${ $:docker exec -i $self sh >debug/warn($self) };
        } else {
            $eval.${ $:docker start >debug/warn($self) -i $self };
        }
    }

    method commit(
DockerImg :$name,
          :$tag,
          :@cmd,
          :$env,
          :@entrypoint,
          :$expose,
     Pair :@labels,
          :$onbuild,
          :$user,
          :$volume,
          :$workdir,
     Bool :$remove-old
    )-->DockerImg {
        if $remove-old and $name.exists {
            info "Removing previously existing $name";
            $name.remove;
        }

        my @args = \${
            ("-c=CMD " ~ .JSON if @cmd )
            ("-c=ENV $_" if $env )
            ("-c=ENTRYPOINT " ~ .JSON if @entrypoint)
            ("-c=EXPOSE $_" if $expose)
            ("-c=USER $_" if $user)
            ("-c=VOLUME $_" if $volume)
            ("-c=WORKDIR $_" if $workdir)
            (if @labels {
                '-c=LABEL ' ~ (“{.key}="{.value}"” for @labels).join(" ")
            })
            $self
            @$name
        };

        debug "docker commit: ‘{@args.join('’, ‘')}’";
        my $image = ${$:docker commit  !>fatal @args |
                      sed -r 's/^sha256:(.{12}).*/\1/'};
        $name || $image;
    }
}

augment DockerImg {
    method exists? { ~$self and ${ $:docker image inspect $self *>X } }
    method remove? { $self-->List[DockerImg].remove }
    method Bool { $self.exists }

    method cleanup^ {
        $?docker-img-cleanup.push: $self;
        END {
            $?docker-img-cleanup.slurp-->List[DockerImg].remove;
            $?docker-cleanup.remove;
        }
    }
    method add-tag ($tag)^ {
        my $tagged-name = $tag.contains(':') ?? $tag !! "{$self.name}:$tag";
        debug "Tagging $self with $tagged-name";
        ${$:docker tag $self $tagged-name !>fatal} ?? $tagged-name !! ()
    }

    method name~ {
        $self.${sed -r 's/(.*):.*/\1/'}
    }

    method tag~ {
        $self.${sed 's/.*://;t;s/.*/latest/' }
    }

    method id -->DockerImg ${ $:docker image inspect $self '-f={{.Id}}' !>error }

    method tags -->List[DockerImg] ${
        $:docker image inspect $self
        ｢-f={{range $i, $e := .RepoTags}}{{if $i}}{{"\n"}}{{end}}{{$e}}{{end}}｣
    }

    method copy-from($from,$to) -->File {
        debug "copying $from in $self to $to";
        my $tmp = Docker.create($self);
        $tmp.copy-from($from,$to);
        ${ $:docker rm -f $tmp >X};
        $to;
    }

    method labels --> List[Pair] {
        ${$:docker image inspect
          '-f={{range $key, $value := .Config.Labels}}{{print $key "\t" $value "\n"}}{{end}}'
          $self
         }
    }

    static method images(
        :@has-labels,
        Pair :@labels,
        Bool :$dangling,
        :$reference
    ) --> List[DockerImg] ${
            $:docker images -qa
            ("-f=label={.key}={.value}" for @labels)
            ("-f=label=$_" for @has-labels)
            ("-f=dangling=true" if $dangling)
            ("-f=reference=$_" if $reference)
            | uniq
    }
}

augment List[Docker] {
    method remove? {
        debug "Removing containers: {$self.join(',')}" if $self;
        ${ $:docker rm -f @$self *>X }
    }
}

augment List[DockerImg] {
    method remove? {
        debug "Removing images: {$self.join(',')}";
        ${ $:docker rmi @$self >debug/warn }
    }
}

class Docker { }
class DockerImg { }

constant File $:docker-socket = '/var/run/docker.sock';
constant File $:docker-install-cli-path = '/usr/local/bin/docker';

constant Cmd $:docker is logged-as("🐳") = on {
    Spit-Helper { 'docker' }
    Linux {
        Cmd<docker> or (
            if $:docker-socket {
                debug "$_ exists so only installing docker cli";
                Docker.install-cli;
            } else {
                debug "$_ doesn't exist so installing the full Docker package";
                Docker.install;
                'docker';
            }
        )
    }
}

constant $:moby-github = GitHubRepo<moby/moby>;

constant File $?docker-cleanup = ${mktemp};
constant File $?docker-img-cleanup = ${mktemp};

augment Docker {

    static method latest-build-ver~ {
        my $gh-release-url = $:moby-github.latest-release-url;
        die "unable to get latest docker version" unless $gh-release-url;
        $gh-release-url.${sed 's/.*\/v//'};
    }

    static method latest-build-tgz-url-->HTTP {
        my $latest-v = Docker.latest-build-ver;
        "https://download.docker.com/linux/static/stable/x86_64/docker-{$latest-v}.tgz";
    }
    static method install-cli-->File {
        my $url = Docker.latest-build-tgz-url;
        info "Getting latest docker build from $url";
        my $docker-dir = $url.get.extract.cleanup;
        info "Installing to $:docker-install-cli-path";
        $docker-dir.add('docker').move-to($:docker-install-cli-path);
        $:docker-install-cli-path;
    }

    static method install {
        info 'running https://get.docker.com script';
        ${
            sh -c (HTTP<https://get.docker.com>.get)
            >debug('docker-install.sh')
            !>debug('docker-install.sh')
        };
    }

    static method hello-world? {
        ${ $:docker run -i --rm hello-world *>debug('hello-world') };
    }

    method cleanup -->Docker {
        $?docker-cleanup.push($self);
        END {
            $?docker-cleanup.slurp-->List[Docker].remove;
            $?docker-cleanup.remove;
        }
    }

    static method create(
        $from,
        :$name,
        :@args,
        Bool :$mount-socket
    ) -->Docker {
        my $id = ${
            $:docker create -i !>error
            ("--name=$_" if $name)
            ("-v=$:docker-socket:$:docker-socket" if $mount-socket)
            --entrypoint ''
            @args $from sh -c ':;sh'
        }.substr(0,12); # 12 is a sufficient length of hash
        $name || $id;
    }

    method exists? ${ $:docker container inspect $self *>X }
    method Bool    { $self.exists }

    method copy($from, File $to) -->File {
        ${ $:docker cp $from "$self:$to" !>error};
        $to.add($from);
    }

    method copy-from($from,$to) --> File {
        ${ $:docker cp "$self:$from" $to !>error};
        $to;
    }

    method remove? { $self-->List[Docker].remove }

    method running? {
        ${ $:docker inspect -f '{{.State.Status}}' $self } eq 'running';
    }

    method start-sleep {
        debug "Starting docker container $self with sleep";
        'while true; do sleep 1; done'.${ $:docker start $self >X !>error};
    }

    method exec(Str $eval)? {
        if $self.running {
            $eval.${ $:docker exec -i $self sh >debug/warn($self) };
        } else {
            $eval.${ $:docker start >debug/warn($self) -i $self };
        }
    }

    method commit(
DockerImg :$name,
          :$tag,
          :@cmd,
          :$env,
          :@entrypoint,
          :$expose,
     Pair :@labels,
          :$onbuild,
          :$user,
          :$volume,
          :$workdir,
     Bool :$remove-old
    )-->DockerImg {
        if $remove-old and $name.exists {
            info "Removing previously existing $name";
            $name.remove;
        }

        my @args = \${
            ("-c=CMD " ~ .JSON if @cmd )
            ("-c=ENV $_" if $env )
            ("-c=ENTRYPOINT " ~ .JSON if @entrypoint)
            ("-c=EXPOSE $_" if $expose)
            ("-c=USER $_" if $user)
            ("-c=VOLUME $_" if $volume)
            ("-c=WORKDIR $_" if $workdir)
            (if @labels {
                '-c=LABEL ' ~ (“{.key}="{.value}"” for @labels).join(" ")
            })
            $self
            @$name
        };

        debug "docker commit: ‘{@args.join('’, ‘')}’";
        my $image = ${$:docker commit  !>fatal @args |
                      sed -r 's/^sha256:(.{12}).*/\1/'};
        $name || $image;
    }
}

augment DockerImg {
    method exists? { ~$self and ${ $:docker image inspect $self *>X } }
    method remove? { $self-->List[DockerImg].remove }
    method Bool { $self.exists }

    method cleanup^ {
        $?docker-img-cleanup.push: $self;
        END {
            $?docker-img-cleanup.slurp-->List[DockerImg].remove;
            $?docker-cleanup.remove;
        }
    }
    method add-tag ($tag)^ {
        my $tagged-name = $tag.contains(':') ?? $tag !! "{$self.name}:$tag";
        debug "Tagging $self with $tagged-name";
        ${$:docker tag $self $tagged-name !>fatal} ?? $tagged-name !! ()
    }

    method name~ {
        $self.${sed -r 's/(.*):.*/\1/'}
    }

    method tag~ {
        $self.${sed 's/.*://;t;s/.*/latest/' }
    }

    method id -->DockerImg ${ $:docker image inspect $self '-f={{.Id}}' !>error }

    method tags -->List[DockerImg] ${
        $:docker image inspect $self
        ｢-f={{range $i, $e := .RepoTags}}{{if $i}}{{"\n"}}{{end}}{{$e}}{{end}}｣
    }

    method copy-from($from,$to) -->File {
        debug "copying $from in $self to $to";
        my $tmp = Docker.create($self);
        $tmp.copy-from($from,$to);
        ${ $:docker rm -f $tmp >X};
        $to;
    }

    method labels --> List[Pair] {
        ${$:docker image inspect
          '-f={{range $key, $value := .Config.Labels}}{{print $key "\t" $value "\n"}}{{end}}'
          $self
         }
    }

    static method images(
        :@has-labels,
        Pair :@labels,
        Bool :$dangling,
        :$reference
    ) --> List[DockerImg] ${
            $:docker images -qa
            ("-f=label={.key}={.value}" for @labels)
            ("-f=label=$_" for @has-labels)
            ("-f=dangling=true" if $dangling)
            ("-f=reference=$_" if $reference)
            | uniq
    }
}

augment List[Docker] {
    method remove? {
        debug "Removing containers: {$self.join(',')}" if $self;
        ${ $:docker rm -f @$self *>X }
    }
}

augment List[DockerImg] {
    method remove? {
        debug "Removing images: {$self.join(',')}";
        ${ $:docker rmi @$self >debug/warn }
    }
}