]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/race.qc
Merge branch 'master' into terencehill/quickmenu
[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 void 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         print("Surfaces of ", etos(e), ":\n");
552
553         print("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                 print("  Surface ", ftos(si), ":\n");
561                 norm = getsurfacenormal(e, si);
562                 print("    Normal = ", vtos(norm), "\n");
563                 for(ni = 0; ni < n; ++ni)
564                 {
565                         vec = getsurfacepoint(e, si, ni);
566                         print("    Point ", ftos(ni), " = ", vtos(vec), " (", ftos(norm * vec), ")\n");
567                 }
568         }
569 }
570
571 void checkpoint_passed()
572 {
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                 self = 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 {
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 {
739         entity oldself, 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         oldself = self;
750         self = spawn();
751         self.classname = "player";
752
753         if(g_race)
754         {
755                 for(i = 0; i <= race_highest_checkpoint; ++i)
756                 {
757                         self.race_checkpoint = race_NextCheckpoint(i);
758
759                         // race only (middle of the race)
760                         g_race_qualifying = 0;
761                         self.race_place = 0;
762                         if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), 0, false))
763                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for respawning in race) - bailing out"));
764
765                         if(i == 0)
766                         {
767                                 // qualifying only
768                                 g_race_qualifying = 1;
769                                 self.race_place = race_lowest_place_spawn;
770                                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), 0, false))
771                                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
772
773                                 // race only (initial spawn)
774                                 g_race_qualifying = 0;
775                                 for(p = 1; p <= race_highest_place_spawn; ++p)
776                                 {
777                                         self.race_place = p;
778                                         if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), 0, false))
779                                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for initially spawning in race) - bailing out"));
780                                 }
781                         }
782                 }
783         }
784         else if(!defrag_ents)
785         {
786                 // qualifying only
787                 self.race_checkpoint = race_NextCheckpoint(0);
788                 g_race_qualifying = 1;
789                 self.race_place = race_lowest_place_spawn;
790                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), 0, false))
791                         error(strcat("Checkpoint 0 misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
792         }
793         else
794         {
795                 self.race_checkpoint = race_NextCheckpoint(0);
796                 g_race_qualifying = 1;
797                 self.race_place = 0; // there's only one spawn on defrag maps
798
799                 // check if a defragcp file already exists, then read it and apply the checkpoint order
800                 float fh;
801                 float len;
802                 string l;
803
804                 defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_READ);
805                 if(fh >= 0)
806                 {
807                         while((l = fgets(fh)))
808                         {
809                                 len = tokenize_console(l);
810                                 if(len != 2)
811                                 {
812                                         defragcpexists = -1; // something's wrong in the defrag cp file, set defragcpexists to -1 so that it will be rewritten when someone finishes
813                                         continue;
814                                 }
815                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
816                                         if(argv(0) == cp.targetname)
817                                                 cp.race_checkpoint = stof(argv(1));
818                         }
819                         fclose(fh);
820                 }
821         }
822
823         g_race_qualifying = qual;
824
825         if(race_timed_checkpoint)
826         {
827                 if(defrag_ents)
828                 {
829                         for(cp = world; (cp = find(cp, classname, "target_startTimer"));)
830                                 WaypointSprite_UpdateSprites(cp.sprite, WP_RaceStart, WP_Null, WP_Null);
831                         for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
832                                 WaypointSprite_UpdateSprites(cp.sprite, WP_RaceFinish, WP_Null, WP_Null);
833
834                         for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
835                         {
836                                 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
837                                         defragcpexists = -1;
838                         }
839
840                         if(defragcpexists != -1)
841                         {
842                                 float largest_cp_id = 0;
843                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
844                                         if(cp.race_checkpoint > largest_cp_id)
845                                                 largest_cp_id = cp.race_checkpoint;
846                                 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
847                                         cp.race_checkpoint = largest_cp_id + 1; // finish line
848                                 race_highest_checkpoint = largest_cp_id + 1;
849                                 race_timed_checkpoint = largest_cp_id + 1;
850                         }
851                         else
852                         {
853                                 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
854                                         cp.race_checkpoint = 255; // finish line
855                                 race_highest_checkpoint = 255;
856                                 race_timed_checkpoint = 255;
857                         }
858                 }
859                 else
860                 {
861                         for(cp = world; (cp = find(cp, classname, "trigger_race_checkpoint")); )
862                                 if(cp.sprite)
863                                 {
864                                         if(cp.race_checkpoint == 0)
865                                                 WaypointSprite_UpdateSprites(cp.sprite, WP_RaceStart, WP_Null, WP_Null);
866                                         else if(cp.race_checkpoint == race_timed_checkpoint)
867                                                 WaypointSprite_UpdateSprites(cp.sprite, WP_RaceFinish, WP_Null, WP_Null);
868                                 }
869                 }
870         }
871
872         if(defrag_ents)
873         {
874                 entity trigger, targ;
875                 for(trigger = world; (trigger = find(trigger, classname, "trigger_multiple")); )
876                         for(targ = world; (targ = find(targ, targetname, trigger.target)); )
877                                 if (targ.classname == "target_checkpoint" || targ.classname == "target_startTimer" || targ.classname == "target_stopTimer")
878                                 {
879                                         trigger.wait = 0;
880                                         trigger.delay = 0;
881                                         targ.wait = 0;
882                                         targ.delay = 0;
883
884                     // These just make the game crash on some maps with oddly shaped triggers.
885                     // (on the other hand they used to fix the case when two players ran through a checkpoint at once,
886                     // and often one of them just passed through without being registered. Hope it's fixed  in a better way now.
887                     // (happened on item triggers too)
888                     //
889                                         //targ.wait = -2;
890                                         //targ.delay = 0;
891
892                                         //setsize(targ, trigger.mins, trigger.maxs);
893                                         //setorigin(targ, trigger.origin);
894                                         //remove(trigger);
895                                 }
896         }
897         remove(self);
898         self = oldself;
899 }
900
901 vector trigger_race_checkpoint_spawn_evalfunc(entity player, entity spot, vector current)
902 {
903         if(g_race_qualifying)
904         {
905                 // spawn at first
906                 if(self.race_checkpoint != 0)
907                         return '-1 0 0';
908                 if(spot.race_place != race_lowest_place_spawn)
909                         return '-1 0 0';
910         }
911         else
912         {
913                 if(self.race_checkpoint != player.race_respawn_checkpoint)
914                         return '-1 0 0';
915                 // try reusing the previous spawn
916                 if(self == player.race_respawn_spotref || spot == player.race_respawn_spotref)
917                         current.x += SPAWN_PRIO_RACE_PREVIOUS_SPAWN;
918                 if(self.race_checkpoint == 0)
919                 {
920                         float pl;
921                         pl = player.race_place;
922                         if(pl > race_highest_place_spawn)
923                                 pl = 0;
924                         if(pl == 0 && !player.race_started)
925                                 pl = race_highest_place_spawn; // use last place if he has not even touched finish yet
926                         if(spot.race_place != pl)
927                                 return '-1 0 0';
928                 }
929         }
930         return current;
931 }
932
933 void spawnfunc_trigger_race_checkpoint()
934 {
935         vector o;
936         if(!g_race && !g_cts) { remove(self); return; }
937
938         EXACTTRIGGER_INIT;
939
940         self.use = checkpoint_use;
941         if (!(self.spawnflags & 1))
942                 self.touch = checkpoint_touch;
943
944         o = (self.absmin + self.absmax) * 0.5;
945         tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o.z - self.absmin.z), MOVE_NORMAL, self);
946         waypoint_spawnforitem_force(self, trace_endpos);
947         self.nearestwaypointtimeout = time + 1000000000;
948
949         if(self.message == "")
950                 self.message = "went backwards";
951         if (self.message2 == "")
952                 self.message2 = "was pushed backwards by";
953         if (self.race_penalty_reason == "")
954                 self.race_penalty_reason = "missing a checkpoint";
955
956         self.race_checkpoint = self.cnt;
957
958         if(self.race_checkpoint > race_highest_checkpoint)
959         {
960                 race_highest_checkpoint = self.race_checkpoint;
961                 if(self.spawnflags & 8)
962                         race_timed_checkpoint = self.race_checkpoint;
963                 else
964                         race_timed_checkpoint = 0;
965         }
966
967         if(!self.race_penalty)
968         {
969                 if(self.race_checkpoint)
970                         WaypointSprite_SpawnFixed(WP_RaceCheckpoint, o, self, sprite, RADARICON_NONE);
971                 else
972                         WaypointSprite_SpawnFixed(WP_RaceStartFinish, o, self, sprite, RADARICON_NONE);
973         }
974
975         self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
976         self.spawn_evalfunc = trigger_race_checkpoint_spawn_evalfunc;
977
978         InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
979 }
980
981 void spawnfunc_target_checkpoint() // defrag entity
982 {
983         vector o;
984         if(!g_race && !g_cts) { remove(self); return; }
985         defrag_ents = 1;
986
987         EXACTTRIGGER_INIT;
988
989         self.use = checkpoint_use;
990         if (!(self.spawnflags & 1))
991                 self.touch = checkpoint_touch;
992
993         o = (self.absmin + self.absmax) * 0.5;
994         tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o.z - self.absmin.z), MOVE_NORMAL, self);
995         waypoint_spawnforitem_force(self, trace_endpos);
996         self.nearestwaypointtimeout = time + 1000000000;
997
998         if(self.message == "")
999                 self.message = "went backwards";
1000         if (self.message2 == "")
1001                 self.message2 = "was pushed backwards by";
1002         if (self.race_penalty_reason == "")
1003                 self.race_penalty_reason = "missing a checkpoint";
1004
1005         if(self.classname == "target_startTimer")
1006                 self.race_checkpoint = 0;
1007         else
1008                 self.race_checkpoint = -2;
1009
1010         race_timed_checkpoint = 1;
1011
1012         if(self.race_checkpoint == 0)
1013                 WaypointSprite_SpawnFixed(WP_RaceStart, o, self, sprite, RADARICON_NONE);
1014         else
1015                 WaypointSprite_SpawnFixed(WP_RaceCheckpoint, o, self, sprite, RADARICON_NONE);
1016
1017         self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
1018
1019         InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
1020 }
1021
1022 void spawnfunc_target_startTimer() { spawnfunc_target_checkpoint(); }
1023 void spawnfunc_target_stopTimer() { spawnfunc_target_checkpoint(); }
1024
1025 void race_AbandonRaceCheck(entity p)
1026 {
1027         if(race_completing && !p.race_completed)
1028         {
1029                 p.race_completed = 1;
1030                 MAKE_INDEPENDENT_PLAYER(p);
1031                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_RACE_ABANDONED, p.netname);
1032                 ClientData_Touch(p);
1033         }
1034 }
1035
1036 void race_StartCompleting()
1037 {
1038         entity p;
1039         race_completing = 1;
1040         FOR_EACH_PLAYER(p)
1041                 if(p.deadflag != DEAD_NO)
1042                         race_AbandonRaceCheck(p);
1043 }
1044
1045 void race_PreparePlayer()
1046 {
1047         race_ClearTime(self);
1048         self.race_place = 0;
1049         self.race_started = 0;
1050         self.race_respawn_checkpoint = 0;
1051         self.race_respawn_spotref = world;
1052 }
1053
1054 void race_RetractPlayer()
1055 {
1056         if(!g_race && !g_cts)
1057                 return;
1058         if(self.race_respawn_checkpoint == 0 || self.race_respawn_checkpoint == race_timed_checkpoint)
1059                 race_ClearTime(self);
1060         self.race_checkpoint = self.race_respawn_checkpoint;
1061 }
1062
1063 void spawnfunc_info_player_race (void)
1064 {
1065         if(!g_race && !g_cts) { remove(self); return; }
1066         ++race_spawns;
1067         spawnfunc_info_player_deathmatch();
1068
1069         if(self.race_place > race_highest_place_spawn)
1070                 race_highest_place_spawn = self.race_place;
1071         if(self.race_place < race_lowest_place_spawn)
1072                 race_lowest_place_spawn = self.race_place;
1073 }
1074
1075 void race_ClearRecords()
1076 {
1077         float i;
1078         entity e;
1079
1080         for(i = 0; i < MAX_CHECKPOINTS; ++i)
1081         {
1082                 race_checkpoint_records[i] = 0;
1083                 if(race_checkpoint_recordholders[i])
1084                         strunzone(race_checkpoint_recordholders[i]);
1085                 race_checkpoint_recordholders[i] = string_null;
1086         }
1087
1088         e = self;
1089         FOR_EACH_CLIENT(self)
1090         {
1091                 float p;
1092                 p = self.race_place;
1093                 race_PreparePlayer();
1094                 self.race_place = p;
1095         }
1096         self = e;
1097 }
1098
1099 void race_ImposePenaltyTime(entity pl, float penalty, string reason)
1100 {
1101         if(g_race_qualifying)
1102         {
1103                 pl.race_penalty_accumulator += penalty;
1104                 if(IS_REAL_CLIENT(pl))
1105                 {
1106                         msg_entity = pl;
1107                         WRITESPECTATABLE_MSG_ONE({
1108                                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
1109                                 WriteByte(MSG_ONE, TE_CSQC_RACE);
1110                                 WriteByte(MSG_ONE, RACE_NET_PENALTY_QUALIFYING);
1111                                 WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1112                                 WriteString(MSG_ONE, reason);
1113                         });
1114                 }
1115         }
1116         else
1117         {
1118                 pl.race_penalty = time + penalty;
1119                 if(IS_REAL_CLIENT(pl))
1120                 {
1121                         msg_entity = pl;
1122                         WRITESPECTATABLE_MSG_ONE_VARNAME(dummy, {
1123                                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
1124                                 WriteByte(MSG_ONE, TE_CSQC_RACE);
1125                                 WriteByte(MSG_ONE, RACE_NET_PENALTY_RACE);
1126                                 WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1127                                 WriteString(MSG_ONE, reason);
1128                         });
1129                 }
1130         }
1131 }
1132
1133 void penalty_touch()
1134 {
1135         EXACTTRIGGER_TOUCH;
1136         if(other.race_lastpenalty != self)
1137         {
1138                 other.race_lastpenalty = self;
1139                 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
1140         }
1141 }
1142
1143 void penalty_use()
1144 {
1145         race_ImposePenaltyTime(activator, self.race_penalty, self.race_penalty_reason);
1146 }
1147
1148 void spawnfunc_trigger_race_penalty()
1149 {
1150         EXACTTRIGGER_INIT;
1151
1152         self.use = penalty_use;
1153         if (!(self.spawnflags & 1))
1154                 self.touch = penalty_touch;
1155
1156         if (self.race_penalty_reason == "")
1157                 self.race_penalty_reason = "missing a checkpoint";
1158         if (!self.race_penalty)
1159                 self.race_penalty = 5;
1160 }
1161
1162 float race_GetFractionalLapCount(entity e)
1163 {
1164         // interesting metrics (idea by KrimZon) to maybe sort players in the
1165         // scoreboard, immediately updates when overtaking
1166         //
1167         // requires the track to be built so you never get farther away from the
1168         // next checkpoint, though, and current Xonotic race maps are not built that
1169         // way
1170         //
1171         // also, this code is slow and would need optimization (i.e. "next CP"
1172         // links on CP entities)
1173
1174         float l;
1175         l = PlayerScore_Add(e, SP_RACE_LAPS, 0);
1176         if(e.race_completed)
1177                 return l; // not fractional
1178
1179         vector o0, o1;
1180         float bestfraction, fraction;
1181         entity lastcp, cp0, cp1;
1182         float nextcpindex, lastcpindex;
1183
1184         nextcpindex = max(e.race_checkpoint, 0);
1185         lastcpindex = e.race_respawn_checkpoint;
1186         lastcp = e.race_respawn_spotref;
1187
1188         if(nextcpindex == lastcpindex)
1189                 return l; // finish
1190
1191         bestfraction = 1;
1192         for(cp0 = world; (cp0 = find(cp0, classname, "trigger_race_checkpoint")); )
1193         {
1194                 if(cp0.race_checkpoint != lastcpindex)
1195                         continue;
1196                 if(lastcp)
1197                         if(cp0 != lastcp)
1198                                 continue;
1199                 o0 = (cp0.absmin + cp0.absmax) * 0.5;
1200                 for(cp1 = world; (cp1 = find(cp1, classname, "trigger_race_checkpoint")); )
1201                 {
1202                         if(cp1.race_checkpoint != nextcpindex)
1203                                 continue;
1204                         o1 = (cp1.absmin + cp1.absmax) * 0.5;
1205                         if(o0 == o1)
1206                                 continue;
1207                         fraction = bound(0.0001, vlen(e.origin - o1) / vlen(o0 - o1), 1);
1208                         if(fraction < bestfraction)
1209                                 bestfraction = fraction;
1210                 }
1211         }
1212
1213         // we are at CP "nextcpindex - bestfraction"
1214         // race_timed_checkpoint == 4: then nextcp==4 means 0.9999x, nextcp==0 means 0.0000x
1215         // race_timed_checkpoint == 0: then nextcp==0 means 0.9999x
1216         float c, nc;
1217         nc = race_highest_checkpoint + 1;
1218         c = ((nextcpindex - race_timed_checkpoint + nc + nc - 1) % nc) + 1 - bestfraction;
1219
1220         return l + c / nc;
1221 }