]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/race.qc
race: improved respawn handling:
[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 void race_send_recordtime(float t, float msg)
100 {
101         // send the server best time
102         WriteByte(msg, SVC_TEMPENTITY);
103         WriteByte(msg, TE_CSQC_RACE);
104         WriteByte(msg, RACE_NET_SERVER_RECORD);
105         WriteInt24_t(msg, t);
106 }
107
108 void race_SendTime(entity e, float cp, float t, float tvalid)
109 {
110         float snew, l;
111         entity p;
112
113         if(g_race_qualifying)
114                 t += e.race_penalty_accumulator;
115
116         t = TIME_ENCODE(t); // make integer
117         // adding just 0.4 so it rounds down in the .5 case (matching the timer display)
118
119         if(tvalid)
120         if(cp == race_timed_checkpoint) // finish line
121         if not(e.race_completed)
122         {
123                 float s;
124                 if(g_race_qualifying)
125                 {
126                         s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
127                         if(!s || t < s)
128                                 PlayerScore_Add(e, SP_RACE_FASTEST, t - s);
129                 }
130                 else
131                 {
132                         s = PlayerScore_Add(e, SP_RACE_TIME, 0);
133                         snew = TIME_ENCODE(time - game_starttime);
134                         PlayerScore_Add(e, SP_RACE_TIME, snew - s);
135                         l = PlayerTeamScore_Add(e, SP_RACE_LAPS, ST_RACE_LAPS, 1);
136
137                         if(cvar("fraglimit"))
138                                 if(l >= cvar("fraglimit"))
139                                         race_StartCompleting();
140
141                         if(race_completing)
142                         {
143                                 e.race_completed = 1;
144                                 MAKE_INDEPENDENT_PLAYER(e);
145                                 bprint(e.netname, "^7 has finished the race.\n");
146                                 ClientData_Touch(e);
147                         }
148                 }
149         }
150
151         float recordtime;
152         string recordholder;
153         if(g_race_qualifying)
154         {
155                 if(tvalid)
156                 {
157                         recordtime = race_checkpoint_records[cp];
158                         recordholder = strcat1(race_checkpoint_recordholders[cp]); // make a tempstring copy, as we'll possibly strunzone it!
159                         if(recordholder == e.netname)
160                                 recordholder = "";
161
162                         if(t != 0)
163                         if(t < recordtime || recordtime == 0)
164                         {
165                                 race_checkpoint_records[cp] = t;
166                                 if(race_checkpoint_recordholders[cp])
167                                         strunzone(race_checkpoint_recordholders[cp]);
168                                 race_checkpoint_recordholders[cp] = strzone(e.netname);
169                                 if(cp == race_timed_checkpoint)
170                                 {
171                                         float grecordtime;
172                                         string grecordholder, recorddifference;
173                                         string rr;
174                                         if(g_cts)
175                                                 rr = CTS_RECORD;
176                                         else
177                                                 rr = RACE_RECORD;
178                                         grecordtime = stof(db_get(ServerProgsDB, strcat(GetMapname(), rr, "time")));
179                                         grecordholder = db_get(ServerProgsDB, strcat(GetMapname(), rr, "netname"));
180                                         if(grecordholder == e.netname)
181                                                 grecordholder = "";
182                                         if(grecordtime == 0)
183                                         {
184                                                 bprint(e.netname, "^7 set the all-time fastest lap record with ", TIME_ENCODED_TOSTRING(t), "\n");
185                                                 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time"), ftos(t));
186                                                 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname"), e.netname);
187                                                 write_recordmarker(e, time - TIME_DECODE(t), TIME_DECODE(t));
188                                                 race_send_recordtime(t, MSG_ALL);
189                                         }
190                                         else if(t < grecordtime)
191                                         {
192                                                 recorddifference = strcat("^2", " [-", TIME_ENCODED_TOSTRING(grecordtime-t), "]");
193                                                 if(grecordholder == "")
194                                                         bprint(e.netname, strcat("^7 broke their all-time fastest lap record ", TIME_ENCODED_TOSTRING(grecordtime), " with ", TIME_ENCODED_TOSTRING(t), recorddifference), "\n");
195                                                 else
196                                                         bprint(e.netname, strcat("^7 broke ", grecordholder, "^7's all-time fastest lap record ", TIME_ENCODED_TOSTRING(grecordtime), " with ", TIME_ENCODED_TOSTRING(t), recorddifference), "\n");
197                                                 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time"), ftos(t));
198                                                 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname"), e.netname);
199                                                 write_recordmarker(e, time - TIME_DECODE(t), TIME_DECODE(t));
200                                                 race_send_recordtime(t, MSG_ALL);
201                                         }
202                                         else
203                                         {
204                                                 recorddifference = strcat("^1", " [+", TIME_ENCODED_TOSTRING(t-grecordtime), "]");
205                                                 if(grecordholder == "")
206                                                         bprint(e.netname, strcat("^7's new fastest lap ", TIME_ENCODED_TOSTRING(t), " could not break their all-time fastest lap record of ", TIME_ENCODED_TOSTRING(grecordtime), recorddifference), "\n");
207                                                 else
208                                                         bprint(e.netname, strcat("^7's new fastest lap ", TIME_ENCODED_TOSTRING(t), " could not break ", grecordholder, "^7's all-time fastest lap record of ", TIME_ENCODED_TOSTRING(grecordtime), recorddifference), "\n");
209                                         }
210                                 }
211
212                                 if(g_race_qualifying)
213                                 {
214                                         FOR_EACH_REALPLAYER(p)
215                                                 if(p.race_checkpoint == cp)
216                                                         race_SendNextCheckpoint(p, 0);
217                                 }
218                         }
219                 }
220                 else
221                 {
222                         // dummies
223                         t = 0;
224                         recordtime = 0;
225                         recordholder = "";
226                 }
227
228                 msg_entity = e;
229                 if(g_race_qualifying)
230                 {
231                         WRITESPECTATABLE_MSG_ONE_VARNAME(dummy1, {
232                                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
233                                 WriteByte(MSG_ONE, TE_CSQC_RACE);
234                                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_QUALIFYING);
235                                 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
236                                 WriteInt24_t(MSG_ONE, t); // time to that intermediate
237                                 WriteInt24_t(MSG_ONE, recordtime); // previously best time
238                                 WriteString(MSG_ONE, recordholder); // record holder
239                         });
240                 }
241         }
242         else // RACE! Not Qualifying
243         {
244                 float lself, lother, othtime;
245                 entity oth;
246                 oth = race_checkpoint_lastplayers[cp];
247                 if(oth)
248                 {
249                         lself = PlayerScore_Add(e, SP_RACE_LAPS, 0);
250                         lother = race_checkpoint_lastlaps[cp];
251                         othtime = race_checkpoint_lasttimes[cp];
252                 }
253                 else
254                         lself = lother = othtime = 0;
255
256                 msg_entity = e;
257                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy2, {
258                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
259                         WriteByte(MSG_ONE, TE_CSQC_RACE);
260                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE);
261                         WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
262                         if(e == oth)
263                         {
264                                 WriteInt24_t(MSG_ONE, 0);
265                                 WriteByte(MSG_ONE, 0);
266                                 WriteString(MSG_ONE, "");
267                         }
268                         else
269                         {
270                                 WriteInt24_t(MSG_ONE, TIME_ENCODE(time - race_checkpoint_lasttimes[cp]));
271                                 WriteByte(MSG_ONE, lself - lother);
272                                 WriteString(MSG_ONE, oth.netname); // record holder
273                         }
274                 });
275
276                 race_checkpoint_lastplayers[cp] = e;
277                 race_checkpoint_lasttimes[cp] = time;
278                 race_checkpoint_lastlaps[cp] = lself;
279
280                 msg_entity = oth;
281                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy3, {
282                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
283                         WriteByte(MSG_ONE, TE_CSQC_RACE);
284                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE_BY_OPPONENT);
285                         WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
286                         if(e == oth)
287                         {
288                                 WriteInt24_t(MSG_ONE, 0);
289                                 WriteByte(MSG_ONE, 0);
290                                 WriteString(MSG_ONE, "");
291                         }
292                         else
293                         {
294                                 WriteInt24_t(MSG_ONE, TIME_ENCODE(time - othtime));
295                                 WriteByte(MSG_ONE, lother - lself);
296                                 WriteString(MSG_ONE, e.netname); // record holder
297                         }
298                 });
299         }
300 }
301
302 void race_ClearTime(entity e)
303 {
304         e.race_checkpoint = -1;
305         e.race_laptime = 0;
306         e.race_movetime = e.race_movetime_frac = e.race_movetime_count = 0;
307         e.race_penalty_accumulator = 0;
308         e.race_lastpenalty = world;
309
310         msg_entity = e;
311         WRITESPECTATABLE_MSG_ONE({
312                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
313                 WriteByte(MSG_ONE, TE_CSQC_RACE);
314                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_CLEAR); // next
315         });
316 }
317
318 void dumpsurface(entity e)
319 {
320         float n, si, ni;
321         vector norm, vec;
322         print("Surfaces of ", etos(e), ":\n");
323
324         print("TEST = ", ftos(getsurfacenearpoint(e, '0 0 0')), "\n");
325
326         for(si = 0; ; ++si)
327         {
328                 n = getsurfacenumpoints(e, si);
329                 if(n <= 0)
330                         break;
331                 print("  Surface ", ftos(si), ":\n");
332                 norm = getsurfacenormal(e, si);
333                 print("    Normal = ", vtos(norm), "\n");
334                 for(ni = 0; ni < n; ++ni)
335                 {
336                         vec = getsurfacepoint(e, si, ni);
337                         print("    Point ", ftos(ni), " = ", vtos(vec), " (", ftos(norm * vec), ")\n");
338                 }
339         }
340 }
341
342 void checkpoint_passed()
343 {
344         string oldmsg;
345         entity cp;
346
347         if(other.classname == "porto")
348         {
349                 // do not allow portalling through checkpoints
350                 trace_plane_normal = normalize(-1 * other.velocity);
351                 self = other;
352                 W_Porto_Fail(0);
353                 return;
354         }
355
356         /*
357          * Trigger targets
358          */
359         if not((self.spawnflags & 2) && (other.classname == "player"))
360         {
361                 activator = other;
362                 oldmsg = self.message;
363                 self.message = "";
364                 SUB_UseTargets();
365                 self.message = oldmsg;
366         }
367
368         if(other.classname != "player")
369                 return;
370
371         /*
372          * Remove unauthorized equipment
373          */
374         Portal_ClearAll(other);
375
376         other.porto_forbidden = 2; // decreased by 1 each StartFrame
377
378         if(defrag_ents) {
379                 if(self.race_checkpoint == -2) 
380                 {
381                         self.race_checkpoint = other.race_checkpoint;
382                 }
383
384                 float largest_cp_id;
385                 float cp_amount;
386                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
387                         cp_amount += 1;
388                         if(cp.race_checkpoint > largest_cp_id) // update the finish id if someone hit a new checkpoint
389                         {
390                                 largest_cp_id = cp.race_checkpoint;
391                                 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
392                                         cp.race_checkpoint = largest_cp_id + 1; // finish line
393                                 race_highest_checkpoint = largest_cp_id + 1;
394                                 race_timed_checkpoint = largest_cp_id + 1;
395
396                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
397                                         if(cp.race_checkpoint == -2) // set defragcpexists to -1 so that the cp id file will be rewritten when someone finishes
398                                                 defragcpexists = -1;
399                                 }       
400                         }
401                 }
402                 if(cp_amount == 0) {
403                         for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
404                                 cp.race_checkpoint = 1;
405                         race_highest_checkpoint = 1;
406                         race_timed_checkpoint = 1;
407                 }
408         }
409
410         if((other.race_checkpoint == -1 && self.race_checkpoint == 0) || (other.race_checkpoint == self.race_checkpoint))
411         {
412                 // once he has seen the start, he can spawn normally at place-0 spawns
413                 other.race_started = 1;
414
415                 if(self.race_penalty)
416                 {
417                         if(other.race_lastpenalty != self)
418                         {
419                                 other.race_lastpenalty = self;
420                                 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
421                         }
422                 }
423
424                 if(other.race_penalty)
425                         return;
426
427                 /*
428                  * Trigger targets
429                  */
430                 if(self.spawnflags & 2)
431                 {
432                         activator = other;
433                         oldmsg = self.message;
434                         self.message = "";
435                         SUB_UseTargets();
436                         self.message = oldmsg;
437                 }
438
439                 other.race_respawn_checkpoint = self.race_checkpoint;
440                 other.race_respawn_spotref = self; // this is not a spot but a CP, but spawnpoint selection will deal with that
441                 other.race_checkpoint = race_NextCheckpoint(self.race_checkpoint);
442
443                 race_SendTime(other, self.race_checkpoint, other.race_movetime, !!other.race_laptime);
444
445                 if(!self.race_checkpoint) // start line
446                 {
447                         other.race_laptime = time;
448                         other.race_movetime = other.race_movetime_frac = other.race_movetime_count = 0;
449                         other.race_penalty_accumulator = 0;
450                         other.race_lastpenalty = world;
451                 }
452
453                 if(g_race_qualifying)
454                         race_SendNextCheckpoint(other, 0);
455
456                 if(defrag_ents && defragcpexists < 0 && self.classname == "target_stopTimer")
457                 {
458                         float fh;
459                         defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_WRITE);
460                         if(fh >= 0)
461                         {
462                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
463                                 fputs(fh, strcat(cp.targetname, " ", ftos(cp.race_checkpoint), "\n"));
464                         }
465                         fclose(fh);
466                 }
467         }
468         else if(other.race_checkpoint == race_NextCheckpoint(self.race_checkpoint))
469         {
470                 // ignored
471         }
472         else
473         {
474                 if(self.spawnflags & 4)
475                         Damage (other, self, self, 10000, DEATH_HURTTRIGGER, other.origin, '0 0 0');
476         }
477 }
478
479 void checkpoint_touch()
480 {
481         EXACTTRIGGER_TOUCH;
482         checkpoint_passed();
483 }
484
485 void checkpoint_use()
486 {
487         if(other.classname == "info_player_deathmatch") // a spawn, a spawn
488                 return;
489
490         other = activator;
491         checkpoint_passed();
492 }
493
494 float race_waypointsprite_visible_for_player(entity e)
495 {
496         if(e.race_checkpoint == -1 || e.race_checkpoint == -2)
497                 return TRUE;
498         else if(e.race_checkpoint == self.owner.race_checkpoint)
499                 return TRUE;
500         else
501                 return FALSE;
502 }
503
504 float have_verified;
505 void trigger_race_checkpoint_verify()
506 {
507         entity oldself, cp;
508         float i, p;
509         float qual;
510
511         if(have_verified)
512                 return;
513         have_verified = 1;
514         
515         qual = g_race_qualifying;
516
517         oldself = self;
518         self = spawn();
519         self.classname = "player";
520
521         if(g_race)
522         {
523                 for(i = 0; i <= race_highest_checkpoint; ++i)
524                 {
525                         self.race_checkpoint = race_NextCheckpoint(i);
526
527                         // race only (middle of the race)
528                         g_race_qualifying = 0;
529                         self.race_place = 0;
530                         if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
531                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for respawning in race) - bailing out"));
532
533                         if(i == 0)
534                         {
535                                 // qualifying only
536                                 g_race_qualifying = 1;
537                                 self.race_place = race_lowest_place_spawn;
538                                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
539                                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
540                                 
541                                 // race only (initial spawn)
542                                 g_race_qualifying = 0;
543                                 for(p = 1; p <= race_highest_place_spawn; ++p)
544                                 {
545                                         self.race_place = p;
546                                         if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
547                                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for initially spawning in race) - bailing out"));
548                                 }
549                         }
550                 }
551         }
552         else if(!defrag_ents)
553         {
554                 // qualifying only
555                 self.race_checkpoint = race_NextCheckpoint(0);
556                 g_race_qualifying = 1;
557                 self.race_place = race_lowest_place_spawn;
558                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
559                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
560         }
561         else
562         {
563                 self.race_checkpoint = race_NextCheckpoint(0);
564                 g_race_qualifying = 1;
565                 self.race_place = 0; // there's only one spawn on defrag maps
566  
567                 // check if a defragcp file already exists, then read it and apply the checkpoint order
568                 float fh;
569                 float len;
570                 string l;
571
572                 defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_READ);
573                 if(fh >= 0)
574                 {
575                         while((l = fgets(fh)))
576                         {
577                                 len = tokenize_console(l);
578                                 if(len != 2) {
579                                         defragcpexists = -1; // something's wrong in the defrag cp file, set defragcpexists to -1 so that it will be rewritten when someone finishes
580                                         continue;
581                                 }
582                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
583                                         if(argv(0) == cp.targetname)
584                                                 cp.race_checkpoint = stof(argv(1));
585                         }
586                         fclose(fh);
587                 }
588         }
589
590         g_race_qualifying = qual;
591
592         if(race_timed_checkpoint) {
593                 if(defrag_ents) {
594                         for(cp = world; (cp = find(cp, classname, "target_startTimer"));)
595                                 WaypointSprite_UpdateSprites(cp.sprite, "race-start", "", "");
596                         for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
597                                 WaypointSprite_UpdateSprites(cp.sprite, "race-finish", "", "");
598
599                         for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
600                                 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
601                                         defragcpexists = -1;
602                         }
603
604                         if(defragcpexists != -1){
605                                 float largest_cp_id;
606                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
607                                         if(cp.race_checkpoint > largest_cp_id)
608                                                 largest_cp_id = cp.race_checkpoint;
609                                 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
610                                         cp.race_checkpoint = largest_cp_id + 1; // finish line
611                                 race_highest_checkpoint = largest_cp_id + 1;
612                                 race_timed_checkpoint = largest_cp_id + 1;
613                         } else {
614                                 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
615                                         cp.race_checkpoint = 255; // finish line
616                                 race_highest_checkpoint = 255;
617                                 race_timed_checkpoint = 255;
618                         }
619                         entity targ;
620                         for(targ = world; (targ = find(targ, target, self.targetname)); )
621                                 if (targ.classname == "trigger_multiple") {
622                                         targ.wait = -2; // still needs investigating, why cant two players pass a (defrag) checkpoint at the same time?
623                                         targ.delay = 0;
624                                 }
625                 }
626                 else {
627                         for(cp = world; (cp = find(cp, classname, "trigger_race_checkpoint")); )
628                                 if(cp.sprite)
629                                 {
630                                         if(cp.race_checkpoint == 0)
631                                                 WaypointSprite_UpdateSprites(cp.sprite, "race-start", "", "");
632                                         else if(cp.race_checkpoint == race_timed_checkpoint)
633                                                 WaypointSprite_UpdateSprites(cp.sprite, "race-finish", "", "");
634                                 }
635                 }
636         }
637
638         remove(self);
639         self = oldself;
640 }
641
642 void spawnfunc_trigger_race_checkpoint()
643 {
644         vector o;
645         if(!g_race && !g_cts)
646         {
647                 remove(self);
648                 return;
649         }
650
651         EXACTTRIGGER_INIT;
652
653         self.use = checkpoint_use;
654         if not(self.spawnflags & 1)
655                 self.touch = checkpoint_touch;
656
657         o = (self.absmin + self.absmax) * 0.5;
658         tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o_z - self.absmin_z), MOVE_NORMAL, self);
659         waypoint_spawnforitem_force(self, trace_endpos);
660         self.nearestwaypointtimeout = time + 1000000000;
661
662         if(!self.message)
663                 self.message = "went backwards";
664         if (!self.message2)
665                 self.message2 = "was pushed backwards by";
666         if (!self.race_penalty_reason)
667                 self.race_penalty_reason = "missing a checkpoint";
668         
669         self.race_checkpoint = self.cnt;
670
671         if(self.race_checkpoint > race_highest_checkpoint)
672         {
673                 race_highest_checkpoint = self.race_checkpoint;
674                 if(self.spawnflags & 8)
675                         race_timed_checkpoint = self.race_checkpoint;
676                 else
677                         race_timed_checkpoint = 0;
678         }
679
680         if(!self.race_penalty)
681         {
682                 if(self.race_checkpoint)
683                         WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
684                 else
685                         WaypointSprite_SpawnFixed("race-finish", o, self, sprite);
686         }
687
688         self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
689
690         InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
691 }
692
693 void spawnfunc_target_checkpoint() // defrag entity
694 {
695         vector o;
696         if(!g_race && !g_cts)
697         {
698                 remove(self);
699                 return;
700         }
701         defrag_ents = 1;
702
703         EXACTTRIGGER_INIT;
704
705         self.use = checkpoint_use;
706         //if not(self.spawnflags & 1)
707         //      self.touch = checkpoint_touch;
708
709         o = (self.absmin + self.absmax) * 0.5;
710         tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o_z - self.absmin_z), MOVE_NORMAL, self);
711         waypoint_spawnforitem_force(self, trace_endpos);
712         self.nearestwaypointtimeout = time + 1000000000;
713
714         if(!self.message)
715                 self.message = "went backwards";
716         if (!self.message2)
717                 self.message2 = "was pushed backwards by";
718         if (!self.race_penalty_reason)
719                 self.race_penalty_reason = "missing a checkpoint";
720
721         if(self.classname == "target_startTimer")
722                 self.race_checkpoint = 0;
723         else
724                 self.race_checkpoint = -2;
725
726         race_timed_checkpoint = 1;
727
728         if(self.race_checkpoint == 0)
729                 WaypointSprite_SpawnFixed("race-start", o, self, sprite);
730         else
731                 WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
732
733         self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
734
735         InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
736 }
737
738 void spawnfunc_target_startTimer() { spawnfunc_target_checkpoint(); }
739 void spawnfunc_target_stopTimer() { spawnfunc_target_checkpoint(); }
740
741 void race_AbandonRaceCheck(entity p)
742 {
743         if(race_completing && !p.race_completed)
744         {
745                 p.race_completed = 1;
746                 MAKE_INDEPENDENT_PLAYER(p);
747                 bprint(p.netname, "^7 has abandoned the race.\n");
748                 ClientData_Touch(p);
749         }
750 }
751
752 void race_StartCompleting()
753 {
754         entity p;
755         race_completing = 1;
756         FOR_EACH_PLAYER(p)
757                 if(p.deadflag != DEAD_NO)
758                         race_AbandonRaceCheck(p);
759 }
760
761 void race_PreparePlayer()
762 {
763         race_ClearTime(self);
764         self.race_place = 0;
765         self.race_started = 0;
766         self.race_respawn_checkpoint = -1;
767         self.race_respawn_spotref = world;
768 }
769
770 void race_RetractPlayer()
771 {
772         if(!g_race && !g_cts)
773                 return;
774         if(self.race_respawn_checkpoint == 0 || self.race_respawn_checkpoint == -1 || self.race_respawn_checkpoint == race_timed_checkpoint)
775                 race_ClearTime(self);
776         self.race_checkpoint = self.race_respawn_checkpoint;
777 }
778
779 void race_PreDie()
780 {
781         if(!g_race && !g_cts)
782                 return;
783
784         race_AbandonRaceCheck(self);
785 }
786
787 void race_PreSpawn()
788 {
789         if(!g_race && !g_cts)
790                 return;
791         if(self.killcount == -666 /* initial spawn */ || g_race_qualifying) // spawn
792                 race_PreparePlayer();
793         else // respawn
794                 race_RetractPlayer();
795
796         race_AbandonRaceCheck(self);
797 }
798
799 void race_PostSpawn(entity spot)
800 {
801         if(!g_race && !g_cts)
802                 return;
803
804         if(spot.target == "")
805                 // Emergency: this wasn't a real spawnpoint. Can this ever happen?
806                 race_PreparePlayer();
807
808         // if we need to respawn, do it right
809         self.race_respawn_checkpoint = self.race_checkpoint;
810         self.race_respawn_spotref = spot;
811
812         self.race_place = 0;
813 }
814
815 void race_PreSpawnObserver()
816 {
817         if(!g_race && !g_cts)
818                 return;
819         race_PreparePlayer();
820 }
821
822 void spawnfunc_info_player_race (void)
823 {
824         if(!g_race && !g_cts)
825         {
826                 remove(self);
827                 return;
828         }
829         ++race_spawns;
830         spawnfunc_info_player_deathmatch();
831
832         if(self.race_place > race_highest_place_spawn)
833                 race_highest_place_spawn = self.race_place;
834         if(self.race_place < race_lowest_place_spawn)
835                 race_lowest_place_spawn = self.race_place;
836 }
837
838 void race_ClearRecords()
839 {
840         float i;
841         entity e;
842
843         for(i = 0; i < MAX_CHECKPOINTS; ++i)
844         {
845                 race_checkpoint_records[i] = 0;
846                 if(race_checkpoint_recordholders[i])
847                         strunzone(race_checkpoint_recordholders[i]);
848                 race_checkpoint_recordholders[i] = string_null;
849         }
850
851         e = self;
852         FOR_EACH_CLIENT(self)
853         {
854                 float p;
855                 p = self.race_place;
856                 race_PreparePlayer();
857                 self.race_place = p;
858         }
859         self = e;
860 }
861
862 void race_ReadyRestart()
863 {
864         float s;
865
866         Score_NicePrint(world);
867
868         race_ClearRecords();
869         PlayerScore_Sort(race_place);
870
871         entity e;
872         FOR_EACH_CLIENT(e)
873         {
874                 if(e.race_place)
875                 {
876                         s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
877                         if(!s)
878                                 e.race_place = 0;
879                 }
880                 print(e.netname, " = ", ftos(e.race_place), "\n");
881         }
882
883         if(g_race_qualifying == 2)
884         {
885                 g_race_qualifying = 0;
886                 independent_players = 0;
887                 cvar_set("fraglimit", ftos(race_fraglimit));
888                 cvar_set("leadlimit", ftos(race_leadlimit));
889                 cvar_set("timelimit", ftos(race_timelimit));
890                 ScoreRules_race();
891         }
892 }
893
894 void race_ImposePenaltyTime(entity pl, float penalty, string reason)
895 {
896         if(g_race_qualifying)
897         {
898                 pl.race_penalty_accumulator += penalty;
899                 msg_entity = pl;
900                 WRITESPECTATABLE_MSG_ONE({
901                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
902                         WriteByte(MSG_ONE, TE_CSQC_RACE);
903                         WriteByte(MSG_ONE, RACE_NET_PENALTY_QUALIFYING);
904                         WriteShort(MSG_ONE, TIME_ENCODE(penalty));
905                         WriteString(MSG_ONE, reason);
906                 });
907         }
908         else
909         {
910                 pl.race_penalty = time + penalty;
911                 msg_entity = pl;
912                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy, {
913                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
914                         WriteByte(MSG_ONE, TE_CSQC_RACE);
915                         WriteByte(MSG_ONE, RACE_NET_PENALTY_RACE);
916                         WriteShort(MSG_ONE, TIME_ENCODE(penalty));
917                         WriteString(MSG_ONE, reason);
918                 });
919         }
920 }
921
922 void penalty_touch()
923 {
924         EXACTTRIGGER_TOUCH;
925         if(other.race_lastpenalty != self)
926         {
927                 other.race_lastpenalty = self;
928                 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
929         }
930 }
931
932 void penalty_use()
933 {
934         race_ImposePenaltyTime(activator, self.race_penalty, self.race_penalty_reason);
935 }
936
937 void spawnfunc_trigger_race_penalty()
938 {
939         EXACTTRIGGER_INIT;
940
941         self.use = penalty_use;
942         if not(self.spawnflags & 1)
943                 self.touch = penalty_touch;
944
945         if (!self.race_penalty_reason)
946                 self.race_penalty_reason = "missing a checkpoint";
947         if (!self.race_penalty)
948                 self.race_penalty = 5;
949 }