/*
	server/clientfuncs.qc

	Used to communicate between server and client

	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

*/

#ifdef FTE

//
// FTE_RunParticleEffect(target, particle_type, position, optional_field, optional_entity)
// Fires a CSQC_EVENT_PARTICLE to clients specified (or
// world for global).
//
void(entity target, float particle_type, vector position, float optional_field, entity optional_entity) FTE_RunParticleEffect =
{
	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, CSQC_EVENT_PARTICLE);
	WriteByte(MSG_MULTICAST, particle_type);
	WriteCoord(MSG_MULTICAST, position_x);
	WriteCoord(MSG_MULTICAST, position_y);
	WriteCoord(MSG_MULTICAST, position_z);
	WriteByte(MSG_MULTICAST, optional_field);
	WriteEntity(MSG_MULTICAST, optional_entity);

	if (target != world) {
		msg_entity = target;
		multicast(position, MULTICAST_ONE);
	} else {
		multicast(position, MULTICAST_ALL);
	}
};

#endif // FTE

void() NotifyGameEnd =
{
#ifdef FTE

	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, EVENT_ENDGAME);
	multicast('0 0 0', MULTICAST_ALL);

#endif // FTE
}

void SetUpdate(entity client, float type, float val1, float val2, float val3)
{
#ifdef FTE

	if (type != 2)
	{
		WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
		WriteByte(MSG_MULTICAST, EVENT_UPDATE);
		WriteByte(MSG_MULTICAST, type); // UT_HUD
		WriteByte(MSG_MULTICAST, val1);
		WriteByte(MSG_MULTICAST, val2);
		WriteByte(MSG_MULTICAST, val3); // misc flags/vars for later if needed
		msg_entity = client;
		multicast('0 0 0', MULTICAST_ONE);
	}
	else
	{
		WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
		WriteByte(MSG_MULTICAST, EVENT_UPDATE);
		WriteByte(MSG_MULTICAST, type); // UT_ROUNDS_CHANGE
		WriteByte(MSG_MULTICAST, val1);
		WriteByte(MSG_MULTICAST, val2);
		WriteByte(MSG_MULTICAST, val3); // misc flags/vars for later if needed
		multicast('0 0 0', MULTICAST_ALL);
	}

#endif // FTE
}

#ifdef FTE
void(float mode, entity to) ReportMapMode = {
	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, EVENT_MAPTYPE);
	WriteByte(MSG_MULTICAST, mode);
	msg_entity = to;
	multicast('0 0 0', MULTICAST_ONE);
}

void(entity to, float type, float cost, float weapon) useprint = {
	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, CSQC_EVENT_USEPRINT);
	WriteByte(MSG_MULTICAST, type);
	WriteShort(MSG_MULTICAST, cost);
	WriteByte(MSG_MULTICAST, weapon);
	msg_entity = to;
	multicast('0 0 0', MULTICAST_ONE);
}

void(string track_name) songegg =
{
	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, CSQC_EVENT_MUSICSTREAM);
	WriteString(MSG_MULTICAST, track_name);
	multicast('0 0 0', MULTICAST_ALL);
};
#endif // FTE

void(vector org) CallExplosion = {
#ifndef FTE

    WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);
	WriteByte (MSG_BROADCAST, TE_EXPLOSION);
	WriteCoord (MSG_BROADCAST, org_x);
	WriteCoord (MSG_BROADCAST, org_y);
	WriteCoord (MSG_BROADCAST, org_z);

#else

	FTE_RunParticleEffect(world, CSQC_PART_EXPLOSION, org, 0, world);

#endif // FTE
}

#ifdef FTE

//
// FTE_IncrementRound()
// Alerts all clients of new value for Rounds.
// -- RELIABLE --
//
void(float round) FTE_IncrementRound =
{
	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, CSQC_EVENT_ROUNDCHANGE);
	WriteByte(MSG_MULTICAST, round);
	multicast('0 0 0', MULTICAST_ALL_R);
};


#endif // FTE

void(float player_index, float state) ChangeReviveIconState =
{
#ifdef FTE

	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, EVENT_REVIVECHANGE);
	WriteByte(MSG_MULTICAST, player_index);
	WriteByte(MSG_MULTICAST, state);
	multicast('0 0 0', MULTICAST_ALL);

#endif // FTE
}

void(float player_index) EnableReviveIcon =
{
#ifdef FTE

	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, EVENT_REVIVEON);
	WriteByte(MSG_MULTICAST, player_index);
	multicast('0 0 0', MULTICAST_ALL);

#endif // FTE
}

