#include "waypointsprites.qh"

#ifdef GAMEQC
REGISTER_MUTATOR(waypointsprites, true);
REGISTER_NET_LINKED(waypointsprites)
#endif

#ifdef SVQC
bool WaypointSprite_SendEntity(entity this, entity to, float sendflags)
{
    WriteHeader(MSG_ENTITY, waypointsprites);

    sendflags &= 0x7F;

    if (this.max_health || (this.pain_finished && (time < this.pain_finished + 0.25)))
        sendflags |= 0x80;

    int hide_flags = 0;
    if(this.currentammo == 1) hide_flags |= 1; // hideable
    else if(this.currentammo == 2) hide_flags |= 2; // radar only
    if(this.exteriormodeltoclient == to) hide_flags |= 2; // my own

    MUTATOR_CALLHOOK(SendWaypoint, this, to, sendflags, hide_flags);
    sendflags = M_ARGV(2, int);
    hide_flags = M_ARGV(3, int);

    WriteByte(MSG_ENTITY, sendflags);
    WriteByte(MSG_ENTITY, this.wp_extra);

    if (sendflags & 0x80)
    {
        if (this.max_health)
        {
            WriteByte(MSG_ENTITY, (GetResource(this, RES_HEALTH) / this.max_health) * 191.0);
        }
        else
        {
            float dt = this.pain_finished - time;
            dt = bound(0, dt * 32, 16383);
            WriteByte(MSG_ENTITY, (dt & 0xFF00) / 256 + 192);
            WriteByte(MSG_ENTITY, (dt & 0x00FF));
        }
    }

    if (sendflags & 64)
    {
        WriteVector(MSG_ENTITY, this.origin);
    }

    if (sendflags & 1)
    {
        WriteByte(MSG_ENTITY, this.team);
        WriteByte(MSG_ENTITY, this.rule);
    }

    if (sendflags & 2)
        WriteString(MSG_ENTITY, this.model1);

    if (sendflags & 4)
        WriteString(MSG_ENTITY, this.model2);

    if (sendflags & 8)
        WriteString(MSG_ENTITY, this.model3);

    if (sendflags & 16)
    {
        WriteCoord(MSG_ENTITY, this.fade_time);
        WriteCoord(MSG_ENTITY, this.teleport_time);
        WriteShort(MSG_ENTITY, bound(0, this.fade_rate, 32767)); // maxdist
        WriteByte(MSG_ENTITY, hide_flags);
    }

    if (sendflags & 32)
    {
        WriteByte(MSG_ENTITY, this.cnt); // icon on radar
        WriteByte(MSG_ENTITY, this.colormod.x * 255.0);
        WriteByte(MSG_ENTITY, this.colormod.y * 255.0);
        WriteByte(MSG_ENTITY, this.colormod.z * 255.0);

        if (WaypointSprite_isteammate(this.owner, WaypointSprite_getviewentity(to)))
        {
            float dt = bound(0, (this.waypointsprite_helpmetime - time) / 0.1, 255);
            WriteByte(MSG_ENTITY, dt);
        }
        else
            WriteByte(MSG_ENTITY, 0);
    }

    return true;
}
#endif

#ifdef CSQC
void Ent_WaypointSprite(entity this, bool isnew);
NET_HANDLE(waypointsprites, bool isnew) {
    Ent_WaypointSprite(this, isnew);
    return true;
}

void Ent_RemoveWaypointSprite(entity this)
{
    strfree(this.netname);
    strfree(this.netname2);
    strfree(this.netname3);
}

void Ent_WaypointSprite(entity this, bool isnew)
{
    int sendflags = ReadByte();
    this.wp_extra = ReadByte();

    if (!this.spawntime)
        this.spawntime = time;

    this.draw2d = Draw_WaypointSprite;
    if (isnew) {
		IL_PUSH(g_drawables_2d, this);
		IL_PUSH(g_radaricons, this);
    }

    InterpolateOrigin_Undo(this);
    this.iflags |= IFLAG_ORIGIN;

    if (sendflags & 0x80)
    {
        int t = ReadByte();
        if (t < 192)
        {
            SetResourceExplicit(this, RES_HEALTH, t / 191.0);
            this.build_finished = 0;
        }
        else
        {
            t = (t - 192) * 256 + ReadByte();
            this.build_started = servertime;
            if (this.build_finished)
                this.build_starthealth = bound(0, GetResource(this, RES_HEALTH), 1);
            else
                this.build_starthealth = 0;
            this.build_finished = servertime + t / 32;
        }
    }
    else
    {
        SetResourceExplicit(this, RES_HEALTH, -1);
        this.build_finished = 0;
    }

    if (sendflags & 64)
    {
        // unfortunately, this needs to be exact (for the 3D display)
        this.origin = ReadVector();
        setorigin(this, this.origin);
    }

    if (sendflags & 1)
    {
        this.team = ReadByte();
        this.rule = ReadByte();
    }

    if (sendflags & 2)
    {
        strcpy(this.netname, ReadString());
    }

    if (sendflags & 4)
    {
        strcpy(this.netname2, ReadString());
    }

    if (sendflags & 8)
    {
        strcpy(this.netname3, ReadString());
    }

    if (sendflags & 16)
    {
        this.lifetime = ReadCoord();
        this.fadetime = ReadCoord();
        this.maxdistance = ReadShort();
        this.hideflags = ReadByte();
    }

    if (sendflags & 32)
    {
        int f = ReadByte();
        this.teamradar_icon = f & BITS(7);
        if (f & BIT(7))
        {
            this.(teamradar_times[this.teamradar_time_index]) = time;
            this.teamradar_time_index = (this.teamradar_time_index + 1) % MAX_TEAMRADAR_TIMES;
        }
        this.teamradar_color_x = ReadByte() / 255.0;
        this.teamradar_color_y = ReadByte() / 255.0;
        this.teamradar_color_z = ReadByte() / 255.0;
        this.helpme = ReadByte() * 0.1;
        if (this.helpme > 0)
            this.helpme += servertime;
    }

    InterpolateOrigin_Note(this);

    this.entremove = Ent_RemoveWaypointSprite;
}
#endif

