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