--#=============================================================================#
--Any Mobjthinkers that aren't for the main player object are handled here.
--Bots are handled at the bottom, and the Planet Egg object is handled in XS-EGG!
--#=============================================================================#
local XSon = XSon
local function P_MobjFlip(s) if s and s.valid and ((s.eflags&MFE_VERTICALFLIP) or (s.flags2&MF2_OBJECTFLIP)) return -1 end return 1 end

--Grab this for unlimited Ring Burst energy!
addHook("MobjSpawn", function(s)
	s.renderflags = RF_FULLBRIGHT|RF_NOCOLORMAPS
	s.blendmode = AST_ADD
	s.spritexscale, s.spriteyscale = FRACUNIT-25000,FRACUNIT+25000
	s.spriteyoffset = -8<<16
	s.shadowscale = 34000
	s.rollangle = ANG30
end, MT_XSPOWERRING)

addHook("MobjThinker", function(s) if not s.valid return end		
	s.frame = $ & ~FF_TRANSMASK
	if (leveltime%3==0)
		s.color,s.colorized = SKINCOLOR_SUPERSILVER1, true
		s.frame = $|FF_TRANS20
	elseif (leveltime%2==0)
		s.color,s.colorized = SKINCOLOR_GOLDENROD, true
		s.frame = $|FF_TRANS10
	else
		s.color,s.colorized = SKINCOLOR_NONE,false
	end
	
	local FUSE = s.fuse or 0
	local INIT = s.init or 0 --Initial anim.
	s.init = INIT+1	
	if s.CanOffer
		s.CanOffer = $-1
	end
	if (INIT < 6)
		local ghs = P_SpawnMobjFromMobj(s,0,0,0, MT_IVSP) --Appearance animation. You'll rarely see it.
		if ghs and ghs.valid
			ghs.blendmode = AST_ADD
			ghs.flags2 = $|MF2_SHADOW
			ghs.destscale = 1
			ghs.color,ghs.colorized = s.color,s.colorized
		end
		s.bounceCD = 0
		s.spriteyoffset = min(0, $+2<<16)
		s.spritexscale,s.spriteyscale = min(FRACUNIT, $+5000), max(FRACUNIT, $-5000)
		if INIT==5 or not INIT
			if not INIT
				XSon.RadiantLight(s, SKINCOLOR_SUPERGOLD5, 16, nil, S|FF_TRANS50)
				local OLDRAD,OLDHEIGHT = s.radius,s.height
				s.scale = $/2
				s.radius,s.height = OLDRAD,OLDHEIGHT
				if FUSE
					S_StartSoundAtVolume(s, sfx_xssfe5, 60) 
				end
			elseif INIT == 5 
				if FUSE
					local ANG = R_PointToAngle2(s.x, s.y, s.x+s.momx, s.y+s.momy)
					local OLDX,OLDY = s.x, s.y
					local TRAD = max(35<<16, (s.radius*5/2))
					for i = 1,6
						if (P_TryMove(s, s.x+FixedMul(TRAD, cos(ANG)), s.y+FixedMul(TRAD, sin(ANG)), true))
							P_SetOrigin(s, OLDX, OLDY, s.z)
							if (i>=6)
								s.radius = $*2 break -- Only increase radius if we have confirmed space to grow.
							end
							ANG = $+ANG60
						else
							P_SetOrigin(s, OLDX, OLDY, s.z) break --
						end
					end
					s.height = $*2
				end
				local E = s.EmblemMobj --Snap to emblem point if we were given one.
				if (E) and E.valid
					P_SetOrigin(s, E.x, E.y, E.z)
				elseif s.EmblemXYZ
					P_SetOrigin(s, s.EmblemXYZ.X, s.EmblemXYZ.Y, s.EmblemXYZ.Z) 
				end
			end
		end
	elseif s.bounceCD
		s.bounceCD = $-1
	end
	if FUSE
		if (FUSE < 50) --flicker until you vanish.
			s.flags2 = ($ & ~MF2_DONTDRAW)| ((s.fuse%2) and MF2_DONTDRAW or 0) 
			return --
		end	
		if INIT==5 or not INIT or ((INIT%10==0) and (INIT < 51))
			XS_RingBurstSparkles(s, 22, SKINCOLOR_FLAME, 16)
			if (INIT < 10)
				XSon.SquashFlash(s, SKINCOLOR_SUPERGOLD5, 9, FF_TRANS20, 2<<16) 
				XSon.PuffDust(s, SKINCOLOR_WHITE)
				local ghs = XSon.SmokeMist(s, 0, 99*s.scale)
				if ghs and ghs.valid
					ghs.color = SKINCOLOR_SUPERGOLD4
					ghs.scale = $*2
					ghs.spriteyscale = 120500
					ghs.spriteyoffset = $-22<<16
					if ghs.sttXspeed
						ghs.sttXspeed = $*2/5
					end
				end				
			end
			
			local ghs = XSon.ElectricBody(s, 8, SKINCOLOR_GOLDENROD) --Golden Rod best new skincolor
			ghs.sprite = SPR_XSG1
			ghs.frame = S|FF_TRANS30
			ghs.blendmode = AST_ADD
			ghs.sttfade = 7
			ghs.scalespeed = FRACUNIT/7
			ghs.destscale = 99<<16
			ghs.colorized = false
			ghs.sttspeed = 4
			ghs.sttfirstframe = S
			ghs.sttlastframe = T
			ghs.dispoffset = 30
			ghs.tracer = s
		end
	end
	
	if (s.flags & MF_GRENADEBOUNCE)
		local CEILINGZ = (s.eflags & MFE_VERTICALFLIP) and s.floorz or s.ceilingz
		if CEILINGZ and (abs(s.z-CEILINGZ) < 124*s.scale)
			P_SetObjectMomZ(s, -95536, true)
		end
	end
	if P_IsObjectOnGround(s) and (s.flags & MF_GRENADEBOUNCE)
		if not s.bounceCD
			XSSpawnJumpDust(s, 5<<16)
			s.momz = max(abs(s.momz), 8<<16)*P_MobjFlip(s) --Bounce higher than that.
			s.momx,s.momy = $/3,$/3
			XSon.PowerRingSparkles(s) 
			S_StartSound(s, sfx_s227)
			S_StartSoundAtVolume(s, sfx_kc35, 95)
			s.mirrored = not s.mirrored
			s.bounceCD = 5
		end
	elseif (s.momx or s.momy or s.momz) or (s.flags & MF_NOGRAVITY)
		if ((INIT < 8) or (FUSE < 100)) and (INIT % 2 == 0)
		or (INIT > 7) and (FUSE >=100)
			local ghs = P_SpawnMobjFromMobj(s, 0,0,0, MT_XSAUTOFRAME)
			ghs.sprite = s.sprite
			ghs.frame = (s.frame & ~FF_TRANSMASK)|FF_TRANS40
			ghs.scale = s.scale
			ghs.blendmode = AST_ADD
			ghs.renderflags = RF_FULLBRIGHT|RF_NOCOLORMAPS
			ghs.color = (leveltime%3==0) and SKINCOLOR_SUPERGOLD5 or (leveltime%2==0) and SKINCOLOR_FLAME or SKINCOLOR_WHITE
			ghs.colorized = true
			ghs.sttfade = 44
			ghs.fuse,ghs.tics = 11,99
			ghs.dispoffset = -139
			if (s.flags & MF_NOGRAVITY)
				ghs.destscale = 99<<16
				ghs.scalespeed = FRACUNIT/5
				ghs.fuse = 6
			else
				ghs.destscale = 1
			end
		end
		XSon.PowerRingSparkles(s)		
	end
end, MT_XSPOWERRING)

