UPGRADEABLE_WEAPONS = {
	"tf_weapon_scattergun",
	"tf_weapon_pistol",
	"tf_weapon_bat",
	"tf_weapon_rocketlauncher",
	"tf_weapon_shotgun_primary",
	"tf_weapon_shovel",
	"tf_weapon_flamethrower",
	"tf_weapon_fireaxe",
	"tf_weapon_grenadelauncher",
	"tf_weapon_pipebomblauncher",
	"tf_weapon_bottle",
	"tf_weapon_minigun",
	"tf_weapon_fists",
	"tf_weapon_wrench",
	"tf_weapon_syringegun_medic",
	"tf_weapon_medigun",
	"tf_weapon_bonesaw",
	"tf_weapon_sniperrifle",
	"tf_weapon_smg",
	"tf_weapon_club",
	"tf_weapon_revolver",
	"tf_weapon_builder",
	"tf_weapon_knife",
	"tf_weapon_invis",
};

WEARS = {
	["factory new"]    = 0.0,
	["minimal wear"]   = 0.25,
	["field-tested"]   = 0.5,
	["well-worn"]      = 0.75,
	["battle scarred"] = 1.0,
};

UNUSUALS = {
	["reset"]      = 0,
	["hot"]        = 701,
	["isotope"]    = 702,
	["cool"]       = 703,
	["energy orb"] = 704,
};

KILLSTREAKS = {
	["reset"]             = 0,
	["team shine"]        = 1,
	["deadly daffodil"]   = 2,
	["manndarin"]         = 3,
	["mean green"]        = 4,
	["agonizing emerald"] = 5,
	["villainous violet"] = 6,
	["hot rod"]           = 7,
};

WARPAINT_WEAPONS = {
	[TF_CLASS_SCOUT] = {
		[LOADOUT_POSITION_PRIMARY]   = { "tf_weapon_scattergun", "the shortstop", "the soda popper" },
		[LOADOUT_POSITION_SECONDARY] = { "tf_weapon_pistol", "the winger" },
		[LOADOUT_POSITION_MELEE]     = { "the holy mackerel" }
	},
	[TF_CLASS_SOLDIER] = {
		[LOADOUT_POSITION_PRIMARY]   = { "tf_weapon_rocketlauncher", "the black box", "the air strike" },
		[LOADOUT_POSITION_SECONDARY] = { "tf_weapon_shotgun_primary", "panic attack shotgun", "the reserve shooter" },
		[LOADOUT_POSITION_MELEE]     = { "the disciplinary action" }
	},
	[TF_CLASS_PYRO] = {
		[LOADOUT_POSITION_PRIMARY]   = { "tf_weapon_flamethrower", "the degreaser", "the dragon's fury" },
		[LOADOUT_POSITION_SECONDARY] = { "tf_weapon_shotgun_primary", "panic attack shotgun", "the reserve shooter", "the detonator", "the scorch shot", },
		[LOADOUT_POSITION_MELEE]     = { "the powerjack", "the back scratcher" }
	},
	[TF_CLASS_DEMOMAN] = {
		[LOADOUT_POSITION_PRIMARY]   = { "tf_weapon_grenadelauncher", "the loch-n-load", "the loose cannon", "the iron bomber" },
		[LOADOUT_POSITION_SECONDARY] = { "tf_weapon_pipebomblauncher" },
		[LOADOUT_POSITION_MELEE]     = { "the scotsman's skullcutter", "the claidheamohmor", "the persian persuader" }
	},
	[TF_CLASS_HEAVYWEAPONS] = {
		[LOADOUT_POSITION_PRIMARY]   = { "tf_weapon_minigun", "the brass beast", "tomislav" },
		[LOADOUT_POSITION_SECONDARY] = { "tf_weapon_shotgun_primary", "panic attack shotgun", "the family business" },
		[LOADOUT_POSITION_MELEE]     = {}
	},
	[TF_CLASS_ENGINEER] = {
		[LOADOUT_POSITION_PRIMARY]   = { "tf_weapon_shotgun_primary", "panic attack shotgun", "the rescue ranger" },
		[LOADOUT_POSITION_SECONDARY] = { "tf_weapon_pistol" },
		[LOADOUT_POSITION_MELEE]     = { "tf_weapon_wrench", "the jag" }
	},
	[TF_CLASS_MEDIC] = {
		[LOADOUT_POSITION_PRIMARY]   = { "the crusader's crossbow" },
		[LOADOUT_POSITION_SECONDARY] = { "tf_weapon_medigun" },
		[LOADOUT_POSITION_MELEE]     = { "the ubersaw", "the amputator" }
	},
	[TF_CLASS_SNIPER] = {
		[LOADOUT_POSITION_PRIMARY]   = { "tf_weapon_sniperrifle", "the bazaar bargain" },
		[LOADOUT_POSITION_SECONDARY] = { "tf_weapon_smg" },
		[LOADOUT_POSITION_MELEE]     = { "the shahanshah" }
	},
	[TF_CLASS_SPY] = {
		[LOADOUT_POSITION_PRIMARY]   = { "tf_weapon_revolver" },
		[LOADOUT_POSITION_SECONDARY] = {},
		[LOADOUT_POSITION_MELEE]     = { "tf_weapon_knife" }
	},
};

