-- It started with a script that allows projectiles to waterrun, to make them more useful in certain maps
-- (such as toxic palace, where they can fall in pools of water)
-- Then i thought, why not try put MT_PLAYER in list of mobjs this script applies to
-- ...now we have this thing, which mostly consists of C functions from kart code translated to lua

-- Already loaded
if playerwaterrun then return end

rawset(_G, "playerwaterrun", {
	running = false,
})

-- More just for fun/testing
local cv_everywhere = CV_RegisterVar {
    name = "waterruneverywhere",
    defaultvalue = "Off",
    possiblevalue = CV_OnOff,
    flags = CV_NETVAR,
}

addHook("MapLoad", function(mapid)
    playerwaterrun.running = false

    local toggles = {
        ["true"] = true,
        ["on"] = true,
        ["1"] = true,
        ["false"] = false,
        ["off"] = false,
        ["0"] = false
    }

    -- Always check for valid value, just in case (even with waterruneverywhere active)
    if mapheaderinfo[mapid].waterrun then
        playerwaterrun.running = toggles[mapheaderinfo[mapid].waterrun:lower()]

        if playerwaterrun.running == nil then
            print("\130WARNING:\128 unknown Lua.WaterRun value for map "..G_BuildMapName(mapid)..", ignoring")
            playerwaterrun.running = false
        end
    end

    playerwaterrun.running = playerwaterrun.running or cv_everywhere.value
end)

addHook("NetVars", function(net)
    playerwaterrun.running = net($)
end)

local KART_FULLTURN = 800
local MAXPREDICTTICS = 12

local WATERFOF = (FF_EXISTS|FF_SWIMMABLE)

local overwrite = {"momx", "momy", "rmomx", "rmomy", "cmomx", "cmomy"}

local function rescale(value, oldmin, oldmax, newmin, newmax)
    return newmin + FixedMul(newmax-newmin, FixedDiv(value-oldmin, oldmax-oldmin))
end

-- -_-
local function P_IsLocalPlayer(p)
    if p == consoleplayer then return true end

    for i = 1, splitscreen do
        if p == displayplayers[i] then return true end
    end

    return false
end

-- Need to overwrite so it doesn't check P_IsObjectOnGround
local function K_GetKartSpeed(player, doboostpower)
	local k_speed = 150
	local g_cc = FRACUNIT
	local xspd = 3072		-- 4.6875 aka 3/64
	local kartspeed = player.kartspeed
	local finalspeed

	if gamespeed == 0 then
		g_cc = 53248 + xspd --  50cc =  81.25 + 4.69 =  85.94%
    elseif gamespeed == 2 then
		g_cc = 77824 + xspd -- 150cc = 118.75 + 4.69 = 123.44%
    else
		g_cc = 65536 + xspd -- 100cc = 100.00 + 4.69 = 104.69%
    end

	if G_BattleGametype() and player.kartstuff[k_bumper] <= 0 then
		kartspeed = 1
    end

	k_speed = $ + kartspeed*3 -- 153 - 177

	finalspeed = FixedMul(FixedMul(k_speed<<14, g_cc), player.mo.scale)

	if doboostpower then
		return FixedMul(finalspeed, player.kartstuff[k_boostpower]+player.kartstuff[k_speedboost])
    end

	return finalspeed
end