#ifdef CSQC
float spritelookupblinkvalue(entity this, string s)
{
    if (s == WP_Weapon.netname) {
        if (REGISTRY_GET(Weapons, this.wp_extra).spawnflags & WEP_FLAG_SUPERWEAPON)
            return 2;
    }
    if (s == WP_Item.netname) return REGISTRY_GET(Items, this.wp_extra).m_waypointblink;
    if(s == WP_FlagReturn.netname) return 2;

    return 1;
}

vector spritelookupcolor(entity this, string s, vector def)
{
    if (s == WP_Weapon.netname  || s == RADARICON_Weapon.netname) return REGISTRY_GET(Weapons, this.wp_extra).m_color;
    if (s == WP_Item.netname    || s == RADARICON_Item.netname) return REGISTRY_GET(Items, this.wp_extra).m_color;
    if (MUTATOR_CALLHOOK(WP_Format, this, s))
    {
        return M_ARGV(2, vector);
    }
    return def;
}

string spritelookuptext(entity this, string s)
{
	if(autocvar_g_waypointsprite_spam && waypointsprite_count >= autocvar_g_waypointsprite_spam)
		return "Spam"; // no need to translate this debug string
    if (s == WP_RaceStartFinish.netname) return (race_checkpointtime || race_mycheckpointtime) ? _("Finish") : _("Start");
    if (s == WP_Weapon.netname) return REGISTRY_GET(Weapons, this.wp_extra).m_name;
    if (s == WP_Item.netname) return REGISTRY_GET(Items, this.wp_extra).m_waypoint;
    if (s == WP_Monster.netname) return get_monsterinfo(this.wp_extra).m_name;
    if (MUTATOR_CALLHOOK(WP_Format, this, s))
    {
        return M_ARGV(3, string);
    }

    // need to loop, as our netname could be one of three
    FOREACH(Waypoints, it.netname == s, {
        return it.m_name;
    });

    return s;
}

string spritelookupicon(entity this, string s)
{
    // TODO: needs icons! //if (s == WP_RaceStartFinish.netname) return (race_checkpointtime || race_mycheckpointtime) ? _("Finish") : _("Start");
    if (s == WP_Weapon.netname) return REGISTRY_GET(Weapons, this.wp_extra).model2;
    if (s == WP_Item.netname) return REGISTRY_GET(Items, this.wp_extra).m_icon;
    if (s == WP_Vehicle.netname) return REGISTRY_GET(Vehicles, this.wp_extra).m_icon;
    //if (s == WP_Monster.netname) return get_monsterinfo(this.wp_extra).m_icon;
    if (MUTATOR_CALLHOOK(WP_Format, this, s))
    {
        return M_ARGV(4, string);
    }

    // need to loop, as our netname could be one of three
    FOREACH(Waypoints, it.netname == s, {
        return it.m_icon;
    });

    return s;
}
#endif

#ifdef CSQC
void drawrotpic(vector org, float rot, string pic, vector sz, vector hotspot, vector rgb, float a, float f)
{
    vector v1, v2, v3, v4;

    hotspot = -1 * hotspot;

    // hotspot-relative coordinates of the corners
    v1 = hotspot;
    v2 = hotspot + '1 0 0' * sz.x;
    v3 = hotspot + '1 0 0' * sz.x + '0 1 0' * sz.y;
    v4 = hotspot                  + '0 1 0' * sz.y;

    // rotate them, and make them absolute
    rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
    v1 = Rotate(v1, rot) + org;
    v2 = Rotate(v2, rot) + org;
    v3 = Rotate(v3, rot) + org;
    v4 = Rotate(v4, rot) + org;

    // draw them
    R_BeginPolygon(pic, f, true);
    R_PolygonVertex(v1, '0 0 0', rgb, a);
    R_PolygonVertex(v2, '1 0 0', rgb, a);
    R_PolygonVertex(v3, '1 1 0', rgb, a);
    R_PolygonVertex(v4, '0 1 0', rgb, a);
    R_EndPolygon();
}