--Move a bit further from the wall, pls
addHook("MobjMoveBlocked", function(s, m, line)
	if m and m.valid
		if m.player or (m.flags & (MF_MONITOR|MF_ENEMY|MF_SHOOTABLE))
			return false --
		end
	end
	if line and line.valid
		local x,y = P_ClosestPointOnLine(s.x, s.y, line) 
		local TOWALLANG = R_PointToAngle2(s.x, s.y, x, y)+ANGLE_180
		for i = -1,1,2
			if (P_TryMove(s, s.x+FixedMul(36<<16, cos(TOWALLANG)), s.y+FixedMul(36<<16, sin(TOWALLANG)), true) )
				if not S_SoundPlaying(s, sfx_s227)			
					S_StartSound(s, sfx_s227) S_StartSoundAtVolume(s, sfx_kc35, 95)
				end
				return false --
			end
			TOWALLANG = $+ANGLE_180
		end
	end
end, MT_XSPOWERRING)

//Tracker for various purposes, like kicking pushables.
addHook("MobjThinker", function(s)
	if not (s.target and s.target.valid) P_RemoveMobj(s) return end --
	local t = s.target
	s.tics,s.fuse = 4,4 --Keep self alive.
	
	if s.DisappearIn --Makes object disappear in a certain amount of tics.	
		s.DisappearIn = max($-1,0)
		if s.DisappearIn==0
			if not t.player
				if s.tornado
					for z in players.iterate
						if z and (z.powers[pw_carry]==CR_DUSTDEVIL) and z.mo and z.mo.valid
							local DIST = FixedHypot(s.x-z.mo.x, s.y-z.mo.y)
							if (DIST <= max(t.radius, 200<<16))
								z.powers[pw_carry] = 0
							end
						end
					end 
				end
				P_RemoveMobj(t)
			end 
			P_RemoveMobj(s)
		end return --
	elseif s.XYFailSafe --To stop certain shenanigans.	
		s.XYFailSafe = max($-1,0)
		local p = s.pFailSafe
		if not (p and p.valid and p.mo and p.mo.valid and p.mo.health)
			local TABLE = s.XYFailSafeTable
			s.XYFailSafe = 0
			if type(TABLE)=="table"
				t.spritexscale,t.spriteyscale = TABLE.Xscale, TABLE.Yscale
				t.dispoffset = TABLE.dispoffset
			end
		end if s.XYFailSafe==0 t.XS_FStracker = nil P_RemoveMobj(s) end
		return --
	elseif (s.BossStopper != nil) --Stops Egg Zapper from flying forever.
		s.BossStopper = max($-1,0)
		if (s.BossStopper < 5)
			if not (s.BossStopper)	
				if (t.BossStopper == s) t.BossStopper = nil end
				t.momx,t.momy = 0,0
				P_RemoveMobj(s) return --
			elseif t.momx or t.momy
				t.momx = FixedDiv(FixedMul($,30),31)
				t.momy = FixedDiv(FixedMul($,30),31)
			end
		end
	end
	
	if s.MakeSolidAgain
		s.MakeSolidAgain = max($-1,0)
		if not s.MakeSolidAgain
			t.flags = $|MF_SOLID
			t.MakeSolidAgain = nil
			P_RemoveMobj(s) return --
		end
	end
	
	P_SetOrigin(s, t.x, t.y, t.z)
	
	if s.EggmanNeedsATaxi --Give drunk bosses who got beat up in a bar fight by Dark Sonic a free retour trip home.
		s.EggmanNeedsATaxi = $-1
		
		if s.SX == nil
			s.SX,s.SY,s.SZ = t.x,t.y,t.z --Motherfucker is just gonna teleport back otherwise.
		end
		
		local DIST = FixedHypot(s.SX-s.TAXIX, s.SY-s.TAXIY)
		local ZDIST = abs(s.SZ-s.TAXIZ)		
		if (DIST >= 48<<16) or (ZDIST >= 48<<16) --Looks we're in need of moving.
			local TOANG = R_PointToAngle2(s.SX, s.SY, s.TAXIX, s.TAXIY)
			local STEP = (DIST > 500<<16) and 45<<16 or 24<<16
			s.SX = $+FixedMul(STEP, cos(TOANG))
			s.SY = $+FixedMul(STEP, sin(TOANG))
			
			STEP = (ZDIST > 500<<16) and 36<<16 or 16<<16
			s.SZ = (s.SZ < s.TAXIZ) and $+STEP or $-STEP
			
			DIST = FixedHypot(s.SX-s.TAXIX, s.SY-s.TAXIY)
			ZDIST = abs(s.SZ-s.TAXIZ)	
			
			P_MoveOrigin(t, s.SX, s.SY, s.SZ) --I don't give a crap what you want, you go over here right now.
			t.flags2 = (leveltime%2) and $|MF2_DONTDRAW or $ & ~MF2_DONTDRAW --Flash to make this feel a bit more natural.
		end
		
		--Are we done?
		if not s.EggmanNeedsATaxi
		or (ZDIST < 48<<16) and (DIST < 48<<16)
		or t.x == s.TAXIX and t.y == s.TAXIY and t.z == s.TAXIZ
			s.EggmanNeedsATaxi = nil
			t.flags2 = $ & ~MF2_DONTDRAW
			t.momz = 0
			P_MoveOrigin(t, s.TAXIX, s.TAXIY, s.TAXIZ)
			P_RemoveMobj(s) return --Done!
		end		
		return 777 --
	end	
			
	--Add smoking trails effects.
	if s.SmokeTrail	
		local ghs = P_SpawnMobjFromMobj(t, 0,0,0, MT_THOK) --Kind of feels like Smash Bros launching.
		ghs.state = S_SPINDUST1+P_RandomRange(0,2)
		ghs.colorized = true
		ghs.color = (SKINCOLOR_SUPERGOLD5+5)-(s.SmokeTrail*2)
		ghs.fuse = 32
		ghs.frame = ($ & FF_FRAMEMASK)|FF_TRANS40
		ghs.scale = (t.scale*s.SmokeTrail)/5
		ghs.scalespeed = $/2
		ghs.destscale = 99<<16
		s.SmokeTrail = max($-1,0)
		if (s.SmokeTrail < 6)
			ghs.flags2 = $|MF2_SHADOW	
		end		
	end
	
	--Kick pushables
	if s.PushKick
		s.PushKick = max($-1,0)
		if (s.PushKick <= 0) 
			if t.valid 
				t.KickedByBlueBlur = nil
			end
			if s.BossStopper
				s.PushKick = nil
			else
				P_RemoveMobj(s)
			end
			return --
		end
		
		P_TryMove(t, t.x+FixedMul(s.KickXY, cos(s.KickANG)), t.y+FixedMul(s.KickXY, sin(s.KickANG)), ((s.KickZ > 0)) and true or false)
		if not (P_IsObjectOnGround(t) and (s.KickZ <= 0))
			if not (t.flags & MF_NOGRAVITY)
				t.z = $+(s.KickZ*P_MobjFlip(t))
			end
		else
			s.PushKick = max(0, $-2)
		end
		s.KickZ = max(0, $-(FRACUNIT/2))
		s.KickXY = max(0, $-FRACUNIT)	
		
		if P_IsObjectOnGround(t)
			local ghs = P_SpawnMobjFromMobj(t, FixedMul(16*t.scale, cos(s.KickANG+ANGLE_90)),FixedMul(16*t.scale, sin(s.KickANG+ANGLE_90)), 0, MT_SPINDUST)
			ghs.frame = XSLocalRNG(4,2,t)-2|FF_TRANS10
			ghs.colorized = true
			ghs.color = SKINCOLOR_BEIGE			
			ghs = P_SpawnMobjFromMobj(t, FixedMul(16*t.scale, cos(s.KickANG-ANGLE_90)), FixedMul(16*t.scale, sin(s.KickANG-ANGLE_90)), 0, MT_SPINDUST)
			ghs.frame = XSLocalRNG(0,2,t)|FF_TRANS10
			ghs.colorized = true
			ghs.color = SKINCOLOR_BEIGE
			S_StartSound(s, (leveltime % 2) and sfx_s3kbcs or sfx_s3k7e)
		elseif (s.KickXY > 4<<16)
			XSWindSpawn(t)
			local ghs = P_SpawnMobjFromMobj(t, 0,0,t.height/3, MT_EXPLODE)
			ghs.spriteyscale = $/2 ghs.spritexscale = $/2
			ghs.destscale = 1			
		end
		return 4 --
		
	--Kick...uhh, Amy?
	
	elseif s.RosyKick
		
		if s.KickRoll == nil
			s.KickRollD = P_RandomRange(0,1) or -1
			s.KickRoll = 0
		end
		
		s.RosyKick = $-1
		if (s.RosyKick <= 0) if t.valid t.KickedByBlueBlur = nil t.rollangle = 0 end s.state = S_NULL return end --
		s.KickRoll = $+(ANG30*s.KickRollD)
		if not P_IsObjectOnGround(t)
			t.rollangle = s.KickRoll
		else
			t.rollangle = 0
		end
		s.KickZ = max(-30<<16, $-(FRACUNIT-25000))	
		t.momx = 0 t.momy = 0 t.momz = 0
		t.frame = ($ & ~FF_FRAMEMASK)|H
		P_TryMove(t, t.x+FixedMul(s.KickXY, cos(s.KickANG)), t.y+FixedMul(s.KickXY, sin(s.KickANG)), true)
		if not (P_IsObjectOnGround(t) and (s.KickZ < 0))
			t.z = $+(s.KickZ*P_MobjFlip(t))
			if P_IsObjectOnGround(t)
				s.RosyKick = max(0, $-4) t.rollangle = 0
			end
		else
			s.RosyKick = max(0, $-9) t.rollangle = 0
		end
		local ghs = P_SpawnMobjFromMobj(t, 0,0,0, MT_EXPLODE)
		ghs.colorized = true ghs.color = SKINCOLOR_ROSY
		ghs.destscale = 1
		return 5 --
	end
end, MT_XSTRACKER)

