/*
 * Wolfenstein: Enemy Territory GPL Source Code
 * Copyright (C) 1999-2010 id Software LLC, a ZeniMax Media company.
 *
 * ET: Legacy
 * Copyright (C) 2012-2024 ET:Legacy team <mail@etlegacy.com>
 *
 * This file is part of ET: Legacy - http://www.etlegacy.com
 *
 * ET: Legacy 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 3 of the License, or
 * (at your option) any later version.
 *
 * ET: Legacy 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 ET: Legacy. If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, Wolfenstein: Enemy Territory GPL Source Code is also
 * subject to certain additional terms. You should have received a copy
 * of these additional terms immediately following the terms and conditions
 * of the GNU General Public License which accompanied the source code.
 * If not, please request a copy in writing from id Software at the address below.
 *
 * id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.
 */
/**
 * @file g_weapon.c
 * @brief Perform the server side effects of a weapon firing
 */

#include "g_local.h"
#include "g_lua.h"

#ifdef FEATURE_OMNIBOT
#include "g_etbot_interface.h"
#endif

static vec3_t forward, right, up;
static vec3_t muzzleEffect;
vec3_t        muzzleTrace;  // used in G_Damage from g_combat.c

// forward dec
gentity_t *Bullet_Fire(gentity_t *ent);
void Bullet_Fire_Extended(gentity_t *source, gentity_t *attacker, vec3_t start, vec3_t end, vec3_t dir, int damage, qboolean distance_falloff, meansOfDeath_t mod);

/**
======================================================================
KNIFE
======================================================================
*/

/**
 * @brief Weapon_Knife
 * @param[in] ent
 */
gentity_t *Weapon_Knife(gentity_t *ent)
{
	trace_t        tr;
	gentity_t      *traceEnt, *tent;
	int            damage;
	meansOfDeath_t mod = GetWeaponTableData(ent->s.weapon)->mod;
	vec3_t         end;

	AngleVectors(ent->client->ps.viewangles, forward, right, up);
	CalcMuzzlePoint(ent, ent->s.weapon, forward, right, up, muzzleTrace);
	VectorMA(muzzleTrace, CH_KNIFE_DIST, forward, end);

	G_TempTraceIgnoreBodies();
	G_HistoricalTrace(ent, &tr, muzzleTrace, NULL, NULL, end, ent->s.number, MASK_SHOT);
	G_ResetTempTraceIgnoreEnts();

	// ignore hits on NOIMPACT surfaces or no contact
	if ((tr.surfaceFlags & SURF_NOIMPACT) || tr.fraction == 1.0f)
	{
		return NULL;
	}

	if (tr.entityNum >= MAX_CLIENTS)       // world brush or non-player entity (no blood)
	{
		tent = G_TempEntity(tr.endpos, EV_MISSILE_MISS);
	}
	else                                // other player
	{
		tent = G_TempEntity(tr.endpos, EV_MISSILE_HIT);
	}

	tent->s.otherEntityNum = tr.entityNum;
	tent->s.eventParm      = DirToByte(tr.plane.normal);
	tent->s.weapon         = ent->s.weapon;
	tent->s.clientNum      = ent->r.ownerNum;

	if (tr.entityNum == ENTITYNUM_WORLD)     // don't worry about doing any damage
	{
		return NULL;
	}

	traceEnt = &g_entities[tr.entityNum];

	if (!(traceEnt->takedamage))
	{
		return NULL;
	}

	damage = GetWeaponTableData(ent->s.weapon)->damage;   // default knife damage for frontal attacks (10)

	// no damage
	if (!damage)
	{
		return NULL;
	}

	// Covert ops deal double damage with a knife
	if (ent->client->sess.playerType == PC_COVERTOPS)
	{
		damage <<= 1; // damage *= 2;    // Watch it - you could hurt someone with that thing!

	}
	// removed not allowing insta-gib backstabs on wounded players (CHRUKER: b002 "fix")
	if (traceEnt->client) // && traceEnt->health > 0)
	{
		if (G_GetEnemyPosition(ent, traceEnt) == POSITION_BEHIND)           // from behind(-ish)
		{
			// enough to drop a 'normal' (100 health) human with one jab
			// (not for real if there is a medic in the team because of medic MP bonus)
			damage = 100;
			mod    = MOD_BACKSTAB;

			if (BG_IsSkillAvailable(ent->client->sess.skill, SK_MILITARY_INTELLIGENCE_AND_SCOPED_WEAPONS, SK_COVERTOPS_ASSASSIN))
			{
				if (traceEnt->health > 0)
				{
					damage = traceEnt->health;
				}
				else if (traceEnt->health <= 0) // allow 'instagib' on wounded players
				{
					damage = traceEnt->health - GIB_HEALTH;
				}
			}
		}
	}

	// FIXME: should we keep this random add ?!
	G_Damage(traceEnt, ent, ent, vec3_origin, tr.endpos, (damage + rand() % 5), 0, mod);

	return NULL;
}

/**
 * @brief Class-specific in multiplayer
 * @param[in] ent
 */
gentity_t *Weapon_Medic(gentity_t *ent)
{
	vec3_t velocity, angles, tosspos, viewpos;

	VectorCopy(ent->client->ps.viewangles, angles);

	// clamp pitch
	if (angles[PITCH] < -30)
	{
		angles[PITCH] = -30;
	}
	else if (angles[PITCH] > 30)
	{
		angles[PITCH] = 30;
	}

	AngleVectors(angles, velocity, NULL, NULL);
	VectorScale(velocity, 75, velocity);
	velocity[2] += 50 + crandom() * 25;

	VectorCopy(muzzleEffect, tosspos);
	VectorMA(tosspos, 48, forward, tosspos);
	VectorCopy(ent->client->ps.origin, viewpos);

	Weapon_Medic_Ext(ent, viewpos, tosspos, velocity);

	return NULL;
}

/**
 * @brief Weapon_Medic_Ext
 * @param[in,out] ent
 * @param[in,out] viewpos
 * @param[in,out] tosspos
 * @param[in] velocity
 */
void Weapon_Medic_Ext(gentity_t *ent, vec3_t viewpos, vec3_t tosspos, vec3_t velocity)
{
	gentity_t *ent2;
	vec3_t    mins, maxs;
	trace_t   tr;

	VectorSet(mins, -(ITEM_RADIUS + 8), -(ITEM_RADIUS + 8), 0);
	VectorSet(maxs, (ITEM_RADIUS + 8), (ITEM_RADIUS + 8), 2 * (ITEM_RADIUS + 8));

	trap_ItemTrace(ent, &tr, viewpos, mins, maxs, tosspos, ent->s.number, MASK_MISSILESHOT);
	if (tr.startsolid)
	{
		VectorCopy(forward, viewpos);
		VectorNormalizeFast(viewpos);
		VectorMA(ent->r.currentOrigin, -24.f, viewpos, viewpos);

		trap_ItemTrace(ent, &tr, viewpos, mins, maxs, tosspos, ent->s.number, MASK_MISSILESHOT);

		VectorCopy(tr.endpos, tosspos);
	}
	else if (tr.fraction < 1)       // oops, bad launch spot
	{
		VectorCopy(tr.endpos, tosspos);
		SnapVectorTowards(tosspos, viewpos);
	}

	ent2            = LaunchItem(BG_GetItem(ITEM_HEALTH), tosspos, velocity, ent->s.number);
	ent2->think     = G_MagicSink;
	ent2->nextthink = level.time + 30000;

	ent2->parent = ent; // so we can score properly later

#ifdef FEATURE_OMNIBOT
	// Omni-bot - Send a fire event.
	Bot_Event_FireWeapon(ent - g_entities, Bot_WeaponGameToBot(ent->s.weapon), ent2);
#endif
}

/**
 * @brief Weapon_MagicAmmo
 * @param[in] ent
 */
gentity_t *Weapon_MagicAmmo(gentity_t *ent)
{
	vec3_t velocity, tosspos, viewpos, angles;

	VectorCopy(ent->client->ps.viewangles, angles);

	// clamp pitch
	if (angles[PITCH] < -30)
	{
		angles[PITCH] = -30;
	}
	else if (angles[PITCH] > 30)
	{
		angles[PITCH] = 30;
	}

	AngleVectors(angles, velocity, NULL, NULL);
	VectorScale(velocity, 75, velocity);
	velocity[2] += 50 + crandom() * 25;

	VectorCopy(muzzleEffect, tosspos);
	VectorMA(tosspos, 48, forward, tosspos);
	VectorCopy(ent->client->ps.origin, viewpos);

	Weapon_MagicAmmo_Ext(ent, viewpos, tosspos, velocity);

	return NULL;
}

/**
 * @brief Weapon_MagicAmmo_Ext
 * @param[in,out] ent
 * @param[in,out] viewpos
 * @param[in,out] tosspos
 * @param[in] velocity
 */
void Weapon_MagicAmmo_Ext(gentity_t *ent, vec3_t viewpos, vec3_t tosspos, vec3_t velocity)
{
	vec3_t    mins, maxs;
	trace_t   tr;
	gentity_t *ent2;

	VectorSet(mins, -(ITEM_RADIUS + 8), -(ITEM_RADIUS + 8), 0);
	VectorSet(maxs, (ITEM_RADIUS + 8), (ITEM_RADIUS + 8), 2 * (ITEM_RADIUS + 8));

	trap_ItemTrace(ent, &tr, viewpos, mins, maxs, tosspos, ent->s.number, MASK_MISSILESHOT);
	if (tr.startsolid)
	{
		VectorCopy(forward, viewpos);
		VectorNormalizeFast(viewpos);
		VectorMA(ent->r.currentOrigin, -24.f, viewpos, viewpos);

		trap_ItemTrace(ent, &tr, viewpos, mins, maxs, tosspos, ent->s.number, MASK_MISSILESHOT);

		VectorCopy(tr.endpos, tosspos);
	}
	else if (tr.fraction < 1)       // oops, bad launch spot
	{
		VectorCopy(tr.endpos, tosspos);
		SnapVectorTowards(tosspos, viewpos);
	}

	ent2            = LaunchItem(BG_GetItem(BG_IsSkillAvailable(ent->client->sess.skill, SK_SIGNALS, SK_FIELDOPS_RESOURCES) ? ITEM_WEAPON_MAGICAMMO2 : ITEM_WEAPON_MAGICAMMO), tosspos, velocity, ent->s.number);
	ent2->think     = G_MagicSink;
	ent2->nextthink = level.time + 30000;

	ent2->parent = ent;

	if (BG_IsSkillAvailable(ent->client->sess.skill, SK_SIGNALS, SK_FIELDOPS_RESOURCES))
	{
		ent2->count     = 2;
		ent2->s.density = 2;
	}
	else
	{
		ent2->count     = 1;
		ent2->s.density = 1;
	}

#ifdef FEATURE_OMNIBOT
	// Omni-bot - Send a fire event.
	Bot_Event_FireWeapon(ent - g_entities, Bot_WeaponGameToBot(ent->s.weapon), ent2);
#endif
}

void ReviveEntityEvent(gentity_t *reviver, gentity_t *revivee, int invulnEndTime)
{
	gentity_t *te;

	te = G_TempEntity(revivee->r.currentOrigin, EV_PLAYER_REVIVE);

	te->s.eventParm   = revivee->s.clientNum;
	te->s.clientNum   = reviver->s.clientNum;
	te->s.effect3Time = invulnEndTime;

#ifdef FEATURE_LUA
	G_LuaHook_Revive(revivee->s.clientNum, reviver->s.clientNum, invulnEndTime);
#endif
}

/**
 * @brief Took this out of Weapon_Syringe so we can use it from other places
 * @param[in] ent - Syringe user
 * @param[in,out] traceEnt - PM_DEAD target
 * @return
 */
void ReviveEntity(gentity_t *ent, gentity_t *traceEnt)
{
	vec3_t   org;
	vec3_t   legacyReviveAngles;
	trace_t  tr;
	int      healamt, headshot, oldclasstime = 0;
	int      invulnEndTime;
	qboolean useLegacyReviveAngles;

	// heal the dude
	// copy some stuff out that we'll wanna restore
	VectorCopy(traceEnt->client->ps.origin, org);

	headshot = traceEnt->client->ps.eFlags & EF_HEADSHOT;

	if (BG_IsSkillAvailable(ent->client->sess.skill, SK_FIRST_AID, SK_MEDIC_FULL_REVIVE))
	{
		healamt = traceEnt->client->ps.stats[STAT_MAX_HEALTH];
	}
	else
	{
		healamt = (int)(traceEnt->client->ps.stats[STAT_MAX_HEALTH] * 0.5);
	}

	// keep class special weapon time to keep them from exploiting revives
	oldclasstime = traceEnt->client->ps.classWeaponTime;

	// Capture revive orientation before ClientSpawn rebuilds playerstate data.
	useLegacyReviveAngles = G_LegacyRevive_GetReviveViewAngles(traceEnt, legacyReviveAngles);

	ClientSpawn(traceEnt, qtrue, qfalse, qtrue, qtrue);

	if (useLegacyReviveAngles)
	{
		// Preserve legacy downed/current look direction after revive spawn reinitialization.
		SetClientViewAngle(traceEnt, legacyReviveAngles);
	}

	invulnEndTime = traceEnt->client->ps.powerups[PW_INVULNERABLE];

#ifdef FEATURE_OMNIBOT
	Bot_Event_Revived(traceEnt - g_entities, ent);
#endif

	traceEnt->client->ps.stats[STAT_PLAYER_CLASS] = traceEnt->client->sess.playerType;

	if (headshot)
	{
		traceEnt->client->ps.eFlags |= EF_HEADSHOT;
	}

	traceEnt->client->ps.weaponstate     = WEAPON_READY;
	traceEnt->client->ps.classWeaponTime = oldclasstime;

	// set idle animation on weapon
	traceEnt->client->ps.weapAnim = PM_IdleAnimForWeapon(traceEnt->client->ps.weapon);

	traceEnt->health = healamt;
	VectorCopy(org, traceEnt->s.origin);
	VectorCopy(org, traceEnt->r.currentOrigin);
	VectorCopy(org, traceEnt->client->ps.origin);

	trap_Trace(&tr, traceEnt->client->ps.origin, traceEnt->client->ps.mins, traceEnt->client->ps.maxs, traceEnt->client->ps.origin, traceEnt->s.number, MASK_PLAYERSOLID);
	if (tr.allsolid)
	{
		traceEnt->client->ps.pm_flags |= PMF_DUCKED;
	}

	traceEnt->r.contents = CONTENTS_CORPSE;
	trap_LinkEntity(ent);

	// Let the person being revived know about it
	trap_SendServerCommand(traceEnt - g_entities, va("cp \"[lon]You have been revived by [lof]%s^7!\"", ent->client->pers.netname));

	traceEnt->props_frame_state = ent->s.number;

	ReviveEntityEvent(ent, traceEnt, invulnEndTime);

	traceEnt->client->pers.lastrevive_client = ent->s.clientNum;
	traceEnt->client->pers.lasthealth_client = ent->s.clientNum;

	if (g_fastres.integer > 0)
	{
		BG_AnimScriptEvent(&traceEnt->client->ps, traceEnt->client->pers.character->animModelInfo, ANIM_ET_JUMP, qfalse);
	}
	else
	{
		// Play revive animation
		BG_AnimScriptEvent(&traceEnt->client->ps, traceEnt->client->pers.character->animModelInfo, ANIM_ET_REVIVE, qfalse);
		traceEnt->client->ps.pm_flags |= PMF_TIME_LOCKPLAYER;
		traceEnt->client->ps.pm_time   = 2100;
	}
}

/**
 * @brief Try to heal a living teammate with a syringe when enabled by cvar.
 * @param[in] healer
 * @param[in,out] target
 * @param[out] refundAmmo Set to qtrue when the syringe should be refunded.
 * @return qtrue if syringe healing logic handled this target, qfalse otherwise.
 */
static qboolean G_TrySyringeHeal(gentity_t *healer, gentity_t *target, qboolean *refundAmmo)
{
	int maxHealth;
	int healAmount;

	*refundAmmo = qfalse;

	// Keep stock ET behavior unless explicitly enabled.
	if (g_syringeHealing.integer != 1)
	{
		return qfalse;
	}

	// Only living players are eligible for syringe healing.
	if (!target->client || target->client->ps.pm_type != PM_NORMAL)
	{
		return qfalse;
	}

	// Invalid heal targets still consume the shot, so we refund here.
	if (target->client->sess.sessionTeam != healer->client->sess.sessionTeam)
	{
		*refundAmmo = qtrue;
		return qtrue;
	}

	maxHealth = target->client->ps.stats[STAT_MAX_HEALTH];
	if (target->health > (int)(maxHealth * 0.25f))
	{
		*refundAmmo = qtrue;
		return qtrue;
	}

	if (BG_IsSkillAvailable(healer->client->sess.skill, SK_FIRST_AID, SK_MEDIC_FULL_REVIVE))
	{
		healAmount = maxHealth;
	}
	else
	{
		healAmount = (int)(maxHealth * 0.5f);
	}

	target->health                         = healAmount;
	target->client->ps.stats[STAT_HEALTH]  = healAmount;
	target->client->pers.lasthealth_client = healer->s.clientNum;

	G_Sound(target, GAMESOUND_MISC_REVIVE);
	G_AddSkillPoints(healer, SK_FIRST_AID, 2.f, "healing");

	return qtrue;
}

/**
* @brief Shoot the syringe, do the old lazarus bit
*
* @param[in,out] ent
*
*/
gentity_t *Weapon_Syringe(gentity_t *ent)
{
	vec3_t    end;
	trace_t   tr;
	gentity_t *traceEnt;
	int       i;
	qboolean  refundAmmo;

	AngleVectors(ent->client->ps.viewangles, forward, right, up);
	CalcMuzzlePointForActivate(ent, forward, right, up, muzzleTrace);
	VectorMA(muzzleTrace, CH_REVIVE_DIST, forward, end);

	// right on top of intended revivee.
	G_TempTraceIgnorePlayersFromTeam(ent->s.teamNum == TEAM_AXIS ? TEAM_ALLIES : TEAM_AXIS);
	G_TempTraceIgnoreBodies();

	// Re-trace if we hit blocking deployables, otherwise either pass (hit a
	// revivable body) or block (hit a wall etc.).
	for (i = 0; i < level.num_entities; i++)
	{
		G_HistoricalTrace(ent, &tr, muzzleTrace, NULL, NULL, end, ent->s.number, MASK_SHOT);

		if (tr.startsolid)
		{
			VectorMA(muzzleTrace, 8, forward, end);
			trap_Trace(&tr, muzzleTrace, NULL, NULL, end, ent->s.number, MASK_SHOT);
		}

		if (tr.fraction == 1.0f || tr.entityNum == ENTITYNUM_WORLD || tr.entityNum == ENTITYNUM_NONE)
		{
			break;
		}

		traceEnt = &g_entities[tr.entityNum];
		if (traceEnt->s.eType == ET_MISSILE
		    && (traceEnt->s.weapon == WP_SATCHEL
		        || traceEnt->s.weapon == WP_DYNAMITE
		        || traceEnt->s.weapon == WP_LANDMINE))
		{
			G_TempTraceIgnoreEntity(traceEnt);
			continue;
		}

		break;
	}

	G_ResetTempTraceIgnoreEnts();

	if (tr.fraction == 1.0f) // no hit
	{
		// give back ammo
		ent->client->ps.ammoclip[GetWeaponTableData(WP_MEDIC_SYRINGE)->clipIndex] += 1;
		return NULL;
	}

	traceEnt = &g_entities[tr.entityNum];

	if (!traceEnt->client)
	{
		// give back ammo
		ent->client->ps.ammoclip[GetWeaponTableData(WP_MEDIC_SYRINGE)->clipIndex] += 1;
		return NULL;
	}

	// Optional syringe-heal logic for living teammates.
	if (G_TrySyringeHeal(ent, traceEnt, &refundAmmo))
	{
		if (refundAmmo)
		{
			ent->client->ps.ammoclip[GetWeaponTableData(WP_MEDIC_SYRINGE)->clipIndex] += 1;
		}
		return NULL;
	}

	if (traceEnt->client->ps.pm_type == PM_DEAD &&
	    traceEnt->client->sess.sessionTeam == ent->client->sess.sessionTeam)
	{
		// moved all the revive stuff into its own function
		ReviveEntity(ent, traceEnt);

		// syringe "hit"
		if (g_gamestate.integer == GS_PLAYING)
		{
			ent->client->sess.aWeaponStats[WS_SYRINGE].hits++;
		}

		// Let medics know who they just revived
		trap_SendServerCommand(ent - g_entities, va("cp \"[lon]You have revived [lof]%s^7!\"", traceEnt->client->pers.netname));

		G_LogPrintf("Medic_Revive: %d %d\n", (int)(ent - g_entities), (int)(traceEnt - g_entities));

		if (!traceEnt->isProp) // flag for if they were teamkilled or not
		{
			G_AddSkillPoints(ent, SK_FIRST_AID, 4.f, "revive");
		}

		// calculate ranks to update numFinalDead arrays. Have to do it manually as addscore has an early out
		if (g_gametype.integer == GT_WOLF_LMS)
		{
			CalculateRanks();
		}
	}
	else
	{
		// If the medicine wasn't used, give back the ammo
		ent->client->ps.ammoclip[GetWeaponTableData(WP_MEDIC_SYRINGE)->clipIndex] += 1;
	}

	return NULL;
}

