]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/race.qc
Only update spotref if we ACTUALLY advanced by a CP. So if you spawn at the finish...
[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                 if(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_respawn_checkpoint = self.race_checkpoint;
442                 other.race_checkpoint = race_NextCheckpoint(self.race_checkpoint);
443
444                 race_SendTime(other, self.race_checkpoint, other.race_movetime, !!other.race_laptime);
445
446                 if(!self.race_checkpoint) // start line
447                 {
448                         other.race_laptime = time;
449                         other.race_movetime = other.race_movetime_frac = other.race_movetime_count = 0;
450                         other.race_penalty_accumulator = 0;
451                         other.race_lastpenalty = world;
452                 }
453
454                 if(g_race_qualifying)
455                         race_SendNextCheckpoint(other, 0);
456
457                 if(defrag_ents && defragcpexists < 0 && self.classname == "target_stopTimer")
458                 {
459                         float fh;
460                         defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_WRITE);
461                         if(fh >= 0)
462                         {
463                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
464                                 fputs(fh, strcat(cp.targetname, " ", ftos(cp.race_checkpoint), "\n"));
465                         }
466                         fclose(fh);
467                 }
468         }
469         else if(other.race_checkpoint == race_NextCheckpoint(self.race_checkpoint))
470         {
471                 // ignored
472         }
473         else
474         {
475                 if(self.spawnflags & 4)
476                         Damage (other, self, self, 10000, DEATH_HURTTRIGGER, other.origin, '0 0 0');
477         }
478 }
479
480 void checkpoint_touch()
481 {
482         EXACTTRIGGER_TOUCH;
483         checkpoint_passed();
484 }
485
486 void checkpoint_use()
487 {
488         if(other.classname == "info_player_deathmatch") // a spawn, a spawn
489                 return;
490
491         other = activator;
492         checkpoint_passed();
493 }
494
495 float race_waypointsprite_visible_for_player(entity e)
496 {
497         if(e.race_checkpoint == -1 || e.race_checkpoint == -2)
498                 return TRUE;
499         else if(e.race_checkpoint == self.owner.race_checkpoint)
500                 return TRUE;
501         else
502                 return FALSE;
503 }
504
505 float have_verified;
506 void trigger_race_checkpoint_verify()
507 {
508         entity oldself, cp;
509         float i, p;
510         float qual;
511
512         if(have_verified)
513                 return;
514         have_verified = 1;
515         
516         qual = g_race_qualifying;
517
518         oldself = self;
519         self = spawn();
520         self.classname = "player";
521
522         if(g_race)
523         {
524                 for(i = 0; i <= race_highest_checkpoint; ++i)
525                 {
526                         self.race_checkpoint = race_NextCheckpoint(i);
527
528                         // race only (middle of the race)
529                         g_race_qualifying = 0;
530                         self.race_place = 0;
531                         if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
532                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for respawning in race) - bailing out"));
533
534                         if(i == 0)
535                         {
536                                 // qualifying only
537                                 g_race_qualifying = 1;
538                                 self.race_place = race_lowest_place_spawn;
539                                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
540                                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
541                                 
542                                 // race only (initial spawn)
543                                 g_race_qualifying = 0;
544                                 for(p = 1; p <= race_highest_place_spawn; ++p)
545                                 {
546                                         self.race_place = p;
547                                         if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
548                                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for initially spawning in race) - bailing out"));
549                                 }
550                         }
551                 }
552         }
553         else if(!defrag_ents)
554         {
555                 // qualifying only
556                 self.race_checkpoint = race_NextCheckpoint(0);
557                 g_race_qualifying = 1;
558                 self.race_place = race_lowest_place_spawn;
559                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
560                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
561         }
562         else
563         {
564                 self.race_checkpoint = race_NextCheckpoint(0);
565                 g_race_qualifying = 1;
566                 self.race_place = 0; // there's only one spawn on defrag maps
567  
568                 // check if a defragcp file already exists, then read it and apply the checkpoint order
569                 float fh;
570                 float len;
571                 string l;
572
573                 defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_READ);
574                 if(fh >= 0)
575                 {
576                         while((l = fgets(fh)))
577                         {
578                                 len = tokenize_console(l);
579                                 if(len != 2) {
580                                         defragcpexists = -1; // something's wrong in the defrag cp file, set defragcpexists to -1 so that it will be rewritten when someone finishes
581                                         continue;
582                                 }
583                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
584                                         if(argv(0) == cp.targetname)
585                                                 cp.race_checkpoint = stof(argv(1));
586                         }
587                         fclose(fh);
588                 }
589         }
590
591         g_race_qualifying = qual;
592
593         if(race_timed_checkpoint) {
594                 if(defrag_ents) {
595                         for(cp = world; (cp = find(cp, classname, "target_startTimer"));)
596                                 WaypointSprite_UpdateSprites(cp.sprite, "race-start", "", "");
597                         for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
598                                 WaypointSprite_UpdateSprites(cp.sprite, "race-finish", "", "");
599
600                         for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
601                                 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
602                                         defragcpexists = -1;
603                         }
604
605                         if(defragcpexists != -1){
606                                 float largest_cp_id;
607                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
608                                         if(cp.race_checkpoint > largest_cp_id)
609                                                 largest_cp_id = cp.race_checkpoint;
610                                 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
611                                         cp.race_checkpoint = largest_cp_id + 1; // finish line
612                                 race_highest_checkpoint = largest_cp_id + 1;
613                                 race_timed_checkpoint = largest_cp_id + 1;
614                         } else {
615                                 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
616                                         cp.race_checkpoint = 255; // finish line
617                                 race_highest_checkpoint = 255;
618                                 race_timed_checkpoint = 255;
619                         }
620                         entity targ;
621                         for(targ = world; (targ = find(targ, target, self.targetname)); )
622                                 if (targ.classname == "trigger_multiple") {
623                                         targ.wait = -2; // still needs investigating, why cant two players pass a (defrag) checkpoint at the same time?
624                                         targ.delay = 0;
625                                 }
626                 }
627                 else {
628                         for(cp = world; (cp = find(cp, classname, "trigger_race_checkpoint")); )
629                                 if(cp.sprite)
630                                 {
631                                         if(cp.race_checkpoint == 0)
632                                                 WaypointSprite_UpdateSprites(cp.sprite, "race-start", "", "");
633                                         else if(cp.race_checkpoint == race_timed_checkpoint)
634                                                 WaypointSprite_UpdateSprites(cp.sprite, "race-finish", "", "");
635                                 }
636                 }
637         }
638
639         remove(self);
640         self = oldself;
641 }
642
643 void spawnfunc_trigger_race_checkpoint()
644 {
645         vector o;
646         if(!g_race && !g_cts)
647         {
648                 remove(self);
649                 return;
650         }
651
652         EXACTTRIGGER_INIT;
653
654         self.use = checkpoint_use;
655         if not(self.spawnflags & 1)
656                 self.touch = checkpoint_touch;
657
658         o = (self.absmin + self.absmax) * 0.5;
659         tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o_z - self.absmin_z), MOVE_NORMAL, self);
660         waypoint_spawnforitem_force(self, trace_endpos);
661         self.nearestwaypointtimeout = time + 1000000000;
662
663         if(!self.message)
664                 self.message = "went backwards";
665         if (!self.message2)
666                 self.message2 = "was pushed backwards by";
667         if (!self.race_penalty_reason)
668                 self.race_penalty_reason = "missing a checkpoint";
669         
670         self.race_checkpoint = self.cnt;
671
672         if(self.race_checkpoint > race_highest_checkpoint)
673         {
674                 race_highest_checkpoint = self.race_checkpoint;
675                 if(self.spawnflags & 8)
676                         race_timed_checkpoint = self.race_checkpoint;
677                 else
678                         race_timed_checkpoint = 0;
679         }
680
681         if(!self.race_penalty)
682         {
683                 if(self.race_checkpoint)
684                         WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
685                 else
686                         WaypointSprite_SpawnFixed("race-finish", o, self, sprite);
687         }
688
689         self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
690
691         InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
692 }
693
694 void spawnfunc_target_checkpoint() // defrag entity
695 {
696         vector o;
697         if(!g_race && !g_cts)
698         {
699                 remove(self);
700                 return;
701         }
702         defrag_ents = 1;
703
704         EXACTTRIGGER_INIT;
705
706         self.use = checkpoint_use;
707         //if not(self.spawnflags & 1)
708         //      self.touch = checkpoint_touch;
709
710         o = (self.absmin + self.absmax) * 0.5;
711         tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o_z - self.absmin_z), MOVE_NORMAL, self);
712         waypoint_spawnforitem_force(self, trace_endpos);
713         self.nearestwaypointtimeout = time + 1000000000;
714
715         if(!self.message)
716                 self.message = "went backwards";
717         if (!self.message2)
718                 self.message2 = "was pushed backwards by";
719         if (!self.race_penalty_reason)
720                 self.race_penalty_reason = "missing a checkpoint";
721
722         if(self.classname == "target_startTimer")
723                 self.race_checkpoint = 0;
724         else
725                 self.race_checkpoint = -2;
726
727         race_timed_checkpoint = 1;
728
729         if(self.race_checkpoint == 0)
730                 WaypointSprite_SpawnFixed("race-start", o, self, sprite);
731         else
732                 WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
733
734         self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
735
736         InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
737 }
738
739 void spawnfunc_target_startTimer() { spawnfunc_target_checkpoint(); }
740 void spawnfunc_target_stopTimer() { spawnfunc_target_checkpoint(); }
741
742 void race_AbandonRaceCheck(entity p)
743 {
744         if(race_completing && !p.race_completed)
745         {
746                 p.race_completed = 1;
747                 MAKE_INDEPENDENT_PLAYER(p);
748                 bprint(p.netname, "^7 has abandoned the race.\n");
749                 ClientData_Touch(p);
750         }
751 }
752
753 void race_StartCompleting()
754 {
755         entity p;
756         race_completing = 1;
757         FOR_EACH_PLAYER(p)
758                 if(p.deadflag != DEAD_NO)
759                         race_AbandonRaceCheck(p);
760 }
761
762 void race_PreparePlayer()
763 {
764         race_ClearTime(self);
765         self.race_place = 0;
766         self.race_started = 0;
767         self.race_respawn_checkpoint = -1;
768         self.race_respawn_spotref = world;
769 }
770
771 void race_RetractPlayer()
772 {
773         if(!g_race && !g_cts)
774                 return;
775         if(self.race_respawn_checkpoint == 0 || self.race_respawn_checkpoint == -1 || self.race_respawn_checkpoint == race_timed_checkpoint)
776                 race_ClearTime(self);
777         self.race_checkpoint = self.race_respawn_checkpoint;
778 }
779
780 void race_PreDie()
781 {
782         if(!g_race && !g_cts)
783                 return;
784
785         race_AbandonRaceCheck(self);
786 }
787
788 void race_PreSpawn()
789 {
790         if(!g_race && !g_cts)
791                 return;
792         if(self.killcount == -666 /* initial spawn */ || g_race_qualifying) // spawn
793                 race_PreparePlayer();
794         else // respawn
795                 race_RetractPlayer();
796
797         race_AbandonRaceCheck(self);
798 }
799
800 void race_PostSpawn(entity spot)
801 {
802         if(!g_race && !g_cts)
803                 return;
804
805         if(spot.target == "")
806                 // Emergency: this wasn't a real spawnpoint. Can this ever happen?
807                 race_PreparePlayer();
808
809         // if we need to respawn, do it right
810         self.race_respawn_checkpoint = self.race_checkpoint;
811         self.race_respawn_spotref = spot;
812
813         self.race_place = 0;
814 }
815
816 void race_PreSpawnObserver()
817 {
818         if(!g_race && !g_cts)
819                 return;
820         race_PreparePlayer();
821 }
822
823 void spawnfunc_info_player_race (void)
824 {
825         if(!g_race && !g_cts)
826         {
827                 remove(self);
828                 return;
829         }
830         ++race_spawns;
831         spawnfunc_info_player_deathmatch();
832
833         if(self.race_place > race_highest_place_spawn)
834                 race_highest_place_spawn = self.race_place;
835         if(self.race_place < race_lowest_place_spawn)
836                 race_lowest_place_spawn = self.race_place;
837 }
838
839 void race_ClearRecords()
840 {
841         float i;
842         entity e;
843
844         for(i = 0; i < MAX_CHECKPOINTS; ++i)
845         {
846                 race_checkpoint_records[i] = 0;
847                 if(race_checkpoint_recordholders[i])
848                         strunzone(race_checkpoint_recordholders[i]);
849                 race_checkpoint_recordholders[i] = string_null;
850         }
851
852         e = self;
853         FOR_EACH_CLIENT(self)
854         {
855                 float p;
856                 p = self.race_place;
857                 race_PreparePlayer();
858                 self.race_place = p;
859         }
860         self = e;
861 }
862
863 void race_ReadyRestart()
864 {
865         float s;
866
867         Score_NicePrint(world);
868
869         race_ClearRecords();
870         PlayerScore_Sort(race_place);
871
872         entity e;
873         FOR_EACH_CLIENT(e)
874         {
875                 if(e.race_place)
876                 {
877                         s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
878                         if(!s)
879                                 e.race_place = 0;
880                 }
881                 print(e.netname, " = ", ftos(e.race_place), "\n");
882         }
883
884         if(g_race_qualifying == 2)
885         {
886                 g_race_qualifying = 0;
887                 independent_players = 0;
888                 cvar_set("fraglimit", ftos(race_fraglimit));
889                 cvar_set("leadlimit", ftos(race_leadlimit));
890                 cvar_set("timelimit", ftos(race_timelimit));
891                 ScoreRules_race();
892         }
893 }
894
895 void race_ImposePenaltyTime(entity pl, float penalty, string reason)
896 {
897         if(g_race_qualifying)
898         {
899                 pl.race_penalty_accumulator += penalty;
900                 msg_entity = pl;
901                 WRITESPECTATABLE_MSG_ONE({
902                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
903                         WriteByte(MSG_ONE, TE_CSQC_RACE);
904                         WriteByte(MSG_ONE, RACE_NET_PENALTY_QUALIFYING);
905                         WriteShort(MSG_ONE, TIME_ENCODE(penalty));
906                         WriteString(MSG_ONE, reason);
907                 });
908         }
909         else
910         {
911                 pl.race_penalty = time + penalty;
912                 msg_entity = pl;
913                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy, {
914                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
915                         WriteByte(MSG_ONE, TE_CSQC_RACE);
916                         WriteByte(MSG_ONE, RACE_NET_PENALTY_RACE);
917                         WriteShort(MSG_ONE, TIME_ENCODE(penalty));
918                         WriteString(MSG_ONE, reason);
919                 });
920         }
921 }
922
923 void penalty_touch()
924 {
925         EXACTTRIGGER_TOUCH;
926         if(other.race_lastpenalty != self)
927         {
928                 other.race_lastpenalty = self;
929                 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
930         }
931 }
932
933 void penalty_use()
934 {
935         race_ImposePenaltyTime(activator, self.race_penalty, self.race_penalty_reason);
936 }
937
938 void spawnfunc_trigger_race_penalty()
939 {
940         EXACTTRIGGER_INIT;
941
942         self.use = penalty_use;
943         if not(self.spawnflags & 1)
944                 self.touch = penalty_touch;
945
946         if (!self.race_penalty_reason)
947                 self.race_penalty_reason = "missing a checkpoint";
948         if (!self.race_penalty)
949                 self.race_penalty = 5;
950 }