/*
	client/main.qc

	main csqc code

	Copyright (C) 2021-2024 NZ:P Team

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	as published by the Free Software Foundation; either version 2
	of the License, or (at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

	See the GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to:

		Free Software Foundation, Inc.
		59 Temple Place - Suite 330
		Boston, MA  02111-1307, USA

*/

void() ToggleMenu =
{
	if (serverkey("constate") != "disconnected")
	{
		if (player_count == 0)
			localcmd("cmd pause\n");

		if(current_menu == MENU_NONE)
		{
			current_menu = MENU_PAUSE;
			setcursormode(TRUE, cvar_string("cl_cursor"), __NULL__, cvar("cl_cursor_scale"));
		}
		else
		{
			menu_paus_submenu = 0;
			current_menu = MENU_NONE;
			setcursormode(FALSE);
		}
	}
}

//
// SetGamepadBindings()
// Since our menu architecture currently sucks,
// we don't yet support a good binds menu that
// let's you get good gamepad control.. so
// force these binds (sorry!)
//
void() SetGamepadBindings =
{
	localcmd("unbindall;  bind ~ \"toggleconsole\"; bind ` \"toggleconsole\"; bind ESCAPE \"togglemenu\"; joyadvaxisr 4; joyadvaxisu 2; joyadvaxisx 3; joyadvaxisy -1; bind JOY1 \"+button5\"; bindlevel JOY2 30 \"null\"; bind JOY3 \"+button4\"; bind GP_LSHOULDER \"impulse 33\"; bind AUX1 \"+moveup\"; bind AUX2 \"+button3\"; bindlevel AUX3 30 \"null\"; bindlevel AUX4 30 \"null\"; bind AUX5 \"togglemenu\"; bind AUX10 \"+attack\"; bind GP_Y \"+button4\"; bindlevel GP_A 30 \"impulse 10\"; bindlevel GP_B 30 \"impulse 30\"; bindlevel GP_X 30 \"+button5\"; bindlevel GP_LTHUMB 30 \"impulse 23\"; bindlevel GP_RTHUMB 30 \"+button6\"; bind GP_LTRIGGER \"+button8\"; bindlevel GP_DPAD_DOWN 30 \"+button7\"; bindlevel GP_RSHOULDER 30 \"+button3\"; bindlevel GP_DPAD_RIGHT 30 \"impulse 33\"; bindlevel GP_DPAD_UP 30 \"+button7\"; bind GP_VIEW \"showscores\"; joysidesensitivity 0.8; joyyawsensitivity 0.6; joypitchsensitivity 0.55\n");
}

float(float isnew) SetZombieSkinning =
{
	self.drawmask = MASK_ENGINE;
	setcustomskin(self, __NULL__, sprintf("replace \"\" \"models/ai/zfull.mdl_%d.pcx\"\n", self.skin));
	return PREDRAW_NEXT;
};