/**
 * @brief Hmmmm. Needles. With stuff in it. Woooo.
 * @param[in,out] ent
 */
gentity_t *Weapon_AdrenalineSyringe(gentity_t *ent)
{
	ent->client->ps.powerups[PW_ADRENALINE] = level.time + 10000;

	return NULL;
}

/**
 * @brief Crude version of G_RadiusDamage to see if the dynamite can damage a func_constructible
 * @param[in] origin
 * @param[in] radius
 * @param[out] damagedList
 * @return
 */
int EntsThatRadiusCanDamage(vec3_t origin, float radius, int *damagedList)
{
	float     dist;
	gentity_t *ent;
	int       entityList[MAX_GENTITIES];
	int       numListedEntities;
	vec3_t    mins, maxs;
	vec3_t    v;
	int       i, e;
	float     boxradius;
	vec3_t    dest;
	trace_t   tr;
	vec3_t    midpoint;
	int       numDamaged = 0;

	if (radius < 1)
	{
		radius = 1;
	}

	boxradius = M_SQRT2 * radius; // radius * sqrt(2) for bounding box enlargement --
	// bounding box was checking against radius / sqrt(2) if collision is along box plane
	for (i = 0 ; i < 3 ; i++)
	{
		mins[i] = origin[i] - boxradius;
		maxs[i] = origin[i] + boxradius;
	}

	numListedEntities = trap_EntitiesInBox(mins, maxs, entityList, MAX_GENTITIES);

	for (e = 0 ; e < numListedEntities ; e++)
	{
		ent = &g_entities[entityList[e]];

		if (!ent->r.bmodel)
		{
			VectorSubtract(ent->r.currentOrigin, origin, v);
		}
		else
		{
			for (i = 0 ; i < 3 ; i++)
			{
				if (origin[i] < ent->r.absmin[i])
				{
					v[i] = ent->r.absmin[i] - origin[i];
				}
				else if (origin[i] > ent->r.absmax[i])
				{
					v[i] = origin[i] - ent->r.absmax[i];
				}
				else
				{
					v[i] = 0;
				}
			}
		}

		dist = VectorLength(v);
		if (dist >= radius)
		{
			continue;
		}

		if (CanDamage(ent, origin))
		{
			damagedList[numDamaged++] = entityList[e];
		}
		else
		{
			VectorAdd(ent->r.absmin, ent->r.absmax, midpoint);
			VectorScale(midpoint, 0.5f, midpoint);
			VectorCopy(midpoint, dest);

			trap_Trace(&tr, origin, vec3_origin, vec3_origin, dest, ENTITYNUM_NONE, MASK_SOLID);
			if (tr.fraction < 1.0f)
			{
				VectorSubtract(dest, origin, dest);
				dist = VectorLength(dest);
				if (dist < radius * 0.2f)     // closer than 1/4 dist
				{
					damagedList[numDamaged++] = entityList[e];
				}
			}
		}
	}

	return numDamaged;
}

extern void G_LandminePrime(gentity_t *self);
extern void explosive_indicator_think(gentity_t *ent);

#define MIN_BLOCKINGWARNING_INTERVAL 3000

/**
 * @brief MakeTemporarySolid
 * @param[in,out] ent
 */
static void MakeTemporarySolid(gentity_t *ent)
{
	if (ent->entstate == STATE_UNDERCONSTRUCTION)
	{
		ent->clipmask   = ent->realClipmask;
		ent->r.contents = ent->realContents;
		if (!ent->realNonSolidBModel)
		{
			ent->s.eFlags &= ~EF_NONSOLID_BMODEL;
		}
	}

	trap_LinkEntity(ent);
}

/**
 * @brief UndoTemporarySolid
 * @param[in,out] ent
 */
static void UndoTemporarySolid(gentity_t *ent)
{
	ent->entstate     = STATE_UNDERCONSTRUCTION;
	ent->s.powerups   = STATE_UNDERCONSTRUCTION;
	ent->realClipmask = ent->clipmask;
	ent->clipmask     = 0;
	ent->realContents = ent->r.contents;
	ent->r.contents   = 0;
	if (ent->s.eFlags & EF_NONSOLID_BMODEL)
	{
		ent->realNonSolidBModel = qtrue;
	}
	else
	{
		ent->s.eFlags |= EF_NONSOLID_BMODEL;
	}

	trap_LinkEntity(ent);
}

/**
 * @brief HandleEntsThatBlockConstructible
 * @param[in] constructor
 * @param[in,out] constructible
 * @param[in] handleBlockingEnts kill players, return flags, remove entities
 * @param[in] warnBlockingPlayers warn any players that are in the constructible area
 */
static void HandleEntsThatBlockConstructible(gentity_t *constructor, gentity_t *constructible, qboolean handleBlockingEnts, qboolean warnBlockingPlayers)
{
	// check if something blocks us
	int       constructibleList[MAX_GENTITIES];
	int       entityList[MAX_GENTITIES];
	int       blockingList[MAX_GENTITIES];
	int       constructibleEntities = 0;
	int       listedEntities, e;
	int       blockingEntities = 0;
	gentity_t *check, *block;
	// backup...
	int constructibleModelindex     = constructible->s.modelindex;
	int constructibleClipmask       = constructible->clipmask;
	int constructibleContents       = constructible->r.contents;
	int constructibleNonSolidBModel = (constructible->s.eFlags & EF_NONSOLID_BMODEL);

	trap_SetBrushModel(constructible, va("*%i", constructible->s.modelindex2));

	// ...and restore
	constructible->clipmask   = constructibleClipmask;
	constructible->r.contents = constructibleContents;
	if (!constructibleNonSolidBModel)
	{
		constructible->s.eFlags &= ~EF_NONSOLID_BMODEL;
	}
	trap_LinkEntity(constructible);

	// store our origin
	VectorCopy(constructible->r.absmin, constructible->s.origin2);
	VectorAdd(constructible->r.absmax, constructible->s.origin2, constructible->s.origin2);
	VectorScale(constructible->s.origin2, 0.5f, constructible->s.origin2);

	// get all the entities that make up the constructible
	if (constructible->track && constructible->track[0])
	{
		vec3_t mins, maxs;

		VectorCopy(constructible->r.absmin, mins);
		VectorCopy(constructible->r.absmax, maxs);

		check = NULL;

		while (1)
		{
			check = G_Find(check, FOFS(track), constructible->track);

			if (check == constructible)
			{
				continue;
			}

			if (!check)
			{
				break;
			}

			if (constructible->count2)
			{
				if (check->partofstage != constructible->grenadeFired)
				{
					continue;
				}
			}

			// get the bounding box of all entities in the constructible together
			AddPointToBounds(check->r.absmin, mins, maxs);
			AddPointToBounds(check->r.absmax, mins, maxs);

			constructibleList[constructibleEntities++] = check->s.number;
		}

		listedEntities = trap_EntitiesInBox(mins, maxs, entityList, MAX_GENTITIES);

		// make our constructible entities solid so we can check against them
		//trap_LinkEntity( constructible );
		MakeTemporarySolid(constructible);
		for (e = 0; e < constructibleEntities; e++)
		{
			check = &g_entities[constructibleList[e]];

			//trap_LinkEntity( check );
			MakeTemporarySolid(check);
		}
	}
	else
	{
		// changed * to abs*
		listedEntities = trap_EntitiesInBox(constructible->r.absmin, constructible->r.absmax, entityList, MAX_GENTITIES);

		// make our constructible solid so we can check against it
		//trap_LinkEntity( constructible );
		MakeTemporarySolid(constructible);
	}

	for (e = 0; e < listedEntities; e++)
	{
		check = &g_entities[entityList[e]];

		// ignore everything but items, players and missiles (grenades too)
		if (check->s.eType != ET_MISSILE && check->s.eType != ET_ITEM && check->s.eType != ET_PLAYER && !check->physicsObject)
		{
			continue;
		}

		// remove any corpses, this includes dynamite
		if (check->r.contents == CONTENTS_CORPSE)
		{
			blockingList[blockingEntities++] = entityList[e];
			continue;
		}

		// FIXME: dynamite seems to test out of position?
		// see if the entity is in a solid now
		if ((block = G_TestEntityPosition(check)) == NULL)
		{
			continue;
		}

		// the entity is blocked and it is a player, then warn the player
		if (warnBlockingPlayers && check->s.eType == ET_PLAYER)
		{
			if ((level.time - check->client->lastConstructibleBlockingWarnTime) >= MIN_BLOCKINGWARNING_INTERVAL)
			{
				trap_SendServerCommand(check->s.number, "cp \"Warning, leave the construction area...\" 1");
				check->client->lastConstructibleBlockingWarnTime = level.time;
			}
		}

		blockingList[blockingEntities++] = entityList[e];
	}

	// undo the temporary solid for our entities
	UndoTemporarySolid(constructible);
	if (constructible->track && constructible->track[0])
	{
		for (e = 0; e < constructibleEntities; e++)
		{
			check = &g_entities[constructibleList[e]];

			//trap_UnlinkEntity( check );
			UndoTemporarySolid(check);
		}
	}

	if (handleBlockingEnts)
	{
		for (e = 0; e < blockingEntities; e++)
		{
			block = &g_entities[blockingList[e]];

			if (block->client || block->s.eType == ET_CORPSE)
			{
				G_Damage(block, constructible, constructor, NULL, NULL, GIB_DAMAGE(block->health), DAMAGE_NO_PROTECTION, MOD_CRUSH_CONSTRUCTION);
			}
			else if (block->s.eType == ET_ITEM && block->item->giType == IT_TEAM)
			{
				// see if it's a critical entity, one that we can't just simply kill (basically flags)
				Team_DroppedFlagThink(block);
			}
			else
			{
				// remove the landmine from both teamlists
				if (block->s.eType == ET_MISSILE && block->methodOfDeath == MOD_LANDMINE)
				{
					mapEntityData_t *mEnt;

					if ((mEnt = G_FindMapEntityData(&mapEntityData[0], block - g_entities)) != NULL)
					{
						G_FreeMapEntityData(&mapEntityData[0], mEnt);
					}

					if ((mEnt = G_FindMapEntityData(&mapEntityData[1], block - g_entities)) != NULL)
					{
						G_FreeMapEntityData(&mapEntityData[1], mEnt);
					}
				}

				// just get rid of it
				G_FreeEntity(block);
			}
		}
	}

	if (constructibleModelindex)
	{
		trap_SetBrushModel(constructible, va("*%i", constructibleModelindex));
		// ...and restore
		constructible->clipmask   = constructibleClipmask;
		constructible->r.contents = constructibleContents;
		if (!constructibleNonSolidBModel)
		{
			constructible->s.eFlags &= ~EF_NONSOLID_BMODEL;
		}
		trap_LinkEntity(constructible);
	}
	else
	{
		constructible->s.modelindex = 0;
		//constructible->clipmask = constructibleClipmask;
		//constructible->r.contents = constructibleContents;
		//if( !constructibleNonSolidBModel )
		//  constructible->s.eFlags &= ~EF_NONSOLID_BMODEL;
		trap_LinkEntity(constructible);
	}
}

#define CONSTRUCT_POSTDECAY_TIME 500

/**
 * @brief TryConstructing
 *
 * @param ent
 *
 * @return qfalse when it couldn't build
 *
 * @note !! If the conditions here of a buildable constructible change, then BotIsConstructible() must reflect those changes !!
 */
static qboolean TryConstructing(gentity_t *ent, gentity_t *trigger)
{
	gentity_t *constructible = trigger->target_ent;

	// no construction during prematch
	if (level.warmupTime)
	{
		return qfalse;
	}

	// see if we are in a trigger_objective_info targetting multiple func_constructibles
	if (constructible->s.eType == ET_CONSTRUCTIBLE && trigger->chain)
	{
		gentity_t *otherconstructible = NULL;

		// use the target that has the same team as the player
		if (constructible->s.teamNum != ent->client->sess.sessionTeam)
		{
			constructible = trigger->chain;
		}

		otherconstructible = constructible->chain;

		// make sure the other constructible isn't built/underconstruction/something
		if (otherconstructible->s.angles2[0] != 0.f ||
		    otherconstructible->s.angles2[1] != 0.f ||
		    (otherconstructible->count2 && otherconstructible->grenadeFired))
		{
			return qfalse;
		}
	}

	// see if we are in a trigger_objective_info targetting a func_constructible
	if (constructible->s.eType == ET_CONSTRUCTIBLE &&
	    constructible->s.teamNum == ent->client->sess.sessionTeam)
	{
		// constructible xp sharing
		float addhealth;
		float xpperround;

		if (constructible->s.angles2[0] >= 250)     // have to do this so we don't score multiple times
		{
			return qfalse;
		}

		if (constructible->s.angles2[1] != 0.f)
		{
			return qfalse;
		}

		// Check if we can construct - updates the classWeaponTime as well
		if (!ReadyToConstruct(ent, constructible, qtrue))
		{
			return qtrue;
		}

		// try to start building
		if (constructible->s.angles2[0] <= 0)
		{
			// wait a bit, this prevents network spam
			if (level.time - constructible->lastHintCheckTime < CONSTRUCT_POSTDECAY_TIME)
			{
				return qtrue;      // likely will come back soon - so override other plier bits anyway
			}

			// swap brushmodels if staged
			if (constructible->count2)
			{
				constructible->grenadeFired++;
				constructible->s.modelindex2 = constructible->conbmodels[constructible->grenadeFired - 1];
				//trap_SetBrushModel( constructible, va( "*%i", constructible->conbmodels[constructible->grenadeFired-1] ) );
			}

			G_SetEntState(constructible, STATE_UNDERCONSTRUCTION);

			if (!constructible->count2)
			{
				// call script
				G_Script_ScriptEvent(constructible, "buildstart", "final");
				constructible->s.frame = 1;
			}
			else
			{
				if (constructible->grenadeFired == constructible->count2)
				{
					G_Script_ScriptEvent(constructible, "buildstart", "final");
					constructible->s.frame = constructible->grenadeFired;
				}
				else
				{
					switch (constructible->grenadeFired)
					{
					case 1:
						G_Script_ScriptEvent(constructible, "buildstart", "stage1");
						constructible->s.frame = 1;
						break;
					case 2:
						G_Script_ScriptEvent(constructible, "buildstart", "stage2");
						constructible->s.frame = 2;
						break;
					case 3:
						G_Script_ScriptEvent(constructible, "buildstart", "stage3");
						constructible->s.frame = 3;
						break;
					default:
						break;
					}
				}
			}

			if (trigger->chain && trigger->count2)
			{
				// find the constructible indicator and change team
				mapEntityData_t      *mEnt;
				mapEntityData_Team_t *teamList;
				gentity_t            *indicator = &g_entities[trigger->count2];

				indicator->s.teamNum = constructible->s.teamNum;

				// update the map for the other team
				teamList = indicator->s.teamNum == TEAM_AXIS ? &mapEntityData[1] : &mapEntityData[0]; // inversed
				if ((mEnt = G_FindMapEntityData(teamList, indicator - g_entities)) != NULL)
				{
					G_FreeMapEntityData(teamList, mEnt);
				}
			}

			if (!constructible->count2 || constructible->grenadeFired == 1)
			{
				// link in if we just started building
				G_UseEntity(constructible, trigger, ent);
			}

			// setup our think function for decaying
			constructible->think     = func_constructible_underconstructionthink;
			constructible->nextthink = level.time + FRAMETIME;

			G_PrintClientSpammyCenterPrint(ent - g_entities, "Constructing...");
		}

		if (!ent->client->constructSoundTime || level.time > ent->client->constructSoundTime)
		{
			// construction sound sent as event (was temp entity)
			G_AddEvent(ent, EV_GENERAL_SOUND, GAMESOUND_WORLD_BUILD);
			ent->client->constructSoundTime = level.time + 4000; // duration of sound
		}

		// constructible xp sharing
		addhealth  = (255.f / (constructible->constructibleStats.duration / (float)FRAMETIME));
		xpperround = constructible->constructibleStats.constructxpbonus / (255.f / addhealth) + 0.01f;
		G_AddSkillPoints(ent, SK_EXPLOSIVES_AND_CONSTRUCTION, xpperround, "constructing");

		// Give health until it is full, don't continue
		constructible->s.angles2[0] += addhealth;

		if (constructible->s.angles2[0] >= 250)
		{
			// hint task completed
			ent->lastTaskAchievedTime = level.time;

			constructible->s.angles2[0] = 0;
			HandleEntsThatBlockConstructible(ent, constructible, qtrue, qfalse);
		}
		else
		{
			constructible->lastHintCheckTime = level.time;
			HandleEntsThatBlockConstructible(ent, constructible, qfalse, qtrue);
			return qtrue;      // properly constructed
		}

		if (constructible->count2)
		{
			// backup...
			int constructibleClipmask       = constructible->clipmask;
			int constructibleContents       = constructible->r.contents;
			int constructibleNonSolidBModel = (constructible->s.eFlags & EF_NONSOLID_BMODEL);

			constructible->s.modelindex2 = 0;
			trap_SetBrushModel(constructible, va("*%i", constructible->conbmodels[constructible->grenadeFired - 1]));

			// ...and restore
			constructible->clipmask   = constructibleClipmask;
			constructible->r.contents = constructibleContents;
			if (!constructibleNonSolidBModel)
			{
				constructible->s.eFlags &= ~EF_NONSOLID_BMODEL;
			}

			if (constructible->grenadeFired == constructible->count2)
			{
				constructible->s.angles2[1] = 1;
			}
		}
		else
		{
			// backup...
			int constructibleClipmask       = constructible->clipmask;
			int constructibleContents       = constructible->r.contents;
			int constructibleNonSolidBModel = (constructible->s.eFlags & EF_NONSOLID_BMODEL);

			constructible->s.modelindex2 = 0;
			trap_SetBrushModel(constructible, constructible->model);

			// ...and restore
			constructible->clipmask   = constructibleClipmask;
			constructible->r.contents = constructibleContents;
			if (!constructibleNonSolidBModel)
			{
				constructible->s.eFlags &= ~EF_NONSOLID_BMODEL;
			}

			constructible->s.angles2[1] = 1;
		}

		// unlink the objective info to get rid of the indicator for now
		// don't unlink, we still want the location popup. Instead, constructible_indicator_think got changed to free
		// the indicator when the constructible is constructed
		//if( constructible->parent )
		//  trap_UnlinkEntity( constructible->parent );

		G_SetEntState(constructible, STATE_DEFAULT);

		// make destructable
		if (!(constructible->spawnflags & 2))
		{
			constructible->takedamage = qtrue;
			constructible->health     = constructible->sound1to2;
		}

		// Stop thinking
		constructible->think     = NULL;
		constructible->nextthink = 0;

		if (!constructible->count2)
		{
			// call script
			G_Script_ScriptEvent(constructible, "built", "final");
			G_LogPrintf("Repair: %d\n", (int)(ent - g_entities));
		}
		else
		{
			if (constructible->grenadeFired == constructible->count2)
			{
				G_Script_ScriptEvent(constructible, "built", "final");
				G_LogPrintf("Repair: %d\n", (int)(ent - g_entities));
			}
			else
			{
				switch (constructible->grenadeFired)
				{
				case 1:
					G_Script_ScriptEvent(constructible, "built", "stage1");
					break;
				case 2:
					G_Script_ScriptEvent(constructible, "built", "stage2");
					break;
				case 3:
					G_Script_ScriptEvent(constructible, "built", "stage3");
					break;
				default:
					break;
				}
			}
		}

		// Stop sound
		if (constructible->parent->spawnflags & 8)
		{
			constructible->parent->s.loopSound = 0;
		}
		else
		{
			constructible->s.loopSound = 0;
		}

		//ent->client->ps.classWeaponTime = level.time; // Out of "ammo"

		// if not invulnerable and dynamite-able, create a 'destructable' marker for the other team
		if (!(constructible->spawnflags & CONSTRUCTIBLE_INVULNERABLE) && (constructible->constructibleStats.weaponclass >= 1))
		{
			if (!constructible->count2 || constructible->grenadeFired == 1)
			{
				gentity_t *tent = NULL;
				gentity_t *e;

				e               = G_Spawn();
				e->r.svFlags    = SVF_BROADCAST;
				e->classname    = "explosive_indicator";
				e->s.pos.trType = TR_STATIONARY;
				e->s.eType      = ET_EXPLOSIVE_INDICATOR;

				// Find the trigger_objective_info that targets us (if not set before)
				while ((tent = G_Find(tent, FOFS(target), constructible->targetname)) != NULL)
				{
					if (tent->s.eType == ET_OID_TRIGGER)
					{
						if (tent->spawnflags & 8)
						{
							e->s.eType = ET_TANK_INDICATOR;
						}
						e->parent = tent;
					}
				}

				if (constructible->spawnflags & AXIS_CONSTRUCTIBLE)
				{
					e->s.teamNum = TEAM_AXIS;
				}
				else if (constructible->spawnflags & ALLIED_CONSTRUCTIBLE)
				{
					e->s.teamNum = TEAM_ALLIES;
				}

				e->s.modelindex2 = trigger->s.teamNum;
				e->r.ownerNum    = constructible->s.number;
				e->think         = explosive_indicator_think;
				e->nextthink     = level.time + FRAMETIME;

				e->s.effect1Time = constructible->constructibleStats.weaponclass;

				if (constructible->parent->tagParent)
				{
					e->tagParent = constructible->parent->tagParent;
					Q_strncpyz(e->tagName, constructible->parent->tagName, MAX_QPATH);
				}
				else
				{
					VectorCopy(constructible->r.absmin, e->s.pos.trBase);
					VectorAdd(constructible->r.absmax, e->s.pos.trBase, e->s.pos.trBase);
					VectorScale(e->s.pos.trBase, 0.5f, e->s.pos.trBase);
				}

				SnapVector(e->s.pos.trBase);

				trap_LinkEntity(e);
			}
			else
			{
				gentity_t *check;
				int       i;

				// find our marker and update it's coordinates
				for (i = 0, check = g_entities; i < level.num_entities; i++, check++)
				{
					if (check->s.eType != ET_EXPLOSIVE_INDICATOR && check->s.eType != ET_TANK_INDICATOR && check->s.eType != ET_TANK_INDICATOR_DEAD)
					{
						continue;
					}

					if (check->r.ownerNum == constructible->s.number)
					{
						// found it!
						if (constructible->parent->tagParent)
						{
							check->tagParent = constructible->parent->tagParent;
							Q_strncpyz(check->tagName, constructible->parent->tagName, MAX_QPATH);
						}
						else
						{
							VectorCopy(constructible->r.absmin, check->s.pos.trBase);
							VectorAdd(constructible->r.absmax, check->s.pos.trBase, check->s.pos.trBase);
							VectorScale(check->s.pos.trBase, 0.5f, check->s.pos.trBase);

							SnapVector(check->s.pos.trBase);
						}

						trap_LinkEntity(check);
						break;
					}
				}
			}
		}

		return qtrue;      // building
	}

	return qfalse;
}