-- Yea, almost entire thing just for the funni woter drift
local function K_KartDrift(player)
	local minspeed = (10 * player.mo.scale)
	local dsone = K_GetKartDriftSparkValue(player)
	local dstwo = dsone*2
	local dsthree = dstwo*2

	-- Drifting is actually straffing + automatic turning.
	-- Holding the Jump button will enable drifting.

	-- Drift Release (Moved here so you can't "chain" drifts)
	if (player.kartstuff[k_drift] ~= -5 and player.kartstuff[k_drift] ~= 5) and player.kartstuff[k_driftcharge] < dsone then
		player.kartstuff[k_driftcharge] = 0
    elseif (player.kartstuff[k_drift] ~=-5 and player.kartstuff[k_drift] ~= 5)
        and (player.kartstuff[k_driftcharge] >= dsone and player.kartstuff[k_driftcharge] < dstwo) then
		if player.kartstuff[k_driftboost] < 20 then
			player.kartstuff[k_driftboost] = 20
        end
		S_StartSound(player.mo, sfx_s23c)
		--K_SpawnDashDustRelease(player)
		player.kartstuff[k_driftcharge] = 0
	elseif (player.kartstuff[k_drift] ~=-5 and player.kartstuff[k_drift] ~=5)
		and player.kartstuff[k_driftcharge] < dsthree then
		if player.kartstuff[k_driftboost] < 50 then
			player.kartstuff[k_driftboost] = 50
        end
		S_StartSound(player.mo, sfx_s23c)
		--K_SpawnDashDustRelease(player)
		player.kartstuff[k_driftcharge] = 0
	elseif (player.kartstuff[k_drift] ~=-5 and player.kartstuff[k_drift] ~= 5)
		and player.kartstuff[k_driftcharge] >= dsthree then
		if player.kartstuff[k_driftboost] < 125 then
			player.kartstuff[k_driftboost] = 125
        end
		S_StartSound(player.mo, sfx_s23c)
		--K_SpawnDashDustRelease(player)
		player.kartstuff[k_driftcharge] = 0
    end

	-- Drifting: left or right?
	if (player.cmd.driftturn > 0) and player.speed > minspeed and player.kartstuff[k_jmp] == 1
		and (player.kartstuff[k_drift] == 0 or player.kartstuff[k_driftend] == 1) then
		-- Starting left drift
		player.kartstuff[k_drift] = 1
		player.kartstuff[k_driftend] = 0
	elseif (player.cmd.driftturn < 0) and player.speed > minspeed and player.kartstuff[k_jmp] == 1
		and (player.kartstuff[k_drift] == 0 or player.kartstuff[k_driftend] == 1) then
		-- Starting right drift
		player.kartstuff[k_drift] = -1
		player.kartstuff[k_driftend] = 0
	elseif player.kartstuff[k_jmp] == 0 then
		-- drift is not being performed so if we're just finishing set driftend and decrement counters
		if player.kartstuff[k_drift] > 0 then
			player.kartstuff[k_drift] = $ - 1
			player.kartstuff[k_driftend] = 1
		elseif player.kartstuff[k_drift] < 0 then
			player.kartstuff[k_drift] = $ + 1
			player.kartstuff[k_driftend] = 1
		else
			player.kartstuff[k_driftend] = 0
        end
    end


	-- Incease/decrease the drift value to continue drifting in that direction
	if player.kartstuff[k_spinouttimer] == 0 and player.kartstuff[k_jmp] == 1 and player.kartstuff[k_drift] ~= 0 then
		local driftadditive = 24

		if player.kartstuff[k_drift] >= 1 then -- Drifting to the left
			player.kartstuff[k_drift] = $ + 1
			if player.kartstuff[k_drift] > 5 then
				player.kartstuff[k_drift] = 5
            end

			if player.cmd.driftturn > 0 then -- Inward
				driftadditive = $ + abs(player.cmd.driftturn)/100
            end
			if player.cmd.driftturn < 0 then -- Outward
				driftadditive = $ - abs(player.cmd.driftturn)/75
            end
		elseif player.kartstuff[k_drift] <= -1 then -- Drifting to the right
			player.kartstuff[k_drift] = $ - 1
			if player.kartstuff[k_drift] < -5 then
				player.kartstuff[k_drift] = -5
            end

			if player.cmd.driftturn < 0 then -- Inward
				driftadditive = $ + abs(player.cmd.driftturn)/100
            end
			if player.cmd.driftturn > 0 then -- Outward
				driftadditive = $ - abs(player.cmd.driftturn)/75
            end
        end

		-- Disable drift-sparks until you're going fast enough
		if player.kartstuff[k_getsparks] == 0 or (player.kartstuff[k_offroad] and not player.kartstuff[k_invincibilitytimer] and not player.kartstuff[k_hyudorotimer] and not player.kartstuff[k_sneakertimer]) then
			driftadditive = 0
        end
		if player.speed > minspeed*2 then
			player.kartstuff[k_getsparks] = 1
        end

		-- Sound whenever you get a different tier of sparks
		if (player.kartstuff[k_driftcharge] < dsone and player.kartstuff[k_driftcharge]+driftadditive >= dsone)
			or (player.kartstuff[k_driftcharge] < dstwo and player.kartstuff[k_driftcharge]+driftadditive >= dstwo)
			or (player.kartstuff[k_driftcharge] < dsthree and player.kartstuff[k_driftcharge]+driftadditive >= dsthree) then

			if P_IsLocalPlayer(player) then -- UGHGHGH...
				S_StartSoundAtVolume(player.mo, sfx_s3ka2, 192) -- Ugh...
            end
        end

		player.kartstuff[k_driftcharge] = $ + driftadditive
		player.kartstuff[k_driftend] = 0
    end

	-- Stop drifting
	if player.kartstuff[k_spinouttimer] > 0 or player.speed < minspeed then
		player.kartstuff[k_drift] = 0
        player.kartstuff[k_driftcharge] = 0
		player.kartstuff[k_aizdriftstrat] = 0
        player.kartstuff[k_brakedrift] = 0

		player.kartstuff[k_getsparks] = 0
    end

	if player.kartstuff[k_drift]
		and ((player.cmd.buttons & BT_BRAKE)
		or not (player.cmd.buttons & BT_ACCELERATE)) then
		if not player.kartstuff[k_brakedrift] then
			--K_SpawnBrakeDriftSparks(player)
        end
		player.kartstuff[k_brakedrift] = 1
	else
		player.kartstuff[k_brakedrift] = 0
    end

    if player.kartstuff[k_drift] > 0 then
		if not (player.mo.state >= S_KART_DRIFT1_L and player.mo.state <= S_KART_DRIFT2_L) then
			player.mo.state = S_KART_DRIFT1_L + leveltime % 2
        end
	elseif player.kartstuff[k_drift] < 0 then
		if not (player.mo.state >= S_KART_DRIFT1_R and player.mo.state <= S_KART_DRIFT2_R) then
			player.mo.state = S_KART_DRIFT1_R + leveltime % 2
        end
    end
end

local function K_GetKartDriftValue(player, countersteer)
	local basedrift, driftangle
	local driftweight = player.kartweight*14 -- 12

	if player.kartstuff[k_driftend] != 0 then
		return -266*player.kartstuff[k_drift] -- Drift has ended and we are tweaking their angle back a bit
    end

	basedrift = 83*player.kartstuff[k_drift] - (driftweight - 14)*player.kartstuff[k_drift]/5 -- 415 - 303
	driftangle = abs((252 - driftweight)*player.kartstuff[k_drift]/5)

	return basedrift + FixedMul(driftangle, countersteer)
end

local function K_GetKartTurnValue(player, turnvalue)
	local p_topspeed = K_GetKartSpeed(player, false)
	local p_curspeed = min(player.speed, p_topspeed * 2)
	local p_maxspeed = p_topspeed * 3
	local adjustangle = FixedDiv((p_maxspeed>>16) - (p_curspeed>>16), (p_maxspeed>>16) + player.kartweight)

    if player.kartstuff[k_drift] then
        if player.kartstuff[k_driftend] == 0 then
            -- 800 is the max set in g_game.c with angleturn
            local countersteer = FixedDiv(turnvalue*FRACUNIT, 800*FRACUNIT)
            turnvalue = K_GetKartDriftValue(player, countersteer)
        else
            turnvalue = turnvalue + K_GetKartDriftValue(player, FRACUNIT)
        end

        return turnvalue
    end

	turnvalue = FixedMul(turnvalue, adjustangle) -- Weight has a small effect on turning

	if player.kartstuff[k_invincibilitytimer] or player.kartstuff[k_sneakertimer] or player.kartstuff[k_growshrinktimer] > 0 then
		turnvalue = FixedMul(turnvalue, FixedDiv(5*FRACUNIT, 4*FRACUNIT))
    end

    return turnvalue
end

-- lua integers are always INT32 (or so), but turn code uses int16 which may (and does) overflow, so we need to simulate that
-- Well i think signed overflow is technically undefined behavior but usually it does so like this
local function int16sub(a, b)
    local r = a - b
    local PERIOD = INT16_MAX - INT16_MIN

    while r < INT16_MIN do r = r + PERIOD end
    while r > INT16_MAX do r = r - PERIOD end

    return r
end

local function int16add(a, b)
    local r = a + b
    local PERIOD = INT16_MAX - INT16_MIN

    while r < INT16_MIN do r = r + PERIOD end
    while r > INT16_MAX do r = r - PERIOD end

    return r
end

local function K_KartTurn(player)
    local pmo = player.mo
    local cmd = player.cmd

    -- If we're drifting on water, hardcode will not care because we're not on ground. So we need to do turning code ourselves
    pmo.angle = pmo.lastangle -- Disregard whatever hardcode already did

    local angle_diff, max_left_turn, max_right_turn
    local add_delta = true

    -- Kart: store the current turn range for later use
    -- (hardcode does that, not sure if i should too but i want it to be more or less consistent with hardcode sooo)
    if player.mo and player.speed > 0 then
        player.lturn_max[leveltime%MAXPREDICTTICS] = K_GetKartTurnValue(player, KART_FULLTURN)+1
        player.rturn_max[leveltime%MAXPREDICTTICS] = K_GetKartTurnValue(player, -KART_FULLTURN)-1
    else
        player.lturn_max[leveltime%MAXPREDICTTICS] = 0
        player.rturn_max[leveltime%MAXPREDICTTICS] = 0
    end

    local starttime = 6*TICRATE + (3*TICRATE/4)

    if leveltime >= starttime then
        -- KART: Don't directly apply angleturn! It may have been either A) forged by a malicious client, or B) not be a smooth turn due to a player dropping frames.
        -- Instead, turn the player only up to the amount they're supposed to turn accounting for latency. Allow exactly 1 extra turn unit to try to keep old replays synced.
        angle_diff = int16sub(cmd.angleturn, (pmo.angle>>16))
        max_left_turn = player.lturn_max[(leveltime + MAXPREDICTTICS - cmd.latency) % MAXPREDICTTICS]
        max_right_turn = player.rturn_max[(leveltime + MAXPREDICTTICS - cmd.latency) % MAXPREDICTTICS]

        --print("--------------------------------------------------")
        --print(player.name)
        --print(cmd.angleturn.." "..(pmo.angle>>16))
        --print(max_left_turn.." "..angle_diff.." "..max_right_turn)

        if angle_diff > max_left_turn then
            angle_diff = max_left_turn
        elseif angle_diff < max_right_turn then
            angle_diff = max_right_turn
        else
            -- Try to keep normal turning as accurate to 1.0.1 as possible to reduce replay desyncs.
            pmo.angle = cmd.angleturn<<16
            add_delta = false
        end

        if add_delta then
            pmo.angle = $ + (angle_diff<<16)
        end
    end
end

local function K_3dKartMovement(player, onground, forwardmove)
	local accelmax = 4000
	local newspeed, oldspeed, finalspeed
	local p_speed = K_GetKartSpeed(player, true)
	local p_accel = K_GetKartAccel(player)

    if not onground then return 0 end -- If the player isn't on the ground, there is no change in speed

    local ORIG_FRICTION = 62914

	-- ACCELCODE!!!1!11!
	oldspeed = R_PointToDist2(0, 0, player.rmomx, player.rmomy) -- FixedMul(P_AproxDistance(player.rmomx, player.rmomy), player.mo.scale)
	newspeed = FixedDiv(FixedDiv(FixedMul(oldspeed, accelmax - p_accel) + FixedMul(p_speed, p_accel), accelmax), ORIG_FRICTION)

	if player.kartstuff[k_pogospring] then -- Pogo Spring minimum/maximum thrust
		local hscale = mapobjectscale
		local minspeed = 24*hscale
		local maxspeed = 28*hscale

		if newspeed > maxspeed and player.kartstuff[k_pogospring] == 2 then
			newspeed = maxspeed
        end
		if newspeed < minspeed then
			newspeed = minspeed
        end
    end

	finalspeed = newspeed - oldspeed

	-- forwardmove is:
	--  50 while accelerating,
	--  25 while clutching,
	--   0 with no gas, and
	-- -25 when only braking.

	finalspeed = $ * forwardmove/25
	finalspeed = $ / 2

	if forwardmove < 0 and finalspeed > mapobjectscale*2 then
		return finalspeed/2
	elseif forwardmove < 0 then
		return -mapobjectscale/2
    end

	if finalspeed < 0 then
		finalspeed = 0
    end

	return finalspeed
end

-- Holy shit
-- Thats probably just some matrix multiplications unfolded but still...
local function FV3_Rotate(rotVec, axisVec, angle)
	-- Rotate the point (x,y,z) around the vector (u,v,w)
	local ux = FixedMul(axisVec.x, rotVec.x)
	local uy = FixedMul(axisVec.x, rotVec.y)
	local uz = FixedMul(axisVec.x, rotVec.z)
	local vx = FixedMul(axisVec.y, rotVec.x)
	local vy = FixedMul(axisVec.y, rotVec.y)
	local vz = FixedMul(axisVec.y, rotVec.z)
	local wx = FixedMul(axisVec.z, rotVec.x)
	local wy = FixedMul(axisVec.z, rotVec.y)
	local wz = FixedMul(axisVec.z, rotVec.z)
	local sa = sin(angle)
	local ca = cos(angle)
	local ua = ux+vy+wz
	local ax = FixedMul(axisVec.x,ua)
	local ay = FixedMul(axisVec.y,ua)
	local az = FixedMul(axisVec.z,ua)
	local xs = FixedMul(axisVec.x,axisVec.x)
	local ys = FixedMul(axisVec.y,axisVec.y)
	local zs = FixedMul(axisVec.z,axisVec.z)
	local bx = FixedMul(rotVec.x,ys+zs)
	local by = FixedMul(rotVec.y,xs+zs)
	local bz = FixedMul(rotVec.z,xs+ys)
	local cx = FixedMul(axisVec.x,vy+wz)
	local cy = FixedMul(axisVec.y,ux+wz)
	local cz = FixedMul(axisVec.z,ux+vy)
	local dx = FixedMul(bx-cx, ca)
	local dy = FixedMul(by-cy, ca)
	local dz = FixedMul(bz-cz, ca)
	local ex = FixedMul(vz-wy, sa)
	local ey = FixedMul(wx-uz, sa)
	local ez = FixedMul(uy-vx, sa)

	rotVec.x = ax+dx+ex
	rotVec.y = ay+dy+ey
	rotVec.z = az+dz+ez
end

local function P_QuantizeMomentumToSlope(momentum, slope)
	local axis = { x = 0, y = 0, z = 0 }

    if slope.flags & SL_NOPHYSICS then return end

	axis.x = -slope.d.y
	axis.y = slope.d.x
	axis.z = 0

	FV3_Rotate(momentum, axis, slope.zangle)
end

local function P_3dMovement(player)
	local cmd = player.cmd
	local movepushangle, movepushsideangle -- Analog
	local movepushforward, movepushside = 0, 0
	local dangle -- replaces old quadrants bits
	local analogmove = false
	local oldMagnitude, newMagnitude
	local totalthrust = { x = 0, y = 0, z = 0 }

	totalthrust.z = FRACUNIT*P_MobjFlip(player.mo)/3 -- A bit of extra push-back on slopes

	-- Get the old momentum this will be needed at the end of the function! -SH
	oldMagnitude = R_PointToDist2(player.mo.momx - player.cmomx, player.mo.momy - player.cmomy, 0, 0)

	analogmove = player.pflags & (1<<30) -- Not exposed zzz

	if (player.exiting or mapreset) or player.pflags & PF_STASIS or player.kartstuff[k_spinouttimer] then -- pw_introcam?
		cmd.forwardmove, cmd.sidemove = 0, 0
		if player.kartstuff[k_sneakertimer] then
			cmd.forwardmove = 50
        end
    end

	if not (player.pflags & PF_FORCESTRAFE) and not player.kartstuff[k_pogospring] then
		cmd.sidemove = 0
    end

	if analogmove then
		movepushangle = cmd.angleturn<<16
	else
		if player.kartstuff[k_drift] != 0 then
			movepushangle = player.mo.angle-(ANGLE_45/5)*player.kartstuff[k_drift]
		elseif player.kartstuff[k_spinouttimer] or player.kartstuff[k_wipeoutslow] then
			movepushangle = player.kartstuff[k_boostangle]
        else
			movepushangle = player.mo.angle
        end
    end

	movepushsideangle = movepushangle-ANGLE_90

	-- cmomx/cmomy stands for the conveyor belt speed.
	if player.onconveyor == 2 then -- Wind/Current
		if not (player.mo.eflags & (MFE_UNDERWATER|MFE_TOUCHWATER)) then
			player.cmomx, player.cmomy = 0, 0
        end
	elseif player.onconveyor != 2 and player.onconveyor != 4 and player.onconveyor != 1 then
		player.cmomx, player.cmomy = 0, 0
    end

	player.rmomx = player.mo.momx - player.cmomx
	player.rmomy = player.mo.momy - player.cmomy

	-- Calculates player's speed based on distance-of-a-line formula
	player.speed = R_PointToDist2(0, 0, player.rmomx, player.rmomy)

	-- Monster Iestyn - 04-11-13
	-- Quadrants are stupid, excessive and broken, let's do this a much simpler way!
	-- Get delta angle from rmom angle and player angle first
	dangle = R_PointToAngle2(0,0, player.rmomx, player.rmomy) - player.mo.angle
	if dangle > ANGLE_180 then --flip to keep to one side
		dangle = InvAngle(dangle)
    end

	-- anything else will leave both at 0, so no need to do anything else

	-- When sliding, don't allow forward/back
	if player.pflags & PF_SLIDING then
		cmd.forwardmove = 0
    end

	-- Do not let the player control movement if not onground.
	-- SRB2Kart: pogo spring and speed bumps are supposed to control like you're on the ground
	local onground = true -- Called when waterrunning, so just force this to true

	player.aiming = cmd.aiming<<FRACBITS

	-- Forward movement
	if not ((player.exiting or mapreset) or (P_PlayerInPain(player) and not onground)) then
		--movepushforward = cmd.forwardmove * (thrustfactor * acceleration)
		movepushforward = K_3dKartMovement(player, onground, cmd.forwardmove)

		-- don't need to account for scale here with kart accel code
		--movepushforward = FixedMul(movepushforward, player.mo.scale)

		if player.mo.movefactor ~= FRACUNIT then -- Friction-scaled acceleration...
			movepushforward = FixedMul(movepushforward, player.mo.movefactor)
        end

		if cmd.buttons & BT_BRAKE and not cmd.forwardmove then -- SRB2kart - braking isn't instant
			movepushforward = $ / 64
        end

		totalthrust.x = $ + P_ReturnThrustX(player.mo, movepushangle, movepushforward)
		totalthrust.y = $ + P_ReturnThrustY(player.mo, movepushangle, movepushforward)
	elseif not player.kartstuff[k_spinouttimer] then
		K_MomentumToFacing(player)
    end

	-- Sideways movement
	if cmd.sidemove != 0 and !((player.exiting or mapreset) or player.kartstuff[k_spinouttimer]) then
		if cmd.sidemove > 0 then
			movepushside = (cmd.sidemove * FRACUNIT/128) + FixedDiv(player.speed, K_GetKartSpeed(player, true))
		else
			movepushside = (cmd.sidemove * FRACUNIT/128) - FixedDiv(player.speed, K_GetKartSpeed(player, true))
        end

		totalthrust.x = $ + P_ReturnThrustX(player.mo, movepushsideangle, movepushside)
		totalthrust.y = $ + P_ReturnThrustY(player.mo, movepushsideangle, movepushside)
    end

    -- Too lazy to mess with slopes for now
    local slope = player.mo.standingwaterslope
	if (totalthrust.x or totalthrust.y)
		and slope and (not (slope.flags & SL_NOPHYSICS)) and abs(slope.zdelta) > FRACUNIT/2 then
		-- Factor thrust to slope, but only for the part pushing up it!
		-- The rest is unaffected.
		local thrustangle = R_PointToAngle2(0, 0, totalthrust.x, totalthrust.y)-slope.xydirection

		if slope.zdelta < 0 then -- Direction goes down, so thrustangle needs to face toward
			if thrustangle < ANGLE_90 or thrustangle > ANGLE_270 then
				P_QuantizeMomentumToSlope(totalthrust, slope)
            end
		else -- Direction goes up, so thrustangle needs to face away
			if thrustangle > ANGLE_90 and thrustangle < ANGLE_270 then
				P_QuantizeMomentumToSlope(totalthrust, slope)
            end
        end
    end

	player.mo.momx = $ + totalthrust.x
	player.mo.momy = $ + totalthrust.y

	-- Time to ask three questions:
	-- 1) Are we over topspeed?
	-- 2) If "yes" to 1, were we moving over topspeed to begin with?
	-- 3) If "yes" to 2, are we now going faster?

	-- If "yes" to 3, normalize to our initial momentum this will allow thoks to stay as fast as they normally are.
	-- If "no" to 3, ignore it the player might be going too fast, but they're slowing down, so let them.
	-- If "no" to 2, normalize to topspeed, so we can't suddenly run faster than it of our own accord.
	-- If "no" to 1, we're not reaching any limits yet, so ignore this entirely!
	-- -Shadow Hog
	newMagnitude = R_PointToDist2(player.mo.momx - player.cmomx, player.mo.momy - player.cmomy, 0, 0)
	if newMagnitude > K_GetKartSpeed(player, true) then
		local tempmomx, tempmomy
		if oldMagnitude > K_GetKartSpeed(player, true) and onground then -- SRB2Kart: onground check for air speed cap
			if newMagnitude > oldMagnitude then
				tempmomx = FixedMul(FixedDiv(player.mo.momx - player.cmomx, newMagnitude), oldMagnitude)
				tempmomy = FixedMul(FixedDiv(player.mo.momy - player.cmomy, newMagnitude), oldMagnitude)
				player.mo.momx = tempmomx + player.cmomx
				player.mo.momy = tempmomy + player.cmomy
            end
		else
			tempmomx = FixedMul(FixedDiv(player.mo.momx - player.cmomx, newMagnitude), K_GetKartSpeed(player, true)) --topspeed)
			tempmomy = FixedMul(FixedDiv(player.mo.momy - player.cmomy, newMagnitude), K_GetKartSpeed(player, true)) --topspeed)
			player.mo.momx = tempmomx + player.cmomx
			player.mo.momy = tempmomy + player.cmomy
        end
    end
