]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/race.qc
race: do not fully verify CTS maps
[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 race_NextCheckpoint(float f)
22 {
23         if(f >= race_highest_checkpoint)
24                 return 0;
25         else
26                 return f + 1;
27 }
28
29 float race_PreviousCheckpoint(float f)
30 {
31         if(f == -1)
32                 return 0;
33         else if(f == 0)
34                 return race_highest_checkpoint;
35         else
36                 return f - 1;
37 }
38
39 // encode as:
40 //   0 = common start/finish
41 // 254 = start
42 // 255 = finish
43 float race_CheckpointNetworkID(float f)
44 {
45         if(race_timed_checkpoint)
46         {
47                 if(f == 0)
48                         return 254; // start
49                 else if(f == race_timed_checkpoint)
50                         return 255; // finish
51         }
52         return f;
53 }
54
55 void race_SendNextCheckpoint(entity e, float spec) // qualifying only
56 {
57         float recordtime;
58         string recordholder;
59         float cp;
60
61         if(!e.race_laptime)
62                 return;
63
64         cp = e.race_checkpoint;
65         recordtime = race_checkpoint_records[cp];
66         recordholder = race_checkpoint_recordholders[cp];
67         if(recordholder == e.netname)
68                 recordholder = "";
69
70         if(!spec)
71                 msg_entity = e;
72         WRITESPECTATABLE_MSG_ONE({
73                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
74                 WriteByte(MSG_ONE, TE_CSQC_RACE);
75                 if(spec)
76                 {
77                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_NEXT_SPEC_QUALIFYING);
78                         //WriteCoord(MSG_ONE, e.race_laptime - e.race_penalty_accumulator);
79                         WriteCoord(MSG_ONE, time - e.race_movetime - e.race_penalty_accumulator);
80                 }
81                 else
82                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_NEXT_QUALIFYING);
83                 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player will be at next
84                 WriteInt24_t(MSG_ONE, recordtime);
85                 WriteString(MSG_ONE, recordholder);
86         });
87 }
88
89 void race_InitSpectator()
90 {
91         if(g_race_qualifying)
92                 if(msg_entity.enemy.race_laptime)
93                         race_SendNextCheckpoint(msg_entity.enemy, 1);
94 }
95
96 void race_SendTime(entity e, float cp, float t, float tvalid)
97 {
98         float snew, l;
99         entity p;
100
101         if(g_race_qualifying)
102                 t += e.race_penalty_accumulator;
103
104         t = TIME_ENCODE(t); // make integer
105         // adding just 0.4 so it rounds down in the .5 case (matching the timer display)
106
107         if(tvalid)
108         if(cp == race_timed_checkpoint) // finish line
109         if not(e.race_completed)
110         {
111                 float s;
112                 if(g_race_qualifying)
113                 {
114                         s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
115                         if(!s || t < s)
116                                 PlayerScore_Add(e, SP_RACE_FASTEST, t - s);
117                 }
118                 else
119                 {
120                         s = PlayerScore_Add(e, SP_RACE_TIME, 0);
121                         snew = TIME_ENCODE(time - game_starttime);
122                         PlayerScore_Add(e, SP_RACE_TIME, snew - s);
123                         l = PlayerTeamScore_Add(e, SP_RACE_LAPS, ST_RACE_LAPS, 1);
124
125                         if(cvar("fraglimit"))
126                                 if(l >= cvar("fraglimit"))
127                                         race_StartCompleting();
128
129                         if(race_completing)
130                         {
131                                 e.race_completed = 1;
132                                 MAKE_INDEPENDENT_PLAYER(e);
133                                 bprint(e.netname, "^7 has finished the race.\n");
134                                 ClientData_Touch(e);
135                         }
136                 }
137         }
138
139         float recordtime;
140         string recordholder;
141
142         if(g_race_qualifying)
143         {
144                 if(tvalid)
145                 {
146                         recordtime = race_checkpoint_records[cp];
147                         recordholder = strcat1(race_checkpoint_recordholders[cp]); // make a tempstring copy, as we'll possibly strunzone it!
148                         if(recordholder == e.netname)
149                                 recordholder = "";
150
151                         if(t != 0)
152                         if(t < recordtime || recordtime == 0)
153                         {
154                                 race_checkpoint_records[cp] = t;
155                                 if(race_checkpoint_recordholders[cp])
156                                         strunzone(race_checkpoint_recordholders[cp]);
157                                 race_checkpoint_recordholders[cp] = strzone(e.netname);
158                                 if(cp == race_timed_checkpoint)
159                                 {
160                                         float grecordtime;
161                                         string grecordholder;
162                                         string rr;
163                                         if(g_cts)
164                                                 rr = CTS_RECORD;
165                                         else
166                                                 rr = RACE_RECORD;
167                                         grecordtime = stof(db_get(ServerProgsDB, strcat(GetMapname(), rr, "time")));
168                                         grecordholder = db_get(ServerProgsDB, strcat(GetMapname(), rr, "netname"));
169                                         if(grecordholder == e.netname)
170                                                 grecordholder = "";
171                                         if(grecordtime == 0)
172                                         {
173                                                 bprint(e.netname, "^7 set the all-time fastest lap record with ", TIME_ENCODED_TOSTRING(t), "\n");
174                                                 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time"), ftos(t));
175                                                 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname"), e.netname);
176                                                 write_recordmarker(e, time - TIME_DECODE(t), TIME_DECODE(t));
177                                         }
178                                         else if(t < grecordtime)
179                                         {
180                                                 if(grecordholder == "")
181                                                         bprint(e.netname, "^7 broke his all-time fastest lap record with ", TIME_ENCODED_TOSTRING(t), "\n");
182                                                 else
183                                                         bprint(e.netname, "^7 broke ", grecordholder, "^7's all-time fastest lap record with ", TIME_ENCODED_TOSTRING(t), "\n");
184                                                 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time"), ftos(t));
185                                                 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname"), e.netname);
186                                                 write_recordmarker(e, time - TIME_DECODE(t), TIME_DECODE(t));
187                                         }
188                                         else
189                                         {
190                                                 if(grecordholder == "")
191                                                         bprint(e.netname, "^7's new fastest lap could not break his all-time fastest lap record of ", TIME_ENCODED_TOSTRING(grecordtime), "\n");
192                                                 else
193                                                         bprint(e.netname, "^7's new fastest lap could not break ", grecordholder, "^7's all-time fastest lap record of ", TIME_ENCODED_TOSTRING(grecordtime), "\n");
194                                         }
195                                 }
196
197                                 if(g_race_qualifying)
198                                 {
199                                         FOR_EACH_REALPLAYER(p)
200                                                 if(p.race_checkpoint == cp)
201                                                         race_SendNextCheckpoint(p, 0);
202                                 }
203                         }
204                 }
205                 else
206                 {
207                         // dummies
208                         t = 0;
209                         recordtime = 0;
210                         recordholder = "";
211                 }
212
213                 msg_entity = e;
214                 if(g_race_qualifying)
215                 {
216                         WRITESPECTATABLE_MSG_ONE_VARNAME(dummy1, {
217                                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
218                                 WriteByte(MSG_ONE, TE_CSQC_RACE);
219                                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_QUALIFYING);
220                                 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
221                                 WriteInt24_t(MSG_ONE, t); // time to that intermediate
222                                 WriteInt24_t(MSG_ONE, recordtime); // previously best time
223                                 WriteString(MSG_ONE, recordholder); // record holder
224                         });
225                 }
226         }
227         else // RACE! Not Qualifying
228         {
229                 float lself, lother, othtime;
230                 entity oth;
231                 oth = race_checkpoint_lastplayers[cp];
232                 if(oth)
233                 {
234                         lself = PlayerScore_Add(e, SP_RACE_LAPS, 0);
235                         lother = race_checkpoint_lastlaps[cp];
236                         othtime = race_checkpoint_lasttimes[cp];
237                 }
238                 else
239                         lself = lother = othtime = 0;
240
241                 msg_entity = e;
242                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy2, {
243                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
244                         WriteByte(MSG_ONE, TE_CSQC_RACE);
245                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE);
246                         WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
247                         if(e == oth)
248                         {
249                                 WriteInt24_t(MSG_ONE, 0);
250                                 WriteByte(MSG_ONE, 0);
251                                 WriteString(MSG_ONE, "");
252                         }
253                         else
254                         {
255                                 WriteInt24_t(MSG_ONE, TIME_ENCODE(time - race_checkpoint_lasttimes[cp]));
256                                 WriteByte(MSG_ONE, lself - lother);
257                                 WriteString(MSG_ONE, oth.netname); // record holder
258                         }
259                 });
260
261                 race_checkpoint_lastplayers[cp] = e;
262                 race_checkpoint_lasttimes[cp] = time;
263                 race_checkpoint_lastlaps[cp] = lself;
264
265                 msg_entity = oth;
266                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy3, {
267                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
268                         WriteByte(MSG_ONE, TE_CSQC_RACE);
269                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE_BY_OPPONENT);
270                         WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
271                         if(e == oth)
272                         {
273                                 WriteInt24_t(MSG_ONE, 0);
274                                 WriteByte(MSG_ONE, 0);
275                                 WriteString(MSG_ONE, "");
276                         }
277                         else
278                         {
279                                 WriteInt24_t(MSG_ONE, TIME_ENCODE(time - othtime));
280                                 WriteByte(MSG_ONE, lother - lself);
281                                 WriteString(MSG_ONE, e.netname); // record holder
282                         }
283                 });
284         }
285 }
286
287 void race_ClearTime(entity e)
288 {
289         e.race_checkpoint = -1;
290         e.race_laptime = 0;
291         e.race_movetime = e.race_movetime_frac = e.race_movetime_count = 0;
292         e.race_penalty_accumulator = 0;
293         e.race_lastpenalty = world;
294
295         msg_entity = e;
296         WRITESPECTATABLE_MSG_ONE({
297                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
298                 WriteByte(MSG_ONE, TE_CSQC_RACE);
299                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_CLEAR); // next
300         });
301 }
302
303 void dumpsurface(entity e)
304 {
305         float n, si, ni;
306         vector norm, vec;
307         print("Surfaces of ", etos(e), ":\n");
308
309         print("TEST = ", ftos(getsurfacenearpoint(e, '0 0 0')), "\n");
310
311         for(si = 0; ; ++si)
312         {
313                 n = getsurfacenumpoints(e, si);
314                 if(n <= 0)
315                         break;
316                 print("  Surface ", ftos(si), ":\n");
317                 norm = getsurfacenormal(e, si);
318                 print("    Normal = ", vtos(norm), "\n");
319                 for(ni = 0; ni < n; ++ni)
320                 {
321                         vec = getsurfacepoint(e, si, ni);
322                         print("    Point ", ftos(ni), " = ", vtos(vec), " (", ftos(norm * vec), ")\n");
323                 }
324         }
325 }
326
327 void checkpoint_passed()
328 {
329         string oldmsg;
330
331         if(other.classname == "porto")
332         {
333                 // do not allow portalling through checkpoints
334                 trace_plane_normal = normalize(-1 * other.velocity);
335                 self = other;
336                 W_Porto_Fail(0);
337                 return;
338         }
339
340         /*
341          * Trigger targets
342          */
343         if not(self.spawnflags & 2)
344         {
345                 activator = other;
346                 oldmsg = self.message;
347                 self.message = "";
348                 SUB_UseTargets();
349                 self.message = oldmsg;
350         }
351
352         if(other.classname != "player")
353                 return;
354
355         /*
356          * Remove unauthorized equipment
357          */
358         Portal_ClearAll(other);
359
360         other.porto_forbidden = 2; // decreased by 1 each StartFrame
361
362         if(other.race_checkpoint == -1 || other.race_checkpoint == self.race_checkpoint)
363         {
364                 if(self.race_penalty)
365                 {
366                         if(other.race_lastpenalty != self)
367                         {
368                                 other.race_lastpenalty = self;
369                                 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
370                         }
371                 }
372
373                 if(other.race_penalty)
374                         return;
375
376                 /*
377                  * Trigger targets
378                  */
379                 if(self.spawnflags & 2)
380                 {
381                         activator = other;
382                         oldmsg = self.message;
383                         self.message = "";
384                         SUB_UseTargets();
385                         self.message = oldmsg;
386                 }
387
388                 other.race_checkpoint = race_NextCheckpoint(self.race_checkpoint);
389
390                 race_SendTime(other, self.race_checkpoint, other.race_movetime, !!other.race_laptime);
391
392                 if(!self.race_checkpoint) // start line
393                 {
394                         other.race_laptime = time;
395                         other.race_movetime = other.race_movetime_frac = other.race_movetime_count = 0;
396                         other.race_penalty_accumulator = 0;
397                         other.race_lastpenalty = world;
398                 }
399
400                 if(g_race_qualifying)
401                         race_SendNextCheckpoint(other, 0);
402         }
403         else if(other.race_checkpoint == race_NextCheckpoint(self.race_checkpoint))
404         {
405                 // ignored
406         }
407         else
408         {
409                 if(self.spawnflags & 4)
410                         Damage (other, self, self, 10000, DEATH_HURTTRIGGER, other.origin, '0 0 0');
411         }
412 }
413
414 void checkpoint_touch()
415 {
416         EXACTTRIGGER_TOUCH;
417         checkpoint_passed();
418 }
419
420 void checkpoint_use()
421 {
422         if(other.classname == "info_player_deathmatch") // a spawn, a spawn
423                 return;
424
425         other = activator;
426         checkpoint_passed();
427 }
428
429 float race_waypointsprite_visible_for_player(entity e)
430 {
431         if(e.race_checkpoint == -1)
432                 return TRUE;
433         else if(e.race_checkpoint == self.owner.race_checkpoint)
434                 return TRUE;
435         else
436                 return FALSE;
437 }
438
439 float have_verified;
440 void trigger_race_checkpoint_verify()
441 {
442         entity oldself, cp;
443         float i, p;
444         float qual;
445
446         if(have_verified)
447                 return;
448         have_verified = 1;
449         
450         qual = g_race_qualifying;
451
452         oldself = self;
453         self = spawn();
454         self.classname = "player";
455
456         if(g_race)
457         {
458                 for(i = 0; i <= race_highest_checkpoint; ++i)
459                 {
460                         self.race_checkpoint = race_NextCheckpoint(i);
461
462                         // race only (middle of the race)
463                         g_race_qualifying = 0;
464                         self.race_place = 0;
465                         if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE))
466                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for respawning in race) - bailing out"));
467
468                         if(i == 0)
469                         {
470                                 // qualifying only
471                                 g_race_qualifying = 1;
472                                 self.race_place = race_lowest_place_spawn;
473                                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE))
474                                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
475                                 
476                                 // race only (initial spawn)
477                                 g_race_qualifying = 0;
478                                 for(p = 1; p <= race_highest_place_spawn; ++p)
479                                 {
480                                         self.race_place = p;
481                                         if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE))
482                                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for initially spawning in race) - bailing out"));
483                                 }
484                         }
485                 }
486         }
487         else
488         {
489                 // qualifying only
490                 self.race_checkpoint = race_NextCheckpoint(0);
491                 g_race_qualifying = 1;
492                 self.race_place = race_lowest_place_spawn;
493                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE))
494                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
495         }
496
497         g_race_qualifying = qual;
498
499         if(race_timed_checkpoint)
500                 for(cp = world; (cp = find(cp, classname, "trigger_race_checkpoint")); )
501                         if(cp.sprite)
502                         {
503                                 if(cp.race_checkpoint == 0)
504                                         WaypointSprite_UpdateSprites(cp.sprite, "race-start", "", "");
505                                 else if(cp.race_checkpoint == race_timed_checkpoint)
506                                         WaypointSprite_UpdateSprites(cp.sprite, "race-finish", "", "");
507                         }
508
509         remove(self);
510         self = oldself;
511 }
512
513 void spawnfunc_trigger_race_checkpoint()
514 {
515         vector o;
516         if(!g_race && !g_cts)
517         {
518                 remove(self);
519                 return;
520         }
521
522         EXACTTRIGGER_INIT;
523
524         self.use = checkpoint_use;
525         if not(self.spawnflags & 1)
526                 self.touch = checkpoint_touch;
527
528         o = (self.absmin + self.absmax) * 0.5;
529         tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o_z - self.absmin_z), MOVE_NORMAL, self);
530         waypoint_spawnforitem_force(self, trace_endpos);
531
532         if(!self.message)
533                 self.message = "went backwards";
534         if (!self.message2)
535                 self.message2 = "was pushed backwards by";
536         if (!self.race_penalty_reason)
537                 self.race_penalty_reason = "missing a checkpoint";
538         
539         self.race_checkpoint = self.cnt;
540
541         if(self.race_checkpoint > race_highest_checkpoint)
542         {
543                 race_highest_checkpoint = self.race_checkpoint;
544                 if(self.spawnflags & 8)
545                         race_timed_checkpoint = self.race_checkpoint;
546                 else
547                         race_timed_checkpoint = 0;
548         }
549
550         if(!self.race_penalty)
551         {
552                 if(self.race_checkpoint)
553                         WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
554                 else
555                         WaypointSprite_SpawnFixed("race-finish", o, self, sprite);
556         }
557
558         self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
559
560         InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
561 }
562
563 void race_AbandonRaceCheck(entity p)
564 {
565         if(race_completing && !p.race_completed)
566         {
567                 p.race_completed = 1;
568                 MAKE_INDEPENDENT_PLAYER(p);
569                 bprint(p.netname, "^7 has abandoned the race.\n");
570                 ClientData_Touch(p);
571         }
572 }
573
574 void race_StartCompleting()
575 {
576         entity p;
577         race_completing = 1;
578         FOR_EACH_PLAYER(p)
579                 if(p.deadflag != DEAD_NO)
580                         race_AbandonRaceCheck(p);
581 }
582
583 void race_PreparePlayer()
584 {
585         race_ClearTime(self);
586         self.race_place = 0;
587 }
588
589 void race_RetractPlayer()
590 {
591         if(!g_race && !g_cts)
592                 return;
593         self.race_checkpoint = race_PreviousCheckpoint(self.race_checkpoint);
594         if(self.race_checkpoint == 0)
595         {
596                 race_ClearTime(self);
597                 self.race_checkpoint = 0;
598         }
599 }
600
601 void race_PreDie()
602 {
603         if(!g_race && !g_cts)
604                 return;
605
606         race_AbandonRaceCheck(self);
607 }
608
609 void race_PreSpawn()
610 {
611         if(!g_race && !g_cts)
612                 return;
613         if(self.killcount == -666 || g_race_qualifying)
614                 race_PreparePlayer();
615
616         race_AbandonRaceCheck(self);
617 }
618
619 void race_PostSpawn(entity spot)
620 {
621         if(!g_race && !g_cts)
622                 return;
623         if(self.killcount != -666 && !g_race_qualifying)
624         {
625                 if(spot.target == "")
626                         // let the player run without timing, if he did not spawn at a targetting spawnpoint
627                         race_PreparePlayer();
628                 else
629                         race_RetractPlayer();
630         }
631
632         if(spot.target != "" && self.race_checkpoint == -1)
633                 self.race_checkpoint = 0;
634
635         self.race_place = 0;
636 }
637
638 void race_PreSpawnObserver()
639 {
640         if(!g_race && !g_cts)
641                 return;
642         race_PreparePlayer();
643 }
644
645 void spawnfunc_info_player_race (void)
646 {
647         if(!g_race && !g_cts)
648         {
649                 remove(self);
650                 return;
651         }
652         ++race_spawns;
653         spawnfunc_info_player_deathmatch();
654
655         if(self.race_place > race_highest_place_spawn)
656                 race_highest_place_spawn = self.race_place;
657         if(self.race_place < race_lowest_place_spawn)
658                 race_lowest_place_spawn = self.race_place;
659 }
660
661 void race_ClearRecords()
662 {
663         float i;
664         entity e;
665
666         for(i = 0; i < MAX_CHECKPOINTS; ++i)
667         {
668                 race_checkpoint_records[i] = 0;
669                 if(race_checkpoint_recordholders[i])
670                         strunzone(race_checkpoint_recordholders[i]);
671                 race_checkpoint_recordholders[i] = string_null;
672         }
673
674         FOR_EACH_CLIENT(e)
675                 race_ClearTime(e);
676 }
677
678 void race_ReadyRestart()
679 {
680         float s;
681
682         Score_NicePrint(world);
683
684         race_ClearRecords();
685         PlayerScore_Sort(race_place);
686
687         entity e;
688         FOR_EACH_CLIENT(e)
689         {
690                 if(e.race_place)
691                 {
692                         s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
693                         if(!s)
694                                 e.race_place = 0;
695                 }
696                 print(e.netname, " = ", ftos(e.race_place), "\n");
697         }
698
699         if(g_race_qualifying == 2)
700         {
701                 g_race_qualifying = 0;
702                 independent_players = 0;
703                 cvar_set("fraglimit", ftos(race_fraglimit));
704                 cvar_set("leadlimit", ftos(race_leadlimit));
705                 cvar_set("timelimit", ftos(race_timelimit));
706                 ScoreRules_race();
707         }
708 }
709
710 void race_ImposePenaltyTime(entity pl, float penalty, string reason)
711 {
712         if(g_race_qualifying)
713         {
714                 pl.race_penalty_accumulator += penalty;
715                 msg_entity = pl;
716                 WRITESPECTATABLE_MSG_ONE({
717                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
718                         WriteByte(MSG_ONE, TE_CSQC_RACE);
719                         WriteByte(MSG_ONE, RACE_NET_PENALTY_QUALIFYING);
720                         WriteShort(MSG_ONE, TIME_ENCODE(penalty));
721                         WriteString(MSG_ONE, reason);
722                 });
723         }
724         else
725         {
726                 pl.race_penalty = time + penalty;
727                 msg_entity = pl;
728                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy, {
729                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
730                         WriteByte(MSG_ONE, TE_CSQC_RACE);
731                         WriteByte(MSG_ONE, RACE_NET_PENALTY_RACE);
732                         WriteShort(MSG_ONE, TIME_ENCODE(penalty));
733                         WriteString(MSG_ONE, reason);
734                 });
735         }
736 }
737
738 void penalty_touch()
739 {
740         EXACTTRIGGER_TOUCH;
741         if(other.race_lastpenalty != self)
742         {
743                 other.race_lastpenalty = self;
744                 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
745         }
746 }
747
748 void penalty_use()
749 {
750         race_ImposePenaltyTime(activator, self.race_penalty, self.race_penalty_reason);
751 }
752
753 void spawnfunc_trigger_race_penalty()
754 {
755         EXACTTRIGGER_INIT;
756
757         self.use = penalty_use;
758         if not(self.spawnflags & 1)
759                 self.touch = penalty_touch;
760
761         if (!self.race_penalty_reason)
762                 self.race_penalty_reason = "missing a checkpoint";
763         if (!self.race_penalty)
764                 self.race_penalty = 5;
765 }