/**
 * @brief AutoBuildConstruction
 * @param[in,out] constructible
 */
void AutoBuildConstruction(gentity_t *constructible)
{
	HandleEntsThatBlockConstructible(NULL, constructible, qtrue, qfalse);
	if (constructible->count2)
	{
		// backup...
		int constructibleClipmask       = constructible->clipmask;
		int constructibleContents       = constructible->r.contents;
		int constructibleNonSolidBModel = (constructible->s.eFlags & EF_NONSOLID_BMODEL);

		constructible->s.modelindex2 = 0;
		trap_SetBrushModel(constructible, va("*%i", constructible->conbmodels[constructible->grenadeFired - 1]));

		// ...and restore
		constructible->clipmask   = constructibleClipmask;
		constructible->r.contents = constructibleContents;
		if (!constructibleNonSolidBModel)
		{
			constructible->s.eFlags &= ~EF_NONSOLID_BMODEL;
		}

		if (constructible->grenadeFired == constructible->count2)
		{
			constructible->s.angles2[1] = 1;
		}
	}
	else
	{
		// backup...
		int constructibleClipmask       = constructible->clipmask;
		int constructibleContents       = constructible->r.contents;
		int constructibleNonSolidBModel = (constructible->s.eFlags & EF_NONSOLID_BMODEL);

		constructible->s.modelindex2 = 0;
		trap_SetBrushModel(constructible, constructible->model);

		// ...and restore
		constructible->clipmask   = constructibleClipmask;
		constructible->r.contents = constructibleContents;
		if (!constructibleNonSolidBModel)
		{
			constructible->s.eFlags &= ~EF_NONSOLID_BMODEL;
		}

		constructible->s.angles2[1] = 1;
	}

	// unlink the objective info to get rid of the indicator for now
	// don't unlink, we still want the location popup. Instead, constructible_indicator_think got changed to free
	// the indicator when the constructible is constructed
	//          if( constructible->parent )
	//              trap_UnlinkEntity( constructible->parent );

	G_SetEntState(constructible, STATE_DEFAULT);

	// make destructable
	if (!(constructible->spawnflags & CONSTRUCTIBLE_INVULNERABLE))
	{
		constructible->takedamage = qtrue;
		constructible->health     = constructible->constructibleStats.health;
	}

	// Stop thinking
	constructible->think     = NULL;
	constructible->nextthink = 0;

	if (!constructible->count2)
	{
		// call script
		G_Script_ScriptEvent(constructible, "built", "final");
	}
	else
	{
		if (constructible->grenadeFired == constructible->count2)
		{
			G_Script_ScriptEvent(constructible, "built", "final");
		}
		else
		{
			switch (constructible->grenadeFired)
			{
			case 1:
				G_Script_ScriptEvent(constructible, "built", "stage1");
				break;
			case 2:
				G_Script_ScriptEvent(constructible, "built", "stage2");
				break;
			case 3:
				G_Script_ScriptEvent(constructible, "built", "stage3");
				break;
			default:
				break;
			}
		}
	}

	// Stop sound
	if (constructible->parent->spawnflags & 8)
	{
		constructible->parent->s.loopSound = 0;
	}
	else
	{
		constructible->s.loopSound = 0;
	}

	//ent->client->ps.classWeaponTime = level.time; // Out of "ammo"

	//if not invulnerable and dynamite-able, create a 'destructable' marker for the other team
	if (!(constructible->spawnflags & CONSTRUCTIBLE_INVULNERABLE) && (constructible->constructibleStats.weaponclass >= 1))
	{
		if (!constructible->count2 || constructible->grenadeFired == 1)
		{
			gentity_t *tent = NULL;
			gentity_t *e;

			e               = G_Spawn();
			e->r.svFlags    = SVF_BROADCAST;
			e->classname    = "explosive_indicator";
			e->s.pos.trType = TR_STATIONARY;
			e->s.eType      = ET_EXPLOSIVE_INDICATOR;

			// Find the trigger_objective_info that targets us (if not set before)
			while ((tent = G_Find(tent, FOFS(target), constructible->targetname)) != NULL)
			{
				if (tent->s.eType == ET_OID_TRIGGER)
				{
					if (tent->spawnflags & 8)
					{
						e->s.eType = ET_TANK_INDICATOR;
					}
					e->parent = tent;
				}
			}

			if (constructible->spawnflags & AXIS_CONSTRUCTIBLE)
			{
				e->s.teamNum = TEAM_AXIS;
			}
			else if (constructible->spawnflags & ALLIED_CONSTRUCTIBLE)
			{
				e->s.teamNum = TEAM_ALLIES;
			}

			e->s.modelindex2 = constructible->parent->s.teamNum == TEAM_AXIS ? TEAM_ALLIES : TEAM_AXIS;
			e->r.ownerNum    = constructible->s.number;
			e->think         = explosive_indicator_think;
			e->nextthink     = level.time + FRAMETIME;

			e->s.effect1Time = constructible->constructibleStats.weaponclass;

			if (constructible->parent->tagParent)
			{
				e->tagParent = constructible->parent->tagParent;
				Q_strncpyz(e->tagName, constructible->parent->tagName, MAX_QPATH);
			}
			else
			{
				VectorCopy(constructible->r.absmin, e->s.pos.trBase);
				VectorAdd(constructible->r.absmax, e->s.pos.trBase, e->s.pos.trBase);
				VectorScale(e->s.pos.trBase, 0.5f, e->s.pos.trBase);
			}

			SnapVector(e->s.pos.trBase);

			trap_LinkEntity(e);
		}
		else
		{
			gentity_t *check;
			int       i;

			// find our marker and update it's coordinates
			for (i = 0, check = g_entities; i < level.num_entities; i++, check++)
			{
				if (check->s.eType != ET_EXPLOSIVE_INDICATOR && check->s.eType != ET_TANK_INDICATOR && check->s.eType != ET_TANK_INDICATOR_DEAD)
				{
					continue;
				}

				if (check->r.ownerNum == constructible->s.number)
				{
					// found it!
					if (constructible->parent->tagParent)
					{
						check->tagParent = constructible->parent->tagParent;
						Q_strncpyz(check->tagName, constructible->parent->tagName, MAX_QPATH);
					}
					else
					{
						VectorCopy(constructible->r.absmin, check->s.pos.trBase);
						VectorAdd(constructible->r.absmax, check->s.pos.trBase, check->s.pos.trBase);
						VectorScale(check->s.pos.trBase, 0.5f, check->s.pos.trBase);

						SnapVector(check->s.pos.trBase);
					}

					trap_LinkEntity(check);
					break;
				}
			}
		}
	}
}

/**
 * @brief G_LandmineTriggered
 * @param[in] ent
 * @return
 */
qboolean G_LandmineTriggered(gentity_t *ent)
{
	return ent->s.effect1Time == 2;
}

/**
 * @brief G_LandmineArmed
 * @param[in] ent
 * @return
 */
qboolean G_LandmineArmed(gentity_t *ent)
{
	return ent->s.effect1Time == 1;
}

/**
 * @brief G_LandmineUnarmed
 * @param[in] ent
 * @return
 */
qboolean G_LandmineUnarmed(gentity_t *ent)
{
	return ent->s.effect1Time == 0;
}

/**
 * @brief G_LandmineSpotted
 * @param[in] ent
 * @return
 */
qboolean G_LandmineSpotted(gentity_t *ent)
{
	return ent->s.modelindex2 ? qtrue : qfalse;
}

/**
 * @brief trap_EngineerTrace
 * @param[out] results
 * @param[in] start
 * @param[in] mins
 * @param[in] maxs
 * @param[in] end
 * @param[in] passEntityNum
 * @param[in] contentmask
 */
void trap_EngineerTrace(gentity_t *ent, trace_t *results, const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end, int passEntityNum, int contentmask)
{
	G_TempTraceRealHitBox(ent);
	G_TempTraceIgnorePlayersAndBodies();

	trap_Trace(results, start, mins, maxs, end, passEntityNum, contentmask);
	if (results->startsolid && results->entityNum == ENTITYNUM_WORLD)
	{
		vec3_t boxmins = { -10, -10, -10 };
		vec3_t boxmaxs = { 10, 10, 10 };
		trap_Trace(results, start, boxmins, boxmaxs, start, passEntityNum, contentmask);
	}

	G_ResetTempTraceIgnoreEnts();
	G_ResetTempTraceRealHitBox();
}

/**
 * @brief trap_ItemTrace
 * @param[out] results
 * @param[in] start
 * @param[in] mins
 * @param[in] maxs
 * @param[in] end
 * @param[in] passEntityNum
 * @param[in] contentmask
 */
void trap_ItemTrace(gentity_t *ent, trace_t *results, const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end, int passEntityNum, int contentmask)
{
	G_TempTraceRealHitBox(ent);
	G_TempTraceIgnorePlayersAndBodies();

	trap_Trace(results, start, mins, maxs, end, passEntityNum, contentmask);

	G_ResetTempTraceIgnoreEnts();
	G_ResetTempTraceRealHitBox();
}

#define PLIERS_TASK_COMPLETED 255
/// emplaced gun
#define EMPLACEDGUN_REPAIR_RATE 3
#define EMPLACEDGUN_TASK_COMPLETED 255
/// landmine
#define LANDMINE_ARM_RATE 12
#define LANDMINE_DISARM_RATE 3
#define LANDMINE_TASK_COMPLETED 250
/// satchel
#define SATCHEL_DISARM_RATE 3
#define SATCHEL_TASK_COMPLETED 250
// dynamite
#define DYNAMITE_ARM_RATE 7
#define DYNAMITE_ARMED_COMPLETED 250
#define DYNAMITE_DISARM_RATE 3
#define DYNAMITE_DISARMED_COMPLETED 248

/**
 * @brief G_RepairEmplacedGun
 * @param traceEnt[in,out] the emplaced gun to repair
 * @param ent[in,out] the ent reparing the emplaced gun
 */
void G_RepairEmplacedGun(gentity_t *traceEnt, gentity_t *ent)
{
	int weaponTime = ent->client->ps.classWeaponTime;

	// "Ammo" for this weapon is time based
	if (weaponTime + level.engineerChargeTime[ent->client->sess.sessionTeam - 1] < level.time)
	{
		weaponTime = level.time - level.engineerChargeTime[ent->client->sess.sessionTeam - 1];
	}

	if (BG_IsSkillAvailable(ent->client->sess.skill, SK_EXPLOSIVES_AND_CONSTRUCTION, SK_ENGINEER_STAMINA))
	{
		weaponTime += .66f * 150;
	}
	else
	{
		weaponTime += 150;
	}

	// no enough stamina to repair the emplaced gun
	if (weaponTime > level.time)
	{
		return;     // Out of "ammo"
	}

	ent->client->ps.classWeaponTime = weaponTime;

	traceEnt->health += EMPLACEDGUN_REPAIR_RATE;

	G_PrintClientSpammyCenterPrint(ent - g_entities, "Repairing MG 42...");

	// constructible xp sharing - repairing an emplaced mg42
	G_AddSkillPoints(ent, SK_EXPLOSIVES_AND_CONSTRUCTION, 0.03529f, "repairing");

	// not yet fully repaired
	if (traceEnt->health < EMPLACEDGUN_TASK_COMPLETED)
	{
		return;
	}

	// hint task completed
	ent->lastTaskAchievedTime = level.time;

	traceEnt->s.frame = 0;

	if (traceEnt->mg42BaseEnt > 0)
	{
		g_entities[traceEnt->mg42BaseEnt].health     = MG42_MULTIPLAYER_HEALTH;
		g_entities[traceEnt->mg42BaseEnt].takedamage = qtrue;
		traceEnt->health                             = 0;
	}
	else
	{
		traceEnt->health = MG42_MULTIPLAYER_HEALTH;
	}

	G_LogPrintf("Repair: %d\n", (int)(ent - g_entities));

	if (traceEnt->sound3to2 != ent->client->sess.sessionTeam)
	{
		// constructible xp sharing - some lucky dood is going to get the last 0.00035 points and the repair bonus
		G_AddSkillPoints(ent, SK_EXPLOSIVES_AND_CONSTRUCTION, 0.00035f, "repairing");
	}

	traceEnt->takedamage = qtrue;
	traceEnt->s.eFlags  &= ~EF_SMOKING;

	trap_SendServerCommand(ent - g_entities, "cp \"You have repaired the MG 42\" 1");

	G_AddEvent(ent, EV_MG42_FIXED, 0);
}

/**
 * @brief G_RecoverLandmine
 * @param[in,out] traceEnt
 * @param[in,out] ent
 *
 * @note This incorrectly gives back the charge to the player who tries to arm the landmine,
 * not the player who originally threw the landmine. This bug originates from etmain,
 * and has become a feature that some players purposefully utilize, to pass charge
 * between players. Only the stats are corrected here to take away the shot from
 * the player who threw the mine, not the player who tried to arm it (also a bug in etmain).
 */
void G_RecoverLandmine(gentity_t *traceEnt, gentity_t *ent)
{
	gentity_t *owner = g_entities + traceEnt->s.clientNum;

	Add_Ammo(owner, WP_LANDMINE, 1, qfalse);

	// give back the correct charge amount (potentially to "wrong" player, see the note above)
	if (BG_IsSkillAvailable(ent->client->sess.skill, SK_EXPLOSIVES_AND_CONSTRUCTION, SK_ENGINEER_STAMINA))
	{
		ent->client->ps.classWeaponTime -= .33f * level.engineerChargeTime[ent->client->sess.sessionTeam - 1];
	}
	else
	{
		ent->client->ps.classWeaponTime -= .5f * level.engineerChargeTime[ent->client->sess.sessionTeam - 1];
	}

	// take away a shot from the player who threw the mine (not necessarily the same player who's trying to arm it)
	owner->client->sess.aWeaponStats[WS_LANDMINE].atts--;

	// free at the very end so traceEnt stays valid for long enough
	G_FreeEntity(traceEnt);
}

