]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/race.qc
FINALLY a fix to the cursed defrag checkpoint bug preventing two players from hitting...
[divverent/nexuiz.git] / data / 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 string rr;
102 float grecordtime[RANKINGS_CNT];
103 string grecordholder[RANKINGS_CNT];
104 float worst_time; // last ranked time
105 float have_recs; // have we already read the records from the database before?
106 float race_GetTime(float pos) {
107         if(g_cts)
108                 rr = CTS_RECORD;
109         else
110                 rr = RACE_RECORD;
111
112         if (!have_recs) { // I guess this is better than checking if the first array item is empty, rumor has it that arrays are slow
113                 float i;
114                 for(i=0;i<RANKINGS_CNT;++i) {
115                         grecordtime[i] = stof(db_get(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i))));
116                         grecordholder[i] = strzone(db_get(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i))));
117                 }
118                 grecordtime[0] = stof(db_get(ServerProgsDB, strcat(GetMapname(), rr, "time")));
119                 grecordholder[0] = strzone(db_get(ServerProgsDB, strcat(GetMapname(), rr, "netname")));
120                 worst_time = stof(db_get(ServerProgsDB, strcat(GetMapname(), rr, strcat("time", ftos(RANKINGS_CNT-1)))));
121                 have_recs = 1;
122         }
123
124         return grecordtime[pos-1];
125 }
126
127 string race_GetName(float pos) { // these other functions assume that race_GetTime has been called >= once before
128         return grecordholder[pos-1];
129 }
130
131 float race_CheckName(string netname) { // Does the name already exist in rankings? In that case, where? (otherwise 0)
132         float i;
133         for (i=RANKINGS_CNT-1;i>=0;--i)
134                 if(grecordholder[i] == netname)
135                         return i+1;
136         return 0;
137 }
138
139 float race_GetPos(float t) {
140         float i;
141
142         if(worst_time == 0)
143         for (i=0;i<RANKINGS_CNT;++i)
144                 if (grecordtime[i] == 0 || grecordtime[i] > t)
145                         return i+1;
146
147         for (i=0;i<RANKINGS_CNT;++i)
148                 if (grecordtime[i] > t)
149                         return i+1;
150         return 0;
151 }
152
153 void race_send_recordtime(float msg)
154 {
155         // send the server best time
156         WriteByte(msg, SVC_TEMPENTITY);
157         WriteByte(msg, TE_CSQC_RACE);
158         WriteByte(msg, RACE_NET_SERVER_RECORD);
159         WriteInt24_t(msg, race_GetTime(1));
160 }
161
162 void race_SendRankings(float pos, float prevpos, float del, float msg)
163 {
164         WriteByte(msg, SVC_TEMPENTITY);
165         WriteByte(msg, TE_CSQC_RACE);
166         WriteByte(msg, RACE_NET_SERVER_RANKINGS);
167         WriteShort(msg, pos);
168         WriteShort(msg, prevpos);
169         WriteShort(msg, del);
170         WriteString(msg, race_GetName(pos));
171         WriteInt24_t(msg, race_GetTime(pos));
172 }
173
174 string race_PlaceName(float pos) {
175         if(pos == 1)
176                 return "1st";
177         else if(pos == 2)
178                 return "2nd";
179         else if(pos == 3)
180                 return "3rd";
181         else
182                 return strcat(ftos(pos), "th");
183 }
184
185 void race_SetTime(entity e, float t) {
186         if (worst_time && t > worst_time)
187                 return;
188
189         float oldrec;
190         string oldname;
191         float pos;
192         pos = race_GetPos(t);
193
194         float prevpos = race_CheckName(e.netname);
195         if (prevpos && (prevpos < pos || !pos))
196         {
197                 oldrec = race_GetTime(prevpos);
198                 string recorddifference = strcat(" ^1[+", TIME_ENCODED_TOSTRING(t - oldrec), "]");
199                 bprint(e.netname, "^1 couldn't break their ", race_PlaceName(prevpos), " place record of ", TIME_ENCODED_TOSTRING(oldrec), recorddifference, "\n");
200
201                 return;
202         } else if (!pos) { // no ranking, time worse than the worst ranked
203                 string recorddifference = strcat(" ^1[+", TIME_ENCODED_TOSTRING(t - grecordtime[RANKINGS_CNT-1]), "]");
204                 bprint(e.netname, "^1 couldn't break the ", race_PlaceName(RANKINGS_CNT), " place record of ", TIME_ENCODED_TOSTRING(grecordtime[RANKINGS_CNT-1]), recorddifference, "\n");
205                 return;
206         }
207
208
209         // move other rankings out of the way
210         float i;
211         if (prevpos) { // player improved his existing record
212                 if(prevpos == pos) {
213                         oldrec = grecordtime[pos-1];
214                 }
215                 for (i=prevpos-1;i>pos-1;--i) {
216                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), ftos(grecordtime[i-1]));
217                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i)), grecordholder[i-1]);
218                         if(i == prevpos-1) {
219                                 oldrec = grecordtime[i];
220                                 oldname = grecordholder[i];
221                         }
222                         grecordtime[i] = grecordtime[i-1];
223                         if (grecordholder[i])
224                                 strunzone(grecordholder[i]);
225                         grecordholder[i] = strzone(grecordholder[i-1]);
226                 }
227         } else { // player has no ranked record yet
228                 for (i=RANKINGS_CNT-1;i>pos-1;--i) {
229                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), ftos(grecordtime[i-1]));
230                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i)), grecordholder[i-1]);
231                         grecordtime[i] = grecordtime[i-1];
232                         if (grecordholder[i])
233                                 strunzone(grecordholder[i]);
234                         grecordholder[i] = strzone(grecordholder[i-1]);
235                 }
236         }
237         
238         // store new ranking
239         if (pos == 1) {
240                 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time"), ftos(t));
241                 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname"), e.netname);
242                 grecordtime[0] = t;
243                 if (grecordholder[0])
244                         strunzone(grecordholder[0]);
245                 grecordholder[0] = strzone(e.netname);
246                 write_recordmarker(e, time - TIME_DECODE(t), TIME_DECODE(t));
247                 race_send_recordtime(MSG_ALL);
248         } else {
249                 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(pos-1)), ftos(t));
250                 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(pos-1)), e.netname);
251                 grecordtime[pos-1] = t;
252                 if (grecordholder[pos-1])
253                         strunzone(grecordholder[pos-1]);
254                 grecordholder[pos-1] = strzone(e.netname);
255         }
256
257         if (pos == RANKINGS_CNT)
258                 worst_time = t;
259
260         race_SendRankings(pos, prevpos, 0, MSG_ALL);
261         if(rankings_reply)
262                 strunzone(rankings_reply);
263         rankings_reply = strzone(getrankings());
264         if(pos == prevpos) {
265                 string recorddifference = strcat(" ^3[-", TIME_ENCODED_TOSTRING(oldrec - t), "]");
266                 bprint(e.netname, "^3 improved their ", race_PlaceName(pos), " ^3place record with ", TIME_ENCODED_TOSTRING(t), recorddifference, "\n");
267         }
268         else if(oldname == "")
269                 bprint(e.netname, "^2 set the ", race_PlaceName(pos), " ^2place record with ", TIME_ENCODED_TOSTRING(t), "\n");
270         else {
271                 string recorddifference = strcat(" ^2[-", TIME_ENCODED_TOSTRING(oldrec - t), "]");
272                 bprint(e.netname, "^2 broke ", oldname, "^2's ", race_PlaceName(pos), " ^2place record ", strcat(TIME_ENCODED_TOSTRING(oldrec), " ^2with ", TIME_ENCODED_TOSTRING(t), recorddifference, "\n"));
273         }
274 }
275
276 void race_DeleteTime(float pos) {
277         float i;
278
279         for (i = pos-1; i <= RANKINGS_CNT-1; ++i) {
280                 if (i == 0) {
281                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "time"), ftos(grecordtime[1]));
282                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname"), grecordholder[1]);
283                         grecordtime[0] = grecordtime[1];
284                         if (grecordholder[i])
285                                 strunzone(grecordholder[0]);
286                         grecordholder[0] = strzone(grecordholder[1]);
287                 }
288                 else if (i == RANKINGS_CNT-1) {
289                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), string_null);
290                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i)), string_null);
291                         grecordtime[i] = 0;
292                         if (grecordholder[i])
293                                 strunzone(grecordholder[i]);
294                         grecordholder[i] = string_null;
295                 }
296                 else {
297                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), ftos(grecordtime[i+1]));
298                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i)), grecordholder[i+1]);
299                         grecordtime[i] = grecordtime[i+1];
300                         if (grecordholder[i])
301                                 strunzone(grecordholder[i]);
302                         grecordholder[i] = strzone(grecordholder[i+1]);
303                 }
304         }
305
306         race_SendRankings(pos, 0, 1, MSG_ALL);
307         if(pos == 1)
308                 race_send_recordtime(MSG_ALL);
309
310         if(rankings_reply)
311                 strunzone(rankings_reply);
312         rankings_reply = strzone(getrankings());
313
314         worst_time = 0;
315 }
316
317 void race_SendTime(entity e, float cp, float t, float tvalid)
318 {
319         float snew, l;
320         entity p;
321
322         if(g_race_qualifying)
323                 t += e.race_penalty_accumulator;
324
325         t = TIME_ENCODE(t); // make integer
326         // adding just 0.4 so it rounds down in the .5 case (matching the timer display)
327
328         if(tvalid)
329         if(cp == race_timed_checkpoint) // finish line
330         if not(e.race_completed)
331         {
332                 float s;
333                 if(g_race_qualifying)
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                 else
340                 {
341                         s = PlayerScore_Add(e, SP_RACE_TIME, 0);
342                         snew = TIME_ENCODE(time - game_starttime);
343                         PlayerScore_Add(e, SP_RACE_TIME, snew - s);
344                         l = PlayerTeamScore_Add(e, SP_RACE_LAPS, ST_RACE_LAPS, 1);
345
346                         if(cvar("fraglimit"))
347                                 if(l >= cvar("fraglimit"))
348                                         race_StartCompleting();
349
350                         if(race_completing)
351                         {
352                                 e.race_completed = 1;
353                                 MAKE_INDEPENDENT_PLAYER(e);
354                                 bprint(e.netname, "^7 has finished the race.\n");
355                                 ClientData_Touch(e);
356                         }
357                 }
358         }
359
360         float recordtime;
361         string recordholder;
362         if(g_race_qualifying)
363         {
364                 if(tvalid)
365                 {
366                         recordtime = race_checkpoint_records[cp];
367                         recordholder = strcat1(race_checkpoint_recordholders[cp]); // make a tempstring copy, as we'll possibly strunzone it!
368                         if(recordholder == e.netname)
369                                 recordholder = "";
370
371                         if(t != 0)
372                         if(t < worst_time || worst_time == 0)
373                         {
374                                 if(cp == race_timed_checkpoint)
375                                 {
376                                         race_SetTime(e, t);
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 not((self.spawnflags & 2) && (other.classname == "player"))
534         {
535                 activator = other;
536                 oldmsg = self.message;
537                 self.message = "";
538                 SUB_UseTargets();
539                 self.message = oldmsg;
540         }
541
542         if(other.classname != "player")
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                 if(self.race_checkpoint == -2) 
554                 {
555                         self.race_checkpoint = other.race_checkpoint;
556                 }
557
558                 float largest_cp_id;
559                 float cp_amount;
560                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
561                         cp_amount += 1;
562                         if(cp.race_checkpoint > largest_cp_id) // update the finish id if someone hit a new checkpoint
563                         {
564                                 largest_cp_id = cp.race_checkpoint;
565                                 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
566                                         cp.race_checkpoint = largest_cp_id + 1; // finish line
567                                 race_highest_checkpoint = largest_cp_id + 1;
568                                 race_timed_checkpoint = largest_cp_id + 1;
569
570                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
571                                         if(cp.race_checkpoint == -2) // set defragcpexists to -1 so that the cp id file will be rewritten when someone finishes
572                                                 defragcpexists = -1;
573                                 }       
574                         }
575                 }
576                 if(cp_amount == 0) {
577                         for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
578                                 cp.race_checkpoint = 1;
579                         race_highest_checkpoint = 1;
580                         race_timed_checkpoint = 1;
581                 }
582         }
583
584         if((other.race_checkpoint == -1 && self.race_checkpoint == 0) || (other.race_checkpoint == self.race_checkpoint))
585         {
586                 if(self.race_penalty)
587                 {
588                         if(other.race_lastpenalty != self)
589                         {
590                                 other.race_lastpenalty = self;
591                                 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
592                         }
593                 }
594
595                 if(other.race_penalty)
596                         return;
597
598                 /*
599                  * Trigger targets
600                  */
601                 if(self.spawnflags & 2)
602                 {
603                         activator = other;
604                         oldmsg = self.message;
605                         self.message = "";
606                         SUB_UseTargets();
607                         self.message = oldmsg;
608                 }
609
610                 if(other.race_respawn_checkpoint != self.race_checkpoint || !other.race_started)
611                         other.race_respawn_spotref = self; // this is not a spot but a CP, but spawnpoint selection will deal with that
612                 other.race_respawn_checkpoint = self.race_checkpoint;
613                 other.race_checkpoint = race_NextCheckpoint(self.race_checkpoint);
614                 other.race_started = 1;
615
616                 race_SendTime(other, self.race_checkpoint, other.race_movetime, !!other.race_laptime);
617
618                 if(!self.race_checkpoint) // start line
619                 {
620                         other.race_laptime = time;
621                         other.race_movetime = other.race_movetime_frac = other.race_movetime_count = 0;
622                         other.race_penalty_accumulator = 0;
623                         other.race_lastpenalty = world;
624                 }
625
626                 if(g_race_qualifying)
627                         race_SendNextCheckpoint(other, 0);
628
629                 if(defrag_ents && defragcpexists < 0 && self.classname == "target_stopTimer")
630                 {
631                         float fh;
632                         defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_WRITE);
633                         if(fh >= 0)
634                         {
635                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
636                                 fputs(fh, strcat(cp.targetname, " ", ftos(cp.race_checkpoint), "\n"));
637                         }
638                         fclose(fh);
639                 }
640         }
641         else if(other.race_checkpoint == race_NextCheckpoint(self.race_checkpoint))
642         {
643                 // ignored
644         }
645         else
646         {
647                 if(self.spawnflags & 4)
648                         Damage (other, self, self, 10000, DEATH_HURTTRIGGER, other.origin, '0 0 0');
649         }
650 }
651
652 void checkpoint_touch()
653 {
654         EXACTTRIGGER_TOUCH;
655         checkpoint_passed();
656 }
657
658 void checkpoint_use()
659 {
660         if(other.classname == "info_player_deathmatch") // a spawn, a spawn
661                 return;
662
663         other = activator;
664         checkpoint_passed();
665 }
666
667 float race_waypointsprite_visible_for_player(entity e)
668 {
669         if(e.race_checkpoint == -1 || self.owner.race_checkpoint == -2)
670                 return TRUE;
671         else if(e.race_checkpoint == self.owner.race_checkpoint)
672                 return TRUE;
673         else
674                 return FALSE;
675 }
676
677 float have_verified;
678 void trigger_race_checkpoint_verify()
679 {
680         entity oldself, cp;
681         float i, p;
682         float qual;
683
684         if(have_verified)
685                 return;
686         have_verified = 1;
687         
688         qual = g_race_qualifying;
689
690         oldself = self;
691         self = spawn();
692         self.classname = "player";
693
694         if(g_race)
695         {
696                 for(i = 0; i <= race_highest_checkpoint; ++i)
697                 {
698                         self.race_checkpoint = race_NextCheckpoint(i);
699
700                         // race only (middle of the race)
701                         g_race_qualifying = 0;
702                         self.race_place = 0;
703                         if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
704                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for respawning in race) - bailing out"));
705
706                         if(i == 0)
707                         {
708                                 // qualifying only
709                                 g_race_qualifying = 1;
710                                 self.race_place = race_lowest_place_spawn;
711                                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
712                                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
713                                 
714                                 // race only (initial spawn)
715                                 g_race_qualifying = 0;
716                                 for(p = 1; p <= race_highest_place_spawn; ++p)
717                                 {
718                                         self.race_place = p;
719                                         if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
720                                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for initially spawning in race) - bailing out"));
721                                 }
722                         }
723                 }
724         }
725         else if(!defrag_ents)
726         {
727                 // qualifying only
728                 self.race_checkpoint = race_NextCheckpoint(0);
729                 g_race_qualifying = 1;
730                 self.race_place = race_lowest_place_spawn;
731                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
732                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
733         }
734         else
735         {
736                 self.race_checkpoint = race_NextCheckpoint(0);
737                 g_race_qualifying = 1;
738                 self.race_place = 0; // there's only one spawn on defrag maps
739  
740                 // check if a defragcp file already exists, then read it and apply the checkpoint order
741                 float fh;
742                 float len;
743                 string l;
744
745                 defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_READ);
746                 if(fh >= 0)
747                 {
748                         while((l = fgets(fh)))
749                         {
750                                 len = tokenize_console(l);
751                                 if(len != 2) {
752                                         defragcpexists = -1; // something's wrong in the defrag cp file, set defragcpexists to -1 so that it will be rewritten when someone finishes
753                                         continue;
754                                 }
755                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
756                                         if(argv(0) == cp.targetname)
757                                                 cp.race_checkpoint = stof(argv(1));
758                         }
759                         fclose(fh);
760                 }
761         }
762
763         g_race_qualifying = qual;
764
765         if(race_timed_checkpoint) {
766                 if(defrag_ents) {
767                         for(cp = world; (cp = find(cp, classname, "target_startTimer"));)
768                                 WaypointSprite_UpdateSprites(cp.sprite, "race-start", "", "");
769                         for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
770                                 WaypointSprite_UpdateSprites(cp.sprite, "race-finish", "", "");
771
772                         for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
773                                 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
774                                         defragcpexists = -1;
775                         }
776
777                         if(defragcpexists != -1){
778                                 float largest_cp_id;
779                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
780                                         if(cp.race_checkpoint > largest_cp_id)
781                                                 largest_cp_id = cp.race_checkpoint;
782                                 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
783                                         cp.race_checkpoint = largest_cp_id + 1; // finish line
784                                 race_highest_checkpoint = largest_cp_id + 1;
785                                 race_timed_checkpoint = largest_cp_id + 1;
786                         } else {
787                                 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
788                                         cp.race_checkpoint = 255; // finish line
789                                 race_highest_checkpoint = 255;
790                                 race_timed_checkpoint = 255;
791                         }
792                 }
793                 else {
794                         for(cp = world; (cp = find(cp, classname, "trigger_race_checkpoint")); )
795                                 if(cp.sprite)
796                                 {
797                                         if(cp.race_checkpoint == 0)
798                                                 WaypointSprite_UpdateSprites(cp.sprite, "race-start", "", "");
799                                         else if(cp.race_checkpoint == race_timed_checkpoint)
800                                                 WaypointSprite_UpdateSprites(cp.sprite, "race-finish", "", "");
801                                 }
802                 }
803         }
804
805         if(defrag_ents) {
806                 entity trigger, targ;
807                 for(trigger = world; (trigger = find(trigger, classname, "trigger_multiple")); )
808                         for(targ = world; (targ = find(targ, targetname, trigger.target)); )
809                                 if (targ.classname == "target_checkpoint" || targ.classname == "target_startTimer" || targ.classname == "target_stopTimer") {
810                                         targ.wait = -2;
811                                         targ.delay = 0;
812
813                                         setsize(targ, trigger.mins, trigger.maxs);
814                                         setorigin(targ, trigger.origin);
815                                         remove(trigger);
816                                 }
817         }
818         remove(self);
819         self = oldself;
820 }
821
822 void spawnfunc_trigger_race_checkpoint()
823 {
824         vector o;
825         if(!g_race && !g_cts)
826         {
827                 remove(self);
828                 return;
829         }
830
831         EXACTTRIGGER_INIT;
832
833         self.use = checkpoint_use;
834         if not(self.spawnflags & 1)
835                 self.touch = checkpoint_touch;
836
837         o = (self.absmin + self.absmax) * 0.5;
838         tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o_z - self.absmin_z), MOVE_NORMAL, self);
839         waypoint_spawnforitem_force(self, trace_endpos);
840         self.nearestwaypointtimeout = time + 1000000000;
841
842         if(!self.message)
843                 self.message = "went backwards";
844         if (!self.message2)
845                 self.message2 = "was pushed backwards by";
846         if (!self.race_penalty_reason)
847                 self.race_penalty_reason = "missing a checkpoint";
848         
849         self.race_checkpoint = self.cnt;
850
851         if(self.race_checkpoint > race_highest_checkpoint)
852         {
853                 race_highest_checkpoint = self.race_checkpoint;
854                 if(self.spawnflags & 8)
855                         race_timed_checkpoint = self.race_checkpoint;
856                 else
857                         race_timed_checkpoint = 0;
858         }
859
860         if(!self.race_penalty)
861         {
862                 if(self.race_checkpoint)
863                         WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
864                 else
865                         WaypointSprite_SpawnFixed("race-finish", o, self, sprite);
866         }
867
868         self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
869
870         InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
871 }
872
873 void spawnfunc_target_checkpoint() // defrag entity
874 {
875         vector o;
876         if(!g_race && !g_cts)
877         {
878                 remove(self);
879                 return;
880         }
881         defrag_ents = 1;
882
883         EXACTTRIGGER_INIT;
884
885         self.use = checkpoint_use;
886         if not(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         if(self.classname == "target_startTimer")
902                 self.race_checkpoint = 0;
903         else
904                 self.race_checkpoint = -2;
905
906         race_timed_checkpoint = 1;
907
908         if(self.race_checkpoint == 0)
909                 WaypointSprite_SpawnFixed("race-start", o, self, sprite);
910         else
911                 WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
912
913         self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
914
915         InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
916 }
917
918 void spawnfunc_target_startTimer() { spawnfunc_target_checkpoint(); }
919 void spawnfunc_target_stopTimer() { spawnfunc_target_checkpoint(); }
920
921 void race_AbandonRaceCheck(entity p)
922 {
923         if(race_completing && !p.race_completed)
924         {
925                 p.race_completed = 1;
926                 MAKE_INDEPENDENT_PLAYER(p);
927                 bprint(p.netname, "^7 has abandoned the race.\n");
928                 ClientData_Touch(p);
929         }
930 }
931
932 void race_StartCompleting()
933 {
934         entity p;
935         race_completing = 1;
936         FOR_EACH_PLAYER(p)
937                 if(p.deadflag != DEAD_NO)
938                         race_AbandonRaceCheck(p);
939 }
940
941 void race_PreparePlayer()
942 {
943         race_ClearTime(self);
944         self.race_place = 0;
945         self.race_started = 0;
946         self.race_respawn_checkpoint = 0;
947         self.race_respawn_spotref = world;
948 }
949
950 void race_RetractPlayer()
951 {
952         if(!g_race && !g_cts)
953                 return;
954         if(self.race_respawn_checkpoint == 0 || self.race_respawn_checkpoint == race_timed_checkpoint)
955                 race_ClearTime(self);
956         self.race_checkpoint = self.race_respawn_checkpoint;
957 }
958
959 void race_PreDie()
960 {
961         if(!g_race && !g_cts)
962                 return;
963
964         race_AbandonRaceCheck(self);
965 }
966
967 void race_PreSpawn()
968 {
969         if(!g_race && !g_cts)
970                 return;
971         if(self.killcount == -666 /* initial spawn */ || g_race_qualifying) // spawn
972                 race_PreparePlayer();
973         else // respawn
974                 race_RetractPlayer();
975
976         race_AbandonRaceCheck(self);
977 }
978
979 void race_PostSpawn(entity spot)
980 {
981         if(!g_race && !g_cts)
982                 return;
983
984         if(spot.target == "")
985                 // Emergency: this wasn't a real spawnpoint. Can this ever happen?
986                 race_PreparePlayer();
987
988         // if we need to respawn, do it right
989         self.race_respawn_checkpoint = self.race_checkpoint;
990         self.race_respawn_spotref = spot;
991
992         self.race_place = 0;
993 }
994
995 void race_PreSpawnObserver()
996 {
997         if(!g_race && !g_cts)
998                 return;
999         race_PreparePlayer();
1000         self.race_checkpoint = -1;
1001 }
1002
1003 void spawnfunc_info_player_race (void)
1004 {
1005         if(!g_race && !g_cts)
1006         {
1007                 remove(self);
1008                 return;
1009         }
1010         ++race_spawns;
1011         spawnfunc_info_player_deathmatch();
1012
1013         if(self.race_place > race_highest_place_spawn)
1014                 race_highest_place_spawn = self.race_place;
1015         if(self.race_place < race_lowest_place_spawn)
1016                 race_lowest_place_spawn = self.race_place;
1017 }
1018
1019 void race_ClearRecords()
1020 {
1021         float i;
1022         entity e;
1023
1024         for(i = 0; i < MAX_CHECKPOINTS; ++i)
1025         {
1026                 race_checkpoint_records[i] = 0;
1027                 if(race_checkpoint_recordholders[i])
1028                         strunzone(race_checkpoint_recordholders[i]);
1029                 race_checkpoint_recordholders[i] = string_null;
1030         }
1031
1032         e = self;
1033         FOR_EACH_CLIENT(self)
1034         {
1035                 float p;
1036                 p = self.race_place;
1037                 race_PreparePlayer();
1038                 self.race_place = p;
1039         }
1040         self = e;
1041 }
1042
1043 void race_ReadyRestart()
1044 {
1045         float s;
1046
1047         Score_NicePrint(world);
1048
1049         race_ClearRecords();
1050         PlayerScore_Sort(race_place);
1051
1052         entity e;
1053         FOR_EACH_CLIENT(e)
1054         {
1055                 if(e.race_place)
1056                 {
1057                         s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
1058                         if(!s)
1059                                 e.race_place = 0;
1060                 }
1061                 print(e.netname, " = ", ftos(e.race_place), "\n");
1062         }
1063
1064         if(g_race_qualifying == 2)
1065         {
1066                 g_race_qualifying = 0;
1067                 independent_players = 0;
1068                 cvar_set("fraglimit", ftos(race_fraglimit));
1069                 cvar_set("leadlimit", ftos(race_leadlimit));
1070                 cvar_set("timelimit", ftos(race_timelimit));
1071                 ScoreRules_race();
1072         }
1073 }
1074
1075 void race_ImposePenaltyTime(entity pl, float penalty, string reason)
1076 {
1077         if(g_race_qualifying)
1078         {
1079                 pl.race_penalty_accumulator += penalty;
1080                 msg_entity = pl;
1081                 WRITESPECTATABLE_MSG_ONE({
1082                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
1083                         WriteByte(MSG_ONE, TE_CSQC_RACE);
1084                         WriteByte(MSG_ONE, RACE_NET_PENALTY_QUALIFYING);
1085                         WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1086                         WriteString(MSG_ONE, reason);
1087                 });
1088         }
1089         else
1090         {
1091                 pl.race_penalty = time + penalty;
1092                 msg_entity = pl;
1093                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy, {
1094                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
1095                         WriteByte(MSG_ONE, TE_CSQC_RACE);
1096                         WriteByte(MSG_ONE, RACE_NET_PENALTY_RACE);
1097                         WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1098                         WriteString(MSG_ONE, reason);
1099                 });
1100         }
1101 }
1102
1103 void penalty_touch()
1104 {
1105         EXACTTRIGGER_TOUCH;
1106         if(other.race_lastpenalty != self)
1107         {
1108                 other.race_lastpenalty = self;
1109                 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
1110         }
1111 }
1112
1113 void penalty_use()
1114 {
1115         race_ImposePenaltyTime(activator, self.race_penalty, self.race_penalty_reason);
1116 }
1117
1118 void spawnfunc_trigger_race_penalty()
1119 {
1120         EXACTTRIGGER_INIT;
1121
1122         self.use = penalty_use;
1123         if not(self.spawnflags & 1)
1124                 self.touch = penalty_touch;
1125
1126         if (!self.race_penalty_reason)
1127                 self.race_penalty_reason = "missing a checkpoint";
1128         if (!self.race_penalty)
1129                 self.race_penalty = 5;
1130 }