Update sbpp_sleuth.sp

Update sbpp_sleuth.sp

Fixed the query when checking for permanent bans.

// *************************************************************************
//  This file is part of SourceBans++.
//
//  Copyright (C) 2014-2016 SourceBans++ Dev Team <https://github.com/sbpp>
//
//  SourceBans++ is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, per version 3 of the License.
//
//  SourceBans++ is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with SourceBans++. If not, see <http://www.gnu.org/licenses/>.
//
//  This file is based off work(s) covered by the following copyright(s):
//
//   SourceSleuth 1.3 fix
//   Copyright (C) 2013-2015 ecca
//   Licensed under GNU GPL version 3, or later.
//   Page: <https://forums.alliedmods.net/showthread.php?p=1818793> - <https://github.com/ecca/SourceMod-Plugins>
//
// *************************************************************************

#pragma semicolon 1
#include <sourcemod>
#undef REQUIRE_PLUGIN
#include <sourcebans>

#define PLUGIN_VERSION "(SB++) 1.5.5-dev"

#define LENGTH_ORIGINAL 1
#define LENGTH_CUSTOM 2
#define LENGTH_DOUBLE 3
#define LENGTH_NOTIFY 4

//- Handles -//
new Handle:hDatabase = INVALID_HANDLE;
new Handle:g_hAllowedArray = INVALID_HANDLE;

//- ConVars -//
ConVar g_cVar_actions;
ConVar g_cVar_banduration;
ConVar g_cVar_sbprefix;
ConVar g_cVar_bansAllowed;
ConVar g_cVar_bantype;
ConVar g_cVar_bypass;

//- Bools -//
new bool:CanUseSourcebans = false;

public Plugin:myinfo =
{
	name = "SourceBans++: SourceSleuth",
	author = "ecca, SourceBans++ Dev Team",
	description = "Useful for TF2 servers. Plugin will check for banned ips and ban the player.",
	version = PLUGIN_VERSION,
	url = "https://sbpp.sarabveer.me/"
};

public OnPluginStart()
{
	LoadTranslations("sourcesleuth.phrases");

	CreateConVar("sm_sourcesleuth_version", PLUGIN_VERSION, "SourceSleuth plugin version", FCVAR_SPONLY | FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DONTRECORD);

	g_cVar_actions = CreateConVar("sm_sleuth_actions", "3", "Sleuth Ban Type: 1 - Original Length, 2 - Custom Length, 3 - Double Length, 4 - Notify Admins Only", 0, true, 1.0, true, 4.0);
	g_cVar_banduration = CreateConVar("sm_sleuth_duration", "0", "Required: sm_sleuth_actions 1: Bantime to ban player if we got a match (0 = permanent (defined in minutes) )", 0);
	g_cVar_sbprefix = CreateConVar("sm_sleuth_prefix", "sb", "Prexfix for sourcebans tables: Default sb", 0);
	g_cVar_bansAllowed = CreateConVar("sm_sleuth_bansallowed", "0", "How many active bans are allowed before we act", 0);
	g_cVar_bantype = CreateConVar("sm_sleuth_bantype", "0", "0 - ban all type of lengths, 1 - ban only permanent bans", 0, true, 0.0, true, 1.0);
	g_cVar_bypass = CreateConVar("sm_sleuth_adminbypass", "0", "0 - Inactivated, 1 - Allow all admins with ban flag to pass the check", 0, true, 0.0, true, 1.0);

	g_hAllowedArray = CreateArray(256);

	AutoExecConfig(true, "Sm_SourceSleuth");

	SQL_TConnect(SQL_OnConnect, "sourcebans");

	RegAdminCmd("sm_sleuth_reloadlist", ReloadListCallBack, ADMFLAG_ROOT);

	LoadWhiteList();
}

public OnAllPluginsLoaded()
{
	CanUseSourcebans = LibraryExists("sourcebans");
}

public OnLibraryAdded(const String:name[])
{
	if (StrEqual("sourcebans", name))
	{
		CanUseSourcebans = true;
	}
}

public OnLibraryRemoved(const String:name[])
{
	if (StrEqual("sourcebans", name))
	{
		CanUseSourcebans = false;
	}
}

public SQL_OnConnect(Handle:owner, Handle:hndl, const String:error[], any:data)
{
	if (hndl == INVALID_HANDLE)
	{
		LogError("SourceSleuth: Database connection error: %s", error);
	}
	else
	{
		hDatabase = hndl;
	}
}

public Action:ReloadListCallBack(client, args)
{
	ClearArray(g_hAllowedArray);

	LoadWhiteList();

	LogMessage("%L reloaded the whitelist", client);

	if (client != 0)
	{
		PrintToChat(client, "[SourceSleuth] WhiteList has been reloaded!");
	}

	return Plugin_Continue;
}

public OnClientPostAdminCheck(client)
{
	if (CanUseSourcebans && !IsFakeClient(client))
	{
		new String:steamid[32];
		GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid));

		if (g_cVar_bypass.BoolValue && CheckCommandAccess(client, "sleuth_admin", ADMFLAG_BAN, false))
		{
			return;
		}

		if (FindStringInArray(g_hAllowedArray, steamid) == -1)
		{
			new String:IP[32], String:Prefix[64];
			GetClientIP(client, IP, sizeof(IP));

			g_cVar_sbprefix.GetString(Prefix, sizeof(Prefix));

			new String:query[1024];

			FormatEx(query, sizeof(query), "SELECT * FROM %s_bans WHERE ip='%s' AND RemoveType IS NULL AND (ends > %d OR length = 0)", Prefix, IP, g_cVar_bantype.IntValue == 0 ? GetTime() : 0);

			new Handle:datapack = CreateDataPack();

			WritePackCell(datapack, GetClientUserId(client));
			WritePackString(datapack, steamid);
			WritePackString(datapack, IP);
			ResetPack(datapack);

			SQL_TQuery(hDatabase, SQL_CheckHim, query, datapack);
		}
	}
}

public SQL_CheckHim(Handle:owner, Handle:hndl, const String:error[], any:datapack)
{
	new client;
	decl String:steamid[32], String:IP[32];

	if (datapack != INVALID_HANDLE)
	{
		client = GetClientOfUserId(ReadPackCell(datapack));
		ReadPackString(datapack, steamid, sizeof(steamid));
		ReadPackString(datapack, IP, sizeof(IP));
		CloseHandle(datapack);
	}

	if (hndl == INVALID_HANDLE)
	{
		LogError("SourceSleuth: Database query error: %s", error);
		return;
	}

	if (SQL_FetchRow(hndl))
	{
		new TotalBans = SQL_GetRowCount(hndl);

		if (TotalBans > g_cVar_bansAllowed.IntValue)
		{
			switch (g_cVar_actions.IntValue)
			{
				case LENGTH_ORIGINAL:
				{
					new length = SQL_FetchInt(hndl, 6);
					new time = length * 60;

					BanPlayer(client, time);
				}
				case LENGTH_CUSTOM:
				{
					new time = g_cVar_banduration.IntValue;
					BanPlayer(client, time);
				}
				case LENGTH_DOUBLE:
				{
					new length = SQL_FetchInt(hndl, 6);

					new time = 0;

					if (length != 0)
					{
						time = length / 60 * 2;
					}

					BanPlayer(client, time);
				}
				case LENGTH_NOTIFY:
				{
					/* Notify Admins when a client with an ip on the bans list connects */
					PrintToAdmins("[SourceSleuth] %t", "sourcesleuth_admintext", client, steamid, IP);
				}
			}
		}
	}
}

stock BanPlayer(client, time)
{
	decl String:Reason[255];
	Format(Reason, sizeof(Reason), "[SourceSleuth] %t", "sourcesleuth_banreason");
	SBBanPlayer(0, client, time, Reason);
}

PrintToAdmins(const String:format[], any:...)
{
	new String:g_Buffer[256];

	for (new i = 1; i <= MaxClients; i++)
	{
		if (CheckCommandAccess(i, "sm_sourcesleuth_printtoadmins", ADMFLAG_BAN) && IsClientInGame(i))
		{
			VFormat(g_Buffer, sizeof(g_Buffer), format, 2);

			PrintToChat(i, "%s", g_Buffer);
		}
	}
}

public LoadWhiteList()
{
	decl String:path[PLATFORM_MAX_PATH], String:line[256];

	BuildPath(Path_SM, path, PLATFORM_MAX_PATH, "configs/sourcesleuth_whitelist.cfg");

	new Handle:fileHandle = OpenFile(path, "r");

	while (!IsEndOfFile(fileHandle) && ReadFileLine(fileHandle, line, sizeof(line)))
	{
		ReplaceString(line, sizeof(line), "\n", "", false);

		PushArrayString(g_hAllowedArray, line);
	}

	CloseHandle(fileHandle);
}

// *************************************************************************
//  This file is part of SourceBans++.
//
//  Copyright (C) 2014-2016 SourceBans++ Dev Team <https://github.com/sbpp>
//
//  SourceBans++ is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, per version 3 of the License.
//
//  SourceBans++ is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with SourceBans++. If not, see <http://www.gnu.org/licenses/>.
//
//  This file is based off work(s) covered by the following copyright(s):
//
//   SourceSleuth 1.3 fix
//   Copyright (C) 2013-2015 ecca
//   Licensed under GNU GPL version 3, or later.
//   Page: <https://forums.alliedmods.net/showthread.php?p=1818793> - <https://github.com/ecca/SourceMod-Plugins>
//
// *************************************************************************

#pragma semicolon 1
#include <sourcemod>
#undef REQUIRE_PLUGIN
#include <sourcebans>

#define PLUGIN_VERSION "(SB++) 1.5.5-dev"