end

local function K_KartMove(player)
    local pmo = player.mo

    if pmo["old"..overwrite[1]] ~= nil then
        for _, field in ipairs(overwrite) do
            pmo[field] = pmo["old"..field]
        end

        P_3dMovement(player)
    end

    -- Slowing down on water (unless you have boosts that grant offroad immunity)
    local slowdown = 20*FRACUNIT/TICRATE

    if player.kartstuff[k_sneakertimer] > 0 or player.kartstuff[k_invincibilitytimer] > 0 then
        slowdown = 0
    end

    local momangle = R_PointToAngle2(0, 0, pmo.momx, pmo.momy)

    P_Thrust(pmo, momangle+ANGLE_180, slowdown)
end

local function K_KartUseItems(player)
    local attack = player.attackdown == 1

    if attack then
        if player.kartstuff[k_rocketsneakertimer] then
			K_DoSneaker(player, 2)
			K_PlayBoostTaunt(player.mo)
			player.kartstuff[k_rocketsneakertimer] = $ - 2*TICRATE
			if player.kartstuff[k_rocketsneakertimer] < 1 then
				player.kartstuff[k_rocketsneakertimer] = 1
            end
        elseif player.kartstuff[k_itemamount] > 0 then
            if player.kartstuff[k_itemtype] == KITEM_SNEAKER then
                K_DoSneaker(player, 1)
                player.kartstuff[k_itemamount] = $ - 1
            elseif player.kartstuff[k_itemtype] == KITEM_ROCKETSNEAKER then
                player.kartstuff[k_rocketsneakertimer] = 3*8*TICRATE
                S_StartSound(player.mo, sfx_s3k3a)
                local prev = player.mo
                for i = 0, 1 do
                    local mo = P_SpawnMobj(player.mo.x, player.mo.y, player.mo.z, MT_ROCKETSNEAKER)
                    K_MatchGenericExtraFlags(mo, player.mo)
                    mo.flags = $ | MF_NOCLIPTHING
                    mo.angle = player.mo.angle
                    mo.target = player.mo
                    mo.hprev = prev
                    prev.hnext = mo
                    prev = mo
                end
            end
        end
    end
