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