/**
 * @brief G_ArmLandmine
 * @param[in,out] traceEnt the landmine to be armed
 * @param[in,out] ent arming the landmine
 */
void G_ArmLandmine(gentity_t *traceEnt, gentity_t *ent)
{
	// opposing team cannot accidentally arm it
	if (traceEnt->s.teamNum != ent->client->sess.sessionTeam)
	{
		return;
	}

	// the team reach the maximum landmines to be armed, abord planting and free the landmine
	if (G_CountTeamLandmines(ent->client->sess.sessionTeam) >= team_maxLandmines.integer)
	{
		trap_SendServerCommand(ent - g_entities, "cp \"Your team has too many landmines placed\" 1");
		G_RecoverLandmine(traceEnt, ent);
		return;
	}

	G_PrintClientSpammyCenterPrint(ent - g_entities, "Arming landmine...");

	// give health until it is full, don't continue
	if (BG_IsSkillAvailable(ent->client->sess.skill, SK_EXPLOSIVES_AND_CONSTRUCTION, SK_ENGINEER_STAMINA))
	{
		traceEnt->health += LANDMINE_ARM_RATE * 2;
	}
	else
	{
		traceEnt->health += LANDMINE_ARM_RATE;
	}

	if (traceEnt->health < LANDMINE_TASK_COMPLETED)
	{
		return;
	}

	trap_SendServerCommand(ent - g_entities, "cp \"Landmine armed\" 1");

	// hint task completed
	ent->lastTaskAchievedTime = level.time;

	// crosshair mine owner id
	traceEnt->s.otherEntityNum = ent->s.number;

	traceEnt->r.snapshotCallback = qtrue;
	traceEnt->r.contents         = 0; // (player can walk through)
	trap_LinkEntity(traceEnt);

	// don't allow disarming for sec (so guy that WAS arming doesn't start disarming it!
	traceEnt->timestamp = level.time + 1000;
	traceEnt->health    = 0;

	traceEnt->s.effect1Time = 1; // armed
	traceEnt->s.modelindex2 = 0;

	traceEnt->nextthink = level.time + 2000;
	traceEnt->think     = G_LandminePrime;
}

/**
 * @brief G_DisarmLandmine
 * @param[in] traceEnt the landmine to be disarmed
 * @param[in] ent disarming the landmine
 */
void G_DisarmLandmine(gentity_t *traceEnt, gentity_t *ent)
{
	if (traceEnt->timestamp > level.time)
	{
		return;
	}

	if (traceEnt->health >= LANDMINE_TASK_COMPLETED)     // have to do this so we don't score multiple times
	{
		return;
	}

	if (BG_IsSkillAvailable(ent->client->sess.skill, SK_EXPLOSIVES_AND_CONSTRUCTION, SK_ENGINEER_STAMINA))
	{
		traceEnt->health += LANDMINE_DISARM_RATE * 2;
	}
	else
	{
		traceEnt->health += LANDMINE_DISARM_RATE;
	}

	G_PrintClientSpammyCenterPrint(ent - g_entities, "Defusing landmine...");

	if (traceEnt->health >= LANDMINE_TASK_COMPLETED)
	{
		mapEntityData_t *mEnt;

		// hint task completed
		ent->lastTaskAchievedTime = level.time;

		trap_SendServerCommand(ent - g_entities, "cp \"Landmine defused\" 1");

		Add_Ammo(ent, WP_LANDMINE, 1, qfalse);

		if (traceEnt->s.teamNum != ent->client->sess.sessionTeam)
		{
			G_AddSkillPoints(ent, SK_EXPLOSIVES_AND_CONSTRUCTION, 4.f, "defusing landmine");
		}

		// update our map
		if ((mEnt = G_FindMapEntityData(&mapEntityData[0], traceEnt - g_entities)) != NULL)
		{
			G_FreeMapEntityData(&mapEntityData[0], mEnt);
		}

		if ((mEnt = G_FindMapEntityData(&mapEntityData[1], traceEnt - g_entities)) != NULL)
		{
			G_FreeMapEntityData(&mapEntityData[1], mEnt);
		}

		G_FreeEntity(traceEnt);
	}
}

/**
 * @brief G_DisarmSatchel
 * @param[in] traceEnt the satchel to be disarmed
 * @param[in] ent satchel the landmine
 */
void G_DisarmSatchel(gentity_t *traceEnt, gentity_t *ent)
{
	if (traceEnt->health >= PLIERS_TASK_COMPLETED)     // have to do this so we don't score multiple times
	{
		return;
	}

	// give health until it is full, don't continue
	traceEnt->health += SATCHEL_DISARM_RATE;

	G_PrintClientSpammyCenterPrint(ent - g_entities, "Disarming satchel charge...");

	if (traceEnt->health >= SATCHEL_TASK_COMPLETED)
	{
		// hint task completed
		ent->lastTaskAchievedTime = level.time;

		traceEnt->health    = PLIERS_TASK_COMPLETED;
		traceEnt->think     = G_FreeEntity;
		traceEnt->nextthink = level.time + FRAMETIME;

		// consistency with dynamite defusing
		G_PrintClientSpammyCenterPrint(ent - g_entities, "Satchel charge disarmed");

		G_AddSkillPoints(ent, SK_EXPLOSIVES_AND_CONSTRUCTION, 6.f, "disarming satchel");
	}
}

/**
 * @brief Ensure the dynamite isn't planted on friendly objective alone and return
 * dynamitable objective if exist
 * @param[in,out] traceEnt
 * @param[in,out] ent
 * @param[out] the dynamitable objective, otherwise NULL
 * @return true if the dynamite can be armed, otherwise false
 */
qboolean G_CheckDynamitableObjective(gentity_t *traceEnt, gentity_t *ent, gentity_t **obj)
{
	int       entityList[MAX_GENTITIES];
	gentity_t *hit;
	vec3_t    org;
	vec3_t    mins;
	vec3_t    maxs;
	int       i;
	int       num;

	qboolean friendlyObj = qfalse;

	*obj = NULL;

	// First, we check if the dynamite is touching an trigger objective around his hitbox
	// either enemy or allies

	VectorCopy(traceEnt->r.currentOrigin, org);
	SnapVector(org);
	VectorAdd(org, GetWeaponFireTableData(traceEnt->s.weapon)->hitBox[0], mins);
	VectorAdd(org, GetWeaponFireTableData(traceEnt->s.weapon)->hitBox[1], maxs);
	num = trap_EntitiesInBox(mins, maxs, entityList, MAX_GENTITIES);
	VectorAdd(org, GetWeaponFireTableData(traceEnt->s.weapon)->hitBox[0], mins);
	VectorAdd(org, GetWeaponFireTableData(traceEnt->s.weapon)->hitBox[1], maxs);

	for (i = 0 ; i < num ; i++)
	{
		hit = &g_entities[entityList[i]];

		if (!(hit->r.contents & CONTENTS_TRIGGER))
		{
			continue;
		}

		if (hit->s.eType != ET_OID_TRIGGER)
		{
			continue;
		}

		if (!(hit->spawnflags & (AXIS_OBJECTIVE | ALLIED_OBJECTIVE)))
		{
			continue;
		}

		// only if it targets a func_explosive
		if (hit->target_ent && Q_stricmp(hit->target_ent->classname, "func_explosive"))
		{
			continue;
		}

		if (((hit->spawnflags & AXIS_OBJECTIVE) && (ent->client->sess.sessionTeam == TEAM_AXIS)) ||
		    ((hit->spawnflags & ALLIED_OBJECTIVE) && (ent->client->sess.sessionTeam == TEAM_ALLIES)))
		{
			friendlyObj = qtrue;
			continue;
		}

		// early return, we hit an enemy objective, so even if friendly one exist, allow planting the dynamite
		if (((hit->spawnflags & AXIS_OBJECTIVE) && (ent->client->sess.sessionTeam == TEAM_ALLIES)) ||
		    ((hit->spawnflags & ALLIED_OBJECTIVE) && (ent->client->sess.sessionTeam == TEAM_AXIS)))
		{
			// spawnflags 128 = disabled
			if (!(hit->spawnflags & 128))
			{
				*obj = hit;
				return qtrue;
			}
		}
	}

	// in case the dynamite is touching a friendly trigger ojective, deny planting the dynamite
	if (friendlyObj)
	{
		G_FreeEntity(traceEnt);
		trap_SendServerCommand(ent - g_entities, "cp \"You cannot arm dynamite near a friendly objective!\" 1");
		return qfalse;
	}

	// Second, try to figure if the dynamite blast radius is in range of friendly objective
	// if so, we will deny planting the dynamite

	VectorCopy(traceEnt->r.currentOrigin, org);
	org[2] += 4;        // move out of ground

	G_TempTraceIgnorePlayersAndBodies();
	num = EntsThatRadiusCanDamage(org, traceEnt->splashRadius, entityList);
	G_ResetTempTraceIgnoreEnts();

	for (i = 0; i < num; i++)
	{
		hit = &g_entities[entityList[i]];

		if (hit->s.eType != ET_CONSTRUCTIBLE)
		{
			continue;
		}

		// invulnerable
		if ((hit->spawnflags & CONSTRUCTIBLE_INVULNERABLE) || (hit->parent && (hit->parent->spawnflags & 8)))
		{
			continue;
		}

		if (!G_ConstructionIsPartlyBuilt(hit))
		{
			continue;
		}

		// is it a friendly constructible, deny planting the dynamite
		if (hit->s.teamNum == traceEnt->s.teamNum)
		{
			G_FreeEntity(traceEnt);
			trap_SendServerCommand(ent - g_entities, "cp \"You cannot arm dynamite near a friendly objective!\" 1");
			return qfalse;
		}

		// not dynamite-able
		if (hit->constructibleStats.weaponclass < 1)
		{
			continue;
		}

		if (!hit->parent)
		{
			continue;
		}

		*obj = hit;
		break;
	}

	return qtrue;
}

/**
 * @brief G_DynamitePlantedAnnoucement
 * @param[in] traceEnt the dynamite
 * @param[in] ent the planter
 * @param[in] hit the objective
 */
void G_DynamitePlantedAnnoucement(gentity_t *traceEnt, gentity_t *ent, gentity_t *hit)
{
	gentity_t *pm;
#ifdef FEATURE_OMNIBOT
	const char *Goalname = _GetEntityName(hit);

	// notify omni-bot framework of planted dynamite
	hit->numPlanted += 1;
	Bot_AddDynamiteGoal(traceEnt, traceEnt->s.teamNum, va("%s_%i", Goalname, hit->numPlanted));
#endif

	pm                = G_PopupMessage(PM_DYNAMITE);
	pm->s.effect2Time = 0;         // 0 = planted
	pm->s.teamNum     = ent->client->sess.sessionTeam;

	traceEnt->etpro_misc_1 |= 1;    // planted

	G_Script_ScriptEvent(hit, "dynamited", ent->client->sess.sessionTeam == TEAM_AXIS ? "axis" : "allies");

	if (hit->s.eType == ET_OID_TRIGGER)
	{
		pm->s.effect3Time = hit->s.teamNum;

		traceEnt->etpro_misc_2 = hit->s.number;

		if (hit->spawnflags & AXIS_OBJECTIVE)
		{
			if (ent->client->sess.sessionTeam == TEAM_ALLIES)         // transfer score info if this is a bomb scoring objective
			{
				traceEnt->accuracy = hit->accuracy;
			}
		}
		else if (hit->spawnflags & ALLIED_OBJECTIVE)
		{
			if (ent->client->sess.sessionTeam == TEAM_AXIS)         // dito other team
			{
				traceEnt->accuracy = hit->accuracy;
			}
		}

		if (hit->spawnflags & OBJECTIVE_DESTROYED)
		{
			return;
		}

		if (traceEnt->parent && traceEnt->parent->client)
		{
			G_LogPrintf("Dynamite_Plant: %d %s\n", (int)(traceEnt->parent - g_entities), hit->track);
		}
	}
	else // ET_CONSTRUCTIBLE
	{
		pm->s.effect3Time = hit->parent->s.teamNum;

		if (hit->parent->spawnflags & OBJECTIVE_DESTROYED)
		{
			return;
		}

		if (traceEnt->parent && traceEnt->parent->client)
		{
			G_LogPrintf("Dynamite_Plant: %d %s\n", (int)(traceEnt->parent - g_entities), hit->parent->track);
		}
	}

	// give explode score to guy who armed it
	traceEnt->parent = ent;

	// dyno chaining
	traceEnt->onobjective = hit;
	G_DPrintf("dyno chaining: hit: %p\n", hit);
}

/**
 * @brief G_ArmDynamite
 * @param[in,out] traceEnt the dynamite to be armed
 * @param[in,out] ent arming the dynamite
 */
void G_ArmDynamite(gentity_t *traceEnt, gentity_t *ent)
{
	gentity_t *hit = NULL;

	// Opposing team cannot accidentally arm it
	if (traceEnt->s.teamNum != ent->client->sess.sessionTeam)
	{
		return;
	}

	if (level.suddenDeath)
	{
		G_PrintClientSpammyCenterPrint(ent - g_entities, "Too late to arm a dynamite.");
		return;
	}

	// ensure we don't plant a dynamite on friendly obj
	// and retrieved the enemy obj
	if (!G_CheckDynamitableObjective(traceEnt, ent, &hit))
	{
		return;
	}

	// dyno chaining
	traceEnt->onobjective = NULL;

	G_PrintClientSpammyCenterPrint(ent - g_entities, "Arming dynamite...");

	// Give health until it is full, don't continue
	if (BG_IsSkillAvailable(ent->client->sess.skill, SK_EXPLOSIVES_AND_CONSTRUCTION, SK_ENGINEER_PLIERS_DEXTERITY))
	{
		traceEnt->health += DYNAMITE_ARM_RATE * 2;
	}
	else
	{
		traceEnt->health += DYNAMITE_ARM_RATE;
	}

	// not full, don't continue
	if (traceEnt->health < DYNAMITE_ARMED_COMPLETED)
	{
		return;
	}

	traceEnt->health = PLIERS_TASK_COMPLETED;

	// hint task completed
	ent->lastTaskAchievedTime = level.time;

	// don't allow disarming for sec (so guy that WAS arming doesn't start disarming it!
	traceEnt->timestamp = level.time + 1000;
	traceEnt->health    = 5;

	// for dynamic light pulsing
	traceEnt->s.effect1Time = level.time;

	// dynamite crosshair ID
	traceEnt->s.otherEntityNum = ent->s.number;

	// arm it
	traceEnt->nextthink = level.time + 30000;
	traceEnt->think     = G_ExplodeMissile;

	// moved down here to prevent two prints when dynamite IS near objective
	trap_SendServerCommand(ent - g_entities, "cp \"Dynamite is now armed with a 30 second timer!\" 1");

	// not planted near objective, nothing to do more
	if (hit)
	{
		G_DynamitePlantedAnnoucement(traceEnt, ent, hit);
	}
}

/**
 * @brief G_DisarmDynamite
 * @param[in,out] traceEnt the landmine to be armed
 * @param[in,out] ent arming the landmine
 */
void G_DisarmDynamite(gentity_t *traceEnt, gentity_t *ent)
{
	int       entityList[MAX_GENTITIES];
	vec3_t    org;
	gentity_t *hit;
	vec3_t    mins;
	vec3_t    maxs;
	int       i;
	int       num;

	if (traceEnt->timestamp > level.time)
	{
		return;
	}

	if (traceEnt->health >= PLIERS_TASK_COMPLETED)         // have to do this so we don't score multiple times
	{
		return;
	}

	if (BG_IsSkillAvailable(ent->client->sess.skill, SK_EXPLOSIVES_AND_CONSTRUCTION, SK_ENGINEER_PLIERS_DEXTERITY))
	{
		traceEnt->health += DYNAMITE_DISARM_RATE * 2;
	}
	else
	{
		traceEnt->health += DYNAMITE_DISARM_RATE;
	}

	G_PrintClientSpammyCenterPrint(ent - g_entities, "Defusing dynamite...");

	if (traceEnt->health < DYNAMITE_DISARMED_COMPLETED)
	{
		return;
	}

	traceEnt->health = PLIERS_TASK_COMPLETED;

	trap_SendServerCommand(ent - g_entities, "cp \"Dynamite defused\" 1");

	// hint task completed
	ent->lastTaskAchievedTime = level.time;

	traceEnt->think     = G_FreeEntity;
	traceEnt->nextthink = level.time + FRAMETIME;

	// don't report if not disarming *enemy* dynamite in field
	if (traceEnt->s.teamNum == ent->client->sess.sessionTeam)
	{
		return;
	}

	VectorCopy(traceEnt->r.currentOrigin, org);
	SnapVector(org);
	VectorAdd(org, GetWeaponFireTableData(traceEnt->s.weapon)->hitBox[0], mins);
	VectorAdd(org, GetWeaponFireTableData(traceEnt->s.weapon)->hitBox[1], maxs);
	num = trap_EntitiesInBox(mins, maxs, entityList, MAX_GENTITIES);

	for (i = 0 ; i < num ; i++)
	{
		gentity_t *pm;
		hit = &g_entities[entityList[i]];

		if (!(hit->r.contents & CONTENTS_TRIGGER))
		{
			continue;
		}

		if (hit->s.eType != ET_OID_TRIGGER)
		{
			continue;
		}

		if (!(hit->spawnflags & (AXIS_OBJECTIVE | ALLIED_OBJECTIVE)))
		{
			continue;
		}

		// spawnflags 128 = disabled (#309)
		if (hit->spawnflags & 128)
		{
			continue;
		}

		// prevent plant/defuse exploit near a/h cabinets or non-destroyable locations (bank doors on goldrush)
		if (!hit->target_ent || hit->target_ent->s.eType != ET_EXPLOSIVE)
		{
			continue;
		}

		if (hit->spawnflags & (ent->client->sess.sessionTeam == TEAM_AXIS ? AXIS_OBJECTIVE : ALLIED_OBJECTIVE))
		{
			G_LogPrintf("Dynamite_Diffuse: %d %s\n", (int)(ent - g_entities), hit->parent ? hit->parent->track : hit->track);
			G_AddSkillPoints(ent, SK_EXPLOSIVES_AND_CONSTRUCTION, 6.f, "defusing dynamite");
		}

		if (hit->target_ent)
		{
			G_Script_ScriptEvent(hit->target_ent, "defused", ent->client->sess.sessionTeam == TEAM_AXIS ? "axis" : "allies");
		}

		pm = G_PopupMessage(PM_DYNAMITE);

		pm->s.effect2Time = 1;     // 1 = defused
		pm->s.effect3Time = hit->s.teamNum;
		pm->s.teamNum     = ent->client->sess.sessionTeam;

		// the dynamite has been defused
		return;
	}

	// reordered this check so its AFTER the primary obj check
	// - first see if the dynamite was planted near a constructable object that would have been destroyed

	VectorCopy(traceEnt->r.currentOrigin, org);
	org[2] += 4;        // move out of ground

	G_TempTraceIgnorePlayersAndBodies();
	num = EntsThatRadiusCanDamage(org, traceEnt->splashRadius, entityList);
	G_ResetTempTraceIgnoreEnts();

	for (i = 0; i < num; i++)
	{
		gentity_t *pm;
		hit = &g_entities[entityList[i]];

		if (hit->s.eType != ET_CONSTRUCTIBLE)
		{
			continue;
		}

		// invulnerable
		if (hit->spawnflags & CONSTRUCTIBLE_INVULNERABLE)
		{
			continue;
		}

		// not dynamite-able
		if (hit->constructibleStats.weaponclass < 1)
		{
			continue;
		}

		// we got something to destroy
		if (hit->s.teamNum == ent->client->sess.sessionTeam)
		{
			G_LogPrintf("Dynamite_Diffuse: %d %s\n", (int)(ent - g_entities), hit->parent ? hit->parent->track : hit->track);
			G_AddSkillPoints(ent, SK_EXPLOSIVES_AND_CONSTRUCTION, 6.f, "defusing dynamite");
		}

		G_Script_ScriptEvent(hit, "defused", ent->client->sess.sessionTeam == TEAM_AXIS ? "axis" : "allies");

		pm = G_PopupMessage(PM_DYNAMITE);

		pm->s.effect2Time = 1;     // 1 = defused
		pm->s.effect3Time = hit->parent->s.teamNum;
		pm->s.teamNum     = ent->client->sess.sessionTeam;

		// the dynamite has been defused
		break;
	}
}