noref void(float apiver, string enginename, float enginever) CSQC_Init =
{
	setwindowcaption("Nazi Zombies: Portable");
	
	precache_sound("sounds/menu/enter.wav");
	precache_sound("sounds/menu/navigate.wav");
	precache_model("models/player.mdl");

	registercommand("togglemenu");
	registercommand("promptjoin");
	registercommand("showscores");

	cvar_set("sv_cheats", ftos(1));
	cvar_set("r_fb_models", ftos(0));
	autocvar(r_viewmodel_default_fov, 70);
	autocvar(cl_controllerglyphs, "xbox");
	autocvar(in_rumbleenabled, 1);
	autocvar(in_aimassist, 0);

	// Runtime check if we're running this in WebASM/WebGL.
	if (cvar_string("sys_platform") == "Web")
		platform_is_web = true;
	else
		platform_is_web = false;

	if (platform_is_web) {
		cvar_set("com_protocolname", "NZP-REBOOT-WEB");
	}

	// cvars for custom settings
	autocvar(cl_cinematic, 0);
	autocvar(nzp_particles, 1);
	autocvar(nzp_decals, 1);
	autocvar(vid_ultrawide_limiter, 0);
	autocvar(cl_bobside, 0.02);

	// per-channel volume
	autocvar(snd_channel1volume, 1);
	autocvar(snd_channel2volume, 1);
	autocvar(snd_channel3volume, 1);
	autocvar(snd_channel4volume, 1);
	autocvar(snd_channel5volume, 1);
	autocvar(snd_channel6volume, 1);

	// FTE is overriding cl_bobup values.. so be evil
	// and force it to 0.02.
	cvar_set("cl_bobup", "0.02");

	// force nearest filtering for hud elements to avoid blur
	cvar_set("gl_texturemode2d", "gl_nearest");
	cvar_set("r_font_linear", "0");

	// force build date text in menu
	cvar_set("cl_showbuildtime", "1");

	// in-game stopwatch
	autocvar(scr_serverstopwatch, 0);
	stopwatch_sec = stopwatch_min = stopwatch_hr = 0;
	stopwatch_round_sec = stopwatch_round_min = stopwatch_round_hr = 0;
	stopwatch_round_starttime = time;

	// FTE via emscripten (for the fault of the browser?) bugs out
	// when using mousewheel. So as a hack, just unbind the mouse
	// wheel. :/
	if (platform_is_web) {
		localcmd("unbind MWHEELUP; unbind MWHEELDOWN\n");
		localcmd("bind 1 +button4; bind 2 +button4\n");
	}

	//
	// get the build date
	//
	float file = fopen("version.txt", FILE_READ);

	if (file != -1) {
		build_datetime = fgets(file);
		fclose(file);
	}

	// Intercept every Zombie mesh to apply custom skinning
	for(float i = 0; i < zombie_skins.length; i++) {
		deltalisten(zombie_skins[i], SetZombieSkinning, 0);
	}

	InitKerningMap();
};

noref void() CSQC_WorldLoaded =
{
	Achievement_Init();
	Particles_Init();
	nameprint_time = time + 8;

	huddir = "gfx/hud/";
};

vector weapon_bob_factor;
float weapon_bob_factor_z_coef;
vector dampening_factor;
float vaccel;
float vzaccel;

// FIXME: Move Power-Up CSQC drawing to separate file :)
float 	mdlflag_poweruprotate_duration;
float 	mdlflag_poweruprotate_starttime;

vector 	mdlflag_poweruprotate_startangles;
vector 	mdlflag_poweruprotate_differenceangles;
vector 	mdlflag_poweruprotate_currentangles;

float 	last_puframetime;

void() PU_UpdateAngles =
{
	// Don't update more than once per frame.
	if (last_puframetime != frametime) {
		// New cycle, dictate new rotation time and target angle. 
		if (mdlflag_poweruprotate_duration <= time) {
			mdlflag_poweruprotate_starttime = time;
			mdlflag_poweruprotate_duration = time + (random() * 25 + 25)/10; // Take between 2.5 and 5 seconds.

			mdlflag_poweruprotate_startangles[0] = mdlflag_poweruprotate_currentangles[0];
			mdlflag_poweruprotate_startangles[1] = mdlflag_poweruprotate_currentangles[1];
			mdlflag_poweruprotate_startangles[2] = mdlflag_poweruprotate_currentangles[2];

			float target_pitch = (random() * 120) - 60;
			float target_yaw = (random() * 240) + 60;
			float target_roll = (random() * 90) - 45;

			vector target_angles;
			target_angles[0] = target_pitch;
			target_angles[1] = target_yaw;
			target_angles[2] = target_roll;

			// Calculate the difference from our start to our target.
			for(float i = 0; i < 2; i++) {
				if (mdlflag_poweruprotate_currentangles[i] > target_angles[i])
					mdlflag_poweruprotate_differenceangles[i] = (mdlflag_poweruprotate_currentangles[i] - target_angles[i]) * -1;
				else
					mdlflag_poweruprotate_differenceangles[i] = fabs(mdlflag_poweruprotate_currentangles[i] - target_angles[i]);
			}
		}

		float percentage_complete = (time - mdlflag_poweruprotate_starttime) / (mdlflag_poweruprotate_duration - mdlflag_poweruprotate_starttime);

		for(float j = 0; j < 2; j++) {
			mdlflag_poweruprotate_currentangles[j] = mdlflag_poweruprotate_startangles[j] + (mdlflag_poweruprotate_differenceangles[j] * percentage_complete);
		}

		last_puframetime = frametime;
	}
};

