Revert "Merge branch 'TimePath/bot_api' into 'master'\r"
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / race.qc
1 #include "race.qh"
2 #include "_all.qh"
3
4 #include "cl_client.qh"
5 #include "portals.qh"
6 #include "scores.qh"
7 #include "spawnpoints.qh"
8 #include "bot/waypoints.qh"
9 #include "bot/navigation.qh"
10 #include "command/getreplies.qh"
11 #include "../common/deathtypes.qh"
12 #include "../common/notifications.qh"
13 #include "../common/mapinfo.qh"
14 #include "../warpzonelib/util_server.qh"
15
16 void W_Porto_Fail(float failhard);
17
18 float race_readTime(string map, float pos)
19 {
20         string rr = (g_cts) ? CTS_RECORD : RACE_RECORD;
21
22         return stof(db_get(ServerProgsDB, strcat(map, rr, "time", ftos(pos))));
23 }
24
25 string race_readUID(string map, float pos)
26 {
27         string rr = (g_cts) ? CTS_RECORD : RACE_RECORD;
28
29         return db_get(ServerProgsDB, strcat(map, rr, "crypto_idfp", ftos(pos)));
30 }
31
32 float race_readPos(string map, float t)
33 {
34         float i;
35         for (i = 1; i <= RANKINGS_CNT; ++i)
36         if (race_readTime(map, i) == 0 || race_readTime(map, i) > t)
37                 return i;
38
39         return 0; // pos is zero if unranked
40 }
41
42 void race_writeTime(string map, float t, string myuid)
43 {
44         string rr = (g_cts) ? CTS_RECORD : RACE_RECORD;
45
46         float newpos;
47         newpos = race_readPos(map, t);
48
49         float i, prevpos = 0;
50         for(i = 1; i <= RANKINGS_CNT; ++i)
51         {
52                 if(race_readUID(map, i) == myuid)
53                         prevpos = i;
54         }
55         if (prevpos)
56         {
57                 // player improved his existing record, only have to iterate on ranks between new and old recs
58                 for (i = prevpos; i > newpos; --i)
59                 {
60                         db_put(ServerProgsDB, strcat(map, rr, "time", ftos(i)), ftos(race_readTime(map, i - 1)));
61                         db_put(ServerProgsDB, strcat(map, rr, "crypto_idfp", ftos(i)), race_readUID(map, i - 1));
62                 }
63         }
64         else
65         {
66                 // player has no ranked record yet
67                 for (i = RANKINGS_CNT; i > newpos; --i)
68                 {
69                         db_put(ServerProgsDB, strcat(map, rr, "time", ftos(i)), ftos(race_readTime(map, i - 1)));
70                         db_put(ServerProgsDB, strcat(map, rr, "crypto_idfp", ftos(i)), race_readUID(map, i - 1));
71                 }
72         }
73
74         // store new time itself
75         db_put(ServerProgsDB, strcat(map, rr, "time", ftos(newpos)), ftos(t));
76         db_put(ServerProgsDB, strcat(map, rr, "crypto_idfp", ftos(newpos)), myuid);
77 }
78
79 string race_readName(string map, float pos)
80 {
81         string rr = (g_cts) ? CTS_RECORD : RACE_RECORD;
82
83         return uid2name(db_get(ServerProgsDB, strcat(map, rr, "crypto_idfp", ftos(pos))));
84 }
85
86
87 const float MAX_CHECKPOINTS = 255;
88
89 spawnfunc(target_checkpoint);
90
91 .float race_penalty;
92 .float race_penalty_accumulator;
93 .string race_penalty_reason;
94 .float race_checkpoint; // player: next checkpoint that has to be reached
95 .entity race_lastpenalty;
96
97 .entity sprite;
98
99 float race_checkpoint_records[MAX_CHECKPOINTS];
100 string race_checkpoint_recordholders[MAX_CHECKPOINTS];
101 float race_checkpoint_lasttimes[MAX_CHECKPOINTS];
102 float race_checkpoint_lastlaps[MAX_CHECKPOINTS];
103 entity race_checkpoint_lastplayers[MAX_CHECKPOINTS];
104
105 float race_highest_checkpoint;
106 float race_timed_checkpoint;
107
108 float defrag_ents;
109 float defragcpexists;
110
111 float race_NextCheckpoint(float f)
112 {
113         if(f >= race_highest_checkpoint)
114                 return 0;
115         else
116                 return f + 1;
117 }
118
119 float race_PreviousCheckpoint(float f)
120 {
121         if(f == -1)
122                 return 0;
123         else if(f == 0)
124                 return race_highest_checkpoint;
125         else
126                 return f - 1;
127 }
128
129 // encode as:
130 //   0 = common start/finish
131 // 254 = start
132 // 255 = finish
133 float race_CheckpointNetworkID(float f)
134 {
135         if(race_timed_checkpoint)
136         {
137                 if(f == 0)
138                         return 254; // start
139                 else if(f == race_timed_checkpoint)
140                         return 255; // finish
141         }
142         return f;
143 }
144
145 void race_SendNextCheckpoint(entity e, float spec) // qualifying only
146 {
147         float recordtime;
148         string recordholder;
149         float cp;
150
151         if(!e.race_laptime)
152                 return;
153
154         cp = e.race_checkpoint;
155         recordtime = race_checkpoint_records[cp];
156         recordholder = race_checkpoint_recordholders[cp];
157         if(recordholder == e.netname)
158                 recordholder = "";
159
160         if(!IS_REAL_CLIENT(e))
161                 return;
162
163         if(!spec)
164                 msg_entity = e;
165         WRITESPECTATABLE_MSG_ONE({
166                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
167                 WriteByte(MSG_ONE, TE_CSQC_RACE);
168                 if(spec)
169                 {
170                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_NEXT_SPEC_QUALIFYING);
171                         //WriteCoord(MSG_ONE, e.race_laptime - e.race_penalty_accumulator);
172                         WriteCoord(MSG_ONE, time - e.race_movetime - e.race_penalty_accumulator);
173                 }
174                 else
175                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_NEXT_QUALIFYING);
176                 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player will be at next
177                 WriteInt24_t(MSG_ONE, recordtime);
178                 WriteString(MSG_ONE, recordholder);
179         });
180 }
181
182 void race_send_recordtime(float msg)
183 {
184         // send the server best time
185         WriteByte(msg, SVC_TEMPENTITY);
186         WriteByte(msg, TE_CSQC_RACE);
187         WriteByte(msg, RACE_NET_SERVER_RECORD);
188         WriteInt24_t(msg, race_readTime(GetMapname(), 1));
189 }
190
191
192 void race_send_speedaward(float msg)
193 {
194         // send the best speed of the round
195         WriteByte(msg, SVC_TEMPENTITY);
196         WriteByte(msg, TE_CSQC_RACE);
197         WriteByte(msg, RACE_NET_SPEED_AWARD);
198         WriteInt24_t(msg, floor(speedaward_speed+0.5));
199         WriteString(msg, speedaward_holder);
200 }
201
202 void race_send_speedaward_alltimebest(float msg)
203 {
204         // send the best speed
205         WriteByte(msg, SVC_TEMPENTITY);
206         WriteByte(msg, TE_CSQC_RACE);
207         WriteByte(msg, RACE_NET_SPEED_AWARD_BEST);
208         WriteInt24_t(msg, floor(speedaward_alltimebest+0.5));
209         WriteString(msg, speedaward_alltimebest_holder);
210 }
211
212 void race_SendRankings(float pos, float prevpos, float del, float msg)
213 {
214         WriteByte(msg, SVC_TEMPENTITY);
215         WriteByte(msg, TE_CSQC_RACE);
216         WriteByte(msg, RACE_NET_SERVER_RANKINGS);
217         WriteShort(msg, pos);
218         WriteShort(msg, prevpos);
219         WriteShort(msg, del);
220         WriteString(msg, race_readName(GetMapname(), pos));
221         WriteInt24_t(msg, race_readTime(GetMapname(), pos));
222 }
223
224 void race_SendStatus(float id, entity e)
225 {
226         if(!IS_REAL_CLIENT(e))
227                 return;
228
229         float msg;
230         if (id == 0)
231                 msg = MSG_ONE;
232         else
233                 msg = MSG_ALL;
234         msg_entity = e;
235         WRITESPECTATABLE_MSG_ONE_VARNAME(dummy3, {
236                 WriteByte(msg, SVC_TEMPENTITY);
237                 WriteByte(msg, TE_CSQC_RACE);
238                 WriteByte(msg, RACE_NET_SERVER_STATUS);
239                 WriteShort(msg, id);
240                 WriteString(msg, e.netname);
241         });
242 }
243
244 void race_setTime(string map, float t, string myuid, string mynetname, entity e)
245 {
246         // netname only used TEMPORARILY for printing
247         float newpos, player_prevpos;
248         newpos = race_readPos(map, t);
249
250         float i;
251         player_prevpos = 0;
252         for(i = 1; i <= RANKINGS_CNT; ++i)
253         {
254                 if(race_readUID(map, i) == myuid)
255                         player_prevpos = i;
256         }
257
258         float oldrec;
259         string oldrec_holder;
260         if (player_prevpos && (player_prevpos < newpos || !newpos))
261         {
262                 oldrec = race_readTime(GetMapname(), player_prevpos);
263                 race_SendStatus(0, e); // "fail"
264                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_RACE_FAIL_RANKED, mynetname, player_prevpos, t, oldrec);
265                 return;
266         }
267         else if (!newpos)
268         {
269                 // no ranking, time worse than the worst ranked
270                 oldrec = race_readTime(GetMapname(), RANKINGS_CNT);
271                 race_SendStatus(0, e); // "fail"
272                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_RACE_FAIL_UNRANKED, mynetname, RANKINGS_CNT, t, oldrec);
273                 return;
274         }
275
276         // if we didn't hit a return yet, we have a new record!
277
278         // if the player does not have a UID we can unfortunately not store the record, as the rankings system relies on UIDs
279         if(myuid == "")
280         {
281                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_RACE_NEW_MISSING_UID, mynetname, t);
282                 return;
283         }
284
285         oldrec = race_readTime(GetMapname(), newpos);
286         oldrec_holder = race_readName(GetMapname(), newpos);
287
288         // store new ranking
289         race_writeTime(GetMapname(), t, myuid);
290
291         if (newpos == 1)
292         {
293                 write_recordmarker(e, time - TIME_DECODE(t), TIME_DECODE(t));
294                 race_send_recordtime(MSG_ALL);
295         }
296
297         race_SendRankings(newpos, player_prevpos, 0, MSG_ALL);
298         if(rankings_reply)
299                 strunzone(rankings_reply);
300         rankings_reply = strzone(getrankings());
301
302         if(newpos == player_prevpos)
303         {
304                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_RACE_NEW_IMPROVED, mynetname, newpos, t, oldrec);
305                 if(newpos == 1) { race_SendStatus(3, e); } // "new server record"
306                 else { race_SendStatus(1, e); } // "new time"
307         }
308         else if(oldrec == 0)
309         {
310                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_RACE_NEW_SET, mynetname, newpos, t);
311                 if(newpos == 1) { race_SendStatus(3, e); } // "new server record"
312                 else { race_SendStatus(2, e); } // "new rank"
313         }
314         else
315         {
316                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_RACE_NEW_BROKEN, mynetname, oldrec_holder, newpos, t, oldrec);
317                 if(newpos == 1) { race_SendStatus(3, e); } // "new server record"
318                 else { race_SendStatus(2, e); } // "new rank"
319         }
320 }
321
322 void race_deleteTime(string map, float pos)
323 {
324         string rr;
325         if(g_cts)
326                 rr = CTS_RECORD;
327         else
328                 rr = RACE_RECORD;
329
330         float i;
331         for (i = pos; i <= RANKINGS_CNT; ++i)
332         {
333                 if (i == RANKINGS_CNT)
334                 {
335                         db_put(ServerProgsDB, strcat(map, rr, "time", ftos(i)), string_null);
336                         db_put(ServerProgsDB, strcat(map, rr, "crypto_idfp", ftos(i)), string_null);
337                 }
338                 else
339                 {
340                         db_put(ServerProgsDB, strcat(map, rr, "time", ftos(i)), ftos(race_readTime(GetMapname(), i+1)));
341                         db_put(ServerProgsDB, strcat(map, rr, "crypto_idfp", ftos(i)), race_readUID(GetMapname(), i+1));
342                 }
343         }
344
345         race_SendRankings(pos, 0, 1, MSG_ALL);
346         if(pos == 1)
347                 race_send_recordtime(MSG_ALL);
348
349         if(rankings_reply)
350                 strunzone(rankings_reply);
351         rankings_reply = strzone(getrankings());
352 }
353
354 void race_SendTime(entity e, float cp, float t, float tvalid)
355 {
356         float snew, l;
357         entity p;
358
359         if(g_race_qualifying)
360                 t += e.race_penalty_accumulator;
361
362         t = TIME_ENCODE(t); // make integer
363         // adding just 0.4 so it rounds down in the .5 case (matching the timer display)
364
365         if(tvalid)
366         if(cp == race_timed_checkpoint) // finish line
367         if (!e.race_completed)
368         {
369                 float s;
370                 if(g_race_qualifying)
371                 {
372                         s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
373                         if(!s || t < s)
374                                 PlayerScore_Add(e, SP_RACE_FASTEST, t - s);
375                 }
376                 else
377                 {
378                         s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
379                         if(!s || t < s)
380                                 PlayerScore_Add(e, SP_RACE_FASTEST, t - s);
381
382                         s = PlayerScore_Add(e, SP_RACE_TIME, 0);
383                         snew = TIME_ENCODE(time - game_starttime);
384                         PlayerScore_Add(e, SP_RACE_TIME, snew - s);
385                         l = PlayerTeamScore_Add(e, SP_RACE_LAPS, ST_RACE_LAPS, 1);
386
387                         if(autocvar_fraglimit)
388                                 if(l >= autocvar_fraglimit)
389                                         race_StartCompleting();
390
391                         if(race_completing)
392                         {
393                                 e.race_completed = 1;
394                                 MAKE_INDEPENDENT_PLAYER(e);
395                                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_RACE_FINISHED, e.netname);
396                                 ClientData_Touch(e);
397                         }
398                 }
399         }
400
401         float recordtime;
402         string recordholder;
403         if(g_race_qualifying)
404         {
405                 if(tvalid)
406                 {
407                         recordtime = race_checkpoint_records[cp];
408                         recordholder = strcat1(race_checkpoint_recordholders[cp]); // make a tempstring copy, as we'll possibly strunzone it!
409                         if(recordholder == e.netname)
410                                 recordholder = "";
411
412                         if(t != 0)
413                         {
414                                 if(cp == race_timed_checkpoint)
415                                 {
416                                         race_setTime(GetMapname(), t, e.crypto_idfp, e.netname, e);
417                                         if(g_cts && autocvar_g_cts_finish_kill_delay)
418                                         {
419                                                 CTS_ClientKill(e);
420                                         }
421                                 }
422                                 if(t < recordtime || recordtime == 0)
423                                 {
424                                         race_checkpoint_records[cp] = t;
425                                         if(race_checkpoint_recordholders[cp])
426                                                 strunzone(race_checkpoint_recordholders[cp]);
427                                         race_checkpoint_recordholders[cp] = strzone(e.netname);
428                                         if(g_race_qualifying)
429                                         {
430                                                 FOR_EACH_REALPLAYER(p)
431                                                         if(p.race_checkpoint == cp)
432                                                                 race_SendNextCheckpoint(p, 0);
433                                         }
434                                 }
435                         }
436                 }
437                 else
438                 {
439                         // dummies
440                         t = 0;
441                         recordtime = 0;
442                         recordholder = "";
443                 }
444
445                 if(IS_REAL_CLIENT(e))
446                 {
447                         msg_entity = e;
448                         if(g_race_qualifying)
449                         {
450                                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy1, {
451                                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
452                                         WriteByte(MSG_ONE, TE_CSQC_RACE);
453                                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_QUALIFYING);
454                                         WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
455                                         WriteInt24_t(MSG_ONE, t); // time to that intermediate
456                                         WriteInt24_t(MSG_ONE, recordtime); // previously best time
457                                         WriteString(MSG_ONE, recordholder); // record holder
458                                 });
459                         }
460                 }
461         }
462         else // RACE! Not Qualifying
463         {
464                 float lself, lother, othtime;
465                 entity oth;
466                 oth = race_checkpoint_lastplayers[cp];
467                 if(oth)
468                 {
469                         lself = PlayerScore_Add(e, SP_RACE_LAPS, 0);
470                         lother = race_checkpoint_lastlaps[cp];
471                         othtime = race_checkpoint_lasttimes[cp];
472                 }
473                 else
474                         lself = lother = othtime = 0;
475
476                 if(IS_REAL_CLIENT(e))
477                 {
478                         msg_entity = e;
479                         WRITESPECTATABLE_MSG_ONE_VARNAME(dummy2, {
480                                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
481                                 WriteByte(MSG_ONE, TE_CSQC_RACE);
482                                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE);
483                                 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
484                                 if(e == oth)
485                                 {
486                                         WriteInt24_t(MSG_ONE, 0);
487                                         WriteByte(MSG_ONE, 0);
488                                         WriteString(MSG_ONE, "");
489                                 }
490                                 else
491                                 {
492                                         WriteInt24_t(MSG_ONE, TIME_ENCODE(time - race_checkpoint_lasttimes[cp]));
493                                         WriteByte(MSG_ONE, lself - lother);
494                                         WriteString(MSG_ONE, oth.netname); // record holder
495                                 }
496                         });
497                 }
498
499                 race_checkpoint_lastplayers[cp] = e;
500                 race_checkpoint_lasttimes[cp] = time;
501                 race_checkpoint_lastlaps[cp] = lself;
502
503                 if(IS_REAL_CLIENT(oth))
504                 {
505                         msg_entity = oth;
506                         WRITESPECTATABLE_MSG_ONE_VARNAME(dummy3, {
507                                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
508                                 WriteByte(MSG_ONE, TE_CSQC_RACE);
509                                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE_BY_OPPONENT);
510                                 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
511                                 if(e == oth)
512                                 {
513                                         WriteInt24_t(MSG_ONE, 0);
514                                         WriteByte(MSG_ONE, 0);
515                                         WriteString(MSG_ONE, "");
516                                 }
517                                 else
518                                 {
519                                         WriteInt24_t(MSG_ONE, TIME_ENCODE(time - othtime));
520                                         WriteByte(MSG_ONE, lother - lself);
521                                         WriteString(MSG_ONE, e.netname); // record holder
522                                 }
523                         });
524                 }
525         }
526 }
527
528 void race_ClearTime(entity e)
529 {
530         e.race_checkpoint = 0;
531         e.race_laptime = 0;
532         e.race_movetime = e.race_movetime_frac = e.race_movetime_count = 0;
533         e.race_penalty_accumulator = 0;
534         e.race_lastpenalty = world;
535
536         if(!IS_REAL_CLIENT(e))
537                 return;
538
539         msg_entity = e;
540         WRITESPECTATABLE_MSG_ONE({
541                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
542                 WriteByte(MSG_ONE, TE_CSQC_RACE);
543                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_CLEAR); // next
544         });
545 }
546
547 void dumpsurface(entity e)
548 {
549         float n, si, ni;
550         vector norm, vec;
551         LOG_INFO("Surfaces of ", etos(e), ":\n");
552
553         LOG_INFO("TEST = ", ftos(getsurfacenearpoint(e, '0 0 0')), "\n");
554
555         for(si = 0; ; ++si)
556         {
557                 n = getsurfacenumpoints(e, si);
558                 if(n <= 0)
559                         break;
560                 LOG_INFO("  Surface ", ftos(si), ":\n");
561                 norm = getsurfacenormal(e, si);
562                 LOG_INFO("    Normal = ", vtos(norm), "\n");
563                 for(ni = 0; ni < n; ++ni)
564                 {
565                         vec = getsurfacepoint(e, si, ni);
566                         LOG_INFO("    Point ", ftos(ni), " = ", vtos(vec), " (", ftos(norm * vec), ")\n");
567                 }
568         }
569 }
570
571 void checkpoint_passed()
572 {SELFPARAM();
573         string oldmsg;
574         entity cp;
575
576         if(other.classname == "porto")
577         {
578                 // do not allow portalling through checkpoints
579                 trace_plane_normal = normalize(-1 * other.velocity);
580                 setself(other);
581                 W_Porto_Fail(0);
582                 return;
583         }
584
585         /*
586          * Trigger targets
587          */
588         if (!((self.spawnflags & 2) && (IS_PLAYER(other))))
589         {
590                 activator = other;
591                 oldmsg = self.message;
592                 self.message = "";
593                 SUB_UseTargets();
594                 self.message = oldmsg;
595         }
596
597         if (!IS_PLAYER(other))
598                 return;
599
600         /*
601          * Remove unauthorized equipment
602          */
603         Portal_ClearAll(other);
604
605         other.porto_forbidden = 2; // decreased by 1 each StartFrame
606
607         if(defrag_ents)
608         {
609                 if(self.race_checkpoint == -2)
610                 {
611                         self.race_checkpoint = other.race_checkpoint;
612                 }
613
614                 float largest_cp_id = 0;
615                 float cp_amount = 0;
616                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
617                 {
618                         cp_amount += 1;
619                         if(cp.race_checkpoint > largest_cp_id) // update the finish id if someone hit a new checkpoint
620                         {
621                                 largest_cp_id = cp.race_checkpoint;
622                                 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
623                                         cp.race_checkpoint = largest_cp_id + 1; // finish line
624                                 race_highest_checkpoint = largest_cp_id + 1;
625                                 race_timed_checkpoint = largest_cp_id + 1;
626
627                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
628                                 {
629                                         if(cp.race_checkpoint == -2) // set defragcpexists to -1 so that the cp id file will be rewritten when someone finishes
630                                                 defragcpexists = -1;
631                                 }
632                         }
633                 }
634                 if(cp_amount == 0)
635                 {
636                         for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
637                                 cp.race_checkpoint = 1;
638                         race_highest_checkpoint = 1;
639                         race_timed_checkpoint = 1;
640                 }
641         }
642
643         if((other.race_checkpoint == -1 && self.race_checkpoint == 0) || (other.race_checkpoint == self.race_checkpoint))
644         {
645                 if(self.race_penalty)
646                 {
647                         if(other.race_lastpenalty != self)
648                         {
649                                 other.race_lastpenalty = self;
650                                 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
651                         }
652                 }
653
654                 if(other.race_penalty)
655                         return;
656
657                 /*
658                  * Trigger targets
659                  */
660                 if(self.spawnflags & 2)
661                 {
662                         activator = other;
663                         oldmsg = self.message;
664                         self.message = "";
665                         SUB_UseTargets();
666                         self.message = oldmsg;
667                 }
668
669                 if(other.race_respawn_checkpoint != self.race_checkpoint || !other.race_started)
670                         other.race_respawn_spotref = self; // this is not a spot but a CP, but spawnpoint selection will deal with that
671                 other.race_respawn_checkpoint = self.race_checkpoint;
672                 other.race_checkpoint = race_NextCheckpoint(self.race_checkpoint);
673                 other.race_started = 1;
674
675                 race_SendTime(other, self.race_checkpoint, other.race_movetime, !!other.race_laptime);
676
677                 if(!self.race_checkpoint) // start line
678                 {
679                         other.race_laptime = time;
680                         other.race_movetime = other.race_movetime_frac = other.race_movetime_count = 0;
681                         other.race_penalty_accumulator = 0;
682                         other.race_lastpenalty = world;
683                 }
684
685                 if(g_race_qualifying)
686                         race_SendNextCheckpoint(other, 0);
687
688                 if(defrag_ents && defragcpexists < 0 && self.classname == "target_stopTimer")
689                 {
690                         float fh;
691                         defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_WRITE);
692                         if(fh >= 0)
693                         {
694                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
695                                 fputs(fh, strcat(cp.targetname, " ", ftos(cp.race_checkpoint), "\n"));
696                         }
697                         fclose(fh);
698                 }
699         }
700         else if(other.race_checkpoint == race_NextCheckpoint(self.race_checkpoint))
701         {
702                 // ignored
703         }
704         else
705         {
706                 if(self.spawnflags & 4)
707                         Damage (other, self, self, 10000, DEATH_HURTTRIGGER, other.origin, '0 0 0');
708         }
709 }
710
711 void checkpoint_touch()
712 {
713         EXACTTRIGGER_TOUCH;
714         checkpoint_passed();
715 }
716
717 void checkpoint_use()
718 {
719         if(other.classname == "info_player_deathmatch") // a spawn, a spawn
720                 return;
721
722         other = activator;
723         checkpoint_passed();
724 }
725
726 float race_waypointsprite_visible_for_player(entity e)
727 {SELFPARAM();
728         if(e.race_checkpoint == -1 || self.owner.race_checkpoint == -2)
729                 return true;
730         else if(e.race_checkpoint == self.owner.race_checkpoint)
731                 return true;
732         else
733                 return false;
734 }
735
736 float have_verified;
737 void trigger_race_checkpoint_verify()
738 {SELFPARAM();
739         entity cp;
740         float i, p;
741         float qual;
742
743         if(have_verified)
744                 return;
745         have_verified = 1;
746
747         qual = g_race_qualifying;
748
749         setself(spawn());
750         self.classname = "player";
751
752         if(g_race)
753         {
754                 for(i = 0; i <= race_highest_checkpoint; ++i)
755                 {
756                         self.race_checkpoint = race_NextCheckpoint(i);
757
758                         // race only (middle of the race)
759                         g_race_qualifying = 0;
760                         self.race_place = 0;
761                         if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), 0, false))
762                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for respawning in race) - bailing out"));
763
764                         if(i == 0)
765                         {
766                                 // qualifying only
767                                 g_race_qualifying = 1;
768                                 self.race_place = race_lowest_place_spawn;
769                                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), 0, false))
770                                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
771
772                                 // race only (initial spawn)
773                                 g_race_qualifying = 0;
774                                 for(p = 1; p <= race_highest_place_spawn; ++p)
775                                 {
776                                         self.race_place = p;
777                                         if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), 0, false))
778                                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for initially spawning in race) - bailing out"));
779                                 }
780                         }
781                 }
782         }
783         else if(!defrag_ents)
784         {
785                 // qualifying only
786                 self.race_checkpoint = race_NextCheckpoint(0);
787                 g_race_qualifying = 1;
788                 self.race_place = race_lowest_place_spawn;
789                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), 0, false))
790                         error(strcat("Checkpoint 0 misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
791         }
792         else
793         {
794                 self.race_checkpoint = race_NextCheckpoint(0);
795                 g_race_qualifying = 1;
796                 self.race_place = 0; // there's only one spawn on defrag maps
797
798                 // check if a defragcp file already exists, then read it and apply the checkpoint order
799                 float fh;
800                 float len;
801                 string l;
802
803                 defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_READ);
804                 if(fh >= 0)
805                 {
806                         while((l = fgets(fh)))
807                         {
808                                 len = tokenize_console(l);
809                                 if(len != 2)
810                                 {
811                                         defragcpexists = -1; // something's wrong in the defrag cp file, set defragcpexists to -1 so that it will be rewritten when someone finishes
812                                         continue;
813                                 }
814                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
815                                         if(argv(0) == cp.targetname)
816                                                 cp.race_checkpoint = stof(argv(1));
817                         }
818                         fclose(fh);
819                 }
820         }
821
822         g_race_qualifying = qual;
823
824         if(race_timed_checkpoint)
825         {
826                 if(defrag_ents)
827                 {
828                         for(cp = world; (cp = find(cp, classname, "target_startTimer"));)
829                                 WaypointSprite_UpdateSprites(cp.sprite, WP_RaceStart, WP_Null, WP_Null);
830                         for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
831                                 WaypointSprite_UpdateSprites(cp.sprite, WP_RaceFinish, WP_Null, WP_Null);
832
833                         for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
834                         {
835                                 if(cp.race_checkpoint == -2) // something's wrong with the defrag cp file or it has not been written yet, set defragcpexists to -1 so that it will be rewritten when someone finishes
836                                         defragcpexists = -1;
837                         }
838
839                         if(defragcpexists != -1)
840                         {
841                                 float largest_cp_id = 0;
842                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
843                                         if(cp.race_checkpoint > largest_cp_id)
844                                                 largest_cp_id = cp.race_checkpoint;
845                                 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
846                                         cp.race_checkpoint = largest_cp_id + 1; // finish line
847                                 race_highest_checkpoint = largest_cp_id + 1;
848                                 race_timed_checkpoint = largest_cp_id + 1;
849                         }
850                         else
851                         {
852                                 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
853                                         cp.race_checkpoint = 255; // finish line
854                                 race_highest_checkpoint = 255;
855                                 race_timed_checkpoint = 255;
856                         }
857                 }
858                 else
859                 {
860                         for(cp = world; (cp = find(cp, classname, "trigger_race_checkpoint")); )
861                                 if(cp.sprite)
862                                 {
863                                         if(cp.race_checkpoint == 0)
864                                                 WaypointSprite_UpdateSprites(cp.sprite, WP_RaceStart, WP_Null, WP_Null);
865                                         else if(cp.race_checkpoint == race_timed_checkpoint)
866                                                 WaypointSprite_UpdateSprites(cp.sprite, WP_RaceFinish, WP_Null, WP_Null);
867                                 }
868                 }
869         }
870
871         if(defrag_ents)
872         {
873                 entity trigger, targ;
874                 for(trigger = world; (trigger = find(trigger, classname, "trigger_multiple")); )
875                         for(targ = world; (targ = find(targ, targetname, trigger.target)); )
876                                 if (targ.classname == "target_checkpoint" || targ.classname == "target_startTimer" || targ.classname == "target_stopTimer")
877                                 {
878                                         trigger.wait = 0;
879                                         trigger.delay = 0;
880                                         targ.wait = 0;
881                                         targ.delay = 0;
882
883                     // These just make the game crash on some maps with oddly shaped triggers.
884                     // (on the other hand they used to fix the case when two players ran through a checkpoint at once,
885                     // and often one of them just passed through without being registered. Hope it's fixed  in a better way now.
886                     // (happened on item triggers too)
887                     //
888                                         //targ.wait = -2;
889                                         //targ.delay = 0;
890
891                                         //setsize(targ, trigger.mins, trigger.maxs);
892                                         //setorigin(targ, trigger.origin);
893                                         //remove(trigger);
894                                 }
895         }
896         remove(self);
897         setself(this);
898 }
899
900 vector trigger_race_checkpoint_spawn_evalfunc(entity player, entity spot, vector current)
901 {SELFPARAM();
902         if(g_race_qualifying)
903         {
904                 // spawn at first
905                 if(self.race_checkpoint != 0)
906                         return '-1 0 0';
907                 if(spot.race_place != race_lowest_place_spawn)
908                         return '-1 0 0';
909         }
910         else
911         {
912                 if(self.race_checkpoint != player.race_respawn_checkpoint)
913                         return '-1 0 0';
914                 // try reusing the previous spawn
915                 if(self == player.race_respawn_spotref || spot == player.race_respawn_spotref)
916                         current.x += SPAWN_PRIO_RACE_PREVIOUS_SPAWN;
917                 if(self.race_checkpoint == 0)
918                 {
919                         float pl;
920                         pl = player.race_place;
921                         if(pl > race_highest_place_spawn)
922                                 pl = 0;
923                         if(pl == 0 && !player.race_started)
924                                 pl = race_highest_place_spawn; // use last place if he has not even touched finish yet
925                         if(spot.race_place != pl)
926                                 return '-1 0 0';
927                 }
928         }
929         return current;
930 }
931
932 spawnfunc(trigger_race_checkpoint)
933 {
934         vector o;
935         if(!g_race && !g_cts) { remove(self); return; }
936
937         EXACTTRIGGER_INIT;
938
939         self.use = checkpoint_use;
940         if (!(self.spawnflags & 1))
941                 self.touch = checkpoint_touch;
942
943         o = (self.absmin + self.absmax) * 0.5;
944         tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o.z - self.absmin.z), MOVE_NORMAL, self);
945         waypoint_spawnforitem_force(self, trace_endpos);
946         self.nearestwaypointtimeout = time + 1000000000;
947
948         if(self.message == "")
949                 self.message = "went backwards";
950         if (self.message2 == "")
951                 self.message2 = "was pushed backwards by";
952         if (self.race_penalty_reason == "")
953                 self.race_penalty_reason = "missing a checkpoint";
954
955         self.race_checkpoint = self.cnt;
956
957         if(self.race_checkpoint > race_highest_checkpoint)
958         {
959                 race_highest_checkpoint = self.race_checkpoint;
960                 if(self.spawnflags & 8)
961                         race_timed_checkpoint = self.race_checkpoint;
962                 else
963                         race_timed_checkpoint = 0;
964         }
965
966         if(!self.race_penalty)
967         {
968                 if(self.race_checkpoint)
969                         WaypointSprite_SpawnFixed(WP_RaceCheckpoint, o, self, sprite, RADARICON_NONE);
970                 else
971                         WaypointSprite_SpawnFixed(WP_RaceStartFinish, o, self, sprite, RADARICON_NONE);
972         }
973
974         self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
975         self.spawn_evalfunc = trigger_race_checkpoint_spawn_evalfunc;
976
977         InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
978 }
979
980 spawnfunc(target_checkpoint) // defrag entity
981 {
982         vector o;
983         if(!g_race && !g_cts) { remove(self); return; }
984         defrag_ents = 1;
985
986         EXACTTRIGGER_INIT;
987
988         self.use = checkpoint_use;
989         if (!(self.spawnflags & 1))
990                 self.touch = checkpoint_touch;
991
992         o = (self.absmin + self.absmax) * 0.5;
993         tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o.z - self.absmin.z), MOVE_NORMAL, self);
994         waypoint_spawnforitem_force(self, trace_endpos);
995         self.nearestwaypointtimeout = time + 1000000000;
996
997         if(self.message == "")
998                 self.message = "went backwards";
999         if (self.message2 == "")
1000                 self.message2 = "was pushed backwards by";
1001         if (self.race_penalty_reason == "")
1002                 self.race_penalty_reason = "missing a checkpoint";
1003
1004         if(self.classname == "target_startTimer")
1005                 self.race_checkpoint = 0;
1006         else
1007                 self.race_checkpoint = -2;
1008
1009         race_timed_checkpoint = 1;
1010
1011         if(self.race_checkpoint == 0)
1012                 WaypointSprite_SpawnFixed(WP_RaceStart, o, self, sprite, RADARICON_NONE);
1013         else
1014                 WaypointSprite_SpawnFixed(WP_RaceCheckpoint, o, self, sprite, RADARICON_NONE);
1015
1016         self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
1017
1018         InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
1019 }
1020
1021 spawnfunc(target_startTimer) { spawnfunc_target_checkpoint(this); }
1022 spawnfunc(target_stopTimer) { spawnfunc_target_checkpoint(this); }
1023
1024 void race_AbandonRaceCheck(entity p)
1025 {
1026         if(race_completing && !p.race_completed)
1027         {
1028                 p.race_completed = 1;
1029                 MAKE_INDEPENDENT_PLAYER(p);
1030                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_RACE_ABANDONED, p.netname);
1031                 ClientData_Touch(p);
1032         }
1033 }
1034
1035 void race_StartCompleting()
1036 {
1037         entity p;
1038         race_completing = 1;
1039         FOR_EACH_PLAYER(p)
1040                 if(p.deadflag != DEAD_NO)
1041                         race_AbandonRaceCheck(p);
1042 }
1043
1044 void race_PreparePlayer()
1045 {SELFPARAM();
1046         race_ClearTime(self);
1047         self.race_place = 0;
1048         self.race_started = 0;
1049         self.race_respawn_checkpoint = 0;
1050         self.race_respawn_spotref = world;
1051 }
1052
1053 void race_RetractPlayer()
1054 {SELFPARAM();
1055         if(!g_race && !g_cts)
1056                 return;
1057         if(self.race_respawn_checkpoint == 0 || self.race_respawn_checkpoint == race_timed_checkpoint)
1058                 race_ClearTime(self);
1059         self.race_checkpoint = self.race_respawn_checkpoint;
1060 }
1061
1062 spawnfunc(info_player_race)
1063 {
1064         if(!g_race && !g_cts) { remove(self); return; }
1065         ++race_spawns;
1066         spawnfunc_info_player_deathmatch(this);
1067
1068         if(self.race_place > race_highest_place_spawn)
1069                 race_highest_place_spawn = self.race_place;
1070         if(self.race_place < race_lowest_place_spawn)
1071                 race_lowest_place_spawn = self.race_place;
1072 }
1073
1074 void race_ClearRecords()
1075 {SELFPARAM();
1076         float i;
1077
1078         for(i = 0; i < MAX_CHECKPOINTS; ++i)
1079         {
1080                 race_checkpoint_records[i] = 0;
1081                 if(race_checkpoint_recordholders[i])
1082                         strunzone(race_checkpoint_recordholders[i]);
1083                 race_checkpoint_recordholders[i] = string_null;
1084         }
1085
1086         entity e;
1087         FOR_EACH_CLIENT(e)
1088         {
1089                 float p = e.race_place;
1090                 WITH(entity, self, e, race_PreparePlayer());
1091                 e.race_place = p;
1092         }
1093 }
1094
1095 void race_ImposePenaltyTime(entity pl, float penalty, string reason)
1096 {
1097         if(g_race_qualifying)
1098         {
1099                 pl.race_penalty_accumulator += penalty;
1100                 if(IS_REAL_CLIENT(pl))
1101                 {
1102                         msg_entity = pl;
1103                         WRITESPECTATABLE_MSG_ONE({
1104                                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
1105                                 WriteByte(MSG_ONE, TE_CSQC_RACE);
1106                                 WriteByte(MSG_ONE, RACE_NET_PENALTY_QUALIFYING);
1107                                 WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1108                                 WriteString(MSG_ONE, reason);
1109                         });
1110                 }
1111         }
1112         else
1113         {
1114                 pl.race_penalty = time + penalty;
1115                 if(IS_REAL_CLIENT(pl))
1116                 {
1117                         msg_entity = pl;
1118                         WRITESPECTATABLE_MSG_ONE_VARNAME(dummy, {
1119                                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
1120                                 WriteByte(MSG_ONE, TE_CSQC_RACE);
1121                                 WriteByte(MSG_ONE, RACE_NET_PENALTY_RACE);
1122                                 WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1123                                 WriteString(MSG_ONE, reason);
1124                         });
1125                 }
1126         }
1127 }
1128
1129 void penalty_touch()
1130 {SELFPARAM();
1131         EXACTTRIGGER_TOUCH;
1132         if(other.race_lastpenalty != self)
1133         {
1134                 other.race_lastpenalty = self;
1135                 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
1136         }
1137 }
1138
1139 void penalty_use()
1140 {SELFPARAM();
1141         race_ImposePenaltyTime(activator, self.race_penalty, self.race_penalty_reason);
1142 }
1143
1144 spawnfunc(trigger_race_penalty)
1145 {
1146         EXACTTRIGGER_INIT;
1147
1148         self.use = penalty_use;
1149         if (!(self.spawnflags & 1))
1150                 self.touch = penalty_touch;
1151
1152         if (self.race_penalty_reason == "")
1153                 self.race_penalty_reason = "missing a checkpoint";
1154         if (!self.race_penalty)
1155                 self.race_penalty = 5;
1156 }
1157
1158 float race_GetFractionalLapCount(entity e)
1159 {
1160         // interesting metrics (idea by KrimZon) to maybe sort players in the
1161         // scoreboard, immediately updates when overtaking
1162         //
1163         // requires the track to be built so you never get farther away from the
1164         // next checkpoint, though, and current Xonotic race maps are not built that
1165         // way
1166         //
1167         // also, this code is slow and would need optimization (i.e. "next CP"
1168         // links on CP entities)
1169
1170         float l;
1171         l = PlayerScore_Add(e, SP_RACE_LAPS, 0);
1172         if(e.race_completed)
1173                 return l; // not fractional
1174
1175         vector o0, o1;
1176         float bestfraction, fraction;
1177         entity lastcp, cp0, cp1;
1178         float nextcpindex, lastcpindex;
1179
1180         nextcpindex = max(e.race_checkpoint, 0);
1181         lastcpindex = e.race_respawn_checkpoint;
1182         lastcp = e.race_respawn_spotref;
1183
1184         if(nextcpindex == lastcpindex)
1185                 return l; // finish
1186
1187         bestfraction = 1;
1188         for(cp0 = world; (cp0 = find(cp0, classname, "trigger_race_checkpoint")); )
1189         {
1190                 if(cp0.race_checkpoint != lastcpindex)
1191                         continue;
1192                 if(lastcp)
1193                         if(cp0 != lastcp)
1194                                 continue;
1195                 o0 = (cp0.absmin + cp0.absmax) * 0.5;
1196                 for(cp1 = world; (cp1 = find(cp1, classname, "trigger_race_checkpoint")); )
1197                 {
1198                         if(cp1.race_checkpoint != nextcpindex)
1199                                 continue;
1200                         o1 = (cp1.absmin + cp1.absmax) * 0.5;
1201                         if(o0 == o1)
1202                                 continue;
1203                         fraction = bound(0.0001, vlen(e.origin - o1) / vlen(o0 - o1), 1);
1204                         if(fraction < bestfraction)
1205                                 bestfraction = fraction;
1206                 }
1207         }
1208
1209         // we are at CP "nextcpindex - bestfraction"
1210         // race_timed_checkpoint == 4: then nextcp==4 means 0.9999x, nextcp==0 means 0.0000x
1211         // race_timed_checkpoint == 0: then nextcp==0 means 0.9999x
1212         float c, nc;
1213         nc = race_highest_checkpoint + 1;
1214         c = ((nextcpindex - race_timed_checkpoint + nc + nc - 1) % nc) + 1 - bestfraction;
1215
1216         return l + c / nc;
1217 }