/**
 * @brief Weapon_Engineer
 * @param[in,out] ent
 */
gentity_t *Weapon_Engineer(gentity_t *ent)
{
	trace_t   tr;
	gentity_t *traceEnt;
	vec3_t    end;

	// Can't use pliers if using a mounted mg
	if (ent->client->ps.persistant[PERS_HWEAPON_USE])
	{
		return NULL;
	}

	AngleVectors(ent->client->ps.viewangles, forward, right, up);
	VectorCopy(ent->client->ps.origin, muzzleTrace);
	muzzleTrace[2] += ent->client->ps.viewheight;

	VectorMA(muzzleTrace, CH_MAX_DIST, forward, end);

	// First, we try the specific case where emplaced can be repaired.
	// But since the TOI is a different ent from the emplaced gun
	// it may block the repair process, so we ignore it.
	if (ent->client->touchingTOI)
	{
		ent->client->touchingTOI->r.linked = qfalse;
	}

	trap_EngineerTrace(ent, &tr, muzzleTrace, NULL, NULL, end, ent->s.number, MASK_SHOT | CONTENTS_TRIGGER);

	if (ent->client->touchingTOI)
	{
		ent->client->touchingTOI->r.linked = qtrue;
	}

	traceEnt = &g_entities[tr.entityNum];

	// ensure we are not targeting too far
	if (VectorDistance(muzzleTrace, tr.endpos) > CH_BREAKABLE_DIST)
	{
		// in this case try to build client touching TOI which was ignored previously
		if (ent->client->touchingTOI)
		{
			// might be constructible
			TryConstructing(ent, ent->client->touchingTOI);
		}
		return NULL;
	}

	// identify the targeted ent
	if (G_EmplacedGunIsRepairable(traceEnt, ent))
	{
		G_RepairEmplacedGun(traceEnt, ent);
		return NULL;
	}

	trap_EngineerTrace(ent, &tr, muzzleTrace, NULL, NULL, end, ent->client->ps.clientNum, MASK_SHOT);

	// if we are looking into void or targeting too far, try to construct the touched TOI
	if ((tr.surfaceFlags & SURF_NOIMPACT) || tr.fraction == 1.0f || tr.entityNum == ENTITYNUM_NONE || tr.entityNum == ENTITYNUM_WORLD
	    || VectorDistance(muzzleTrace, tr.endpos) > CH_BREAKABLE_DIST)
	{
		// might be constructible
		if (ent->client->touchingTOI)
		{
			TryConstructing(ent, ent->client->touchingTOI);
		}
		return NULL;
	}

	traceEnt = &g_entities[tr.entityNum];

	if (traceEnt->methodOfDeath == MOD_LANDMINE)
	{
		trace_t tr2;
		vec3_t  base;
		vec3_t  tr_down = { 0, 0, 16 };

		VectorSubtract(traceEnt->s.pos.trBase, tr_down, base);

		trap_EngineerTrace(ent, &tr2, traceEnt->s.pos.trBase, NULL, NULL, base, traceEnt->s.number, MASK_SHOT);

		// ensure the landmine can be planted at the dropped terrain point
		if (!(tr2.surfaceFlags & SURF_LANDMINE) || (tr2.entityNum != ENTITYNUM_WORLD && (!g_entities[tr2.entityNum].inuse || g_entities[tr2.entityNum].s.eType != ET_CONSTRUCTIBLE)))
		{
			trap_SendServerCommand(ent - g_entities, "cp \"Landmine cannot be armed here\" 1");
			G_RecoverLandmine(traceEnt, ent);
			return NULL;
		}

		if (G_LandmineUnarmed(traceEnt))
		{
			G_ArmLandmine(traceEnt, ent);
		}
		else
		{
			G_DisarmLandmine(traceEnt, ent);
		}
	}
	else if (traceEnt->methodOfDeath == MOD_SATCHEL)
	{
		G_DisarmSatchel(traceEnt, ent);
	}
	else if (traceEnt->methodOfDeath == MOD_DYNAMITE)
	{
		// During the first-revive stand-up restriction, only dynamite plier interaction is blocked.
		if (G_LegacyRevive_IsFirstReviveRestricted(ent))
		{
			return NULL;
		}

		// not armed
		if (!traceEnt->s.effect1Time)
		{
			G_ArmDynamite(traceEnt, ent);
		}
		else
		{
			G_DisarmDynamite(traceEnt, ent);
		}
	}
	else if (ent->client->touchingTOI)
	{
		// the targeted trace ent isn't interacting with pliers,
		// try the touched TOI
		// might be constructible
		TryConstructing(ent, ent->client->touchingTOI);
	}

	return NULL;
}

/**
 * @brief G_MaxAvailableAirstrikes
 * @param[in] ent
 * @return
 */
int G_MaxAvailableAirstrikes(gentity_t *ent)
{
	int playerCount;
	int fieldopsCount;

	playerCount   = G_TeamCount(ent, -1);
	fieldopsCount = G_CountTeamFieldops(ent->client->sess.sessionTeam);

	if (fieldopsCount > 6)
	{
		fieldopsCount = 6;
	}
	else if (fieldopsCount < 2)
	{
		fieldopsCount = 2;
	}

	// max number of airstrikes per team per minute
	return ceil(fieldopsCount * playerCount * g_heavyWeaponRestriction.integer * 0.01);
}

/**
 * @brief G_MaxAvailableArtillery
 * @param[in] ent
 * @return
 */
int G_MaxAvailableArtillery(gentity_t *ent)
{
	int playerCount;
	int fieldopsCount;

	playerCount   = G_TeamCount(ent, -1);
	fieldopsCount = G_CountTeamFieldops(ent->client->sess.sessionTeam);

	if (fieldopsCount > 6)
	{
		fieldopsCount = 6;
	}
	else if (fieldopsCount < 2)
	{
		fieldopsCount = 2;
	}

	// max number of artillery per team per minute
	return ceil(fieldopsCount * playerCount * g_heavyWeaponRestriction.integer * 0.01);
}

/**
 * @brief G_AvailableAirstrikes
 * @param[in] ent
 * @return
 */
qboolean G_AvailableAirstrike(gentity_t *ent)
{
	if (ent->client->sess.sessionTeam == TEAM_AXIS)
	{
		if (level.axisAirstrikeCounter > 60000)
		{
			return qfalse;
		}
	}
	else
	{
		if (level.alliedAirstrikeCounter > 60000)
		{
			return qfalse;
		}
	}
	return qtrue;
}

/**
 * @brief Arty/airstrike rate limiting
 * @param[in] ent
 * @return
 */
qboolean G_AvailableArtillery(gentity_t *ent)
{
	if (ent->client->sess.sessionTeam == TEAM_AXIS)
	{
		if (level.axisArtilleryCounter > 60000)
		{
			return qfalse;
		}
	}
	else
	{
		if (level.alliedArtilleryCounter > 60000)
		{
			return qfalse;
		}
	}
	return qtrue;
}

/**
 * @brief G_AddAirstrikeToCounters
 * @param[in] ent
 */
void G_AddAirstrikeToCounters(gentity_t *ent)
{
	if (ent->client->sess.sessionTeam == TEAM_AXIS)
	{
		if (team_maxAirstrikes.value)
		{
			level.axisAirstrikeCounter += 60000 / team_maxAirstrikes.value;
		}
		else if (g_heavyWeaponRestriction.integer)
		{
			level.axisAirstrikeCounter += 60000 / G_MaxAvailableAirstrikes(ent);
		}
		else
		{
			level.axisAirstrikeCounter = 0;
		}
	}
	else
	{
		if (team_maxAirstrikes.value)
		{
			level.alliedAirstrikeCounter += 60000 / team_maxAirstrikes.value;
		}
		else if (g_heavyWeaponRestriction.integer)
		{
			level.alliedAirstrikeCounter += 60000 / G_MaxAvailableAirstrikes(ent);
		}
		else
		{
			level.alliedAirstrikeCounter = 0;
		}
	}
}

/**
 * @brief G_AddArtilleryToCounters
 * @param[in] ent
 */
void G_AddArtilleryToCounters(gentity_t *ent)
{
	if (ent->client->sess.sessionTeam == TEAM_AXIS)
	{
		if (team_maxArtillery.value)
		{
			level.axisArtilleryCounter += 60000 / team_maxArtillery.value;
		}
		else if (g_heavyWeaponRestriction.integer)
		{
			level.axisArtilleryCounter += 60000 / G_MaxAvailableArtillery(ent);
		}
		else
		{
			level.axisArtilleryCounter = 0;
		}
	}
	else
	{
		if (team_maxArtillery.value)
		{
			level.alliedArtilleryCounter += 60000 / team_maxArtillery.value;
		}
		else if (g_heavyWeaponRestriction.integer)
		{
			level.alliedArtilleryCounter += 60000 / G_MaxAvailableArtillery(ent);
		}
		else
		{
			level.alliedArtilleryCounter = 0;
		}
	}
}


/**
 * @brief G_BombTrace ensures bombs from arty/airstrike are never spawned indoors
 * @param[in] tr
 * @param[in] start
 * @param[in] end
 * @param[out] tr
 */
trace_t G_BombTrace(trace_t tr, vec3_t start, vec3_t end, gentity_t *ent)
{
	trap_Trace(&tr, start, NULL, NULL, end, ent->s.number, CONTENTS_SOLID);

	// if we can't trace back to sky, we likely spawned indoors - keep nudging start position up until we find skybox
	while (!(tr.surfaceFlags & SURF_SKY))
	{
		start[2] += 64;
		trap_Trace(&tr, start, NULL, NULL, end, ent->s.number, CONTENTS_SOLID);

		// reached map limit and no sky found
		if (start[2] > MAX_MAP_SIZE || tr.fraction == 1.0f)
		{
			break;
		}
	}

	return tr;
}

#define NUMBOMBS 10
#define BOMBSPREAD 150

/**
 * @brief weapon_checkAirStrikeThink
 * @param[in,out] ent
 */
void weapon_checkAirStrikeThink(gentity_t *ent)
{
	if (!weapon_checkAirStrike(ent))
	{
		ent->think     = G_ExplodeMissile;
		ent->nextthink = level.time + 1000;
		return;
	}

	ent->think = weapon_callAirStrike;

	if (ent->parent->client && BG_IsSkillAvailable(ent->parent->client->sess.skill, SK_SIGNALS, SK_FIELDOPS_EXTRA_FIRE_SUPPORT))
	{
		ent->nextthink = level.time + 2500;
	}
	else
	{
		ent->nextthink = level.time + 1500;
	}
}

/**
 * @brief weapon_checkAirStrike
 * @param[in,out] ent
 * @return
 */
qboolean weapon_checkAirStrike(gentity_t *ent)
{
	// cancel the airstrike if FF off and player joined spec
	// FIXME: this is a stupid workaround. Just store the parent team in the enitity itself and use that - no need to look up the parent
	if (!g_friendlyFire.integer && ent->parent->client && ent->parent->client->sess.sessionTeam == TEAM_SPECTATOR)
	{
		ent->splashDamage = 0;  // no damage
		ent->think        = G_ExplodeMissile;
		ent->nextthink    = (int)(level.time + crandom() * 50);

		ent->active = qfalse;

		return qfalse; // do nothing, don't hurt anyone
	}

	if (!G_AvailableAirstrike(ent->parent))
	{
		G_HQSay(ent->parent, COLOR_YELLOW, "HQ: ", "All available planes are already en-route.");

		G_GlobalClientEvent(EV_AIRSTRIKEMESSAGE, 0, ent->parent - g_entities);

		ent->active = qfalse;

		return qfalse;
	}

	return qtrue;
}

/**
 * @brief G_AirStrikeExplode
 * @param[in,out] ent
 */
void G_AirStrikeThink(gentity_t *ent)
{
	// plane see the target ?
	if (ent->active)
	{
		vec3_t  bomboffset;
		trace_t tr;
		vec3_t  tmp;

		bomboffset[0] = crandom() * .5f * BOMBSPREAD;
		bomboffset[1] = crandom() * .5f * BOMBSPREAD;
		bomboffset[2] = 0.f;
		VectorAdd(ent->r.currentOrigin, bomboffset, bomboffset);

		VectorCopy(bomboffset, tmp);
		tmp[2] = -MAX_MAP_SIZE;

		trap_Trace(&tr, bomboffset, NULL, NULL, tmp, ent->s.number, MASK_MISSILESHOT);

		// ensure the bomb drop in the world, otherwise, skip it
		if (tr.fraction < 1.f /*&& !tr.startsolid*/)
		{
			gentity_t *bomb;
			vec3_t    angle;
			float     ground = tr.endpos[2];

			tmp[2] = MAX_MAP_SIZE;
			tr     = G_BombTrace(tr, tr.endpos, tmp, ent);

			bomb = fire_missile((ent->parent && ent->parent->client) ? ent->parent : ent, tr.endpos,
			                    tv(0, 0, (ground - tr.endpos[2]) * (1.f / 0.75f)), ent->s.weapon);

			// overwrite
			bomb->s.pos.trTime = (int)(level.time + crandom() * 50);

			VectorCopy(ent->r.currentAngles, angle);

			angle[0] = AngleSubtract(angle[0], -(15 + crandom() * 5));
			VectorCopy(angle, bomb->r.currentAngles);
			VectorCopy(angle, bomb->s.apos.trBase);
		}
	}

	ent->nextthink = level.time + FRAMETIME;

	// bomb dropped
	--ent->count;

	// no more bomb to drop
	if (ent->count <= 0)
	{
		ent->think     = G_FreeEntity;
		ent->nextthink = level.time + 5000;

		// don't fade plane
		//if (ent->s.time != -2)
		{
			ent->s.time  = level.time;                  // fade effect
			ent->s.time2 = ent->nextthink;              // fade effect
		}
	}
}

/**
 * @brief weapon_callPlane
 * @param[in,out] ent
 */
void weapon_callPlane(gentity_t *ent)
{
	G_AddEvent(ent, EV_GLOBAL_SOUND, GAMESOUND_WPN_AIRSTRIKE_PLANE);

	G_AirStrikeThink(ent);
	ent->think = G_AirStrikeThink;

	// don't draw plane
	//if (ent->s.time != -2)
	{
		ent->s.time  = 0;       // stop fade effect
		ent->s.time2 = 0;       // stop fade effect
	}
}

/**
 * @brief Launch airstrike as line of bombs mostly-perpendicular to line of grenade travel
 * (close air support should *always* drop parallel to friendly lines, tho accidents do happen)
 * @param[in,out] ent
 */
void weapon_callAirStrike(gentity_t *ent)
{
	int       i;
	vec3_t    bombaxis, lookaxis, pos, dir, angle, end;
	trace_t   tr;
	gentity_t *plane;
	float     skyHeight;

	// FIXME: HARD-CODED!
	static vec3_t planeBBoxMin = { -330, -504, -35 }, planeBBoxMax = { 330, 504, 128 };

	if (ent->parent->client && BG_IsSkillAvailable(ent->parent->client->sess.skill, SK_SIGNALS, SK_FIELDOPS_EXTRA_FIRE_SUPPORT))
	{
		ent->count = 2;
	}
	else
	{
		ent->count = 1;
	}

	// turn off smoke grenade
	ent->think     = G_ExplodeMissile;
	ent->nextthink = (int)(level.time + 950 + ((ent->count - 1) * 2000) + NUMBOMBS * 100 + crandom() * 50);     // 950 offset is for aircraft flyby + plane count

	VectorCopy(ent->s.pos.trBase, end);
	end[2] += MAX_TRACE;

	trap_Trace(&tr, ent->s.pos.trBase, NULL, NULL, end, ent->s.number, MASK_SHOT);
	if ((tr.fraction < 1.0f) && (!(tr.surfaceFlags & SURF_NOIMPACT)))           //SURF_SKY)) ) { // changed for trenchtoast foggie prollem
	{
		G_HQSay(ent->parent, COLOR_YELLOW, "Pilot: ", "Aborting, can't see target.");

		G_GlobalClientEvent(EV_AIRSTRIKEMESSAGE, 1, ent->parent - g_entities);

		ent->active = qfalse;   // plane arrive and shell is abort
	}
	else
	{
		// arty/airstrike rate limiting
		G_AddAirstrikeToCounters(ent->parent);

		G_HQSay(ent->parent, COLOR_YELLOW, "Pilot: ", "Affirmative, on my way!");

		G_GlobalClientEvent(EV_AIRSTRIKEMESSAGE, 2, ent->parent - g_entities);

		ent->active = qtrue;
	}

	// keep sky height
	skyHeight = tr.endpos[2];

	VectorSubtract(ent->s.pos.trBase, ent->parent->client->ps.origin, lookaxis);
	lookaxis[2] = 0;
	VectorNormalize(lookaxis);

	dir[0] = 0;
	dir[1] = 0;
	dir[2] = crandom();     // generate either up or down vector
	VectorNormalize(dir);   // which adds randomness to pass direction below

	for (i = 0; i < ent->count; i++)
	{
		RotatePointAroundVector(bombaxis, dir, lookaxis, 90 + crandom() * 30);   // munge the axis line a bit so it's not totally perpendicular
		VectorNormalize(bombaxis);

		VectorScale(bombaxis, (-.5f * BOMBSPREAD * NUMBOMBS), pos);
		VectorAdd(ent->s.pos.trBase, pos, pos);   // first bomb position
		VectorScale(bombaxis, BOMBSPREAD * NUMBOMBS, bombaxis);   // bomb drop direction offset

		// target can't be see, move the aircraft higher
		if (!ent->active)
		{
			VectorCopy(pos, end);
			pos[2] = MAX_MAP_SIZE;

			trap_TraceCapsule(&tr, pos, planeBBoxMin, planeBBoxMax, end, ent->s.number, MASK_SOLID);

			pos[2] = tr.endpos[2];
		}
		else
		{
			pos[2] = skyHeight;
		}

		// destination point of aircraft
		//VectorMA(pos, 4, bombaxis, end);
		//end[2] = skyHeight;
		//
		//trap_TraceCapsule(&tr, pos, planeBBoxMin, planeBBoxMax, end, ent->s.number, MASK_SOLID);
		//while (tr.fraction < 1.0f && (tr.surfaceFlags & SURF_NOIMPACT) && !VectorCompare(tr.endpos, end)) //SURF_SKY)) ) { // changed for trenchtoast foggie prollem
		//{
		//	// move a little bit to avoid hitting thin surface (coronas, light, ....)
		//	VectorMA(tr.endpos, FRAMETIME * 0.001f, bombaxis, tr.endpos);
		//
		//	trap_TraceCapsule(&tr, tr.endpos, planeBBoxMin, planeBBoxMax, end, ent->s.number, MASK_SOLID);
		//}

		vectoangles(bombaxis, angle);

		// spotter
		plane               = G_Spawn();
		plane->parent       = ent->parent;
		plane->think        = weapon_callPlane;
		plane->active       = ent->active;
		plane->s.weapon     = WP_AIRSTRIKE;
		plane->s.teamNum    = ent->s.teamNum;
		plane->s.clientNum  = ent->s.clientNum;
		plane->r.ownerNum   = ent->r.ownerNum;
		plane->nextthink    = level.time + 250 + (i * 2000);   // 1000 (250 + 750 from first missile) for aircraft flyby, other term for tumble stagger
		plane->r.svFlags    = SVF_BROADCAST;
		plane->count        = NUMBOMBS;
		plane->s.eType      = ET_AIRSTRIKE_PLANE;
		plane->s.pos.trType = TR_LINEAR;
		plane->s.pos.trTime = plane->nextthink;
		plane->s.time       = -1;         // draw nothing   plane->nextthink; // fade effect
		plane->s.time2      = -1;         // draw nothing   level.time;       // fade effect

		//if (tr.fraction < 1.0f && !(tr.surfaceFlags & SURF_NOIMPACT))        //SURF_SKY)) ) { // changed for trenchtoast foggie prollem
		//{
		//	int skyFloor, skyCeil;
		//	skyFloor = BG_GetTracemapSkyGroundFloor();
		//	skyCeil  = BG_GetTracemapSkyGroundCeil();
		//
		//	// some maps block missile throws sky by defining a thin sky surface and adding
		//	// a surface as SURF_NODRAW and SURF_NOMARK, this to ensure no map exploit.
		//	// in case we encounter this kind of sky, simply check if the sky have an height
		//	if (level.tracemapLoaded && skyFloor != skyCeil)
		//	{
		//		pos[2] = skyCeil;
		//	}
		//	else        // don't draw plane at all, sky box is totaly wrong
		//	{
		//		plane->s.time  = -2;
		//		plane->s.time2 = -2;
		//	}
		//}

		VectorCopy(planeBBoxMin, plane->r.mins);
		VectorCopy(planeBBoxMax, plane->r.maxs);

		SnapVector(pos);
		SnapVector(bombaxis);
		SnapVector(angle);

		VectorCopy(pos, plane->r.currentOrigin);
		VectorCopy(pos, plane->s.pos.trBase);
		VectorCopy(angle, plane->r.currentAngles);
		VectorCopy(angle, plane->s.apos.trBase);
		VectorCopy(bombaxis, plane->s.pos.trDelta);
	}
}

