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