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