--[[

Zombie Survival
by William "JetBoom" Moodhe
williammoodhe@gmail.com -or- jetboom@noxiousnet.com
http://www.noxiousnet.com/

Further credits displayed by pressing F1 in-game.
This was my first ever gamemode. A lot of stuff is from years ago and some stuff is very recent.

]]

AddCSLuaFile("cl_init.lua")
AddCSLuaFile("shared.lua")

AddCSLuaFile("sh_translate.lua")
AddCSLuaFile("sh_colors.lua")
AddCSLuaFile("sh_serialization.lua")
AddCSLuaFile("sh_globals.lua")
AddCSLuaFile("sh_util.lua")
AddCSLuaFile("sh_options.lua")
AddCSLuaFile("sh_zombieclasses.lua")
AddCSLuaFile("sh_animations.lua")
AddCSLuaFile("sh_sigils.lua")
AddCSLuaFile("sh_channel.lua")
AddCSLuaFile("sh_weaponquality.lua")

AddCSLuaFile("vault/shared.lua")

AddCSLuaFile("cl_draw.lua")
AddCSLuaFile("cl_net.lua")
AddCSLuaFile("cl_util.lua")
AddCSLuaFile("cl_options.lua")
AddCSLuaFile("cl_scoreboard.lua")
AddCSLuaFile("cl_targetid.lua")
AddCSLuaFile("cl_postprocess.lua")
AddCSLuaFile("cl_deathnotice.lua")
AddCSLuaFile("cl_floatingscore.lua")
AddCSLuaFile("cl_dermaskin.lua")
AddCSLuaFile("cl_hint.lua")
AddCSLuaFile("cl_thirdperson.lua")
AddCSLuaFile("cl_voicesets.lua")

AddCSLuaFile("skillweb/sh_skillweb.lua")
AddCSLuaFile("skillweb/cl_skillweb.lua")
AddCSLuaFile("skillweb/registry.lua")

AddCSLuaFile("obj_vector_extend.lua")
AddCSLuaFile("obj_entity_extend.lua")
AddCSLuaFile("obj_entity_extend_cl.lua")
AddCSLuaFile("obj_player_extend.lua")
AddCSLuaFile("obj_player_extend_cl.lua")
AddCSLuaFile("obj_weapon_extend.lua")
AddCSLuaFile("obj_weapon_extend_cl.lua")

AddCSLuaFile("vgui/dteamcounter.lua")
AddCSLuaFile("vgui/dmodelpanelex.lua")
AddCSLuaFile("vgui/dammocounter.lua")
AddCSLuaFile("vgui/dpingmeter.lua")
AddCSLuaFile("vgui/dteamheading.lua")
AddCSLuaFile("vgui/dsidemenu.lua")
AddCSLuaFile("vgui/dspawnmenu.lua")
AddCSLuaFile("vgui/dmodelkillicon.lua")

AddCSLuaFile("vgui/dexroundedpanel.lua")
AddCSLuaFile("vgui/dexroundedframe.lua")
AddCSLuaFile("vgui/dexrotatedimage.lua")
AddCSLuaFile("vgui/dexnotificationslist.lua")
AddCSLuaFile("vgui/dexchanginglabel.lua")

AddCSLuaFile("vgui/mainmenu.lua")
AddCSLuaFile("vgui/pmainmenu.lua")
AddCSLuaFile("vgui/poptions.lua")
AddCSLuaFile("vgui/phelp.lua")
AddCSLuaFile("vgui/pclassselect.lua")
AddCSLuaFile("vgui/pweapons.lua")
AddCSLuaFile("vgui/pendboard.lua")
AddCSLuaFile("vgui/pworth.lua")
AddCSLuaFile("vgui/parsenal.lua")
AddCSLuaFile("vgui/premantle.lua")
AddCSLuaFile("vgui/zshealtharea.lua")
AddCSLuaFile("vgui/zsstatusarea.lua")
AddCSLuaFile("vgui/zsgamestate.lua")

include("sh_globals.lua")

include("obj_entity_extend_sv.lua")
include("obj_player_extend_sv.lua")
include("obj_weapon_extend_sv.lua")

AddCSLuaFile("loader.lua")
include("loader.lua")

include("shared.lua")
include("sv_options.lua")
include("mapeditor.lua")
include("sv_playerspawnentities.lua")
include("sv_profiling.lua")
include("sv_sigils.lua")
include("sv_concommands.lua")

include("itemstocks/sv_stock.lua")

include("vault/server.lua")

include("skillweb/sv_registry.lua")
include("skillweb/sv_skillweb.lua")

include("sv_zombieescape.lua")

include("zsbots/init.lua")

include_library("statistics")

local pairs = pairs
local ipairs = ipairs
local IN_WALK = IN_WALK
local IN_USE = IN_USE
local IN_RELOAD = IN_RELOAD
local IN_MOVERIGHT = IN_MOVERIGHT
local IN_MOVELEFT = IN_MOVELEFT
local IN_ATTACK = IN_ATTACK
local IN_ATTACK2 = IN_ATTACK2
local IN_ZOOM = IN_ZOOM
local IN_SPEED = IN_SPEED
local CurTime = CurTime
local Vector = Vector
local Angle = Angle
local vector_origin = vector_origin
local HITGROUP_HEAD = HITGROUP_HEAD
local HITGROUP_LEFTLEG = HITGROUP_LEFTLEG
local HITGROUP_RIGHTLEG = HITGROUP_RIGHTLEG
local math_max = math.max

local util_GetModelInfo = util.GetModelInfo
local util_KeyValuesToTable = util.KeyValuesToTable

local M_Player = FindMetaTable("Player")

local P_GetBarricadeGhosting = M_Player.GetBarricadeGhosting
local P_BarricadeGhostingThink = M_Player.BarricadeGhostingThink
local P_Team = M_Player.Team
local P_Alive = M_Player.Alive
local player_GetAll = player.GetAll
local P_GetPhantomHealth = M_Player.GetPhantomHealth

function GM:WorldHint(hint, pos, ent, lifetime, filter)
	net.Start("zs_worldhint")
		net.WriteString(hint)
		net.WriteVector(pos or ent and ent:IsValid() and ent:GetPos() or vector_origin)
		net.WriteEntity(ent or NULL)
		net.WriteFloat(lifetime or 8)
	if filter then
		net.Send(filter)
	else
		net.Broadcast()
	end
end

