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