#define LENGTH_ORIGINAL 1
#define LENGTH_CUSTOM 2
#define LENGTH_DOUBLE 3
#define LENGTH_NOTIFY 4

//- Handles -//
new Handle:hDatabase = INVALID_HANDLE;
new Handle:g_hAllowedArray = INVALID_HANDLE;

//- ConVars -//
ConVar g_cVar_actions;
ConVar g_cVar_banduration;
ConVar g_cVar_sbprefix;
ConVar g_cVar_bansAllowed;
ConVar g_cVar_bantype;
ConVar g_cVar_bypass;

//- Bools -//
new bool:CanUseSourcebans = false;

public Plugin:myinfo =
{
	name = "SourceBans++: SourceSleuth",
	author = "ecca, SourceBans++ Dev Team",
	description = "Useful for TF2 servers. Plugin will check for banned ips and ban the player.",
	version = PLUGIN_VERSION,
	url = "https://sbpp.sarabveer.me/"
};

public OnPluginStart()
{
	LoadTranslations("sourcesleuth.phrases");

	CreateConVar("sm_sourcesleuth_version", PLUGIN_VERSION, "SourceSleuth plugin version", FCVAR_SPONLY | FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DONTRECORD);

	g_cVar_actions = CreateConVar("sm_sleuth_actions", "3", "Sleuth Ban Type: 1 - Original Length, 2 - Custom Length, 3 - Double Length, 4 - Notify Admins Only", 0, true, 1.0, true, 4.0);
	g_cVar_banduration = CreateConVar("sm_sleuth_duration", "0", "Required: sm_sleuth_actions 1: Bantime to ban player if we got a match (0 = permanent (defined in minutes) )", 0);
	g_cVar_sbprefix = CreateConVar("sm_sleuth_prefix", "sb", "Prexfix for sourcebans tables: Default sb", 0);
	g_cVar_bansAllowed = CreateConVar("sm_sleuth_bansallowed", "0", "How many active bans are allowed before we act", 0);
	g_cVar_bantype = CreateConVar("sm_sleuth_bantype", "0", "0 - ban all type of lengths, 1 - ban only permanent bans", 0, true, 0.0, true, 1.0);
	g_cVar_bypass = CreateConVar("sm_sleuth_adminbypass", "0", "0 - Inactivated, 1 - Allow all admins with ban flag to pass the check", 0, true, 0.0, true, 1.0);

	g_hAllowedArray = CreateArray(256);

	AutoExecConfig(true, "Sm_SourceSleuth");

	SQL_TConnect(SQL_OnConnect, "sourcebans");

	RegAdminCmd("sm_sleuth_reloadlist", ReloadListCallBack, ADMFLAG_ROOT);

	LoadWhiteList();
}

public OnAllPluginsLoaded()
{
	CanUseSourcebans = LibraryExists("sourcebans");
}

public OnLibraryAdded(const String:name[])
{
	if (StrEqual("sourcebans", name))
	{
		CanUseSourcebans = true;
	}
}

public OnLibraryRemoved(const String:name[])
{
	if (StrEqual("sourcebans", name))
	{
		CanUseSourcebans = false;
	}
}

public SQL_OnConnect(Handle:owner, Handle:hndl, const String:error[], any:data)
{
	if (hndl == INVALID_HANDLE)
	{
		LogError("SourceSleuth: Database connection error: %s", error);
	}
	else
	{
		hDatabase = hndl;
	}
}

public Action:ReloadListCallBack(client, args)
{
	ClearArray(g_hAllowedArray);

	LoadWhiteList();

	LogMessage("%L reloaded the whitelist", client);

	if (client != 0)
	{
		PrintToChat(client, "[SourceSleuth] WhiteList has been reloaded!");
	}

	return Plugin_Continue;
}

public OnClientPostAdminCheck(client)
{
	if (CanUseSourcebans && !IsFakeClient(client))
	{
		new String:steamid[32];
		GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid));

		if (g_cVar_bypass.BoolValue && CheckCommandAccess(client, "sleuth_admin", ADMFLAG_BAN, false))
		{
			return;
		}

		if (FindStringInArray(g_hAllowedArray, steamid) == -1)
		{
			new String:IP[32], String:Prefix[64];
			GetClientIP(client, IP, sizeof(IP));

			g_cVar_sbprefix.GetString(Prefix, sizeof(Prefix));

			new String:query[1024];

			FormatEx(query, sizeof(query), "SELECT * FROM %s_bans WHERE ip='%s' AND RemoveType IS NULL AND ends > %d", Prefix, IP, g_cVar_bantype.IntValue == 0 ? GetTime() : 0);

			new Handle:datapack = CreateDataPack();

			WritePackCell(datapack, GetClientUserId(client));
			WritePackString(datapack, steamid);
			WritePackString(datapack, IP);
			ResetPack(datapack);

			SQL_TQuery(hDatabase, SQL_CheckHim, query, datapack);
		}
	}
}

public SQL_CheckHim(Handle:owner, Handle:hndl, const String:error[], any:datapack)
{
	new client;
	decl String:steamid[32], String:IP[32];

	if (datapack != INVALID_HANDLE)
	{
		client = GetClientOfUserId(ReadPackCell(datapack));
		ReadPackString(datapack, steamid, sizeof(steamid));
		ReadPackString(datapack, IP, sizeof(IP));
		CloseHandle(datapack);
	}

	if (hndl == INVALID_HANDLE)
	{
		LogError("SourceSleuth: Database query error: %s", error);
		return;
	}

	if (SQL_FetchRow(hndl))
	{
		new TotalBans = SQL_GetRowCount(hndl);

		if (TotalBans > g_cVar_bansAllowed.IntValue)
		{
			switch (g_cVar_actions.IntValue)
			{
				case LENGTH_ORIGINAL:
				{
					new length = SQL_FetchInt(hndl, 6);
					new time = length * 60;

					BanPlayer(client, time);
				}
				case LENGTH_CUSTOM:
				{
					new time = g_cVar_banduration.IntValue;
					BanPlayer(client, time);
				}
				case LENGTH_DOUBLE:
				{
					new length = SQL_FetchInt(hndl, 6);

					new time = 0;

					if (length != 0)
					{
						time = length / 60 * 2;
					}

					BanPlayer(client, time);
				}
				case LENGTH_NOTIFY:
				{
					/* Notify Admins when a client with an ip on the bans list connects */
					PrintToAdmins("[SourceSleuth] %t", "sourcesleuth_admintext", client, steamid, IP);
				}
			}
		}
	}
}

stock BanPlayer(client, time)
{
	decl String:Reason[255];
	Format(Reason, sizeof(Reason), "[SourceSleuth] %t", "sourcesleuth_banreason");
	SBBanPlayer(0, client, time, Reason);
}

PrintToAdmins(const String:format[], any:...)
{
	new String:g_Buffer[256];

	for (new i = 1; i <= MaxClients; i++)
	{
		if (CheckCommandAccess(i, "sm_sourcesleuth_printtoadmins", ADMFLAG_BAN) && IsClientInGame(i))
		{
			VFormat(g_Buffer, sizeof(g_Buffer), format, 2);

			PrintToChat(i, "%s", g_Buffer);
		}
	}
}

public LoadWhiteList()
{
	decl String:path[PLATFORM_MAX_PATH], String:line[256];

	BuildPath(Path_SM, path, PLATFORM_MAX_PATH, "configs/sourcesleuth_whitelist.cfg");

	new Handle:fileHandle = OpenFile(path, "r");

	while (!IsEndOfFile(fileHandle) && ReadFileLine(fileHandle, line, sizeof(line)))
	{
		ReplaceString(line, sizeof(line), "\n", "", false);

		PushArrayString(g_hAllowedArray, line);
	}

	CloseHandle(fileHandle);
}


fix rtv output

fix rtv output

#include <mapchooser_redux>
#include <nextmap>
#include <cstrike>
#include <sdktools>

#pragma newdecls required

bool g_bAllowRTV;
bool g_bInChange;
bool g_bVoted[MAXPLAYERS+1];

public Plugin myinfo =
{
    name        = "Rock The Vote Redux",
    author      = "Kyle",
    description = "Provides RTV Map Voting",
    version     = MCR_VERSION,
    url         = "http://steamcommunity.com/id/_xQy_/"
};

public void OnPluginStart()
{
    RegAdminCmd("sm_forcertv", Command_ForceRTV, ADMFLAG_CHANGEMAP, "Force an RTV vote");
}

public void OnMapStart()
{
    g_bInChange = false;
    g_bAllowRTV = false;
    CreateTimer(180.0, Timer_DelayRTV, _, TIMER_FLAG_NO_MAPCHANGE);
}

public Action Timer_DelayRTV(Handle timer)
{
    g_bAllowRTV = true;
}

public void OnClientConnected(int client)
{
    g_bVoted[client] = false;
}

public void OnClientDisconnect(int client)
{
    g_bVoted[client] = false;
}

public void OnClientSayCommand_Post(int client, const char[] command, const char[] sArgs)
{
    if(!client)
        return;

    if(strcmp(sArgs, "!rtv", false) == 0 || strcmp(sArgs, "rtv", false) == 0)
        AttemptRTV(client);
}

void AttemptRTV(int client)
{
    if(!g_bAllowRTV)
    {
        PrintToChat(client, "[\x04MCR\x01]  当前不允许RTV!");
        return;
    }

    if(!CanMapChooserStartVote())
    {
        PrintToChat(client, "[\x04MCR\x01]  RTV投票已启动!");
        return;
    }

    if(g_bVoted[client])
    {
        RTV_CheckStatus(client, true, true);
        return;
    }

    g_bVoted[client] = true;

    if(RTV_CheckStatus(client, true, false))
        StartRTV();
}