DEFAULT_WARPAINT_WEAPONS = {
	[TF_CLASS_SCOUT]        = { "tf_weapon_scattergun",      "The Winger",                 "The Holy Mackerel"          },
	[TF_CLASS_SOLDIER]      = { "tf_weapon_rocketlauncher",  "tf_weapon_shotgun",          "The Disciplinary Action"    },
	[TF_CLASS_PYRO]         = { "tf_weapon_flamethrower",    "The Detonator",              "The Powerjack"              },
	[TF_CLASS_DEMOMAN]      = { "tf_weapon_grenadelauncher", "tf_weapon_pipebomblauncher", "The Scotsman's Skullcutter" },
	[TF_CLASS_HEAVYWEAPONS] = { "tf_weapon_minigun",         "The Family Business",        nil                          },
	[TF_CLASS_ENGINEER]     = { "The Rescue Ranger",         "tf_weapon_pistol",           "tf_weapon_wrench"           },
	[TF_CLASS_MEDIC]        = { "The Crusader's Crossbow",   "tf_weapon_medigun",          "The Amputator"              },
	[TF_CLASS_SNIPER]       = { "tf_weapon_sniperrifle",     "tf_weapon_smg",              "The Shahanshah"             },
	[TF_CLASS_SPY]          = { "tf_weapon_revolver",        nil,                          "tf_weapon_knife"            },
};

-- Console has a limit to how many characters you can print to it at a time, need to split up documentation
HELP_TEXT1 = [[

	COMMANDS
	--------
	!give <weapon name/defindex> [warpaint id] [wear value/label] [effect value/label] [killstreak value/label]
		- Give yourself an item, optionally with a warpaint and other attributes
		
		-- Wear Values --
		0.00 - `factory new`
		0.25 - `minimal wear`
		0.50 - `field-tested`
		0.75 - `well-worn`
		1.00 - `battle scarred`
		
		-- Effect Values --
		0   - `reset`
		701 - `hot`
		702 - `isotope`
		703 - `cool`
		704 - `energy orb`
		
		-- Killstreak Values --
		0 - `reset`
		1 - `team shine`
		2 - `deadly daffodil`
		3 - `manndarin`
		4 - `mean green`
		5 - `agonizing emerald`
		6 - `villainous violet`
		7 - `hot rod`
		
		--- Examples ---
		!give `The Shortstop`
		!give `The Shortstop` 420
		!give `The Shortstop` 420 `factory new`
		!give `The Shortstop` 420 fac
		!give 220 420 battle
		!give 220 420 1
		!give tf_weapon_scattergun 420 min
		!give 13 420 battle cool `hot rod`
]];

