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