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