void drawquad(vector o, vector ri, vector up, string pic, vector rgb, float a, float f)
{
    R_BeginPolygon(pic, f, true);
    R_PolygonVertex(o, '0 0 0', rgb, a);
    R_PolygonVertex(o + ri, '1 0 0', rgb, a);
    R_PolygonVertex(o + up + ri, '1 1 0', rgb, a);
    R_PolygonVertex(o + up, '0 1 0', rgb, a);
    R_EndPolygon();
}

void drawhealthbar(vector org, float rot, float h, vector sz, vector hotspot, float width, float theheight, float margin, float border, float align, vector rgb, float a, vector hrgb, float ha, float f)
{
    vector o, ri, up;
    float owidth; // outer width

    hotspot = -1 * hotspot;

    // hotspot-relative coordinates of the healthbar corners
    o = hotspot;
    ri = '1 0 0';
    up = '0 1 0';

    rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
    o = Rotate(o, rot) + org;
    ri = Rotate(ri, rot);
    up = Rotate(up, rot);

    owidth = width + 2 * border;
    o += up * -(margin + border + theheight) + ri * (sz.x - owidth) * 0.5;

    drawquad(o - up * border,                               ri * owidth,    up * border,    "", rgb,  a,  f);
    drawquad(o + up * theheight,                            ri * owidth,    up * border,    "", rgb,  a,  f);
    drawquad(o,                                             ri * border,    up * theheight, "", rgb,  a,  f);
    drawquad(o + ri * (owidth - border),                    ri * border,    up * theheight, "", rgb,  a,  f);
    drawquad(o + ri * (border + align * ((1 - h) * width)), ri * width * h, up * theheight, "", hrgb, ha, f);
}

// returns location of sprite text
vector drawspritearrow(vector o, float ang, vector rgb, float a, float t)
{
    float size   = 9.0 * t;
    float border = 1.5 * t;
    float margin = 4.0 * t;

    float borderDiag = border * M_SQRT2;
    vector arrowX  = eX * size;
    vector arrowY  = eY * (size+borderDiag);
    vector borderX = eX * (size+borderDiag);
    vector borderY = eY * (size+borderDiag+border);

    R_BeginPolygon("", DRAWFLAG_NORMAL, true);
    R_PolygonVertex(o,                                  '0 0 0', '0 0 0', a);
    R_PolygonVertex(o + Rotate(arrowY  - borderX, ang), '0 0 0', '0 0 0', a);
    R_PolygonVertex(o + Rotate(borderY - borderX, ang), '0 0 0', '0 0 0', a);
    R_PolygonVertex(o + Rotate(borderY + borderX, ang), '0 0 0', '0 0 0', a);
    R_PolygonVertex(o + Rotate(arrowY  + borderX, ang), '0 0 0', '0 0 0', a);
    R_EndPolygon();

    R_BeginPolygon("", DRAWFLAG_ADDITIVE, true);
    R_PolygonVertex(o + Rotate(eY * borderDiag, ang), '0 0 0', rgb, a);
    R_PolygonVertex(o + Rotate(arrowY - arrowX, ang), '0 0 0', rgb, a);
    R_PolygonVertex(o + Rotate(arrowY + arrowX, ang), '0 0 0', rgb, a);
    R_EndPolygon();

    return o + Rotate(eY * (borderDiag+size+margin), ang);
}

// returns location of sprite healthbar
vector drawsprite_TextOrIcon(bool is_text, vector o, float ang, float minwidth, vector rgb, float a, vector sz, string str)
{
    float algnx, algny;
    float sw, w, h;
    float aspect, sa, ca;

    if (is_text)
        sw = stringwidth(str, false, sz);
    else
        sw = sz.x;

    if (sw > minwidth)
        w = sw;
    else
        w = minwidth;
    h = sz.y;

    // how do corners work?
    aspect = vid_conwidth / vid_conheight;
    sa = sin(ang);
    ca = cos(ang) * aspect;
    if (fabs(sa) > fabs(ca))
    {
        algnx = (sa < 0);
        float f = fabs(sa);
        algny = 0.5 - 0.5 * (f ? (ca / f) : 0);
    }
    else
    {
        float f = fabs(ca);
        algnx = 0.5 - 0.5 * (f ? (sa / f) : 0);
        algny = (ca < 0);
    }

    // align
    o.x -= w * algnx;
    o.y -= h * algny;

    // we want to be onscreen
    if (o.x < 0)
        o.x = 0;
    if (o.y < 0)
        o.y = 0;
    if (o.x > vid_conwidth - w)
        o.x = vid_conwidth - w;
    if (o.y > vid_conheight - h)
        o.y = vid_conheight - h;

    o.x += 0.5 * (w - sw);

    if (is_text)
        drawstring(o, str, sz, rgb, a, DRAWFLAG_NORMAL);
    else
        drawpic(o, str, sz, rgb, a, DRAWFLAG_NORMAL);

    o.x += 0.5 * sw;
    o.y += 0.5 * h;

    return o;
}

vector fixrgbexcess_move(vector rgb, vector src, vector dst)
{
    vector yvec = '0.299 0.587 0.114';
    return rgb + dst * ((src * yvec) / (dst * yvec)) * ((rgb - '1 1 1') * src);
}