HELP_TEXT2 = [[

	!givebot <weapon name/defindex> [warpaint id] [warpaint value/label] [effect value/label] [killstreak value/label]
		- (Same usage as !give.) Give a bot under your crosshair an item, optionally with and other attributes

	!paint <warpaint id>
		- Change the warpaint of your currently held weapon, keeping its other attributes
	
	!loadoutpaint [warpaint id]
		- With no arguments, (just !loadoutpaint) this command will toggle off
		- With an argument, this command will give you warpaint items with the warpaint id every time you spawn for every class
		
	!next
		- With loadoutpaint active, switch your active weapon to the next warpaint weapon
	
	!prev
		- With loadoutpaint active, switch your active weapon to the previous warpaint weapon
		
	!wear <wear value/label>
		- Change the wear of your currently held weapon, keeping its other attributes

]]

HELP_TEXT3 = [[
	!effect <effect value/label>
		- Change the unusual effect attached to your currently held weapon, keeping its other attributes
	
	!killstreak or !ks <killstreak value/label>
		- Change the killstreak sheen of your currently held weapon, keeping its other attributes
		
	!randomseed or !rs
		- Randomize the paintkit seed on your currently held weapon
		
	!seed [new seed]
		- With no arguments, (just !seed) this command will return the seed of the currently held weapon
		- With an argument, this command will change the seed of the currently held weapon
		
	!thirdperson
		- Toggle thirdperson mode on yourself
		
	!switchteam
		- Switch your team between red/blue.
		
	!wavestart
		- Start / Restart the wave.
		
	!wavepause
		- Pause / Unpause bot spawning and AI.
	
	KEYBINDS
	--------
	+Reload - Toggle hud visibility
	
]]

local pop_interface = Entity("point_populator_interface");
local gamerules     = ents.FindByClass("tf_gamerules");

local wave_paused = false;

-- Split a string by delimiter
function string.split(s, delim)
	delim = delim or "%s"; -- whitespace
	
	local t = {};
	for str in string.gmatch(s, "([^"..delim.."]+)") do -- voodoo magic
		table.insert(t, str);
	end
	
	return t
end

-- Round a number (because including a round function in your programming language is too difficult)
math.round = function(num, decimals)
	if (not decimals or decimals <= 0) then
		return math.floor(num + 0.5);
	else
		local mod = 10 ^ decimals;
		return math.floor((num * mod) + 0.5) / mod;
	end
end

-- Random float
math.randomfloat = function(m, n)
	if (m) then
		if (n) then
			if (n == m) then return m;
			elseif (m > n) then m, n = n, m; end -- n should be greater than m
			
			local dif  = n - m;
			local mod  = dif * math.random();

			return m + mod;
			
		else
			return m * math.random();
		end
	else
		return math.random();
	end
end

