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