end

-- Returns true if mobj is on top of woter and can waterrun
-- Dumb game doesn't set MFE_UNDERWATER so i gotta do that myself
local function checkWater(mo)
    local flip = mo.eflags & MFE_VERTICALFLIP

    local touchingwater = false

    for rover in mo.subsector.sector.ffloors() do
        if (rover.flags & WATERFOF) ~= WATERFOF then continue end
        if (rover.flags & FF_GOOWATER) == FF_GOOWATER then continue end -- Let this do its job

        local top = rover.topheight
        local bottom = rover.bottomheight

        if rover.t_slope then top = P_GetZAt(rover.t_slope, mo.x, mo.y) end
        if rover.b_slope then bottom = P_GetZAt(rover.b_slope, mo.x, mo.y) end

        -- Fully underwater
        if mo.z > bottom and mo.z + mo.height < top then
            mo.underwater = true
            return false -- underwater mobj can't waterride
        elseif (mo.z < top and mo.z + mo.height > top) or (mo.z < bottom and mo.z + mo.height > bottom) then
            touchingwater = true
        end

        if mo.underwater then continue end -- underwater mobjs can't waterride

        local zdiff, zpos

        local slope = false

        if flip then
            slope = rover.b_slope ~= nil
            zdiff = mo.z + mo.height - bottom
            zpos = bottom - mo.height
        else
            slope = rover.t_slope ~= nil
            zdiff = top - mo.z
            zpos = top
        end

        -- On sloped water fofs, lets just allow projectiles to be more submerged so they can "climb" the slope up
        if zdiff >= 0 and zdiff <= mo.height/(slope and 1 or 2) then
            mo.standingwaterslope = flip and rover.b_slope or rover.t_slope -- We might need this
            return true, zpos
        end
    end

    -- Reset this flag only when we aren't touching water completely
    if not touchingwater then
        mo.underwater = false
    end

    return false
