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