float() PU_PreDraw =
{
	PU_UpdateAngles();
	self.angles = mdlflag_poweruprotate_currentangles;
	addentity(self);

	return PREDRAW_NEXT;
};

float Player_PreDraw() =
{
	self.lerpfrac -= frametime*10;
	while(self.lerpfrac < 0) {
		self.frame2 = self.frame;
		self.lerpfrac += 1;
	}

    if (self.entnum == player_localentnum) {
        self.movetype = MOVETYPE_WALK;
		// Prepare rollback
		vector vOldOrigin = self.origin;
		vector vOldVelocity = self.velocity;
		float fOldPMoveFlags = self.pmove_flags;

		// Apply physics for every single input-frame that has not yet been
		// acknowledged by the server (servercommandframe = last acknowledged frame)
		for (int i = servercommandframe + 1; i <= clientcommandframe; i++) {
		float flSuccess = getinputstate(i);

			if (flSuccess == FALSE) {
				continue;
			}

			// Partial frames are the worst
			if (input_timelength == 0) {
				break;
			}
			runstandardplayerphysics(self);
		}
				
		// Smooth stair stepping, this has to be done manually!
		playerOriginOld = playerOrigin;
				
		if ((self.flags & FL_ONGROUND) && (self.origin_z - playerOriginOld_z > 0)) {
			playerOriginOld_z += frametime * 150;
					
			if (playerOriginOld_z > self.origin_z) {
				playerOriginOld_z = self.origin_z;
			}
			if (self.origin_z - playerOriginOld_z > 18) {
				playerOriginOld_z = self.origin_z - 18;
			}
			playerOrigin_z += playerOriginOld_z - self.origin_z;
		} else {
			playerOriginOld_z = self.origin_z;
		}
			
		playerOrigin = [self.origin_x, self.origin_y, playerOriginOld_z];
		playerVelocity = self.velocity;
		addentity(self);

		// Time to roll back
		self.origin = vOldOrigin;
		setorigin(self, self.origin);
		self.velocity = vOldVelocity;
		self.pmove_flags = fOldPMoveFlags;
		self.movetype = MOVETYPE_NONE;

		// Set renderflag for mirrors!
		self.renderflags = RF_EXTERNALMODEL;
    } else {
        addentity(self);
    }
    return PREDRAW_NEXT;
}