void(float index) DisableReviveIcon =
{
#ifdef FTE

	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, EVENT_REVIVEOFF);
	WriteByte(MSG_MULTICAST, index);
	multicast('0 0 0', MULTICAST_ALL);

#endif // FTE
}

void(entity who, float weapon) CL_SendWeaponFire =
{
	float recoil_return_time = getWeaponRecoilReturn(weapon);
	vector weapon_recoil = GetWeaponRecoil(weapon);

	msg_entity = who;

#ifdef FTE

	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, EVENT_WEAPONRECOIL);
	WriteCoord (MSG_MULTICAST, weapon_recoil_x);
	WriteCoord (MSG_MULTICAST, weapon_recoil_y);
	WriteCoord (MSG_MULTICAST, weapon_recoil_z);
	multicast('0 0 0', MULTICAST_ONE);

#else

	WriteByte(MSG_ONE, SVC_WEAPONFIRE);
	WriteLong(MSG_ONE, recoil_return_time); // FIXME: A long for this is overkill.
	WriteCoord(MSG_ONE, weapon_recoil_x);
	WriteCoord(MSG_ONE, weapon_recoil_y);
	WriteCoord(MSG_ONE, weapon_recoil_z);

#endif // FTE

	self.recoil_delay = 60/recoil_return_time + time;
}

#ifdef FTE

//
// FTE_BroadcastMessage(target, broadcast_type, broadcast_type, player_id)
// Sends a CSQC Event to display text on the screen for the
// desired clients. Use 'world' to send to everyone.
// -- RELIABLE --
//
void(entity target, float broadcast_type, float broadcast_time, float player_id) FTE_BroadcastMessage =
{
	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, CSQC_EVENT_BROADCASTMESSAGE);
	WriteByte(MSG_MULTICAST, broadcast_type);
	WriteByte(MSG_MULTICAST, broadcast_time);
	WriteByte(MSG_MULTICAST, player_id);

	if (target != world) {
		msg_entity = target;
		multicast('0 0 0', MULTICAST_ONE_R);
	} else {
		multicast('0 0 0', MULTICAST_ALL_R);
	}
};

//
// nzp_screenflash(target, color, duration, type)
// FTE equivalent of nzp_screenflash builtin.
//
void(entity target, float color, float duration, float type) nzp_screenflash =
{
	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, CSQC_EVENT_SCREENFLASH);
	WriteByte(MSG_MULTICAST, color);
	WriteByte(MSG_MULTICAST, duration);
	WriteByte(MSG_MULTICAST, type);

	if (target != world) {
		msg_entity = target;
		multicast('0 0 0', MULTICAST_ONE);
	} else {
		multicast('0 0 0', MULTICAST_ALL);
	}
};

//
// nzp_rumble(target, low_frequency, high_frequency, duration)
// FTE equivalent of the nzp_rumble builtin.
//
void(entity target, float low_frequency, float high_frequency, float duration) nzp_rumble =
{
	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, CSQC_EVENT_RUMBLE);
	WriteShort(MSG_MULTICAST, low_frequency);
	WriteShort(MSG_MULTICAST, high_frequency);
	WriteShort(MSG_MULTICAST, duration);

	msg_entity = target;
	multicast('0 0 0', MULTICAST_ONE);
};

#endif // FTE

void(float count) UpdatePlayerCount = {
#ifdef FTE
	if (count == 0)
		return;
	else {
		WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
		WriteByte(MSG_MULTICAST, EVENT_PLAYERUPDATE);
		WriteByte(MSG_MULTICAST, count);
		multicast('0 0 0', MULTICAST_ALL);
	}

#endif // FTE
}

#ifdef FTE
void(entity who) grenade_pulse =
{

	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, EVENT_GRENADEPULSE);
	msg_entity = who;
	multicast('0 0 0', MULTICAST_ONE);

}
#endif // FTE

#ifdef FTE

void(entity who) nzp_bettyprompt =
{
	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, EVENT_BETTYPROMPT);
	msg_entity = who;
	multicast('0 0 0', MULTICAST_ONE);
}

#endif // FTE

#ifdef FTE

//
// FTE_InterpolatePunchAngle(person)
// Punch Angle interpolation from our native sourceports,
// handled in SSQC for FTE.
//
void(entity person) FTE_InterpolatePunchAngle =
{
	vector last_punchangle = person.punchangle;
	float lerp_factor = 0.9*frametime; // It's fine that this is server-dependent, since it's server-executed..

	// Early-out: Let's not spam fabs calls if we have no punchangle.
	if (person.punchangle_x == 0 && person.punchangle_y == 0)
		return;

	// Begin interpolation.
	for(int i = 0; i < 2; i++) {
		float difference = last_punchangle[i] * lerp_factor;

		if (fabs(person.punchangle[i]) > 0.01) {
			if (person.punchangle[i] >= difference)
				person.punchangle[i] -= difference;
			else if (person.punchangle[i] <= -difference)
				person.punchangle[i] -= difference;
			else
				person.punchangle[i] = 0;
		} else {
			person.punchangle[i] = 0;
		}
	}
};