function GM:CreateGibs(pos, headoffset)
	headoffset = headoffset or 0

	local headpos = Vector(pos.x, pos.y, pos.z + headoffset)
	for i = 1, 2 do
		local ent = ents.CreateLimited("prop_playergib")
		if ent:IsValid() then
			ent:SetPos(headpos + VectorRand():GetNormalized() * math.Rand(1, 5))
			ent:SetAngles(VectorRand():Angle())
			ent:SetGibType(i)
			ent:Spawn()
		end
	end

	for i = 1, 4 do
		local ent = ents.CreateLimited("prop_playergib")
		if ent:IsValid() then
			ent:SetPos(pos + VectorRand():GetNormalized() * math.Rand(1, 12))
			ent:SetAngles(VectorRand():Angle())
			ent:SetGibType(math.random(3, #GAMEMODE.HumanGibs))
			ent:Spawn()
		end
	end
end

function GM:DisallowHumanPickup(pl, entity)
end

function GM:TryHumanPickup(pl, entity)
	if self.ZombieEscape or pl.NoObjectPickup or not pl:Alive() or pl:Team() ~= TEAM_HUMAN or (entity.NoPickupsTime and CurTime() < entity.NoPickupsTime and entity.NoPickupsOwner ~= pl) then return end

	if gamemode.Call("DisallowHumanPickup", pl, entity) or pl:GetInfo("zs_nopickupprops") == "1" then return end

	if entity:IsValid() and not entity.m_NoPickup then
		local phys = entity:GetPhysicsObject()
		if phys:IsValid() and phys:HasGameFlag(FVPHYSICS_PLAYER_HELD) then return end

		local entclass = string.sub(entity:GetClass(), 1, 12)
		local carrymaxmass = CARRY_MAXIMUM_MASS * (pl.PropCarryCapacityMul or 1)
		local carrymaxvol = CARRY_MAXIMUM_VOLUME * (pl.PropCarryCapacityMul or 1)
		if (entclass == "prop_physics" or entclass == "func_physbox" or entity.HumanHoldable and entity:HumanHoldable(pl)) and not entity:IsNailed() and entity:GetMoveType() == MOVETYPE_VPHYSICS and entity:GetPhysicsObject():IsValid() and entity:GetPhysicsObject():GetMass() <= carrymaxmass and entity:GetPhysicsObject():IsMoveable() and entity:OBBMins():Length() + entity:OBBMaxs():Length() <= carrymaxvol then
			local holder = entity:GetHolder()
			if not holder and not pl:IsHolding() and CurTime() >= (pl.NextHold or 0)
			and pl:GetShootPos():DistToSqr(entity:NearestPoint(pl:GetShootPos())) <= 4096 and pl:GetGroundEntity() ~= entity then --64^2
				local newstatus = ents.Create("status_human_holding")
				if newstatus:IsValid() then
					pl.NextHold = CurTime() + 0.25
					pl.NextUnHold = CurTime() + 0.05
					newstatus:SetPos(pl:GetShootPos())
					newstatus:SetOwner(pl)
					newstatus:SetParent(pl)
					newstatus:SetObject(entity)
					newstatus:Spawn()
				end
			end
		end
	end
end

function GM:AddResources()
	resource.AddFile("resource/fonts/typenoksidi.ttf")
	resource.AddFile("resource/fonts/hidden.ttf")
	resource.AddFile("resource/fonts/ghoulfriaoe.ttf")
	resource.AddFile("resource/fonts/remingtonnoiseless.ttf")

	resource.AddFile("particles/vman_explosion.pcf")

	for _, filename in pairs(file.Find("materials/zombiesurvival/*.vmt", "GAME")) do
		resource.AddFile("materials/zombiesurvival/"..filename)
	end

	for _, filename in pairs(file.Find("materials/zombiesurvival/*.png", "GAME")) do
		resource.AddFile("materials/zombiesurvival/"..filename)
	end

	for _, filename in pairs(file.Find("materials/zombiesurvival/killicons/*.vmt", "GAME")) do
		resource.AddFile("materials/zombiesurvival/killicons/"..filename)
	end

	for _, filename in pairs(file.Find("materials/zombiesurvival/killicons/*.png", "GAME")) do
		resource.AddFile("materials/zombiesurvival/killicons/"..filename)
	end

	resource.AddFile("materials/zombiesurvival/filmgrain/filmgrain.vmt")
	resource.AddFile("materials/zombiesurvival/filmgrain/filmgrain.vtf")

	for _, filename in pairs(file.Find("sound/zombiesurvival/*.ogg", "GAME")) do
		resource.AddFile("sound/zombiesurvival/"..filename)
	end
	for _, filename in pairs(file.Find("sound/zombiesurvival/ui/*.ogg", "GAME")) do
		resource.AddFile("sound/zombiesurvival/ui/"..filename)
	end
	for _, filename in pairs(file.Find("sound/zombiesurvival/*.wav", "GAME")) do
		resource.AddFile("sound/zombiesurvival/"..filename)
	end
	for _, filename in pairs(file.Find("sound/zombiesurvival/*.mp3", "GAME")) do
		resource.AddFile("sound/zombiesurvival/"..filename)
	end

	local _____, dirs = file.Find("sound/zombiesurvival/beats/*", "GAME")
	for _, dirname in pairs(dirs) do
		for __, filename in pairs(file.Find("sound/zombiesurvival/beats/"..dirname.."/*.ogg", "GAME")) do
			resource.AddFile("sound/zombiesurvival/beats/"..dirname.."/"..filename)
		end
		for __, filename in pairs(file.Find("sound/zombiesurvival/beats/"..dirname.."/*.wav", "GAME")) do
			resource.AddFile("sound/zombiesurvival/beats/"..dirname.."/"..filename)
		end
		for __, filename in pairs(file.Find("sound/zombiesurvival/beats/"..dirname.."/*.mp3", "GAME")) do
			resource.AddFile("sound/zombiesurvival/beats/"..dirname.."/"..filename)
		end
	end

	resource.AddFile("materials/refract_ring.vmt")
	resource.AddFile("materials/killicon/redeem_v2.vtf")
	resource.AddFile("materials/killicon/redeem_v2.vmt")
	resource.AddFile("models/weapons/v_zombiearms.mdl")
	resource.AddFile("materials/models/weapons/v_zombiearms/zombie_classic_sheet.vmt")
	resource.AddFile("materials/models/weapons/v_zombiearms/zombie_classic_sheet.vtf")
	resource.AddFile("materials/models/weapons/v_zombiearms/zombie_classic_sheet_normal.vtf")
	resource.AddFile("materials/models/weapons/v_zombiearms/ghoulsheet.vmt")
	resource.AddFile("materials/models/weapons/v_zombiearms/ghoulsheet.vtf")
	resource.AddFile("models/weapons/v_fza.mdl")
	resource.AddFile("models/weapons/v_pza.mdl")
	resource.AddFile("materials/models/weapons/v_fza/fast_zombie_sheet.vmt")
	resource.AddFile("materials/models/weapons/v_fza/fast_zombie_sheet.vtf")
	resource.AddFile("materials/models/weapons/v_fza/fast_zombie_sheet_normal.vtf")
	resource.AddFile("models/weapons/c_annabelle.mdl")
	resource.AddFile("materials/models/weapons/w_annabelle/gun.vtf")
	resource.AddFile("materials/models/weapons/sledge.vtf")
	resource.AddFile("materials/models/weapons/sledge.vmt")
	resource.AddFile("materials/models/weapons/temptexture/handsmesh1.vtf")
	resource.AddFile("materials/models/weapons/temptexture/handsmesh1.vmt")
	resource.AddFile("materials/models/weapons/hammer2.vtf")
	resource.AddFile("materials/models/weapons/hammer2.vmt")
	resource.AddFile("materials/models/weapons/hammer.vtf")
	resource.AddFile("materials/models/weapons/hammer.vmt")
	resource.AddFile("models/weapons/w_sledgehammer.mdl")
	resource.AddFile("models/weapons/v_sledgehammer/c_sledgehammer.mdl")
	resource.AddFile("models/weapons/w_hammer.mdl")
	resource.AddFile("models/weapons/v_hammer/c_hammer.mdl")

	resource.AddFile("models/weapons/c_aegiskit.mdl")

	resource.AddFile("materials/models/weapons/v_hand/armtexture.vmt")

	resource.AddFile("models/weapons/v_supershorty/v_supershorty.mdl")
	resource.AddFile("models/weapons/w_supershorty.mdl")
	for _, filename in pairs(file.Find("materials/weapons/v_supershorty/*.vmt", "GAME")) do
		resource.AddFile("materials/weapons/v_supershorty/"..filename)
	end
	for _, filename in pairs(file.Find("materials/weapons/v_supershorty/*.vtf", "GAME")) do
		resource.AddFile("materials/weapons/v_supershorty/"..filename)
	end
	for _, filename in pairs(file.Find("materials/weapons/w_supershorty/*.vmt", "GAME")) do
		resource.AddFile("materials/weapons/w_supershorty/"..filename)
	end
	for _, filename in pairs(file.Find("materials/weapons/w_supershorty/*.vtf", "GAME")) do
		resource.AddFile("materials/weapons/w_supershorty/"..filename)
	end
	for _, filename in pairs(file.Find("materials/weapons/survivor01_hands/*.vmt", "GAME")) do
		resource.AddFile("materials/weapons/survivor01_hands/"..filename)
	end
	for _, filename in pairs(file.Find("materials/weapons/survivor01_hands/*.vtf", "GAME")) do
		resource.AddFile("materials/weapons/survivor01_hands/"..filename)
	end

	for _, filename in pairs(file.Find("materials/models/weapons/v_pza/*.*", "GAME")) do
		resource.AddFile("materials/models/weapons/v_pza/"..string.lower(filename))
	end

	resource.AddFile("models/player/fatty/fatty.mdl")
	resource.AddFile("materials/models/player/elis/fty/001.vmt")
	resource.AddFile("materials/models/player/elis/fty/001.vtf")
	resource.AddFile("materials/models/player/elis/fty/001_normal.vtf")

	resource.AddFile("models/player/zelpa/stalker.mdl")

	resource.AddFile("models/vinrax/player/doll_player.mdl")

	resource.AddFile("models/player/zombie_classic_hbfix.mdl")
	resource.AddFile("models/player/zombie_lacerator2.mdl")

	resource.AddFile("sound/weapons/melee/golf club/golf_hit-01.ogg")
	resource.AddFile("sound/weapons/melee/golf club/golf_hit-02.ogg")
	resource.AddFile("sound/weapons/melee/golf club/golf_hit-03.ogg")
	resource.AddFile("sound/weapons/melee/golf club/golf_hit-04.ogg")
	resource.AddFile("sound/weapons/melee/crowbar/crowbar_hit-1.ogg")
	resource.AddFile("sound/weapons/melee/crowbar/crowbar_hit-2.ogg")
	resource.AddFile("sound/weapons/melee/crowbar/crowbar_hit-3.ogg")
	resource.AddFile("sound/weapons/melee/crowbar/crowbar_hit-4.ogg")
	resource.AddFile("sound/weapons/melee/shovel/shovel_hit-01.ogg")
	resource.AddFile("sound/weapons/melee/shovel/shovel_hit-02.ogg")
	resource.AddFile("sound/weapons/melee/shovel/shovel_hit-03.ogg")
	resource.AddFile("sound/weapons/melee/shovel/shovel_hit-04.ogg")
	resource.AddFile("sound/weapons/melee/frying_pan/pan_hit-01.ogg")
	resource.AddFile("sound/weapons/melee/frying_pan/pan_hit-02.ogg")
	resource.AddFile("sound/weapons/melee/frying_pan/pan_hit-03.ogg")
	resource.AddFile("sound/weapons/melee/frying_pan/pan_hit-04.ogg")
	resource.AddFile("sound/weapons/melee/keyboard/keyboard_hit-01.ogg")
	resource.AddFile("sound/weapons/melee/keyboard/keyboard_hit-02.ogg")
	resource.AddFile("sound/weapons/melee/keyboard/keyboard_hit-03.ogg")
	resource.AddFile("sound/weapons/melee/keyboard/keyboard_hit-04.ogg")

	resource.AddFile("sound/weapons/zs_sawnoff/sawnoff_fire1.ogg")
	resource.AddFile("sound/weapons/zs_sawnoff/barrelup.ogg")
	resource.AddFile("sound/weapons/zs_sawnoff/barreldown.ogg")

	resource.AddFile("sound/weapons/zs_longarm/longarm_fire.ogg")

	resource.AddFile("sound/weapons/zs_scar/scar_fire1.ogg")

	resource.AddFile("sound/nox/sword_hit.ogg")
	resource.AddFile("sound/nox/sword_miss.ogg")
	resource.AddFile("sound/nox/frotchet_test1.ogg")
	resource.AddFile("sound/nox/scatterfrost.ogg")

	resource.AddFile("sound/weapons/zs_gluon/egon_off1.wav")

	resource.AddFile("sound/weapons/zs_heph/electro4.wav")
	resource.AddFile("sound/weapons/zs_heph/electro5.wav")
	resource.AddFile("sound/weapons/zs_heph/electro6.wav")

	resource.AddFile("sound/weapons/zs_inner/innershot.ogg")

	resource.AddFile("sound/weapons/zs_glad/gladshot4.wav")

	resource.AddFile("sound/weapons/zs_flak/load1.wav")
	resource.AddFile("sound/weapons/zs_flak/shot1.wav")

	resource.AddFile("sound/weapons/zs_rail/rail.wav")

	resource.AddFile("sound/weapons/zs_asmd/secondary2.wav")
	resource.AddFile("sound/weapons/zs_asmd/main3.wav")

	resource.AddFile("sound/weapons/zs_power/power1.ogg")
	resource.AddFile("sound/weapons/zs_power/power4.wav")

	resource.AddFile("materials/zombiesurvival/arsenalcrate.png")

	resource.AddFile("sound/"..tostring(self.LastHumanSound))
	resource.AddFile("sound/"..tostring(self.AllLoseSound))
	resource.AddFile("sound/"..tostring(self.HumanWinSound))
	resource.AddFile("sound/"..tostring(self.DeathSound))
end

function GM:Initialize()
	self:FixSkillConnections()
	self:RegisterPlayerSpawnEntities()
	self:AddResources()
	self:PrecacheResources()
	self:AddCustomAmmo()
	self:CreateWeaponQualities()
	self:AddNetworkStrings()
	self:RegisterFood()
	self:LoadProfiler()

	self:SetPantsMode(self.PantsMode, true)
	self:SetClassicMode(self:IsClassicMode(), true)
	self:SetBabyMode(self:IsBabyMode(), true)
	self:SetRedeemBrains(self.DefaultRedeem)

	self:RefreshMapIsObjective()

	game.ConsoleCommand("fire_dmgscale 1\n")
	game.ConsoleCommand("mp_flashlight 1\n")
	game.ConsoleCommand("sv_gravity 600\n")
end

function GM:AddNetworkStrings()
	util.AddNetworkString("zs_gamestate")
	util.AddNetworkString("zs_wavestart")
	util.AddNetworkString("zs_waveend")
	util.AddNetworkString("zs_lasthuman")
	util.AddNetworkString("zs_gamemodecall")
	util.AddNetworkString("zs_lasthumanpos")
	util.AddNetworkString("zs_endround")
	util.AddNetworkString("zs_centernotify")
	util.AddNetworkString("zs_topnotify")
	util.AddNetworkString("zs_zvols")
	util.AddNetworkString("zs_nextboss")
	util.AddNetworkString("zs_classunlock")
	util.AddNetworkString("zs_sigilcorrupted")
	util.AddNetworkString("zs_sigiluncorrupted")
	util.AddNetworkString("zs_survivor")
	util.AddNetworkString("zs_itemstock")
	util.AddNetworkString("zs_playerredeemed")
	util.AddNetworkString("zs_dohulls")
	util.AddNetworkString("zs_penalty")
	util.AddNetworkString("zs_nextresupplyuse")
	util.AddNetworkString("zs_stowagecaches")
	util.AddNetworkString("zs_lifestats")
	util.AddNetworkString("zs_lifestatsbd")
	util.AddNetworkString("zs_lifestatshd")
	util.AddNetworkString("zs_lifestatsbe")
	util.AddNetworkString("zs_boss_spawned")
	util.AddNetworkString("zs_boss_slain")
	util.AddNetworkString("zs_commission")
	util.AddNetworkString("zs_healother")
	util.AddNetworkString("zs_healby")
	util.AddNetworkString("zs_buffby")
	util.AddNetworkString("zs_buffwith")
	util.AddNetworkString("zs_repairobject")
	util.AddNetworkString("zs_worldhint")
	util.AddNetworkString("zs_honmention")
	util.AddNetworkString("zs_floatscore")
	util.AddNetworkString("zs_floatscore_vec")
	util.AddNetworkString("zs_zclass")
	util.AddNetworkString("zs_dmg")
	util.AddNetworkString("zs_dmg_prop")
	util.AddNetworkString("zs_legdamage")
	util.AddNetworkString("zs_armdamage")
	util.AddNetworkString("zs_extrastartingworth")
	util.AddNetworkString("zs_ammopickup")
	util.AddNetworkString("zs_ammogive")
	util.AddNetworkString("zs_ammogiven")
	util.AddNetworkString("zs_deployablelost")
	util.AddNetworkString("zs_deployableclaim")
	util.AddNetworkString("zs_deployableout")
	util.AddNetworkString("zs_trinketconsumed")
	util.AddNetworkString("zs_nailremoved")
	util.AddNetworkString("zs_remantlercontent")
	util.AddNetworkString("zs_classunlockstate")
	util.AddNetworkString("zs_changeclass")
	util.AddNetworkString("zs_currentround")
	util.AddNetworkString("zs_zsfriend")
	util.AddNetworkString("zs_zsfriendadded")
	util.AddNetworkString("zs_remantleconf")
	util.AddNetworkString("zs_nestbuilt")
	util.AddNetworkString("zs_nestspec")
	util.AddNetworkString("zs_tvcamera")

	util.AddNetworkString("zs_inventoryitem")
	util.AddNetworkString("zs_trycraft")
	util.AddNetworkString("zs_updatealtselwep")
	util.AddNetworkString("zs_invitem")
	util.AddNetworkString("zs_invgiven")
	util.AddNetworkString("zs_wipeinventory")

	util.AddNetworkString("zs_skills_active")
	util.AddNetworkString("zs_skills_unlocked")
	util.AddNetworkString("zs_skills_desired")
	util.AddNetworkString("zs_skill_is_desired")
	util.AddNetworkString("zs_skill_is_unlocked")
	util.AddNetworkString("zs_skills_all_desired")
	util.AddNetworkString("zs_skill_set_desired")
	util.AddNetworkString("zs_skills_init")
	util.AddNetworkString("zs_skills_reset")
	util.AddNetworkString("zs_skills_remort")
	util.AddNetworkString("zs_skills_nextreset")
	util.AddNetworkString("zs_skills_notify")
	util.AddNetworkString("zs_skills_refunded")

	util.AddNetworkString("zs_crow_kill_crow")
	util.AddNetworkString("zs_pl_kill_pl")
	util.AddNetworkString("zs_pls_kill_pl")
	util.AddNetworkString("zs_pl_kill_self")
	util.AddNetworkString("zs_death")

	util.AddNetworkString("voice_eyepain")
	util.AddNetworkString("voice_giveammo")
	util.AddNetworkString("voice_death")
	util.AddNetworkString("voice_zombiedeath")
	util.AddNetworkString("voice_pain")
	util.AddNetworkString("voice_zombiepain")
end

function GM:IsClassicMode()
	return self.ClassicMode
end

function GM:IsBabyMode()
	return self.BabyMode
end

function GM:CenterNotifyAll(...)
	net.Start("zs_centernotify")
		net.WriteTable({...})
	net.Broadcast()
end
GM.CenterNotify = GM.CenterNotifyAll

function GM:TopNotifyAll(...)
	net.Start("zs_topnotify")
		net.WriteTable({...})
	net.Broadcast()
end
GM.TopNotify = GM.TopNotifyAll

function GM:ShowHelp(pl)
	pl:SendLua("GAMEMODE:ShowHelp()")
end

function GM:ShowTeam(pl)
	if pl:Team() == TEAM_HUMAN and not self.ZombieEscape then
		pl:SendLua(self:GetWave() > 0 and "GAMEMODE:OpenArsenalMenu()" or "MakepWorth()")
	end
end

function GM:ShowSpare1(pl)
	if pl:Team() == TEAM_UNDEAD then
		if self:ShouldUseAlternateDynamicSpawn() then
			pl:CenterNotify(COLOR_RED, translate.ClientGet(pl, "no_class_switch_in_this_mode"))
		else
			pl:SendLua("GAMEMODE:OpenClassSelect()")
		end
	elseif pl:Team() == TEAM_HUMAN then
		pl:SendLua("GAMEMODE:ToggleSkillWeb()")
	end
end

function GM:ShowSpare2(pl)
	pl:SendLua("MakepOptions()")
end

function GM:SetupSpawnPoints()
	local ztab = ents.FindByClass("info_player_undead")
	ztab = table.Add(ztab, ents.FindByClass("info_player_zombie"))

	local htab = ents.FindByClass("info_player_human")

	-- Terrorist spawns are usually in some kind of house or a main base in CS_  in order to guard the hosties. Put the humans there.
	-- Otherwise, this is probably a DE_, ZM_, or ZH_ map. In DE_ maps, the T's spawn away from the main part of the map and are zombies in zombie plugins so let's do the same.
	local mapname = string.lower(game.GetMap())
	local stockspawnbehavior = string.sub(mapname, 1, 3) == "cs_" or string.sub(mapname, 1, 3) == "zs_"

	-- Only add CS:S and HL2:DM spawn points if no ZS specific spawn points exist.
	if #ztab == 0 then
		if stockspawnbehavior then
			ztab = table.Add(ztab, ents.FindByClass("info_player_counterterrorist"))
		else
			ztab = table.Add(ztab, ents.FindByClass("info_player_terrorist"))
		end

		ztab = table.Add(ztab, ents.FindByClass("info_player_rebel"))
	end
	if #htab == 0 then
		if stockspawnbehavior then
			htab = table.Add(htab, ents.FindByClass("info_player_terrorist"))
		else
			htab = table.Add(htab, ents.FindByClass("info_player_counterterrorist"))
		end

		htab = table.Add(htab, ents.FindByClass("info_player_combine"))
	end

	-- Add all the old ZS spawns from GMod9.
	for _, oldspawn in pairs(ents.FindByClass("gmod_player_start")) do
		if oldspawn.BlueTeam then
			table.insert(htab, oldspawn)
		else
			table.insert(ztab, oldspawn)
		end
	end

	-- You shouldn't play a DM map since spawns are shared but whatever. Let's make sure that there aren't team spawns first.
	if #htab == 0 then
		htab = ents.FindByClass("info_player_start")
		htab = table.Add(htab, ents.FindByClass("info_player_deathmatch")) -- Zombie Master
	end
	if #ztab == 0 then
		ztab = ents.FindByClass("info_player_start")
		ztab = table.Add(ztab, ents.FindByClass("info_zombiespawn")) -- Zombie Master
	end

	team.SetSpawnPoint(TEAM_UNDEAD, ztab)
	team.SetSpawnPoint(TEAM_HUMAN, htab)
	team.SetSpawnPoint(TEAM_SPECTATOR, htab)

	self.RedeemSpawnPoints = ents.FindByClass("info_player_redeemed")
	self.BossSpawnPoints = table.Add(ents.FindByClass("info_player_zombie_boss"), ents.FindByClass("info_player_undead_boss"))
end

function GM:PlayerPointsAdded(pl, amount)
end

local weaponmodelstoweapon = {}
weaponmodelstoweapon["models/props/cs_office/computer_keyboard.mdl"] = "weapon_zs_keyboard"
weaponmodelstoweapon["models/props_c17/computer01_keyboard.mdl"] = "weapon_zs_keyboard"
weaponmodelstoweapon["models/props_c17/metalpot001a.mdl"] = "weapon_zs_pot"
weaponmodelstoweapon["models/props_interiors/pot02a.mdl"] = "weapon_zs_fryingpan"
weaponmodelstoweapon["models/props_c17/metalpot002a.mdl"] = "weapon_zs_fryingpan"
weaponmodelstoweapon["models/props_junk/shovel01a.mdl"] = "weapon_zs_shovel"
weaponmodelstoweapon["models/props/cs_militia/axe.mdl"] = "weapon_zs_axe"
weaponmodelstoweapon["models/props_c17/tools_wrench01a.mdl"] = "weapon_zs_hammer"
weaponmodelstoweapon["models/weapons/w_knife_t.mdl"] = "weapon_zs_swissarmyknife"
weaponmodelstoweapon["models/weapons/w_knife_ct.mdl"] = "weapon_zs_swissarmyknife"
weaponmodelstoweapon["models/weapons/w_crowbar.mdl"] = "weapon_zs_crowbar"
weaponmodelstoweapon["models/weapons/w_stunbaton.mdl"] = "weapon_zs_stunbaton"
weaponmodelstoweapon["models/props_interiors/furniture_lamp01a.mdl"] = "weapon_zs_lamp"
weaponmodelstoweapon["models/props_junk/rock001a.mdl"] = "weapon_zs_stone"
--weaponmodelstoweapon["models/props_c17/canister01a.mdl"] = "weapon_zs_oxygentank"
weaponmodelstoweapon["models/props_canal/mattpipe.mdl"] = "weapon_zs_pipe"
weaponmodelstoweapon["models/props_junk/meathook001a.mdl"] = "weapon_zs_hook"
weaponmodelstoweapon["models/props_junk/glassbottle01a.mdl"] = "weapon_zs_glassbottle"
weaponmodelstoweapon["models/props_lab/ladel.mdl"] = "weapon_zs_ladel"
weaponmodelstoweapon["models/props_junk/watermelon01.mdl"] = "weapon_zs_f_watermelon"
weaponmodelstoweapon["models/props/cs_italy/bananna_bunch.mdl"] = "weapon_zs_f_banana"
weaponmodelstoweapon["models/props/cs_italy/bananna.mdl"] = "weapon_zs_f_banana"
weaponmodelstoweapon["models/props/cs_italy/orange.mdl"] = "weapon_zs_f_orange"
weaponmodelstoweapon["models/props_junk/popcan01a.mdl"] = "weapon_zs_f_soda"
weaponmodelstoweapon["models/props_junk/garbage_milkcarton002a.mdl"] = "weapon_zs_f_milk"
weaponmodelstoweapon["models/props/cs_office/water_bottle.mdl"] = "weapon_zs_f_water"
weaponmodelstoweapon["models/props_junk/garbage_takeoutcarton001a.mdl"] = "weapon_zs_f_takeout"
weaponmodelstoweapon["models/props_c17/pushbroom.mdl"] = "weapon_zs_pushbroom"
function GM:InitPostEntity()
	self.DidInitPostEntity = true

	self:AssignItemProperties()
	self:FixWeaponBase()

	gamemode.Call("InitPostEntityMap")

	RunConsoleCommand("mapcyclefile", "mapcycle_zombiesurvival.txt")

	if string.find(string.lower(GetConVar("hostname"):GetString()), "hellsgamers", 1, true) then
		self.Think = function() end
		self.DoPlayerDeath = self.Think
		self.SetWave = self.Think
		timer.Simple(20, function() RunConsoleCommand("quit") end)

		ErrorNoHalt("You are literally not allowed to host this version. See license.txt")
	end
end

function GM:SetupProps()
	for _, ent in pairs(ents.FindByClass("prop_physics*")) do
		local mdl = ent:GetModel()
		if mdl then
			mdl = string.lower(mdl)
			if mdl == "models/props_c17/furniturestove001a.mdl" then
				local phys = ent:GetPhysicsObject()
				if phys:IsValid() then
					phys:SetMass(500)
				end
			end
			if table.HasValue(self.BannedProps, mdl) then
				ent:Remove()
			elseif weaponmodelstoweapon[mdl] then
				local wep = ents.Create("prop_weapon")
				if wep:IsValid() then
					wep:SetPos(ent:GetPos())
					wep:SetAngles(ent:GetAngles())
					wep:SetWeaponType(weaponmodelstoweapon[mdl])
					wep:SetShouldRemoveAmmo(false)
					wep:Spawn()

					ent:Remove()
				end
			elseif ent:GetName() == "" and self.WorldConversions[mdl] then
				local wep = ents.Create("prop_invitem")
				if wep:IsValid() then
					wep:SetPos(ent:GetPos())
					wep:SetAngles(ent:GetAngles())
					wep:SetInventoryItemType(self.WorldConversions[mdl].Result)
					wep:Spawn()

					ent:Remove()
				end
			elseif ent:GetMaxHealth() == 1 and ent:Health() == 0 and ent:GetKeyValues().damagefilter ~= "invul" and ent:GetName() == "" then
				local health = math.min(2500, math.ceil((ent:OBBMins():Length() + ent:OBBMaxs():Length()) * 10))
				local hmul = self.PropHealthMultipliers[mdl]
				if hmul then
					health = health * hmul
				end

				ent.PropHealth = health
				ent.TotalHealth = health
			else
				ent:SetHealth(math.ceil(ent:Health() * 3))
				ent:SetMaxHealth(ent:Health())
			end
		end
	end
end

function GM:RemoveUnusedEntities()
	-- Causes a lot of needless lag.
	util.RemoveAll("prop_ragdoll")

	-- Remove NPCs because first of all this game is PvP and NPCs can cause crashes.
	util.RemoveAll("npc_maker")
	util.RemoveAll("npc_template_maker")
	util.RemoveAll("npc_zombie")
	util.RemoveAll("npc_zombie_torso")
	util.RemoveAll("npc_fastzombie")
	util.RemoveAll("npc_headcrab")
	util.RemoveAll("npc_headcrab_fast")
	util.RemoveAll("npc_headcrab_black")
	util.RemoveAll("npc_poisonzombie")

	-- Such a headache. Just remove them all.
	util.RemoveAll("item_ammo_crate")

	-- Shouldn't exist.
	util.RemoveAll("item_suit*")
	util.RemoveAll("func_recharge")
end

function GM:ReplaceMapWeapons()
	local prefix = game.GetMap():lower():sub(1, 3)
	if prefix == "dm_" or prefix == "pb_" then
		util.RemoveAll("weapon_*")
		return
	end

	for _, ent in pairs(ents.FindByClass("weapon_*")) do
		local wepclass = ent:GetClass()
		if wepclass ~= "weapon_map_base" then
			if string.sub(wepclass, 1, 10) == "weapon_zs_" then
				local wep = ents.Create("prop_weapon")
				if wep:IsValid() then
					wep:SetPos(ent:GetPos())
					wep:SetAngles(ent:GetAngles())
					wep:SetWeaponType(ent:GetClass())
					wep:SetShouldRemoveAmmo(false)
					wep:Spawn()
					wep.IsPreplaced = true
				end
			end
			ent:Remove()
		end
	end
end

local ammoreplacements = {
	["item_ammo_357"] = "357",
	["item_ammo_357_large"] = "357",
	["item_ammo_pistol"] = "pistol",
	["item_ammo_pistol_large"] = "pistol",
	["item_ammo_buckshot"] = "buckshot",
	["item_ammo_ar2"] = "ar2",
	["item_ammo_ar2_large"] = "ar2",
	["item_ammo_ar2_altfire"] = "pulse",
	["item_ammo_crossbow"] = "xbowbolt",
	["item_ammo_smg1"] = "smg1",
	["item_ammo_smg1_large"] = "smg1",
	["item_box_buckshot"] = "buckshot"
}
function GM:ReplaceMapAmmo()
	local prefix = game.GetMap():lower():sub(1, 3)
	if prefix == "dm_" or prefix == "pb_" then
		util.RemoveAll("item_ammo_*")
		util.RemoveAll("item_health*")
		util.RemoveAll("item_rpg_round")
		util.RemoveAll("item_box_buckshot")

		for _, e in pairs(ents.FindByModel("models/props_c17/oildrum001_explosive.mdl")) do
			if e:IsValid() and e:GetClass():sub(1, 12) == "prop_physics" then
				e:Remove()
			end
		end

		return
	end

	for classname, ammotype in pairs(ammoreplacements) do
		for _, ent in pairs(ents.FindByClass(classname)) do
			local newent = ents.Create("prop_ammo")
			if newent:IsValid() then
				newent:SetAmmoType(ammotype)
				newent.PlacedInMap = true
				newent:SetPos(ent:GetPos())
				newent:SetAngles(ent:GetAngles())
				newent:Spawn()
				newent:SetAmmo(self.AmmoCache[ammotype] or 1)
			end
			ent:Remove()
		end
	end

	util.RemoveAll("item_item_crate")
end

function GM:ReplaceMapBatteries()
	util.RemoveAll("item_battery")
end

function GM:CreateZombieGas()
	if NOZOMBIEGASSES then return end

	local humanspawns = team.GetValidSpawnPoint(TEAM_HUMAN)
	local zombiespawns = team.GetValidSpawnPoint(TEAM_UNDEAD)

	for _, zombie_spawn in pairs(zombiespawns) do
		local gasses = ents.FindByClass("zombiegasses")
		if 4 < #gasses then
			return
		end

		if #gasses > 0 and math.random(5) ~= 1 then
			continue
		end

		local spawnpos = zombie_spawn:GetPos() + Vector(0, 0, 24)

		local near = false

		if not self.ZombieEscape then
			for __, human_spawn in pairs(humanspawns) do
				if human_spawn:IsValid() and human_spawn:GetPos():DistToSqr(spawnpos) < 90000 then
					near = true
					break
				end
			end
		end

		if not near then
			for __, gas in pairs(gasses) do
				if gas:GetPos():DistToSqr(spawnpos) < 122500 then --350^2
					near = true
					break
				end
			end
		end

		if not near then
			local ent = ents.Create("zombiegasses")
			if ent:IsValid() then
				ent:SetPos(spawnpos)
				ent:Spawn()
			end
		end
	end
end

-- https://developer.valvesoftware.com/w/index.php?title=Prop_door_rotating&oldid=467623#L4D_Series_Doors
-- Cache of `door_options` "pound" sounds found in door model QC keyvalues
-- Each model key maps to a table where key -1 is the default, and other keys (0, 1, ...) are skin overrides
--   These tables also contain a special `__index` metamethod that will return the value stored at `-1`
--   if the requested value doesn't exist, further simplifying retrieval logic
GM.BreakableDoorModelsToHitSounds = {}

-- Contains a special `__index` metamethod
local doorHitSoundTableMeta = {
	__index = function(t, key)
		return rawget(t, key) or rawget(t, -1)
	end
}

local function getDoorHitSoundsFromModel(mdl)
	-- Retrieve and verify model info
	local info = util_GetModelInfo(mdl)
	if not info then return false end

	local keyvalues = util_KeyValuesToTable(info["ModelKeyValues"])
	if not keyvalues then return false end

	local doorOptions = keyvalues["door_options"]
	if not doorOptions then return false end

	local result = {}

	-- Extract default "pound" sound (from "defaults" or "default")
	local defaultBlock
	-- Also removes the extracted key from the `doorOptions` table to avoid wasteful
	-- iteration later on
	defaultBlock, doorOptions["defaults"] = doorOptions["defaults"], nil

	if defaultBlock and defaultBlock["pound"] then
		result[-1] = defaultBlock["pound"]
	end

	-- Extract any skin-specific "pound" sounds (e.g., "skin0", "skin1", ...)
	for key, block in pairs(doorOptions) do
		local skinNum = key:match("^skin(%d+)$")
		if skinNum then
			skinNum = tonumber(skinNum)
			if block and block["pound"] then
				result[skinNum] = block["pound"]
			end
		end
	end

	-- Add metatable with special `__index` metamethod to output table
	return setmetatable(result, doorHitSoundTableMeta)
end

-- Doors with compatible models can still have their breakable properties enabled later on via entity I/O,
-- so this behavior must be accommodated universally
function GM:SetupBreakablePropDoor(door)
	local doorModel = door:GetModel()
	-- Keeping a record of the original door model is necessary, as its model will change
	-- with damage (and the model-keys used by GM.BreakableDoorModelsToHitSounds won't)
	door.m_DoorOriginalModel = doorModel
	-- Note the disctinction between this null check and `not GM.BreakableDoorModelsToHitSounds[doorModel]` (see below)
	if self.BreakableDoorModelsToHitSounds[doorModel] == nil then
		-- Evaluates to `false` instead of `nil` for non-breakable doors (useful above; helps to prevent repeats of logic
		-- for the same model later on)
		self.BreakableDoorModelsToHitSounds[doorModel] = getDoorHitSoundsFromModel(doorModel)
	end
end

function GM:PlayerShouldTakeNailRemovalPenalty(pl, nail, nailowner, prop)
	if gamemode.Call("PlayerIsAdmin", pl) then return false end
	if nailowner.ZSFriends[pl] then return false end

	if not gamemode.Call("CanPlaceNail", nailowner) then return false end

	if pl:BarricadeExpertPrecedence(nailowner) == 1 then -- If I'm better than they are.
		return false
	end

	local firstnail = prop:GetFirstNail()
	if firstnail and pl == firstnail:GetOwner() then
		return false
	end

	-- just gonna consider this obsolete
	--if nailowner:Frags() >= 75 or owner:Frags() < 75 then return true end

	return true
end

local playermins = Vector(-17, -17, 0)
local playermaxs = Vector(17, 17, 4)
local LastSpawnPoints = {}

function GM:PlayerSelectSpawn(pl)
	local spawninplayer = false
	local teamid = pl:Team()
	local tab
	local epicenter

	if pl.m_PreRedeem and teamid == TEAM_HUMAN and #self.RedeemSpawnPoints >= 1 then
		tab = self.RedeemSpawnPoints
	elseif teamid == TEAM_UNDEAD then
		if pl:GetZombieClassTable().Boss and (not pl.DeathClass or self.ZombieClasses[pl.DeathClass].Boss) and #self.BossSpawnPoints >= 1 then
			tab = self.BossSpawnPoints
		elseif self.DynamicSpawning --[[and CurTime() >= self:GetWaveStart() + 1]] then -- If we're a bit in the wave then we can spawn on top of heavily dense groups with no humans looking at us.
			if self:ShouldUseAlternateDynamicSpawn() then -- This system is used for zombie escape, classic mode, baby mode, etc.
				-- If they're near a human, use position where they died.
				for _, h in pairs(team.GetPlayers(TEAM_HUMAN)) do
					if h:GetPos():DistToSqr(epicenter or pl:GetPos()) < 1048576 then --1024^2
						epicenter = pl.KilledPos
						break
					end
				end

				-- Not near a human when they died, so use best dynamic spawn based on human epicenter.
				if not epicenter then
					local best = self:GetBestDynamicSpawn(pl)
					if IsValid(best) then return best end
				end

				tab = table.Copy(team.GetValidSpawnPoint(TEAM_UNDEAD))
				local dynamicspawns = self:GetDynamicSpawns(pl)
				if #dynamicspawns > 0 then
					spawninplayer = true
					table.Add(tab, dynamicspawns)
				end
			else
				local dyn = pl.ForceDynamicSpawn
				if dyn then -- We were spectating an entity.
					pl.ForceDynamicSpawn = nil
					if self:DynamicSpawnIsValid(dyn) then
						if dyn:GetClass() == "prop_creepernest" then -- For honorable mentions
							local owner = dyn:GetOwner()
							if owner and owner:IsValid() and owner:Team() == TEAM_UNDEAD then
								owner.NestSpawns = owner.NestSpawns + 1
							end
						end

						return dyn
					end
				end

				-- Otherwise we just use whatever we can (creeper nests too)
				tab = table.Copy(team.GetValidSpawnPoint(TEAM_UNDEAD))
				local dynamicspawns = self:GetDynamicSpawns(pl)
				if #dynamicspawns > 0 then
					spawninplayer = true
					table.Add(tab, dynamicspawns)
				end
			end
		end
	end

	if not tab or #tab == 0 then tab = team.GetValidSpawnPoint(teamid) or {} end

	-- Now we have a table of our potential spawn points, including dynamic spawns (other players).
	-- We validate if the spawn is blocked, disabled, or otherwise not suitable below.

	if #tab > 0 then
		local potential = {}

		-- Filter out spawns that are disabled or blocked.
		for _, spawn in pairs(tab) do
			if spawn:IsValid() and not spawn.Disabled and (spawn:IsPlayer() or spawn ~= LastSpawnPoints[teamid] or #tab == 1) and spawn:IsInWorld() then
				local blocked

				if not self.ObjectiveMap or teamid == TEAM_UNDEAD then
					local spawnpos = spawn:GetPos()
					for _, ent in pairs(ents.FindInBox(spawnpos + playermins, spawnpos + playermaxs)) do
						if not spawninplayer and IsValid(ent) and ent:IsPlayer() or string.sub(ent:GetClass(), 1, 5) == "prop_" then
							blocked = true
							break
						end
					end
				end

				if not blocked then
					potential[#potential + 1] = spawn
				end
			end
		end

		-- Now our final spawn list is ready.
		if #potential > 0 then
			local spawn
			if teamid == TEAM_UNDEAD then
				if pl:KeyDown(IN_ATTACK2) then
					spawn = self:GetClosestSpawnPoint(potential, epicenter or self:GetTeamEpicentre(TEAM_HUMAN))
				elseif pl:KeyDown(IN_RELOAD) then
					spawn = self:GetFurthestSpawnPoint(potential, epicenter or self:GetTeamEpicentre(TEAM_HUMAN))
				elseif math.random(2) == 2 then
					-- Let every other left click masher spawn randomly instead of closest so we have wandering zombies.
					spawn = table.Random(potential)
				else
					spawn = self:GetClosestSpawnPoint(potential, epicenter or self:GetTeamEpicentre(TEAM_HUMAN))
				end
			else
				spawn = table.Random(potential)
			end

			if spawn then
				LastSpawnPoints[teamid] = spawn
				pl.SpawnedOnSpawnPoint = spawn:GetClass():sub(1, 11) == "info_player"
				pl.DidntSpawnOnSpawnPoint = pl.DidntSpawnOnSpawnPoint or not pl.SpawnedOnSpawnPoint
				return spawn
			end
		end
	end

	pl.SpawnedOnSpawnPoint = false
	pl.DidntSpawnOnSpawnPoint = true

	-- Fallback.
	return LastSpawnPoints[teamid] or #tab > 0 and table.Random(tab) or pl
end

local function BossZombieSort(za, zb)
	local ascore = za.WaveBarricadeDamage * 0.05 + za.WaveHumanDamage
	local bscore = zb.WaveBarricadeDamage * 0.05 + zb.WaveHumanDamage
	if ascore == bscore then
		return za:Deaths() < zb:Deaths()
	end

	return ascore > bscore
end

function GM:SpawnBossZombie(bossplayer, silent, bossindex, triggerboss)
	if not bossplayer then
		bossplayer = self:CalculateNextBoss()
	end

	if not bossplayer then return end

	if not bossindex then
		bossindex = bossplayer:GetBossZombieIndex()
	end

	if bossindex == -1 then return end

	if not triggerboss then
		bossplayer.BossDeathNotification = true
		GAMEMODE.StatTracking:IncreaseElementKV(STATTRACK_TYPE_ZOMBIECLASS, GAMEMODE.ZombieClasses[bossindex].Name, "BossSpawn", 1)
	end

	self.LastBossZombieSpawned = self:GetWave()

	local curclass = bossplayer.DeathClass or bossplayer:GetZombieClass()
	bossplayer:KillSilent()
	bossplayer:SetZombieClass(bossindex)
	bossplayer:DoHulls(bossindex, TEAM_UNDEAD)
	bossplayer.DeathClass = nil
	bossplayer:UnSpectateAndSpawn()
	bossplayer.DeathClass = curclass
	bossplayer.BossHealRemaining = 750

	if not silent then
		net.Start("zs_boss_spawned")
			net.WriteEntity(bossplayer)
			net.WriteUInt(bossindex, 8)
		net.Broadcast()
	end
end

function GM:SendZombieVolunteers(pl, nonemptyonly)
	if nonemptyonly and #self.ZombieVolunteers == 0 then return end

	net.Start("zs_zvols")
		net.WriteUInt(#self.ZombieVolunteers, 8)
		for _, p in ipairs(self.ZombieVolunteers) do
			net.WriteEntity(p)
		end
	if pl then
		net.Send(pl)
	else
		net.Broadcast()
	end
end

function GM:ZombieSpawnDistanceSort(other)
	return self._ZombieSpawnDistance < other._ZombieSpawnDistance
end

function GM:ZombieSpawnDistanceSortSigils(other)
	return self._ZombieSpawnDistance > other._ZombieSpawnDistance
end

function GM:SortZombieSpawnDistances(allplayers)
	local plpos, dist

	-- If using sigils then we sort by inverse distance from sigils instead of this.
	local zspawns = self:GetSigils()
	local sortbysigils = #zspawns > 0
	if not sortbysigils then
		zspawns = ents.FindByClass("zombiegasses")
		if #zspawns == 0 then
			zspawns = team.GetValidSpawnPoint(TEAM_UNDEAD)
		end
	end

	local maxdist = 1280000000--12800000
	for _, pl in pairs(allplayers) do
		if pl:Team() == TEAM_UNDEAD then
			pl._ZombieSpawnDistance = sortbysigils and maxdist + 2 or -2
		elseif pl:GetInfo("zs_alwaysvolunteer") == "1" then
			pl._ZombieSpawnDistance = sortbysigils and maxdist + 1 or -1
		elseif CLIENT or pl.LastNotAFK and CurTime() <= pl.LastNotAFK + 60 then
			plpos = pl:GetPos()
			dist = maxdist
			for __, ent in pairs(zspawns) do
				dist = math.min(dist, ent:NearestPoint(plpos):DistToSqr(plpos))
			end
			pl._ZombieSpawnDistance = dist
		else
			pl._ZombieSpawnDistance = sortbysigils and 128 or maxdist -- AFK people should NOT be considered volunteers but also people ACTIVELY next to sigils should be picked AFTER AFK people.
		end
	end

	table.sort(allplayers, sortbysigils and self.ZombieSpawnDistanceSortSigils or self.ZombieSpawnDistanceSort)
end

function GM:ShouldRestartRound()
	if self.TimeLimit == -1 or self.RoundLimit == -1 then return true end

	local roundlimit = self.RoundLimit
	--[[if self.ZombieEscape and roundlimit > 0 then
		roundlimit = math.ceil(roundlimit * 1.5)
	end]]

	local timelimit = self.TimeLimit
	if self.ZombieEscape and timelimit > 0 then
		timelimit = timelimit * 1.5
	end

	if timelimit > 0 and CurTime() >= timelimit
	or roundlimit > 0 and self.CurrentRound >= roundlimit
	or not self.ZombieEscape and ROUNDWINNER == TEAM_HUMAN then
		return false
	end

	return true
end

local NextTick = 0
function GM:Think()
	local time = CurTime()
	local wave = self:GetWave()

	if not self.RoundEnded then
		if self:GetWaveActive() then
			if self:GetWaveEnd() <= time and self:GetWaveEnd() ~= -1 then
				gamemode.Call("SetWaveActive", false)
			end
		elseif self:GetWaveStart() ~= -1 then
			if self:GetWaveStart() <= time then
				gamemode.Call("SetWaveActive", true)
			elseif self.BossZombies and not self.PantsMode and not self:IsClassicMode() and not self.ZombieEscape
			and self.LastBossZombieSpawned ~= wave and wave > 0 and not self.RoundEnded
			and (self.BossZombiePlayersRequired <= 0 or #player.GetAll() >= self.BossZombiePlayersRequired) then
				if self:GetWaveStart() - 5 <= time then
					self:SpawnBossZombie()
				else
					self:CalculateNextBoss()
				end
			end
		end
	end

	local allplayers = player_GetAll()

	for _, pl in pairs(allplayers) do
		if pl.ShouldFlinch then
			pl.ShouldFlinch = nil
			--pl:Flinch()
		end

		if P_Team(pl) == TEAM_HUMAN then
			if P_GetBarricadeGhosting(pl) then
				P_BarricadeGhostingThink(pl)
			end

			if pl.PointQueue >= 1 and time >= pl.LastDamageDealtTime + 2 then
				pl:PointCashOut(pl.LastDamageDealtPos or pl:GetPos(), FM_NONE)
			end

			if P_GetPhantomHealth(pl) > 0 and P_Alive(pl) and pl:IsSkillActive(SKILL_BLOODLUST) then
				pl:SetPhantomHealth(math_max(0, P_GetPhantomHealth(pl) - 5 * FrameTime()))
			end
		end
	end

	if wave == 0 then
		self:CalculateZombieVolunteers()
	end

	if NextTick <= time then
		NextTick = time + 1

		local plpos

		if wave == 0 and not self:GetWaveActive() then
			for _, pl in pairs(allplayers) do
				if P_Team(pl) == TEAM_HUMAN then
					plpos = pl:GetPos()
					if pl.LastAFKPosition and (pl.LastAFKPosition.x ~= plpos.x or pl.LastAFKPosition.y ~= plpos.y) then
						pl.LastNotAFK = time
					end
					pl.LastAFKPosition = plpos
				end
			end
		end

		for _, pl in pairs(allplayers) do
			if P_Team(pl) == TEAM_HUMAN and P_Alive(pl) then
				plpos = pl:GetPos()
				if doafk then
					if pl.LastAFKPosition and (pl.LastAFKPosition.x ~= plpos.x or pl.LastAFKPosition.y ~= plpos.y) then
						pl.LastNotAFK = time
					end
					pl.LastAFKPosition = plpos
				end

				if pl:WaterLevel() >= 3 and not (pl.status_drown and pl.status_drown:IsValid()) then
					pl:GiveStatus("drown")
				end

				local healmax = pl:IsSkillActive(SKILL_D_FRAIL) and math.floor(pl:GetMaxHealth() * 0.25) or pl:GetMaxHealth()

				if pl:IsSkillActive(SKILL_REGENERATOR) and time >= pl.NextRegenerate and pl:Health() < math.min(healmax, pl:GetMaxHealth() * 0.6) then
					pl.NextRegenerate = time + 6
					pl:SetHealth(math.min(healmax, pl:Health() + 1))
				end

				if pl:HasTrinket("regenimplant") and time >= pl.NextRegenTrinket and pl:Health() < healmax then
					pl.NextRegenTrinket = time + 12
					pl:SetHealth(math.min(healmax, pl:Health() + 1))
				end

				if pl:IsSkillActive(SKILL_BLOODARMOR) and pl.MaxBloodArmor > 0 and time >= pl.NextBloodArmorRegen and pl:GetBloodArmor() < pl.MaxBloodArmor then
					pl.NextBloodArmorRegen = time + 8
					pl:SetBloodArmor(math.min(pl.MaxBloodArmor, pl:GetBloodArmor() + (1 * pl.BloodarmorGainMul)))
				end

				if pl:KeyDown(IN_SPEED) and pl:GetVelocity() ~= vector_origin and pl:IsSkillActive(SKILL_CARDIOTONIC) then
					if pl:GetBloodArmor() > 0 then
						pl:SetBloodArmor(pl:GetBloodArmor() - 1)
						if pl:GetBloodArmor() == 0 and pl:IsSkillActive(SKILL_BLOODLETTER) then
							local bleed = pl:GiveStatus("bleed")
							if bleed and bleed:IsValid() then
								bleed:AddDamage(5)
								bleed.Damager = pl
							end
						end
					else
						pl:ResetSpeed()
					end
				end

				if pl:IsSkillActive(SKILL_D_LATEBUYER) and not pl.LateBuyerMessage then
					local midwave = self:GetWave() < self:GetNumberOfWaves() / 2 or self:GetWave() == self:GetNumberOfWaves() / 2 and self:GetWaveActive() and time < self:GetWaveEnd() - (self:GetWaveEnd() - self:GetWaveStart()) / 2
					if not midwave then
						pl:CenterNotify(COLOR_CYAN, translate.ClientGet(pl, "late_buyer_finished"))
						pl:SendLua("surface.PlaySound(\"buttons/button5.wav\")")
						pl.LateBuyerMessage = true
					end
				end

				pl:CheckTrinketRecharges()

				if pl:HasTrinket("autoreload") and pl.OldWeaponToReload and time > (pl.NextAutomatedReload or 0) then
					local mywep = pl.OldWeaponToReload
					if mywep and mywep:IsValid() and mywep.FinishReload then
						local max1 = mywep:GetPrimaryClipSize()

						if max1 > 0 then
							local ammotype = mywep:GetPrimaryAmmoType()
							local spare = pl:GetAmmoCount(ammotype)
							local current = mywep:Clip1()
							local needed = max1 - current

							needed = math.min(spare, needed)

							mywep:SetClip1(current + needed)
							pl:RemoveAmmo(needed, ammotype)
						end
					end

					pl.NextAutomatedReload = math.huge
					pl.OldWeaponToReload = nil
				end

				if pl:IsSkillActive(SKILL_STOWAGE) and self:GetWave() > 0 and time > (pl.NextResupplyUse or 0) then
					local stockpiling = pl:IsSkillActive(SKILL_STOCKPILE)

					pl.NextResupplyUse = time + self.ResupplyBoxCooldown * (pl.ResupplyDelayMul or 1) * (stockpiling and 2.12 or 1)
					pl.StowageCaches = (pl.StowageCaches or 0) + (stockpiling and 2 or 1)

					net.Start("zs_nextresupplyuse")
						net.WriteFloat(pl.NextResupplyUse)
					net.Send(pl)

					net.Start("zs_stowagecaches")
						net.WriteInt(pl.StowageCaches, 8)
					net.Send(pl)
				end
			end
		end

		if self:GetEscapeStage() == ESCAPESTAGE_DEATH then
			for _, pl in pairs(allplayers) do
				if P_Team(pl) == TEAM_HUMAN then
					pl:TakeSpecialDamage(15, DMG_ACID)
				end
			end
		end
	end
end

function GM:PlayerSwitchWeapon(pl, old, new)
	if pl:HasTrinket("autoreload") then
		pl.NextAutomatedReload = CurTime() + 3.95
		pl.OldWeaponToReload = old
	end
end

-- We calculate the volunteers. If the list changed then broadcast the new list.
function GM:CalculateZombieVolunteers()
	local volunteers = {}
	local allplayers = player_GetAll()
	self:SortZombieSpawnDistances(allplayers)
	for i = 1, self:GetDesiredStartingZombies() do
		volunteers[i] = allplayers[i]
	end

	local mismatch = false
	if #volunteers ~= #self.ZombieVolunteers then
		mismatch = true
	else
		for i=1, #volunteers do
			if volunteers[i] ~= self.ZombieVolunteers[i] then
				mismatch = true
				break
			end
		end
	end
	if mismatch then
		self.ZombieVolunteers = volunteers
		self:SendZombieVolunteers()
	end
end

GM.LastCalculatedBossTime = 0
function GM:CalculateNextBoss()
	local zombies = {}
	for _, ent in pairs(team.GetPlayers(TEAM_UNDEAD)) do
		if ent:GetInfo("zs_nobosspick") == "0" and not ent:GetZombieClassTable().Boss then
			table.insert(zombies, ent)
		end
	end
	table.sort(zombies, BossZombieSort)
	local newboss = zombies[1]

	if newboss ~= self.LastCalculatedBoss or CurTime() >= self.LastCalculatedBossTime + 2 then
		self.LastCalculatedBoss = newboss
		self.LastCalculatedBossTime = CurTime()

		net.Start("zs_nextboss")
		if newboss and newboss:IsValid() then
			net.WriteEntity(newboss)
			net.WriteUInt(newboss:GetBossZombieIndex(), 8)
		else
			net.WriteEntity(NULL)
			net.WriteUInt(1, 8)
		end
		net.Broadcast()
	end

	return newboss
end

function GM:LastBite(victim, attacker)
	LAST_BITE = attacker
end

function GM:CalculateInfliction(victim, attacker)
	if self.RoundEnded or self:GetWave() == 0 then return self.CappedInfliction end

	local players = 0
	local zombies = 0
	local humans = 0
	local wonhumans = 0
	local hum
	for _, pl in pairs(player.GetAllActive()) do
		if not pl.Disconnecting then
			if pl:Team() == TEAM_UNDEAD then
				zombies = zombies + 1
			elseif pl:HasWon() then
				wonhumans = wonhumans + 1
			else
				humans = humans + 1
				hum = pl
			end
		end
	end

	players = humans + zombies

	if players == 0 and wonhumans == 0 then return self.CappedInfliction end

	local infliction = math.max(zombies / players, self.CappedInfliction)
	self.CappedInfliction = infliction

	if humans == 1 and 2 < zombies then
		gamemode.Call("LastHuman", hum)
	elseif 1 <= infliction then
		infliction = 1

		if wonhumans >= 1 then
			gamemode.Call("EndRound", TEAM_HUMAN)
		else
			gamemode.Call("EndRound", TEAM_UNDEAD)

			if attacker and attacker:IsValid() and attacker:IsPlayer() and attacker:Team() == TEAM_UNDEAD and attacker ~= victim then
				gamemode.Call("LastBite", victim, attacker)
			end
		end
	end

	if not self:IsClassicMode() and not self.ZombieEscape and not self:IsBabyMode() and not self.PantsMode then
		for k, v in ipairs(self.ZombieClasses) do
			if v.Infliction and infliction >= v.Infliction and not self:IsClassUnlocked(v.Name) then
				v.Unlocked = true

				for _, ent in pairs(ents.FindByClass("logic_classunlock")) do
					local classname = v.Name
					if ent.Class == string.lower(classname) then
						ent:Input("onclassunlocked", ent, ent, classname)
					end
				end

				if not self.PantsMode and not self:IsClassicMode() and not self:IsBabyMode() and not self.ZombieEscape and not v.Locked then
					net.Start("zs_classunlockstate")
					net.WriteInt(k, 8)
					net.WriteBool(v.Unlocked)
					net.Broadcast()

					for _, pl in pairs(player.GetAll()) do
						pl:CenterNotify(COLOR_RED, translate.ClientFormat(pl, "infliction_reached", v.Infliction * 100))
						pl:CenterNotify(translate.ClientFormat(pl, "x_unlocked", translate.ClientGet(pl, v.TranslationName)))
					end
				end
			end
		end
	end

	for _, ent in pairs(ents.FindByClass("logic_infliction")) do
		if ent.Infliction <= infliction then
			ent:Input("oninflictionreached", NULL, NULL, infliction)
		end
	end

	return infliction
end
timer.Create("CalculateInfliction", 2, 0, function() gamemode.Call("CalculateInfliction") end)

function GM:OnNPCKilled(ent, attacker, inflictor)
end

function GM:LastHuman(pl)
	if not LASTHUMAN then
		net.Start("zs_lasthuman")
			net.WriteEntity(pl or NULL)
		net.Broadcast()

		for _, ent in pairs(ents.FindByClass("logic_infliction")) do
			ent:Input("onlasthuman", pl, pl, pl and pl:IsValid() and pl:EntIndex() or -1)
		end

		LASTHUMAN = true
	end

	self.TheLastHuman = pl
end

function GM:PlayerHealedTeamMember(pl, other, health, wep, pointmul, nobymsg, floater)
	health = health - other:RemoveUselessDamage(health)
	if self:GetWave() == 0 or health <= 0 or pl == other then return end

	pl.HealedThisRound = pl.HealedThisRound + health

	if pointmul ~= 0 then
		local hpperpoint = self.MedkitPointsPerHealth
		if hpperpoint <= 0 then return end

		local points = health / hpperpoint * pointmul

		pl:AddPoints(points)
	end

	net.Start("zs_healother")
		net.WriteBool(not floater)
		net.WriteEntity(other)
		net.WriteFloat(health)
	net.Send(pl)

	if not nobymsg then
		net.Start("zs_healby")
			net.WriteFloat(health)
			net.WriteEntity(pl)
		net.Send(other)
	end
end

function GM:ObjectPackedUp(pack, packer, owner)
end

function GM:PlayerRepairedObject(pl, other, health, wep)
	health = health - other:RemoveUselessDamage(health)
	if self:GetWave() == 0 or health <= 0 then return end

	pl.RepairedThisRound = pl.RepairedThisRound + health

	local hpperpoint = self.RepairPointsPerHealth
	if hpperpoint <= 0 then return end

	local points = health / hpperpoint

	pl:AddPoints(points)

	net.Start("zs_repairobject")
		net.WriteEntity(other)
		net.WriteFloat(health)
	net.Send(pl)
end

function GM:CacheHonorableMentions()
	if self.CachedHMs then return end

	self.CachedHMs = {}

	for i, hm in ipairs(self.HonorableMentions) do
		if hm.GetPlayer then
			local pl, magnitude = hm.GetPlayer(self)
			if pl then
				self.CachedHMs[i] = {pl, i, magnitude or 0}
			end
		end
	end

	gamemode.Call("PostDoHonorableMentions")
end

function GM:DoHonorableMentions(filter)
	self:CacheHonorableMentions()

	for i, tab in pairs(self.CachedHMs) do
		net.Start("zs_honmention")
			net.WriteEntity(tab[1])
			net.WriteUInt(tab[2], 8)
			net.WriteInt(tab[3], 32)
		if filter then
			net.Send(filter)
		else
			net.Broadcast()
		end
	end
end

function GM:PostDoHonorableMentions()
end

function GM:PostEndRound(winner)
	self:SaveAllVaults()
end

-- You can override or hook and return false in case you have your own map change system.
local function RealMap(map)
	return string.match(map, "(.+)%.bsp")
end
function GM:LoadNextMap()
	-- Just in case.
	timer.Simple(10, game.LoadNextMap)
	timer.Simple(15, function() RunConsoleCommand("changelevel", game.GetMap()) end)

	if file.Exists(GetConVar("mapcyclefile"):GetString(), "GAME") then
		game.LoadNextMap()
	else
		local maps = file.Find("maps/zs_*.bsp", "GAME")
		maps = table.Add(maps, file.Find("maps/ze_*.bsp", "GAME"))
		maps = table.Add(maps, file.Find("maps/zm_*.bsp", "GAME"))
		table.sort(maps)
		if #maps > 0 then
			local currentmap = game.GetMap()
			for i, map in ipairs(maps) do
				local lowermap = string.lower(map)
				local realmap = RealMap(lowermap)
				if realmap == currentmap then
					if maps[i + 1] then
						local nextmap = RealMap(maps[i + 1])
						if nextmap then
							RunConsoleCommand("changelevel", nextmap)
						end
					else
						local nextmap = RealMap(maps[1])
						if nextmap then
							RunConsoleCommand("changelevel", nextmap)
						end
					end

					break
				end
			end
		end
	end
end

function GM:PreRestartRound()
	for _, pl in pairs(player.GetAll()) do
		pl:StripWeapons()
		pl:Spectate(OBS_MODE_ROAMING)
		pl:GodDisable()
	end
end

GM.CurrentRound = 1
function GM:RestartRound()
	self.CurrentRound = self.CurrentRound + 1

	net.Start("zs_currentround")
		net.WriteUInt(self.CurrentRound, 6)
	net.Broadcast()

	self:RestartLua()
	self:RestartGame()

	net.Start("zs_gamemodecall")
		net.WriteString("RestartRound")
	net.Broadcast()
end

GM.DynamicSpawning = true
GM.CappedInfliction = 0
GM.PeakPopulation = 0
GM.StartingZombie = {}
GM.CheckedOut = {}
GM.PreviouslyDied = {}
GM.StoredUndeadFrags = {}

function GM:RestartLua()
	self.CachedHMs = nil
	self.TheLastHuman = nil
	self.LastBossZombieSpawned = nil
	self.UseSigils = nil
	--self:SetAllSigilsDestroyed(false)

	-- logic_pickups
	self.MaxWeaponPickups = nil
	self.MaxAmmoPickups = nil
	self.MaxFlashlightPickups = nil
	self.WeaponRequiredForAmmo = nil
	for _, pl in pairs(player.GetAll()) do
		pl.AmmoPickups = nil
		pl.WeaponPickups = nil
	end

	self.OverrideEndSlomo = nil
	if type(GetGlobalBool("endcamera", 1)) ~= "number" then
		SetGlobalBool("endcamera", nil)
	end
	if GetGlobalString("winmusic", "-") ~= "-" then
		SetGlobalString("winmusic", nil)
	end
	if GetGlobalString("losemusic", "-") ~= "-" then
		SetGlobalString("losemusic", nil)
	end
	if type(GetGlobalVector("endcamerapos", 1)) ~= "number" then
		SetGlobalVector("endcamerapos", nil)
	end

	self.CappedInfliction = 0
	self.PeakPopulation = 0

	self.StartingZombie = {}
	self.CheckedOut = {}
	self.PreviouslyDied = {}
	self.StoredUndeadFrags = {}

	ROUNDWINNER = nil
	LAST_BITE = nil
	LASTHUMAN = nil

	hook.Remove("PlayerShouldTakeDamage", "EndRoundShouldTakeDamage")
	hook.Remove("PlayerCanHearPlayersVoice", "EndRoundCanHearPlayersVoice")

	self:RevertZombieClasses()
	self:ClearItemStocks(true)
end

-- I don't know.
local function CheckBroken()
	for _, pl in pairs(player.GetAll()) do
		if pl:Alive() and (pl:Health() <= 0 or pl:GetObserverMode() ~= OBS_MODE_NONE or pl:OBBMaxs().x ~= 16) then
			pl:SetObserverMode(OBS_MODE_NONE)
			pl:UnSpectateAndSpawn()
		end
	end
end

function GM:DoRestartGame()
	self.RoundEnded = nil

	for _, ent in pairs(ents.FindByClass("prop_weapon")) do
		ent:Remove()
	end

	for _, ent in pairs(ents.FindByClass("prop_ammo")) do
		ent:Remove()
	end

	for _, ent in pairs(ents.FindByClass("prop_invitem")) do
		ent:Remove()
	end

	self:SetUseSigils(false)
	self:SetEscapeStage(ESCAPESTAGE_NONE)

	self:SetWave(0)
	self:SetWaveActive(false)
	if self.ZombieEscape then
		self:SetWaveStart(CurTime() + 30)
	else
		self:SetWaveStart(CurTime() + self.WaveZeroLength)
	end
	self:SetWaveEnd(self:GetWaveStart() + self:GetWaveOneLength())

	SetGlobalInt("numwaves", -2)

	timer.Create("CheckBroken", 10, 1, CheckBroken)

	game.CleanUpMap(false, self.CleanupFilter)
	gamemode.Call("InitPostEntityMap")

	for _, pl in pairs(player.GetAll()) do
		pl:UnSpectateAndSpawn()
		pl:GodDisable()
		gamemode.Call("PlayerInitialSpawnRound", pl)
		gamemode.Call("PlayerReadyRound", pl)

		if pl:Team() == TEAM_UNDEAD then -- bots?
			pl:KillSilent()
		end
	end
end

function GM:RestartGame()
	for _, pl in pairs(player.GetAll()) do
		pl:StripWeapons()
		pl:StripAmmo()
		pl:SetFrags(0)
		pl:SetDeaths(0)
		pl:SetPoints(0)
		if not pl.IsZSBot then
			pl:ChangeTeam(TEAM_HUMAN)
		end
		pl:DoHulls()
		pl:SetZombieClass(self.DefaultZombieClass)
		pl.DeathClass = nil
	end

	for _, ent in pairs(ents.FindByClass("prop_obj_sigil")) do
		ent:Remove()
	end

	self:SetWave(0)
	if self.ZombieEscape then
		self:SetWaveStart(CurTime() + 30)
	else
		self:SetWaveStart(CurTime() + self.WaveZeroLength)
	end
	self:SetWaveEnd(self:GetWaveStart() + self:GetWaveOneLength())
	self:SetWaveActive(false)

	SetGlobalInt("numwaves", -2)
	if GetGlobalString("hudoverride"..TEAM_UNDEAD, "") ~= "" then
		SetGlobalString("hudoverride"..TEAM_UNDEAD, "")
	end
	if GetGlobalString("hudoverride"..TEAM_HUMAN, "") ~= "" then
		SetGlobalString("hudoverride"..TEAM_HUMAN, "")
	end

	timer.Simple(0.25, function() GAMEMODE:DoRestartGame() end)
end

function GM:InitPostEntityMap(fromze)
	pcall(gamemode.Call, "LoadMapEditorFile")

	gamemode.Call("SetupSpawnPoints")
	gamemode.Call("RemoveUnusedEntities")
	if not fromze then
		gamemode.Call("ReplaceMapWeapons")
		gamemode.Call("ReplaceMapAmmo")
		gamemode.Call("ReplaceMapBatteries")
	end
	gamemode.Call("CreateZombieGas")
	gamemode.Call("SetupProps")

	for _, ent in pairs(ents.FindByClass("prop_ammo")) do ent.PlacedInMap = true end
	for _, ent in pairs(ents.FindByClass("prop_weapon")) do ent.PlacedInMap = true end
	for _, ent in pairs(ents.FindByClass("func_door_rotating")) do ent.NoTraceAttack = true end
	for _, ent in pairs(ents.FindByClass("func_physbox")) do ent.IsPhysbox = true end
	for _, ent in pairs(ents.FindByClass("func_physbox_multiplayer")) do
		ent.IsPhysbox = true
		ent.IgnoreZEProtect = true
	end
	for _, ent in ipairs(ents.FindByClass("prop_door_rotating")) do
		gamemode.Call("SetupBreakablePropDoor", ent)
	end

	for _, ent in pairs(ents.FindByClass("item_*")) do ent.NoNails = true end

	if self.ObjectiveMap then
		self:SetDynamicSpawning(false)
		self.BossZombies = false
	end

	if game.MaxPlayers() > 16 then
		local e = ents.FindByClass("shadow_control")[1]
		if not e then
			e = ents.Create("shadow_control")
			e:Spawn()
		end
		if e:IsValid() then
			e:SetKeyValue("disableallshadows", "1")
		end

		util.RemoveAll("func_precipitation")
	end

	gamemode.Call("CreateSigils")
end

function GM:SetDynamicSpawning(onoff)
	SetGlobalBool("DynamicSpawningDisabled", not onoff)
	self.DynamicSpawning = onoff
end

local function EndRoundPlayerShouldTakeDamage(pl, attacker) return pl:Team() == TEAM_UNDEAD or not attacker:IsPlayer() end
local function EndRoundPlayerCanSuicide(pl) return pl:Team() == TEAM_UNDEAD end

local function EndRoundSetupPlayerVisibility(pl)
	if GAMEMODE.LastHumanPosition and GAMEMODE.RoundEnded then
		AddOriginToPVS(GAMEMODE.LastHumanPosition)
	else
		hook.Remove("SetupPlayerVisibility", "EndRoundSetupPlayerVisibility")
	end
end

function GM:OnPlayerWin(pl)
	local xp = math.Clamp(#player.GetAll() * 6, 20, 200) * (GAMEMODE.WinXPMulti or 1)
	if self.ZombieEscape then
		xp = xp / 4
	end
	pl:AddZSXP(xp)
end

function GM:OnPlayerLose(pl)
end

function GM:EndRound(winner)
	if self.RoundEnded then return end
	self.RoundEnded = true
	self.RoundEndedTime = CurTime()
	ROUNDWINNER = winner

	if self.OverrideEndSlomo == nil or self.OverrideEndSlomo then
		game.SetTimeScale(0.25)
		timer.Simple(2, function() game.SetTimeScale(1) end)
	end

	hook.Add("PlayerCanHearPlayersVoice", "EndRoundCanHearPlayersVoice", function() return true, false end)

	if self.OverrideEndCamera == nil or self.OverrideEndCamera then
		hook.Add("SetupPlayerVisibility", "EndRoundSetupPlayerVisibility", EndRoundSetupPlayerVisibility)
	end

	if self:ShouldRestartRound() then
		timer.Simple(self.EndGameTime - 3, function() gamemode.Call("PreRestartRound") end)
		timer.Simple(self.EndGameTime, function() gamemode.Call("RestartRound") end)
	else
		timer.Simple(self.EndGameTime, function() gamemode.Call("LoadNextMap") end)
	end

	-- Get rid of some lag.
	util.RemoveAll("prop_ammo")
	util.RemoveAll("prop_weapon")
	util.RemoveAll("prop_invitem")

	timer.Simple(5, function() gamemode.Call("DoHonorableMentions") end)

	if winner == TEAM_HUMAN then
		self.LastHumanPosition = nil

		for _, pl in pairs(player.GetAll()) do
			if pl:Team() == TEAM_HUMAN then
				if not self:GetUseSigils() then
					gamemode.Call("OnPlayerWin", pl)
				end
			elseif pl:Team() == TEAM_UNDEAD then
				gamemode.Call("OnPlayerLose", pl)
			end
		end

		hook.Add("PlayerShouldTakeDamage", "EndRoundShouldTakeDamage", EndRoundPlayerShouldTakeDamage)
	elseif winner == TEAM_UNDEAD then
		hook.Add("PlayerShouldTakeDamage", "EndRoundShouldTakeDamage", EndRoundPlayerCanSuicide)

		for _, pl in pairs(team.GetPlayers(TEAM_UNDEAD)) do
			gamemode.Call("OnPlayerLose", pl)
		end
	end

	net.Start("zs_endround")
		net.WriteUInt(winner or -1, 8)
		net.WriteString(game.GetMapNext())
	net.Broadcast()

	GAMEMODE.StatTracking:IncreaseElementKV(STATTRACK_TYPE_ROUND,
		winner == TEAM_HUMAN and "Wins" or ("LossWave"..self:GetWave()), game.GetMap(), 1)

	if winner == TEAM_HUMAN then
		for _, ent in pairs(ents.FindByClass("logic_winlose")) do
			ent:Input("onwin")
		end
	else
		for _, ent in pairs(ents.FindByClass("logic_winlose")) do
			ent:Input("onlose")
		end
	end

	gamemode.Call("PostEndRound", winner)

	self:SetWaveStart(CurTime() + 9999)
end

function GM:ScalePlayerDamage(pl, hitgroup, dmginfo)
	local attacker = dmginfo:GetAttacker()
	local inflictor = dmginfo:GetInflictor()
	GAMEMODE.StatTracking:IncreaseElementKV(STATTRACK_TYPE_WEAPON, inflictor:GetClass(), "Hits", 1)
	if hitgroup == HITGROUP_HEAD then
		GAMEMODE.StatTracking:IncreaseElementKV(STATTRACK_TYPE_WEAPON, inflictor:GetClass(), "Headshots", 1)
	end

	if not dmginfo:IsBulletDamage() then return end

	if hitgroup == HITGROUP_HEAD and dmginfo:IsBulletDamage() then
		pl.m_LastHeadShot = CurTime()
	end

	--local crouchpunish = pl:ShouldCrouchJumpPunish()

	if not pl:CallZombieFunction2("ScalePlayerDamage", hitgroup, dmginfo) then
		if hitgroup == HITGROUP_HEAD then
			dmginfo:SetDamage(dmginfo:GetDamage() * (inflictor.HeadshotMulti or 2) * (attacker:IsPlayer() and attacker:GetStatus("renegade") and 1.1 or 1))
		elseif hitgroup == HITGROUP_LEFTLEG or hitgroup == HITGROUP_RIGHTLEG then
			--if not crouchpunish then
			if not pl:ShouldCrouchJumpPunish() then
				dmginfo:SetDamage(dmginfo:GetDamage() / 4)
			end
		end
	end

	if (hitgroup == HITGROUP_LEFTLEG or hitgroup == HITGROUP_RIGHTLEG) and self:PlayerShouldTakeDamage(pl, dmginfo:GetAttacker()) and not pl:CallZombieFunction1("IgnoreLegDamage", dmginfo) then
		pl:AddLegDamage(
			pl:ShouldCrouchJumpPunish() and not (pl.LastBarricadeHit and pl.LastBarricadeHit + 2 > CurTime()) and dmginfo:GetDamage()/4
			or dmginfo:GetDamage()
		)
	end
end

function GM:PlayerReady(pl)
	gamemode.Call("PlayerReadyRound", pl)

	self:PlayerReadyVault(pl)

	pl.PlayerReady = true
end

function GM:PlayerReadyRound(pl)
	if not pl:IsValid() then return end

	self:FullGameUpdate(pl)
	pl:UpdateAllZombieClasses()

	local classid = pl:GetZombieClass()
	pl:SetZombieClass(classid, true, pl)

	if self.OverrideStartingWorth then
		pl:SendLua("GAMEMODE.StartingWorth="..tostring(self.StartingWorth))
	end

	if pl:Team() == TEAM_UNDEAD then
		-- This is just so they get updated on what class they are and have their hulls set up right.
		pl:DoHulls(classid, TEAM_UNDEAD)
	elseif pl:Team() == TEAM_HUMAN then
		if self:GetWave() <= 0 and self.StartingWorth > 0 and not self.StartingLoadout and not self.ZombieEscape then
			pl:SendLua("InitialWorthMenu()")
		else
			gamemode.Call("GiveDefaultOrRandomEquipment", pl)
		end
	end

	net.Start("zs_currentround")
		net.WriteUInt(self.CurrentRound, 6)
	net.Send(pl)

	if self.RoundEnded then
		pl:SendLua("gamemode.Call(\"EndRound\", "..tostring(ROUNDWINNER)..", \""..game.GetMapNext().."\")")
		gamemode.Call("DoHonorableMentions", pl)
	end

	if pl:GetInfo("zs_noredeem") == "1" then
		pl.NoRedeeming = true
	end

	if self:GetWave() == 0 then
		self:SendZombieVolunteers(pl, true)
	end

	if self:IsClassicMode() then
		pl:SendLua("SetGlobalBool(\"classicmode\", true)")
	elseif self:IsBabyMode() then
		pl:SendLua("SetGlobalBool(\"babymode\", true)")
	end

	self:RefreshItemStocks(pl)
	self:ClassUnlocksUpdate(pl)
end

function GM:FullGameUpdate(pl)
	net.Start("zs_gamestate")
		net.WriteInt(self:GetWave(), 16)
		net.WriteFloat(self:GetWaveStart())
		net.WriteFloat(self:GetWaveEnd())
	if pl then
		net.Send(pl)
	else
		net.Broadcast()
	end
end

concommand.Add("initpostentity", function(sender, command, arguments)
	if not sender.DidInitPostEntity then
		sender.DidInitPostEntity = true

		gamemode.Call("PlayerReady", sender)
	end
end)

local playerheight = Vector(0, 0, 72)
local function groupsort(ga, gb)
	return #ga > #gb
end
function GM:AttemptHumanDynamicSpawn(pl)
	if not self.DynamicSpawning or not pl:IsValidLivingHuman() then return false end

	local sigils = self:GetSigils()
	local randsigil = sigils[math.random(#sigils)]
	if randsigil and randsigil:IsValid() and not randsigil:GetSigilCorrupted() then
		pl:SetBarricadeGhosting(true)
		pl:SetPos(randsigil:GetPos())
		return true
	end

	local group, pos, nearzombie

	local allplayers = team.GetPlayers(TEAM_HUMAN)

	local groups = self:GetTeamRallyGroups(TEAM_HUMAN)
	table.sort(groups, groupsort)

	for i=1, #groups do
		group = groups[i]

		for _, otherpl in pairs(group) do
			if otherpl ~= pl then
				pos = otherpl:GetPos() + Vector(0, 0, 1)
				if otherpl:Alive() and otherpl:GetMoveType() == MOVETYPE_WALK and not util.TraceHull({start = pos, endpos = pos + playerheight, mins = playermins, maxs = playermaxs, mask = MASK_SOLID, filter = allplayers}).Hit then
					nearzombie = false
					for __, ent in pairs(team.GetPlayers(TEAM_UNDEAD)) do
						if ent:Alive() and ent:GetPos():DistToSqr(pos) <= 65536 then --256^2
							nearzombie = true
						end
					end

					if not nearzombie then
						pl:SetPos(otherpl:GetPos())
						return true
					end
				end
			end
		end
	end

	return false
end

function GM:PlayerInitialSpawn(pl)
	pl.MaxBloodArmor = GAMEMODE.ZombieEscape and 0 or 10
	pl.NextFlashlightSwitch = 0
	pl.NextPainSound = 0
	pl.NextFlinch = 0
	pl.LastSentESW = 0
	pl.m_LastWaveStartSpawn = 0
	pl.m_LastGasHeal = 0

	self:InitializeVault(pl)

	gamemode.Call("PlayerInitialSpawnRound", pl)

	self.PeakPopulation = math.max(self.PeakPopulation, #player.GetAll())
end

function GM:PlayerInitialSpawnRound(pl)
	pl:SprintDisable()
	if pl:KeyDown(IN_WALK) then
		pl:ConCommand("-walk")
	end

	pl:SetCanWalk(false)
	pl:SetCanZoom(false)

	-- This is the culprit for shitty player to player collisions when standing on an enemy's head. No idea why.
	pl:SetNoCollideWithTeammates(false) --pl:SetNoCollideWithTeammates(true)
	pl:SetCustomCollisionCheck(true)

	pl.ZombiesKilled = 0
	pl.ZombiesKilledAssists = 0
	pl.Headshots = 0
	pl.BrainsEaten = 0

	pl.ResupplyBoxUsedByOthers = 0

	pl.WaveJoined = self:GetWave()

	pl.CrowKills = 0
	pl.DefenceDamage = 0
	pl.StrengthBoostDamage = 0

	pl.BarricadeDamage = 0

	pl.PointsRemainder = 0

	pl.XPRemainder = 0

	pl.LegDamage = 0
	pl.ArmDamage = 0

	pl.DamageDealt = {}
	pl.DamageDealt[TEAM_UNDEAD] = 0
	pl.DamageDealt[TEAM_HUMAN] = 0

	pl.ZSFriends = {}

	pl.LifeBarricadeDamage = 0
	pl.LifeHumanDamage = 0
	pl.LifeBrainsEaten = 0

	pl.WaveBarricadeDamage = 0
	pl.WaveHumanDamage = 0

	pl.PointQueue = 0
	pl.LastDamageDealtTime = 0

	pl.HealedThisRound = 0
	pl.RepairedThisRound = 0
	pl.NextRegenerate = 0
	pl.NextBloodArmorRegen = 0
	pl.NextRegenTrinket = 0
	pl.LateBuyerMessage = nil
	pl.NestsDestroyed = 0
	pl.NestSpawns = 0
	pl.LastRevive = 0

	pl.ZSInventory = {}

	--local nosend = not pl.DidInitPostEntity
	pl.DamageVulnerability = nil

	self:LoadVault(pl)

	local uniqueid = pl:UniqueID()

	if self.PreviouslyDied[uniqueid] or ZSBOT then
		-- They already died and reconnected.
		pl:ChangeTeam(TEAM_UNDEAD)
	elseif LASTHUMAN then ----
		pl.SpawnedTime = CurTime()
		pl:ChangeTeam(TEAM_UNDEAD)
	elseif self:GetWave() <= 0 then
		pl.SpawnedTime = CurTime()
		pl:ChangeTeam(TEAM_HUMAN)
		if self.DynamicSpawning then
			timer.Simple(1, function()
				if IsValid(pl) and pl:Team() == TEAM_HUMAN then
					GAMEMODE:AttemptHumanDynamicSpawn(pl)
					pl:SetBarricadeGhosting(true, true)
				end
			end)
		end
	elseif self:GetNumberOfWaves() == -1 or self.NoNewHumansWave <= self:GetWave() or team.NumPlayers(TEAM_UNDEAD) == 0 and 1 <= team.NumPlayers(TEAM_HUMAN) then -- Joined during game, no zombies, some humans or joined past the deadline.
		pl:ChangeTeam(TEAM_UNDEAD)
		self.PreviouslyDied[uniqueid] = CurTime()
	else
		pl.SpawnedTime = CurTime()
		pl:ChangeTeam(TEAM_HUMAN)
		if self.DynamicSpawning then
			timer.Simple(0, function()
				if IsValid(pl) and pl:Team() == TEAM_HUMAN then
					GAMEMODE:AttemptHumanDynamicSpawn(pl)
					pl:SetBarricadeGhosting(true, true)
				end
			end)
		end
	end

	if pl:Team() == TEAM_UNDEAD and not self:GetWaveActive() then
		pl:SetZombieClassName("Crow")
		pl.DeathClass = self.DefaultZombieClass
	else
		pl:SetZombieClass(self.DefaultZombieClass)
	end

	if pl:Team() == TEAM_UNDEAD and self.StoredUndeadFrags[uniqueid] then
		pl:SetFrags(self.StoredUndeadFrags[uniqueid])
		self.StoredUndeadFrags[uniqueid] = nil
	end
end

function GM:GetDynamicSpawning()
	return self.DynamicSpawning
end

function GM:PrePlayerRedeemed(pl, silent, noequip)
end

function GM:PostPlayerRedeemed(pl, silent, noequip)
end

function GM:PlayerDisconnected(pl)
	pl.Disconnecting = true

	local uid = pl:UniqueID()

	self.PreviouslyDied[uid] = CurTime()

	if pl:Team() == TEAM_HUMAN then
		pl:DropAll()
	elseif pl:Team() == TEAM_UNDEAD then
		self.StoredUndeadFrags[uid] = pl:Frags()
	end

	if pl:Health() > 0 and not pl:IsSpectator() then
		local lastattacker = pl:GetLastAttacker()
		if IsValid(lastattacker) then
			pl:TakeDamage(1000, lastattacker, lastattacker)

			PrintTranslatedMessage(HUD_PRINTCONSOLE, "disconnect_killed", pl:Name(), lastattacker:Name())
		end
	end

	self:SaveVault(pl)

	gamemode.Call("CalculateInfliction")
end

function GM:CanDamageNail(ent, attacker, inflictor, damage, dmginfo)
	return not attacker:IsPlayer() or attacker:Team() == TEAM_UNDEAD
end

function GM:CanPlaceNail(pl, tr)
	if tr and not pl:HasBarricadeExpert() and tr.Entity.ExpertProtection and tr.Entity.ExpertProtection > CurTime() then
		return false
	end

	return true
end

function GM:CanRemoveNail(pl, nail)
	return not nail.m_NailUnremovable
end

function GM:CanRemoveOthersNail(pl, nailowner, ent)
	-- obsolete
	--[[local plpoints = pl:Frags()
	local ownerpoints = nailowner:Frags()
	if plpoints >= 75 or ownerpoints < 75 then return true end]]

	if gamemode.Call("PlayerIsAdmin", pl) then return true end
	if nailowner.ZSFriends[pl] then return true end

	if pl:BarricadeExpertPrecedence(nailowner) == -1 then
		pl:PrintTranslatedMessage(HUD_PRINTCENTER, "cant_remove_nails_of_superior_player")
		return false
	end

	return true
end

function GM:SetRedeemBrains(amount)
	SetGlobalInt("redeembrains", amount)
end

-- Reevaluates a prop and its constraint system (or all props if no arguments) to determine if they should be frozen or not from nails.
function GM:EvaluatePropFreeze(ent, neighbors)
	if not ent then
		for _, e in pairs(ents.GetAll()) do
			if e and e:IsValid() then
				self:EvaluatePropFreeze(e)
			end
		end

		return
	end

	if ent:IsNailedToWorldHierarchy() then
		ent:SetNailFrozen(true)
	elseif ent:GetNailFrozen() then
		ent:SetNailFrozen(false)
	end

	neighbors = neighbors or {}
	table.insert(neighbors, ent)

	local baseent, attachent

	for _, nail in pairs(ent:GetNails()) do
		if nail:IsValid() then
			baseent = nail:GetBaseEntity()
			attachent = nail:GetAttachEntity()
			if baseent:IsValid() and not baseent:IsWorld() and not table.HasValue(neighbors, baseent) then
				self:EvaluatePropFreeze(baseent, neighbors)
			end
			if attachent:IsValid() and not attachent:IsWorld() and not table.HasValue(neighbors, attachent) then
				self:EvaluatePropFreeze(attachent, neighbors)
			end
		end
	end
end

-- A nail takes some damage. isdead is true if the damage is enough to remove the nail. The nail is invalid after this function call if it dies.
function GM:OnNailDamaged(ent, attacker, inflictor, damage, dmginfo)
end

-- A nail is removed between two entities. The nail is no longer considered valid right after this function and is not in the entities' Nails tables. remover may not be nil if it was removed with the hammer's unnail ability.
local function evalfreeze(ent)
	if ent and ent:IsValid() then
		gamemode.Call("EvaluatePropFreeze", ent)
	end
end
function GM:OnNailRemoved(nail, ent1, ent2, remover)
	if ent1 and ent1:IsValid() and not ent1:IsWorld() then
		timer.Simple(0, function() evalfreeze(ent1) end)
		timer.Simple(0.2, function() evalfreeze(ent1) end)
	end
	if ent2 and ent2:IsValid() and not ent2:IsWorld() then
		timer.Simple(0, function() evalfreeze(ent2) end)
		timer.Simple(0.2, function() evalfreeze(ent2) end)
	end

	if remover and remover:IsValid() and remover:IsPlayer() then
		local deployer = nail:GetDeployer()
		local deployername = "[unconnected]"
		if deployer:IsValid() and deployer:Team() == TEAM_HUMAN then
			deployername = deployer:Name()

			if deployer ~= remover then
				net.Start("zs_nailremoved")
					net.WriteEntity(remover)
				net.Send(deployer)
			end
		end

		PrintTranslatedMessage(HUD_PRINTCONSOLE, "nail_removed_by", remover:Name(), deployername)

		if remover:HasBarricadeExpert() then
			if ent1 and ent1:IsValid() and not ent1:IsWorld() then ent1.ExpertProtection = CurTime() + 5 end
			if ent2 and ent2:IsValid() and not ent2:IsWorld() then ent2.ExpertProtection = CurTime() + 5 end
		end
	end
end

-- A nail is created between two entities.
function GM:OnNailCreated(ent1, ent2, nail)
	if ent1 and ent1:IsValid() and not ent1:IsWorld() then
		timer.Simple(0, function() evalfreeze(ent1) end)
	end
	if ent2 and ent2:IsValid() and not ent2:IsWorld() then
		timer.Simple(0, function() evalfreeze(ent2) end)
	end
end

function GM:RemoveDuplicateAmmo(pl)
	local AmmoCounts = {}
	local WepAmmos = {}

	for _, wep in pairs(pl:GetWeapons()) do
		if wep.Primary then
			local ammotype = wep:ValidPrimaryAmmo()
			if ammotype and wep.Primary.DefaultClip > 0 then
				AmmoCounts[ammotype] = (AmmoCounts[ammotype] or 0) + 1
				WepAmmos[wep] = wep.Primary.DefaultClip - wep.Primary.ClipSize
			end
			local ammotype2 = wep:ValidSecondaryAmmo()
			if ammotype2 and wep.Secondary.DefaultClip > 0 then
				AmmoCounts[ammotype2] = (AmmoCounts[ammotype2] or 0) + 1
				WepAmmos[wep] = wep.Secondary.DefaultClip - wep.Secondary.ClipSize
			end
		end
	end

	for ammotype, count in pairs(AmmoCounts) do
		if count > 1 then
			local highest = 0
			local highestwep
			for wep, extraammo in pairs(WepAmmos) do
				if wep.Primary.Ammo == ammotype then
					highest = math.max(highest, extraammo)
					highestwep = wep
				end
			end
			if highestwep then
				for wep, extraammo in pairs(WepAmmos) do
					if wep ~= highestwep and wep.Primary.Ammo == ammotype then
						pl:RemoveAmmo(extraammo, ammotype)
					end
				end
			end
		end
	end
end

local function TimedOut(pl)
	if pl:IsValid() and pl:Team() == TEAM_HUMAN and pl:Alive() and not GAMEMODE.CheckedOut[pl:UniqueID()] then
		gamemode.Call("GiveRandomEquipment", pl)
	end
end

function GM:GiveDefaultOrRandomEquipment(pl)
	if not self.CheckedOut[pl:UniqueID()] and not self.ZombieEscape then
		if self.StartingLoadout then
			self:GiveStartingLoadout(pl)
		else
			pl:SendLua("GAMEMODE:RequestedDefaultCart()")
			if self.StartingWorth > 0 then
				timer.Simple(4, function() TimedOut(pl) end)
			end
		end
	end
end

function GM:GiveStartingLoadout(pl)
	if self.CheckedOut[pl:UniqueID()] then return end
	self.CheckedOut[pl:UniqueID()] = true

	for item, amount in pairs(self.StartingLoadout) do
		for i=1, amount do
			pl:Give(item)

			GAMEMODE.StatTracking:IncreaseElementKV(STATTRACK_TYPE_WEAPON, item, "StartingLoadout", 1)
		end
	end
end

function GM:GiveRandomEquipment(pl)
	if self.CheckedOut[pl:UniqueID()] or self.ZombieEscape then return end
	self.CheckedOut[pl:UniqueID()] = true

	if self.StartingLoadout then
		self:GiveStartingLoadout(pl)
	elseif GAMEMODE.OverrideStartingWorth then
		pl:Give("weapon_zs_swissarmyknife")
	elseif #self.StartLoadouts >= 1 then
		for _, id in pairs(self.StartLoadouts[math.random(#self.StartLoadouts)]) do
			local tab = FindStartingItem(id)
			if tab then
				if tab.Callback then
					tab.Callback(pl)
				elseif tab.SWEP then
					if not pl:AddInventoryItem(tab.SWEP) then
						pl:StripWeapon(tab.SWEP)
						pl:Give(tab.SWEP)
					end

					GAMEMODE.StatTracking:IncreaseElementKV(STATTRACK_TYPE_WEAPON, tab.SWEP, "RandomCheckouts", 1)
				end
			end
		end
	end
end

function GM:PlayerCanCheckout(pl)
	return pl:IsValid() and pl:Team() == TEAM_HUMAN and pl:Alive() and not self.CheckedOut[pl:UniqueID()] and not self.StartingLoadout and not self.ZombieEscape and self.StartingWorth > 0 and self:GetWave() < 2
end

function GM:PlayerDeathThink(pl)
	if self.RoundEnded or pl.Revive or self:GetWave() == 0 then return end

	if pl:GetObserverMode() == OBS_MODE_CHASE then
		local target = pl:GetObserverTarget()
		if not target or not target:IsValid() or target:IsPlayer() and (not target:Alive() or target:Team() ~= pl:Team()) then
			pl:StripWeapons()
			pl:Spectate(OBS_MODE_ROAMING)
			pl:SpectateEntity(NULL)
		end
	end

	if pl:Team() ~= TEAM_UNDEAD then
		pl.StartCrowing = nil
		pl.StartSpectating = nil
		return
	end

	if pl.NextSpawnTime and pl.NextSpawnTime <= CurTime() then -- Force spawn.
		pl.NextSpawnTime = nil

		pl:RefreshDynamicSpawnPoint()
		pl:UnSpectateAndSpawn()
	elseif pl:GetObserverMode() == OBS_MODE_NONE then -- Not in spectator yet.
		if self:GetWaveActive() then -- During wave.
			if not pl.StartSpectating or CurTime() >= pl.StartSpectating then
				pl.StartSpectating = nil

				pl:StripWeapons()
				local best = self:GetBestDynamicSpawn(pl)
				if best then
					pl:Spectate(OBS_MODE_CHASE)
					pl:SpectateEntity(best)
				else
					pl:Spectate(OBS_MODE_ROAMING)
					pl:SpectateEntity(NULL)
				end
			end
		elseif not pl.StartCrowing or CurTime() >= pl.StartCrowing then -- Not during wave. Turn in to a crow. If we die as a crow then we get turned to spectator anyway.
			pl:ChangeToCrow()
		end
	else -- In spectator.
		if pl:KeyDown(IN_ATTACK) or pl:KeyDown(IN_ATTACK2) or pl:IsBot() then
			pl:RefreshDynamicSpawnPoint()

			local forcespawn = pl.ForceDynamicSpawn
			if forcespawn and forcespawn.MinionSpawn then
				pl:TrySpawnAsGoreChild(forcespawn)
			else
				if self:GetWaveActive() then
					pl:UnSpectateAndSpawn()
				else
					pl:ChangeToCrow()
				end
			end
		elseif pl:KeyDown(IN_RELOAD) then
			if self:GetWaveActive() then
				pl.ForceDynamicSpawn = nil
				local prev = self.DynamicSpawning
				self.DynamicSpawning = false
				pl:UnSpectateAndSpawn()
				self.DynamicSpawning = prev
			else
				pl:ChangeToCrow()
			end
		elseif pl:KeyPressed(IN_JUMP) then
			if pl:GetObserverMode() ~= OBS_MODE_ROAMING then
				pl:Spectate(OBS_MODE_ROAMING)
				pl:SpectateEntity(NULL)
				pl.SpectatedPlayerKey = nil
			end
		end
	end
end

function GM:ShouldAntiGrief(ent, attacker, dmginfo, health)
	return ent.m_AntiGrief and self.GriefMinimumHealth <= health and attacker:IsPlayer() and attacker:Team() == TEAM_HUMAN and not dmginfo:IsExplosionDamage()
end

function GM:PropBreak(attacker, ent)
	gamemode.Call("PropBroken", ent, attacker)
end

function GM:PropBroken(ent, attacker)
	if IsValid(ent) and IsValid(attacker) and not ent._PROPBROKEN and attacker:IsPlayer() and attacker:Team() == TEAM_HUMAN then
		ent._PROPBROKEN = true

		PrintMessage(HUD_PRINTCONSOLE, attacker:LogID().." broke "..ent:GetModel())
	end
end

function GM:NestDestroyed(ent, attacker)
	if IsValid(ent) and IsValid(attacker) and attacker:IsPlayer() and attacker:Team() == TEAM_UNDEAD then
		PrintMessage(HUD_PRINTCONSOLE, attacker:LogID().." team killed a nest at "..tostring(ent:GetPos()).." (builder: "..(ent:GetOwner() and ent:GetOwner():IsValid() and ent:GetOwner():IsPlayer() and ent:GetOwner():LogID() or "unknown")..")")
	end
end

function GM:EntityTakeDamage(ent, dmginfo)
	local attacker, inflictor = dmginfo:GetAttacker(), dmginfo:GetInflictor()

	if attacker == inflictor and attacker:IsProjectile() and dmginfo:GetDamageType() == DMG_CRUSH then -- Fixes projectiles doing physics-based damage.
		dmginfo:SetDamage(0)
		dmginfo:ScaleDamage(0)
		return
	end

	if ent.LastHeld and CurTime() < ent.LastHeld + 0.1 and attacker:IsPlayer() and P_Team(attacker) == TEAM_HUMAN then
		dmginfo:SetDamage(0)
		dmginfo:SetDamageType(0)
		dmginfo:ScaleDamage(0)
		dmginfo:SetDamageForce(vector_origin)
		return
	end

	-- Props about to be broken props take 3x damage from anything except zombies
	if ent._BARRICADEBROKEN and not (attacker:IsPlayer() and attacker:Team() == TEAM_UNDEAD) then
		dmginfo:SetDamage(dmginfo:GetDamage() * 3)
	end

	if ent.GetObjectHealth and not (attacker:IsPlayer() and attacker:Team() == TEAM_HUMAN) then
		ent.m_LastDamaged = CurTime()
	end

	if ent.ProcessDamage and ent:ProcessDamage(dmginfo) then return end
	attacker, inflictor = dmginfo:GetAttacker(), dmginfo:GetInflictor()

	-- Don't allow blowing up props during wave 0.
	if self:GetWave() <= 0 and string.sub(ent:GetClass(), 1, 12) == "prop_physics" and inflictor.NoPropDamageDuringWave0 then
		dmginfo:SetDamage(0)
		dmginfo:SetDamageType(DMG_ALWAYSGIB)
		return
	end

	-- We need to stop explosive chains team killing.
	if inflictor:IsValid() then
		local dmgtype = dmginfo:GetDamageType()
		if ent:IsPlayer() and (dmgtype == DMG_ALWAYSGIB or dmgtype == DMG_BURN or dmgtype == DMG_SLOWBURN) and string.sub(inflictor:GetClass(), 1, 12) == "prop_physics" then -- We'll assume a barrel did this damage to a player
			if inflictor.LastDamagedByTeam == ent:Team() and inflictor.LastDamagedBy ~= ent then -- A team member is trying to screw with us
				dmginfo:SetDamage(0)
				dmginfo:ScaleDamage(0)
				return
			end
		elseif string.sub(ent:GetClass(), 1, 12) == "prop_physics" then -- Physics object damaged by...
			if inflictor:IsPlayer() then
				ent.LastDamagedByTeam = inflictor:Team()
				ent.LastDamagedBy = inflictor
			elseif (dmgtype == DMG_ALWAYSGIB or dmgtype == DMG_BURN or dmgtype == DMG_SLOWBURN) and string.sub(inflictor:GetClass(), 1, 12) == "prop_physics" then -- A barrel damaging a barrel. Probably.
				if inflictor.LastDamagedByTeam then
					ent.LastDamagedByTeam = inflictor.LastDamagedByTeam
					ent.LastDamagedBy = inflictor.LastDamagedBy
				end
			end
		end
	end

	-- Prop is nailed. Forward damage to the nails.
	if ent:DamageNails(attacker, inflictor, dmginfo:GetDamage(), dmginfo) then return end

	local dispatchdamagedisplay = false
	local entclass = ent:GetClass()

	if ent:IsPlayer() then
		dispatchdamagedisplay = true

		if attacker.PBAttacker and attacker.PBAttacker:IsValid() then
			attacker = attacker.PBAttacker
		end

		if attacker:IsValid() then
			if attacker:IsPlayer() then
				ent:SetLastAttacker(attacker)

				local myteam = attacker:Team()
				local otherteam = ent:Team()

				if myteam ~= otherteam then
					local damage = math.min(dmginfo:GetDamage(), ent:Health())
					if damage > 0 then
						local time = CurTime()

						attacker.DamageDealt[myteam] = attacker.DamageDealt[myteam] + damage

						if myteam == TEAM_UNDEAD then
							if otherteam == TEAM_HUMAN then
								attacker:AddLifeHumanDamage(damage)
								GAMEMODE.StatTracking:IncreaseElementKV(STATTRACK_TYPE_ZOMBIECLASS, attacker:GetZombieClassTable().Name, "HumanDamage", damage)
							end
						elseif myteam == TEAM_HUMAN and otherteam == TEAM_UNDEAD then
							ent.DamagedBy[attacker] = (ent.DamagedBy[attacker] or 0) + damage
							if time >= ent.m_LastWaveStartSpawn + 3 and time >= ent.m_LastGasHeal + 2 then
								local points = damage / ent:GetMaxHealth() * ent:GetZombieClassTable().Points
								if POINTSMULTIPLIER then
									points = points * POINTSMULTIPLIER
								end
								if ent.PointsMultiplier then
									points = points * ent.PointsMultiplier
								end
								attacker.PointQueue = attacker.PointQueue + points

								GAMEMODE.StatTracking:IncreaseElementKV(STATTRACK_TYPE_WEAPON, inflictor:GetClass(), "PointsEarned", points)
								GAMEMODE.StatTracking:IncreaseElementKV(STATTRACK_TYPE_WEAPON, inflictor:GetClass(), "Damage", damage)
							end

							local pos = ent:GetPos()
							pos.z = pos.z + 32
							attacker.LastDamageDealtPos = pos
							attacker.LastDamageDealtTime = time
						end
					end
				end
			elseif attacker:GetClass() == "trigger_hurt" then
				ent.LastHitWithTriggerHurt = CurTime()
			end
		end
	elseif ent.PropHealth then -- A prop that was invulnerable and converted to vulnerable.
		if ent._PROPBROKEN or self.NoPropDamageFromHumanMelee and attacker:IsPlayer() and attacker:Team() == TEAM_HUMAN and inflictor.IsMelee then
			dmginfo:SetDamage(0)
			return
		end

		if gamemode.Call("ShouldAntiGrief", ent, attacker, dmginfo, ent.PropHealth) then
			attacker:AntiGrief(dmginfo)
			if dmginfo:GetDamage() <= 0 then return end
		end

		ent.PropHealth = ent.PropHealth - dmginfo:GetDamage()

		dispatchdamagedisplay = true

		if ent.PropHealth <= 0 then
			local effectdata = EffectData()
				effectdata:SetOrigin(ent:GetPos())
			util.Effect("Explosion", effectdata, true, true)
			ent:Fire("break")

			gamemode.Call("PropBroken", ent, attacker)
		else
			local brit = math.Clamp(ent.PropHealth / ent.TotalHealth, 0, 1)
			local col = ent:GetColor()
			col.r = 255
			col.g = 255 * brit
			col.b = 255 * brit
			ent:SetColor(col)
		end
	elseif entclass == "func_door_rotating" then
		if ent:GetKeyValues().damagefilter == "invul" or ent.Broken then return end

		if self.ZombieEscape then
			return
		end

		if not ent.Heal then
			local br = ent:BoundingRadius()
			if br > 80 then return end -- Don't break these kinds of doors that are bigger than this.

			local health = br * 35
			ent.Heal = health
			ent.TotalHeal = health
		end

		if gamemode.Call("ShouldAntiGrief", ent, attacker, dmginfo, ent.TotalHeal) then
			attacker:AntiGrief(dmginfo)
			if dmginfo:GetDamage() <= 0 then return end
		end

		if dmginfo:GetDamage() >= 20 and attacker:IsPlayer() and attacker:Team() == TEAM_UNDEAD then
			ent:EmitSound(math.random(2) == 1 and "npc/zombie/zombie_pound_door.wav" or "ambient/materials/door_hit1.wav")
		end

		ent.Heal = ent.Heal - dmginfo:GetDamage()
		local brit = math.Clamp(ent.Heal / ent.TotalHeal, 0, 1)
		local col = ent:GetColor()
		col.r = 255
		col.g = 255 * brit
		col.b = 255 * brit
		ent:SetColor(col)

		dispatchdamagedisplay = true

		if ent.Heal <= 0 then
			ent.Broken = true

			ent:EmitSound("Breakable.Metal")
			ent:Fire("unlock", "", 0)
			ent:Fire("open", "", 0.01) -- Trigger any area portals.
			ent:Fire("break", "", 0.1)
			ent:Fire("kill", "", 0.15)
		end
	elseif entclass == "prop_door_rotating" then
		if ent:GetKeyValues().damagefilter == "invul" or ent:HasSpawnFlags(2048) and ent:IsDoorLocked() or ent.Broken then return end

		if not ent.Heal then
			ent.Heal = ent:BoundingRadius() * 35
			ent.TotalHeal = ent.Heal
		end

		if dmginfo:GetDamage() >= 20 and attacker:IsPlayer() and attacker:Team() == TEAM_UNDEAD then
			-- If the door is lacking an `m_DoorOriginalModel` field, set it up accordingly
			-- (This could theoretically happen if the door was spawned after InitPostEntityMap)
			if not ent.m_DoorOriginalModel and ent:GetModel() then
				gamemode.Call("SetupBreakablePropDoor", ent)
			end
			-- Get the breakable door model hitsounds table for this door model (if it exists);
			-- also check first to see if `m_DoorOriginalModel` even exists on `ent` (which it always should),
			-- circumventing errors if a `prop_door_rotating` was spawned after `GM:PostEntityMap`
			local breakableDoorModelHitSounds = ent.m_DoorOriginalModel and self.BreakableDoorModelsToHitSounds[ent.m_DoorOriginalModel]
			-- Because of these hitsound tables' `__index` metamethod, indexing a key not present
			-- in the table will instead evaluate to whatever is stored at index `-1` (the default
			-- hit sound for that door, if it exists)
			local hitSound = breakableDoorModelHitSounds and breakableDoorModelHitSounds[ent:GetSkin()]
			-- Fall back to the regular ZS behavior when neither the current skin has a "pound" sound override
			-- nor a default
			if breakableDoorModelHitSounds and hitSound then
				ent:EmitSound(hitSound)
			else
				ent:EmitSound(math.random(2) == 1 and "npc/zombie/zombie_pound_door.wav" or "ambient/materials/door_hit1.wav")
			end
		end

		-- If `GM.OnlyUndeadCanGibDoors` (zs_onlyzombiescangibdoors) is enabled and the attacker is anything but,
		-- undead temporarily disable the door's native breakability (and restore in `GM:PostEntityTakeDamage`
		-- AFTER the current damage event has finished taking place)
		if self.OnlyUndeadCanGibDoors and ent:IsDoorNativelyBreakable() and (not attacker:IsPlayer() or attacker:Team() ~= TEAM_UNDEAD) then
			ent:SetSaveValue("m_bBreakable", false)
			ent.m_bWasBreakableBeforeDmg = true
		end

		if self.ZombieEscape then
			return
		end

		if gamemode.Call("ShouldAntiGrief", ent, attacker, dmginfo, ent.TotalHeal) then
			attacker:AntiGrief(dmginfo)
			if dmginfo:GetDamage() <= 0 then return end
		end

		ent.Heal = ent.Heal - dmginfo:GetDamage()
		local brit = math.Clamp(ent.Heal / ent.TotalHeal, 0, 1)
		local col = ent:GetColor()
		col.r = 255
		col.g = 255 * brit
		col.b = 255 * brit
		ent:SetColor(col)

		dispatchdamagedisplay = true

		if ent.Heal <= 0 then
			ent.Broken = true

			ent:EmitSound("Breakable.Metal")
			ent:Fire("unlock", "", 0)
			ent:Fire("open", "", 0.01) -- Trigger any area portals.
			ent:Fire("break", "", 0.1)
			ent:Fire("kill", "", 0.15)

			local physprop = ents.Create("prop_physics")
			if physprop:IsValid() then
				physprop:SetPos(ent:GetPos())
				physprop:SetAngles(ent:GetAngles())
				physprop:SetSkin(ent:GetSkin() or 0)
				physprop:SetMaterial(ent:GetMaterial())
				physprop:SetModel(ent:GetModel())
				physprop:Spawn()
				physprop:SetPhysicsAttacker(attacker)
				if attacker:IsValid() then
					local phys = physprop:GetPhysicsObject()
					if phys:IsValid() then
						phys:SetVelocityInstantaneous((physprop:NearestPoint(attacker:EyePos()) - attacker:EyePos()):GetNormalized() * math.Clamp(dmginfo:GetDamage() * 3, 40, 300))
					end
				end
				if physprop:GetMaxHealth() == 1 and physprop:Health() == 0 then
					local health = math.ceil((physprop:OBBMins():Length() + physprop:OBBMaxs():Length()) * 2)
					if health < 2000 then
						physprop.PropHealth = health
						physprop.TotalHealth = health
					end
				end
			end
		end
	elseif entclass == "func_breakable" then
		if ent:GetKeyValues().damagefilter == "invul" then return end

		if self.ZombieEscape then
			dispatchdamagedisplay = true
			return
		end

		if gamemode.Call("ShouldAntiGrief", ent, attacker, dmginfo, ent:GetMaxHealth()) then
			attacker:AntiGrief(dmginfo, true)
			if dmginfo:GetDamage() <= 0 then return end
		end

		if ent:Health() == 0 and ent:GetMaxHealth() == 1 then return end

		local brit = math.Clamp(ent:Health() / ent:GetMaxHealth(), 0, 1)
		local col = ent:GetColor()
		col.r = 255
		col.g = 255 * brit
		col.b = 255 * brit
		ent:SetColor(col)

		dispatchdamagedisplay = true
	elseif string.sub(entclass, 1, 12) == "func_physbox" then
		local holder, status = ent:GetHolder()
		if holder then status:Remove() end

		if ent:GetKeyValues().damagefilter == "invul" then return end

		if not ent.IgnoreZEProtect and self.ZombieEscape then
			dispatchdamagedisplay = true
			return
		end

		ent.Heal = ent.Heal or ent:BoundingRadius() * 35
		ent.TotalHeal = ent.TotalHeal or ent.Heal

		if gamemode.Call("ShouldAntiGrief", ent, attacker, dmginfo, ent.TotalHeal) then
			attacker:AntiGrief(dmginfo)
			if dmginfo:GetDamage() <= 0 then return end
		end

		ent.Heal = ent.Heal - dmginfo:GetDamage()
		local brit = math.Clamp(ent.Heal / ent.TotalHeal, 0, 1)
		local col = ent:GetColor()
		col.r = 255
		col.g = 255 * brit
		col.b = 255 * brit
		ent:SetColor(col)

		dispatchdamagedisplay = true

		if ent.Heal <= 0 then
			local foundaxis = false
			local entname = ent:GetName()
			local allaxis = ents.FindByClass("phys_hinge")
			for _, axis in pairs(allaxis) do
				local keyvalues = axis:GetKeyValues()
				if keyvalues.attach1 == entname or keyvalues.attach2 == entname then
					foundaxis = true
					axis:Remove()
					ent.Heal = ent.Heal + 120
				end
			end

			if not foundaxis then
				ent:Fire("break", "", 0)
			end
		end
	elseif ent:IsBarricadeProp() and attacker:IsValidLivingZombie() or ent.ZombieConstruction and attacker:IsValidLivingHuman() then
		dispatchdamagedisplay = true
	end

	local dmg = dmginfo:GetDamage()
	if dmg > 0 then
		local holder, status = ent:GetHolder()
		if holder and not holder.BuffTaut then status:Remove() end

		local dmgpos = dmginfo:GetDamagePosition()
		local hasdmgsess = attacker:IsPlayer() and attacker:HasDamageNumberSession()

		if attacker:IsPlayer() and dispatchdamagedisplay and not hasdmgsess then
			self:DamageFloater(attacker, ent, dmgpos, dmg)
		elseif hasdmgsess and dispatchdamagedisplay then
			attacker:CollectDamageNumberSession(dmg, dmgpos, ent:IsPlayer())
		end
	end
end

function GM:PostEntityTakeDamage(ent, dmginfo, wasDamageTaken)
	local entclass = ent:GetClass()

	if entclass == "prop_door_rotating" then
		-- If this door was breakable before the current damage event began...
		if ent.m_bWasBreakableBeforeDmg then
			-- Restore the door's native breakability state
			ent:SetSaveValue("m_bBreakable", true)
			ent.m_bWasBreakableBeforeDmg = nil
		end
	end
end

function GM:DamageFloater(attacker, victim, dmgpos, dmg, definiteply)
	if attacker == victim then return end
	if dmgpos == vector_origin then dmgpos = victim:NearestPoint(attacker:EyePos()) end

	net.Start((definiteply or victim:IsPlayer()) and "zs_dmg" or "zs_dmg_prop")
		if INFDAMAGEFLOATER then
			INFDAMAGEFLOATER = nil
			net.WriteUInt(9999, 16)
		else
			net.WriteUInt(math.ceil(dmg), 16)
		end
		net.WriteVector(dmgpos)
	net.Send(attacker)
end

function GM:SetRandomToZombie()
	local plays = team.GetPlayers(TEAM_HUMAN)
	local pl = plays[math.random(#plays)]

	if not pl then return end

	pl:ChangeTeam(TEAM_UNDEAD)
	pl:SetFrags(0)
	pl:SetDeaths(0)

	self.StartingZombie[pl:UniqueID()] = true
	self.PreviouslyDied[pl:UniqueID()] = CurTime()
	pl:UnSpectateAndSpawn()

	return pl
end

function GM:PreOnPlayerChangedTeam(pl, oldteam, newteam)
	--[[if oldteam == TEAM_HUMAN then
		self:SaveVault(pl)
	end]]
end

function GM:OnPlayerChangedTeam(pl, oldteam, newteam)
	if newteam == TEAM_UNDEAD then
		pl:SetPoints(0)

		--pl.WaveBarricadeDamage = 0
		--pl.WaveHumanDamage = 0
		pl.DamagedBy = {}

		pl:SetBarricadeGhosting(false)
		self.CheckedOut[pl:UniqueID()] = true
	elseif newteam == TEAM_HUMAN then
		self.PreviouslyDied[pl:UniqueID()] = nil

		if self.PointSaving > 0 and pl.PointsVault ~= nil and not self.ZombieEscape and not self:IsClassicMode() then
			pl:SetPoints(math.floor(pl.PointsVault))
		else
			pl:SetPoints(0)
		end

		self:RefreshItemStocks(pl)
	end

	if newteam ~= TEAM_HUMAN then
		pl:RemoveSkills()
	end

	pl:SetLastAttacker(nil)
	for _, p in pairs(player.GetAll()) do
		if p.LastAttacker == pl then
			p.LastAttacker = nil
		end
	end

	pl.PointQueue = 0

	timer.Simple(0, function() gamemode.Call("CalculateInfliction") end)
end

function GM:SetToDefaultZombieClass(pl)
	if pl:Team() == TEAM_UNDEAD then
		pl:KillSilent()
		pl:SetZombieClass(self.DefaultZombieClass or 1)
		pl:UnSpectateAndSpawn()
	else
		pl:SetZombieClass(self.DefaultZombieClass or 1)
	end
end

function GM:SetPantsMode(mode)
	if self.ZombieEscape then return end

	self.PantsMode = mode and self.ZombieClasses["Zombie Legs"] ~= nil and not self:IsClassicMode() and not self:IsBabyMode()

	if self.PantsMode then
		local index = self.ZombieClasses["Zombie Legs"].Index

		self.PreOverrideDefaultZombieClass = self.PreOverrideDefaultZombieClass or self.DefaultZombieClass
		self.DefaultZombieClass = index

		for _, pl in pairs(player.GetAll()) do
			local classname = pl:GetZombieClassTable().Name
			if classname ~= "Zombie Legs" and classname ~= "Crow" then
				if pl:Team() == TEAM_UNDEAD then
					pl:KillSilent()
					pl:SetZombieClass(index)
					pl:UnSpectateAndSpawn()
				else
					pl:SetZombieClass(index)
				end
			end
			pl.DeathClass = index
		end
	else
		self.DefaultZombieClass = self.PreOverrideDefaultZombieClass or self.DefaultZombieClass

		for _, pl in pairs(player.GetAll()) do
			if pl:GetZombieClassTable().Name == "Zombie Legs" then
				self:SetToDefaultZombieClass(pl)
			end
		end
	end
end

function GM:SetClassicMode(mode)
	if self.ZombieEscape then return end

	self.ClassicMode = mode and self.ZombieClasses["Classic Zombie"] ~= nil and not self.PantsMode and not self:IsBabyMode()

	SetGlobalBool("classicmode", self.ClassicMode)

	if self:IsClassicMode() then
		util.RemoveAll("prop_nail")

		local index = self.ZombieClasses["Classic Zombie"].Index

		self.PreOverrideDefaultZombieClass = self.PreOverrideDefaultZombieClass or self.DefaultZombieClass
		self.DefaultZombieClass = index

		for _, pl in pairs(player.GetAll()) do
			local classname = pl:GetZombieClassTable().Name
			if classname ~= "Classic Zombie" and classname ~= "Crow" then
				if pl:Team() == TEAM_UNDEAD then
					pl:KillSilent()
					pl:SetZombieClass(index)
					pl:UnSpectateAndSpawn()
				else
					pl:SetZombieClass(index)
				end
			end
			pl.DeathClass = index
		end
	else
		self.DefaultZombieClass = self.PreOverrideDefaultZombieClass or self.DefaultZombieClass

		for _, pl in pairs(player.GetAll()) do
			if pl:GetZombieClassTable().Name == "Classic Zombie" then
				self:SetToDefaultZombieClass(pl)
			end
		end
	end
end

function GM:SetBabyMode(mode)
	if self.ZombieEscape then return end

	self.BabyMode = mode and self.ZombieClasses["Gore Child"] ~= nil and not self.PantsMode and not self:IsClassicMode()

	SetGlobalBool("babymode", self.BabyMode)

	if self:IsBabyMode() then
		local index = self.ZombieClasses["Gore Child"].Index

		self.PreOverrideDefaultZombieClass = self.PreOverrideDefaultZombieClass or self.DefaultZombieClass
		self.DefaultZombieClass = index

		for _, pl in pairs(player.GetAll()) do
			local classname = pl:GetZombieClassTable().Name
			if classname ~= "Gore Child" and classname ~= "Giga Gore Child" and classname ~= "Crow" then
				if pl:Team() == TEAM_UNDEAD then
					pl:KillSilent()
					pl:SetZombieClass(index)
					pl:UnSpectateAndSpawn()
				else
					pl:SetZombieClass(index)
				end
			end
			pl.DeathClass = index
		end
	else
		self.DefaultZombieClass = self.PreOverrideDefaultZombieClass or self.DefaultZombieClass

		for _, pl in pairs(player.GetAll()) do
			if pl:GetZombieClassTable().Name == "Gore Child" then
				self:SetToDefaultZombieClass(pl)
			end
		end
	end
end

GM.InitialVolunteers = {}
function GM:SetClosestsToZombie()
	local allplayers = player.GetAllActive()
	local numplayers = #allplayers
	if numplayers <= 1 then return end

	local desiredzombies = self:GetDesiredStartingZombies()

	self:SortZombieSpawnDistances(allplayers)

	local zombies = {}
	for _, pl in pairs(allplayers) do
		if pl:Team() ~= TEAM_HUMAN or not pl:Alive() then
			table.insert(zombies, pl)
		end
	end

	-- Need to place some people back on the human team.
	if #zombies > desiredzombies then
		local toswap = #zombies - desiredzombies
		for _, pl in pairs(zombies) do
			if pl.DiedDuringWave0 and pl:GetInfo("zs_alwaysvolunteer") ~= "1" and not pl.IsZSBot then
				pl:ChangeTeam(TEAM_HUMAN)
				pl:UnSpectateAndSpawn()
				toswap = toswap - 1
				if toswap <= 0 then
					break
				end
			end
		end
	end

	for i = 1, desiredzombies do
		local pl = allplayers[i]
		if pl:Team() ~= TEAM_UNDEAD then
			pl:ChangeTeam(TEAM_UNDEAD)
			self.PreviouslyDied[pl:UniqueID()] = CurTime()
			self.InitialVolunteers[pl:UniqueID()] = true
		end
		pl:SetFrags(0)
		pl:SetDeaths(0)

		local unlocked = {}
		for _, v in ipairs(self.ZombieClasses) do
			if v.Unlocked and not v.Hidden and v.NotRandomStart then
				unlocked[#unlocked + 1] = v.Index
			end
		end
		pl:SetZombieClass(unlocked[math.random(#unlocked)])

		self.StartingZombie[pl:UniqueID()] = true
		pl:UnSpectateAndSpawn()
	end
end

function GM:AllowPlayerPickup(pl, ent)
	return false
end

function GM:PlayerShouldTakeDamage(pl, attacker)
	if attacker.PBAttacker and attacker.PBAttacker:IsValid() and CurTime() < attacker.NPBAttacker then -- Protection against prop_physbox team killing. physboxes don't respond to SetPhysicsAttacker()
		attacker = attacker.PBAttacker
	end

	if attacker:IsPlayer() and attacker ~= pl and not attacker.AllowTeamDamage and not pl.AllowTeamDamage and attacker:Team() == pl:Team() then return false end

	return true
end

function GM:PlayerHurt(victim, attacker, healthremaining, damage)
	if healthremaining < 1 then return end

	if victim:Team() == TEAM_HUMAN then
		victim:PlayPainSound()

		if healthremaining < 75 then
			victim:ResetSpeed(nil, healthremaining)
		end

		if healthremaining < victim:GetMaxHealth() * 0.5 and victim:GetBloodArmor() < victim.MaxBloodArmor + 10 and victim:HasTrinket("bloodpack") then
			victim:SetBloodArmor(math.min(victim:GetBloodArmor() + (20 * victim.BloodarmorGainMul), victim.MaxBloodArmor + (20 * victim.MaxBloodArmorMul)))
			victim:TakeInventoryItem("trinket_bloodpack")

			net.Start("zs_trinketconsumed")
				net.WriteString("Blood Transfusion Pack")
			net.Send(victim)
		end
	else
		victim:PlayZombiePainSound()
	end
end

function GM:WeaponDeployed(pl, wep)
	self:DoChangeDeploySpeed(wep)

	-- Don't change speed instantly to stop people from shooting and then running away with a faster weapon.
	local timername = tostring(pl).."speedchange"
	timer.Remove(timername)

	local speed = pl:ResetSpeed(true) -- Determine what speed we SHOULD get without actually setting it.
	if speed < pl:GetMaxSpeed() then
		pl:SetSpeed(speed)
	elseif pl:GetMaxSpeed() < speed then
		local unbound = pl:IsSkillActive(SKILL_UNBOUND) and 0.4 or 1

		timer.Create(timername, (0.333 / (pl.DeploySpeedMultiplier or 1)) * unbound, 1, function() if pl:IsValid() then pl:SetHumanSpeed(speed) end end)
	end
end

function GM:KeyPress(pl, key)
	if key == IN_USE then
		if pl:Team() == TEAM_HUMAN and pl:Alive() then
			if pl:IsCarrying() then
				pl.status_human_holding:OnRemove() -- No idea...
				pl.status_human_holding:RemoveNextFrame()
			else
				self:TryHumanPickup(pl, pl:TraceLine(64).Entity)
			end
		end
	elseif key == IN_SPEED then
		if pl:Alive() then
			if pl:Team() == TEAM_HUMAN then
				pl:DispatchAltUse()

				if not pl:IsCarrying() and pl:KeyPressed(IN_SPEED) and pl:IsSkillActive(SKILL_CARDIOTONIC) and pl:GetBloodArmor() > 0 then
					pl:SetBloodArmor(pl:GetBloodArmor() - 1)
					pl:EmitSound("player/suit_sprint.wav", 50)
					if pl:GetBloodArmor() == 0 and pl:IsSkillActive(SKILL_BLOODLETTER) then
						local bleed = pl:GiveStatus("bleed")
						if bleed and bleed:IsValid() then
							bleed:AddDamage(5)
							bleed.Damager = pl
						end
					end
					pl:ResetSpeed()
				end
			elseif pl:Team() == TEAM_UNDEAD then
				pl:CallZombieFunction0("AltUse")
			end
		end
	elseif key == IN_ZOOM then
		if pl:Team() == TEAM_HUMAN and pl:Alive() and not self.ZombieEscape then
			if pl:IsOnGround() then
				pl.LastGhostFailureVelocity = nil
				pl:SetBarricadeGhosting(true)
			else
				local plvel = pl:GetVelocity()
				if pl:GetPhysicsObject():IsPenetrating() then
					if (plvel.x == 0 and plvel.y == 0 and (plvel.z == -4.5 or plvel.z == 0)) then
						pl.LastGhostFailureVelocity = nil
						pl:SetBarricadeGhosting(true)
					else
						pl:SetLocalVelocity(vector_origin)
					end
				elseif pl.LastGhostFailureVelocity == plvel then
					pl.LastGhostFailureVelocity = nil
					pl:SetBarricadeGhosting(true)
				else
					pl.LastGhostFailureVelocity = plvel
				end
			end
		end
	end
end

function GM:GetNearestSpawn(pos, teamid)
	local nearest = NULL

	local nearestdist = math.huge
	for _, ent in pairs(team.GetValidSpawnPoint(teamid)) do
		if ent.Disabled then continue end

		local dist = ent:GetPos():DistToSqr(pos)
		if dist < nearestdist then
			nearestdist = dist
			nearest = ent
		end
	end

	return nearest
end

function GM:EntityWouldBlockSpawn(ent)
	local spawnpoint = self:GetNearestSpawn(ent:GetPos(), TEAM_UNDEAD)

	if spawnpoint:IsValid() then
		local spawnpos = spawnpoint:GetPos()
		if spawnpos:DistToSqr(ent:NearestPoint(spawnpos)) <= 1600 then return true end
	end

	return false
end

function GM:GetNearestSpawnDistance(pos, teamid)
	local nearest = self:GetNearestSpawn(pos, teamid)
	if nearest:IsValid() then
		return nearest:GetPos():Distance(pos)
	end

	return -1
end

function GM:ShutDown()
	self:SaveAllVaults()
end

function GM:PlayerUse(pl, ent)
	if not pl:Alive() or pl:Team() == TEAM_UNDEAD and pl:GetZombieClassTable().NoUse or pl:GetBarricadeGhosting() then return false end

	if pl:IsHolding() and pl:GetHolding() ~= ent then return false end

	local entclass = ent:GetClass()
	if entclass == "prop_door_rotating" then
		if CurTime() < (ent.m_AntiDoorSpam or 0) then -- Prop doors can be glitched shut by mashing the use button.
			return false
		end
		ent.m_AntiDoorSpam = CurTime() + 0.85
	elseif entclass == "item_healthcharger" then
		if pl:Team() == TEAM_UNDEAD then
			return false
		elseif pl:IsSkillActive(SKILL_D_FRAIL) and pl:Health() >= math.floor(pl:GetMaxHealth() * 0.25) then
			return false
		end
	elseif pl:Team() == TEAM_HUMAN and not pl:IsCarrying() and pl:KeyPressed(IN_USE) then
		self:TryHumanPickup(pl, ent)
	end

	return true
end

function GM:PlayerDeath(pl, inflictor, attacker)
end

function GM:PlayerDeathSound()
	return true
end

local function SortDist(pa, pb)
	return pa._temp < pb._temp
end
function GM:CanPlayerSuicide(pl)
	if self.RoundEnded or pl:HasWon() then return false end

	if pl:Team() == TEAM_HUMAN then
		if self:GetWave() <= self.NoSuicideWave then
			pl:PrintTranslatedMessage(HUD_PRINTCENTER, "give_time_before_suicide")
			return false
		end

		-- If a person is going to suicide with no last attacker, give the kill to the closest zombie.
		if not IsValid(pl:GetLastAttacker()) then
			local plpos = pl:EyePos()
			local tosort = {}
			for _, zom in pairs(team.GetPlayers(TEAM_UNDEAD)) do
				if zom:Alive() then
					local dist = zom:GetPos():DistToSqr(plpos)
					if dist <= 262144 then --512^2
						zom._temp = dist
						table.insert(tosort, zom)
					end
				end
			end

			table.sort(tosort, SortDist)

			if tosort[1] then
				pl:SetLastAttacker(tosort[1])
			end
		end
	elseif pl:Team() == TEAM_UNDEAD then
		local ret = pl:CallZombieFunction0("CanPlayerSuicide")
		if ret == false then return false end
	end

	return pl:GetObserverMode() == OBS_MODE_NONE and pl:Alive() and (not pl.SpawnNoSuicide or pl.SpawnNoSuicide < CurTime())
end

function GM:DefaultRevive(pl)
	local status = pl:GiveStatus("revive")
	if status and status:IsValid() then
		status:SetReviveTime(CurTime() + 2)
	end
end

function GM:HumanKilledZombie(pl, attacker, inflictor, dmginfo, headshot, suicide)
	if (pl:GetZombieClassTable().Points or 0) == 0 or self.RoundEnded then return end

	-- Simply distributes based on damage but also do some stuff for assists.

	local totaldamage = 0
	for otherpl, dmg in pairs(pl.DamagedBy) do
		if otherpl:IsValid() and otherpl:Team() == TEAM_HUMAN then
			totaldamage = totaldamage + dmg
		end
	end

	local mostassistdamage = 0
	local halftotaldamage = totaldamage / 2
	local mostdamager
	for otherpl, dmg in pairs(pl.DamagedBy) do
		if otherpl ~= attacker and otherpl:IsValid() and otherpl:Team() == TEAM_HUMAN and dmg > mostassistdamage and dmg >= halftotaldamage then
			mostassistdamage = dmg
			mostdamager = otherpl
		end
	end

	attacker.ZombiesKilled = attacker.ZombiesKilled + 1

	if mostdamager then
		attacker:PointCashOut(pl, FM_LOCALKILLOTHERASSIST)
		mostdamager:PointCashOut(pl, FM_LOCALASSISTOTHERKILL)

		mostdamager.ZombiesKilledAssists = mostdamager.ZombiesKilledAssists + 1
	else
		attacker:PointCashOut(pl, FM_NONE)
	end

	if inflictor:IsValid() and inflictor == attacker:GetActiveWeapon() then
		local wep = attacker:GetActiveWeapon()
		if attacker.MeleeMovementSpeedOnKill and attacker.MeleeMovementSpeedOnKill ~= 0 and wep.IsMelee then
			local boost = attacker:GiveStatus("adrenalineamp", 10)
			if boost and boost:IsValid() then
				boost:SetSpeed(attacker.MeleeMovementSpeedOnKill)
			end
		end

		if #self.Food > 0 and pl.ChefMarkTime and pl.ChefMarkTime > CurTime() and pl.ChefMarkOwner == attacker then
			local rfood = self.Food[math.random(#self.Food)]
			if not attacker:HasWeapon(rfood) then
				attacker:Give(rfood)
			end
		end

		if pl:WasHitInHead() then
			attacker.Headshots = (attacker.Headshots or 0) + 1
		end

		GAMEMODE.StatTracking:IncreaseElementKV(STATTRACK_TYPE_WEAPON, wep:GetClass(), "Kills", 1)

		if wep.OnZombieKilled then
			wep:OnZombieKilled(pl, totaldamage, dmginfo)
		end
	end

	gamemode.Call("PostHumanKilledZombie", pl, attacker, inflictor, dmginfo, mostdamager, mostassistdamage, headshot)

	return mostdamager
end

function GM:PostHumanKilledZombie(pl, attacker, inflictor, dmginfo, assistpl, assistamount, headshot)
end

function GM:ZombieKilledHuman(pl, attacker, inflictor, dmginfo, headshot, suicide)
	if self.RoundEnded then return end

	local plpos = pl:GetPos()
	local dist = 999999999
	local xp = 18 * (GAMEMODE.ZombieXPMulti or 1)
	for _, ent in pairs(team.GetValidSpawnPoint(TEAM_UNDEAD)) do
		dist = math.min(ent:GetPos():DistToSqr(plpos), dist)
	end
	pl.ZombieSpawnDeathDistance = math.ceil(math.sqrt(dist))

	attacker:AddBrains(1)
	attacker:AddLifeBrainsEaten(1)
	attacker:AddZSXP(self.InitialVolunteers[attacker:UniqueID()] and xp or math.floor(xp/4))

	local classtab = attacker:GetZombieClassTable()
	if classtab and classtab.Name then
		GAMEMODE.StatTracking:IncreaseElementKV(STATTRACK_TYPE_ZOMBIECLASS, classtab.Name, "BrainsEaten", 1)
	end

	if not pl.Gibbed and not suicide then
		local status = pl:GiveStatus("revive_slump_human")
		if status then
			status:SetReviveTime(CurTime() + 4)
			status:SetZombieInitializeTime(CurTime() + 2)
		end

		pl:SetZombieClassName(self.ZombieEscape and "Super Zombie" or self:IsClassicMode() and "Classic Zombie" or self:IsBabyMode() and "Gore Child" or "Fresh Dead")
	end

	gamemode.Call("PostZombieKilledHuman", pl, attacker, inflictor, dmginfo, headshot, suicide)

	return attacker:Frags()
end

function GM:PostZombieKilledHuman(pl, attacker, inflictor, dmginfo, headshot, suicide)
end

local function DelayedChangeToZombie(pl)
	if pl:IsValid() then
		if pl.ChangeTeamFrags then
			pl:SetFrags(pl.ChangeTeamFrags)
			pl.ChangeTeamFrags = 0
		end

		pl:ChangeTeam(TEAM_UNDEAD)
	end
end
function GM:DoPlayerDeath(pl, attacker, dmginfo)
	pl:RemoveEphemeralStatuses()
	pl:Extinguish()
	pl:SetPhantomHealth(0)

	local inflictor = dmginfo:GetInflictor()
	local plteam = pl:Team()
	local ct = CurTime()
	local suicide = attacker == pl or attacker:IsWorld()

	if attacker.PBAttacker and attacker.PBAttacker:IsValid() then
		attacker = attacker.PBAttacker
	end

	pl:Freeze(false)

	local headshot = pl:WasHitInHead()

	if suicide then attacker = pl:GetLastAttacker() or attacker end
	pl:SetLastAttacker()

	if inflictor == NULL then inflictor = attacker end

	if inflictor == attacker and attacker:IsPlayer() then
		local wep = attacker:GetActiveWeapon()
		if wep:IsValid() then
			inflictor = wep
		end
	end

	if headshot then
		local effectdata = EffectData()
			effectdata:SetOrigin(dmginfo:GetDamagePosition())
			local force = dmginfo:GetDamageForce()
			effectdata:SetMagnitude(force:Length() * 3)
			effectdata:SetNormal(force:GetNormalized())
			effectdata:SetEntity(pl)
		util.Effect("headshot", effectdata, true, true)
	end

	if not pl:CallZombieFunction5("OnKilled", attacker, inflictor, suicide, headshot, dmginfo) then
		if pl:Health() <= -70 and not pl.NoGibs and not self.ZombieEscape then
			pl:Gib(dmginfo)
		elseif not pl.KnockedDown then
			pl:CreateRagdoll()
		end
	end

	pl:RemoveStatus("overridemodel", false, true)

	local revive = false
	local assistpl
	if plteam == TEAM_UNDEAD then
		if GAMEMODE.ZombieEscape then
			local zewep = pl:GetWeapon("weapon_knife")
			if zewep and zewep:IsValid() then
				pl:DropWeapon(zewep)
			end
		end

		local classtable = pl:GetZombieClassTable()

		pl:PlayZombieDeathSound()

		if classtable.Boss and not self.ObjectiveMap and pl.BossDeathNotification then
			net.Start("zs_boss_slain")
				net.WriteEntity(pl)
				net.WriteUInt(classtable.Index, 8)
			net.Broadcast()

			timer.Simple(0, function()
				pl:MakeBossDrop()
			end)

			pl.BossDeathNotification = nil
		end

		if not classtable.NoDeaths then
			pl:AddDeaths(1)
		end

		if self:GetWaveActive() then
			pl.StartSpectating = ct + 2
		else
			pl.StartCrowing = ct + 3
		end

		if attacker:IsValid() and attacker:IsPlayer() and attacker ~= pl then
			if classtable.Revives and not pl.Gibbed and not headshot and CurTime() > pl.LastRevive + 4 then
				if classtable.ReviveCallback then
					revive = classtable:ReviveCallback(pl, attacker, dmginfo)
				elseif math.random(4) ~= 1 then
					self:DefaultRevive(pl)
					revive = true
				end
			end

			if revive then
				pl.LastRevive = CurTime()
			elseif attacker:Team() == TEAM_HUMAN then
				assistpl = gamemode.Call("HumanKilledZombie", pl, attacker, inflictor, dmginfo, headshot, suicide)
			end
		end

		if not revive and (pl.LifeBarricadeDamage ~= 0 or pl.LifeHumanDamage ~= 0 or pl.LifeBrainsEaten ~= 0) then
			timer.Simple(0, function() if pl:IsValid() then pl:SendLifeStats() end end)
		end

		pl:CallZombieFunction5("PostOnKilled", attacker, inflictor, suicide, headshot, dmginfo)
	elseif plteam == TEAM_HUMAN then
		pl.NextSpawnTime = ct + 4

		pl:PlayDeathSound()

		if attacker:IsPlayer() and attacker ~= pl then
			gamemode.Call("ZombieKilledHuman", pl, attacker, inflictor, dmginfo, headshot, suicide)
		end

		pl:DropAll()
		timer.Simple(0, function() DelayedChangeToZombie(pl) end) -- We don't want people shooting barrels near teammates.
		self.PreviouslyDied[pl:UniqueID()] = CurTime()
		if self:GetWave() == 0 then
			pl.DiedDuringWave0 = true
		end

		local frags = pl:Frags()
		if frags < 0 then
			pl.ChangeTeamFrags = math.ceil(frags / 5)
		else
			pl.ChangeTeamFrags = 0
		end

		if pl.SpawnedTime then
			pl.SurvivalTime = math.max(ct - pl.SpawnedTime, pl.SurvivalTime or 0)
			pl.SpawnedTime = nil
		end

		if team.NumPlayers(TEAM_HUMAN) <= 1 then
			self.LastHumanPosition = pl:WorldSpaceCenter()

			net.Start("zs_lasthumanpos")
				net.WriteVector(self.LastHumanPosition)
			net.Broadcast()
		end

		local hands = pl:GetHands()
		if IsValid(hands) then
			hands:Remove()
		end
	end

	if revive or pl:CallZombieFunction2("NoDeathMessage", attacker, dmginfo) or pl:IsSpectator() then return end

	if attacker == pl then
		net.Start("zs_pl_kill_self")
			net.WriteEntity(pl)
			net.WriteUInt(plteam, 8)
		net.Broadcast()
	elseif attacker:IsPlayer() then
		if assistpl then
			net.Start("zs_pls_kill_pl")
				net.WriteEntity(pl)
				net.WriteEntity(attacker)
				net.WriteEntity(assistpl)
				net.WriteString(inflictor:GetClass())
				net.WriteUInt(plteam, 8)
				net.WriteUInt(attacker:Team(), 8) -- Assuming assistants are always on the same team.
				net.WriteBit(headshot)
			net.Broadcast()

			gamemode.Call("PlayerKilledByPlayer", pl, assistpl, inflictor, headshot, dmginfo, true)
		else
			net.Start("zs_pl_kill_pl")
				net.WriteEntity(pl)
				net.WriteEntity(attacker)
				net.WriteString(inflictor:GetClass())
				net.WriteUInt(plteam, 8)
				net.WriteUInt(attacker:Team(), 8)
				net.WriteBit(headshot)
			net.Broadcast()
		end

		gamemode.Call("PlayerKilledByPlayer", pl, attacker, inflictor, headshot, dmginfo)
	else
		net.Start("zs_death")
			net.WriteEntity(pl)
			net.WriteString(inflictor:GetClass())
			net.WriteString(attacker:GetClass())
			net.WriteUInt(plteam, 8)
		net.Broadcast()
	end
end

function GM:WeaponEquip(wep)
	if wep.m_WeaponDeploySpeed then
		timer.Simple(0, function() GAMEMODE:DoChangeDeploySpeed(wep) end)
	end
end

function GM:PlayerKilledByPlayer(pl, attacker, inflictor, headshot, dmginfo, is_assistant)
end

function GM:PlayerCanPickupWeapon(pl, ent)
	if pl:IsSpectator() then return false end

	if pl:Team() == TEAM_UNDEAD then return ent:GetClass() == pl:GetZombieClassTable().SWEP end

	return not ent.ZombieOnly
end

function GM:PlayerCanPickupItem(pl, ent)
	if pl:IsSkillActive(SKILL_D_FRAIL) then
		local class = ent:GetClass()
		if class == "item_healthkit" or class == "item_healthvial" then
			local healamount = #class == 14 and 25 or 10
			if pl:Health() + healamount > math.floor(pl:GetMaxHealth() * 0.25) then
				return false
			end
		end
	end

	return true
end

-- This function is only for footsteps for players not in the local player's pvs or something.
-- The cl_init.lua version usually overrides this number so I just set it to a static number to save cycles.
function GM:PlayerStepSoundTime(pl, iType, bWalking)
	return 350
end

-- Again, don't bother overriding anything due to above.
function GM:PlayerFootstep(pl, vPos, iFoot, strSoundName, fVolume, pFilter)
end

local VoiceSetTranslate = {}
VoiceSetTranslate["models/player/alyx.mdl"] = VOICESET_ALYX
VoiceSetTranslate["models/player/barney.mdl"] = VOICESET_BARNEY
VoiceSetTranslate["models/player/combine_soldier.mdl"] = VOICESET_COMBINE
VoiceSetTranslate["models/player/combine_soldier_prisonguard.mdl"] = VOICESET_COMBINE
VoiceSetTranslate["models/player/combine_super_soldier.mdl"] = VOICESET_COMBINE
VoiceSetTranslate["models/player/police.mdl"] = VOICESET_COMBINE
VoiceSetTranslate["models/grim.mdl"] = VOICESET_COMBINE
VoiceSetTranslate["models/player/police_fem.mdl"] = VOICESET_COMBINE
VoiceSetTranslate["models/player/monk.mdl"] = VOICESET_MONK
VoiceSetTranslate["models/jason278-players/gabe_3.mdl"] = VOICESET_MONK
VoiceSetTranslate["models/player/mossman.mdl"] = VOICESET_FEMALE
VoiceSetTranslate["models/player/brsp.mdl"] = VOICESET_FEMALE
VoiceSetTranslate["models/player/moe_glados_p.mdl"] = VOICESET_FEMALE
VoiceSetTranslate["models/player/mossman_arctic.mdl"] = VOICESET_FEMALE
VoiceSetTranslate["models/player/p2_chell.mdl"] = VOICESET_FEMALE
VoiceSetTranslate["models/dawson/obese_male_deluxe/obese_male_deluxe.mdl"] = VOICESET_MONK
VoiceSetTranslate["models/player/cirno/cirno_player.mdl"] = VOICESET_FEMALE
VoiceSetTranslate["models/player/dewobedil/eromanga_sensei/sagiri/pajama_p.mdl"] = VOICESET_FEMALE
VoiceSetTranslate["models/loyalists/mmd/remilia/remilia_mp_pm.mdl"] = VOICESET_FEMALE
VoiceSetTranslate["models/loyalists/mmd/flandre/flandre_mp_pm.mdl"] = VOICESET_FEMALE
VoiceSetTranslate["models/jazzmcfly/kantai/yuudachi/yuudachi.mdl"] = VOICESET_FEMALE
VoiceSetTranslate["models/player/dewobedil/vocaloid/haku/bikini_p.mdl"] = VOICESET_FEMALE
VoiceSetTranslate["models/player/dewobedil/touhou/junko/default_p.mdl"] = VOICESET_FEMALE
function GM:PlayerSpawn(pl)
	pl:StripWeapons()
	pl:WipePlayerInventory()
	pl:GiveAmmo(1, "dummy", true) -- Fixes empty weapon deploy bug.
	pl:RemoveStatus("confusion", false, true)
	pl:RemoveFlags(FL_ONGROUND) -- fixes :OnGround() returning true on spawn even if they're not on the ground.

	if pl:GetMaterial() ~= "" then
		pl:SetMaterial("")
	end

	pl:UnSpectate()

	pl.StartCrowing = nil
	pl.StartSpectating = nil
	pl.NextSpawnTime = nil
	pl.Gibbed = nil

	pl.SpawnNoSuicide = CurTime() + 1
	pl.SpawnedTime = CurTime()

	pl:ShouldDropWeapon(false)

	pl:SetLegDamage(0)
	pl:SetLastAttacker()

	local pcol = Vector(pl:GetInfo("cl_playercolor"))
	pcol.x = math.Clamp(pcol.x, 0, 2.5)
	pcol.y = math.Clamp(pcol.y, 0, 2.5)
	pcol.z = math.Clamp(pcol.z, 0, 2.5)
	pl:SetPlayerColor(pcol)

	if pl:Team() == TEAM_UNDEAD then
		if pl.ActivatedHumanSkills then
			pl.ActivatedHumanSkills = false
			pl:ApplySkills({})
		end

		if not pl.Revived then
			pl.DamagedBy = {}
		end

		pl.LifeBarricadeDamage = 0
		pl.LifeHumanDamage = 0
		pl.LifeBrainsEaten = 0

		pl.BossHealRemaining = nil

		if self:GetUseSigils() and self:GetEscapeSequence() and self:GetEscapeStage() >= ESCAPESTAGE_BOSS and not pl.Revived then
			pl:SetZombieClassName("Super Zombie")
		elseif pl.DeathClass and self:GetWaveActive() then
			pl:SetZombieClass(self:GetBestAvailableZombieClass(pl.DeathClass))
			pl.DeathClass = nil
		end

		local cur = pl:GetZombieClassTable().Name
		local best = self:GetBestAvailableZombieClass(cur)
		if cur ~= best then
			pl:SetZombieClass(best)
		end

		local classtab = pl:GetZombieClassTable()
		pl:DoHulls(pl:GetZombieClass(), TEAM_UNDEAD)
		--pl:SetCustomCollisionCheck(pl.NoCollideAll == true)
		pl:CollisionRulesChanged()

		if classtab.Model then
			pl:SetModel(classtab.Model)
		elseif classtab.UsePlayerModel then
			local mdl = player_manager.TranslatePlayerModel(pl:GetInfo("cl_playermodel"))
			if table.HasValue(self.RestrictedModels, mdl) then
				pl:SelectRandomPlayerModel()
			else
				pl:SetModel(mdl)
			end
		elseif classtab.UsePreviousModel then
			local curmodel = string.lower(pl:GetModel())
			if table.HasValue(self.RestrictedModels, curmodel) or (not VoiceSetTranslate[curmodel] and string.sub(curmodel, 1, 14) ~= "models/player") then
				pl:SelectRandomPlayerModel()
			end
		elseif classtab.UseRandomModel then
			pl:SelectRandomPlayerModel()
		else
			pl:SetModel("models/player/zombie_classic_hbfix.mdl")
		end

		if classtab.NoPlayerColor then
			pl:SetColor(COLOR_WHITE)
			pl:SetPlayerColor(Vector(255, 255, 255))
		end

		if classtab.Boss then
			pl:SetHealth(classtab.Health)
		else
			local lowundead = team.NumPlayers(TEAM_UNDEAD) < 4

			local healthmulti = (self.ObjectiveMap or self.ZombieEscape) and 1 or lowundead and 1.5 or 1
			pl:SetHealth(classtab.Health * healthmulti)
		end

		if classtab.SWEP then
			pl:Give(classtab.SWEP)
		end

		pl:SetNoTarget(true)
		pl:SetMaxHealth(1)

		pl:ResetSpeed()
		pl:SetCrouchedWalkSpeed(classtab.CrouchedWalkSpeed or 0.45)

		if not pl.Revived or not self:GetWaveActive() or CurTime() > self:GetWaveEnd() then
			pl.StartCrowing = 0
		end

		if pl.ForceSpawnAngles then
			pl:SetEyeAngles(pl.ForceSpawnAngles)
			pl.ForceSpawnAngles = nil
		end

		if not pl.Revived and not pl:GetZombieClassTable().NeverAlive and pl.SpawnedOnSpawnPoint and not pl.DidntSpawnOnSpawnPoint then
			pl:GiveStatus("zombiespawnbuff", self.ObjectiveMap and 1.5 or 3)
		end
		pl.DidntSpawnOnSpawnPoint = nil
		pl.SpawnedOnSpawnPoint = nil

		local overridemodel = pl:GetZombieClassTable().OverrideModel
		if overridemodel then
			local current = pl:GiveStatus("overridemodel")
			if current and current:IsValid() then
				current:SetModel(overridemodel)
				current:ResetBones()
				pl:CallZombieFunction1("ManipulateOverrideModel", current)
			end
		else
			pl:RemoveStatus("overridemodel", false, true)
		end

		local oldhands = pl:GetHands()
		if IsValid(oldhands) then
			oldhands:Remove()
		end

		GAMEMODE.StatTracking:IncreaseElementKV(STATTRACK_TYPE_ZOMBIECLASS, classtab.Name, "ClassSpawn", 1)

		pl:CallZombieFunction0("OnSpawned")
	elseif pl:Team() == TEAM_HUMAN then
		pl.PointQueue = 0
		pl.PackedItems = {}
		pl:ClearUselessDamage()

		local desiredname = pl:GetInfo("cl_playermodel")
		local modelname = player_manager.TranslatePlayerModel(#desiredname == 0 and self.RandomPlayerModels[math.random(#self.RandomPlayerModels)] or desiredname)
		local lowermodelname = string.lower(modelname)
		if table.HasValue(self.RestrictedModels, lowermodelname) then
			modelname = "models/player/alyx.mdl"
			lowermodelname = modelname
		end
		pl:SetModel(modelname)

		-- Cache the voice set.
		if VoiceSetTranslate[lowermodelname] then
			pl:SetDTInt(DT_PLAYER_INT_VOICESET, VoiceSetTranslate[lowermodelname])
		elseif string.find(lowermodelname, "female", 1, true) then
			pl:SetDTInt(DT_PLAYER_INT_VOICESET, VOICESET_FEMALE)
		else
			pl:SetDTInt(DT_PLAYER_INT_VOICESET, VOICESET_MALE)
		end

		--pl.HumanSpeedAdder = nil

		pl:SetNoTarget(false)
		pl:SetMaxHealth(100)
		--pl:SetCustomCollisionCheck(false)
		pl:CollisionRulesChanged()

		if not self.NoSkills then
			pl.ActivatedHumanSkills = true
			pl.AdjustedStartPointsSkill = nil
			pl.AdjustedStartScrapSkill = nil
			pl:ApplySkills()
		end

		pl.StowageCaches = 0

		net.Start("zs_stowagecaches")
			net.WriteInt(pl.StowageCaches, 8)
		net.Send(pl)

		pl:ResetSpeed()
		pl:ResetJumpPower()
		pl:SetCrouchedWalkSpeed(0.45)

		pl:SetViewOffset(DEFAULT_VIEW_OFFSET)
		pl:SetViewOffsetDucked(DEFAULT_VIEW_OFFSET_DUCKED)

		if self.ZombieEscape then
			local randomprimary = table.Random(self.ZombieEscapeWeaponsPrimary)
			local randomsecondary = table.Random(self.ZombieEscapeWeaponsSecondary)
			
			pl:Give("weapon_zs_zeknife")
			pl:Give("weapon_zs_zegrenade")
			pl:Give(randomprimary)
			pl:Give(randomsecondary)
		else
			local start = pl:GetRandomStartingItem()
			if start then
				local func = self:GetInventoryItemType(start) == INVCAT_TRINKETS and pl.AddInventoryItem or pl.Give
				func(pl, start)
			end

			pl:Give("weapon_zs_fists")

			if self.StartingLoadout then
				self:GiveStartingLoadout(pl)
			elseif pl.m_PreRedeem then
				if self.RedeemLoadout then
					for _, class in pairs(self.RedeemLoadout) do
						pl:Give(class)
					end
				else
					pl:Give("weapon_zs_redeemers")
					pl:Give("weapon_zs_swissarmyknife")
				end
			end
		end

		local oldhands = pl:GetHands()
		if IsValid(oldhands) then
			oldhands:Remove()
		end

		local hands = ents.Create("zs_hands")
		if hands:IsValid() then
			hands:DoSetup(pl)
			hands:Spawn()
		end
	end

	local wcol = Vector(pl:GetInfo("cl_weaponcolor"))
	wcol.x = math.Clamp(wcol.x, 0, 2.5)
	wcol.y = math.Clamp(wcol.y, 0, 2.5)
	wcol.z = math.Clamp(wcol.z, 0, 2.5)
	pl:SetWeaponColor(wcol)
end

function GM:SetWave(wave)
	local previouslylocked = {}
	local UnlockedClasses = {}
	for classid, classtab in ipairs(GAMEMODE.ZombieClasses) do
		if not gamemode.Call("IsClassUnlocked", classid) then
			previouslylocked[classid] = true
		end
	end

	SetGlobalInt("wave", wave)

	for classid in pairs(previouslylocked) do
		if gamemode.Call("IsClassUnlocked", classid) then
			local classtab = self.ZombieClasses[classid]
			classtab.Locked = false
			classtab.Unlocked = true

			if not classtab.UnlockedNotify then
				classtab.UnlockedNotify = true
				table.insert(UnlockedClasses, classid)
			end

			for _, ent in pairs(ents.FindByClass("logic_classunlock")) do
				local classname = GAMEMODE.ZombieClasses[classid].Name
				if ent.Class == string.lower(classname) then
					ent:Input("onclassunlocked", ent, ent, classname)
				end
			end

			net.Start("zs_classunlockstate")
				net.WriteInt(classid, 8)
				net.WriteBool(classtab.Unlocked)
			net.Broadcast()
		end
	end

	if #UnlockedClasses > 0 then
		for _, pl in pairs(player.GetAll()) do
			local classnames = {}
			for __, classid in pairs(UnlockedClasses) do
				local classtbl = self.ZombieClasses[classid]

				table.insert(classnames, translate.ClientGet(pl, classtbl.TranslationName))
			end
			net.Start("zs_classunlock")
				net.WriteString(string.AndSeparate(classnames))
			net.Send(pl)
		end
	end
end

GM.NextEscapeDamage = 0
function GM:WaveStateChanged(newstate)
	if newstate then
		if self:GetWave() == 0 then
			gamemode.Call("CreateSigils", true) -- Try creating sigils again. Only really matters if nobody seeded the map yet.

			self:SetClosestsToZombie()

			local humans = {}
			for _, pl in pairs(player.GetAll()) do
				if pl:Team() == TEAM_HUMAN and pl:Alive() then
					table.insert(humans, pl)
				end
			end

			for _, pl in pairs(humans) do
				if pl.PlayerReady then -- There's a chance they might not be ready to send their desired cart yet.
					gamemode.Call("GiveDefaultOrRandomEquipment", pl)
				end
			end

			-- We should spawn a crate in a random spawn point if no one has any.
			if not self.ZombieEscape and #ents.FindByClass("prop_arsenalcrate") == 0 then
				local have = false
				for _, pl in pairs(humans) do
					if pl:HasWeapon("weapon_zs_arsenalcrate") then
						have = true
						break
					end
				end

				if not have and #humans >= 1 then
					local spawn = self:PlayerSelectSpawn(humans[math.random(#humans)])
					if spawn and spawn:IsValid() then
						local ent = ents.Create("prop_arsenalcrate")
						if ent:IsValid() then
							ent:SetPos(spawn:GetPos() + Vector(0, 0, 8))
							ent:Spawn()
							ent:DropToFloor()
							ent:SetCollisionGroup(COLLISION_GROUP_DEBRIS_TRIGGER) -- Just so no one gets stuck in it.
							ent.NoTakeOwnership = true
						end
					end
				end
			end
		end

		local prevwave = self:GetWave()

		if self:GetUseSigils() and prevwave >= self:GetNumberOfWaves() then return end

		gamemode.Call("SetWave", prevwave + 1)
		gamemode.Call("SetWaveStart", CurTime())
		if self.ZombieEscape then
			gamemode.Call("SetWaveEnd", -1)
			SetGlobalInt("numwaves", -1)
		else
			gamemode.Call("SetWaveEnd", self:GetWaveStart() + self:GetWaveOneLength() + (self:GetWave() - 1) * (GetGlobalBool("classicmode") and self.TimeAddedPerWaveClassic or self.TimeAddedPerWave))
		end

		net.Start("zs_wavestart")
			net.WriteInt(self:GetWave(), 16)
			net.WriteFloat(self:GetWaveEnd())
		net.Broadcast()

		for _, pl in pairs(team.GetPlayers(TEAM_UNDEAD)) do
			pl.m_LastWaveStartSpawn = CurTime()

			if pl:GetZombieClassTable().Name == "Crow" then
				pl:SetZombieClass(pl.DeathClass or 1)
				pl:UnSpectateAndSpawn()
			elseif not pl:Alive() and not pl.Revive then
				pl:UnSpectateAndSpawn()
			end
		end

		for _, pl in pairs(player.GetAll()) do
			pl.WaveBarricadeDamage = 0
			pl.WaveHumanDamage = 0
		end

		local curwave = self:GetWave()
		for _, ent in pairs(ents.FindByClass("logic_waves")) do
			if ent.Wave == curwave or ent.Wave == -1 then
				ent:Input("onwavestart", ent, ent, curwave)
			end
		end
		for _, ent in pairs(ents.FindByClass("logic_wavestart")) do
			if ent.Wave == curwave or ent.Wave == -1 then
				ent:Input("onwavestart", ent, ent, curwave)
			end
		end
	elseif self:GetWave() >= self:GetNumberOfWaves() then -- Last wave is over
		if self:GetUseSigils() then
			if self:GetEscapeStage() == ESCAPESTAGE_BOSS then
				-- 2 minutes is enough to decide people left are stuck or griefing.
				self:SetEscapeStage(ESCAPESTAGE_DEATH)

				gamemode.Call("SetWaveEnd", -1)
			elseif self:GetEscapeStage() == ESCAPESTAGE_ESCAPE then
				self:SetEscapeStage(ESCAPESTAGE_BOSS)

				-- Some time to get out with everyone spawning as bosses.
				gamemode.Call("SetWaveEnd", CurTime() + 45)

				-- Start spawning boss zombies.
			elseif self:GetEscapeStage() == ESCAPESTAGE_NONE then
				-- If we're using sigils, remove them all and spawn the doors.
				for _, sigil in pairs(ents.FindByClass("prop_obj_sigil")) do
					local ent = ents.Create("prop_obj_exit")
					if ent:IsValid() then
						ent:SetPos(sigil.NodePos or sigil:GetPos())
						ent:SetAngles(sigil:GetAngles())
						ent:Spawn()
					end

					sigil:Remove()
				end

				-- Some time to escape.
				gamemode.Call("SetWaveActive", true)
				gamemode.Call("SetWaveEnd", CurTime() + 45)
				self:SetEscapeStage(ESCAPESTAGE_ESCAPE)

				local curwave = self:GetWave()
				for _, ent in pairs(ents.FindByClass("logic_waves")) do
					if ent.Wave == curwave or ent.Wave == -1 then
						ent:Input("onwaveend", ent, ent, curwave)
					end
				end
				for _, ent in pairs(ents.FindByClass("logic_waveend")) do
					if ent.Wave == curwave or ent.Wave == -1 then
						ent:Input("onwaveend", ent, ent, curwave)
					end
				end
			end
		else
			-- If not using sigils then humans all win.
			gamemode.Call("EndRound", TEAM_HUMAN)

			local curwave = self:GetWave()
			for _, ent in pairs(ents.FindByClass("logic_waves")) do
				if ent.Wave == curwave or ent.Wave == -1 then
					ent:Input("onwaveend", ent, ent, curwave)
				end
			end
			for _, ent in pairs(ents.FindByClass("logic_waveend")) do
				if ent.Wave == curwave or ent.Wave == -1 then
					ent:Input("onwaveend", ent, ent, curwave)
				end
			end
		end
	else
		gamemode.Call("SetWaveStart", CurTime() + (GetGlobalBool("classicmode") and self.WaveIntermissionLengthClassic or self.WaveIntermissionLength))

		net.Start("zs_waveend")
			net.WriteInt(self:GetWave(), 16)
			net.WriteFloat(self:GetWaveStart())
		net.Broadcast()

		local pointsbonus
		if self.EndWavePointsBonus > 0 then
			pointsbonus = self.EndWavePointsBonus + (self:GetWave() - 1) * self.EndWavePointsBonusPerWave
		end

		for _, pl in pairs(player.GetAll()) do
			if pl:Team() == TEAM_HUMAN and pl:Alive() then
				if self.EndWaveHealthBonus > 0 then
					pl:SetHealth(math.min(pl:GetMaxHealth(), pl:Health() + self.EndWaveHealthBonus))
				end
				if pointsbonus then
					local pointsreward = pointsbonus + (pl.EndWavePointsExtra or 0)

					if pl:IsSkillActive(SKILL_SCOURER) then
						pl:GiveAmmo(math.ceil(pointsreward), "scrap")
					else
						pl:AddPoints(pointsreward, nil, nil, true)
					end
				end
			elseif pl:Team() == TEAM_UNDEAD and not pl:Alive() and not pl.Revive then
				local curclass = pl.DeathClass or pl:GetZombieClass()
				local crowindex = GAMEMODE.ZombieClasses["Crow"].Index
				pl:SetZombieClass(crowindex)
				pl:DoHulls(crowindex, TEAM_UNDEAD)
				pl.DeathClass = nil
				pl:UnSpectateAndSpawn()
				pl.DeathClass = curclass
			end

			pl.SkipCrow = nil
		end

		local curwave = self:GetWave()
		for _, ent in pairs(ents.FindByClass("logic_waves")) do
			if ent.Wave == curwave or ent.Wave == -1 then
				ent:Input("onwaveend", ent, ent, curwave)
			end
		end
		for _, ent in pairs(ents.FindByClass("logic_waveend")) do
			if ent.Wave == curwave or ent.Wave == -1 then
				ent:Input("onwaveend", ent, ent, curwave)
			end
		end
	end

	gamemode.Call("OnWaveStateChanged")
end

function GM:PlayerSwitchFlashlight(pl, newstate)
	if pl:Team() == TEAM_UNDEAD then
		return false
	end

	if pl:Team() == TEAM_HUMAN and CurTime() >= pl.NextFlashlightSwitch then
		pl.NextFlashlightSwitch = CurTime() + 0.75
		return true
	end

	return false
end

function GM:PlayerStepSoundTime(pl, iType, bWalking)
	return 350
end

function GM:OnZEWeaponPickup(pl, wep)
end

net.Receive("zs_changeclass", function(len, sender)
	if sender:Team() ~= TEAM_UNDEAD or sender.Revive or GAMEMODE.PantsMode or GAMEMODE:IsClassicMode() or GAMEMODE:IsBabyMode() or GAMEMODE.ZombieEscape then return end

	local classname = GAMEMODE:GetBestAvailableZombieClass(net.ReadString())
	local suicide = net.ReadBool()
	local classtab = GAMEMODE.ZombieClasses[classname]
	if not classtab or classtab.Disabled or classtab.Boss or classtab.Hidden and not (classtab.CanUse and classtab:CanUse(sender)) then return end

	if not gamemode.Call("IsClassUnlocked", classname) then
		sender:CenterNotify(COLOR_RED, translate.ClientFormat(sender, "class_not_unlocked_will_be_unlocked_x", classtab.Wave))
	elseif sender:GetZombieClassTable().Name == classname and not sender.DeathClass then
		sender:CenterNotify(COLOR_RED, translate.ClientFormat(sender, "you_are_already_a_x", translate.ClientGet(sender, classtab.TranslationName)))
	else
		sender.DeathClass = classtab.Index
		sender:CenterNotify(translate.ClientFormat(sender, "you_will_spawn_as_a_x", translate.ClientGet(sender, classtab.TranslationName)))

		if suicide and sender:Alive() and GAMEMODE:GetWaveActive() and (CurTime() < GAMEMODE:GetWaveEnd() - 4 or GAMEMODE:GetWaveEnd() < 0) and not sender:GetZombieClassTable().Boss and gamemode.Call("CanPlayerSuicide", sender) then
			sender:Kill()
		end
	end
end)

net.Receive("zs_zsfriend", function(len, sender)
	local zsfriendid = net:ReadString()
	local zsfriendent = player.GetBySteamID(zsfriendid)

	if not zsfriendent then return end
	local isfriend = net:ReadBool()
	sender.ZSFriends[zsfriendent] = isfriend

	net.Start("zs_zsfriendadded")
		net.WriteEntity(sender)
		net.WriteBool(isfriend)
	net.Send(zsfriendent)
end)


net.Receive("zs_nestspec", function(len, sender)
	if not sender:IsValidZombie() then return end
	if sender:GetObserverMode() == OBS_MODE_NONE then return end

	local nest = net:ReadEntity()
	local neveralive = sender:GetZombieClassTable().NeverAlive

	if neveralive and nest.MinionSpawn then
		sender:TrySpawnAsGoreChild(nest)
	end

	if sender:Alive() or neveralive then return end

	if nest:IsValid() then
		sender:Spectate(OBS_MODE_CHASE)
		sender:SpectateEntity(nest)
	end
end)