/**
 * @brief Makes smoke disappear after a bit (just unregisters stuff)
 * @param[out] ent
 */
void artilleryGoAway(gentity_t *ent)
{
	ent->freeAfterEvent = qtrue;
	trap_LinkEntity(ent);
}

/**
 * @brief Makes smoke appear for the first bomb
 * @param[in] ent
 */
void G_ArtilleryExplode(gentity_t *ent)
{
	// make smoke appear ?
	if (ent->s.effect1Time == 1)
	{
		int i;

		for (i = 0; i < 7; i++)
		{
			gentity_t *bomb;
			vec3_t    tmpdir;

			tmpdir[0] = crandom();
			tmpdir[1] = crandom();
			tmpdir[2] = 1;
			VectorNormalize(tmpdir);
			tmpdir[2] = 1;           // extra up
			VectorScale(tmpdir, 500 + random() * 500, tmpdir);

			bomb = fire_missile(ent->parent ? ent->parent : ent, ent->r.currentOrigin, tmpdir, WP_SMOKETRAIL);

			// add randomness
			bomb->nextthink += random() * 300;
		}
	}
}

/**
 * @brief G_ArtilleryThink
 * @param[in,out] ent
 */
void G_ArtilleryThink(gentity_t *ent)
{
	G_AddEvent(ent, EV_MISSILE_FALLING, 0);
}

/**
 * @brief Dropping the artillery bomb
 * @param[in,out] ent
 * @note arty nextthink is used to store delay between each shelling bomb, it is overwrite
 * to play the shelling sound once the bomb is enough close to the ground
 */
void artillerySpotterThink(gentity_t *ent)
{
	gentity_t *bomb;
	vec3_t    bomboffset;
	trace_t   tr;
	vec3_t    tmp;
	float     ground;

	// spotter, bomb dropped
	ent->count -= 1;

	// is first bomb dropped ?
	if (ent->count2 == 1)
	{
		// spotter round is always dead on (OK, unrealistic but more fun)
		bomboffset[0] = crandom() * 50;     // was 0; changed per id request to prevent spotter round assassinations
		bomboffset[1] = crandom() * 50;     // was 0;
		bomboffset[2] = 0;
		VectorAdd(bomboffset, ent->s.pos.trBase, bomboffset);

		VectorCopy(bomboffset, tmp);
		tmp[2] = -MAX_MAP_SIZE;

		trap_Trace(&tr, bomboffset, NULL, NULL, tmp, ent->s.number, MASK_MISSILESHOT);

		if (tr.fraction == 1.f || tr.startsolid)
		{
			tr.endpos[0] = ent->s.pos.trBase[0];
			tr.endpos[1] = ent->s.pos.trBase[1];
		}

		ground = tr.endpos[2];

		tmp[2] = MAX_MAP_SIZE;
		tr     = G_BombTrace(tr, tr.endpos, tmp, ent);

		bomb = fire_missile((ent->parent && ent->parent->client) ? ent->parent : ent, tr.endpos, tv(0, 0, (ground - tr.endpos[2]) * (1.f / 0.75f)), ent->s.weapon);

		bomb->nextthink        += 3950; // overwrite, add delay between 1st bomb and 2nd one
		bomb->splashDamage      = 90;   // overwrite
		bomb->splashRadius      = 50;   // overwrite
		bomb->s.effect1Time     = 1;    // first bomb, make smoke appear
		bomb->s.otherEntityNum2 = 1;    // hud mortar marker

		// spotter, first bomb dropped
		ent->count2 = 0;
	}
	else
	{
		bomboffset[0] = crandom() * 250;
		bomboffset[1] = crandom() * 250;
		bomboffset[2] = 0;
		VectorAdd(bomboffset, ent->s.pos.trBase, bomboffset);

		VectorCopy(bomboffset, tmp);
		tmp[2] = -MAX_MAP_SIZE;

		trap_Trace(&tr, bomboffset, NULL, NULL, tmp, ent->s.number, MASK_MISSILESHOT);

		if (tr.fraction == 1.f /*|| tr.startsolid*/)
		{
			tr.endpos[0] = ent->s.pos.trBase[0];
			tr.endpos[1] = ent->s.pos.trBase[1];
		}

		ground = tr.endpos[2];

		tmp[2] = MAX_MAP_SIZE;
		tr     = G_BombTrace(tr, tr.endpos, tmp, ent);

		bomb = fire_missile((ent->parent && ent->parent->client) ? ent->parent : ent, tr.endpos, tv(0, 0, (ground - tr.endpos[2]) * (1.f / 0.75f)), ent->s.weapon);
	}

	// next bomb drop, add randomness
	ent->nextthink = bomb->nextthink + crandom() * 800;

	// overwrite
	bomb->nextthink = level.time + FRAMETIME;

	// no more bomb to drop
	if (ent->count <= 0)
	{
		ent->freeAfterEvent = qtrue;
		trap_LinkEntity(ent);

		return;
	}
}

/**
 * @brief G_GlobalClientEvent
 * @param[in] event
 * @param[in] param
 * @param[in] client
 */
void G_GlobalClientEvent(entity_event_t event, int param, int client)
{
	gentity_t *tent;

	tent = G_TempEntity(vec3_origin, event);

	tent->s.density      = param;
	tent->r.singleClient = client;
	tent->r.svFlags      = SVF_SINGLECLIENT | SVF_BROADCAST;

	// Calling for a lot of artillery or airstrikes can result voice over spam
	tent->s.effect1Time = 1; // don't buffer
}

/**
 * @brief Weapon_Artillery
 * @param[in,out] ent
 */
void Weapon_Artillery(gentity_t *ent)
{
	trace_t   tr;
	vec3_t    muzzlePoint, end, pos;
	gentity_t *spotter;

	if (ent->client->ps.stats[STAT_PLAYER_CLASS] != PC_FIELDOPS)
	{
		G_Printf("not a fieldops, you can't shoot this!\n");
		return;
	}

	// FIXME: decide if we want to do charge costs for 'Insufficient fire support' calls
	//        and remove ReadyToCallArtillery() function
	// FIXME: check omnibot interface to deal with this
	if (!ReadyToCallArtillery(ent))
	{
		return;
	}

	if (ent->client->sess.sessionTeam == TEAM_AXIS || ent->client->sess.sessionTeam == TEAM_ALLIES)
	{
		if (!G_AvailableArtillery(ent))
		{
			G_HQSay(ent, COLOR_YELLOW, "Fire Mission: ", "Insufficient fire support.");
			ent->active = qfalse;

			G_GlobalClientEvent(EV_ARTYMESSAGE, 0, ent - g_entities);

			return;
		}
	}

	AngleVectors(ent->client->ps.viewangles, forward, right, up);

	VectorCopy(ent->r.currentOrigin, muzzlePoint);
	muzzlePoint[2] += ent->client->ps.viewheight;

	VectorMA(muzzlePoint, MAX_TRACE, forward, end);
	trap_Trace(&tr, muzzlePoint, NULL, NULL, end, ent->s.number, MASK_SHOT);

	if (tr.surfaceFlags & SURF_NOIMPACT)
	{
		return;
	}

	VectorCopy(tr.endpos, pos);
	pos[2] = BG_GetSkyHeightAtPoint(pos);

	trap_Trace(&tr, tr.endpos, NULL, NULL, pos, ent->s.number, MASK_SHOT);
	if (tr.fraction < 1.0f && !(tr.surfaceFlags & SURF_NOIMPACT)) // was SURF_SKY
	{
		G_HQSay(ent, COLOR_YELLOW, "Fire Mission: ", "Aborting, can't see target.");

		G_GlobalClientEvent(EV_ARTYMESSAGE, 1, ent - g_entities);

		return;
	}

	// final result
	VectorCopy(tr.endpos, pos);

	// arty/airstrike rate limiting
	G_AddArtilleryToCounters(ent);

	G_HQSay(ent, COLOR_YELLOW, "Fire Mission: ", "Firing for effect!");

	G_GlobalClientEvent(EV_ARTYMESSAGE, 2, ent - g_entities);

	// spotter
	spotter               = G_Spawn();
	spotter->parent       = ent;
	spotter->think        = artillerySpotterThink;
	spotter->s.weapon     = WP_ARTY;
	spotter->s.teamNum    = ent->client->sess.sessionTeam;
	spotter->s.clientNum  = ent->client->ps.clientNum;
	spotter->r.ownerNum   = ent->s.number;
	spotter->nextthink    = level.time + 5000;
	spotter->r.svFlags    = SVF_BROADCAST;
	spotter->count2       = 1;                           // first bomb
	spotter->s.pos.trType = TR_STATIONARY;
	SnapVector(pos);
	VectorCopy(pos, spotter->r.currentOrigin);
	VectorCopy(pos, spotter->s.pos.trBase);

	// "spotter" round (i == 0)
	// i == 1->4 is regular explosives
	if (BG_IsSkillAvailable(ent->client->sess.skill, SK_SIGNALS, SK_FIELDOPS_EXTRA_FIRE_SUPPORT))
	{
		spotter->count = 9;
	}
	else
	{
		spotter->count = 5;
	}

	if (BG_IsSkillAvailable(ent->client->sess.skill, SK_SIGNALS, SK_FIELDOPS_FIRE_SUPPORT_STAMINA))
	{
		if (level.time - ent->client->ps.classWeaponTime > level.fieldopsChargeTime[ent->client->sess.sessionTeam - 1])
		{
			ent->client->ps.classWeaponTime = level.time - level.fieldopsChargeTime[ent->client->sess.sessionTeam - 1];
		}

		ent->client->ps.classWeaponTime += 0.66f * level.fieldopsChargeTime[ent->client->sess.sessionTeam - 1];
	}
	else
	{
		ent->client->ps.classWeaponTime = level.time;
	}

	// weapon stats
#ifndef DEBUG_STATS
	if (g_gamestate.integer == GS_PLAYING)
#endif
	ent->client->sess.aWeaponStats[WS_ARTILLERY].atts++;
#ifdef FEATURE_OMNIBOT
	// Omni-bot - Send a fire event.
	Bot_Event_FireWeapon(ent - g_entities, Bot_WeaponGameToBot(WP_ARTY), 0);
#endif
}

#define SMOKEBOMB_GROWTIME 1000
#define SMOKEBOMB_SMOKETIME 15000
#define SMOKEBOMB_POSTSMOKETIME 2000
#define SMOKEBOMB_STARTRADIUS 16
#define SMOKEBOMB_FINALRADIUS 640

/**
 * @brief Increases postsmoke time from 2000->32000, this way, the entity
 * is still around while the smoke is around, so we can check if it blocks bot's vision
 * - eeeeeh this is wrong. 32 seconds is way too long. Also - we shouldn't be
 * rendering the grenade anymore after the smoke stops and definately not send it to the client
 * - back to the old value 2000, now that it looks like smoke disappears more
 * quickly
 *
 * @param[in,out] ent
 */
void weapon_smokeBombExplode(gentity_t *ent)
{
	int lived = 0;

	if (!ent->grenadeExplodeTime)
	{
		ent->grenadeExplodeTime = level.time;
		ent->s.effect1Time      = SMOKEBOMB_STARTRADIUS;
	}

	lived          = level.time - ent->grenadeExplodeTime;
	ent->nextthink = level.time + FRAMETIME;

	if (lived < SMOKEBOMB_GROWTIME)
	{
		// Just been thrown, increase radius
		ent->s.effect1Time = (int)(SMOKEBOMB_STARTRADIUS + lived * ((SMOKEBOMB_FINALRADIUS - SMOKEBOMB_STARTRADIUS) / (float)SMOKEBOMB_GROWTIME));
	}
	else if (lived < SMOKEBOMB_SMOKETIME + SMOKEBOMB_GROWTIME)
	{
		// Smoking
		ent->s.effect1Time = SMOKEBOMB_FINALRADIUS;
	}
	else if (lived < SMOKEBOMB_SMOKETIME + SMOKEBOMB_GROWTIME + SMOKEBOMB_POSTSMOKETIME)
	{
		// Dying out
		ent->s.effect1Time = -1;
	}
	else
	{
		// Poof and it's gone
		G_FreeEntity(ent);
	}
}

/*
======================================================================
MACHINEGUN
======================================================================
*/

/**
 * @brief Round a vector to integers for more efficient network
 * transmission, but make sure that it rounds towards a given point
 * rather than blindly truncating.  This prevents it from truncating
 * into a wall.
 *
 * @param[in,out] v
 * @param[out] to
 *
 * @note Modified so it doesn't have trouble with negative locations (quadrant problems)
 * (this was causing some problems with bullet marks appearing since snapping
 * too far off the target surface causes the the distance between the transmitted impact
 * point and the actual hit surface larger than the mark radius.  (so nothing shows) )
 */
void SnapVectorTowards(vec3_t v, vec3_t to)
{
	int i;

	for (i = 0 ; i < 3 ; i++)
	{
		if (to[i] <= v[i])
		{
			//          v[i] = (int)v[i];
			v[i] = (float)floor(v[i]);
		}
		else
		{
			//          v[i] = (int)v[i] + 1;
			v[i] = (float)ceil(v[i]);
		}
	}
}

/**
 * @brief See if a new particle emitter should be created at the bullet impact point
 * @param[in] ent
 * @param[in] attacker
 * @param[in] tr
 */
void EmitterCheck(gentity_t *ent, gentity_t *attacker, trace_t *tr)
{
	vec3_t origin;

	VectorCopy(tr->endpos, origin);
	SnapVectorTowards(origin, attacker->s.origin);

	if (Q_stricmp(ent->classname, "func_leaky") == 0)
	{
		gentity_t *tent;

		tent = G_TempEntity(origin, EV_EMITTER);

		VectorCopy(origin, tent->s.origin);
		tent->s.time    = 1234;
		tent->s.density = 9876;
		VectorCopy(tr->plane.normal, tent->s.origin2);
	}
}

/**
 * @brief Find target end position for bullet trace based on entities weapon and accuracy
 *
 * @param[in] ent
 * @param[in] spread
 * @param[out] end
 */
void Bullet_Endpos(gentity_t *ent, float spread, vec3_t *end)
{
	if (GetWeaponTableData(ent->s.weapon)->type & WEAPON_TYPE_SCOPED)
	{
		// aim dir already accounted for sway of scoped weapons in CalcMuzzlePoints()
		VectorMA(muzzleTrace, 2 * MAX_TRACE, forward, *end);
	}
	else
	{
		VectorMA(muzzleTrace, MAX_TRACE, forward, *end);

		// random spread
		VectorMA(*end, (crandom() * spread), right, *end);
		VectorMA(*end, (crandom() * spread), up, *end);
	}
}

/**
 * @brief Bullet_Fire
 * @param[in] ent
 */
gentity_t *Bullet_Fire(gentity_t *ent)
{
	vec3_t end;
	float  spread = GetWeaponTableData(ent->s.weapon)->spread;
	float  aimSpreadScale;

	if (g_userAim.integer)
	{
		aimSpreadScale = ent->client->currentAimSpreadScale;
		// add accuracy factor for AI
		aimSpreadScale += 0.15f; // just adding a temp /maximum/ accuracy for player (this will be re-visited in greater detail :)
		if (aimSpreadScale > 1)
		{
			aimSpreadScale = 1.0f;  // still cap at 1.0
		}
	}
	else
	{
		aimSpreadScale = 1.f;
	}

	if (ent->client->ps.groundEntityNum == ENTITYNUM_NONE)
	{
		aimSpreadScale = 2.f;
	}

	if (GetWeaponTableData(ent->s.weapon)->type & WEAPON_TYPE_RIFLE)
	{
		aimSpreadScale = 1.f;
	}

	spread *= aimSpreadScale;

	if ((GetWeaponTableData(ent->s.weapon)->type & (WEAPON_TYPE_SMG | WEAPON_TYPE_PISTOL))
	    && !(GetWeaponTableData(ent->s.weapon)->type & WEAPON_TYPE_SCOPED)
	    && !(GetWeaponTableData(ent->s.weapon)->attributes & WEAPON_ATTRIBUT_AKIMBO))
	{
		// increase in accuracy (spread reduction) at level 3
		if (BG_IsSkillAvailable(ent->client->sess.skill, SK_LIGHT_WEAPONS, SK_LIGHT_WEAPONS_HANDLING))
		{
			spread *= .65f;
		}
	}
	else if (GetWeaponTableData(ent->s.weapon)->type & WEAPON_TYPE_MG)
	{
		if (GetWeaponTableData(ent->s.weapon)->type & WEAPON_TYPE_SET)
		{
			spread *= .05f;
		}
		else if ((ent->client->ps.pm_flags & PMF_DUCKED) || (ent->client->ps.eFlags & EF_PRONE))
		{
			spread *= .6f;
		}
	}

	Bullet_Endpos(ent, spread, &end);

	G_HistoricalTraceBegin(ent);

	// skip corpses for bullet tracing (=non gibbing weapons)
	G_TempTraceIgnoreBodies();

	Bullet_Fire_Extended(ent, ent, muzzleTrace, end, forward, GetWeaponTableData(ent->s.weapon)->damage,
	                     GetWeaponTableData(ent->s.weapon)->attributes & WEAPON_ATTRIBUT_FALL_OFF, GetWeaponTableData(ent->s.weapon)->mod);

	// ok let the bodies be traced again
	G_ResetTempTraceIgnoreEnts();

	G_HistoricalTraceEnd(ent);

	return NULL;
}