--Second tracker object, for my sanity.
addHook("MobjThinker", function(s)
	if not (s.target and s.target.valid) s.state = S_NULL return end --
	local t = s.target
	
	if s.SnapDiveStretch
		s.SnapDiveStretch = $-1
		if not s.SnapDiveStretch
			t.spritexscale,t.spriteyscale = FRACUNIT,FRACUNIT		
			P_RemoveMobj(s) --Only ever use SnapDiveStretch by itself!
		end return --
	end
	
	if not t.health s.state = S_NULL return end --Be alive from here on out.
	
	--Remove invincibility periods of bosses and foes!
	if s.FretType != nil 
		s.FretType = max($-1,0)
		if (t.flags & MF_SHOOTABLE) and not (t.flags2 & MF2_FRET)
		or not s.FretType
			s.state = S_NULL return --Remove self, job done!
		end	
		if s.FretState == nil
			s.FretState = t.state
		end				
		if (t.state == s.FretState)
		or (t.state == t.info.painstate)
			if (t.tics > 2)
				t.tics = $-2
			else
				t.flags2 = $ & ~MF2_FRET
				s.FretType = 0
			end
		end
	end	
	if s.RollType != nil --Cause the target to roll for a bit, untill they hit the ground.
		if not s.RollDirection
			s.RollDirection = (XSon.RNG(0,1,s) == 1) and 1 or -1	
			if s.RollTypeTimer == nil
				s.RollTypeTimer = 300
			end
		end
		if s.RollTypeTimer --Automatically stop?
			s.RollTypeTimer = $-1
		end
		if P_IsObjectOnGround(t) and not (t.momz*P_MobjFlip(t) > 0)
		or (s.RollTypeTimer == 0)
			if s.FretType == nil and s.XYshove == nil
				t.rollangle = 0
				s.state = S_NULL return --
			end
		else
			local MAXROLL = (s.RollTypeSpeed) or FixedInt((abs(t.momx)+abs(t.momy)+abs(t.momz)))*(ANG1/2)
			if (MAXROLL < ANG1*3) MAXROLL = ANG1*3 end
			s.RollType = $+(MAXROLL*s.RollDirection)
			t.rollangle = s.RollType
			if s.RollTypeWild
				if t.player
					t.player.drawangle = s.RollType
				else
					t.angle = s.RollType
				end
			end
		end
	end
	--Got shoved by these attacks?
	if s.XYshove != nil
		if not s.XYshove
		or s.XYshoveANG == nil
			if s.RollType == nil and s.FretType == nil
				if (t.type == MT_EGGMOBILE) --Stop, scoundrel!
					t.momx = 0 t.momy = 0 t.momz = 0
				end
				s.state = S_NULL return --
			end
		elseif (abs(s.XYshove) <= 3*t.scale)
			s.XYshove = 0
		else
			if not (P_TryMove(t, t.x+FixedMul(s.XYshove, cos(s.XYshoveANG)), t.y+FixedMul(s.XYshove, sin(s.XYshoveANG)), true))
				s.XYshove = FixedDiv(FixedMul($,8),9) --Diminish harder!
			else
				s.XYshove = FixedDiv(FixedMul($,15),16) --Diminish shoving force over time!
			end
			if (abs(t.momz) < 3*s.scale) and (abs(s.XYshove) > 7*t.scale)
				XS_QuickDust(t)
				XSSpawnDust(t)
			end
		end	
	end
	--Add steamtrail?
	if s.SteamTrail != nil 
		s.SteamTrail = $+1	
		if (s.SteamTrail > 7) and (s.SteamTrail < 17) 
			local ghs = P_SpawnMobjFromMobj(t, 0,0,0, MT_THOK)
			ghs.state = S_SPINDUST1+P_RandomRange(0,2)
			ghs.colorized = true
			ghs.color = (SKINCOLOR_SUPERGOLD5+5)-(s.SteamTrail*2)
			ghs.fuse = 32
			ghs.frame = ($ & FF_FRAMEMASK)|FF_TRANS60
			ghs.scale = (t.scale*s.SteamTrail)/7
			ghs.scalespeed = FRACUNIT/25
			ghs.destscale = 99<<16
			if (s.SteamTrail < 13)
				ghs.flags2 = $|MF2_SHADOW
			end
		end
		if s.SteamTrailEnd
			if (s.SteamTrail >= s.SteamTrailEnd)
				t.rollangle = 0
				s.state = S_NULL  return --
			end
		elseif (s.SteamTrail > 16) and s.RollType == nil and s.FretType == nil and s.XYshove == nil
			s.state = S_NULL  return --
		end
	end		
end, MT_XSTRACKER2)

