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