void StartRTV()
{
    if(g_bInChange)
        return;

    ResetRTV();
    g_bAllowRTV = false;

    if(HasEndOfMapVoteFinished())
    {
        char map[128];
        if(GetNextMap(map, 128))
        {
            g_bInChange = true;

            PrintToChatAll("[\x04MCR\x01]  正在更换地图到[\x05%s\x01]", map);
            CreateTimer(10.0, Timer_ChangeMap, _, TIMER_FLAG_NO_MAPCHANGE);
        }

        return;
    }

    if(CanMapChooserStartVote())
    {
        InitiateMapChooserVote(MapChange_RoundEnd, null);
        CreateTimer(300.0, Timer_DelayRTV, _, TIMER_FLAG_NO_MAPCHANGE);
    }
}

void ResetRTV()
{
    for(int i = 1; i <= MaxClients+1; i++)
        g_bVoted[i] = false;
}

public Action Timer_ChangeMap(Handle timer)
{
    FindConVar("mp_halftime").SetInt(0);
    FindConVar("mp_timelimit").SetInt(0);
    FindConVar("mp_maxrounds").SetInt(0);
    FindConVar("mp_roundtime").SetInt(1);

    CS_TerminateRound(12.0, CSRoundEnd_Draw, true);

    for(int client = 1; client <= MaxClients; ++client)
    {
        if(!IsClientInGame(client))
            continue;

        if(!IsPlayerAlive(client))
            continue;

        ForcePlayerSuicide(client);
    }

    return Plugin_Stop;
}

public Action Command_ForceRTV(int client, int args)
{
    StartRTV();
    PrintToChatAll("[\x04MCR\x01]  已强制启动RTV投票");
    return Plugin_Handled;
}

bool RTV_CheckStatus(int client, bool notice, bool self)
{
    int need, done;
    GetPlayers(need, done);

    if(notice)
    {
        if(self)
            PrintToChatAll("[\x04MCR\x01]  您已发起RTV投票. (\x07%d\x01/\x04%d\x01票)", done, need);
        else
            PrintToChatAll("[\x04MCR\x01]  \x05%N\x01想要RTV投票. (\x07%d\x01/\x04%d\x01票)", client, done, need);
    }

    return (done >= need);
}

void GetPlayers(int &need, int &done)
{
    need = 0;
    done = 0;

    for(int client = 1; client <= MaxClients; client++)
        if(IsClientInGame(client) && !IsFakeClient(client) && !IsClientSourceTV(client))
        {
            need++;
            if(g_bVoted[client])
                done++;
        }

    need = RoundFloat(need*0.6);
}

#include <mapchooser_redux>
#include <nextmap>
#include <cstrike>
#include <sdktools>

#pragma newdecls required

bool g_bAllowRTV;
bool g_bInChange;
bool g_bKzTimer;
bool g_bVoted[MAXPLAYERS+1];

public Plugin myinfo =
{
    name        = "Rock The Vote Redux",
    author      = "Kyle",
    description = "Provides RTV Map Voting",
    version     = MCR_VERSION,
    url         = "http://steamcommunity.com/id/_xQy_/"
};

public void OnPluginStart()
{
    RegAdminCmd("sm_forcertv", Command_ForceRTV, ADMFLAG_CHANGEMAP, "Force an RTV vote");
}

public void OnMapStart()
{
    g_bInChange = false;
    g_bAllowRTV = false;
    CreateTimer(180.0, Timer_DelayRTV, _, TIMER_FLAG_NO_MAPCHANGE);
}

public Action Timer_DelayRTV(Handle timer)
{
    g_bAllowRTV = true;
}

public void OnClientConnected(int client)
{
    g_bVoted[client] = false;
}

public void OnClientDisconnect(int client)
{
    g_bVoted[client] = false;
}

public void OnClientSayCommand_Post(int client, const char[] command, const char[] sArgs)
{
    if(!client)
        return;

    if(strcmp(sArgs, "!rtv", false) == 0 || strcmp(sArgs, "rtv", false) == 0)
        AttemptRTV(client);
}

void AttemptRTV(int client)
{
    if(!g_bAllowRTV)
    {
        PrintToChat(client, "[\x04MCR\x01]  当前不允许RTV!");
        return;
    }

    if(!CanMapChooserStartVote())
    {
        PrintToChat(client, "[\x04MCR\x01]  RTV投票已启动!");
        return;
    }

    if(g_bVoted[client])
    {
        RTV_CheckStatus(client, true, true);
        return;
    }

    g_bVoted[client] = true;

    if(RTV_CheckStatus(client, true, false))
        StartRTV();
}

void StartRTV()
{
    if(g_bInChange)
        return;

    ResetRTV();
    g_bAllowRTV = false;

    if(HasEndOfMapVoteFinished())
    {
        char map[128];
        if(GetNextMap(map, 128))
        {
            g_bInChange = true;

            PrintToChatAll("[\x04MCR\x01]  正在更换地图到[\x05%s\x01]", map);
            CreateTimer(10.0, Timer_ChangeMap, _, TIMER_FLAG_NO_MAPCHANGE);
        }

        return;
    }

    if(CanMapChooserStartVote())
    {
        InitiateMapChooserVote(MapChange_RoundEnd, null);
        CreateTimer(300.0, Timer_DelayRTV, _, TIMER_FLAG_NO_MAPCHANGE);
    }
}

void ResetRTV()
{
    for(int i = 1; i <= MaxClients+1; i++)
        g_bVoted[i] = false;
}

public Action Timer_ChangeMap(Handle timer)
{
    FindConVar("mp_halftime").SetInt(0);
    FindConVar("mp_timelimit").SetInt(0);
    FindConVar("mp_maxrounds").SetInt(0);
    FindConVar("mp_roundtime").SetInt(1);

    CS_TerminateRound(12.0, CSRoundEnd_Draw, true);

    for(int client = 1; client <= MaxClients; ++client)
    {
        if(!IsClientInGame(client))
            continue;

        if(!IsPlayerAlive(client))
            continue;

        ForcePlayerSuicide(client);
    }

    return Plugin_Stop;
}

public Action Command_ForceRTV(int client, int args)
{
    StartRTV();
    PrintToChatAll("[\x04MCR\x01]  已强制启动RTV投票");
    return Plugin_Handled;
}

bool RTV_CheckStatus(int client, bool notice, bool self)
{
    int need, done;
    GetPlayers(need, done);

    if(notice)
    {
        if(self)
            PrintToChatAll("[\x04MCR\x01]  您已发起RTV投票. (\x07%d\x01/\x04%d\x01票)", done, need);
        else
            PrintToChatAll("[\x04MCR\x01]  \x05%s\x01想要RTV投票. (\x07%d\x01/\x04%d\x01票)", name, done, need);
    }

    return (done >= need);
}

void GetPlayers(int &need, int &done)
{
    need = 0;
    done = 0;

    for(int client = 1; client <= MaxClients; client++)
        if(IsClientInGame(client) && !IsFakeClient(client) && !IsClientSourceTV(client))
        {
            need++;
            if(g_bVoted[client])
                done++;
        }

    need = RoundFloat(need*0.6);
}


Add case where donator library not installed

