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