--Metal Sonic, ffs, do your damn thing!
addHook("MobjThinker", function(s)
	if (s.health <= 0) and (s.state < S_METALSONIC_DEATH1)
		s.flags = MF_NOGRAVITY|MF_BOSS|MF_SLIDEME|MF_SHOOTABLE|MF_ENEMY	
		s.state = S_METALSONIC_DEATH1 
		P_KillMobj(s) return ----You should kill yourself, NOW!!
	end
end, MT_METALSONIC_BATTLE)

--Do a taunt.
addHook("MobjDeath", function(s, i, so)
	if (gamemap==03) and so and so.player and IsXSonic(so) and so.player.xsonictable and not so.player.xsonictable.darkphase
		XSVoice(so.player, so, so.player.xsonictable, sfx_xsvoc3)
	end
end, MT_EGGMOBILE)

--Eject the passenger, we did NOT sign up for this!! Only in coop, though, we don't want to give X-Sonic an advantage in races.
addHook("MobjThinker", function(s)
	if s.health and s.target and s.target.valid
		local t = s.target
		if IsXSonic(t) and t.player and t.player.xsonictable
		local p = t.player local xs = p.xsonictable local yu = xs
			if (p.powers[pw_carry] == CR_MINECART) and (gametype == GT_COOP)
				if (xs.spin and xs.spin < 3) --Leave minecarts at will.
					p.powers[pw_carry] = 0
					p.pflags = $|PF_JUMPED & ~PF_THOKKED
					s.target.momx = s.momx
					s.target.momy = s.momy
					P_DoJump(p, true)
					P_SetObjectMomZ(s.target, 8<<16, true)
					s.target = nil return --
				end
			end
		end
	end
end, MT_MINECART)

