]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/race.qc
some bot fixes :(
[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         self.nearestwaypointtimeout = time + 1000000000;
532
533         if(!self.message)
534                 self.message = "went backwards";
535         if (!self.message2)
536                 self.message2 = "was pushed backwards by";
537         if (!self.race_penalty_reason)
538                 self.race_penalty_reason = "missing a checkpoint";
539         
540         self.race_checkpoint = self.cnt;
541
542         if(self.race_checkpoint > race_highest_checkpoint)
543         {
544                 race_highest_checkpoint = self.race_checkpoint;
545                 if(self.spawnflags & 8)
546                         race_timed_checkpoint = self.race_checkpoint;
547                 else
548                         race_timed_checkpoint = 0;
549         }
550
551         if(!self.race_penalty)
552         {
553                 if(self.race_checkpoint)
554                         WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
555                 else
556                         WaypointSprite_SpawnFixed("race-finish", o, self, sprite);
557         }
558
559         self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
560
561         InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
562 }
563
564 void race_AbandonRaceCheck(entity p)
565 {
566         if(race_completing && !p.race_completed)
567         {
568                 p.race_completed = 1;
569                 MAKE_INDEPENDENT_PLAYER(p);
570                 bprint(p.netname, "^7 has abandoned the race.\n");
571                 ClientData_Touch(p);
572         }
573 }
574
575 void race_StartCompleting()
576 {
577         entity p;
578         race_completing = 1;
579         FOR_EACH_PLAYER(p)
580                 if(p.deadflag != DEAD_NO)
581                         race_AbandonRaceCheck(p);
582 }
583
584 void race_PreparePlayer()
585 {
586         race_ClearTime(self);
587         self.race_place = 0;
588 }
589
590 void race_RetractPlayer()
591 {
592         if(!g_race && !g_cts)
593                 return;
594         self.race_checkpoint = race_PreviousCheckpoint(self.race_checkpoint);
595         if(self.race_checkpoint == 0)
596         {
597                 race_ClearTime(self);
598                 self.race_checkpoint = 0;
599         }
600 }
601
602 void race_PreDie()
603 {
604         if(!g_race && !g_cts)
605                 return;
606
607         race_AbandonRaceCheck(self);
608 }
609
610 void race_PreSpawn()
611 {
612         if(!g_race && !g_cts)
613                 return;
614         if(self.killcount == -666 || g_race_qualifying)
615                 race_PreparePlayer();
616
617         race_AbandonRaceCheck(self);
618 }
619
620 void race_PostSpawn(entity spot)
621 {
622         if(!g_race && !g_cts)
623                 return;
624         if(self.killcount != -666 && !g_race_qualifying)
625         {
626                 if(spot.target == "")
627                         // let the player run without timing, if he did not spawn at a targetting spawnpoint
628                         race_PreparePlayer();
629                 else
630                         race_RetractPlayer();
631         }
632
633         if(spot.target != "" && self.race_checkpoint == -1)
634                 self.race_checkpoint = 0;
635
636         self.race_place = 0;
637 }
638
639 void race_PreSpawnObserver()
640 {
641         if(!g_race && !g_cts)
642                 return;
643         race_PreparePlayer();
644 }
645
646 void spawnfunc_info_player_race (void)
647 {
648         if(!g_race && !g_cts)
649         {
650                 remove(self);
651                 return;
652         }
653         ++race_spawns;
654         spawnfunc_info_player_deathmatch();
655
656         if(self.race_place > race_highest_place_spawn)
657                 race_highest_place_spawn = self.race_place;
658         if(self.race_place < race_lowest_place_spawn)
659                 race_lowest_place_spawn = self.race_place;
660 }
661
662 void race_ClearRecords()
663 {
664         float i;
665         entity e;
666
667         for(i = 0; i < MAX_CHECKPOINTS; ++i)
668         {
669                 race_checkpoint_records[i] = 0;
670                 if(race_checkpoint_recordholders[i])
671                         strunzone(race_checkpoint_recordholders[i]);
672                 race_checkpoint_recordholders[i] = string_null;
673         }
674
675         FOR_EACH_CLIENT(e)
676                 race_ClearTime(e);
677 }
678
679 void race_ReadyRestart()
680 {
681         float s;
682
683         Score_NicePrint(world);
684
685         race_ClearRecords();
686         PlayerScore_Sort(race_place);
687
688         entity e;
689         FOR_EACH_CLIENT(e)
690         {
691                 if(e.race_place)
692                 {
693                         s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
694                         if(!s)
695                                 e.race_place = 0;
696                 }
697                 print(e.netname, " = ", ftos(e.race_place), "\n");
698         }
699
700         if(g_race_qualifying == 2)
701         {
702                 g_race_qualifying = 0;
703                 independent_players = 0;
704                 cvar_set("fraglimit", ftos(race_fraglimit));
705                 cvar_set("leadlimit", ftos(race_leadlimit));
706                 cvar_set("timelimit", ftos(race_timelimit));
707                 ScoreRules_race();
708         }
709 }
710
711 void race_ImposePenaltyTime(entity pl, float penalty, string reason)
712 {
713         if(g_race_qualifying)
714         {
715                 pl.race_penalty_accumulator += penalty;
716                 msg_entity = pl;
717                 WRITESPECTATABLE_MSG_ONE({
718                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
719                         WriteByte(MSG_ONE, TE_CSQC_RACE);
720                         WriteByte(MSG_ONE, RACE_NET_PENALTY_QUALIFYING);
721                         WriteShort(MSG_ONE, TIME_ENCODE(penalty));
722                         WriteString(MSG_ONE, reason);
723                 });
724         }
725         else
726         {
727                 pl.race_penalty = time + penalty;
728                 msg_entity = pl;
729                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy, {
730                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
731                         WriteByte(MSG_ONE, TE_CSQC_RACE);
732                         WriteByte(MSG_ONE, RACE_NET_PENALTY_RACE);
733                         WriteShort(MSG_ONE, TIME_ENCODE(penalty));
734                         WriteString(MSG_ONE, reason);
735                 });
736         }
737 }
738
739 void penalty_touch()
740 {
741         EXACTTRIGGER_TOUCH;
742         if(other.race_lastpenalty != self)
743         {
744                 other.race_lastpenalty = self;
745                 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
746         }
747 }
748
749 void penalty_use()
750 {
751         race_ImposePenaltyTime(activator, self.race_penalty, self.race_penalty_reason);
752 }
753
754 void spawnfunc_trigger_race_penalty()
755 {
756         EXACTTRIGGER_INIT;
757
758         self.use = penalty_use;
759         if not(self.spawnflags & 1)
760                 self.touch = penalty_touch;
761
762         if (!self.race_penalty_reason)
763                 self.race_penalty_reason = "missing a checkpoint";
764         if (!self.race_penalty)
765                 self.race_penalty = 5;
766 }