//
// FTE_UpdateDynamicFOV(person)
// Context-sensitive adjustment of client's viewzoom
// factor.
//
void(entity person) FTE_UpdateDynamicFOV =
{
	// Early-out if client is not zoomed in at all
	// and viewzoom is correctly applied
	if (!person.zoom && person.viewzoom == 1)
	 	return;

	// Calculate viewzoom FOV differential
	float viewzoom_differential = 1 - (0.018*GetWeaponZoomAmount(person.weapon));
	float has_scoped_weapon = WepDef_HasSniperScore(person.weapon);

	// Sniper scope-in: Force-set as soon as the Scope draws
	if (person.zoom == 2) {
		person.viewzoom = viewzoom_differential;
		return;
	}
	// Force-set viewzoom back to one if client is holding a scoped
	// weapon and need their viewzoom adjusted.
	else if (person.viewzoom != 1 && has_scoped_weapon) {
		person.viewzoom = 1;
		return;
	} else if (has_scoped_weapon) {
		return;
	}

	// Utilize a sigmoid curve to minimize motion sickness.
	float sigmoid_input, sigmoid_output;
	float viewzoom_adjustment_rate;

	// If we're Aiming down the Sight and viewzoom has not reached it's end
	if (person.zoom == 1 && person.viewzoom != viewzoom_differential) {
		sigmoid_input = (person.viewzoom - viewzoom_differential) * 10;
		sigmoid_output = 1 / (1 + exp(-sigmoid_input));
		viewzoom_adjustment_rate = 0.06 * (sigmoid_output + 0.5) * (frametime*20);

		if (person.viewzoom > viewzoom_differential) {
			person.viewzoom -= viewzoom_adjustment_rate;
			if (person.viewzoom < viewzoom_differential)
				person.viewzoom = viewzoom_differential;
		} else {
			person.viewzoom = viewzoom_differential;
		}
	}
	// If we're not Aiming down the Sight and viewzoom is not reset
	else if (person.zoom != 1 && person.viewzoom != 1) {
		// We can zoom out at a constant rate without consequence.
		person.viewzoom += 0.85 * frametime;
		if (person.viewzoom > 1)
			person.viewzoom = 1;
	}
};

#endif // FTE

#ifdef FTE

//
// nzp_maxammo()
// FTE equivalent of nzp_maxammo builtin.
//
void() nzp_maxammo =
{
	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, CSQC_EVENT_MAXAMMOTEXT);
	multicast('0 0 0', MULTICAST_ALL);
};

//
// nzp_setplayername(target, player_name)
// FTE equivalent of nzp_setplayername builtin.
//
void(entity target, string player_name) nzp_setplayername =
{
	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, CSQC_EVENT_PLAYERNAME);
	WriteString(MSG_MULTICAST, player_name);
	msg_entity = target;
	multicast('0 0 0', MULTICAST_ONE);
};

void(string chaptertitle, string location, string date, string person, entity who) WorldText = {
	if (player_count == 0) {
		WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
		WriteByte(MSG_MULTICAST, EVENT_WORLDDATA);
		WriteString(MSG_MULTICAST, chaptertitle);
		WriteString(MSG_MULTICAST, location);
		WriteString(MSG_MULTICAST, date);
		WriteString(MSG_MULTICAST, person);
		msg_entity = who;
		multicast('0 0 0', MULTICAST_ONE);
	} else {
		WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
		WriteByte(MSG_MULTICAST, EVENT_WORLDDATA);
		WriteString(MSG_MULTICAST, chaptertitle);
		WriteString(MSG_MULTICAST, location);
		WriteString(MSG_MULTICAST, date);
		WriteString(MSG_MULTICAST, person);
		multicast('0 0 0', MULTICAST_ALL);
	}
}
#endif // FTE