--Test for moving platforms and polyobjects.
local function PlatformTest(t, s)
	if t.subsector != s.subsector return true end
	if t.subsector
		for polyobj in t.subsector.polyList()
			if polyobj.valid return true end --PolyObject!
		end
	end
end

--Seperate function so I can call it instantly on spawn.
XSon.EdgeStopThinker = function(s) if not s.valid return end --
	local t = s.target
  	if not (s.active and t and t.valid and IsXSonic(t) and t.health and t.player and t.player.xsonictable)
		s.state = S_NULL return --
	end
	local p = t.player local xs = p.xsonictable local yu = xs
	
	if xs.snapdive or xs.snapdivetrick or p.powers[pw_carry] or p.xstauntstance return end --Never snap to ledges in these cases!
	if (t.eflags & (MFE_TOUCHLAVA|MFE_SPRUNG|MFE_APPLYPMOMZ|MFE_GOOWATER)) return end
	
	
	local DOIT = false
	--Flipped version is finnicky with ObjectOnGround checks.
	if (t.eflags & MFE_VERTICALFLIP)
		if ((s.ceilingz-60*s.scale) > t.ceilingz)
			if (s.floorz < t.ceilingz-48*s.scale)
				if (t.ceilingz-t.height > s.floorz) 
					DOIT = true
				end
			end
		end
	else
		--Non-flipped version
		if (s.floorz < t.z-(60*t.scale)) 
		and (t.ceilingz > s.floorz+s.height) --These sometimes give inconsistent info, so make doubly sure!!
		and not (t.floorz >= s.ceilingz-(23*t.scale))
		and PlatformTest(t, s)
			DOIT = true
		end
	end
		
	--We got permission, do the behaviour now!
	if DOIT != false
		XSZipDashCancel(p, t, xs)
		t.state = S_PLAY_EDGE
		p.powers[pw_nocontrol] = max($,4)
				
		--Stop player and play animation.		
		t.momx = p.cmomx t.momy = p.cmomy
		xs.ledgeprotectcd = 11
		xs.ledgereactivate = true
		xs.ledgeprotectangle = nil

		--Adjust player angle and position...
				
		if xs.edgestop.sonicang != nil
			p.drawangle = xs.edgestop.sonicang
		--	t.angle = xs.edgestop.sonicang
			xs.ledgeprotectangle = xs.edgestop.sonicang
			if not (t.player.cmomx or t.player.cmomy) --Can't trust trymove on moving platforms.
			and not ((s.flags2 & MF2_TWOD) or twodlevel)
				for d = 1,22 --Move Sonic as close to the ledge as possible.
					P_TryMove(s, s.x+FixedMul(FRACUNIT, cos(s.angle)), s.y+FixedMul(FRACUNIT, sin (s.angle)), false)
				end
			end
			xs.edgestop.sonicang = nil
		end
	end
end

--Ledge protection object! Edgestop object. Edge Protection. 
addHook("MobjThinker", function(s) XSon.EdgeStopThinker(s) end, MT_XSEDGESTOP)

//Allow ability to shoot off TNT barrels with the snap dive!
addHook("MobjThinker", function(s)
	if s.tntdelayexpX
		s.tntdelayexpX = $-1
		if s.tntdelayexpX == 1
			s.flags = MF_MISSILE|MF_NOBLOCKMAP
			s.momz = $-1 s.momy = $-1 s.momx = $-1 --Give SOME momentum so it explodes.
		end
	end
end, MT_TNTBARREL)

addHook("MobjSpawn", function(s) if not s.valid return end --
	if not s.iamatreetotal s.iamatreetotal = 1 end
	if s.iamatreeEXTRA == nil s.iamatreeEXTRA  = {} end	
end, MT_XTREETRACKER)

--Respawns destructible objects.
addHook("MobjThinker", function(s) if not s.valid return end --	
	if s.iamatree --Not necessarily a tree, any scenery item will do.
		
		s.tics = max($,3)
		s.iamatree = $-1
		local t = s.target
		for i = 1,s.iamatreetotal
			if i!=1
				t = s.iamatreeEXTRA[i]
			end
			if not (t and t.valid) continue end --
			
			if (s.iamatree > 50)
				if not (t.flags2 & MF2_DONTDRAW)
					t.flags2 = $|MF2_DONTDRAW
					S_StopSound(t) --Keep it quiet!
				end
			else
				P_SetOrigin(s, t.x, t.y, t.z)
				s.momx,s.momy,s.momz = t.momx,t.momy,t.momz
				s.angle = t.angle
				s.spritexscale,s.spriteyscale = t.spritexscale,t.spriteyscale
				s.color,s.colorized = t.color,t.colorized
				s.rollangle = t.rollangle
				if (t.eflags & MFE_VERTICALFLIP)
					s.eflags = $|MFE_VERTICALFLIP
				else
					s.eflags = $ & ~MFE_VERTICALFLIP
				end
				
				if s.iamatree == 50--Regrow the tree by pretending we are it and growing to the correct size.
					if i == 1
						XSSpawnJumpDust(t)
					end
					s.flags2 = $ & ~MF2_DONTDRAW
					s.scalespeed = max(1, (t.scale/50))
					s.destscale = t.scale
					if (t.frame & FF_HORIZONTALFLIP)
						s.frame = $|FF_HORIZONTALFLIP
					else
						s.frame = $ & ~FF_HORIZONTALFLIP
					end				
					if (t.frame & FF_VERTICALFLIP)
						s.frame = $|FF_VERTICALFLIP
					else
						s.frame = $ & ~FF_VERTICALFLIP
					end	
					s.mirrored = t.mirrored
				elseif (s.iamatree <= 1)
					if i == 1
						XSSpawnJumpDust(t)
						local ghs = XSon.SquashFlash(t, SKINCOLOR_ICY, nil, FF_TRANS20, 2<<16)
						if ghs and ghs.valid
							local ADD = FixedDiv(FixedMul((t.height*2)/3, t.spriteyscale), FRACUNIT) 
							ghs.z = t.z + (ADD*P_MobjFlip(t))
						end
					elseif t.WaitingXTree and t.WaitingXTree != true
						t.flags = t.WaitingXTree
					end
					t.WaitingXTree = nil
					t.flags = $ & ~MF_NOCLIPTHING -- The tree has returned, all thanks to the power of friendship.
					t.flags2 = $ & ~MF2_DONTDRAW
				end
			end
		end
		if (s.iamatree <= 1)
			P_RemoveMobj(s) return --
		end
	end
end, MT_XTREETRACKER)