end

local function K_KartWaterRun(player)
    if not player.mo then return end

    local mo = player.mo

    if player.cmd.buttons & BT_ATTACK then
        player.attackdown = ($ or 0) + 1
    else
        player.attackdown = 0
    end

    local kartspeed = K_GetKartSpeed(player, false)

    -- Also allow startboost, so respawning next to water isn't too punishing
    local hassneaker = (player.kartstuff[k_sneakertimer] > 0) or (player.kartstuff[k_startboost] > 0)

    local MINSPEED = kartspeed
    local SNEAKER_MINSPEED = kartspeed/2
    local MAXSPEED = MINSPEED*4 -- For visual effect only

    local flip = mo.eflags & MFE_VERTICALFLIP
    local goingdown = (flip and mo.momz >= 0) or (not flip and mo.momz <= 0)
    local speed = FixedHypot(mo.momx, mo.momy)

    mo.standingwaterslope = nil -- Gets set again in checkWater, if player is waterrunning on a slope
    mo.waterrunning = false

    local on_water, zpos = checkWater(mo)

    if on_water and not P_IsObjectOnGround(mo) and not P_PlayerInPain(player) and goingdown and abs(mo.momz) < 20*FRACUNIT and (speed > (hassneaker and SNEAKER_MINSPEED or MINSPEED)) then
        mo.waterrunning = true
        mo.z = zpos
        mo.momz = 0

        -- Doesn't do anything, but set for other scripts maybe. Man i wish it was that easy.......
        mo.eflags = $ | MFE_ONGROUND
		
		local scale = rescale(min(max(speed, MINSPEED), MAXSPEED), MINSPEED, MAXSPEED, mo.scale, 4*mo.scale)

        P_WaterRunEffect(mo, zpos, scale)

        -- Friction on woter
        mo.movefactor = 8*FRACUNIT/10

        K_KartTurn(player) -- Turning (cam angle) code
        K_KartMove(player) -- Movement code
        K_KartDrift(player) -- Drift spark code
        K_KartUseItems(player) -- Item use code (limited to sneakers and rocketsneakers)
    end

    mo.lastangle = mo.angle

    if not player.lturn_max then
        player.lturn_max = {}
        player.rturn_max = {}

        for i = 0, 11 do
            player.lturn_max[i] = 0
            player.rturn_max[i] = 0
        end
    end

    for _, field in ipairs(overwrite) do
        mo["old"..field] = mo[field]
    end