/**
 * vim: set ts=4 :
 * =============================================================================
 * in_game_audio_donator_intro
 * Plays a user's theme song when they join the server if they are a donator.
 * A user's theme is set through the In Game Audio website.
 *
 * Copyright 2013 Crimsontautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <in_game_audio>
#undef REQUIRE_PLUGIN
#include <donator>

#define PLUGIN_VERSION "0.2"

new bool:g_CanIntroPlay[MAXPLAYERS+1];
new bool:g_DonatorLibraryExists = false;

public Plugin:myinfo =
{
    name = "In Game Audio Donator Intro",
    author = "CrimsonTautology",
    description = "Play donator's theme song when they join the server",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_in_game_audio"
};

public OnPluginStart()
{
    AddCommandListener(Event_JoinClass, "joinclass");
    g_DonatorLibraryExists = LibraryExists("donators");
}

public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max)
{
	MarkNativeAsOptional("IsPlayerDonator");
	return APLRes_Success;
}

public OnLibraryRemoved(const String:name[])
{
	if (StrEqual(name, "donators"))
	{
		g_DonatorLibraryExists = false;
	}
}

public OnLibraryAdded(const String:name[])
{
	if (StrEqual(name, "donators"))
	{
		g_DonatorLibraryExists = true;
	}
}

public OnPostDonatorCheck(client)
{
    if (g_DonatorLibraryExists)
    {
        g_CanIntroPlay[client] = IsPlayerDonator(client);
    }
}

public OnClientConnected(client)
{
    if (!g_DonatorLibraryExists)
    {
        g_CanIntroPlay[client] = true;
    }
}

public Action:Event_JoinClass(client, const String:command[], args)
{
    if(g_CanIntroPlay[client])
    {
        UserTheme(client);
        g_CanIntroPlay[client] = false;
    }

    return Plugin_Continue;
}

/**
 * vim: set ts=4 :
 * =============================================================================
 * in_game_audio_donator_intro
 * Plays a user's theme song when they join the server if they are a donator.
 * A user's theme is set through the In Game Audio website.
 *
 * Copyright 2013 Crimsontautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <in_game_audio>
#undef REQUIRE_PLUGIN
#include <donator>

#define PLUGIN_VERSION "0.2"

new bool:g_CanIntroPlay[MAXPLAYERS+1];
new bool:g_DonatorLibraryExists = false;

public Plugin:myinfo =
{
    name = "In Game Audio Donator Intro",
    author = "CrimsonTautology",
    description = "Play donator's theme song when they join the server",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_in_game_audio"
};

public OnPluginStart()
{
    AddCommandListener(Event_JoinClass, "joinclass");
    g_DonatorLibraryExists = LibraryExists("donators");
}

public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max)
{
	MarkNativeAsOptional("IsPlayerDonator");
	return APLRes_Success;
}

public OnLibraryRemoved(const String:name[])
{
	if (StrEqual(name, "donators"))
	{
		g_DonatorLibraryExists = false;
	}
}

public OnLibraryAdded(const String:name[])
{
	if (StrEqual(name, "donators"))
	{
		g_DonatorLibraryExists = true;
	}
}

public OnPostDonatorCheck(client)
{
    if (g_DonatorLibraryExists)
    {
        g_CanIntroPlay[client] = IsPlayerDonator(client);
    }
}

public Action:Event_JoinClass(client, const String:command[], args)
{
    if(g_CanIntroPlay[client])
    {
        UserTheme(client);
        g_CanIntroPlay[client] = false;
    }

    return Plugin_Continue;
}


Attempt at a native, should use ArrayLists

No inc file yet

#pragma semicolon 1
#pragma newdecls required

#include <sourcemod>
#include <tf2_stocks>
#include <sdkhooks>
#include <colorvariables>

#define PLUGIN_VERSION "1.0.0"

// ConVar BleedChance;
Menu voteMenu[MAXPLAYERS + 1];

bool voteEnabled;

int results[MAXPLAYERS + 1];
int resultCount[5] = {0, 0, 0, 0, 0};

int numOptions = 0;

char question[1024], options[5][2][48], baseoptions[5][48];

public Plugin myinfo = {
	name            = "[NGS] True Democracy",
	author          = "TheXeon",
	description     = "True democracy through smart votes.",
	version         = PLUGIN_VERSION,
	url             = "https://www.neogenesisnetwork.net/"
};

public void OnPluginStart( )
{
	CreateConVar("sm_truedemocracy_version", PLUGIN_VERSION, "True democracy randomized vote version");

	RegAdminCmd("sm_rvote", CommandRandomVote, ADMFLAG_VOTE, "Creates a randomized vote.");
	RegAdminCmd("sm_rvoteresults", CommandRandomVoteResults, ADMFLAG_VOTE, "Prints vote results.");
	RegAdminCmd("sm_rvoteend", CommandRandomVoteEnd, ADMFLAG_VOTE, "Ends a vote.");
	RegAdminCmd("sm_rvoteclear", CommandRandomVoteClear, ADMFLAG_VOTE, "Clears results of last vote.");

	RegConsoleCmd("sm_rrevote", CommandRandomVoteRevote, "Revote on a randomized vote!");

	LoadTranslations("common.phrases");
}

public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
{
   CreateNative("TrueDemocracy_StartVote", Native_StartRandomizedVote);
   return APLRes_Success;
}

public Action CommandRandomVote(int client, int args)
{
	if (voteEnabled)
	{
		CReplyToCommand(client, "{GREEN}[SM]{DEFAULT} There is already a vote going on!");
		return Plugin_Handled;
	}
	if (args < 3)
	{
		CReplyToCommand(client, "{GREEN}[SM]{DEFAULT} Please provide a question and two options!");
		return Plugin_Handled;
	}
	voteEnabled = true;
	numOptions = args - 1;

	ClearVoteResults();

	GetCmdArg(1, question, sizeof(question));
	for (int i = 2; i <= numOptions + 1; i++)
	{
		int place = i - 2;
		GetCmdArg(i, options[place][0], 48);
		strcopy(baseoptions[place], 48, options[place][0]);
		Format(options[place][1], 48, "option%d", (place + 1));
	}
	DisplayRandomVoteToAll(30.0);
	return Plugin_Handled;
}

void DisplayRandomVoteToAll(float time)
{
	char voteTitle[64];
	Format(voteTitle, sizeof(voteTitle), "%s (random options)", question);
	for (int i = 1; i <= MaxClients; i++)
	{
		if (!IsValidClient(i)) continue;
		PrepareVoteMenu(i, voteTitle);
		voteMenu[i].Display(i, 30);
	}
	CreateTimer(time, OnVoteTimerEnd);
}

public void RandomizeOptions()
{
	for (int j = 0; j < numOptions; j++)
	{
		int randPos = GetRandomInt(0, numOptions - 1);
		char tmpQuestion[48], tmpOption[48];
		strcopy(tmpQuestion, 48, options[j][0]);
		strcopy(tmpOption, 48, options[j][1]);
		strcopy(options[j][0], 48, options[randPos][0]);
		strcopy(options[j][1], 48, options[randPos][1]);
		strcopy(options[randPos][0], 48, tmpQuestion);
		strcopy(options[randPos][1], 48, tmpOption);
	}
}

public int Native_StartRandomizedVote(Handle plugin, int numParams)
{
	if (voteEnabled)
		return ThrowNativeError(1, "There is currently a vote already happening!");
	if (numParams < 4) // at least question, two options, and a time
		return ThrowNativeError(2, "There are not enough options in this vote!");
	voteEnabled = true;
	GetNativeString(1, question, sizeof(question));
	numOptions = numParams - 2;
	for (int i = 2; i <= numParams - 1; i++)
	{
		int place = i - 2;
		GetNativeString(i, options[place][0], 48);
		strcopy(baseoptions[place], 48, options[place][0]);
		Format(options[place][1], 48, "option%d", (place + 1));
	}
	DisplayRandomVoteToAll(GetNativeCell(numParams));
}

public Action OnVoteTimerEnd(Handle timer, any data)
{
	ServerCommand("sm_rvoteend");
	ServerCommand("sm_rvoteresults");
}

public Action CommandRandomVoteResults(int client, int args)
{
	CountVoteResults();
	CPrintToChatAll("{GREEN}[SM]{DEFAULT} === Vote results ===");
	for (int i = 0; i < numOptions; i++)
	{
		CPrintToChatAll("{CRIMSON}%d.{DEFAULT} {YELLOW}%s{DEFAULT}: %d.", i + 1, baseoptions[i], resultCount[i]);
	}
	return Plugin_Handled;
}

public Action CommandRandomVoteEnd(int client, int args)
{
	voteEnabled = false;
	CPrintToChatAll("{GREEN}[SM]{DEFAULT} Vote has ended.");
	return Plugin_Handled;
}

public Action CommandRandomVoteRevote(int client, int args)
{
	if (!IsValidClient(client) || !voteEnabled) return Plugin_Handled;
	char voteTitle[64];
	Format(voteTitle, sizeof(voteTitle), "%s (random options)", question);
	PrepareVoteMenu(client, voteTitle);
	voteMenu[client].Display(client, 30);
	return Plugin_Handled;
}

public Action CommandRandomVoteClear(int client, int args)
{
	ClearVoteResults();
	CReplyToCommand(client, "{GREEN}[SM]{DEFAULT} Vote results cleared.");
	return Plugin_Handled;
}

public int RandomizedVoteMenuHandler(Menu menu, MenuAction action, int param1, int param2)
{
	switch(action)
	{
		case MenuAction_Select:
		{
			if (!voteEnabled) return 0;
			char info[32], displayBuffer[48];
			menu.GetItem(param2, info, sizeof(info), _, displayBuffer, sizeof(displayBuffer));
			int place = info[6] - 48;
			CPrintToChat(param1, "{GREEN}[SM]{DEFAULT} Vote for {OLIVE}%s{DEFAULT} counted! Use {YELLOW}!rrevote{DEFAULT} to revote!", displayBuffer);
//			CPrintToChatAdmins(ADMFLAG_ROOT, "%N chose %s", param1, info);
//			CPrintToChatAdmins(ADMFLAG_ROOT, "%N set to place info[6] - 48 is %d", param1, place);
			results[param1] = place;
		}
		case MenuAction_Cancel:
			PrintToServer("Client %d's menu was cancelled for reason %d", param1, param2);
		case MenuAction_End:
			delete menu;
	}

	return 0;
}

void PrepareVoteMenu(int client, char[] title)
{
//	if (voteMenu[client] != null) return; commenting this out but I'm not sure it'll leak
	voteMenu[client] = new Menu(RandomizedVoteMenuHandler);
	voteMenu[client].SetTitle(title);
	RandomizeOptions();
	for (int k = 0; k < numOptions; k++)
	{
		voteMenu[client].AddItem(options[k][1], options[k][0]);
	}
	voteMenu[client].ExitButton = false;
}

void CountVoteResults()
{
	for (int i = 1; i <= MaxClients; i++)
	{
		int result = results[i];
		if (result == 0) continue;
		resultCount[result - 1]++;
	}
}

void ClearVoteResults()
{
	for (int i = 1; i <= MaxClients; i++)
	{
		results[i] = 0;
	}
	for (int i = 0; i < 5; i++)
	{
		resultCount[i] = 0;
	}
}

stock bool IsValidClient(int client, bool aliveTest=false, bool botTest=true, bool rangeTest=true,
	bool ingameTest=true)
{
	if (client > 4096) client = EntRefToEntIndex(client);
	if (rangeTest && (client < 1 || client > MaxClients)) return false;
	if (ingameTest && !IsClientInGame(client)) return false;
	if (botTest && IsFakeClient(client)) return false;
	if (GetEntProp(client, Prop_Send, "m_bIsCoaching")) return false;
	if (aliveTest && !IsPlayerAlive(client)) return false;
	return true;
}

#pragma semicolon 1
#pragma newdecls required

#include <sourcemod>
#include <tf2_stocks>
#include <sdkhooks>
#include <colorvariables>

#define PLUGIN_VERSION "1.0.0"

// ConVar BleedChance;
Menu voteMenu[MAXPLAYERS + 1];

bool voteEnabled;

int results[MAXPLAYERS + 1];
int resultCount[5] = {0, 0, 0, 0, 0};

int numOptions = 0;

char question[1024], options[5][2][48], baseoptions[5][48];
//new Handle:BleedTime;

public Plugin myinfo = {
	name            = "[NGS] Randomized Votes - True Democracy",
	author          = "TheXeon",
	description     = "Chance for bottles to break.",
	version         = PLUGIN_VERSION,
	url             = "https://www.neogenesisnetwork.net/"
};

public void OnPluginStart( )
{
	CreateConVar("sm_truedemocracy_version", PLUGIN_VERSION, "True democracy randomized vote version");

	RegAdminCmd("sm_rvote", CommandRandomVote, ADMFLAG_VOTE, "Creates a randomized vote.");
	RegAdminCmd("sm_rvoteresults", CommandRandomVoteResults, ADMFLAG_VOTE, "Prints vote results.");
	RegAdminCmd("sm_rvoteend", CommandRandomVoteEnd, ADMFLAG_VOTE, "Ends a vote.");
	RegAdminCmd("sm_rvoteclear", CommandRandomVoteClear, ADMFLAG_VOTE, "Clears results of last vote.");

	RegConsoleCmd("sm_rrevote", CommandRandomVoteRevote, "Revote on a randomized vote!");

	LoadTranslations("common.phrases");
}

public Action CommandRandomVote(int client, int args)
{
	if (voteEnabled)
	{
		CReplyToCommand(client, "{GREEN}[SM]{DEFAULT} There is already a vote going on!");
		return Plugin_Handled;
	}
	if (args < 3)
	{
		CReplyToCommand(client, "{GREEN}[SM]{DEFAULT} Please provide a question and two options!");
		return Plugin_Handled;
	}
	voteEnabled = true;
	numOptions = args - 1;
	char voteTitle[64];

	ClearVoteResults();

	GetCmdArg(1, question, sizeof(question));
	for (int i = 2; i <= numOptions + 1; i++)
	{
		int place = i - 2;
		GetCmdArg(i, options[place][0], 48);
		strcopy(baseoptions[place], 48, options[place][0]);
		Format(options[place][1], 48, "option%d", (place + 1));
	}

	Format(voteTitle, sizeof(voteTitle), "%s (random options)", question);
	for (int i = 1; i <= MaxClients; i++)
	{
		if (!IsValidClient(i)) continue;
		PrepareVoteMenu(i, voteTitle);
		voteMenu[i].Display(i, 30);
	}
	CreateTimer(30.0, OnVoteTimerEnd);
	return Plugin_Handled;
}

public void RandomizeOptions()
{
	for (int j = 0; j < numOptions; j++)
	{
		int randPos = GetRandomInt(0, numOptions - 1);
		char tmpQuestion[48], tmpOption[48];
		strcopy(tmpQuestion, 48, options[j][0]);
		strcopy(tmpOption, 48, options[j][1]);
		strcopy(options[j][0], 48, options[randPos][0]);
		strcopy(options[j][1], 48, options[randPos][1]);
		strcopy(options[randPos][0], 48, tmpQuestion);
		strcopy(options[randPos][1], 48, tmpOption);
	}
}

public Action OnVoteTimerEnd(Handle timer, any data)
{
	ServerCommand("sm_rvoteend");
	ServerCommand("sm_rvoteresults");
}

public Action CommandRandomVoteResults(int client, int args)
{
	CountVoteResults();
	CPrintToChatAll("{GREEN}[SM]{DEFAULT} === Vote results ===");
	for (int i = 0; i < numOptions; i++)
	{
		CPrintToChatAll("{CRIMSON}%d.{DEFAULT} {YELLOW}%s{DEFAULT}: %d.", i + 1, baseoptions[i], resultCount[i]);
	}
	return Plugin_Handled;
}

public Action CommandRandomVoteEnd(int client, int args)
{
	voteEnabled = false;
	CPrintToChatAll("{GREEN}[SM]{DEFAULT} Vote has ended.");
	return Plugin_Handled;
}

public Action CommandRandomVoteRevote(int client, int args)
{
	if (!IsValidClient(client) || !voteEnabled) return Plugin_Handled;
	char voteTitle[64];
	Format(voteTitle, sizeof(voteTitle), "%s (random options)", question);
	PrepareVoteMenu(client, voteTitle);
	voteMenu[client].Display(client, 30);
	return Plugin_Handled;
}

public Action CommandRandomVoteClear(int client, int args)
{
	ClearVoteResults();
	CReplyToCommand(client, "{GREEN}[SM]{DEFAULT} Vote results cleared.");
	return Plugin_Handled;
}

public int RandomizedVoteMenuHandler(Menu menu, MenuAction action, int param1, int param2)
{
	switch(action)
	{
		case MenuAction_Select:
		{
			if (!voteEnabled) return 0;
			char info[32], displayBuffer[48];
			menu.GetItem(param2, info, sizeof(info), _, displayBuffer, sizeof(displayBuffer));
			int place = info[6] - 48;
			CPrintToChat(param1, "{GREEN}[SM]{DEFAULT} Vote for {OLIVE}%s{DEFAULT} counted! Use {YELLOW}!rrevote{DEFAULT} to revote!", displayBuffer);
//			CPrintToChatAdmins(ADMFLAG_ROOT, "%N chose %s", param1, info);
//			CPrintToChatAdmins(ADMFLAG_ROOT, "%N set to place info[6] - 48 is %d", param1, place);
			results[param1] = place;
		}
		case MenuAction_Cancel:
			PrintToServer("Client %d's menu was cancelled for reason %d", param1, param2);
		case MenuAction_End:
			delete menu;
	}

	return 0;
}

void PrepareVoteMenu(int client, char[] title)
{
//	if (voteMenu[client] != null) return; commenting this out but I'm not sure it'll leak
	voteMenu[client] = new Menu(RandomizedVoteMenuHandler);
	voteMenu[client].SetTitle(title);
	RandomizeOptions();
	for (int k = 0; k < numOptions; k++)
	{
		voteMenu[client].AddItem(options[k][1], options[k][0]);
	}
	voteMenu[client].ExitButton = false;
}

void CountVoteResults()
{
	for (int i = 1; i <= MaxClients; i++)
	{
		int result = results[i];
		if (result == 0) continue;
		resultCount[result - 1]++;
	}
}

void ClearVoteResults()
{
	for (int i = 1; i <= MaxClients; i++)
	{
		results[i] = 0;
	}
	for (int i = 0; i < 5; i++)
	{
		resultCount[i] = 0;
	}
}

stock bool IsValidClient(int client, bool aliveTest=false, bool botTest=true, bool rangeTest=true,
	bool ingameTest=true)
{
	if (client > 4096) client = EntRefToEntIndex(client);
	if (rangeTest && (client < 1 || client > MaxClients)) return false;
	if (ingameTest && !IsClientInGame(client)) return false;
	if (botTest && IsFakeClient(client)) return false;
	if (GetEntProp(client, Prop_Send, "m_bIsCoaching")) return false;
	if (aliveTest && !IsPlayerAlive(client)) return false;
	return true;
}


Better timing.

Better timing.

#include <sourcemod>
#include <sdkhooks>
#pragma semicolon 1

public Plugin:myinfo =
{
	name = "Smooth witch damage",
	author = "Darkid",
	description = "Smooths out the damage taken from a witch while incapped.",
	version = "1.7",
	url = "https://github.com/jbzdarkid/Random/blob/master/smooth_witch_damage.sp"
}

new const bool:DEBUG = true;

new Float:witchDamage;
new Handle:deadWitches;
new Handle:engagedWitches;
new Float:startTick;
new bool:lateLoad = false;

public APLRes:AskPluginLoad2(Handle:plugin, bool:late, String:error[], errMax) {
	lateLoad = late;
	return APLRes_Success;
}

public OnPluginStart() {
	if (lateLoad) {
		for (new client=1; client<=MaxClients; client++) {
			if (!IsClientInGame(client)) continue;
			OnClientPostAdminCheck(client);
		}
	}

	decl String:witchDamageString[64];
	GetConVarString(FindConVar("z_witch_damage_per_kill_hit"), witchDamageString, sizeof(witchDamageString));
	HookConVarChange(FindConVar("z_witch_damage_per_kill_hit"), OnWitchDamageChange);
	witchDamage = 1.0*StringToInt(witchDamageString);

	deadWitches = CreateArray(64);
	engagedWitches = CreateArray(64);

	HookEvent("round_start", round_start);
	HookEvent("witch_killed", witch_killed);
	startTick = 0.0;
}

public OnWitchDamageChange(Handle:cvar, const String:oldVal[], const String:newVal[]) {
	witchDamage = 1.0*StringToInt(newVal);
}

public OnClientPostAdminCheck(client) {
	SDKHook(client, SDKHook_OnTakeDamage, OnTakeDamage);
}

public round_start(Handle:event, const String:name[], bool:dontBroadcast) {
	ClearArray(deadWitches);
	ClearArray(engagedWitches);
}

bool:IsWitch(entity) {
	if (entity <= 0 || !IsValidEntity(entity) || !IsValidEdict(entity)) return false;
	decl String:className[8];
	GetEdictClassname(entity, className, sizeof(className));
	return strcmp(className, "witch") == 0;
}

public Action:OnTakeDamage(victim, &attacker, &inflictor, &Float:damage, &damageType) {
	if (victim <= 0 || victim > MaxClients || !IsClientInGame(victim)) return Plugin_Continue;
	if (!IsWitch(attacker)) return Plugin_Continue;
	// A hack. We assume the first scratch isn't the same damage as an incap scratch.
	// It's also worth noting that this prevents infinite recursion, since WitchScratch reduces the damage dealt.
	if (damage != witchDamage) return Plugin_Continue;
	if (FindValueInArray(engagedWitches, attacker) != -1) return Plugin_Stop;
	startTick = GetEngineTime();
	PushArrayCell(engagedWitches, attacker);
	new Handle:data = CreateArray(64);
	PushArrayCell(data, victim);
	PushArrayCell(data, attacker);
	PushArrayCell(data, inflictor);
	PushArrayCell(data, damageType);
	// The witch scratch animation is every 1/4 second. Repeat until survivor DIES.
	CreateTimer(0.25, WitchScratch, data, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE);

	damage = witchDamage/2.5;
	if (DEBUG) PrintToChatAll("[%02.3f] Player %d took %3f damage from witch %d/%d.", GetEngineTime()-startTick, victim, witchDamage/2.5, attacker, inflictor);
	return Plugin_Continue;
}

public Action:WitchScratch(Handle:timer, any:data) {
	new victim = GetArrayCell(data, 0);
	// If defibrillator_use_time is less than .25, this may bug out.
	new attacker = GetArrayCell(data, 1);
	if (!IsPlayerAlive(victim)) {
		new index = FindValueInArray(engagedWitches, attacker);
		RemoveFromArray(engagedWitches, index);
		return Plugin_Stop;
	}
	new index = FindValueInArray(deadWitches, attacker);
	if (index != -1) {
		RemoveFromArray(deadWitches, index);
		index = FindValueInArray(engagedWitches, attacker);
		if (index != -1) RemoveFromArray(engagedWitches, index);
		return Plugin_Stop;
	}
	new inflictor = GetArrayCell(data, 2);
	new damageType = GetArrayCell(data, 3);
	SDKHooks_TakeDamage(victim, attacker, inflictor, witchDamage/2.5, damageType);
	if (DEBUG) PrintToChatAll("[%02.3f] Player %d took %3f damage from witch %d/%d", GetEngineTime()-startTick, victim, witchDamage/2.5, attacker, inflictor);
	return Plugin_Continue;
}

public witch_killed(Handle:event, const String:name[], bool:dontBroadcast) {
	new witch = GetEventInt(event, "witchid");
	PushArrayCell(deadWitches, witch);
}

#include <sourcemod>
#include <sdkhooks>
#define SPLIT 5
#pragma semicolon 1

public Plugin:myinfo =
{
	name = "Smooth witch damage",
	author = "Darkid",
	description = "Smooths out the damage taken from a witch while incapped.",
	version = "1.5",
	url = "https://github.com/jbzdarkid/Random/witchdamage"
}

new Float:witchDamage;
new Handle:deadWitches;
new Float:startTick;
new bool:lateLoad = false;

public APLRes:AskPluginLoad2(Handle:plugin, bool:late, String:error[], errMax) {
	lateLoad = late;
	return APLRes_Success;
}

public OnPluginStart() {
	if (lateLoad) {
		for (new client=1; client<=MaxClients; client++) {
			if (!IsClientInGame(client)) continue;
			OnClientPostAdminCheck(client);
		}
	}

	decl String:witchDamageString[64];
	GetConVarString(FindConVar("z_witch_damage_per_kill_hit"), witchDamageString, sizeof(witchDamageString));
	HookConVarChange(FindConVar("z_witch_damage_per_kill_hit"), OnWitchDamageChange);
	witchDamage = 1.0*StringToInt(witchDamageString);

	deadWitches = CreateArray(64);

	HookEvent("round_start", round_start);
	HookEvent("witch_killed", witch_killed);
	startTick = 0.0;
}

public OnWitchDamageChange(Handle:cvar, const String:oldVal[], const String:newVal[]) {
	witchDamage = 1.0*StringToInt(newVal);
}

public OnClientPostAdminCheck(client) {
	SDKHook(client, SDKHook_OnTakeDamage, OnTakeDamage);
}

public round_start(Handle:event, const String:name[], bool:dontBroadcast) {
	ClearArray(deadWitches);
}

bool:IsWitch(entity) {
	if (entity <= 0 || !IsValidEntity(entity) || !IsValidEdict(entity)) return false;
	decl String:className[8];
	GetEdictClassname(entity, className, sizeof(className));
	return strcmp(className, "witch") == 0;
}

public Action:OnTakeDamage(victim, &attacker, &inflictor, &Float:damage, &damageType) {
	if (victim <= 0 || victim > MaxClients || !IsClientInGame(victim)) return Plugin_Continue;
	if (!IsWitch(attacker)) return Plugin_Continue;
	if (damage != witchDamage) return Plugin_Continue; // A hack. We assume the first scratch isn't the same damage as an incap scratch.
	if (startTick == 0.0) startTick = GetEngineTime();
	new Handle:data = CreateArray(64);
	PushArrayCell(data, victim);
	PushArrayCell(data, attacker);
	PushArrayCell(data, inflictor);
	PushArrayCell(data, damageType);
	PushArrayCell(data, SPLIT-1);
	// The witch scratch animation is every 1/4 second. Repeat until all the damage is done.
	CreateTimer(0.25, WitchScratch, data, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE);

	// Allow this scratch's damage through, to an appropriate degree.
	damage = witchDamage/SPLIT;
	PrintToChatAll("[%2.3f] Player %d took %3f damage (%d of %d) from witch %d/%d.", GetEngineTime()-startTick, victim, witchDamage/SPLIT, 1, SPLIT, attacker, inflictor);
	return Plugin_Continue;
}

public Action:WitchScratch(Handle:timer, any:data) {
	new victim = GetArrayCell(data, 0);
	// If defibrillator_use_time is less than .25, this may bug out.
	if (!IsPlayerAlive(victim)) return Plugin_Stop;
	new attacker = GetArrayCell(data, 1);
	new inflictor = GetArrayCell(data, 2);
	new damageType = GetArrayCell(data, 3);
	new count = GetArrayCell(data, 4);
	if (count <= 0) return Plugin_Stop;
	new index = FindValueInArray(deadWitches, attacker);
	if (index != -1) return Plugin_Stop;
	SDKHooks_TakeDamage(victim, attacker, inflictor, witchDamage/SPLIT, damageType);
	SetArrayCell(data, 4, count-1);
	return Plugin_Continue;
}

public witch_killed(Handle:event, const String:name[], bool:dontBroadcast) {
	new witch = GetEventInt(event, "witchid");
	PushArrayCell(deadWitches, witch);
}


Make chmod die if it fails

Make chmod die if it fails

augment List[File] {
    method remove? { $self.${xargs rm -rf !>X} }

    method cat@ { ${cat @$self} }
}

my File $?file-cleanup = ${mktemp};

#| File is string representing a filesystem path (relative or absolute).
#|{
    my $outdated = 'foo1.myorg.com';
    if File</etc/foo.conf>.contains($outdated) {
        note "$outdated exists in $_...fixing!";
        .copy-to('/etc/foo.conf.bak');
        .subst($outdated,'foo2.myorg.com');
    }
}
augment File {
    #| Returns True if the file exists.
    method exists? { ${test -e $self}  }
    #| Alias for .exists
    method e?      { $self.exists }
    #| In Bool context, Files return [.exists](#exists)
    method Bool { $self.exists }
    #| Returns True if the file is a directory]
    method dir?    { ${test -d $self} }
    #| Alias for .dir
    method d?      { $self.dir }
    #| Returns True if the file is file
    method file?   { ${test -f $self}  }
    #| Alias for .file
    method f?      { $self.file }
    #| Removes (unlinks) the file and returns True if it was successful.
    #| If the file is a directory it will recursively it's children.
    method remove? ${rm -rf $self}
    #| Returns True if the file is empty
    method empty?  { ! ${test -s $self} }
    #| Returns True if the file is writable by the current user.
    method writable? { ${test -w $self}  }
    #| Alias for .writable
    method w?        { $self.writable        }
    #| Returns True if the file is executable
    method executable? { ${test -x $self} }
    #| Alias for .executable
    method x?          { $self.executable     }
    #| Returns True if the file is readable by the current user.
    method readable?   { ${test -r $self} }
    #| Alias for .readable
    method r?          { $self.readable }
    #| Returns the size of the file in bytes
    method size+   { ${wc -c < $self}  }
    #| Alias for .size
    method s+      { $self.size }

    method mtime-->DateTime on {
        GNU     ${ date -u -r $self '+%FT%T.%3N' !>X}
        BusyBox ${ stat -c '%y' $self !>X| sed -r 's/ /T/;s/0{6}$//' }
    }

    method ctime-->DateTime on {
        #TODO: roll these into one
        GNU     ${ stat -c '%z' $self !>X | sed -r 's/ /T/;s/[0-9]{6} \S*$//' }
        BusyBox ${ stat -c '%z' $self !>X | sed -r 's/ /T/;s/0{6}$//' }
    }

    method lines+ { ${wc -l < $self} }
    #| Returns the line of text at an index. `.at-pos` is the
    #| internal method called when using postcircumfix `[]`.
    #|{
        my File $file;
        $file.write(~<foo bar baz>);
        say $file[1] #-> bar
    }
    method at-pos(#|[The index of the line to return]Int $i)~ {
        ${sed -n ($i+1 ~ 'p') $self}
    }
    #| Calls chmod(1) with the file as the last argument.
    #|{ .chmod(400) if File<foo.txt>.writeable }
    method chmod(#|[The argument passed to chmod(1)]$mode)^ {
        ${chmod $mode $self !>error} ?? $self !! die "Failed to run ‘chmod $mode’ on $self";
    }
    #| Returns the name of the user that owns the file.
    method owner~ on {
        GNU { ${ stat -c '%U' $self } }
        Any { ${ ls -ld $self | awk '{print $3}' } }
    }

    #| Returns the name of the group that own the file.
    method group~ on {
        GNU { ${ stat -c '%G' $self } }
        Any { ${ ls -ld $self | awk '{print $4}' } }
    }

    #| Appends raw data to the file.
    method append(#|[data to append] $data) { $data.${ >> $self } }

    #| Appends a string to a file,. If the file doesn't end in a
    #| newline, one will be prepended to the string. A newline is
    #| appended to the string regardless of whether it already ends in
    #| a string. Returns the argument string.
    method push(#|[string to push] $str)~ {
        $self.try-slurp.${
            awk :l($str) -v "f=$self"
             '{print>f}END{l=ENVIRON["l"];print l>f; printf "%s", l}'
        }
    }

    method unshift($str)~ {
        $self.try-slurp.${
            awk :l($str) -v "f=$self" -v 'RS=^$'
              'END{l=ENVIRON["l"];printf "%s",l;print l>f;printf "%s",$0>f}'
        }
    }

    method shift~   ${sed -nri -e '1!p;1w/dev/stdout' $self}
    method pop~     ${sed -nri -e '$!p;$w/dev/stdout' $self}

    #| Calls `touch(1)` on the file.
    #|{ .touch unless File<foo.txt> }
    method touch? {
        # Ohhh when I think about you I
        ${touch $self}
    }
    #| Copies the file to another path
    method copy-to(#|[destination path] $dst,
                   #|[preserve permissions] Bool :$p){
        ${cp "-r{$p && 'p'}" $self $dst}
    }

    #| Moves the file to another location. Overwrites pre-existing
    #| files at the destination location. Returns whether the move
    #| was completed successfully.
    method move-to(#|[The path to move the file to] $destination)^ {
        ${mv $self $destination} ?? $destination !! ()
    }

    #| Sets the file's contents to `$data`. If the file doesn't exist
    #| it will be created.
    method write(#|[The string to write to the file] $data)^  {
        $data.${ > $self  } ?? $self !! ()
    }

    #| Returns true if the file contains the string
    #|{ say "we're in trouble" unless File</etc/hosts>.contains("localhost") }
    method contains(#|[String to be searched for] $needle,
                    #|[Enable case insensitivity] Bool :$i)? {
        $self.slurp.contains($needle,:$i);
    }

    #| Replaces the target strnig with the replacement string in the file.
    #| **This modifies the file**.
    #|{
        given File.tmp {
            .write("foood");
            .subst("o","e");
            .slurp.say; #-> feood
            .subst("o","e",:g);
            .slurp.say; #-> feeed
        }
    }
    method subst(#|[The string to be replaced]$target,
                 #|[The string to replace it with]$replacement,
                 #|[Turn on global matching]Bool :$g){
        $self.slurp.subst($target,$replacement,:$g).write-to($self);
    }

    #| Reads the file into the file's content as a List of lines
    #|{
        my $content = File</etc/hosts>.slurp
        say $content[2]; # print the 3rd line
    }
    method slurp@ {
        $self ?? ${cat $self} !! die "$self could't be slurped because it doesn't exist"
    }

    method try-slurp@ {
        $self.f ?? ${cat $self} !! ()
    }
    #| Opens the file and returns a FD that can be written to.
    #|{
        my File $file = 'foo.txt';
        my $fd = $file.open-w;
        $fd.write("written to via file descriptor");
        say $file.slurp; #-> written to via file descriptor!
    }
    method open-w-->FD is return-by-var {
        my $fd = FD.next-free;
        $fd.open-w($self);
        $fd;
    }

    #| Opens the file and returns a FD that can be read from.
    #|{
        my File $file = 'foo.txt';
        $file.write(<The quick brown fox jumped over the lazy dog>);
        my $fd = $file.open-r;
        $fd.get() && say $~; #-> The
        $fd.get() && say $~; #-> quick
    }
    method open-r-->FD is return-by-var {
        my $fd = FD.next-free;
        $fd.open-r($self);
        $fd;
    }

    #| Returns the file's path (relative or absolute) as a [Str].
    #|{
        my File $file = 'foo.text';
        $file.subst('foo','bar'); # modifies the file
        say $file;
        say $file.path.subst('foo','bar');
    }
    method path~ { $self }
    #| Returns the name of the file.
    #|{ say File</etc/hosts>.name #->hosts }
    method name~ { ${basename $self} }
    #| Returns the parent directory of the file.
    #|{ say File</etc/foo/foo.cfg>.name #->/etc/foo }
    method parent-->File{ ${dirname $self} }
    #| Adds an element to the path. This is the same as concatinating
    #| the path with a '/' and the argument.
    #|{ say File</etc/foo>.add('foo.cfg') #->/etc/foo/foo.cfg }
    method add($name)-->File { $self.path ~ '/' ~ $name }

    #| Creates a tempfile via mktemp(1) and adds it to a list of
    #| files which will be removed at the END.
    #|{
      my $tmpfile = File.tmp; # Will be removed at the end
    }
    static method tmp(Bool :$dir)-->File {
        ${mktemp ('-d' if $dir)}-->File.cleanup
    }

    method cleanup^ {
        $?file-cleanup.push($self);
        FILE-CLEAN ${rm -rf @($?file-cleanup.slurp) $?file-cleanup }
    }

    #| Returns a list of children that match the criteria.
    #|{
        given File("$:HOME/src/spitsh/resources/src") {
            my $loc = 0;
            for .find(name => /\.sp$/) { # or just *.sp
                $loc += .lines;
            }
            say "$loc lines of code";
        }
    }
    method find(Pattern :$name)-->List[File] {
        ${find $self ('-name', $name if $name)}
    }

    method ls(:$all)-->List[File] {
        ${ls ('-A' if $all) $self }
    }

    static method tmp-fifo-->File {
        my File $tmpfifo .= tmp;
        $tmpfifo.remove;
        ${mkfifo $tmpfifo};
        $tmpfifo;
    }

    #| Tries to make a directory at the file's path, recursively if need
    #| be. Returns whether it succeeds.
    #|{
       say "creating " ~ File<path/to/my/dir>.mkdir
    }
    method mkdir-->File {
        ${ mkdir -p $self};
        $self;
    }
    #| Changes directory to the file.
    #|{
        File<path/to/my/dir>.mkdir.cd;
        say "$?PWD";
    }
    method cd {
        ${ cd $self }
    }

    method archive(:$to)-->File {
        my $out = ($to || "$self.tgz");
        ?${ tar -C $self.parent -czf $out  $self.name } && $out;
    }

    method extract-->File {
        ${ tar xvf $self | sed -n '1s/\/.*//p' };
    }

    method grep(Regex $regex)@ {
        ${egrep $regex $self}
    }

    method first(Regex $regex)~ {
        ${egrep -m1 $regex $self}
    }

    method remove-lines(Regex $regex)@ {
        on {
            GNU ${sed -rni "\\§$regex§!\{p;b;\};w/dev/stdout" $self}
            # BB sed doesn't likebranch inside {} without \n following
            BusyBox ${sed -rni "\\§$regex§!\{p;b\n\};w/dev/stdout" $self}
        }
    }

    method filter(Regex $regex, $replacement, :$g)@ {
        ${sed -rni "s§$regex§$replacement§{$g && 'g'}p" $self}
    }

    method line-subst(Regex $regex, $replacement, :$g)@ {
        ${sed -ri "s§$regex§$replacement§{$g && 'g'}" $self}
    }

    method capture(Regex $regex)~ {
        ${sed -rn "s§$regex§\\1§;T;s/^.*//p;q" $self}
    }

    method sha256~ ${ sha256sum -b $self | awk '{print $1}' }
    method sha256-ok($sha256-expected, :$what) {
        my $sha256-got = $self.sha256;
        $sha256-got eq $sha256-expected or
        die "SHAsum didn't match for {$what || $self}\n" ~
        "Got:      $sha256-got\n" ~
        "Expected: $sha256-expected";
    }

}