--Can we PLEASE get an S_SKIN flag to not spawn ghost images on super characters and sneakers, so I don't have to do THIS?
addHook("MobjThinker", function(s)
	if s.valid and (s.fuse == 2) and (s.sprite2 & FF_SPR2SUPER) and IsXSonic(s) s.state = S_NULL return end
end, MT_GHOST)

--Bots are gonna be alot more important in the future, I feel, so let's have X Sonic play along appropriately!
addHook("BotTiccmd", function(p, cmd)
	if not (p and p.valid and gamestate == GS_LEVEL) return end --Always check GS_LEVEL gamestate with bots!
	if not (p.realmo and IsXSonic(p.realmo)) return end --Be X Sonic and exist
	if not (p.bot) or p.bot==2 return end --p.bot==2 would mean a player is controlling it!
	
	local tp = nil --Set up our leader. They become tp.mo

	if p.botleader and p.botleader.realmo and p.botleader.realmo.valid
		tp = p.botleader
	elseif (players[0] and players[0].valid and players[0].realmo and players[0].realmo.valid)
		tp = players[0]
	end
	
	if tp == nil or not p.xsonictable or not tp.xsonictable return end
	
	local s = p.mo
	local xs = p.xsonictable local yu = xs
	local xs2 = tp.xsonictable local yu2 = xs2

	p.acceleration = 31
	p.accelstart = 142 -- To be a bit less annoying.
	p.charflags = $|SF_NOSKID
	
	--Also taunt!
	if tp.xstauntstance or (tp.mo and tp.mo.state == S_PLAY_FLY and t.mo.skin == "adventuresonic")
		if not p.xstauntstance
			if (p.panim == PA_IDLE) and (leveltime % 24 == 0)
				XSon.StartTaunts(p, s, xs)
			end
		elseif p.xstauntstance --Be a copy cat.
			local TS = tp.xstauntstance
			p.xstauntstance.flip = (not (TS.flip)) and FF_HORIZONTALFLIP or 0
			if TS.Cbuttons != nil
				cmd.buttons = TS.Cbuttons
			end
			if TS.Cforwardmove or TS.Csidemove 
				cmd.forwardmove = TS.Cforwardmove
				cmd.sidemove = TS.Csidemove
				if p.xstauntstance
					XSon.TauntStance(p, s, xs)
				end
			end
		end
	elseif tp.xsringburst
		if not P_IsObjectOnGround(s) or p.xsringburst
			cmd.buttons = $|BT_SPIN & ~BT_CUSTOM1
		end
		xs.PowerRing = 250
	else
		if s.XSassault 
			local RNG = P_RandomRange(1,100)
			if (RNG < 8)
				cmd.buttons = $|BT_JUMP
			elseif (RNG < 23)
				cmd.buttons = $|BT_SPIN
			elseif (RNG < 26)
				cmd.buttons = $|BT_CUSTOM1
			end
		elseif xs.snapdivetrick == 7 --Randomly kick stuff at times.
			if P_RandomRange(3,4)==4
				xs.spin = 1
				cmd.buttons = $|BT_SPIN --Do a kick!
			end
		else
			local DIST = FixedHypot(s.x-tp.mo.x, s.y-tp.mo.y)
			if xs.zipdash
				if cmd.forwardmove == 0
					cmd.forwardmove = 10
				end		
				xs.BotZDCD = 15
				if (xs.zipdash < 12) or (DIST > 180*s.scale)
					if xs.FreeFlight
						xs.custom1 = max($,5)
						cmd.buttons = $|BT_CUSTOM1
					else
						xs.custom1 = max($,5)
						xs.spin = max($,5)
						cmd.buttons = $|BT_SPIN|BT_CUSTOM1 --Quit spamming and dash for a little bit at least.
					end
				end
			else
				if xs.BotZDCD or (DIST < 150*s.scale) or (tp.speed > 40*tp.mo.scale) 
					if not xs.snapdivetrick and not xs.snapdive
						xs.spin = 99<<16 --Only allow zipdashes if your leader player is FAR away and speeding.	
						cmd.buttons = $ & ~(BT_SPIN|BT_CUSTOM1)		
					end
				elseif (DIST > 700*s.scale)
					if xs.FreeFlight or (tp.cmd.buttons & BT_CUSTOM1)
						cmd.buttons = $|BT_CUSTOM1
					end
				end
			end
			if tp.mo and tp.mo.XSassault and (leveltime%5==0) and xs.darkphase
				cmd.buttons = $|BT_JUMP|BT_SPIN
			elseif xs.FreeFlight and xs2.FreeFlight
				cmd.buttons = p.cmd.buttons
			end
		end		
		xs.PowerRing = 0
		xs.ringburstcharge = 0 --No ring bursts unless the player is doing so too.	
	end
	if xs.BotZDCD
		xs.BotZDCD = $-1
	end	
	
	if tp.mo and IsXSonic(tp.mo) and (tp.mo.skin != s.skin)
		R_SetPlayerSkin(p, tp.mo.skin)
		s.skin = tp.mo.skin
	end
	
	--Super Stuff!
	if (tp.powers[pw_super] > 1) or tp.hypermysticsonic
		p.powers[pw_super] = max($,2)
		p.rings = max($,50)
	elseif p.powers[pw_super]
		p.rings = 0
	end
	if xs2.darkphase and xs.darkphase != xs2.darkphase
		XSon.ObtainTheNPass(p,s,xs)
		xs.darkphase = xs2.darkphase
	end
	if xs.darkphase or p.powers[pw_super]
		p.rings = max($,50)
	end
	if p.XSblurtable
		for d = -4,8,12
			if p.XSblurtable[d] and p.XSblurtable[d].valid
				XSon.OverGlowThinker(p.XSblurtable[d])
			else
				p.XSblurtable[d] = nil
			end
		end
	end
end)