noref void(float isnew) CSQC_Ent_Update =
{
	float ent_type = readbyte();

	// Player
	if (ent_type == 1) {
		if (isnew == TRUE) {
			self.classname = "player";
			self.solid = SOLID_SLIDEBOX;
			self.predraw = Player_PreDraw;
			self.drawmask = MASK_ENGINE;
			setmodel(self, "models/player.mdl");
		}

		float old_points = self.points;
			
		self.origin_x = readcoord();
		self.origin_y = readcoord();
		self.origin_z = readcoord();
		self.angles_x = readangle();
		self.angles_y = readangle();
		self.angles_z = readangle();
		self.velocity_x = readshort();
		self.velocity_y = readshort();
		self.velocity_z = readshort();
		self.playernum = readbyte();
		self.modelindex = readshort();
		self.frame = readbyte();
		self.movetype = readshort();
		self.flags = readshort();
		self.stance = readbyte();
		self.points = readfloat();	// FIXME: this should be made a short, but I know we use price of 1 for some test maps, so I can't do /10 *10 shenanigans.
		self.kills = readshort();
		self.is_in_menu = readbyte();

		RegisterPointChange(self.points - old_points, self.playernum);

		if (self.movetype == MOVETYPE_BOUNCE)
			self.solid = SOLID_NOT;
		else
			self.solid = SOLID_SLIDEBOX;
			
		setmodelindex(self, self.modelindex);

		if (map_compatibility_mode != MAP_COMPAT_BETA) {
			if (self.stance == 2)
				setsize(self, PLAYER_MINS_STANDING, PLAYER_MAXS_STANDING);
			else
				setsize(self, PLAYER_MINS_CROUCHING, PLAYER_MAXS_CROUCHING);
		} else {
			setsize(self, PLAYER_MINS_QUAKE, PLAYER_MAXS_QUAKE);
		}
	} 
	// Power-Up
	else if (ent_type == 2) {
		if (isnew == TRUE) {
			self.classname = "item_powerup";
			self.solid = SOLID_NOT;
			self.predraw = PU_PreDraw;
			self.drawmask = MASK_ENGINE;
		}

		self.origin_x = readcoord();
		self.origin_y = readcoord();
		self.origin_z = readcoord();
		self.modelindex = readshort();

		setmodelindex(self, self.modelindex);
	}
	else {
		if(isnew)
			addentity(self);
	}
}

float(__inout vector v) VectorNormalize =
{
	float length = sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);

	if (length)
	{
		float ilength = 1 / length;
		v[0] *= ilength;
		v[1] *= ilength;
		v[2] *= ilength;
	}
		
	return length;
}

void() DropRecoilKick =
{
	float	len;

	if (crosshair_spread_time > time)
		return;

	len = VectorNormalize(gun_kick);

	len = len - frametime*5;
	if (len < 0)
		len = 0;

	gun_kick[0] *= len;
	gun_kick[1] *= len;
	gun_kick[2] *= len;
}

//
// Key_IsControllerGlyph(bind)
// Returns TRUE if the string provided belongs to
// a gamepad/controller.
//
float(string bind) Key_IsControllerGlyph =
{
	switch(bind) {
		case "GP_A":
		case "GP_B":
		case "GP_X":
		case "GP_Y":
		case "GP_LSHOULDER":
		case "GP_RSHOULDER":
		case "GP_LTRIGGER":
		case "GP_RTRIGGER":
		case "GP_BACK":
		case "GP_START":
		case "GP_LTHUMB":
		case "GP_RTHUMB":
		case "GP_DPAD_UP":
		case "GP_DPAD_DOWN":
		case "GP_DPAD_LEFT":
		case "GP_DPAD_RIGHT":
			return true;
	}

	return false;
};

//
// Key_DrawControllerGlyph(position, bind, scale)
// Draws the associated glyph for the bind name from the
// Glyph tilemap at the provided position, with the
// provided scale.
//
void(vector position, string bind, vector scale) Key_DrawControllerGlyph =
{
	vector tilemap_position = [0, 0];

	// The coordinates in 0-1 range of each button in the glyph tilemap.
	switch(bind) {
		case "GP_A":
			tilemap_position = [0, 0];
			break;
		case "GP_B":
			tilemap_position = [0.125, 0];
			break;
		case "GP_X":
			tilemap_position = [0.250, 0];
			break;
		case "GP_Y":
			tilemap_position = [0.375, 0];
			break;
		case "GP_LSHOULDER":
			tilemap_position = [0.250, 0.125];
			break;
		case "GP_RSHOULDER":
			tilemap_position = [0.375, 0.125];
			break;
		case "GP_LTRIGGER":
			tilemap_position = [0.500, 0.125];
			break;
		case "GP_RTRIGGER":
			tilemap_position = [0.625, 0.125];
			break;
		case "GP_BACK":
			tilemap_position = [0.875, 0.125];
			break;
		case "GP_START":
			tilemap_position = [0.750, 0.125];
			break;
		case "GP_LTHUMB":
			tilemap_position = [0, 0.125];
			break;
		case "GP_RTHUMB":
			tilemap_position = [0.125, 0.125];
			break;
		case "GP_DPAD_UP":
			tilemap_position = [0.500, 0];
			break;
		case "GP_DPAD_DOWN":
			tilemap_position = [0.625, 0];
			break;
		case "GP_DPAD_LEFT":
			tilemap_position = [0.750, 0];
			break;
		case "GP_DPAD_RIGHT":
			tilemap_position = [0.875, 0];
			break;
	}

	drawsubpic([position_x, position_y], [scale_x, scale_y], sprintf("gfx/controller_glyphs/%s.tga", cvar_string("cl_controllerglyphs")), [tilemap_position_x, tilemap_position_y], [0.125, 0.125], [1, 1, 1], 1);
};

