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