vector fixrgbexcess(vector rgb)
{
    if (rgb.x > 1) {
        rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 1');
        if (rgb.y > 1) {
            rgb = fixrgbexcess_move(rgb, '0 1 0', '0 0 1');
            if (rgb.z > 1) rgb.z = 1;
        } else if (rgb.z > 1) {
            rgb = fixrgbexcess_move(rgb, '0 0 1', '0 1 0');
            if (rgb.y > 1) rgb.y = 1;
        }
    } else if (rgb.y > 1) {
        rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 1');
        if (rgb.x > 1) {
            rgb = fixrgbexcess_move(rgb, '1 0 0', '0 0 1');
            if (rgb.z > 1) rgb.z = 1;
        } else if (rgb.z > 1) {
            rgb = fixrgbexcess_move(rgb, '0 0 1', '1 0 0');
            if (rgb.x > 1) rgb.x = 1;
        }
    } else if (rgb.z > 1) {
        rgb = fixrgbexcess_move(rgb, '0 0 1', '1 1 0');
        if (rgb.x > 1) {
            rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 0');
            if (rgb.y > 1) rgb.y = 1;
        } else if (rgb.y > 1) {
            rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 0');
            if (rgb.x > 1) rgb.x = 1;
        }
    }
    return rgb;
}

