]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/race.qc
magic ear: decolorize before matching
[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         for(i = 0; i <= race_highest_checkpoint; ++i)
457         {
458                 self.race_checkpoint = race_NextCheckpoint(i);
459
460                 // race only (middle of the race)
461                 g_race_qualifying = 0;
462                 self.race_place = 0;
463                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE))
464                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for respawning in race) - bailing out"));
465
466                 if(i == 0)
467                 {
468                         // qualifying only
469                         g_race_qualifying = 1;
470                         self.race_place = race_lowest_place_spawn;
471                         if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE))
472                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
473                         
474                         // race only (initial spawn)
475                         g_race_qualifying = 0;
476                         for(p = 1; p <= race_highest_place_spawn; ++p)
477                         {
478                                 self.race_place = p;
479                                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE))
480                                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for initially spawning in race) - bailing out"));
481                         }
482                 }
483         }
484
485         g_race_qualifying = qual;
486
487         if(race_timed_checkpoint)
488                 for(cp = world; (cp = find(cp, classname, "trigger_race_checkpoint")); )
489                         if(cp.sprite)
490                         {
491                                 if(cp.race_checkpoint == 0)
492                                         WaypointSprite_UpdateSprites(cp.sprite, "race-start", "", "");
493                                 else if(cp.race_checkpoint == race_timed_checkpoint)
494                                         WaypointSprite_UpdateSprites(cp.sprite, "race-finish", "", "");
495                         }
496
497         remove(self);
498         self = oldself;
499 }
500
501 void spawnfunc_trigger_race_checkpoint()
502 {
503         vector o;
504         if(!g_race && !g_cts)
505         {
506                 remove(self);
507                 return;
508         }
509
510         EXACTTRIGGER_INIT;
511
512         self.use = checkpoint_use;
513         if not(self.spawnflags & 1)
514                 self.touch = checkpoint_touch;
515
516         o = (self.absmin + self.absmax) * 0.5;
517         tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o_z - self.absmin_z), MOVE_NORMAL, self);
518         waypoint_spawnforitem_force(self, trace_endpos);
519
520         if(!self.message)
521                 self.message = "went backwards";
522         if (!self.message2)
523                 self.message2 = "was pushed backwards by";
524         if (!self.race_penalty_reason)
525                 self.race_penalty_reason = "missing a checkpoint";
526         
527         self.race_checkpoint = self.cnt;
528
529         if(self.race_checkpoint > race_highest_checkpoint)
530         {
531                 race_highest_checkpoint = self.race_checkpoint;
532                 if(self.spawnflags & 8)
533                         race_timed_checkpoint = self.race_checkpoint;
534                 else
535                         race_timed_checkpoint = 0;
536         }
537
538         if(!self.race_penalty)
539         {
540                 if(self.race_checkpoint)
541                         WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
542                 else
543                         WaypointSprite_SpawnFixed("race-finish", o, self, sprite);
544         }
545
546         self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
547
548         InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
549 }
550
551 void race_AbandonRaceCheck(entity p)
552 {
553         if(race_completing && !p.race_completed)
554         {
555                 p.race_completed = 1;
556                 MAKE_INDEPENDENT_PLAYER(p);
557                 bprint(p.netname, "^7 has abandoned the race.\n");
558                 ClientData_Touch(p);
559         }
560 }
561
562 void race_StartCompleting()
563 {
564         entity p;
565         race_completing = 1;
566         FOR_EACH_PLAYER(p)
567                 if(p.deadflag != DEAD_NO)
568                         race_AbandonRaceCheck(p);
569 }
570
571 void race_PreparePlayer()
572 {
573         race_ClearTime(self);
574         self.race_place = 0;
575 }
576
577 void race_RetractPlayer()
578 {
579         if(!g_race && !g_cts)
580                 return;
581         self.race_checkpoint = race_PreviousCheckpoint(self.race_checkpoint);
582         if(self.race_checkpoint == 0)
583         {
584                 race_ClearTime(self);
585                 self.race_checkpoint = 0;
586         }
587 }
588
589 void race_PreDie()
590 {
591         if(!g_race && !g_cts)
592                 return;
593
594         race_AbandonRaceCheck(self);
595 }
596
597 void race_PreSpawn()
598 {
599         if(!g_race && !g_cts)
600                 return;
601         if(self.killcount == -666 || g_race_qualifying)
602                 race_PreparePlayer();
603
604         race_AbandonRaceCheck(self);
605 }
606
607 void race_PostSpawn(entity spot)
608 {
609         if(!g_race && !g_cts)
610                 return;
611         if(self.killcount != -666 && !g_race_qualifying)
612         {
613                 if(spot.target == "")
614                         // let the player run without timing, if he did not spawn at a targetting spawnpoint
615                         race_PreparePlayer();
616                 else
617                         race_RetractPlayer();
618         }
619
620         if(spot.target != "" && self.race_checkpoint == -1)
621                 self.race_checkpoint = 0;
622
623         self.race_place = 0;
624 }
625
626 void race_PreSpawnObserver()
627 {
628         if(!g_race && !g_cts)
629                 return;
630         race_PreparePlayer();
631 }
632
633 void spawnfunc_info_player_race (void)
634 {
635         if(!g_race && !g_cts)
636         {
637                 remove(self);
638                 return;
639         }
640         ++race_spawns;
641         spawnfunc_info_player_deathmatch();
642
643         if(self.race_place > race_highest_place_spawn)
644                 race_highest_place_spawn = self.race_place;
645         if(self.race_place < race_lowest_place_spawn)
646                 race_lowest_place_spawn = self.race_place;
647 }
648
649 void race_ClearRecords()
650 {
651         float i;
652         entity e;
653
654         for(i = 0; i < MAX_CHECKPOINTS; ++i)
655         {
656                 race_checkpoint_records[i] = 0;
657                 if(race_checkpoint_recordholders[i])
658                         strunzone(race_checkpoint_recordholders[i]);
659                 race_checkpoint_recordholders[i] = string_null;
660         }
661
662         FOR_EACH_CLIENT(e)
663                 race_ClearTime(e);
664 }
665
666 void race_ReadyRestart()
667 {
668         float s;
669
670         Score_NicePrint(world);
671
672         race_ClearRecords();
673         PlayerScore_Sort(race_place);
674
675         entity e;
676         FOR_EACH_CLIENT(e)
677         {
678                 if(e.race_place)
679                 {
680                         s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
681                         if(!s)
682                                 e.race_place = 0;
683                 }
684                 print(e.netname, " = ", ftos(e.race_place), "\n");
685         }
686
687         if(g_race_qualifying == 2)
688         {
689                 g_race_qualifying = 0;
690                 independent_players = 0;
691                 cvar_set("fraglimit", ftos(race_fraglimit));
692                 cvar_set("leadlimit", ftos(race_leadlimit));
693                 cvar_set("timelimit", ftos(race_timelimit));
694                 ScoreRules_race();
695         }
696 }
697
698 void race_ImposePenaltyTime(entity pl, float penalty, string reason)
699 {
700         if(g_race_qualifying)
701         {
702                 pl.race_penalty_accumulator += penalty;
703                 msg_entity = pl;
704                 WRITESPECTATABLE_MSG_ONE({
705                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
706                         WriteByte(MSG_ONE, TE_CSQC_RACE);
707                         WriteByte(MSG_ONE, RACE_NET_PENALTY_QUALIFYING);
708                         WriteShort(MSG_ONE, TIME_ENCODE(penalty));
709                         WriteString(MSG_ONE, reason);
710                 });
711         }
712         else
713         {
714                 pl.race_penalty = time + penalty;
715                 msg_entity = pl;
716                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy, {
717                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
718                         WriteByte(MSG_ONE, TE_CSQC_RACE);
719                         WriteByte(MSG_ONE, RACE_NET_PENALTY_RACE);
720                         WriteShort(MSG_ONE, TIME_ENCODE(penalty));
721                         WriteString(MSG_ONE, reason);
722                 });
723         }
724 }
725
726 void penalty_touch()
727 {
728         EXACTTRIGGER_TOUCH;
729         if(other.race_lastpenalty != self)
730         {
731                 other.race_lastpenalty = self;
732                 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
733         }
734 }
735
736 void penalty_use()
737 {
738         race_ImposePenaltyTime(activator, self.race_penalty, self.race_penalty_reason);
739 }
740
741 void spawnfunc_trigger_race_penalty()
742 {
743         EXACTTRIGGER_INIT;
744
745         self.use = penalty_use;
746         if not(self.spawnflags & 1)
747                 self.touch = penalty_touch;
748
749         if (!self.race_penalty_reason)
750                 self.race_penalty_reason = "missing a checkpoint";
751         if (!self.race_penalty)
752                 self.race_penalty = 5;
753 }