sub cd(File $dir){ $dir.cd }

augment List[File] {
    method remove? { $self.${xargs rm -rf !>X} }

    method cat@ { ${cat @$self} }
}

my File $?file-cleanup = ${mktemp};

#| File is string representing a filesystem path (relative or absolute).
#|{
    my $outdated = 'foo1.myorg.com';
    if File</etc/foo.conf>.contains($outdated) {
        note "$outdated exists in $_...fixing!";
        .copy-to('/etc/foo.conf.bak');
        .subst($outdated,'foo2.myorg.com');
    }
}
augment File {
    #| Returns True if the file exists.
    method exists? { ${test -e $self}  }
    #| Alias for .exists
    method e?      { $self.exists }
    #| In Bool context, Files return [.exists](#exists)
    method Bool { $self.exists }
    #| Returns True if the file is a directory]
    method dir?    { ${test -d $self} }
    #| Alias for .dir
    method d?      { $self.dir }
    #| Returns True if the file is file
    method file?   { ${test -f $self}  }
    #| Alias for .file
    method f?      { $self.file }
    #| Removes (unlinks) the file and returns True if it was successful.
    #| If the file is a directory it will recursively it's children.
    method remove? ${rm -rf $self}
    #| Returns True if the file is empty
    method empty?  { ! ${test -s $self} }
    #| Returns True if the file is writable by the current user.
    method writable? { ${test -w $self}  }
    #| Alias for .writable
    method w?        { $self.writable        }
    #| Returns True if the file is executable
    method executable? { ${test -x $self} }
    #| Alias for .executable
    method x?          { $self.executable     }
    #| Returns True if the file is readable by the current user.
    method readable?   { ${test -r $self} }
    #| Alias for .readable
    method r?          { $self.readable }
    #| Returns the size of the file in bytes
    method size+   { ${wc -c < $self}  }
    #| Alias for .size
    method s+      { $self.size }

    method mtime-->DateTime on {
        GNU     ${ date -u -r $self '+%FT%T.%3N' !>X}
        BusyBox ${ stat -c '%y' $self !>X| sed -r 's/ /T/;s/0{6}$//' }
    }

    method ctime-->DateTime on {
        #TODO: roll these into one
        GNU     ${ stat -c '%z' $self !>X | sed -r 's/ /T/;s/[0-9]{6} \S*$//' }
        BusyBox ${ stat -c '%z' $self !>X | sed -r 's/ /T/;s/0{6}$//' }
    }

    method lines+ { ${wc -l < $self} }
    #| Returns the line of text at an index. `.at-pos` is the
    #| internal method called when using postcircumfix `[]`.
    #|{
        my File $file;
        $file.write(~<foo bar baz>);
        say $file[1] #-> bar
    }
    method at-pos(#|[The index of the line to return]Int $i)~ {
        ${sed -n ($i+1 ~ 'p') $self}
    }
    #| Calls chmod(1) with the file as the last argument.
    #|{ .chmod(400) if File<foo.txt>.writeable }
    method chmod(#|[The argument passed to chmod(1)]$mode)^ {
        ${chmod $mode $self} ?? $self !! ()
    }
    #| Returns the name of the user that owns the file.
    method owner~ on {
        GNU { ${ stat -c '%U' $self } }
        Any { ${ ls -ld $self | awk '{print $3}' } }
    }

    #| Returns the name of the group that own the file.
    method group~ on {
        GNU { ${ stat -c '%G' $self } }
        Any { ${ ls -ld $self | awk '{print $4}' } }
    }

    #| Appends raw data to the file.
    method append(#|[data to append] $data) { $data.${ >> $self } }

    #| Appends a string to a file,. If the file doesn't end in a
    #| newline, one will be prepended to the string. A newline is
    #| appended to the string regardless of whether it already ends in
    #| a string. Returns the argument string.
    method push(#|[string to push] $str)~ {
        $self.try-slurp.${
            awk :l($str) -v "f=$self"
             '{print>f}END{l=ENVIRON["l"];print l>f; printf "%s", l}'
        }
    }

    method unshift($str)~ {
        $self.try-slurp.${
            awk :l($str) -v "f=$self" -v 'RS=^$'
              'END{l=ENVIRON["l"];printf "%s",l;print l>f;printf "%s",$0>f}'
        }
    }

    method shift~   ${sed -nri -e '1!p;1w/dev/stdout' $self}
    method pop~     ${sed -nri -e '$!p;$w/dev/stdout' $self}

    #| Calls `touch(1)` on the file.
    #|{ .touch unless File<foo.txt> }
    method touch? {
        # Ohhh when I think about you I
        ${touch $self}
    }
    #| Copies the file to another path
    method copy-to(#|[destination path] $dst,
                   #|[preserve permissions] Bool :$p){
        ${cp "-r{$p && 'p'}" $self $dst}
    }

    #| Moves the file to another location. Overwrites pre-existing
    #| files at the destination location. Returns whether the move
    #| was completed successfully.
    method move-to(#|[The path to move the file to] $destination)^ {
        ${mv $self $destination} ?? $destination !! ()
    }

    #| Sets the file's contents to `$data`. If the file doesn't exist
    #| it will be created.
    method write(#|[The string to write to the file] $data)^  {
        $data.${ > $self  } ?? $self !! ()
    }

    #| Returns true if the file contains the string
    #|{ say "we're in trouble" unless File</etc/hosts>.contains("localhost") }
    method contains(#|[String to be searched for] $needle,
                    #|[Enable case insensitivity] Bool :$i)? {
        $self.slurp.contains($needle,:$i);
    }

    #| Replaces the target strnig with the replacement string in the file.
    #| **This modifies the file**.
    #|{
        given File.tmp {
            .write("foood");
            .subst("o","e");
            .slurp.say; #-> feood
            .subst("o","e",:g);
            .slurp.say; #-> feeed
        }
    }
    method subst(#|[The string to be replaced]$target,
                 #|[The string to replace it with]$replacement,
                 #|[Turn on global matching]Bool :$g){
        $self.slurp.subst($target,$replacement,:$g).write-to($self);
    }

    #| Reads the file into the file's content as a List of lines
    #|{
        my $content = File</etc/hosts>.slurp
        say $content[2]; # print the 3rd line
    }
    method slurp@ {
        $self ?? ${cat $self} !! die "$self could't be slurped because it doesn't exist"
    }

    method try-slurp@ {
        $self.f ?? ${cat $self} !! ()
    }
    #| Opens the file and returns a FD that can be written to.
    #|{
        my File $file = 'foo.txt';
        my $fd = $file.open-w;
        $fd.write("written to via file descriptor");
        say $file.slurp; #-> written to via file descriptor!
    }
    method open-w-->FD is return-by-var {
        my $fd = FD.next-free;
        $fd.open-w($self);
        $fd;
    }

    #| Opens the file and returns a FD that can be read from.
    #|{
        my File $file = 'foo.txt';
        $file.write(<The quick brown fox jumped over the lazy dog>);
        my $fd = $file.open-r;
        $fd.get() && say $~; #-> The
        $fd.get() && say $~; #-> quick
    }
    method open-r-->FD is return-by-var {
        my $fd = FD.next-free;
        $fd.open-r($self);
        $fd;
    }

    #| Returns the file's path (relative or absolute) as a [Str].
    #|{
        my File $file = 'foo.text';
        $file.subst('foo','bar'); # modifies the file
        say $file;
        say $file.path.subst('foo','bar');
    }
    method path~ { $self }
    #| Returns the name of the file.
    #|{ say File</etc/hosts>.name #->hosts }
    method name~ { ${basename $self} }
    #| Returns the parent directory of the file.
    #|{ say File</etc/foo/foo.cfg>.name #->/etc/foo }
    method parent-->File{ ${dirname $self} }
    #| Adds an element to the path. This is the same as concatinating
    #| the path with a '/' and the argument.
    #|{ say File</etc/foo>.add('foo.cfg') #->/etc/foo/foo.cfg }
    method add($name)-->File { $self.path ~ '/' ~ $name }

    #| Creates a tempfile via mktemp(1) and adds it to a list of
    #| files which will be removed at the END.
    #|{
      my $tmpfile = File.tmp; # Will be removed at the end
    }
    static method tmp(Bool :$dir)-->File {
        ${mktemp ('-d' if $dir)}-->File.cleanup
    }

    method cleanup^ {
        $?file-cleanup.push($self);
        FILE-CLEAN ${rm -rf @($?file-cleanup.slurp) $?file-cleanup }
    }

    #| Returns a list of children that match the criteria.
    #|{
        given File("$:HOME/src/spitsh/resources/src") {
            my $loc = 0;
            for .find(name => /\.sp$/) { # or just *.sp
                $loc += .lines;
            }
            say "$loc lines of code";
        }
    }
    method find(Pattern :$name)-->List[File] {
        ${find $self ('-name', $name if $name)}
    }

    method ls(:$all)-->List[File] {
        ${ls ('-A' if $all) $self }
    }

    static method tmp-fifo-->File {
        my File $tmpfifo .= tmp;
        $tmpfifo.remove;
        ${mkfifo $tmpfifo};
        $tmpfifo;
    }

    #| Tries to make a directory at the file's path, recursively if need
    #| be. Returns whether it succeeds.
    #|{
       say "creating " ~ File<path/to/my/dir>.mkdir
    }
    method mkdir-->File {
        ${ mkdir -p $self};
        $self;
    }
    #| Changes directory to the file.
    #|{
        File<path/to/my/dir>.mkdir.cd;
        say "$?PWD";
    }
    method cd {
        ${ cd $self }
    }

    method archive(:$to)-->File {
        my $out = ($to || "$self.tgz");
        ?${ tar -C $self.parent -czf $out  $self.name } && $out;
    }

    method extract-->File {
        ${ tar xvf $self | sed -n '1s/\/.*//p' };
    }

    method grep(Regex $regex)@ {
        ${egrep $regex $self}
    }

    method first(Regex $regex)~ {
        ${egrep -m1 $regex $self}
    }

    method remove-lines(Regex $regex)@ {
        on {
            GNU ${sed -rni "\\§$regex§!\{p;b;\};w/dev/stdout" $self}
            # BB sed doesn't likebranch inside {} without \n following
            BusyBox ${sed -rni "\\§$regex§!\{p;b\n\};w/dev/stdout" $self}
        }
    }

    method filter(Regex $regex, $replacement, :$g)@ {
        ${sed -rni "s§$regex§$replacement§{$g && 'g'}p" $self}
    }

    method line-subst(Regex $regex, $replacement, :$g)@ {
        ${sed -ri "s§$regex§$replacement§{$g && 'g'}" $self}
    }

    method capture(Regex $regex)~ {
        ${sed -rn "s§$regex§\\1§;T;s/^.*//p;q" $self}
    }

    method sha256~ ${ sha256sum -b $self | awk '{print $1}' }
    method sha256-ok($sha256-expected, :$what) {
        my $sha256-got = $self.sha256;
        $sha256-got eq $sha256-expected or
        die "SHAsum didn't match for {$what || $self}\n" ~
        "Got:      $sha256-got\n" ~
        "Expected: $sha256-expected";
    }

}

sub cd(File $dir){ $dir.cd }