void (float achievement_id, optional entity who) GiveAchievement =
{
#ifndef FTE

	// temp
	if (achievement_id > 4)
		return;

#endif // FTE

	// this is an achievement specific to an individual
	if ((who && who != world) || player_count == 0) {
		if (player_count == 0) who = find(world, classname, "player");

#ifndef FTE

		achievement(who, achievement_id);

#else

		WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
		WriteByte(MSG_MULTICAST, CSQC_EVENT_GIVEACHIEVEMENT);
		WriteByte(MSG_MULTICAST, achievement_id);
		msg_entity = who;
		multicast('0 0 0', MULTICAST_ONE);

#endif // FTE

	} else {

#ifndef FTE

		entity players;
		players = find(world, classname, "player");
		while(players != world) {
			achievement(players, achievement_id);
			players = find(players, classname, "player");
		}

#else

		WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
		WriteByte(MSG_MULTICAST, CSQC_EVENT_GIVEACHIEVEMENT);
		WriteByte(MSG_MULTICAST, achievement_id);
		multicast('0 0 0', MULTICAST_ALL);

#endif // FTE
	}
}

#ifdef FTE

void CSQC_SendChatMessage(float player_id, string message) {
	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, EVENT_CHATMESSAGE);
	WriteByte(MSG_MULTICAST, num_for_edict(self) - 1);
	WriteByte(MSG_MULTICAST, player_id);
	WriteString(MSG_MULTICAST, message);
	multicast('0 0 0', MULTICAST_ALL);
}

/*
=================
Player_SendEntity

    Networks the player info
=================
*/
float Player_SendEntity( entity ePVEnt, float flChanged ) {
    WriteByte( MSG_ENTITY, 1 );
    WriteCoord( MSG_ENTITY, self.origin_x ); // Position X
    WriteCoord( MSG_ENTITY, self.origin_y ); // Position Y
    WriteCoord( MSG_ENTITY, self.origin_z ); // Position Z
    WriteAngle( MSG_ENTITY, self.angles_x ); // Angle X
    WriteAngle( MSG_ENTITY, self.angles_y ); // Angle Y
    WriteAngle( MSG_ENTITY, self.angles_z ); // Angle Z
    WriteShort( MSG_ENTITY, self.velocity_x ); // Velocity X
    WriteShort( MSG_ENTITY, self.velocity_y ); // Velocity X
    WriteShort( MSG_ENTITY, self.velocity_z ); // Velocity X
	WriteByte( MSG_ENTITY, self.playernum ); // Player ID
	WriteShort( MSG_ENTITY, self.modelindex ); // Player Model
	WriteByte( MSG_ENTITY, self.frame ); // Player's Frame
	WriteShort( MSG_ENTITY, self.movetype ); // Player Movetype
    WriteShort( MSG_ENTITY, self.flags ); // Flags, important for physics
	WriteByte( MSG_ENTITY, self.stance ); // Player Stance
	WriteFloat( MSG_ENTITY, self.points ); // Player Score
	WriteShort( MSG_ENTITY, self.kills ); // Player Kills
	WriteByte( MSG_ENTITY, self.is_in_menu ); // Player is in a Menu State
    return TRUE;
}

void(entity who, float version) nzp_setdoubletapver =
{
	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, EVENT_DOUBLETAPUPDATE);
	WriteByte(MSG_MULTICAST, version);
	msg_entity = who;
	multicast('0 0 0', MULTICAST_ONE);
};

#endif // FTE

void(vector org) Effect_Fire =
{

#ifndef FTE

	particle (self.origin, v_up*8, 111, 0);

#else

	FTE_RunParticleEffect(world, CSQC_PART_FIRE, org, 0, world);

#endif // FTE

};

// *****************************************
// Unrelated to engine, but custom functions
// *****************************************

// "Removes" an entity that is placed back on restart.
void(entity ent) Ent_FakeRemove =
{
	ent.entity_removed = true;
	ent.oldmodel = ent.model;
	ent.oldorigin = ent.origin;
	ent.bbmins = ent.mins;
	ent.bbmaxs = ent.maxs;
	ent.sprintflag = ent.solid;

	ent.solid = SOLID_NOT;
	setsize(ent, '0 0 0', '0 0 0');
	setmodel(ent, "");
}

void(string modelname) Precache_Set = // Precache model, and set myself to it
{
	modelname = Compat_ConvertOldAssetPath(modelname);
    precache_model(modelname);
    setmodel(self, modelname);
};

float() crandom =
{
	return 2*(random() - 0.5);
}

float(entity them, entity me) PlayerIsLooking =
{
	float ret = false;
	float old_solid = me.solid;
	me.solid = SOLID_BBOX;
	setorigin(me, me.origin);

	vector source;
	makevectors (them.v_angle);
	source = them.origin + them.view_ofs;

	// Standard 'are we facing' test..
	traceline(source, source + v_forward*50, 0, them);

	// We're inside of an object.. is it the target?
	if (trace_startsolid) {
		if (v_forward*normalize(me.origin - them.origin) > 0.7)
			ret = true;
	} else if (trace_ent == me) {
		ret = true;
	}

	me.solid = old_solid;
	setorigin(me, me.origin);
	return ret;
};