]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/race.qc
reimplement the cts kill delay from fruitiex/rankings-recode. This is a feature that...
[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                                 {
445                                         race_SetTime(e, t, recordtime);
446                                         if(g_cts && cvar("g_cts_finish_kill_delay"))
447                                         {
448                                                 CTS_ClientKill(cvar("g_cts_finish_kill_delay"));
449                                         }
450
451                                 if(t < recordtime || recordtime == 0)
452                                 {
453                                         race_checkpoint_records[cp] = t;
454                                         if(race_checkpoint_recordholders[cp])
455                                                 strunzone(race_checkpoint_recordholders[cp]);
456                                         race_checkpoint_recordholders[cp] = strzone(e.netname);
457                                         if(g_race_qualifying)
458                                         {
459                                                 FOR_EACH_REALPLAYER(p)
460                                                         if(p.race_checkpoint == cp)
461                                                                 race_SendNextCheckpoint(p, 0);
462                                         }
463                                 }
464                         }
465                 }
466                 else
467                 {
468                         // dummies
469                         t = 0;
470                         recordtime = 0;
471                         recordholder = "";
472                 }
473
474                 msg_entity = e;
475                 if(g_race_qualifying)
476                 {
477                         WRITESPECTATABLE_MSG_ONE_VARNAME(dummy1, {
478                                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
479                                 WriteByte(MSG_ONE, TE_CSQC_RACE);
480                                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_QUALIFYING);
481                                 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
482                                 WriteInt24_t(MSG_ONE, t); // time to that intermediate
483                                 WriteInt24_t(MSG_ONE, recordtime); // previously best time
484                                 WriteString(MSG_ONE, recordholder); // record holder
485                         });
486                 }
487         }
488         else // RACE! Not Qualifying
489         {
490                 float lself, lother, othtime;
491                 entity oth;
492                 oth = race_checkpoint_lastplayers[cp];
493                 if(oth)
494                 {
495                         lself = PlayerScore_Add(e, SP_RACE_LAPS, 0);
496                         lother = race_checkpoint_lastlaps[cp];
497                         othtime = race_checkpoint_lasttimes[cp];
498                 }
499                 else
500                         lself = lother = othtime = 0;
501
502                 msg_entity = e;
503                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy2, {
504                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
505                         WriteByte(MSG_ONE, TE_CSQC_RACE);
506                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE);
507                         WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
508                         if(e == oth)
509                         {
510                                 WriteInt24_t(MSG_ONE, 0);
511                                 WriteByte(MSG_ONE, 0);
512                                 WriteString(MSG_ONE, "");
513                         }
514                         else
515                         {
516                                 WriteInt24_t(MSG_ONE, TIME_ENCODE(time - race_checkpoint_lasttimes[cp]));
517                                 WriteByte(MSG_ONE, lself - lother);
518                                 WriteString(MSG_ONE, oth.netname); // record holder
519                         }
520                 });
521
522                 race_checkpoint_lastplayers[cp] = e;
523                 race_checkpoint_lasttimes[cp] = time;
524                 race_checkpoint_lastlaps[cp] = lself;
525
526                 msg_entity = oth;
527                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy3, {
528                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
529                         WriteByte(MSG_ONE, TE_CSQC_RACE);
530                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE_BY_OPPONENT);
531                         WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
532                         if(e == oth)
533                         {
534                                 WriteInt24_t(MSG_ONE, 0);
535                                 WriteByte(MSG_ONE, 0);
536                                 WriteString(MSG_ONE, "");
537                         }
538                         else
539                         {
540                                 WriteInt24_t(MSG_ONE, TIME_ENCODE(time - othtime));
541                                 WriteByte(MSG_ONE, lother - lself);
542                                 WriteString(MSG_ONE, e.netname); // record holder
543                         }
544                 });
545         }
546 }
547
548 void race_ClearTime(entity e)
549 {
550         e.race_checkpoint = 0;
551         e.race_laptime = 0;
552         e.race_movetime = e.race_movetime_frac = e.race_movetime_count = 0;
553         e.race_penalty_accumulator = 0;
554         e.race_lastpenalty = world;
555
556         msg_entity = e;
557         WRITESPECTATABLE_MSG_ONE({
558                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
559                 WriteByte(MSG_ONE, TE_CSQC_RACE);
560                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_CLEAR); // next
561         });
562 }
563
564 void dumpsurface(entity e)
565 {
566         float n, si, ni;
567         vector norm, vec;
568         print("Surfaces of ", etos(e), ":\n");
569
570         print("TEST = ", ftos(getsurfacenearpoint(e, '0 0 0')), "\n");
571
572         for(si = 0; ; ++si)
573         {
574                 n = getsurfacenumpoints(e, si);
575                 if(n <= 0)
576                         break;
577                 print("  Surface ", ftos(si), ":\n");
578                 norm = getsurfacenormal(e, si);
579                 print("    Normal = ", vtos(norm), "\n");
580                 for(ni = 0; ni < n; ++ni)
581                 {
582                         vec = getsurfacepoint(e, si, ni);
583                         print("    Point ", ftos(ni), " = ", vtos(vec), " (", ftos(norm * vec), ")\n");
584                 }
585         }
586 }
587
588 void checkpoint_passed()
589 {
590         string oldmsg;
591         entity cp;
592
593         if(other.classname == "porto")
594         {
595                 // do not allow portalling through checkpoints
596                 trace_plane_normal = normalize(-1 * other.velocity);
597                 self = other;
598                 W_Porto_Fail(0);
599                 return;
600         }
601
602         /*
603          * Trigger targets
604          */
605         if not((self.spawnflags & 2) && (other.classname == "player"))
606         {
607                 activator = other;
608                 oldmsg = self.message;
609                 self.message = "";
610                 SUB_UseTargets();
611                 self.message = oldmsg;
612         }
613
614         if(other.classname != "player")
615                 return;
616
617         /*
618          * Remove unauthorized equipment
619          */
620         Portal_ClearAll(other);
621
622         other.porto_forbidden = 2; // decreased by 1 each StartFrame
623
624         if(defrag_ents) {
625                 if(self.race_checkpoint == -2) 
626                 {
627                         self.race_checkpoint = other.race_checkpoint;
628                 }
629
630                 float largest_cp_id;
631                 float cp_amount;
632                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
633                         cp_amount += 1;
634                         if(cp.race_checkpoint > largest_cp_id) // update the finish id if someone hit a new checkpoint
635                         {
636                                 largest_cp_id = cp.race_checkpoint;
637                                 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
638                                         cp.race_checkpoint = largest_cp_id + 1; // finish line
639                                 race_highest_checkpoint = largest_cp_id + 1;
640                                 race_timed_checkpoint = largest_cp_id + 1;
641
642                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
643                                         if(cp.race_checkpoint == -2) // set defragcpexists to -1 so that the cp id file will be rewritten when someone finishes
644                                                 defragcpexists = -1;
645                                 }       
646                         }
647                 }
648                 if(cp_amount == 0) {
649                         for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
650                                 cp.race_checkpoint = 1;
651                         race_highest_checkpoint = 1;
652                         race_timed_checkpoint = 1;
653                 }
654         }
655
656         if((other.race_checkpoint == -1 && self.race_checkpoint == 0) || (other.race_checkpoint == self.race_checkpoint))
657         {
658                 if(self.race_penalty)
659                 {
660                         if(other.race_lastpenalty != self)
661                         {
662                                 other.race_lastpenalty = self;
663                                 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
664                         }
665                 }
666
667                 if(other.race_penalty)
668                         return;
669
670                 /*
671                  * Trigger targets
672                  */
673                 if(self.spawnflags & 2)
674                 {
675                         activator = other;
676                         oldmsg = self.message;
677                         self.message = "";
678                         SUB_UseTargets();
679                         self.message = oldmsg;
680                 }
681
682                 if(other.race_respawn_checkpoint != self.race_checkpoint || !other.race_started)
683                         other.race_respawn_spotref = self; // this is not a spot but a CP, but spawnpoint selection will deal with that
684                 other.race_respawn_checkpoint = self.race_checkpoint;
685                 other.race_checkpoint = race_NextCheckpoint(self.race_checkpoint);
686                 other.race_started = 1;
687
688                 race_SendTime(other, self.race_checkpoint, other.race_movetime, !!other.race_laptime);
689
690                 if(!self.race_checkpoint) // start line
691                 {
692                         other.race_laptime = time;
693                         other.race_movetime = other.race_movetime_frac = other.race_movetime_count = 0;
694                         other.race_penalty_accumulator = 0;
695                         other.race_lastpenalty = world;
696                 }
697
698                 if(g_race_qualifying)
699                         race_SendNextCheckpoint(other, 0);
700
701                 if(defrag_ents && defragcpexists < 0 && self.classname == "target_stopTimer")
702                 {
703                         float fh;
704                         defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_WRITE);
705                         if(fh >= 0)
706                         {
707                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
708                                 fputs(fh, strcat(cp.targetname, " ", ftos(cp.race_checkpoint), "\n"));
709                         }
710                         fclose(fh);
711                 }
712         }
713         else if(other.race_checkpoint == race_NextCheckpoint(self.race_checkpoint))
714         {
715                 // ignored
716         }
717         else
718         {
719                 if(self.spawnflags & 4)
720                         Damage (other, self, self, 10000, DEATH_HURTTRIGGER, other.origin, '0 0 0');
721         }
722 }
723
724 void checkpoint_touch()
725 {
726         EXACTTRIGGER_TOUCH;
727         checkpoint_passed();
728 }
729
730 void checkpoint_use()
731 {
732         if(other.classname == "info_player_deathmatch") // a spawn, a spawn
733                 return;
734
735         other = activator;
736         checkpoint_passed();
737 }
738
739 float race_waypointsprite_visible_for_player(entity e)
740 {
741         if(e.race_checkpoint == -1 || self.owner.race_checkpoint == -2)
742                 return TRUE;
743         else if(e.race_checkpoint == self.owner.race_checkpoint)
744                 return TRUE;
745         else
746                 return FALSE;
747 }
748
749 float have_verified;
750 void trigger_race_checkpoint_verify()
751 {
752         entity oldself, cp;
753         float i, p;
754         float qual;
755
756         if(have_verified)
757                 return;
758         have_verified = 1;
759         
760         qual = g_race_qualifying;
761
762         oldself = self;
763         self = spawn();
764         self.classname = "player";
765
766         if(g_race)
767         {
768                 for(i = 0; i <= race_highest_checkpoint; ++i)
769                 {
770                         self.race_checkpoint = race_NextCheckpoint(i);
771
772                         // race only (middle of the race)
773                         g_race_qualifying = 0;
774                         self.race_place = 0;
775                         if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
776                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for respawning in race) - bailing out"));
777
778                         if(i == 0)
779                         {
780                                 // qualifying only
781                                 g_race_qualifying = 1;
782                                 self.race_place = race_lowest_place_spawn;
783                                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
784                                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
785                                 
786                                 // race only (initial spawn)
787                                 g_race_qualifying = 0;
788                                 for(p = 1; p <= race_highest_place_spawn; ++p)
789                                 {
790                                         self.race_place = p;
791                                         if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
792                                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for initially spawning in race) - bailing out"));
793                                 }
794                         }
795                 }
796         }
797         else if(!defrag_ents)
798         {
799                 // qualifying only
800                 self.race_checkpoint = race_NextCheckpoint(0);
801                 g_race_qualifying = 1;
802                 self.race_place = race_lowest_place_spawn;
803                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
804                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
805         }
806         else
807         {
808                 self.race_checkpoint = race_NextCheckpoint(0);
809                 g_race_qualifying = 1;
810                 self.race_place = 0; // there's only one spawn on defrag maps
811  
812                 // check if a defragcp file already exists, then read it and apply the checkpoint order
813                 float fh;
814                 float len;
815                 string l;
816
817                 defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_READ);
818                 if(fh >= 0)
819                 {
820                         while((l = fgets(fh)))
821                         {
822                                 len = tokenize_console(l);
823                                 if(len != 2) {
824                                         defragcpexists = -1; // something's wrong in the defrag cp file, set defragcpexists to -1 so that it will be rewritten when someone finishes
825                                         continue;
826                                 }
827                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
828                                         if(argv(0) == cp.targetname)
829                                                 cp.race_checkpoint = stof(argv(1));
830                         }
831                         fclose(fh);
832                 }
833         }
834
835         g_race_qualifying = qual;
836
837         if(race_timed_checkpoint) {
838                 if(defrag_ents) {
839                         for(cp = world; (cp = find(cp, classname, "target_startTimer"));)
840                                 WaypointSprite_UpdateSprites(cp.sprite, "race-start", "", "");
841                         for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
842                                 WaypointSprite_UpdateSprites(cp.sprite, "race-finish", "", "");
843
844                         for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
845                                 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
846                                         defragcpexists = -1;
847                         }
848
849                         if(defragcpexists != -1){
850                                 float largest_cp_id;
851                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
852                                         if(cp.race_checkpoint > largest_cp_id)
853                                                 largest_cp_id = cp.race_checkpoint;
854                                 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
855                                         cp.race_checkpoint = largest_cp_id + 1; // finish line
856                                 race_highest_checkpoint = largest_cp_id + 1;
857                                 race_timed_checkpoint = largest_cp_id + 1;
858                         } else {
859                                 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
860                                         cp.race_checkpoint = 255; // finish line
861                                 race_highest_checkpoint = 255;
862                                 race_timed_checkpoint = 255;
863                         }
864                 }
865                 else {
866                         for(cp = world; (cp = find(cp, classname, "trigger_race_checkpoint")); )
867                                 if(cp.sprite)
868                                 {
869                                         if(cp.race_checkpoint == 0)
870                                                 WaypointSprite_UpdateSprites(cp.sprite, "race-start", "", "");
871                                         else if(cp.race_checkpoint == race_timed_checkpoint)
872                                                 WaypointSprite_UpdateSprites(cp.sprite, "race-finish", "", "");
873                                 }
874                 }
875         }
876
877         if(defrag_ents) {
878                 entity trigger, targ;
879                 for(trigger = world; (trigger = find(trigger, classname, "trigger_multiple")); )
880                         for(targ = world; (targ = find(targ, targetname, trigger.target)); )
881                                 if (targ.classname == "target_checkpoint" || targ.classname == "target_startTimer" || targ.classname == "target_stopTimer") {
882                                         targ.wait = -2;
883                                         targ.delay = 0;
884
885                                         setsize(targ, trigger.mins, trigger.maxs);
886                                         setorigin(targ, trigger.origin);
887                                         //remove(trigger);
888                                 }
889         }
890         remove(self);
891         self = oldself;
892 }
893
894 void spawnfunc_trigger_race_checkpoint()
895 {
896         vector o;
897         if(!g_race && !g_cts)
898         {
899                 remove(self);
900                 return;
901         }
902
903         EXACTTRIGGER_INIT;
904
905         self.use = checkpoint_use;
906         if not(self.spawnflags & 1)
907                 self.touch = checkpoint_touch;
908
909         o = (self.absmin + self.absmax) * 0.5;
910         tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o_z - self.absmin_z), MOVE_NORMAL, self);
911         waypoint_spawnforitem_force(self, trace_endpos);
912         self.nearestwaypointtimeout = time + 1000000000;
913
914         if(!self.message)
915                 self.message = "went backwards";
916         if (!self.message2)
917                 self.message2 = "was pushed backwards by";
918         if (!self.race_penalty_reason)
919                 self.race_penalty_reason = "missing a checkpoint";
920         
921         self.race_checkpoint = self.cnt;
922
923         if(self.race_checkpoint > race_highest_checkpoint)
924         {
925                 race_highest_checkpoint = self.race_checkpoint;
926                 if(self.spawnflags & 8)
927                         race_timed_checkpoint = self.race_checkpoint;
928                 else
929                         race_timed_checkpoint = 0;
930         }
931
932         if(!self.race_penalty)
933         {
934                 if(self.race_checkpoint)
935                         WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
936                 else
937                         WaypointSprite_SpawnFixed("race-finish", o, self, sprite);
938         }
939
940         self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
941
942         InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
943 }
944
945 void spawnfunc_target_checkpoint() // defrag entity
946 {
947         vector o;
948         if(!g_race && !g_cts)
949         {
950                 remove(self);
951                 return;
952         }
953         defrag_ents = 1;
954
955         EXACTTRIGGER_INIT;
956
957         self.use = checkpoint_use;
958         if not(self.spawnflags & 1)
959                 self.touch = checkpoint_touch;
960
961         o = (self.absmin + self.absmax) * 0.5;
962         tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o_z - self.absmin_z), MOVE_NORMAL, self);
963         waypoint_spawnforitem_force(self, trace_endpos);
964         self.nearestwaypointtimeout = time + 1000000000;
965
966         if(!self.message)
967                 self.message = "went backwards";
968         if (!self.message2)
969                 self.message2 = "was pushed backwards by";
970         if (!self.race_penalty_reason)
971                 self.race_penalty_reason = "missing a checkpoint";
972
973         if(self.classname == "target_startTimer")
974                 self.race_checkpoint = 0;
975         else
976                 self.race_checkpoint = -2;
977
978         race_timed_checkpoint = 1;
979
980         if(self.race_checkpoint == 0)
981                 WaypointSprite_SpawnFixed("race-start", o, self, sprite);
982         else
983                 WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
984
985         self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
986
987         InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
988 }
989
990 void spawnfunc_target_startTimer() { spawnfunc_target_checkpoint(); }
991 void spawnfunc_target_stopTimer() { spawnfunc_target_checkpoint(); }
992
993 void race_AbandonRaceCheck(entity p)
994 {
995         if(race_completing && !p.race_completed)
996         {
997                 p.race_completed = 1;
998                 MAKE_INDEPENDENT_PLAYER(p);
999                 bprint(p.netname, "^7 has abandoned the race.\n");
1000                 ClientData_Touch(p);
1001         }
1002 }
1003
1004 void race_StartCompleting()
1005 {
1006         entity p;
1007         race_completing = 1;
1008         FOR_EACH_PLAYER(p)
1009                 if(p.deadflag != DEAD_NO)
1010                         race_AbandonRaceCheck(p);
1011 }
1012
1013 void race_PreparePlayer()
1014 {
1015         race_ClearTime(self);
1016         self.race_place = 0;
1017         self.race_started = 0;
1018         self.race_respawn_checkpoint = 0;
1019         self.race_respawn_spotref = world;
1020 }
1021
1022 void race_RetractPlayer()
1023 {
1024         if(!g_race && !g_cts)
1025                 return;
1026         if(self.race_respawn_checkpoint == 0 || self.race_respawn_checkpoint == race_timed_checkpoint)
1027                 race_ClearTime(self);
1028         self.race_checkpoint = self.race_respawn_checkpoint;
1029 }
1030
1031 void race_PreDie()
1032 {
1033         if(!g_race && !g_cts)
1034                 return;
1035
1036         race_AbandonRaceCheck(self);
1037 }
1038
1039 void race_PreSpawn()
1040 {
1041         if(!g_race && !g_cts)
1042                 return;
1043         if(self.killcount == -666 /* initial spawn */ || g_race_qualifying) // spawn
1044                 race_PreparePlayer();
1045         else // respawn
1046                 race_RetractPlayer();
1047
1048         race_AbandonRaceCheck(self);
1049 }
1050
1051 void race_PostSpawn(entity spot)
1052 {
1053         if(!g_race && !g_cts)
1054                 return;
1055
1056         if(spot.target == "")
1057                 // Emergency: this wasn't a real spawnpoint. Can this ever happen?
1058                 race_PreparePlayer();
1059
1060         // if we need to respawn, do it right
1061         self.race_respawn_checkpoint = self.race_checkpoint;
1062         self.race_respawn_spotref = spot;
1063
1064         self.race_place = 0;
1065 }
1066
1067 void race_PreSpawnObserver()
1068 {
1069         if(!g_race && !g_cts)
1070                 return;
1071         race_PreparePlayer();
1072         self.race_checkpoint = -1;
1073 }
1074
1075 void spawnfunc_info_player_race (void)
1076 {
1077         if(!g_race && !g_cts)
1078         {
1079                 remove(self);
1080                 return;
1081         }
1082         ++race_spawns;
1083         spawnfunc_info_player_deathmatch();
1084
1085         if(self.race_place > race_highest_place_spawn)
1086                 race_highest_place_spawn = self.race_place;
1087         if(self.race_place < race_lowest_place_spawn)
1088                 race_lowest_place_spawn = self.race_place;
1089 }
1090
1091 void race_ClearRecords()
1092 {
1093         float i;
1094         entity e;
1095
1096         for(i = 0; i < MAX_CHECKPOINTS; ++i)
1097         {
1098                 race_checkpoint_records[i] = 0;
1099                 if(race_checkpoint_recordholders[i])
1100                         strunzone(race_checkpoint_recordholders[i]);
1101                 race_checkpoint_recordholders[i] = string_null;
1102         }
1103
1104         e = self;
1105         FOR_EACH_CLIENT(self)
1106         {
1107                 float p;
1108                 p = self.race_place;
1109                 race_PreparePlayer();
1110                 self.race_place = p;
1111         }
1112         self = e;
1113 }
1114
1115 void race_ReadyRestart()
1116 {
1117         float s;
1118
1119         Score_NicePrint(world);
1120
1121         race_ClearRecords();
1122         PlayerScore_Sort(race_place);
1123
1124         entity e;
1125         FOR_EACH_CLIENT(e)
1126         {
1127                 if(e.race_place)
1128                 {
1129                         s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
1130                         if(!s)
1131                                 e.race_place = 0;
1132                 }
1133                 print(e.netname, " = ", ftos(e.race_place), "\n");
1134         }
1135
1136         if(g_race_qualifying == 2)
1137         {
1138                 g_race_qualifying = 0;
1139                 independent_players = 0;
1140                 cvar_set("fraglimit", ftos(race_fraglimit));
1141                 cvar_set("leadlimit", ftos(race_leadlimit));
1142                 cvar_set("timelimit", ftos(race_timelimit));
1143                 ScoreRules_race();
1144         }
1145 }
1146
1147 void race_ImposePenaltyTime(entity pl, float penalty, string reason)
1148 {
1149         if(g_race_qualifying)
1150         {
1151                 pl.race_penalty_accumulator += penalty;
1152                 msg_entity = pl;
1153                 WRITESPECTATABLE_MSG_ONE({
1154                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
1155                         WriteByte(MSG_ONE, TE_CSQC_RACE);
1156                         WriteByte(MSG_ONE, RACE_NET_PENALTY_QUALIFYING);
1157                         WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1158                         WriteString(MSG_ONE, reason);
1159                 });
1160         }
1161         else
1162         {
1163                 pl.race_penalty = time + penalty;
1164                 msg_entity = pl;
1165                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy, {
1166                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
1167                         WriteByte(MSG_ONE, TE_CSQC_RACE);
1168                         WriteByte(MSG_ONE, RACE_NET_PENALTY_RACE);
1169                         WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1170                         WriteString(MSG_ONE, reason);
1171                 });
1172         }
1173 }
1174
1175 void penalty_touch()
1176 {
1177         EXACTTRIGGER_TOUCH;
1178         if(other.race_lastpenalty != self)
1179         {
1180                 other.race_lastpenalty = self;
1181                 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
1182         }
1183 }
1184
1185 void penalty_use()
1186 {
1187         race_ImposePenaltyTime(activator, self.race_penalty, self.race_penalty_reason);
1188 }
1189
1190 void spawnfunc_trigger_race_penalty()
1191 {
1192         EXACTTRIGGER_INIT;
1193
1194         self.use = penalty_use;
1195         if not(self.spawnflags & 1)
1196                 self.touch = penalty_touch;
1197
1198         if (!self.race_penalty_reason)
1199                 self.race_penalty_reason = "missing a checkpoint";
1200         if (!self.race_penalty)
1201                 self.race_penalty = 5;
1202 }
1203
1204 float race_GetFractionalLapCount(entity e)
1205 {
1206         // interesting metrics (idea by KrimZon) to maybe sort players in the
1207         // scoreboard, immediately updates when overtaking
1208         //
1209         // requires the track to be built so you never get farther away from the
1210         // next checkpoint, though, and current Xonotic race maps are not built that
1211         // way
1212         //
1213         // also, this code is slow and would need optimization (i.e. "next CP"
1214         // links on CP entities)
1215
1216         float l;
1217         l = PlayerScore_Add(e, SP_RACE_LAPS, 0);
1218         if(e.race_completed)
1219                 return l; // not fractional
1220         
1221         vector o0, o1;
1222         float bestfraction, fraction;
1223         entity lastcp, cp0, cp1;
1224         float nextcpindex, lastcpindex;
1225
1226         nextcpindex = max(e.race_checkpoint, 0);
1227         lastcpindex = e.race_respawn_checkpoint;
1228         lastcp = e.race_respawn_spotref;
1229
1230         if(nextcpindex == lastcpindex)
1231                 return l; // finish
1232         
1233         bestfraction = 1;
1234         for(cp0 = world; (cp0 = find(cp0, classname, "trigger_race_checkpoint")); )
1235         {
1236                 if(cp0.race_checkpoint != lastcpindex)
1237                         continue;
1238                 if(lastcp)
1239                         if(cp0 != lastcp)
1240                                 continue;
1241                 o0 = (cp0.absmin + cp0.absmax) * 0.5;
1242                 for(cp1 = world; (cp1 = find(cp1, classname, "trigger_race_checkpoint")); )
1243                 {
1244                         if(cp1.race_checkpoint != nextcpindex)
1245                                 continue;
1246                         o1 = (cp1.absmin + cp1.absmax) * 0.5;
1247                         if(o0 == o1)
1248                                 continue;
1249                         fraction = bound(0.0001, vlen(e.origin - o1) / vlen(o0 - o1), 1);
1250                         if(fraction < bestfraction)
1251                                 bestfraction = fraction;
1252                 }
1253         }
1254
1255         // we are at CP "nextcpindex - bestfraction"
1256         // race_timed_checkpoint == 4: then nextcp==4 means 0.9999x, nextcp==0 means 0.0000x
1257         // race_timed_checkpoint == 0: then nextcp==0 means 0.9999x
1258         float c, nc;
1259         nc = race_highest_checkpoint + 1;
1260         c = (mod(nextcpindex - race_timed_checkpoint + nc + nc - 1, nc) + 1) - bestfraction;
1261
1262         return l + c / nc;
1263 }