/**
 * @brief A modified Bullet_Fire with more parameters.
 *
 * @details The original Bullet_Fire still passes through here and functions as it always has.
 *
 * Uses for this include shooting through entities (windows, doors, other players, etc.) and reflecting bullets
 *
 * @param[in] source
 * @param[in] attacker
 * @param[in] start
 * @param[in] end
 * @param[in] damage
 * @param[in] distance_falloff
 * @return
 */
void Bullet_Fire_Extended(gentity_t *source, gentity_t *attacker, vec3_t start, vec3_t end, vec3_t dir, int damage, qboolean distance_falloff, meansOfDeath_t mod)
{
	trace_t   tr;
	gentity_t *tent;
	gentity_t *traceEnt;
	//qboolean  hitClient = qfalse;
	qboolean waslinked = qfalse;
	vec3_t   impactPos;
	int      hitType = HIT_NONE;

	// prevent shooting ourselves in the head when prone, firing through a breakable
	if (g_entities[attacker->s.number].client && g_entities[attacker->s.number].r.linked == qtrue)
	{
		g_entities[attacker->s.number].r.linked = qfalse;
		waslinked                               = qtrue;
	}

	G_Trace(source, &tr, start, NULL, NULL, end, source->s.number, MASK_SHOT);

	// prevent shooting ourselves in the head when prone, firing through a breakable
	if (waslinked == qtrue)
	{
		g_entities[attacker->s.number].r.linked = qtrue;
	}

	// bullet debugging using Q3A's railtrail
	if (g_debugBullets.integer & 1)
	{
		tent = G_TempEntity(start, EV_RAILTRAIL);
		VectorCopy(tr.endpos, tent->s.origin2);
		tent->s.otherEntityNum2 = attacker->s.number;

		if (g_debugForSingleClient.integer > -1)
		{
			tent->r.svFlags      = SVF_SINGLECLIENT;
			tent->r.singleClient = g_debugForSingleClient.integer;
		}
	}

	traceEnt = &g_entities[tr.entityNum];

	EmitterCheck(traceEnt, attacker, &tr);

	// snap the endpos to integers, but nudged towards the line
	VectorCopy(tr.endpos, impactPos);
	SnapVectorTowards(impactPos, start);

	// TODO: Unused
	/*
		if (AccuracyHit(traceEnt, attacker))
		{
			hitClient = qtrue;
		}
	*/

	// TODO: we don't reflect bullet, for now
	/* bullet impact should reflect off surface
		vec3_t reflect;
		float  dot;

	dot = DotProduct(forward, tr.plane.normal);
	VectorMA(forward, -2 * dot, tr.plane.normal, reflect);
	VectorNormalize(reflect);

	tent->s.eventParm = DirToByte(reflect);
	*/

	if ((g_debugBullets.integer >= 2 && traceEnt->takedamage && traceEnt->client)   // show hit player
	    || g_debugBullets.integer <= -2)        // show hit thing bb
	{
		gentity_t *bboxEnt;
		vec3_t    b1, b2;
		vec3_t    maxs;

		VectorCopy(traceEnt->r.currentOrigin, b1);
		VectorCopy(traceEnt->r.currentOrigin, b2);
		VectorAdd(b1, traceEnt->r.mins, b1);
		VectorCopy(traceEnt->r.maxs, maxs);
		maxs[2] = ClientHitboxMaxZ(traceEnt);
		VectorAdd(b2, maxs, b2);
		bboxEnt = G_TempEntity(b1, EV_RAILTRAIL);
		VectorCopy(b2, bboxEnt->s.origin2);
		bboxEnt->s.dmgFlags = 1;    // ("type")

		if (g_debugForSingleClient.integer > -1)
		{
			bboxEnt->r.svFlags      = SVF_SINGLECLIENT;
			bboxEnt->r.singleClient = g_debugForSingleClient.integer;
		}
	}

	if (traceEnt->takedamage)
	{
		// skip corpses for bullet tracing (=non gibbing weapons)
		G_DamageExt(traceEnt, attacker, attacker, dir, tr.endpos, damage, (distance_falloff ? DAMAGE_DISTANCEFALLOFF : 0), mod, &hitType);

		// allow bullets to "pass through" func_explosives if they break by taking another simultanious shot
		if (traceEnt->s.eType == ET_EXPLOSIVE)
		{
			if (traceEnt->health <= damage)
			{
				// start new bullet at position this hit the bmodel and continue to the end position (ignoring shot-through bmodel in next trace)
				// spread = 0 as this is an extension of an already spread shot
				/*return*/ Bullet_Fire_Extended(traceEnt, attacker, tr.endpos, end, dir, damage, distance_falloff, mod);
			}
		}
	}

	// send bullet impact
	tent                    = G_TempEntity(impactPos, EV_BULLET);
	tent->s.otherEntityNum2 = traceEnt->s.number;
	tent->s.weapon          = GetMODTableData(mod)->weaponIcon;
	tent->s.otherEntityNum  = attacker->s.number;
	tent->s.modelindex      = hitType;   // send the hit sound info in the flesh hit event

	/*return hitClient;*/
}

/**
======================================================================
GRENADE LAUNCHER
  700 has been the standard direction multiplier in fire_grenade()
======================================================================
*/

/**
 * @brief weapon_gpg40_fire
 * @param[in] ent
 * @param[out] firedShot
 * @return
 */
gentity_t *weapon_gpg40_fire(gentity_t *ent)
{
	trace_t tr;
	vec3_t  viewpos;
	vec3_t  tosspos;
	vec3_t  orig_viewpos; // to prevent nade-through-teamdoor sploit

	AngleVectors(ent->client->ps.viewangles, forward, NULL, NULL);

	VectorCopy(muzzleEffect, tosspos);

	// check for valid start spot (so you don't throw through or get stuck in a wall)
	VectorCopy(ent->s.pos.trBase, viewpos);
	viewpos[2] += ent->client->ps.viewheight;
	VectorCopy(viewpos, orig_viewpos);      // to prevent nade-through-teamdoor sploit
	VectorMA(viewpos, 32, forward, viewpos);

	// to prevent nade-through-teamdoor sploit
	trap_Trace(&tr, orig_viewpos, GetWeaponFireTableData(ent->s.weapon)->boundingBox[0], GetWeaponFireTableData(ent->s.weapon)->boundingBox[1], viewpos, ent->s.number, MASK_MISSILESHOT);
	if (tr.fraction < 1)     // oops, bad launch spot
	{
		VectorCopy(tr.endpos, tosspos);
		SnapVectorTowards(tosspos, orig_viewpos);
	}
	else
	{
		trap_Trace(&tr, viewpos, GetWeaponFireTableData(ent->s.weapon)->boundingBox[0], GetWeaponFireTableData(ent->s.weapon)->boundingBox[1], tosspos, ent->s.number, MASK_MISSILESHOT);
		if (tr.fraction < 1)     // oops, bad launch spot
		{
			VectorCopy(tr.endpos, tosspos);
			SnapVectorTowards(tosspos, viewpos);
		}
	}

	VectorScale(forward, 2000, forward);

	// return the grenade so we can do some prediction before deciding if we really want to throw it or not
	return fire_missile(ent, tosspos, forward, ent->s.weapon);
}

/**
 * @brief weapon_mortar_fire
 * @param[in] ent
 * @param[in] firedShot
 * @return
 */
gentity_t *weapon_mortar_fire(gentity_t *ent)
{
	trace_t tr;
	vec3_t  launchPos, testPos;
	vec3_t  angles;

	VectorCopy(ent->client->ps.viewangles, angles);
	angles[PITCH] -= 60.f;
	//if( angles[PITCH] < -89.f )
	//  angles[PITCH] = -89.f;
	AngleVectors(angles, forward, NULL, NULL);

	VectorCopy(muzzleEffect, launchPos);

	// check for valid start spot (so you don't throw through or get stuck in a wall)
	VectorMA(launchPos, 32, forward, testPos);

	forward[0] *= 3000 * 1.1f;
	forward[1] *= 3000 * 1.1f;
	forward[2] *= 1500 * 1.1f;

	trap_Trace(&tr, testPos, GetWeaponFireTableData(ent->s.weapon)->boundingBox[0], GetWeaponFireTableData(ent->s.weapon)->boundingBox[1], launchPos, ent->s.number, MASK_MISSILESHOT);

	if (tr.fraction < 1)     // oops, bad launch spot
	{
		VectorCopy(tr.endpos, launchPos);
		SnapVectorTowards(launchPos, testPos);
	}

	return fire_missile(ent, launchPos, forward, ent->s.weapon);
}

/**
 * @brief weapon_grenadelauncher_fire
 * @param[in] ent
 * @param[out] firedShot
 * @return
 */
gentity_t *weapon_grenadelauncher_fire(gentity_t *ent)
{
	trace_t tr;
	vec3_t  viewpos;
	float   upangle = 0, pitch = ent->s.apos.trBase[0];    // start with level throwing and adjust based on angle
	vec3_t  tosspos;

	// smoke grenades always overhand
	if (pitch >= 0)
	{
		forward[2] += 0.5f;
		// Used later in underhand boost
		pitch = 1.3f;
	}
	else
	{
		pitch       = -pitch;
		pitch       = MIN(pitch, 30);
		pitch      /= 30.f;
		pitch       = 1 - pitch;
		forward[2] += (pitch * 0.5f);

		// Used later in underhand boost
		pitch *= 0.3f;
		pitch += 1.f;
	}

	VectorNormalizeFast(forward);         // make sure forward is normalized

	upangle  = -(ent->s.apos.trBase[0]);  // this will give between  -90 / 90
	upangle  = MIN(upangle, 50);
	upangle  = MAX(upangle, -50);         //    now clamped to  -50 / 50    (don't allow firing straight up/down)
	upangle  = upangle / 100.0f;          //                   -0.5 / 0.5
	upangle += 0.5f;                      //                    0.0 / 1.0

	if (upangle < .1f)
	{
		upangle = .1f;
	}

	if ((GetWeaponTableData(ent->s.weapon)->type & WEAPON_TYPE_GRENADE) || ent->s.weapon == WP_SMOKE_MARKER || ent->s.weapon == WP_SMOKE_BOMB)
	{
		upangle *= 900;
	}
	else    // WP_DYNAMITE / WP_LANDMINE / WP_SATCHEL
	{
		upangle *= 400;
	}

	VectorCopy(muzzleEffect, tosspos);

	// move a little bit more away from the player (so underhand tosses don't get caught on nearby lips)
	VectorMA(muzzleEffect, 8, forward, tosspos);
	tosspos[2] -= 8;    // lower origin for the underhand throw
	upangle    *= pitch;
	SnapVector(tosspos);

	VectorScale(forward, upangle, forward);

	// check for valid start spot (so you don't throw through or get stuck in a wall)
	VectorCopy(ent->s.pos.trBase, viewpos);
	viewpos[2] += ent->client->ps.viewheight;

	trap_Trace(&tr, viewpos, GetWeaponFireTableData(ent->s.weapon)->boundingBox[0], GetWeaponFireTableData(ent->s.weapon)->boundingBox[1], tosspos, ent->s.number, MASK_MISSILESHOT);

	if (tr.startsolid)
	{
		// this code is a bit more solid than the previous code
		VectorCopy(forward, viewpos);
		VectorNormalizeFast(viewpos);
		VectorMA(ent->r.currentOrigin, -24.f, viewpos, viewpos);

		trap_Trace(&tr, viewpos, GetWeaponFireTableData(ent->s.weapon)->boundingBox[0], GetWeaponFireTableData(ent->s.weapon)->boundingBox[1], tosspos, ent->s.number, MASK_MISSILESHOT);

		VectorCopy(tr.endpos, tosspos);
	}
	else if (tr.fraction < 1)        // oops, bad launch spot
	{
		VectorCopy(tr.endpos, tosspos);
		SnapVectorTowards(tosspos, viewpos);
	}

	// return the grenade so we can do some prediction before deciding if we really want to throw it or not
	return fire_missile(ent, tosspos, forward, ent->s.weapon);
}

/**
 * @brief weapon_satcheldet_fire
 * @param[in] ent
 * @return
 */
gentity_t *weapon_satcheldet_fire(gentity_t *ent)
{
	if (G_ExplodeSatchels(ent))
	{
		if (ent->client)
		{
			ent->client->ps.ammoclip[WP_SATCHEL_DET] = 0;
			ent->client->ps.ammoclip[WP_SATCHEL]     = 1;
			G_AddEvent(ent, EV_NOAMMO, 0);
		}
	}

	return NULL;
}

/**
======================================================================
ANTI TANK ROCKETS
======================================================================
*/

/**
 * @brief weapon_antitank_fire
 * @param[in,out] ent
 * @param[out] firedShot
 */
gentity_t *weapon_antitank_fire(gentity_t *ent)
{
	gentity_t *rocket;
	vec3_t    dir;

	//VectorAdd( m->s.pos.trDelta, ent->client->ps.velocity, m->s.pos.trDelta );  // "real" physics
	VectorCopy(forward, dir);
	VectorNormalize(dir);
	VectorScale(dir, 2500, dir);

	rocket = fire_missile(ent, muzzleEffect, dir, ent->s.weapon);

	if (ent->client)
	{
		vec3_t forward;

		AngleVectors(ent->client->ps.viewangles, forward, NULL, NULL);
		VectorMA(ent->client->ps.velocity, -64, forward, ent->client->ps.velocity);
	}

	return rocket;
}

/**
======================================================================
FLAMETHROWER
======================================================================
*/

/**
 * @brief BurnMeGood now takes the flamechunk separately, because
 * the old 'set-self-in-flames' method doesn't have a flamechunk to
 * pass, and deaths were getting blamed on the world/player 0
 *
 * @param[in] self
 * @param[in,out] body
 * @param[in] chunk
 */
void G_BurnMeGood(gentity_t *self, gentity_t *body, gentity_t *chunk, const qboolean directhit)
{
	vec3_t origin;

	// normalize burn dps
	// direct hits every 50ms, indirect (chunk damage) every 100ms
	if (level.time < body->lastBurnedFrameTime + (directhit ? DEFAULT_SV_FRAMETIME : FRAMETIME))
	{
		return;
	}

	// add the new damage
	body->flameQuota         += 5;
	body->flameQuotaTime      = level.time - level.time % DEFAULT_SV_FRAMETIME;
	body->lastBurnedFrameTime = level.time - level.time % DEFAULT_SV_FRAMETIME;

	// fill in our own origin if we have no flamechunk
	if (chunk != NULL)
	{
		VectorCopy(chunk->r.currentOrigin, origin);
	}
	else
	{
		VectorCopy(self->r.currentOrigin, origin);
	}

	G_Damage(body, self, self, vec3_origin, origin, GetWeaponTableData(WP_FLAMETHROWER)->damage, 0, MOD_FLAMETHROWER);

	// make em burn
	if (body->client && (body->health <= 0 || body->flameQuota > 0)) // was > FLAME_THRESHOLD
	{
		if (body->s.onFireEnd < level.time)
		{
			body->s.onFireStart = level.time;
		}
		body->s.onFireEnd = level.time + FIRE_FLASH_TIME;
		// use ourself as the attacker if we have no flamechunk
		body->flameBurnEnt = (chunk != NULL) ? chunk->r.ownerNum : self->s.number;
		// add to playerState for client-side effect
		body->client->ps.onFireStart = level.time;
	}
}

/**
 * @brief Weapon_FlamethrowerFire
 * @param[in,out] ent
 * @param[out] firedShot
 * @return
 */
gentity_t *Weapon_FlamethrowerFire(gentity_t *ent)
{
	vec3_t  start, dir;
	vec3_t  trace_start;
	vec3_t  trace_end;
	trace_t trace;

	VectorCopy(ent->r.currentOrigin, start);
	start[2] += ent->client->ps.viewheight;
	VectorCopy(start, trace_start);

	VectorMA(start, -8, forward, start);
	VectorMA(start, 10, right, start);
	VectorMA(start, -6, up, start);

	// prevent flame thrower cheat, run & fire while aiming at the ground, don't get hurt
	// 72 total box height, 18 xy -> 77 trace radius (from view point towards the ground) is enough to cover the area around the feet
	VectorMA(trace_start, 77.0f, forward, trace_end);
	trap_Trace(&trace, trace_start, GetWeaponFireTableData(ent->s.weapon)->boundingBox[0], GetWeaponFireTableData(ent->s.weapon)->boundingBox[1], trace_end, ent->s.number, MASK_SHOT | MASK_WATER);
	if (trace.fraction != 1.0f)
	{
		// additional checks to filter out false positives
		if (trace.endpos[2] > (ent->r.currentOrigin[2] + ent->r.mins[2] - 8) && trace.endpos[2] < ent->r.currentOrigin[2])
		{
			// trigger in a 21 radius around origin
			trace_start[0] -= trace.endpos[0];
			trace_start[1] -= trace.endpos[1];
			if (trace_start[0] * trace_start[0] + trace_start[1] * trace_start[1] < 441)
			{
				// set self in flames
				G_BurnMeGood(ent, ent, NULL, qtrue);
			}
		}
	}

	// flamethrower exploit fix
	ent->client->flametime = level.time + 2500;
	ent->r.svFlags        |= SVF_BROADCAST;

	VectorCopy(forward, dir);
	VectorNormalize(dir);
	VectorScale(dir, FLAME_START_SPEED, dir);

	return fire_flamechunk(ent, start, dir);
}

//======================================================================

/**
 * @brief Add leaning offset
 * @param[in] ent
 * @param[out] point
 */
void AddLean(gentity_t *ent, vec3_t point)
{
	if (ent->client)
	{
		if (ent->client->ps.leanf != 0.f)
		{
			vec3_t right;

			AngleVectors(ent->client->ps.viewangles, NULL, right, NULL);
			VectorMA(point, ent->client->ps.leanf, right, point);
		}
	}
}

/**
 * @brief AccuracyHit
 * @param[in] target
 * @param[in] attacker
 * @return
 */
qboolean AccuracyHit(gentity_t *target, gentity_t *attacker)
{
	if (!target->takedamage)
	{
		return qfalse;
	}

	if (!attacker)
	{
		return qfalse;
	}

	if (target == attacker)
	{
		return qfalse;
	}

	if (!target->client)
	{
		return qfalse;
	}

	if (!attacker->client)
	{
		return qfalse;
	}

	if (target->client->ps.stats[STAT_HEALTH] <= 0)
	{
		return qfalse;
	}

	if (OnSameTeam(target, attacker))
	{
		return qfalse;
	}

	return qtrue;
}

/**
 * @brief Set muzzle location relative to pivoting eye
 * @param[in] ent
 * @param[in] weapon
 * @param forward - unused
 * @param[in] right
 * @param[in] up
 * @param[out] muzzlePoint
 */