#define SCALE_CONSTANT 		8

//MOVEME
float(float a) angledelta =
{
	a = anglemod(a);
	if (a > 180)
		a -= 360;
	return a;
}

//MOVEME
float delta_pitch, delta_yaw;
vector sniper_sway;
// Sways the camera while scoped in.
void() Camera_SniperSway =
{
	if (getstatf(STAT_WEAPONZOOM) != 2 || (getstatf(STAT_PERKS) & P_DEAD)) {
		sniper_sway = '0 0 0';
		return;
	}

	delta_pitch = (cos(cltime/0.7) + cos(cltime) + sin(cltime/1.1)) * 0.5;
	delta_yaw = (sin(cltime/0.4) + cos(cltime/0.56) + sin(cltime)) * 0.5;

	sniper_sway[0] = angledelta(delta_pitch);
	sniper_sway[1] = angledelta(delta_yaw);
};
float gamepad_enabled;

// CALLED EVERY CLIENT RENDER FRAME
float pap_flash_alternate;
noref void(float width, float height, float menushown) CSQC_UpdateView =
{
	//clear and update our global screen resolution vars
	clearscene();
	g_width = width;
	g_height = height;
	
	// camang is controlled by our punchangles
	camang = getproperty(VF_ANGLES);
	
	//disable quake status bar and quake crosshair
	setviewprop(VF_DRAWENGINESBAR, 0);	
	setviewprop(VF_DRAWCROSSHAIR, 0);

	float sensitivity_factor;

	if (current_menu != MENU_NONE)
		sensitivity_factor = 0;
	else
		sensitivity_factor = (1 + SCALE_CONSTANT * getstatf(STAT_VIEWZOOM)) / (1 + SCALE_CONSTANT);

	if (gamepad_enabled && getstatf(STAT_FACINGENEMY) && cvar("in_aimassist") == 1)
		sensitivity_factor *= 0.5;

	setsensitivityscaler(sensitivity_factor);

	setviewprop(VF_AFOV, autocvar(fov,90)*getstatf(STAT_VIEWZOOM));

	cvar_set("r_viewmodel_fov", ftos(cvar("r_viewmodel_default_fov")*getstatf(STAT_VIEWZOOM)));

	// Increment the stopwatch
	// FIXME: I don't really liket his being in UpdateView.. this has nothing to do with rendering.
	stopwatch_sec = time - (stopwatch_min * 60 + (stopwatch_hr * 3600));

	if (stopwatch_sec >= 60) {
		stopwatch_min += stopwatch_sec/60;
	}

	if (stopwatch_min >= 60) {
		stopwatch_hr += stopwatch_min/60;
		stopwatch_min = 0;
	}

	if (stopwatch_round_isactive) {
		stopwatch_round_sec = (time - stopwatch_round_starttime) - (stopwatch_round_min * 60 + (stopwatch_round_hr * 3600));

		if (stopwatch_round_sec >= 60) {
			stopwatch_round_min += stopwatch_round_sec/60;
		}

		if (stopwatch_round_min >= 60) {
			stopwatch_round_hr += stopwatch_round_min/60;
			stopwatch_round_min = 0;
		}
	} else {
		stopwatch_round_starttime = time;
	}

	//autoadd entities received from servers for drawing	
	addentities(MASK_ENGINE);

	setproperty(VF_ORIGIN, playerOrigin + [ 0, 0, getstatf(STAT_VIEWHEIGHT)]);
	//setproperty(VF_ANGLES, view_angles);

	// Draw the client's viewmodels.
	ViewModel_Draw();
	
	DropRecoilKick();
	Camera_SniperSway();

	camang[0] += gun_kick[0];
	camang[1] += gun_kick[1];
	camang[2] += gun_kick[2];
	camang[0] += sniper_sway[0];
	camang[1] += sniper_sway[1];
	camang[2] += sniper_sway[2];

	setviewprop(VF_ANGLES, camang);

	//does what you think it does
	renderscene();

	if (current_menu != MENU_NONE)
	{
		// sorta-nasty hack: flashes should still draw if we're
		// drawing the menu.
		if (screenflash_duration > time)
			HUD_Screenflash();

		setlocaluserinfo(0, "in_menu", "1");

		sui_begin(g_width, g_height);
		root_menu([g_width, g_height]);
		sui_end();
	}
	else
	{
		setcursormode(FALSE);
		HUD_Draw(g_width, g_height);
		Chat_Draw();
		setlocaluserinfo(0, "in_menu", "0");
	}
};