-- Parse player chat message, returning command name and args list if message was command
function ParseCommand(text)
	if (not text or #text == 0 or string.sub(text, 1, 1) ~= "!") then return; end
	
	local cmd  = nil;
	local args = {};
	
	-- Construct cmd and args
	local t = string.split(text, " ");
	local string_start = nil;
	for index, str in pairs(t) do
		local firstchar  = string.sub(str, 1, 1);
		local lastchar   = string.sub(str, #str);
		
		-- Sanity check string chars
		if ((#str == 1 and firstchar == "`") or
			(not string_start and firstchar ~= "`" and lastchar == "`") or
			(firstchar == "!" and string.find(str, "`"))) then
			return;
		end
	
		if (not string_start) then
			-- Command name
			if (not cmd and firstchar == "!") then
				cmd = string.sub(str, 2);
				
				-- Invalid command name
				if (not cmd or #cmd == 0) then return; end
			
			-- String start
			elseif (firstchar == "`") then
				if (lastchar ~= "`") then
					string_start = index;
					
				-- ... and end
				else
					table.insert(args, string.sub(str, 2, -2));
				end
				
			-- Normal arg
			else
				table.insert(args, str);
			end
		
		-- String end
		else
			if (lastchar == "`") then
				local s = table.concat(t, " ", string_start, index);
				table.insert(args, string.sub(s, 2, -2));
				string_start = nil;
			end
		end
	end
	
	-- Missing closing ` for string
	if (string_start) then return; end
	
	return cmd, args;
end

-- Find key in table where s is a substring of that key
function FindSubstringKey(t, s)
	local chosen = nil;
	for str, value in pairs(t) do
		local i, j = string.find(str, string.lower(s), 1, true);
		if (i ~= 1) then goto continue; end
		
		-- First substring
		if (not chosen) then chosen = str;
		-- Multiple matches
		else return; end
		
		::continue::
	end
	
	return chosen;
end

function IsValidPlayer(ent)
	return IsValid(ent) and ent:IsPlayer();
end

function IsValidRealPlayer(ent)
	return IsValid(ent) and ent:IsRealPlayer();
end

function IsValidAlivePlayer(ent)
	return IsValid(ent) and ent:IsPlayer() and ent:IsAlive();
end

function IsValidAliveRealPlayer(ent)
	return IsValid(ent) and ent:IsRealPlayer() and ent:IsAlive();
end

-- Grab the CEntity table
local _       = Entity("info_target", false, false);
local CEntity = getmetatable(_);
_:Remove();

-- Delayed ShowHudText display
function CEntity:ShowHudDialogue(text, clr, fadein, sound_interval, sound_duration, fadeout, holdtime, fxtime)
	if (not IsValidPlayer(self)) then return; end
	
	-- Defaults
	fadein         = fadein or 0.025;
	sound_interval = sound_interval or 0.2;
	sound_duration = (sound_duration and sound_duration / sound_interval) or (#text * fadein) / sound_interval -- an approximation
	fadeout        = fadeout or 0.3;
	holdtime       = holdtime or 3;
	fxtime         = fxtime or 0.1;
	
	-- Unpack clr
	local r, g, b;
	if (clr and #clr == 3) then r, g, b = table.unpack(clr);
	else r, g, b = 255, 255, 255; end
	
	-- Display
	self:ShowHudText({
		channel = 0, x = -1, y = 0.3, effect = 2,
		r1 = r, g1 = g, b1 = b, a1 = 0,
		r2 = 0, g2 = 0, b2 = 0, a2 = 0,
		fadeinTime = fadein, fadeoutTime = fadeout,
		holdTime = holdtime, fxTime = fxtime,
	}, text);

	-- Sounds for typing out the chars
	timer.Create(sound_interval, function()
		if (math.random(2) == 1) then
			self:PlaySoundToSelf("ui/panel_close.wav");
		else
			self:PlaySoundToSelf("ui/panel_open.wav");
		end
	end, sound_duration);
end

-- Get player eye angles
function CEntity:GetEyeAngles()
	if (not IsValidPlayer(self)) then return; end
	
	return Vector(self["m_angEyeAngles[0]"], self["m_angEyeAngles[1]"], 0);
end

-- Get weapon slot
function CEntity:GetSlot()
	if (not IsValid(self) or not self:IsWeapon()) then return; end
	
	local player = self.m_hOwner or self.m_hOwnerEntity;
	if (not IsValidPlayer(player)) then return; end
	
	for slot = 0, 6 do
		if (player:GetPlayerItemBySlot(slot) == self) then
			return slot;
		end
	end
end

-- Get base weapon name useable in WARPAINT_WEAPONS
function CEntity:GetBaseWeaponName()
	if (not IsValid(self) or not self:IsWeapon()) then return; end
	
	local weaponname = string.lower(self:GetItemName());
	
	-- Fixup shotgun weapon names
	if (weaponname and string.find(weaponname, "tf_weapon_shotgun")) then
		weaponname = "tf_weapon_shotgun_primary";
	end
	
	-- Remove upgradeable
	local i, j = string.find(weaponname, "upgradeable ");
	if (i and j) then
		weaponname = string.sub(weaponname, j+1);
	end
	
	return weaponname;
end

-- Give a warpaint item to a player
function CEntity:GiveWarpaintItem(weaponname, warpaint, wear, effect, seed, killstreak)
	if (not IsValidAlivePlayer(self) or not weaponname) then return; end
	
	-- Warpaints only work with tf_weapon_shotgun_primary
	if (string.find(weaponname, "tf_weapon_shotgun")) then
		weaponname = "tf_weapon_shotgun_primary";
	end
	
	-- Need to make sure stock weapons are upgradeable to allow warpaints...
	local wep = nil;
	local i, j = string.find(weaponname, "upgradeable");
	
	-- User entered upgradeable already in weapon name
	if (i == 1) then
		wep = self:GiveItem(weaponname);
	-- Add it otherwise
	else
		if (table.HasValue(UPGRADEABLE_WEAPONS, weaponname)) then
			wep = self:GiveItem("upgradeable "..weaponname);
		else
			wep = self:GiveItem(weaponname);
		end
	end
	
	if (not IsValid(wep)) then
		self:Print(PRINT_TARGET_CHAT, "Could not give weapon. Usage: !give <weapon name/defindex> [warpaint id] [warpaint value/label]");
		return;
	end
	
	-- Warpaint
	if (warpaint) then
		wep:SetAttributeValue("paintkit_proto_def_index", tonumber(warpaint));
		
		if (not seed) then
			seed = math.round(math.randomfloat(0, 2^64));
		end
		
		wep:SetAttributeValue("custom_paintkit_seed_lo", seed);
		wep:SetAttributeValue("custom_paintkit_seed_hi", seed);
		
		-- For some reason "paintkit_proto_def_index" gets reset on the weapon after setting it
		-- We need to access it later so we store it here
		wep._paintkit = warpaint;
	end
	
	-- Default to factory new when no wear provided
	wear = wear or 0.0;
	
	local attributes = {
		{ wear,   "Set_item_texture_wear",  WEARS,    "Invalid wear name, Choosing Factory New." },
		{ effect, "attach particle effect", UNUSUALS, "Invalid effect name." },
		{ killstreak, "killstreak idleeffect", KILLSTREAKS, "Invalid killstreak name." },
	};
	
	for index, t in pairs(attributes) do
		local arg, attr, reftable, errormsg = table.unpack(t);
		
		if (arg) then
			if (tonumber(arg)) then
				wep:SetAttributeValue(attr, tonumber(arg));
			else
				local chosen = FindSubstringKey(reftable, arg);
				
				if (not chosen) then
					self:Print(PRINT_TARGET_CHAT, "Invalid command usage: "..errormsg);
				else
					-- Give specialized killstreak tier
					if (reftable == KILLSTREAKS) then
						wep:SetAttributeValue("killstreak tier", 2);
					end
					
					wep:SetAttributeValue(attr, reftable[chosen]);
				end
			end
		end		
	end
	
	return wep;
end

function HandleSimpleCommand(player, args, var, giveitem_args)
	if (#args == 0) then
		if (not var) then
			player:Print(PRINT_TARGET_CHAT, "The weapon does not have this attribute.");
		else
			player:Print(PRINT_TARGET_CHAT, tostring(var));
		end
	else
		player:GiveWarpaintItem(table.unpack(giveitem_args));
	end
end

-- Called after a player typed a message in chat
function OnPlayerChat(event_table)
	local cmd, args = ParseCommand(event_table.text);
	local player    = ents.GetPlayerByUserId(event_table.userid);
	
	local wep       = player.m_hActiveWeapon;
	if (not IsValid(wep)) then return; end
	
	local wear       = wep:GetAttributeValue("Set_item_texture_wear");
	local effect     = wep:GetAttributeValue("attach particle effect");
	local seed       = wep:GetAttributeValue("custom_paintkit_seed_lo");
	local killstreak = wep:GetAttributeValue("killstreak idleeffect");
	
	if (not cmd) then return; end
	
	if (cmd == "give" and #args > 0) then
		local weaponname = string.lower(args[1]);
		local itemid     = tonumber(weaponname);
		if (itemid) then
			weaponname = string.lower(util.GetItemDefinitionNameByIndex(itemid));
		end
		
		seed = math.round(math.randomfloat(0, 2^64));
		player:GiveWarpaintItem(weaponname, args[2], args[3], args[4], seed, killstreak);
		
	elseif (cmd == "givebot" and #args > 0) then
		if (util.IsLagCompensationActive()) then return; end
		
		local ang = player:GetEyeAngles();
		
		-- Trace line from eyes
		util.StartLagCompensation(player)
		local traceresult = util.Trace({
			start          = player,
			endpos         = nil,
			distance       = 8192,
			angles         = ang,
			mask           = MASK_SOLID,
			collisiongroup = COLLISION_GROUP_PLAYER,
		});
		util.FinishLagCompensation(player)

		local bot = traceresult.Entity;
		if (not IsValidPlayer(bot) or not bot:IsAlive()) then
			player:Print(PRINT_TARGET_CHAT, "No bot found under crosshair");
			return;
		end
		
		local weaponname = string.lower(args[1]);
		local itemid     = tonumber(weaponname);
		if (itemid) then
			weaponname = string.lower(util.GetItemDefinitionNameByIndex(itemid));
		end
		
		seed = math.round(math.randomfloat(0, 2^64));
		bot:GiveWarpaintItem(weaponname, args[2], args[3], args[4], seed, killstreak);
	
	elseif (cmd == "paint") then
		HandleSimpleCommand(player, args, wep._paintkit, {string.lower(wep:GetItemName()), args[1], wear, effect, seed, killstreak});
		
	elseif (cmd == "loadoutpaint") then
		if (player._loadoutpaint and #args == 0) then
			player._loadoutpaint = nil;
		else
			player._loadoutpaint = args[1];
		end

		player:ForceRespawn();
	
	elseif (cmd == "next" or cmd == "prev") then
		if (not player._loadoutpaint) then return; end
		
		local weaponname = wep:GetBaseWeaponName();
		
		local mod = 1;
		if (cmd == "prev") then mod = -1; end
		
		local weptable = WARPAINT_WEAPONS[player.m_iClass][wep:GetSlot()];
		for index, name in pairs(weptable) do
			if (weaponname == name) then
				if (index + mod > #weptable) then
					weaponname = weptable[1];
				elseif (index + mod < 1) then
					weaponname = weptable[#weptable];
				else
					weaponname = weptable[index + mod];
				end
				break;
			end
		end
		
		player:GiveWarpaintItem(weaponname, wep._paintkit, wear, effect, seed, killstreak);
		
	elseif (cmd == "wear") then
		HandleSimpleCommand(player, args, wear, {string.lower(wep:GetItemName()), wep._paintkit, args[1], effect, seed, killstreak});
		
	elseif (cmd == "effect") then
		HandleSimpleCommand(player, args, effect, {string.lower(wep:GetItemName()), wep._paintkit, wear, args[1], seed, killstreak});
		
	elseif (cmd == "killstreak" or cmd == "ks") then
		HandleSimpleCommand(player, args, killstreak, {string.lower(wep:GetItemName()), wep._paintkit, wear, effect, seed, args[1]});
		
	elseif (cmd == "randomseed") then
		seed = math.round(math.randomfloat(0, 2^64));
		player:GiveWarpaintItem(string.lower(wep:GetItemName()), wep._paintkit, wear, effect, seed, killstreak);
		
	elseif (cmd == "rs") then
		seed = math.round(math.randomfloat(0, 2^16));
		player:GiveWarpaintItem(string.lower(wep:GetItemName()), wep._paintkit, wear, effect, seed, killstreak);		

	elseif (cmd == "seed") then
		HandleSimpleCommand(player, args, seed, {string.lower(wep:GetItemName()), wep._paintkit, wear, effect, args[1], killstreak});
	
	elseif (cmd == "thirdperson") then
		player._thirdperson = player._thirdperson == 1 and 0 or 1
		player:SetForcedTauntCam(player._thirdperson);
		
	elseif (cmd == "switchteam" or cmd == "switchteams") then
		if (player._reprogrammed) then
			player._reprogrammed = false;
			player:RemoveCond(TF_COND_REPROGRAMMED);
		else
			player._reprogrammed = true;
			player:AddCond(TF_COND_REPROGRAMMED);
		end
		
		-- Re-give our weapons to apply the opposite skin of the warpaint
		for slot = 0, 2 do
			local wep = player:GetPlayerItemBySlot(slot);
			if (IsValid(wep)) then
				local wear   = wep:GetAttributeValue("Set_item_texture_wear") or 0;
				local effect = wep:GetAttributeValue("attach particle effect");
				local seed   = wep:GetAttributeValue("custom_paintkit_seed_lo");
				local killstreak = wep:GetAttributeValue("killstreak idleeffect");
				
				player:GiveWarpaintItem(string.lower(wep:GetItemName()), wep._paintkit, wear, effect, seed, killstreak);
			end
		end
		
	elseif (cmd == "wavestart") then
		gamerules.m_flRestartRoundTime = CurTime();
		
	elseif (cmd == "wavepause") then
		-- Setup
		if (gamerules.m_iRoundState == 10) then
			player:Print(PRINT_TARGET_CHAT, "Wave has not been started yet.");
			return;
		end
		
		local players = ents.GetAllPlayers();
		local flags   = ents.FindAllByClass("item_teamflag");
		
		if (not wave_paused) then
			wave_paused = true;
			pop_interface:PauseBotSpawning();
			
			for index, pl in pairs(players) do
				if (pl:IsBot()) then
					pl:SetAttributeValue("no_attack", 1);
					pl:SetAttributeValue("no_duck", 1);
					pl:SetAttributeValue("no_jump", 1);
					pl._movespeed = pl:GetAttributeValue("move speed bonus") or 1;
					pl:SetAttributeValue("move speed bonus", 0.001);
					pl:BotCommand("interrupt_action -pos 0 -2000 750 -lookpos 0 -2000 750 -waituntildone -alwayslook");
				end
			end
			
			for index, flag in pairs(flags) do
				flag:ForceDrop();
				flag:Disable();
			end
		else
			wave_paused = false;
			pop_interface:UnpauseBotSpawning();
			
			for index, pl in pairs(players) do
				if (pl:IsBot()) then
					pl:SetAttributeValue("no_attack", nil);
					pl:SetAttributeValue("no_duck", nil);
					pl:SetAttributeValue("no_jump", nil);
					pl:SetAttributeValue("move speed bonus", pl._movespeed);
					pl:BotCommand("stop interrupt action");
				end
			end	
			
			for index, flag in pairs(flags) do
				flag:Enable();
			end			
		end
		
	elseif (cmd == "spawnbot") then
		local players = ents.GetAllPlayers();
		local bot = nil;
		
		for _, team in pairs({2, 1, 3}) do
			for __, pl in pairs(players) do
				if (pl:IsBot() and pl.m_iTeamNum == team) then
					bot = pl;
					goto break_outer;
				end
			end
		end
		::break_outer::
		
		if (not bot) then
			player:Print(PRINT_TARGET_CHAT, "Could not find adequate target; missing bots on other teams.");
			return;
		end
		
		-- Move to red
		if (bot.m_iTeamNum ~= TEAM_RED) then
			bot:RunScriptCode("self.ForceChangeTeam(2, false);");
			bot:SwitchClass(player.m_iClass);
			bot:ForceRespawn();
		end
		
		-- give cosmetics and human model
		
		-- Teleport
		local trace;
		local position;
		local ang = Vector(player["m_angEyeAngles[0]"], player["m_angEyeAngles[1]"], 0);
		
		if (not util.IsLagCompensationActive()) then
			util.StartLagCompensation(player)
			trace = util.Trace({
				start = player,
				distance = 8192,
				angles = ang,	
			});
			util.FinishLagCompensation(player)
		end
		
		if (not trace or not trace.Hit) then
			position = player:GetAbsOrigin();
		else
			position = trace.HitPos;
			ang = Vector(ang.x, ang.y - 180, ang.z)
		end
		
		bot._think = timer.Create(0.001, function()
			bot:Teleport(position, ang, Vector(0,0,0));
		end, 0);

		
	elseif (cmd == "commandhelp" or cmd == "commands") then
		timer.Create(0.1, function() player:Print(PRINT_TARGET_CONSOLE, HELP_TEXT1); end);
		timer.Create(0.15, function() player:Print(PRINT_TARGET_CONSOLE, HELP_TEXT2); end);
		timer.Create(0.2, function() player:Print(PRINT_TARGET_CONSOLE, HELP_TEXT3); end);
	end
end

function OnPlayerKey(player, key)
	if (not IsValidRealPlayer(player)) then return end;
	
	if (key == IN_RELOAD) then
		if (player._hudvisibility) then
			player._hudvisibility = false;
		else
			player._hudvisibility = true;
		end
		
		player:SetHUDVisibility(player._hudvisibility);
	end
end

function OnPlayerInventoryApplication(eventTable)
	local player = ents.GetPlayerByUserId(eventTable.userid);
	if (not IsValidRealPlayer(player)) then return; end
	
	player._hudvisibility = true;
	player._thirdperson   = 0;
	player._reprogrammed  = false;
end

function OnPlayerSpawn(player)
	timer.Create(1, function()
		player:ShowHudDialogue("Type !commands to get info on command usage in console", {20, 200, 50}, 0.025, 0.1, 1.3, 0.3, 5);
	end, 1);
	
	if (player._loadoutpaint) then
		local seed  = math.round(math.randomfloat(0, 2^64));
		local paint = player._loadoutpaint;
		
		for slot = 0, 2 do
			local wep = player:GetPlayerItemBySlot(slot);
			
			local weaponname = nil;
			if (IsValid(wep)) then
				weaponname = wep:GetBaseWeaponName();
			end
			
			-- Get weapon name from default weapon list
			if (not table.HasValue(WARPAINT_WEAPONS[player.m_iClass][slot], weaponname)) then
				weaponname = DEFAULT_WARPAINT_WEAPONS[player.m_iClass][slot+1];
				if (not weaponname) then goto continue; end
			end
			
			player:GiveWarpaintItem(weaponname, paint, nil, nil, seed);
			
			::continue::
		end
	end
end

function OnPlayerConnected(player)
	if (player:IsRealPlayer()) then
		player:AddCallback(ON_KEY_PRESSED, OnPlayerKey);
		player:AddCallback(ON_SPAWN, OnPlayerSpawn);
		
		player._hudvisibility = true;
		player._thirdperson   = 0;
		player._reprogrammed  = false;
		player._loadoutpaint  = nil;
	end
end

function OnWaveInit(wave)
	wave_paused = false;
	
	if (not IsValid(pop_interface)) then
		pop_interface = Entity("point_populator_interface");
	end
end

AddEventCallback("player_say", OnPlayerChat);
AddEventCallback("post_inventory_application", OnPlayerInventoryApplication);