void CalcMuzzlePoint(gentity_t *ent, int weapon, vec3_t forward, vec3_t right, vec3_t up, vec3_t muzzlePoint)
{
	VectorCopy(ent->r.currentOrigin, muzzlePoint);
	muzzlePoint[2] += ent->client->ps.viewheight;

	// this puts the start point outside the bounding box, isn't necessary
	//VectorMA(muzzlePoint, GetWeaponTableData(weapon)->muzzlePointOffset[0], forward, muzzlePoint);

	// offset for more realistic firing from actual gun position
	// changed this so I can predict weapons
	VectorMA(muzzlePoint, GetWeaponTableData(weapon)->muzzlePointOffset[1], right, muzzlePoint);
	VectorMA(muzzlePoint, GetWeaponTableData(weapon)->muzzlePointOffset[2], up, muzzlePoint);

	// actually, this is sort of moot right now since
	// you're not allowed to fire when leaning.  Leave in
	// in case we decide to enable some lean-firing.
	// - works with gl now
	//AddLean(ent, muzzlePoint);

	// snap to integer coordinates for more efficient network bandwidth usage
	SnapVector(muzzlePoint);
}

/**
 * @brief For activate
 * @param[in] ent
 * @param forward - unused
 * @param right - unused
 * @param up - unused
 * @param[out] muzzlePoint
 */
void CalcMuzzlePointForActivate(gentity_t *ent, vec3_t forward, vec3_t right, vec3_t up, vec3_t muzzlePoint)
{
	VectorCopy(ent->client->ps.origin, muzzlePoint);
	muzzlePoint[2] += ent->client->ps.viewheight;

	AddLean(ent, muzzlePoint);

	// snap to integer coordinates for more efficient network bandwidth usage
	// muzzleTrace is not sent over network
	//SnapVector(muzzlePoint);
}

/**
 * @brief CalcMuzzlePoints
 * @param[in] ent
 * @param[in] weapon
 */
void CalcMuzzlePoints(gentity_t *ent, int weapon)
{
	vec3_t viewang;

	VectorCopy(ent->client->ps.viewangles, viewang);

	// non ai's take into account scoped weapon 'sway' (just another way aimspread is visualized/utilized)
	if (GetWeaponTableData(weapon)->type & WEAPON_TYPE_SCOPED)
	{
		float phase;

		phase           = level.time / 1000.0 * ZOOM_PITCH_FREQUENCY * M_TAU_F;
		viewang[PITCH] += MAX(ZOOM_PITCH_AMPLITUDE * ent->client->currentAimSpreadScale, ZOOM_PITCH_MIN_AMPLITUDE) * sin(phase) ;

		phase         = level.time / 1000.0 * ZOOM_YAW_FREQUENCY * M_TAU_F;
		viewang[YAW] += MAX(ZOOM_YAW_AMPLITUDE * ent->client->currentAimSpreadScale, ZOOM_YAW_MIN_AMPLITUDE) * sin(phase) ;
	}

	// set aiming directions
	AngleVectors(viewang, forward, right, up);

	// modified the muzzle stuff so that weapons that need to fire down a perfect trace
	// straight out of the camera (SP5, Mauser right now) can have that accuracy, but
	// weapons that need an offset effect (bazooka/grenade/etc.) can still look like
	// they came out of the weap.
	CalcMuzzlePointForActivate(ent, forward, right, up, muzzleTrace);
	CalcMuzzlePoint(ent, weapon, forward, right, up, muzzleEffect);
}

/**
 * @brief G_PlayerCanBeSeenByOthers
 * @param[in] ent
 * @return
 */
qboolean G_PlayerCanBeSeenByOthers(gentity_t *ent)
{
	int       i;
	gentity_t *ent2;
	vec3_t    pos[3];

	VectorCopy(ent->client->ps.origin, pos[0]);
	pos[0][2] += ent->client->ps.mins[2];
	VectorCopy(ent->client->ps.origin, pos[1]);
	VectorCopy(ent->client->ps.origin, pos[2]);
	pos[2][2] += ent->client->ps.maxs[2];

	for (i = 0, ent2 = g_entities; i < level.maxclients; i++, ent2++)
	{
		if (!ent2->inuse || ent2 == ent)
		{
			continue;
		}

		if (ent2->client->sess.sessionTeam == TEAM_SPECTATOR)
		{
			continue;
		}

		if (ent2->health <= 0 ||
		    ent2->client->sess.sessionTeam == ent->client->sess.sessionTeam)
		{
			continue;
		}

		if (ent2->client->ps.eFlags & EF_ZOOMING)
		{
			G_SetupFrustum_ForBinoculars(ent2);
		}
		else
		{
			G_SetupFrustum(ent2);
		}

		if (G_VisibleFromBinoculars(ent2, ent, pos[0]) ||
		    G_VisibleFromBinoculars(ent2, ent, pos[1]) ||
		    G_VisibleFromBinoculars(ent2, ent, pos[2]))
		{
			return qtrue;
		}
	}

	return qfalse;
}

// *INDENT-OFF*
weapFireTable_t weapFireTable[] =
{
    // weapon                  fire                         think                       free                eType                  eFlags                      svFlags                       content          trType          trTime                 boundingBox                                      hitBox                                           clipMask          nextThink  accuracy health timeStamp impactDamage
    { WP_NONE,                 NULL,                        NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_STATIONARY,  0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_ALL,         0,         0,       0,     0,        0,           },
    { WP_KNIFE,                Weapon_Knife,                NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_LUGER,                Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_MP40,                 Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_GRENADE_LAUNCHER,     weapon_grenadelauncher_fire, G_ExplodeMissile,           NULL,               ET_MISSILE,            EF_BOUNCE_HALF | EF_BOUNCE, SVF_BROADCAST,                CONTENTS_NONE,   TR_GRAVITY,     MISSILE_PRESTEP_TIME,  { { -4.f, -4.f, 0.f }, { 4.f, 4.f, 6.f } },      { { -4.f, -4.f, 0.f }, { 4.f, 4.f, 6.f } },      MASK_MISSILESHOT, 2500,      0,       0,     0,        20,          },
    { WP_PANZERFAUST,          weapon_antitank_fire,        G_ExplodeMissile,           NULL,               ET_MISSILE,            EF_NONE,                    SVF_BROADCAST,                CONTENTS_NONE,   TR_LINEAR,      MISSILE_PRESTEP_TIME,  { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_MISSILESHOT, 20000,     4,       0,     0,        999,         },
    { WP_FLAMETHROWER,         Weapon_FlamethrowerFire,     NULL,                       NULL,               ET_FLAMETHROWER_CHUNK, EF_NONE,                    SVF_NOCLIENT | SVF_BROADCAST, CONTENTS_CORPSE, TR_DECCELERATE, 0,                     { { -4, -4, -4 }, { 4, 4, 4 } },                 { { -4, -4, -4 }, { 4, 4, 4 } },                 MASK_MISSILESHOT, 0,         0,       0,     0,        20,          },  // this is done client-side only now // - um, no it isnt? FIXME
    { WP_COLT,                 Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_THOMPSON,             Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_GRENADE_PINEAPPLE,    weapon_grenadelauncher_fire, G_ExplodeMissile,           NULL,               ET_MISSILE,            EF_BOUNCE_HALF | EF_BOUNCE, SVF_BROADCAST,                CONTENTS_NONE,   TR_GRAVITY,     MISSILE_PRESTEP_TIME,  { { -4.f, -4.f, 0.f }, { 4.f, 4.f, 6.f } },      { { -4.f, -4.f, 0.f }, { 4.f, 4.f, 6.f } },      MASK_MISSILESHOT, 2500,      0,       0,     0,        20,          },
    //
	{ WP_STEN,                 Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_MEDIC_SYRINGE,        Weapon_Syringe,              NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_AMMO,                 Weapon_MagicAmmo,            G_MagicSink,                NULL,               ET_ITEM,               EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_GRAVITY,     0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        30000,     0,       0,     0,        0,           },
    { WP_ARTY,                 NULL,                        G_ArtilleryThink,           G_ArtilleryExplode, ET_MISSILE,            EF_NONE,                    SVF_BROADCAST,                CONTENTS_NONE,   TR_LINEAR,      MISSILE_PRESTEP_TIME,  { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_MISSILESHOT, 2000,      2,       0,     0,        20,          },
    { WP_SILENCER,             Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_DYNAMITE,             weapon_grenadelauncher_fire, DynaSink,                   DynaFree,           ET_MISSILE,            EF_BOUNCE_HALF | EF_BOUNCE, SVF_BROADCAST,                CONTENTS_CORPSE, TR_GRAVITY,     MISSILE_PRESTEP_TIME,  { { -10.f, -10.f, 0.f }, { 10.f, 10.f, 11.f } }, { { -12.f, -12.f, 0.f }, { 12.f, 12.f, 20.f } }, MASK_MISSILESHOT, 15000,     0,       5,     16500,    20,          },
    { WP_SMOKETRAIL,           NULL,                        artilleryGoAway,            NULL,               ET_MISSILE,            EF_BOUNCE,                  SVF_NONE,                     CONTENTS_NONE,   TR_GRAVITY,     MISSILE_PRESTEP_TIME,  { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_MISSILESHOT, 1000,      0,       0,     0,        20,          },
    { WP_MAPMORTAR,            NULL,                        G_ExplodeMissile,           NULL,               ET_MISSILE,            EF_NONE,                    SVF_BROADCAST,                CONTENTS_NONE,   TR_GRAVITY,     MISSILE_PRESTEP_TIME,  { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_MISSILESHOT, 20000,     4,       0,     0,        999,         },
    { VERYBIGEXPLOSION,        NULL,                        NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_STATIONARY,  0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_ALL,         0,         0,       0,     0,        0,           },
    { WP_MEDKIT,               Weapon_Medic,                G_MagicSink,                NULL,               ET_ITEM,               EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_GRAVITY,     0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        30000,     0,       0,     0,        0,           },
    //
	{ WP_BINOCULARS,           NULL,                        NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_STATIONARY,  0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_ALL,         0,         0,       0,     0,        0,           },
    { WP_PLIERS,               Weapon_Engineer,             NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_STATIONARY,  0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_ALL,         0,         0,       0,     0,        0,           },
    { WP_SMOKE_MARKER,         weapon_grenadelauncher_fire, weapon_checkAirStrikeThink, NULL,               ET_MISSILE,            EF_BOUNCE_HALF | EF_BOUNCE, SVF_BROADCAST,                CONTENTS_NONE,   TR_GRAVITY,     MISSILE_PRESTEP_TIME,  { { -4.f, -4.f, 0.f }, { 4.f, 4.f, 6.f } },      { { -4.f, -4.f, 0.f }, { 4.f, 4.f, 6.f } },      MASK_MISSILESHOT, 2500,      0,       0,     0,        20,          },
    { WP_KAR98,                Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_CARBINE,              Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_GARAND,               Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_LANDMINE,             weapon_grenadelauncher_fire, DynaSink,                   NULL,               ET_MISSILE,            EF_BOUNCE_HALF | EF_BOUNCE, SVF_BROADCAST,                CONTENTS_CORPSE, TR_GRAVITY,     MISSILE_PRESTEP_TIME,  { { -4.f, -4.f, 0.f }, { 4.f, 4.f, 10.f } },     { { -16.f, -16.f, 0.f }, { 16.f, 16.f, 16.f } }, MASK_MISSILESHOT, 15000,     0,       5,     16500,    20,          },
    { WP_SATCHEL,              weapon_grenadelauncher_fire, NULL,                       G_FreeSatchel,      ET_MISSILE,            EF_BOUNCE_HALF | EF_BOUNCE, SVF_BROADCAST,                CONTENTS_CORPSE, TR_GRAVITY,     MISSILE_PRESTEP_TIME,  { { -11.f, -11.f, 0.f }, { 11.f, 11.f, 8.f } },  { { -12.f, -12.f, 0.f }, { 12.f, 12.f, 20.f } }, MASK_MISSILESHOT, 0,         0,       5,     0,        20,          },
    { WP_SATCHEL_DET,          weapon_satcheldet_fire,      NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_STATIONARY,  0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_ALL,         0,         0,       0,     0,        0,           },
    { WP_SMOKE_BOMB,           weapon_grenadelauncher_fire, weapon_smokeBombExplode,    NULL,               ET_MISSILE,            EF_BOUNCE_HALF | EF_BOUNCE, SVF_BROADCAST,                CONTENTS_NONE,   TR_GRAVITY,     MISSILE_PRESTEP_TIME,  { { -4.f, -4.f, 0.f }, { 4.f, 4.f, 6.f } },      { { -4.f, -4.f, 0.f }, { 4.f, 4.f, 6.f } },      MASK_MISSILESHOT, 2500,      0,       0,     0,        20,          },
    //
	{ WP_MOBILE_MG42,          Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_K43,                  Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_FG42,                 Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_DUMMY_MG42,           NULL,                        NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_STATIONARY,  0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_ALL,         0,         0,       0,     0,        0,           },
    { WP_MORTAR,               NULL,                        NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_STATIONARY,  0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_ALL,         0,         0,       0,     0,        0,           },
    { WP_AKIMBO_COLT,          Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_AKIMBO_LUGER,         Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    //
	{ WP_GPG40,                weapon_gpg40_fire,           G_ExplodeMissile,           NULL,               ET_MISSILE,            EF_BOUNCE_HALF | EF_BOUNCE, SVF_BROADCAST,                CONTENTS_NONE,   TR_GRAVITY,     MISSILE_PRESTEP_TIME,  { { -4.f, -4.f, 0.f }, { 4.f, 4.f, 6.f } },      { { -4.f, -4.f, 0.f }, { 4.f, 4.f, 6.f } },      MASK_MISSILESHOT, 4000,      0,       0,     0,        20,          },
    { WP_M7,                   weapon_gpg40_fire,           G_ExplodeMissile,           NULL,               ET_MISSILE,            EF_BOUNCE_HALF | EF_BOUNCE, SVF_BROADCAST,                CONTENTS_NONE,   TR_GRAVITY,     MISSILE_PRESTEP_TIME,  { { -4.f, -4.f, 0.f }, { 4.f, 4.f, 6.f } },      { { -4.f, -4.f, 0.f }, { 4.f, 4.f, 6.f } },      MASK_MISSILESHOT, 4000,      0,       0,     0,        20,          },
    { WP_SILENCED_COLT,        Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    //
	{ WP_GARAND_SCOPE,         Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_K43_SCOPE,            Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_FG42_SCOPE,           Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_MORTAR_SET,           weapon_mortar_fire,          NULL,                       NULL,               ET_MISSILE,            EF_NONE,                    SVF_BROADCAST,                CONTENTS_NONE,   TR_GRAVITY,     MISSILE_PRESTEP_TIME,  { { -4.f, -4.f, 0.f }, { 4.f, 4.f, 6.f } },      { { -4.f, -4.f, 0.f }, { 4.f, 4.f, 6.f } },      MASK_MISSILESHOT, 0,         0,       0,     0,        999,         },
    { WP_MEDIC_ADRENALINE,     Weapon_AdrenalineSyringe,    NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_AKIMBO_SILENCEDCOLT,  Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_AKIMBO_SILENCEDLUGER, Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_MOBILE_MG42_SET,      Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    // league weapons
	{ WP_KNIFE_KABAR,          Weapon_Knife,                NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_MOBILE_BROWNING,      Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_MOBILE_BROWNING_SET,  Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_MORTAR2,              NULL,                        NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_STATIONARY,  0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_ALL,         0,         0,       0,     0,        0,           },
    { WP_MORTAR2_SET,          weapon_mortar_fire,          NULL,                       NULL,               ET_MISSILE,            EF_NONE,                    SVF_BROADCAST,                CONTENTS_NONE,   TR_GRAVITY,     MISSILE_PRESTEP_TIME,  { { -4.f, -4.f, 0.f }, { 4.f, 4.f, 6.f } },      { { -4.f, -4.f, 0.f }, { 4.f, 4.f, 6.f } },      MASK_MISSILESHOT, 0,         0,       0,     0,        999,         },
    { WP_BAZOOKA,              weapon_antitank_fire,        G_ExplodeMissile,           NULL,               ET_MISSILE,            EF_NONE,                    SVF_BROADCAST,                CONTENTS_NONE,   TR_LINEAR,      MISSILE_PRESTEP_TIME,  { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_MISSILESHOT, 20000,     4,       0,     0,        999,         },
    { WP_MP34,                 Bullet_Fire,                 NULL,                       NULL,               ET_GENERAL,            EF_NONE,                    SVF_NONE,                     CONTENTS_NONE,   TR_LINEAR,      0,                     { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_SHOT,        0,         0,       0,     0,        0,           },
    { WP_AIRSTRIKE,            NULL,                        NULL,                       NULL,               ET_MISSILE,            EF_NONE,                    SVF_BROADCAST,                CONTENTS_NONE,   TR_LINEAR,      MISSILE_PRESTEP_TIME,  { { 0, 0, 0 }, { 0, 0, 0 } },                    { { 0, 0, 0 }, { 0, 0, 0 } },                    MASK_MISSILESHOT, 0,         2,       0,     0,        20,          },
};
// *INDENT-ON*

/**
 * @brief FireWeapon
 * @param[in,out] ent
 */
void FireWeapon(gentity_t *ent)
{
	// dead guys don't fire guns
	if (ent->client->ps.pm_type == PM_DEAD)
	{
		return;
	}

	// stationary heavy weapon (e.g. misc_mg42, misc_aagun)
	if (ent->client->ps.persistant[PERS_HWEAPON_USE] && ent->active)
	{
		return;
	}

	// need to call this for AI prediction also
	CalcMuzzlePoints(ent, ent->s.weapon);

	// specific firing handle for artillery using binocular
	if ((ent->client->ps.eFlags & EF_ZOOMING) && (ent->client->ps.stats[STAT_KEYS] & (1 << INV_BINOCS)))
	{
		if (ent->client->sess.playerType == PC_FIELDOPS)
		{
			if (ent->client->ps.leanf == 0.f)
			{
				Weapon_Artillery(ent);
			}
			return;
		}
	}

	// covert ops disguise handling
	if (ent->client->ps.powerups[PW_OPS_DISGUISED])
	{
		if (!(GetWeaponTableData(ent->s.weapon)->attributes & WEAPON_ATTRIBUT_NEVER_LOST_DESGUISE))
		{
			if (!(GetWeaponTableData(ent->s.weapon)->attributes & WEAPON_ATTRIBUT_KEEP_DESGUISE) || G_PlayerCanBeSeenByOthers(ent))
			{
				ent->client->ps.powerups[PW_OPS_DISGUISED] = 0;
				ent->client->disguiseClientNum             = -1;
			}
		}
	}

	// fire the specific weapon
	if (weapFireTable[ent->s.weapon].fire)
	{
		gentity_t *pFiredShot = NULL;       // Omni-bot To tell bots about projectiles
#ifdef FEATURE_LUA
		if (!G_LuaHook_WeaponFire(ent->s.number, ent->s.weapon, &pFiredShot))
#endif
		{
			pFiredShot = weapFireTable[ent->s.weapon].fire(ent);
		}

#ifdef FEATURE_OMNIBOT
		// Omni-bot - Send a fire event.
		if (pFiredShot)
		{
			Bot_Event_FireWeapon(ent - g_entities, Bot_WeaponGameToBot(ent->s.weapon), pFiredShot);
		}
#endif
	}

#ifndef DEBUG_STATS
	if (g_gamestate.integer == GS_PLAYING)
#endif

	ent->client->sess.aWeaponStats[GetWeaponTableData(ent->s.weapon)->indexWeaponStat].atts++;
}
