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