noref float(string cmd) CSQC_ConsoleCommand =
{
	//self = theplayer;
	//if (!self)
	//	return FALSE;

	tokenize(cmd);
	switch(argv(0))
	{
		case "togglemenu":
			ToggleMenu();
			return TRUE;
		case "map":
			return FALSE;
		case "showscores":
			if (score_show)
				score_show = FALSE;
			else
				score_show = TRUE;
			return TRUE;
		default:
			return FALSE;
	}
	return FALSE;
};
//**********************************************************************//
// Input_Movecheck                                                      //
//                                                                      //
// Called at InputEvent and allows to set var if key is at that state   //
// NOTE: ALL movekeys are called in order to prevent unsetting keys     //
//**********************************************************************//

void(float scanx, float setval) Input_Movecheck = 
{
	tokenize(findkeysforcommand("+moveleft"));
	if (scanx == stof(argv(0)))
		K_LEFTDOWN = setval;
	
	tokenize(findkeysforcommand("+moveright"));
	if (scanx == stof(argv(0)))
		K_RIGHTDOWN = setval;
		
	tokenize(findkeysforcommand("+forward"));
	if (scanx == stof(argv(0)))
		K_FORWARDDOWN = setval;
		
	tokenize(findkeysforcommand("+back"));
	if (scanx == stof(argv(0)))
		K_BACKDOWN = setval;
}