void Draw_WaypointSprite(entity this)
{
    if (this.lifetime > 0)
        this.alpha = (bound(0, (this.fadetime - time) / this.lifetime, 1) ** waypointsprite_timealphaexponent);
    else
        this.alpha = 1;

    if (this.hideflags & 2)
        return; // radar only

    if (autocvar_cl_hidewaypoints >= 2)
        return;

    if ((this.hideflags & 1) && autocvar_cl_hidewaypoints)
        return; // fixed waypoint

    InterpolateOrigin_Do(this);

    float t = entcs_GetTeam(player_localnum) + 1;
    string spriteimage = "";

    // choose the sprite
    switch (this.rule)
    {
        case SPRITERULE_SPECTATOR:
            if (!(
                (autocvar_g_waypointsprite_itemstime == 1 && t == NUM_SPECTATOR + 1)
            ||	(autocvar_g_waypointsprite_itemstime == 2 && (t == NUM_SPECTATOR + 1 || warmup_stage || STAT(ITEMSTIME) == 2))
                ))
                return;
            spriteimage = this.netname;
            break;
        case SPRITERULE_DEFAULT:
            if (this.team)
            {
                if (this.team == t)
                    spriteimage = this.netname;
                else
                    spriteimage = "";
            }
            else
                spriteimage = this.netname;
            break;
        case SPRITERULE_TEAMPLAY:
            if (t == NUM_SPECTATOR + 1)
                spriteimage = this.netname3;
            else if (this.team == t)
                spriteimage = this.netname2;
            else
                spriteimage = this.netname;
            break;
        default:
            error("Invalid waypointsprite rule!");
            break;
    }

    if (spriteimage == "")
        return;

    ++waypointsprite_newcount;

    float dist = vlen(this.origin - view_origin);
    float a = this.alpha * autocvar_hud_panel_fg_alpha;

    if(this.maxdistance > 0)
    {
        // restrict maximum normal distance to the waypoint's maximum distance to prevent exploiting cvars
        float maxnormdistance = bound(0, waypointsprite_normdistance, this.maxdistance - 1);
        a *= (bound(0, (this.maxdistance - dist) / (this.maxdistance - maxnormdistance), 1) ** waypointsprite_distancealphaexponent);
    }

    vector rgb = spritelookupcolor(this, spriteimage, this.teamradar_color);
    if (rgb == '0 0 0')
    {
        this.teamradar_color = '1 0 1';
        LOG_INFOF("WARNING: sprite of name %s has no color, using pink so you notice it", spriteimage);
    }

    float health_val = GetResource(this, RES_HEALTH);
    float blink_time = (health_val >= 0) ? (health_val * 10) : time;
    if (blink_time - floor(blink_time) > 0.5)
    {
        if (this.helpme && time < this.helpme)
            a *= SPRITE_HELPME_BLINK;
        else if (!this.lifetime) // fading out waypoints don't blink
            a *= spritelookupblinkvalue(this, spriteimage);
    }

    if (a > 1)
    {
        rgb *= a;
        a = 1;
    }

    if (a <= 0.003)
        return;

    rgb = fixrgbexcess(rgb);

    vector o;
    float ang;

    o = project_3d_to_2d(this.origin);
    if (o.z < 0
    || o.x < (vid_conwidth * waypointsprite_edgeoffset_left)
    || o.y < (vid_conheight * waypointsprite_edgeoffset_top)
    || o.x > (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right))
    || o.y > (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)))
    {
        // scale it to be just in view
        vector d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
        ang = atan2(-d.x, -d.y);
        if (o.z < 0)
            ang += M_PI;

		float f1 = d.x / vid_conwidth;
		float f2 = d.y / vid_conheight;
		if (f1 == 0) { f1 = 0.000001; }
		if (f2 == 0) { f2 = 0.000001; }

        if (max(f1, -f1) > max(f2, -f2)) {
            if (d.z * f1 > 0) {
                // RIGHT edge
                d *= (0.5 - waypointsprite_edgeoffset_right) / f1;
            } else {
                // LEFT edge
                d *= -(0.5 - waypointsprite_edgeoffset_left) / f1;
            }
        } else {
            if (d.z * f2 > 0) {
                // BOTTOM edge
                d *= (0.5 - waypointsprite_edgeoffset_bottom) / f2;
            } else {
                // TOP edge
                d *= -(0.5 - waypointsprite_edgeoffset_top) / f2;
            }
        }

        o = d + '0.5 0 0' * vid_conwidth + '0 0.5 0' * vid_conheight;
    }
    else
    {
#if 1
        ang = M_PI;
#else
        vector d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
        ang = atan2(-d.x, -d.y);
#endif
    }
    o.z = 0;

    float edgedistance_min = min((o.y - (vid_conheight * waypointsprite_edgeoffset_top)),
    (o.x - (vid_conwidth * waypointsprite_edgeoffset_left)),
    (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right)) - o.x,
    (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)) - o.y);

    float crosshairdistance = sqrt( ((o.x - vid_conwidth/2) ** 2) + ((o.y - vid_conheight/2) ** 2) );

    t = waypointsprite_scale;
    a *= waypointsprite_alpha;

    {
        a *= 1 - (1 - waypointsprite_distancefadealpha) * (bound(0, dist/waypointsprite_distancefadedistance, 1));
        t *= 1 - (1 - waypointsprite_distancefadescale) * (bound(0, dist/waypointsprite_distancefadedistance, 1));
    }
    if (edgedistance_min < waypointsprite_edgefadedistance) {
        a *= 1 - (1 - waypointsprite_edgefadealpha) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1));
        t *= 1 - (1 - waypointsprite_edgefadescale) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1));
    }
    if (crosshairdistance < waypointsprite_crosshairfadedistance) {
        a *= 1 - (1 - waypointsprite_crosshairfadealpha) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1));
        t *= 1 - (1 - waypointsprite_crosshairfadescale) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1));
    }

    if (this.build_finished)
    {
        if (time < this.build_finished + 0.25)
        {
            if (time < this.build_started)
                SetResourceExplicit(this, RES_HEALTH, this.build_starthealth);
            else if (time < this.build_finished)
                SetResourceExplicit(this, RES_HEALTH, (time - this.build_started) / (this.build_finished - this.build_started) * (1 - this.build_starthealth) + this.build_starthealth);
            else
                SetResourceExplicit(this, RES_HEALTH, 1);
        }
        else
            SetResourceExplicit(this, RES_HEALTH, -1);
    }

    o = drawspritearrow(o, ang, rgb, a, SPRITE_ARROW_SCALE * t);

	string pic = "";
	bool is_text = true;
	if (!autocvar_g_waypointsprite_text)
	{
		string spr_icon = spritelookupicon(this, spriteimage);
		pic = spr_icon;
		bool icon_found = !(!spr_icon || spr_icon == "");
		if (icon_found) // it's valid, but let's make sure it exists!
		{
			pic = strcat(hud_skin_path, "/", spr_icon);
			if(precache_pic(pic) == "")
			{
				pic = strcat("gfx/hud/default/", spr_icon);
				if(!precache_pic(pic))
					icon_found = false;
			}
		}
		if (icon_found)
			is_text = false;
	}

	vector sz;
	vector col = rgb;
    string txt = string_null; // it will contain either the text or the icon path
    if (is_text)
    {
        txt = spritelookuptext(this, spriteimage);
        if (this.helpme && time < this.helpme)
            txt = sprintf(_("%s needing help!"), txt);
        if (autocvar_g_waypointsprite_uppercase)
            txt = strtoupper(txt);
        sz = waypointsprite_fontsize * '1 1 0';
    }
    else
    {
        txt = pic; // icon path
        if (autocvar_g_waypointsprite_iconcolor == 0)
        	col = '1 1 1';
        else if (autocvar_g_waypointsprite_iconcolor > 0 && autocvar_g_waypointsprite_iconcolor != 1)
        {
            col = rgb_to_hsv(col);
            col.y *= autocvar_g_waypointsprite_iconcolor; // scale saturation
            col = hsv_to_rgb(col);
        }
        sz = autocvar_g_waypointsprite_iconsize * '1 1 0';
    }

    draw_beginBoldFont();
    if (GetResource(this, RES_HEALTH) >= 0)
    {
        float align = 0, marg;
        if (this.build_finished)
            align = 0.5;
        else
            align = 0;
        if (cos(ang) > 0)
            marg = -(SPRITE_HEALTHBAR_MARGIN + SPRITE_HEALTHBAR_HEIGHT + 2 * SPRITE_HEALTHBAR_BORDER) * t - 0.5 * sz.y;
        else
            marg = SPRITE_HEALTHBAR_MARGIN * t + 0.5 * sz.y;

        float minwidth = (SPRITE_HEALTHBAR_WIDTH + 2 * SPRITE_HEALTHBAR_BORDER) * t;
        o = drawsprite_TextOrIcon(is_text, o, ang, minwidth, col, a, sz, txt);
        drawhealthbar(
                o,
                0,
                GetResource(this, RES_HEALTH),
                '0 0 0',
                '0 0 0',
                SPRITE_HEALTHBAR_WIDTH * t,
                SPRITE_HEALTHBAR_HEIGHT * t,
                marg,
                SPRITE_HEALTHBAR_BORDER * t,
                align,
                rgb,
                a * SPRITE_HEALTHBAR_BORDERALPHA,
                rgb,
                a * SPRITE_HEALTHBAR_HEALTHALPHA,
                DRAWFLAG_NORMAL
                 );
    }
    else
    {
        drawsprite_TextOrIcon(is_text, o, ang, 0, col, a, sz, txt);
    }

    draw_endBoldFont();
}