--Lazy edit from SA-Sonic, as usual.
addHook("MobjThinker", function(s) if not s.valid return end --
	if (s.state == S_ROSY_HUG) 
		local t = s.target local p = t and t.player local xs = p and p.xsonictable
		if xs and IsXSonic(t)
			if xs.darkphase==3 and (p.speed or s.momz)
			or xs.darkphase and (s.XS_DarkTable and s.XS_DarkTable.darkrush or (p.pflags & (PF_JUMPED|PF_SPINNING)) )
			or s.XStransform==8 --DON'T TOUCH THAT
			or p.xstauntstance and (p.xstauntstance.taunt==14 or p.xstauntstance.taunt==9) and ((t.frame&FF_FRAMEMASK)>=D)
				S_StopSoundByID(s, sfx_cdpcm6)
				S_StartSoundAtVolume(t, sfx_doord2, 66) 
				S_StartSound(s, sfx_cdpcm7)
				XSon.Quake(t, 80<<16, 5, 1250<<16)
				s.z = $+(s.scale*P_MobjFlip(s))
				P_SetObjectMomZ(s, 10<<16)				
				P_InstaThrust(s, R_PointToAngle2(s.x, s.y, t.x, t.y)+( XSLocalRNG(-25,25,s)*ANG1), -24*s.scale)
				s.state = S_ROSY_PAIN
				P_SpawnGhostMobj(s)
				for i = 1,3
					local ghs = P_SpawnMobjFromMobj(s,0,0,0,MT_EXPLODE)
					ghs.spritexscale,ghs.spriteyscale = $+10000*i,$+10000*i
					ghs.momx,ghs.momy,ghs.momz = s.momx/i,s.momy/i,s.momz/i
					ghs.destscale = 1
				end
			else
				if s.XS_DarkTable
					local SHIT = s.XS_DarkTable.noviolence or 0
					if (SHIT < 1050)
						s.XS_DarkTable.noviolence = 1050		
					end
				end	
				if (leveltime%3==0)
					if xs.darkphase and (leveltime%6==0)
						p.rings = max($-1,0)
						S_StartSoundAtVolume(s, sfx_antiri, 80, p)	
					end
					local RNG1 = P_RandomRange(4,6)
					local ghs = P_SpawnMobjFromMobj(s, XSLocalRNG(-31,31,t)*s.scale, XSLocalRNG(-31,31,s)*s.scale, 
					P_RandomRange(34,99)*s.scale, MT_THOK)
					ghs.sprite = SPR_LHRT
					ghs.frame = A
					ghs.renderflags = RF_SEMIBRIGHT|RF_NOCOLORMAPS
					ghs.scale = $/RNG1
					ghs.scalespeed = $/XSLocalRNG(2,4,t)
					ghs.destscale = s.scale*4
					ghs.fuse = 18
					P_SetObjectMomZ(ghs, XSLocalRNG(3,12,s)<<16)
					if (leveltime%6==0)
						ghs.color,ghs.colorized = SKINCOLOR_RED,true
					end
				end
			end
		end
	end
end, MT_ROSY)

