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