void WaypointSprite_Load_Frames(string ext)
{
    int dh = search_begin(strcat("models/sprites/*_frame*", ext), false, false);
    if (dh < 0) return;
    int ext_len = strlen(ext);
    int n = search_getsize(dh);
    for (int i = 0; i < n; ++i)
    {
        string s = search_getfilename(dh, i);
        s = substring(s, 15, strlen(s) - 15 - ext_len); // strip models/sprites/ and extension

        int o = strstrofs(s, "_frame", 0);
        string sname = strcat("/spriteframes/", substring(s, 0, o));
        string sframes = substring(s, o + 6, strlen(s) - o - 6);
        int f = stof(sframes) + 1;
        db_put(tempdb, sname, ftos(max(f, stof(db_get(tempdb, sname)))));
    }
    search_end(dh);
}

void WaypointSprite_Load();
STATIC_INIT(WaypointSprite_Load) {
    WaypointSprite_Load();
    WaypointSprite_Load_Frames(".tga");
    WaypointSprite_Load_Frames(".jpg");
}
void WaypointSprite_Load()
{
    waypointsprite_fadedistance = vlen(mi_scale);
    waypointsprite_normdistance = autocvar_g_waypointsprite_normdistance;
    waypointsprite_minscale = autocvar_g_waypointsprite_minscale;
    waypointsprite_minalpha = autocvar_g_waypointsprite_minalpha;
    waypointsprite_distancealphaexponent = autocvar_g_waypointsprite_distancealphaexponent;
    waypointsprite_timealphaexponent = autocvar_g_waypointsprite_timealphaexponent;
    waypointsprite_scale = autocvar_g_waypointsprite_scale;
    waypointsprite_fontsize = autocvar_g_waypointsprite_fontsize;
    waypointsprite_edgefadealpha = autocvar_g_waypointsprite_edgefadealpha;
    waypointsprite_edgefadescale = autocvar_g_waypointsprite_edgefadescale;
    waypointsprite_edgefadedistance = autocvar_g_waypointsprite_edgefadedistance;
    waypointsprite_edgeoffset_bottom = autocvar_g_waypointsprite_edgeoffset_bottom;
    waypointsprite_edgeoffset_left = autocvar_g_waypointsprite_edgeoffset_left;
    waypointsprite_edgeoffset_right = autocvar_g_waypointsprite_edgeoffset_right;
    waypointsprite_edgeoffset_top = autocvar_g_waypointsprite_edgeoffset_top;
    waypointsprite_crosshairfadealpha = autocvar_g_waypointsprite_crosshairfadealpha;
    waypointsprite_crosshairfadescale = autocvar_g_waypointsprite_crosshairfadescale;
    waypointsprite_crosshairfadedistance = autocvar_g_waypointsprite_crosshairfadedistance;
    waypointsprite_distancefadealpha = autocvar_g_waypointsprite_distancefadealpha;
    waypointsprite_distancefadescale = autocvar_g_waypointsprite_distancefadescale;
    waypointsprite_distancefadedistance = waypointsprite_fadedistance * autocvar_g_waypointsprite_distancefadedistancemultiplier;
    waypointsprite_alpha = autocvar_g_waypointsprite_alpha * (1 - autocvar__menu_alpha);

    waypointsprite_count = waypointsprite_newcount;
    waypointsprite_newcount = 0;
}
#endif

#ifdef SVQC
void WaypointSprite_UpdateSprites(entity e, entity _m1, entity _m2, entity _m3)
{
    string m1 = _m1.netname;
    string m2 = _m2.netname;
    string m3 = _m3.netname;
    if (m1 != e.model1)
    {
        e.model1 = m1;
        e.SendFlags |= 2;
    }
    if (m2 != e.model2)
    {
        e.model2 = m2;
        e.SendFlags |= 4;
    }
    if (m3 != e.model3)
    {
        e.model3 = m3;
        e.SendFlags |= 8;
    }
}

void WaypointSprite_UpdateHealth(entity e, float f)
{
    f = bound(0, f, e.max_health);
    float step = e.max_health / 40;
    if ((floor(f / step) != floor(GetResource(e, RES_HEALTH) / step)) || e.pain_finished)
    {
        SetResourceExplicit(e, RES_HEALTH, f);
        e.pain_finished = 0;
        e.SendFlags |= 0x80;
    }
}

void WaypointSprite_UpdateMaxHealth(entity e, float f)
{
    if (f != e.max_health || e.pain_finished)
    {
        e.max_health = f;
        e.pain_finished = 0;
        e.SendFlags |= 0x80;
    }
}