end

local function INT16(val)
    return max(min(val, INT16_MAX), INT16_MIN)
end

addHook("ThinkFrame", function()
    if not playerwaterrun.running then return end

    for p in players.iterate do
        K_KartWaterRun(p)
    end
end)

-- If we're waterrunning, make all other mods think we're on ground. Well,
-- except for those that do `local x = x` optimization...
local oldIsObjectOnGround = P_IsObjectOnGround
rawset(_G, "P_IsObjectOnGround", function(mo)
	assert(mo and mo.valid)
	
	if mo.waterrunning then return true end
	
	return oldIsObjectOnGround(mo)
end)

-- Fuck you, game >:(
-- Turns out, turning breaks if you have even 1 frame of delay, so i need to also rewrite cmd.angleturn without having direct access to localangle and stuff like that

local localangle = {}

addHook("PlayerCmd", function(player, cmd)
    if not (player.mo and player.mo.waterrunning) then
        localangle[#player] = nil -- forget the angle for now
        return
    end

    -- Its first tic of waterrun, this seems to be best way to retreive localangle
    if localangle[#player] == nil then
        localangle[#player] = cmd.angleturn<<16
    end

    local lang = localangle[#player]

    lang = lang + K_GetKartTurnValue(player, cmd.driftturn)<<16

    cmd.angleturn = lang>>16

    localangle[#player] = lang
end)
