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