noref float(float evtype, float scanx, float chary, float devid) CSQC_InputEvent =
{
	if (evtype == IE_GYROSCOPE || evtype == IE_ACCELEROMETER)
		return FALSE;

	// Ignore subtle (drift-y) joystick axes
	if (evtype == IE_JOYAXIS && devid > 0 && fabs(chary) < 0.1) {
		return FALSE;
	}

	last_input_deviceid = devid;
	float last_input_storage = last_input_was_gamepad;

	if (last_input_deviceid > 0)
		last_input_was_gamepad = TRUE;
	else
		last_input_was_gamepad = FALSE;

	if (current_menu != MENU_NONE)
	{
		return sui_input_event(evtype, scanx, chary, devid);
	}

	if (evtype == IE_KEYDOWN) {
		switch (scanx) {
			case K_GP_A:
			case K_GP_B:
			case K_GP_X:
			case K_GP_Y:
			case K_GP_LSHOULDER:
			case K_GP_RSHOULDER:
			case K_GP_LTRIGGER:
			case K_GP_RTRIGGER:
			case K_GP_BACK:
			case K_GP_START:
			case K_GP_LTHUMB:
			case K_GP_RTHUMB:
			case K_GP_DPAD_UP:
			case K_GP_DPAD_DOWN:
			case K_GP_DPAD_LEFT:
			case K_GP_DPAD_RIGHT:
			case K_GP_LTHUMB_UP:
			case K_GP_LTHUMB_DOWN:
			case K_GP_LTHUMB_LEFT:
			case K_GP_LTHUMB_RIGHT:
			case K_GP_RTHUMB_UP:
			case K_GP_RTHUMB_DOWN:
			case K_GP_RTHUMB_LEFT:
			case K_GP_RTHUMB_RIGHT:
				if (!GPButtonHeldBeginTime[scanx-816])
					GPButtonHeldBeginTime[scanx-816] = cltime;
				break;

		}
	}

	if (evtype == IE_KEYUP) {
		switch (scanx) {
			case K_GP_A:
			case K_GP_B:
			case K_GP_X:
			case K_GP_Y:
			case K_GP_LSHOULDER:
			case K_GP_RSHOULDER:
			case K_GP_LTRIGGER:
			case K_GP_RTRIGGER:
			case K_GP_BACK:
			case K_GP_START:
			case K_GP_LTHUMB:
			case K_GP_RTHUMB:
			case K_GP_DPAD_UP:
			case K_GP_DPAD_DOWN:
			case K_GP_DPAD_LEFT:
			case K_GP_DPAD_RIGHT:
			case K_GP_LTHUMB_UP:
			case K_GP_LTHUMB_DOWN:
			case K_GP_LTHUMB_LEFT:
			case K_GP_LTHUMB_RIGHT:
			case K_GP_RTHUMB_UP:
			case K_GP_RTHUMB_DOWN:
			case K_GP_RTHUMB_LEFT:
			case K_GP_RTHUMB_RIGHT:
				//print(sprintf("Key: %f Duration: %f\n", scanx, cltime-GPButtonHeldBeginTime[scanx-816]));
				GPButtonHeldBeginTime[scanx-816] = 0;
				break;

		}
	}
	switch(evtype)
	{
		case IE_KEYDOWN:
			Input_Movecheck(scanx, 1);
			return FALSE;
		case IE_KEYUP:
			Input_Movecheck(scanx, 0);
			return FALSE;
		case IE_MOUSEDELTA:
			return FALSE;
		case IE_MOUSEABS:
			return FALSE;
	}

	return FALSE;
};

noref void() CSQC_Input_Frame =
{
	input_angles += gun_kick;
	input_angles += sniper_sway;
}

#define DEG2RAD(x)	(x * M_PI / 180.f)

