]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/mutators/mutator/waypoints/waypointsprites.qc
Restrict g_waypointsprite_normdistance to the maximum viewable distance of the waypoint
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / mutators / mutator / waypoints / waypointsprites.qc
1 #include "waypointsprites.qh"
2
3 REGISTER_MUTATOR(waypointsprites, true);
4
5 REGISTER_NET_LINKED(waypointsprites)
6
7 #ifdef SVQC
8 /** flags origin [team displayrule] [spritename] [spritename2] [spritename3] [lifetime maxdistance hideable] */
9 bool WaypointSprite_SendEntity(entity this, entity to, float sendflags)
10 {
11     WriteHeader(MSG_ENTITY, waypointsprites);
12
13     sendflags = sendflags & 0x7F;
14
15     if (this.max_health || (this.pain_finished && (time < this.pain_finished + 0.25)))
16         sendflags |= 0x80;
17
18     int f = 0;
19     if(this.currentammo == 1)
20         f |= 1; // hideable
21     if(this.exteriormodeltoclient == to)
22         f |= 2; // my own
23     if(this.currentammo == 2)
24         f |= 2; // radar only
25
26     MUTATOR_CALLHOOK(SendWaypoint, this, to, sendflags, f);
27     sendflags = M_ARGV(2, int);
28     f = M_ARGV(3, int);
29
30     WriteByte(MSG_ENTITY, sendflags);
31     WriteByte(MSG_ENTITY, this.wp_extra);
32
33     if (sendflags & 0x80)
34     {
35         if (this.max_health)
36         {
37             WriteByte(MSG_ENTITY, (GetResource(this, RES_HEALTH) / this.max_health) * 191.0);
38         }
39         else
40         {
41             float dt = this.pain_finished - time;
42             dt = bound(0, dt * 32, 16383);
43             WriteByte(MSG_ENTITY, (dt & 0xFF00) / 256 + 192);
44             WriteByte(MSG_ENTITY, (dt & 0x00FF));
45         }
46     }
47
48     if (sendflags & 64)
49     {
50         WriteVector(MSG_ENTITY, this.origin);
51     }
52
53     if (sendflags & 1)
54     {
55         WriteByte(MSG_ENTITY, this.team);
56         WriteByte(MSG_ENTITY, this.rule);
57     }
58
59     if (sendflags & 2)
60         WriteString(MSG_ENTITY, this.model1);
61
62     if (sendflags & 4)
63         WriteString(MSG_ENTITY, this.model2);
64
65     if (sendflags & 8)
66         WriteString(MSG_ENTITY, this.model3);
67
68     if (sendflags & 16)
69     {
70         WriteCoord(MSG_ENTITY, this.fade_time);
71         WriteCoord(MSG_ENTITY, this.teleport_time);
72         WriteShort(MSG_ENTITY, this.fade_rate); // maxdist
73         WriteByte(MSG_ENTITY, f);
74     }
75
76     if (sendflags & 32)
77     {
78         WriteByte(MSG_ENTITY, this.cnt); // icon on radar
79         WriteByte(MSG_ENTITY, this.colormod.x * 255.0);
80         WriteByte(MSG_ENTITY, this.colormod.y * 255.0);
81         WriteByte(MSG_ENTITY, this.colormod.z * 255.0);
82
83         if (WaypointSprite_isteammate(this.owner, WaypointSprite_getviewentity(to)))
84         {
85             float dt = bound(0, (this.waypointsprite_helpmetime - time) / 0.1, 255);
86             WriteByte(MSG_ENTITY, dt);
87         }
88         else
89             WriteByte(MSG_ENTITY, 0);
90     }
91
92     return true;
93 }
94 #endif
95
96 #ifdef CSQC
97 void Ent_WaypointSprite(entity this, bool isnew);
98 NET_HANDLE(waypointsprites, bool isnew) {
99     Ent_WaypointSprite(this, isnew);
100     return true;
101 }
102
103 void Ent_RemoveWaypointSprite(entity this)
104 {
105     strfree(this.netname);
106     strfree(this.netname2);
107     strfree(this.netname3);
108 }
109
110 /** flags origin [team displayrule] [spritename] [spritename2] [spritename3] [lifetime maxdistance hideable] */
111 void Ent_WaypointSprite(entity this, bool isnew)
112 {
113     int sendflags = ReadByte();
114     this.wp_extra = ReadByte();
115
116     if (!this.spawntime)
117         this.spawntime = time;
118
119     this.draw2d = Draw_WaypointSprite;
120     if (isnew) {
121                 IL_PUSH(g_drawables_2d, this);
122                 IL_PUSH(g_radaricons, this);
123     }
124
125     InterpolateOrigin_Undo(this);
126     this.iflags |= IFLAG_ORIGIN;
127
128     if (sendflags & 0x80)
129     {
130         int t = ReadByte();
131         if (t < 192)
132         {
133             SetResourceExplicit(this, RES_HEALTH, t / 191.0);
134             this.build_finished = 0;
135         }
136         else
137         {
138             t = (t - 192) * 256 + ReadByte();
139             this.build_started = servertime;
140             if (this.build_finished)
141                 this.build_starthealth = bound(0, GetResource(this, RES_HEALTH), 1);
142             else
143                 this.build_starthealth = 0;
144             this.build_finished = servertime + t / 32;
145         }
146     }
147     else
148     {
149         SetResourceExplicit(this, RES_HEALTH, -1);
150         this.build_finished = 0;
151     }
152
153     if (sendflags & 64)
154     {
155         // unfortunately, this needs to be exact (for the 3D display)
156         this.origin = ReadVector();
157         setorigin(this, this.origin);
158     }
159
160     if (sendflags & 1)
161     {
162         this.team = ReadByte();
163         this.rule = ReadByte();
164     }
165
166     if (sendflags & 2)
167     {
168         strcpy(this.netname, ReadString());
169     }
170
171     if (sendflags & 4)
172     {
173         strcpy(this.netname2, ReadString());
174     }
175
176     if (sendflags & 8)
177     {
178         strcpy(this.netname3, ReadString());
179     }
180
181     if (sendflags & 16)
182     {
183         this.lifetime = ReadCoord();
184         this.fadetime = ReadCoord();
185         this.maxdistance = ReadShort();
186         this.hideflags = ReadByte();
187     }
188
189     if (sendflags & 32)
190     {
191         int f = ReadByte();
192         this.teamradar_icon = f & BITS(7);
193         if (f & BIT(7))
194         {
195             this.(teamradar_times[this.teamradar_time_index]) = time;
196             this.teamradar_time_index = (this.teamradar_time_index + 1) % MAX_TEAMRADAR_TIMES;
197         }
198         this.teamradar_color_x = ReadByte() / 255.0;
199         this.teamradar_color_y = ReadByte() / 255.0;
200         this.teamradar_color_z = ReadByte() / 255.0;
201         this.helpme = ReadByte() * 0.1;
202         if (this.helpme > 0)
203             this.helpme += servertime;
204     }
205
206     InterpolateOrigin_Note(this);
207
208     this.entremove = Ent_RemoveWaypointSprite;
209 }
210 #endif
211
212 #ifdef CSQC
213 float spritelookupblinkvalue(entity this, string s)
214 {
215     if (s == WP_Weapon.netname) {
216         if (REGISTRY_GET(Weapons, this.wp_extra).spawnflags & WEP_FLAG_SUPERWEAPON)
217             return 2;
218     }
219     if (s == WP_Item.netname) return REGISTRY_GET(Items, this.wp_extra).m_waypointblink;
220     if(s == WP_FlagReturn.netname) return 2;
221
222     return 1;
223 }
224
225 vector spritelookupcolor(entity this, string s, vector def)
226 {
227     if (s == WP_Weapon.netname  || s == RADARICON_Weapon.netname) return REGISTRY_GET(Weapons, this.wp_extra).wpcolor;
228     if (s == WP_Item.netname    || s == RADARICON_Item.netname) return REGISTRY_GET(Items, this.wp_extra).m_color;
229     if (MUTATOR_CALLHOOK(WP_Format, this, s))
230     {
231         return M_ARGV(2, vector);
232     }
233     return def;
234 }
235
236 string spritelookuptext(entity this, string s)
237 {
238         if(autocvar_g_waypointsprite_spam && waypointsprite_count >= autocvar_g_waypointsprite_spam)
239                 return "Spam"; // no need to translate this debug string
240     if (s == WP_RaceStartFinish.netname) return (race_checkpointtime || race_mycheckpointtime) ? _("Finish") : _("Start");
241     if (s == WP_Weapon.netname) return REGISTRY_GET(Weapons, this.wp_extra).m_name;
242     if (s == WP_Item.netname) return REGISTRY_GET(Items, this.wp_extra).m_waypoint;
243     if (s == WP_Monster.netname) return get_monsterinfo(this.wp_extra).monster_name;
244     if (MUTATOR_CALLHOOK(WP_Format, this, s))
245     {
246         return M_ARGV(3, string);
247     }
248
249     // need to loop, as our netname could be one of three
250     FOREACH(Waypoints, it.netname == s, {
251         return it.m_name;
252     });
253
254     return s;
255 }
256
257 string spritelookupicon(entity this, string s)
258 {
259     // TODO: needs icons! //if (s == WP_RaceStartFinish.netname) return (race_checkpointtime || race_mycheckpointtime) ? _("Finish") : _("Start");
260     if (s == WP_Weapon.netname) return REGISTRY_GET(Weapons, this.wp_extra).model2;
261     if (s == WP_Item.netname) return REGISTRY_GET(Items, this.wp_extra).m_icon;
262     if (s == WP_Vehicle.netname) return REGISTRY_GET(Vehicles, this.wp_extra).m_icon;
263     //if (s == WP_Monster.netname) return get_monsterinfo(this.wp_extra).m_icon;
264     if (MUTATOR_CALLHOOK(WP_Format, this, s))
265     {
266         return M_ARGV(4, string);
267     }
268
269     // need to loop, as our netname could be one of three
270     FOREACH(Waypoints, it.netname == s, {
271         return it.m_icon;
272     });
273
274     return s;
275 }
276 #endif
277
278 #ifdef CSQC
279 void drawrotpic(vector org, float rot, string pic, vector sz, vector hotspot, vector rgb, float a, float f)
280 {
281     vector v1, v2, v3, v4;
282
283     hotspot = -1 * hotspot;
284
285     // hotspot-relative coordinates of the corners
286     v1 = hotspot;
287     v2 = hotspot + '1 0 0' * sz.x;
288     v3 = hotspot + '1 0 0' * sz.x + '0 1 0' * sz.y;
289     v4 = hotspot                  + '0 1 0' * sz.y;
290
291     // rotate them, and make them absolute
292     rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
293     v1 = Rotate(v1, rot) + org;
294     v2 = Rotate(v2, rot) + org;
295     v3 = Rotate(v3, rot) + org;
296     v4 = Rotate(v4, rot) + org;
297
298     // draw them
299     R_BeginPolygon(pic, f, true);
300     R_PolygonVertex(v1, '0 0 0', rgb, a);
301     R_PolygonVertex(v2, '1 0 0', rgb, a);
302     R_PolygonVertex(v3, '1 1 0', rgb, a);
303     R_PolygonVertex(v4, '0 1 0', rgb, a);
304     R_EndPolygon();
305 }
306
307 void drawquad(vector o, vector ri, vector up, string pic, vector rgb, float a, float f)
308 {
309     R_BeginPolygon(pic, f, true);
310     R_PolygonVertex(o, '0 0 0', rgb, a);
311     R_PolygonVertex(o + ri, '1 0 0', rgb, a);
312     R_PolygonVertex(o + up + ri, '1 1 0', rgb, a);
313     R_PolygonVertex(o + up, '0 1 0', rgb, a);
314     R_EndPolygon();
315 }
316
317 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)
318 {
319     vector o, ri, up;
320     float owidth; // outer width
321
322     hotspot = -1 * hotspot;
323
324     // hotspot-relative coordinates of the healthbar corners
325     o = hotspot;
326     ri = '1 0 0';
327     up = '0 1 0';
328
329     rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
330     o = Rotate(o, rot) + org;
331     ri = Rotate(ri, rot);
332     up = Rotate(up, rot);
333
334     owidth = width + 2 * border;
335     o = o - up * (margin + border + theheight) + ri * (sz.x - owidth) * 0.5;
336
337     drawquad(o - up * border,                               ri * owidth,    up * border,    "", rgb,  a,  f);
338     drawquad(o + up * theheight,                            ri * owidth,    up * border,    "", rgb,  a,  f);
339     drawquad(o,                                             ri * border,    up * theheight, "", rgb,  a,  f);
340     drawquad(o + ri * (owidth - border),                    ri * border,    up * theheight, "", rgb,  a,  f);
341     drawquad(o + ri * (border + align * ((1 - h) * width)), ri * width * h, up * theheight, "", hrgb, ha, f);
342 }
343
344 // returns location of sprite text
345 vector drawspritearrow(vector o, float ang, vector rgb, float a, float t)
346 {
347     float size   = 9.0 * t;
348     float border = 1.5 * t;
349     float margin = 4.0 * t;
350
351     float borderDiag = border * M_SQRT2;
352     vector arrowX  = eX * size;
353     vector arrowY  = eY * (size+borderDiag);
354     vector borderX = eX * (size+borderDiag);
355     vector borderY = eY * (size+borderDiag+border);
356
357     R_BeginPolygon("", DRAWFLAG_NORMAL, true);
358     R_PolygonVertex(o,                                  '0 0 0', '0 0 0', a);
359     R_PolygonVertex(o + Rotate(arrowY  - borderX, ang), '0 0 0', '0 0 0', a);
360     R_PolygonVertex(o + Rotate(borderY - borderX, ang), '0 0 0', '0 0 0', a);
361     R_PolygonVertex(o + Rotate(borderY + borderX, ang), '0 0 0', '0 0 0', a);
362     R_PolygonVertex(o + Rotate(arrowY  + borderX, ang), '0 0 0', '0 0 0', a);
363     R_EndPolygon();
364
365     R_BeginPolygon("", DRAWFLAG_ADDITIVE, true);
366     R_PolygonVertex(o + Rotate(eY * borderDiag, ang), '0 0 0', rgb, a);
367     R_PolygonVertex(o + Rotate(arrowY - arrowX, ang), '0 0 0', rgb, a);
368     R_PolygonVertex(o + Rotate(arrowY + arrowX, ang), '0 0 0', rgb, a);
369     R_EndPolygon();
370
371     return o + Rotate(eY * (borderDiag+size+margin), ang);
372 }
373
374 // returns location of sprite healthbar
375 vector drawsprite_TextOrIcon(bool is_text, vector o, float ang, float minwidth, vector rgb, float a, vector sz, string str)
376 {
377     float algnx, algny;
378     float sw, w, h;
379     float aspect, sa, ca;
380
381     if (is_text)
382         sw = stringwidth(str, false, sz);
383     else
384         sw = sz.x;
385
386     if (sw > minwidth)
387         w = sw;
388     else
389         w = minwidth;
390     h = sz.y;
391
392     // how do corners work?
393     aspect = vid_conwidth / vid_conheight;
394     sa = sin(ang);
395     ca = cos(ang) * aspect;
396     if (fabs(sa) > fabs(ca))
397     {
398         algnx = (sa < 0);
399         float f = fabs(sa);
400         algny = 0.5 - 0.5 * (f ? (ca / f) : 0);
401     }
402     else
403     {
404         float f = fabs(ca);
405         algnx = 0.5 - 0.5 * (f ? (sa / f) : 0);
406         algny = (ca < 0);
407     }
408
409     // align
410     o.x -= w * algnx;
411     o.y -= h * algny;
412
413     // we want to be onscreen
414     if (o.x < 0)
415         o.x = 0;
416     if (o.y < 0)
417         o.y = 0;
418     if (o.x > vid_conwidth - w)
419         o.x = vid_conwidth - w;
420     if (o.y > vid_conheight - h)
421         o.y = vid_conheight - h;
422
423     o.x += 0.5 * (w - sw);
424
425     if (is_text)
426         drawstring(o, str, sz, rgb, a, DRAWFLAG_NORMAL);
427     else
428         drawpic(o, str, sz, rgb, a, DRAWFLAG_NORMAL);
429
430     o.x += 0.5 * sw;
431     o.y += 0.5 * h;
432
433     return o;
434 }
435
436 vector fixrgbexcess_move(vector rgb, vector src, vector dst)
437 {
438     vector yvec = '0.299 0.587 0.114';
439     return rgb + dst * ((src * yvec) / (dst * yvec)) * ((rgb - '1 1 1') * src);
440 }
441
442 vector fixrgbexcess(vector rgb)
443 {
444     if (rgb.x > 1) {
445         rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 1');
446         if (rgb.y > 1) {
447             rgb = fixrgbexcess_move(rgb, '0 1 0', '0 0 1');
448             if (rgb.z > 1) rgb.z = 1;
449         } else if (rgb.z > 1) {
450             rgb = fixrgbexcess_move(rgb, '0 0 1', '0 1 0');
451             if (rgb.y > 1) rgb.y = 1;
452         }
453     } else if (rgb.y > 1) {
454         rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 1');
455         if (rgb.x > 1) {
456             rgb = fixrgbexcess_move(rgb, '1 0 0', '0 0 1');
457             if (rgb.z > 1) rgb.z = 1;
458         } else if (rgb.z > 1) {
459             rgb = fixrgbexcess_move(rgb, '0 0 1', '1 0 0');
460             if (rgb.x > 1) rgb.x = 1;
461         }
462     } else if (rgb.z > 1) {
463         rgb = fixrgbexcess_move(rgb, '0 0 1', '1 1 0');
464         if (rgb.x > 1) {
465             rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 0');
466             if (rgb.y > 1) rgb.y = 1;
467         } else if (rgb.y > 1) {
468             rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 0');
469             if (rgb.x > 1) rgb.x = 1;
470         }
471     }
472     return rgb;
473 }
474
475 void Draw_WaypointSprite(entity this)
476 {
477     if (this.lifetime > 0)
478         this.alpha = (bound(0, (this.fadetime - time) / this.lifetime, 1) ** waypointsprite_timealphaexponent);
479     else
480         this.alpha = 1;
481
482     if (this.hideflags & 2)
483         return; // radar only
484
485     if (autocvar_cl_hidewaypoints >= 2)
486         return;
487
488     if ((this.hideflags & 1) && autocvar_cl_hidewaypoints)
489         return; // fixed waypoint
490
491     InterpolateOrigin_Do(this);
492
493     float t = entcs_GetTeam(player_localnum) + 1;
494     string spriteimage = "";
495
496     // choose the sprite
497     switch (this.rule)
498     {
499         case SPRITERULE_SPECTATOR:
500             if (!(
501                 (autocvar_g_waypointsprite_itemstime == 1 && t == NUM_SPECTATOR + 1)
502             ||  (autocvar_g_waypointsprite_itemstime == 2 && (t == NUM_SPECTATOR + 1 || warmup_stage || STAT(ITEMSTIME) == 2))
503                 ))
504                 return;
505             spriteimage = this.netname;
506             break;
507         case SPRITERULE_DEFAULT:
508             if (this.team)
509             {
510                 if (this.team == t)
511                     spriteimage = this.netname;
512                 else
513                     spriteimage = "";
514             }
515             else
516                 spriteimage = this.netname;
517             break;
518         case SPRITERULE_TEAMPLAY:
519             if (t == NUM_SPECTATOR + 1)
520                 spriteimage = this.netname3;
521             else if (this.team == t)
522                 spriteimage = this.netname2;
523             else
524                 spriteimage = this.netname;
525             break;
526         default:
527             error("Invalid waypointsprite rule!");
528             break;
529     }
530
531     if (spriteimage == "")
532         return;
533
534     ++waypointsprite_newcount;
535
536     float dist = vlen(this.origin - view_origin);
537     float a = this.alpha * autocvar_hud_panel_fg_alpha;
538
539     if(this.maxdistance > 0)
540     {
541         // restrict maximum normal distance to the waypoint's maximum distance to prevent exploiting cvars
542         float maxnormdistance = bound(0, waypointsprite_normdistance, this.maxdistance - 1);
543         a *= (bound(0, (this.maxdistance - dist) / (this.maxdistance - maxnormdistance), 1) ** waypointsprite_distancealphaexponent);
544     }
545
546     vector rgb = spritelookupcolor(this, spriteimage, this.teamradar_color);
547     if (rgb == '0 0 0')
548     {
549         this.teamradar_color = '1 0 1';
550         LOG_INFOF("WARNING: sprite of name %s has no color, using pink so you notice it", spriteimage);
551     }
552
553     float health_val = GetResource(this, RES_HEALTH);
554     float blink_time = (health_val >= 0) ? (health_val * 10) : time;
555     if (blink_time - floor(blink_time) > 0.5)
556     {
557         if (this.helpme && time < this.helpme)
558             a *= SPRITE_HELPME_BLINK;
559         else if (!this.lifetime) // fading out waypoints don't blink
560             a *= spritelookupblinkvalue(this, spriteimage);
561     }
562
563     if (a > 1)
564     {
565         rgb *= a;
566         a = 1;
567     }
568
569     if (a <= 0.003)
570         return;
571
572     rgb = fixrgbexcess(rgb);
573
574     vector o;
575     float ang;
576
577     o = project_3d_to_2d(this.origin);
578     if (o.z < 0
579     || o.x < (vid_conwidth * waypointsprite_edgeoffset_left)
580     || o.y < (vid_conheight * waypointsprite_edgeoffset_top)
581     || o.x > (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right))
582     || o.y > (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)))
583     {
584         // scale it to be just in view
585         vector d;
586
587         d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
588         ang = atan2(-d.x, -d.y);
589         if (o.z < 0)
590             ang += M_PI;
591
592                 float f1 = d.x / vid_conwidth;
593                 float f2 = d.y / vid_conheight;
594                 if (f1 == 0) { f1 = 0.000001; }
595                 if (f2 == 0) { f2 = 0.000001; }
596
597         if (max(f1, -f1) > max(f2, -f2)) {
598             if (d.z * f1 > 0) {
599                 // RIGHT edge
600                 d = d * ((0.5 - waypointsprite_edgeoffset_right) / f1);
601             } else {
602                 // LEFT edge
603                 d = d * (-(0.5 - waypointsprite_edgeoffset_left) / f1);
604             }
605         } else {
606             if (d.z * f2 > 0) {
607                 // BOTTOM edge
608                 d = d * ((0.5 - waypointsprite_edgeoffset_bottom) / f2);
609             } else {
610                 // TOP edge
611                 d = d * (-(0.5 - waypointsprite_edgeoffset_top) / f2);
612             }
613         }
614
615         o = d + '0.5 0 0' * vid_conwidth + '0 0.5 0' * vid_conheight;
616     }
617     else
618     {
619 #if 1
620         ang = M_PI;
621 #else
622         vector d;
623         d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
624         ang = atan2(-d.x, -d.y);
625 #endif
626     }
627     o.z = 0;
628
629     float edgedistance_min = min((o.y - (vid_conheight * waypointsprite_edgeoffset_top)),
630     (o.x - (vid_conwidth * waypointsprite_edgeoffset_left)),
631     (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right)) - o.x,
632     (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)) - o.y);
633
634     float crosshairdistance = sqrt( ((o.x - vid_conwidth/2) ** 2) + ((o.y - vid_conheight/2) ** 2) );
635
636     t = waypointsprite_scale;
637     a *= waypointsprite_alpha;
638
639     {
640         a = a * (1 - (1 - waypointsprite_distancefadealpha) * (bound(0, dist/waypointsprite_distancefadedistance, 1)));
641         t = t * (1 - (1 - waypointsprite_distancefadescale) * (bound(0, dist/waypointsprite_distancefadedistance, 1)));
642     }
643     if (edgedistance_min < waypointsprite_edgefadedistance) {
644         a = a * (1 - (1 - waypointsprite_edgefadealpha) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1)));
645         t = t * (1 - (1 - waypointsprite_edgefadescale) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1)));
646     }
647     if (crosshairdistance < waypointsprite_crosshairfadedistance) {
648         a = a * (1 - (1 - waypointsprite_crosshairfadealpha) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1)));
649         t = t * (1 - (1 - waypointsprite_crosshairfadescale) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1)));
650     }
651
652     if (this.build_finished)
653     {
654         if (time < this.build_finished + 0.25)
655         {
656             if (time < this.build_started)
657                 SetResourceExplicit(this, RES_HEALTH, this.build_starthealth);
658             else if (time < this.build_finished)
659                 SetResourceExplicit(this, RES_HEALTH, (time - this.build_started) / (this.build_finished - this.build_started) * (1 - this.build_starthealth) + this.build_starthealth);
660             else
661                 SetResourceExplicit(this, RES_HEALTH, 1);
662         }
663         else
664             SetResourceExplicit(this, RES_HEALTH, -1);
665     }
666
667     o = drawspritearrow(o, ang, rgb, a, SPRITE_ARROW_SCALE * t);
668
669         string pic = "";
670         bool is_text = true;
671         if (!autocvar_g_waypointsprite_text)
672         {
673                 string spr_icon = spritelookupicon(this, spriteimage);
674                 pic = spr_icon;
675                 bool icon_found = !(!spr_icon || spr_icon == "");
676                 if (icon_found) // it's valid, but let's make sure it exists!
677                 {
678                         pic = strcat(hud_skin_path, "/", spr_icon);
679                         if(precache_pic(pic) == "")
680                         {
681                                 pic = strcat("gfx/hud/default/", spr_icon);
682                                 if(!precache_pic(pic))
683                                         icon_found = false;
684                         }
685                 }
686                 if (icon_found)
687                         is_text = false;
688         }
689
690         vector sz;
691         vector txt_color;
692     string txt = string_null;
693     if (is_text)
694     {
695         txt = spritelookuptext(this, spriteimage);
696         if (this.helpme && time < this.helpme)
697             txt = sprintf(_("%s needing help!"), txt);
698         if (autocvar_g_waypointsprite_uppercase)
699             txt = strtoupper(txt);
700         txt_color = rgb;
701         sz = waypointsprite_fontsize * '1 1 0';
702     }
703     else
704     {
705         // for convenience icon path and color are saved to txt and txt_color
706         txt = pic;
707         txt_color = ((autocvar_g_waypointsprite_iconcolor) ? '1 1 1' : rgb);
708         sz = autocvar_g_waypointsprite_iconsize * '1 1 0';
709     }
710
711     draw_beginBoldFont();
712     if (GetResource(this, RES_HEALTH) >= 0)
713     {
714         float align = 0, marg;
715         if (this.build_finished)
716             align = 0.5;
717         else
718             align = 0;
719         if (cos(ang) > 0)
720             marg = -(SPRITE_HEALTHBAR_MARGIN + SPRITE_HEALTHBAR_HEIGHT + 2 * SPRITE_HEALTHBAR_BORDER) * t - 0.5 * sz.y;
721         else
722             marg = SPRITE_HEALTHBAR_MARGIN * t + 0.5 * sz.y;
723
724         float minwidth = (SPRITE_HEALTHBAR_WIDTH + 2 * SPRITE_HEALTHBAR_BORDER) * t;
725         o = drawsprite_TextOrIcon(is_text, o, ang, minwidth, txt_color, a, sz, txt);
726         drawhealthbar(
727                 o,
728                 0,
729                 GetResource(this, RES_HEALTH),
730                 '0 0 0',
731                 '0 0 0',
732                 SPRITE_HEALTHBAR_WIDTH * t,
733                 SPRITE_HEALTHBAR_HEIGHT * t,
734                 marg,
735                 SPRITE_HEALTHBAR_BORDER * t,
736                 align,
737                 rgb,
738                 a * SPRITE_HEALTHBAR_BORDERALPHA,
739                 rgb,
740                 a * SPRITE_HEALTHBAR_HEALTHALPHA,
741                 DRAWFLAG_NORMAL
742                  );
743     }
744     else
745     {
746         drawsprite_TextOrIcon(is_text, o, ang, 0, txt_color, a, sz, txt);
747     }
748
749     draw_endBoldFont();
750 }
751
752 void WaypointSprite_Load_Frames(string ext)
753 {
754     int dh = search_begin(strcat("models/sprites/*_frame*", ext), false, false);
755     if (dh < 0) return;
756     int ext_len = strlen(ext);
757     int n = search_getsize(dh);
758     for (int i = 0; i < n; ++i)
759     {
760         string s = search_getfilename(dh, i);
761         s = substring(s, 15, strlen(s) - 15 - ext_len); // strip models/sprites/ and extension
762
763         int o = strstrofs(s, "_frame", 0);
764         string sname = strcat("/spriteframes/", substring(s, 0, o));
765         string sframes = substring(s, o + 6, strlen(s) - o - 6);
766         int f = stof(sframes) + 1;
767         db_put(tempdb, sname, ftos(max(f, stof(db_get(tempdb, sname)))));
768     }
769     search_end(dh);
770 }
771
772 void WaypointSprite_Load();
773 STATIC_INIT(WaypointSprite_Load) {
774     WaypointSprite_Load();
775     WaypointSprite_Load_Frames(".tga");
776     WaypointSprite_Load_Frames(".jpg");
777 }
778 void WaypointSprite_Load()
779 {
780     waypointsprite_fadedistance = vlen(mi_scale);
781     waypointsprite_normdistance = autocvar_g_waypointsprite_normdistance;
782     waypointsprite_minscale = autocvar_g_waypointsprite_minscale;
783     waypointsprite_minalpha = autocvar_g_waypointsprite_minalpha;
784     waypointsprite_distancealphaexponent = autocvar_g_waypointsprite_distancealphaexponent;
785     waypointsprite_timealphaexponent = autocvar_g_waypointsprite_timealphaexponent;
786     waypointsprite_scale = autocvar_g_waypointsprite_scale;
787     waypointsprite_fontsize = autocvar_g_waypointsprite_fontsize;
788     waypointsprite_edgefadealpha = autocvar_g_waypointsprite_edgefadealpha;
789     waypointsprite_edgefadescale = autocvar_g_waypointsprite_edgefadescale;
790     waypointsprite_edgefadedistance = autocvar_g_waypointsprite_edgefadedistance;
791     waypointsprite_edgeoffset_bottom = autocvar_g_waypointsprite_edgeoffset_bottom;
792     waypointsprite_edgeoffset_left = autocvar_g_waypointsprite_edgeoffset_left;
793     waypointsprite_edgeoffset_right = autocvar_g_waypointsprite_edgeoffset_right;
794     waypointsprite_edgeoffset_top = autocvar_g_waypointsprite_edgeoffset_top;
795     waypointsprite_crosshairfadealpha = autocvar_g_waypointsprite_crosshairfadealpha;
796     waypointsprite_crosshairfadescale = autocvar_g_waypointsprite_crosshairfadescale;
797     waypointsprite_crosshairfadedistance = autocvar_g_waypointsprite_crosshairfadedistance;
798     waypointsprite_distancefadealpha = autocvar_g_waypointsprite_distancefadealpha;
799     waypointsprite_distancefadescale = autocvar_g_waypointsprite_distancefadescale;
800     waypointsprite_distancefadedistance = waypointsprite_fadedistance * autocvar_g_waypointsprite_distancefadedistancemultiplier;
801     waypointsprite_alpha = autocvar_g_waypointsprite_alpha * (1 - autocvar__menu_alpha);
802
803     waypointsprite_count = waypointsprite_newcount;
804     waypointsprite_newcount = 0;
805 }
806 #endif
807
808 #ifdef SVQC
809 void WaypointSprite_UpdateSprites(entity e, entity _m1, entity _m2, entity _m3)
810 {
811     string m1 = _m1.netname;
812     string m2 = _m2.netname;
813     string m3 = _m3.netname;
814     if (m1 != e.model1)
815     {
816         e.model1 = m1;
817         e.SendFlags |= 2;
818     }
819     if (m2 != e.model2)
820     {
821         e.model2 = m2;
822         e.SendFlags |= 4;
823     }
824     if (m3 != e.model3)
825     {
826         e.model3 = m3;
827         e.SendFlags |= 8;
828     }
829 }
830
831 void WaypointSprite_UpdateHealth(entity e, float f)
832 {
833     f = bound(0, f, e.max_health);
834     float step = e.max_health / 40;
835     if ((floor(f / step) != floor(GetResource(e, RES_HEALTH) / step)) || e.pain_finished)
836     {
837         SetResourceExplicit(e, RES_HEALTH, f);
838         e.pain_finished = 0;
839         e.SendFlags |= 0x80;
840     }
841 }
842
843 void WaypointSprite_UpdateMaxHealth(entity e, float f)
844 {
845     if (f != e.max_health || e.pain_finished)
846     {
847         e.max_health = f;
848         e.pain_finished = 0;
849         e.SendFlags |= 0x80;
850     }
851 }
852
853 void WaypointSprite_UpdateBuildFinished(entity e, float f)
854 {
855     if (f != e.pain_finished || e.max_health)
856     {
857         e.max_health = 0;
858         e.pain_finished = f;
859         e.SendFlags |= 0x80;
860     }
861 }
862
863 void WaypointSprite_UpdateOrigin(entity e, vector o)
864 {
865     if (o != e.origin)
866     {
867         setorigin(e, o);
868         e.SendFlags |= 64;
869     }
870 }
871
872 void WaypointSprite_UpdateRule(entity e, float t, float r)
873 {
874     // no check, as this is never called without doing an actual change (usually only once)
875     e.rule = r;
876     e.team = t;
877     e.SendFlags |= 1;
878 }
879
880 void WaypointSprite_UpdateTeamRadar(entity e, entity icon, vector col)
881 {
882     // no check, as this is never called without doing an actual change (usually only once)
883     int i = icon.m_id;
884     e.cnt = (e.cnt & BIT(7)) | (i & BITS(7));
885     e.colormod = col;
886     e.SendFlags |= 32;
887 }
888
889 void WaypointSprite_Ping(entity e)
890 {
891     // anti spam
892     if (time < e.waypointsprite_pingtime) return;
893     e.waypointsprite_pingtime = time + 0.3;
894     // ALWAYS sends (this causes a radar circle), thus no check
895     e.cnt |= BIT(7);
896     e.SendFlags |= 32;
897 }
898
899 void WaypointSprite_HelpMePing(entity e)
900 {
901     WaypointSprite_Ping(e);
902     e.waypointsprite_helpmetime = time + waypointsprite_deployed_lifetime;
903     e.SendFlags |= 32;
904 }
905
906 void WaypointSprite_FadeOutIn(entity e, float t)
907 {
908     if (!e.fade_time)
909     {
910         e.fade_time = t;
911         e.teleport_time = time + t;
912     }
913     else if (t < (e.teleport_time - time))
914     {
915         // accelerate the waypoint's dying
916         // ensure:
917         //   (e.teleport_time - time) / wp.fade_time stays
918         //   e.teleport_time = time + fadetime
919         float current_fadetime = e.teleport_time - time;
920         e.teleport_time = time + t;
921         if (e.fade_time < 0)
922                 e.fade_time = -e.fade_time;
923         e.fade_time = e.fade_time * t / current_fadetime;
924     }
925
926     e.SendFlags |= 16;
927 }
928
929 void WaypointSprite_Init()
930 {
931     waypointsprite_limitedrange = autocvar_sv_waypointsprite_limitedrange;
932     waypointsprite_deployed_lifetime = autocvar_sv_waypointsprite_deployed_lifetime;
933     waypointsprite_deadlifetime = autocvar_sv_waypointsprite_deadlifetime;
934 }
935
936 void WaypointSprite_Kill(entity wp)
937 {
938     if (!wp) return;
939     if (wp.owner) wp.owner.(wp.owned_by_field) = NULL;
940     delete(wp);
941 }
942
943 void WaypointSprite_Disown(entity wp, float fadetime)
944 {
945     if (!wp) return;
946     if (wp.classname != "sprite_waypoint")
947     {
948         backtrace("Trying to disown a non-waypointsprite");
949         return;
950     }
951     if (wp.owner)
952     {
953         if (wp.exteriormodeltoclient == wp.owner)
954             wp.exteriormodeltoclient = NULL;
955         wp.owner.(wp.owned_by_field) = NULL;
956         wp.owner = NULL;
957
958         WaypointSprite_FadeOutIn(wp, fadetime);
959     }
960 }
961
962 void WaypointSprite_Think(entity this)
963 {
964     bool doremove = false;
965
966     if (this.fade_time && time >= this.teleport_time)
967     {
968         doremove = true;
969     }
970
971     if (this.exteriormodeltoclient)
972         WaypointSprite_UpdateOrigin(this, this.exteriormodeltoclient.origin + this.view_ofs);
973
974     if (doremove)
975         WaypointSprite_Kill(this);
976     else
977         this.nextthink = time; // WHY?!?
978 }
979
980 bool WaypointSprite_visible_for_player(entity this, entity player, entity view)
981 {
982     // personal waypoints
983     if (this.enemy && this.enemy != view)
984         return false;
985
986     // team waypoints
987     if (this.rule == SPRITERULE_SPECTATOR)
988     {
989         if (!autocvar_sv_itemstime)
990             return false;
991         if (!warmup_stage && IS_PLAYER(view) && autocvar_sv_itemstime != 2)
992             return false;
993     }
994     else if (this.team && this.rule == SPRITERULE_DEFAULT)
995     {
996         if (this.team != view.team)
997             return false;
998         if (!IS_PLAYER(view))
999             return false;
1000     }
1001
1002     return true;
1003 }
1004
1005 entity WaypointSprite_getviewentity(entity e)
1006 {
1007     if (IS_SPEC(e)) e = e.enemy;
1008     /* TODO idea (check this breaks nothing)
1009     else if (e.classname == "observer")
1010         e = NULL;
1011     */
1012     return e;
1013 }
1014
1015 float WaypointSprite_isteammate(entity e, entity e2)
1016 {
1017     if (teamplay)
1018         return e2.team == e.team;
1019     return e2 == e;
1020 }
1021
1022 bool WaypointSprite_Customize(entity this, entity client)
1023 {
1024     // this is not in SendEntity because it shall run every frame, not just every update
1025
1026     // make spectators see what the player would see
1027     entity e = WaypointSprite_getviewentity(client);
1028
1029     if (MUTATOR_CALLHOOK(CustomizeWaypoint, this, client))
1030         return false;
1031
1032     return this.waypointsprite_visible_for_player(this, client, e);
1033 }
1034
1035 bool WaypointSprite_SendEntity(entity this, entity to, float sendflags);
1036
1037 void WaypointSprite_Reset(entity this)
1038 {
1039     // if a WP wants to time out, let it time out immediately; other WPs ought to be reset/killed by their owners
1040
1041     if (this.fade_time)
1042         WaypointSprite_Kill(this);
1043 }
1044
1045 entity WaypointSprite_Spawn(
1046     entity spr, // sprite
1047     float _lifetime, float maxdistance, // lifetime, max distance
1048     entity ref, vector ofs, // position
1049     entity showto, float t, // show to whom? Use a flag to indicate a team
1050     entity own, .entity ownfield, // remove when own gets killed
1051     float hideable, // true when it should be controlled by cl_hidewaypoints
1052     entity icon // initial icon
1053 )
1054 {
1055     entity wp = new(sprite_waypoint);
1056     wp.fade_time = _lifetime; // if negative tells client not to fade it out
1057     if(_lifetime < 0)
1058         _lifetime = -_lifetime;
1059     wp.teleport_time = time + _lifetime;
1060     wp.exteriormodeltoclient = ref;
1061     if (ref)
1062     {
1063         wp.view_ofs = ofs;
1064         setorigin(wp, ref.origin + ofs);
1065     }
1066     else
1067         setorigin(wp, ofs);
1068     wp.enemy = showto;
1069     wp.team = t;
1070     wp.owner = own;
1071     wp.currentammo = hideable;
1072     if (own)
1073     {
1074         if (own.(ownfield))
1075             delete(own.(ownfield));
1076         own.(ownfield) = wp;
1077         wp.owned_by_field = ownfield;
1078     }
1079     wp.fade_rate = maxdistance;
1080     setthink(wp, WaypointSprite_Think);
1081     wp.nextthink = time;
1082     wp.model1 = spr.netname;
1083     setcefc(wp, WaypointSprite_Customize);
1084     wp.waypointsprite_visible_for_player = WaypointSprite_visible_for_player;
1085     wp.reset2 = WaypointSprite_Reset;
1086     wp.cnt = icon.m_id;
1087     wp.colormod = spr.m_color;
1088     Net_LinkEntity(wp, false, 0, WaypointSprite_SendEntity);
1089     return wp;
1090 }
1091
1092 entity WaypointSprite_SpawnFixed(
1093     entity spr,
1094     vector ofs,
1095     entity own,
1096     .entity ownfield,
1097     entity icon // initial icon
1098 )
1099 {
1100     return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, own, ownfield, true, icon);
1101 }
1102
1103 entity WaypointSprite_DeployFixed(
1104     entity spr,
1105     bool limited_range,
1106     entity player,
1107     vector ofs,
1108     entity icon // initial icon
1109 )
1110 {
1111     float t;
1112     if (teamplay)
1113         t = player.team;
1114     else
1115         t = 0;
1116     float maxdistance;
1117     if (limited_range)
1118         maxdistance = waypointsprite_limitedrange;
1119     else
1120         maxdistance = 0;
1121     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, NULL, ofs, NULL, t, player, waypointsprite_deployed_fixed, false, icon);
1122 }
1123
1124 entity WaypointSprite_DeployPersonal(
1125     entity spr,
1126     entity player,
1127     vector ofs,
1128     entity icon // initial icon
1129 )
1130 {
1131     return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, player, waypointsprite_deployed_personal, false, icon);
1132 }
1133
1134 entity WaypointSprite_Attach(
1135     entity spr,
1136     entity player,
1137     bool limited_range,
1138     entity icon // initial icon
1139 )
1140 {
1141     float t;
1142     if (player.waypointsprite_attachedforcarrier)
1143         return NULL; // can't attach to FC
1144     if (teamplay)
1145         t = player.team;
1146     else
1147         t = 0;
1148     float maxdistance;
1149     if (limited_range)
1150         maxdistance = waypointsprite_limitedrange;
1151     else
1152         maxdistance = 0;
1153     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, player, '0 0 64', NULL, t, player, waypointsprite_attached, false, icon);
1154 }
1155
1156 entity WaypointSprite_AttachCarrier(
1157     entity spr,
1158     entity carrier,
1159     entity icon // initial icon and color
1160 )
1161 {
1162     WaypointSprite_Kill(carrier.waypointsprite_attached); // FC overrides attached
1163     entity e = WaypointSprite_Spawn(spr, 0, 0, carrier, '0 0 64', NULL, carrier.team, carrier, waypointsprite_attachedforcarrier, false, icon);
1164     if (GetResource(carrier, RES_HEALTH))
1165     {
1166         WaypointSprite_UpdateMaxHealth(e, 2 * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
1167         WaypointSprite_UpdateHealth(e, healtharmor_maxdamage(GetResource(carrier, RES_HEALTH), GetResource(carrier, RES_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
1168     }
1169     return e;
1170 }
1171
1172 void WaypointSprite_DetachCarrier(entity carrier)
1173 {
1174     WaypointSprite_Disown(carrier.waypointsprite_attachedforcarrier, waypointsprite_deadlifetime);
1175 }
1176
1177 void WaypointSprite_ClearPersonal(entity this)
1178 {
1179     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1180 }
1181
1182 void WaypointSprite_ClearOwned(entity this)
1183 {
1184     WaypointSprite_Kill(this.waypointsprite_deployed_fixed);
1185     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1186     WaypointSprite_Kill(this.waypointsprite_attached);
1187 }
1188
1189 void WaypointSprite_PlayerDead(entity this)
1190 {
1191     WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
1192     WaypointSprite_DetachCarrier(this);
1193 }
1194
1195 void WaypointSprite_PlayerGone(entity this)
1196 {
1197     WaypointSprite_Disown(this.waypointsprite_deployed_fixed, waypointsprite_deadlifetime);
1198     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1199     WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
1200     WaypointSprite_DetachCarrier(this);
1201 }
1202 #endif