--Don't show Dark Sonic on the roulette normally.
addHook("MobjThinker", function(s) if not s.valid return end
	if s.XS_supertagged or s.XS_SUPERSIGN
		if (s.state != s.info.spawnstate) or (s.tics < 0) or (s.tics > 9999)	
			if not s.XS_SUPERSIGN
				for m in mobjs.iterate(mobjs)
					if m and m.valid and (m.state==S_PLAY_SIGN) and not (m.type==MT_PLAYER) and not m.player
						local DIST = FixedHypot(s.x-m.x, s.y-m.y)
						if (DIST < 9<<16)					
							s.XS_SUPERSIGN = m break --Topple, Launch, SMASH!!
						end
					end
				end	
			end
			if s.XS_SUPERSIGN
				local t = s.XS_SUPERSIGN	
				if type(t)=="userdata" and t.valid and not (t.type==MT_PLAYER)
					if (t.state==S_PLAY_SIGN) and t.skin=="xsonic"
						if (leveltime%4==0)
							local ghs = XSon.RadiantLight(t, nil, 7, nil, S|((leveltime%8==0) and FF_TRANS70 or FF_TRANS80))
							ghs.color = SKINCOLOR_XSSUPERAURA
							ghs.scalespeed = $+XSLocalRNG(0,4500,ghs)
							ghs.spriteyoffset = $+14<<16
							ghs.dispoffset = -149
							ghs.sttfade = 3 ghs.ditchtracer = nil
							
							XSon.ElecticBody(t, nil, SKINCOLOR_SUPERSKY5)
							XSon.ShinySplat(t, SKINCOLOR_XSSUPERAURA, 10, 95, true)
						end
						if P_IsObjectOnGround(s) or P_IsObjectOnGround(t) or s.tics == -1
							t.sprite = SPR_XSG4 
							t.renderflags = $|RF_FULLBRIGHT|RF_NOCOLORMAPS
							t.frame = ($ & ~FF_FRAMEMASK)|V|FF_FULLBRIGHT
							t.tics,t.anim_duration = -1,-1
							t.color = SKINCOLOR_SUPASONIKKU
							local ghs = t.XS_SUPERBLUR
							if not (ghs and ghs.valid)
								ghs = P_SpawnMobjFromMobj(t,0,0,0, MT_THOK)
								ghs.sprite = SPR_XSG4
								ghs.renderflags = t.renderflags 
								ghs.dispoffset = 77
								ghs.blendmode = AST_ADD
								ghs.tics,ghs.anim_duration = -1,-1
								ghs.color = SKINCOLOR_XSSUPERBLUR
								t.XS_SUPERBLUR = ghs
								local Zoff = -(((182*8)+122)<<16)
								for i = -4,5 
									local ths = XSon.TransformGFX2(t, Zoff) --Rising orbs
									if ths and ths.valid
										ths.blendmode = AST_ADD
										ths.color,ths.colorized = SKINCOLOR_BLUE, true
										ths.spritexscale = ($*(5-min(4,abs(i)))) *4/5
									end Zoff = $+(188<<16)
								end
							end
							if ghs and ghs.valid
								ghs.z = t.z
								ghs.spritexscale,ghs.spriteyscale = t.spritexscale+XSLocalRNG(0,1500,t), t.spriteyscale+XSLocalRNG(0,1500,ghs)
								ghs.spritexoffset,ghs.spriteyoffset = t.spritexoffset,t.spriteyoffset			
								ghs.frame = W|((leveltime%2) and FF_TRANS70 or FF_TRANS60)
							end
							
							if displayplayer and displayplayer.xsonictable
								if (leveltime%10==0)
									XS_RingBurstDust(displayplayer, t, displayplayer.xsonictable, 350*t.scale)
								end
								XSon.sparkles(displayplayer,t,displayplayer.xsonictable, 120, 6, 5, 5<<16, SKINCOLOR_SUPERGOLD4, AST_ADD)	
							end
						end
					end
				end
			end
		end
	elseif (s.XS_darktagged or s.XS_DARKSIGN)
		if (s.state != s.info.spawnstate) or (s.tics < 0) or (s.tics > 9999)		
			if not (s.XS_DARKSIGN)
				for m in mobjs.iterate(mobjs)
					if m and m.valid and (m.state==S_PLAY_SIGN) and not (m.type==MT_PLAYER) and not m.player
						local DIST = FixedHypot(s.x-m.x, s.y-m.y)
						if (DIST < 9<<16)					
							s.XS_DARKSIGN = m break --Topple, Launch, SMASH!!
						end
					end
				end	
			end
			if s.XS_DARKSIGN
				local t = s.XS_DARKSIGN
				if type(t)=="userdata" and t.valid and not (t.type==MT_PLAYER)
					local DARKAURA = XSon.DarkAura(0)
					XSon.DarkBody(t, nil, nil, DARKAURA, 2)
					if (leveltime%2==0)
						XSon.SmokeMist(t, 2, 149*t.scale, nil, nil, nil, DARKAURA)
						if (leveltime%4==0)
							XSon.ElecticBody(t, 6, SKINCOLOR_CRIMSON)	
						end
					end		
					if (t.state==S_PLAY_SIGN) and (t.skin=="xsonic" or t.skin=="xsonicserious") 					
						t.sprite = SPR_XSG4 
						t.frame = ($ & ~FF_FRAMEMASK)|U|FF_FULLBRIGHT
						t.tics,t.anim_duration = -1,-1
					end
				end
			end
		end
	end
end, MT_SIGN)

--Fixes OpenGL shield rendering, and hides shield when inappropriate
XSon.ShieldThinker = function(s, t, p, xs) if not s.valid return end --
	local t = (s.tracer and s.tracer.valid and not s.tracer.player) and s.tracer
	local TA = (s.target and s.target.valid and not s.target.player) and s.target
	if not s.DispedIt
		local ADD = 17
		s.dispoffset = (s.dispoffset <= 0) and $-ADD or $+ADD
		if t
			t.dispoffset = (t.dispoffset <= 0) and $-ADD or $+ADD
		end
		if TA
			TA.dispoffset = (TA.dispoffset <= 0) and $-ADD or $+ADD
		end
		s.DispedIt = true
	end 
	if p and p.xsringburst or xs and xs.darkphase==3
		s.HideShield = true
		s.flags2 = $|MF2_DONTDRAW
		if t
			t.flags2 = $|MF2_DONTDRAW
		end	
		if TA
			TA.dispoffset = (TA.dispoffset <= 0) and $-20 or $+20
		end
	elseif s.HideShield
		s.HideShield = nil
		s.flags2 = $ & ~MF2_DONTDRAW
		if t
			t.flags2 = $ & ~MF2_DONTDRAW
		end
		if TA
			TA.flags2 = $ & ~MF2_DONTDRAW
		end		
	end
end

local function XCustomShield(s)
	if s and s.valid
		for z in players.iterate
			if z.mo and IsXSonic(z.mo) and z.mo.x == s.x and z.mo.y == s.y and z.mo.z == s.z and z.xsonictable
				z.xsonictable.myshield = s
				s.myXsonic = z
				XSon.ShieldThinker(s) return --We found them, so stop.
			end
		end
	end
end
for ORB = MT_ELEMENTAL_ORB, MT_THUNDERCOIN_ORB
	if ((ORB)!=nil) and type((ORB))=="number" and (ORB)
		addHook("MobjSpawn", XCustomShield, ORB)
	end
end