noref void() CSQC_Parse_Event =
{
	float event_type = readbyte();

	switch (event_type) {
		case CSQC_EVENT_PARTICLE:
			float particle_type = readbyte();
			float part_pos_x = readcoord();
			float part_pos_y = readcoord();
			float part_pos_z = readcoord();
			float part_optional = readbyte();
			float part_entity = readentitynum();
			vector particle_pos = [part_pos_x, part_pos_y, part_pos_z];
			Particles_RunParticle(particle_type, particle_pos, part_optional, part_entity);
			break;
		case CSQC_EVENT_USEPRINT:
			useprint_type = readbyte();
			useprint_cost = readshort();
			useprint_weapon = readbyte();
			useprint_time = time + 0.1;
			break;
		case CSQC_EVENT_ROUNDCHANGE:
			rounds = readbyte();
			HUD_Change_time = time + 6;
			break;
		case CSQC_EVENT_BROADCASTMESSAGE:
			broadcast_type = readbyte();
			broadcast_time = time + readbyte();
			broadcast_num = readbyte();
			break;
		case CSQC_EVENT_MAXAMMOTEXT:
			hud_maxammo_endtime = time + 2;
			hud_maxammo_starttime = time;
			break;
		case CSQC_EVENT_MUSICSTREAM:
			string track_name = readstring();
			string track_extension;
			if (platform_is_web)
				track_extension = ".wav";
			else
				track_extension = ".ogg";

			localsound_enhanced(strcat("tracks/", track_name, track_extension), CHAN_MUSIC, 0.85);
			break;
		case CSQC_EVENT_GIVEACHIEVEMENT:
			float achievement_id = readbyte();

			// Don't unlock an existing achievement or any if on emscripten.
			if (achievements[achievement_id].unlocked || platform_is_web)
				return;

			Achievement_Unlock(achievement_id);
			break;
		case CSQC_EVENT_PLAYERNAME:
			character_name = readstring();
			break;
		case CSQC_EVENT_SCREENFLASH:
			screenflash_color = readbyte();
			screenflash_duration = time + readbyte();
			screenflash_type = readbyte();
			screenflash_worktime = 0;
			screenflash_starttime = time;
			break;
		case CSQC_EVENT_RUMBLE:
			float low_frequency = readshort();
			float high_frequency = readshort();
			float duration = readshort();
			if (cvar("in_rumbleenabled"))
				gp_rumble(last_input_deviceid, low_frequency, high_frequency, duration);
			break;
		case EVENT_WEAPONRECOIL:
			local vector rec;
            rec_x = readcoord()/5;
            rec_y = readcoord()/5;
            rec_z = readcoord()/5;

			gun_kick += rec;
			break;
		case EVENT_CHATMESSAGE:
			int sender = readbyte();
			int player_id = readbyte();
			string message = readstring();
			Chat_Register(sender, player_id, message);
			break;
		case EVENT_ENDGAME:
			game_over = true;
			break;
		case EVENT_DOUBLETAPUPDATE:
			double_tap_version = readbyte();
			break;
		case EVENT_UPDATE:
			float updatetype = readbyte();
			float var_1 = readbyte();
			float var_2 = readbyte();
			float var_3 = readbyte();
			
			switch (updatetype)
			{
				case 1: 
					HUD_Change_time = time + var_1; 
					break;
				case 2:
					rounds_change = var_1;
					break;
				case 3:
					if (Hitmark_time < time)
						Hitmark_time = time + 0.2;
					break;
				case 5:
					crosshair_spread_time = time + 70/getWeaponRecoilReturn(getstatf(STAT_ACTIVEWEAPON));
					break;
				default: 
					break;
			}
			break;
		case EVENT_REVIVECHANGE:
			float revivechange_player_index = readbyte() - 1; // playernum starts at one.
			float state = readbyte();
			revive_icons[revivechange_player_index].state = state;
			break;
		case EVENT_REVIVEON:
			float reviveon_player_index = readbyte() - 1; // playernum starts at one.
			revive_icons[reviveon_player_index].state = 1;
			revive_icons[reviveon_player_index].draw = true;
			break;
		case EVENT_REVIVEOFF:
			float reviveoff_player_index = readbyte() - 1; // playernum starts at one.
			revive_icons[reviveoff_player_index].state = 0;
			revive_icons[reviveoff_player_index].timer = 0;
			revive_icons[reviveoff_player_index].draw = false;
			break;
		case EVENT_MAPTYPE:
			map_compatibility_mode = readbyte();
			break;
		case EVENT_WORLDDATA:
			chaptertitle = readstring();
			location = readstring();
			date = readstring();
			person = readstring();

			if (chaptertitle == "")
				chaptertitle = "'Nazi Zombies'";
			break;
		case EVENT_PLAYERUPDATE:
			player_count = readbyte();
			break;
		case EVENT_GRENADEPULSE:
			crosshair_pulse_grenade = true;
			break;
		case EVENT_BETTYPROMPT:
			bettyprompt_time = time + 4;
			break;
	}
}