void WaypointSprite_UpdateBuildFinished(entity e, float f)
{
    if (f != e.pain_finished || e.max_health)
    {
        e.max_health = 0;
        e.pain_finished = f;
        e.SendFlags |= 0x80;
    }
}

void WaypointSprite_UpdateOrigin(entity e, vector o)
{
    if (o != e.origin)
    {
        setorigin(e, o);
        e.SendFlags |= 64;
    }
}

void WaypointSprite_UpdateRule(entity e, float t, float r)
{
    // no check, as this is never called without doing an actual change (usually only once)
    e.rule = r;
    e.team = t;
    e.SendFlags |= 1;
}

void WaypointSprite_UpdateTeamRadar(entity e, entity icon, vector col)
{
    // no check, as this is never called without doing an actual change (usually only once)
    int i = icon.m_id;
    int new_cnt = (e.cnt & BIT(7)) | (i & BITS(7));
    if (new_cnt != e.cnt || col != e.colormod)
    {
    	e.cnt = new_cnt;
    	e.colormod = col;
    	e.SendFlags |= 32;
    }
}

void WaypointSprite_Ping(entity e)
{
    // anti spam
    if (time < e.waypointsprite_pingtime) return;
    e.waypointsprite_pingtime = time + 0.3;
    // ALWAYS sends (this causes a radar circle), thus no check
    e.cnt |= BIT(7);
    e.SendFlags |= 32;
}

void WaypointSprite_HelpMePing(entity e)
{
    WaypointSprite_Ping(e);
    e.waypointsprite_helpmetime = time + waypointsprite_deployed_lifetime;
    e.SendFlags |= 32;
}

void WaypointSprite_FadeOutIn(entity e, float t)
{
    if (!e.fade_time)
    {
        e.fade_time = t;
        e.teleport_time = time + t;
    }
    else if (t < (e.teleport_time - time))
    {
        // accelerate the waypoint's dying
        // ensure:
        //   (e.teleport_time - time) / wp.fade_time stays
        //   e.teleport_time = time + fadetime
        float current_fadetime = e.teleport_time - time;
        e.teleport_time = time + t;
        if (e.fade_time < 0)
        	e.fade_time = -e.fade_time;
        e.fade_time *= t / current_fadetime;
    }

    e.SendFlags |= 16;
}

void WaypointSprite_Init()
{
    waypointsprite_limitedrange = autocvar_sv_waypointsprite_limitedrange;
    waypointsprite_deployed_lifetime = autocvar_sv_waypointsprite_deployed_lifetime;
    waypointsprite_deadlifetime = autocvar_sv_waypointsprite_deadlifetime;
}

void WaypointSprite_Kill(entity wp)
{
    if (!wp) return;
    if (wp.owner) wp.owner.(wp.owned_by_field) = NULL;
    delete(wp);
}

void WaypointSprite_Disown(entity wp, float fadetime)
{
    if (!wp) return;
    if (wp.classname != "sprite_waypoint")
    {
        backtrace("Trying to disown a non-waypointsprite");
        return;
    }
    if (wp.owner)
    {
        if (wp.exteriormodeltoclient == wp.owner)
            wp.exteriormodeltoclient = NULL;
        wp.owner.(wp.owned_by_field) = NULL;
        wp.owner = NULL;

        WaypointSprite_FadeOutIn(wp, fadetime);
    }
}

void WaypointSprite_Think(entity this)
{
    bool doremove = false;

    if (this.fade_time && time >= this.teleport_time)
    {
        doremove = true;
    }

    if (this.exteriormodeltoclient)
        WaypointSprite_UpdateOrigin(this, this.exteriormodeltoclient.origin + this.view_ofs);

    if (doremove)
        WaypointSprite_Kill(this);
    else
        this.nextthink = time; // WHY?!?
}

bool WaypointSprite_visible_for_player(entity this, entity player, entity view)
{
    // personal waypoints
    if (this.enemy && this.enemy != view)
        return false;

    // team waypoints
    if (this.rule == SPRITERULE_SPECTATOR)
    {
        if (!autocvar_sv_itemstime)
            return false;
        if (!warmup_stage && IS_PLAYER(view) && autocvar_sv_itemstime != 2)
            return false;
    }
    else if (this.team && this.rule == SPRITERULE_DEFAULT)
    {
        if (this.team != view.team)
            return false;
        if (!IS_PLAYER(view))
            return false;
    }

    return true;
}

entity WaypointSprite_getviewentity(entity e)
{
    if (IS_SPEC(e)) e = e.enemy;
    /* TODO idea (check this breaks nothing)
    else if (e.classname == "observer")
        e = NULL;
    */
    return e;
}

float WaypointSprite_isteammate(entity e, entity e2)
{
    if (teamplay)
        return e2.team == e.team;
    return e2 == e;
}

bool WaypointSprite_Customize(entity this, entity client)
{
    // this is not in SendEntity because it shall run every frame, not just every update

    // make spectators see what the player would see
    entity e = WaypointSprite_getviewentity(client);

    if (MUTATOR_CALLHOOK(CustomizeWaypoint, this, client))
        return false;

    return this.waypointsprite_visible_for_player(this, client, e);
}

bool WaypointSprite_SendEntity(entity this, entity to, float sendflags);

void WaypointSprite_Reset(entity this)
{
    // if a WP wants to time out, let it time out immediately; other WPs ought to be reset/killed by their owners

    if (this.fade_time)
        WaypointSprite_Kill(this);
}

entity WaypointSprite_Spawn(
    entity spr, // sprite
    float _lifetime, float maxdistance, // lifetime, max distance
    entity ref, vector ofs, // position
    entity showto, float t, // show to whom? Use a flag to indicate a team
    entity own, .entity ownfield, // remove when own gets killed
    float hideable, // true when it should be controlled by cl_hidewaypoints
    entity icon // initial icon
)
{
    entity wp = new(sprite_waypoint);
    wp.fade_time = _lifetime; // if negative tells client not to fade it out
    if(_lifetime < 0)
    	_lifetime = -_lifetime;
    wp.teleport_time = time + _lifetime;
    wp.exteriormodeltoclient = ref;
    if (ref)
    {
        wp.view_ofs = ofs;
        setorigin(wp, ref.origin + ofs);
    }
    else
        setorigin(wp, ofs);
    wp.enemy = showto;
    wp.team = t;
    wp.owner = own;
    wp.currentammo = hideable;
    if (own)
    {
        if (own.(ownfield))
            delete(own.(ownfield));
        own.(ownfield) = wp;
        wp.owned_by_field = ownfield;
    }
    wp.fade_rate = maxdistance;
    setthink(wp, WaypointSprite_Think);
    wp.nextthink = time;
    wp.model1 = spr.netname;
    setcefc(wp, WaypointSprite_Customize);
    wp.waypointsprite_visible_for_player = WaypointSprite_visible_for_player;
    wp.reset2 = WaypointSprite_Reset;
    wp.cnt = icon.m_id;
    wp.colormod = spr.m_color;
    Net_LinkEntity(wp, false, 0, WaypointSprite_SendEntity);
    return wp;
}

entity WaypointSprite_SpawnFixed(
    entity spr,
    vector ofs,
    entity own,
    .entity ownfield,
    entity icon // initial icon
)
{
    return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, own, ownfield, true, icon);
}

entity WaypointSprite_DeployFixed(
    entity spr,
    bool limited_range,
    entity player,
    vector ofs,
    entity icon // initial icon
)
{
    float t;
    if (teamplay)
        t = player.team;
    else
        t = 0;
    float maxdistance;
    if (limited_range)
        maxdistance = waypointsprite_limitedrange;
    else
        maxdistance = 0;
    return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, NULL, ofs, NULL, t, player, waypointsprite_deployed_fixed, false, icon);
}

entity WaypointSprite_DeployPersonal(
    entity spr,
    entity player,
    vector ofs,
    entity icon // initial icon
)
{
    return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, player, waypointsprite_deployed_personal, false, icon);
}

entity WaypointSprite_Attach(
    entity spr,
    entity player,
    bool limited_range,
    entity icon // initial icon
)
{
    float t;
    if (player.waypointsprite_attachedforcarrier)
        return NULL; // can't attach to FC
    if (teamplay)
        t = player.team;
    else
        t = 0;
    float maxdistance;
    if (limited_range)
        maxdistance = waypointsprite_limitedrange;
    else
        maxdistance = 0;
    return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, player, '0 0 64', NULL, t, player, waypointsprite_attached, false, icon);
}

entity WaypointSprite_AttachCarrier(
    entity spr,
    entity carrier,
    entity icon // initial icon and color
)
{
    WaypointSprite_Kill(carrier.waypointsprite_attached); // FC overrides attached
    entity e = WaypointSprite_Spawn(spr, 0, 0, carrier, '0 0 64', NULL, carrier.team, carrier, waypointsprite_attachedforcarrier, false, icon);
    if (GetResource(carrier, RES_HEALTH))
    {
        WaypointSprite_UpdateMaxHealth(e, 2 * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
        WaypointSprite_UpdateHealth(e, healtharmor_maxdamage(GetResource(carrier, RES_HEALTH), GetResource(carrier, RES_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
    }
    return e;
}

void WaypointSprite_DetachCarrier(entity carrier)
{
    WaypointSprite_Disown(carrier.waypointsprite_attachedforcarrier, waypointsprite_deadlifetime);
}

void WaypointSprite_ClearPersonal(entity this)
{
    WaypointSprite_Kill(this.waypointsprite_deployed_personal);
}

void WaypointSprite_ClearOwned(entity this)
{
    WaypointSprite_Kill(this.waypointsprite_deployed_fixed);
    WaypointSprite_Kill(this.waypointsprite_deployed_personal);
    WaypointSprite_Kill(this.waypointsprite_attached);
}

void WaypointSprite_PlayerDead(entity this)
{
    WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
    WaypointSprite_DetachCarrier(this);
}

void WaypointSprite_PlayerGone(entity this)
{
    WaypointSprite_Disown(this.waypointsprite_deployed_fixed, waypointsprite_deadlifetime);
    WaypointSprite_Kill(this.waypointsprite_deployed_personal);
    WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
    WaypointSprite_DetachCarrier(this);
}
#endif
