]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/race.qc
move the badge to a better place, show them to all players along with a name
[divverent/nexuiz.git] / data / qcsrc / server / race.qc
1 #define MAX_CHECKPOINTS 255
2
3 void spawnfunc_target_checkpoint();
4
5 .float race_penalty;
6 .float race_penalty_accumulator;
7 .string race_penalty_reason;
8 .float race_checkpoint; // player: next checkpoint that has to be reached
9 .float race_laptime;
10 .entity race_lastpenalty;
11
12 .entity sprite;
13
14 float race_checkpoint_records[MAX_CHECKPOINTS];
15 string race_checkpoint_recordholders[MAX_CHECKPOINTS];
16 float race_checkpoint_lasttimes[MAX_CHECKPOINTS];
17 float race_checkpoint_lastlaps[MAX_CHECKPOINTS];
18 entity race_checkpoint_lastplayers[MAX_CHECKPOINTS];
19
20 float race_highest_checkpoint;
21 float race_timed_checkpoint;
22
23 float defrag_ents;
24 float defragcpexists;
25
26 float race_NextCheckpoint(float f)
27 {
28         if(f >= race_highest_checkpoint)
29                 return 0;
30         else
31                 return f + 1;
32 }
33
34 float race_PreviousCheckpoint(float f)
35 {
36         if(f == -1)
37                 return 0;
38         else if(f == 0)
39                 return race_highest_checkpoint;
40         else
41                 return f - 1;
42 }
43
44 // encode as:
45 //   0 = common start/finish
46 // 254 = start
47 // 255 = finish
48 float race_CheckpointNetworkID(float f)
49 {
50         if(race_timed_checkpoint)
51         {
52                 if(f == 0)
53                         return 254; // start
54                 else if(f == race_timed_checkpoint)
55                         return 255; // finish
56         }
57         return f;
58 }
59
60 void race_SendNextCheckpoint(entity e, float spec) // qualifying only
61 {
62         float recordtime;
63         string recordholder;
64         float cp;
65
66         if(!e.race_laptime)
67                 return;
68
69         cp = e.race_checkpoint;
70         recordtime = race_checkpoint_records[cp];
71         recordholder = race_checkpoint_recordholders[cp];
72         if(recordholder == e.netname)
73                 recordholder = "";
74
75         if(!spec)
76                 msg_entity = e;
77         WRITESPECTATABLE_MSG_ONE({
78                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
79                 WriteByte(MSG_ONE, TE_CSQC_RACE);
80                 if(spec)
81                 {
82                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_NEXT_SPEC_QUALIFYING);
83                         //WriteCoord(MSG_ONE, e.race_laptime - e.race_penalty_accumulator);
84                         WriteCoord(MSG_ONE, time - e.race_movetime - e.race_penalty_accumulator);
85                 }
86                 else
87                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_NEXT_QUALIFYING);
88                 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player will be at next
89                 WriteInt24_t(MSG_ONE, recordtime);
90                 WriteString(MSG_ONE, recordholder);
91         });
92 }
93
94 void race_InitSpectator()
95 {
96         if(g_race_qualifying)
97                 if(msg_entity.enemy.race_laptime)
98                         race_SendNextCheckpoint(msg_entity.enemy, 1);
99 }
100
101 string rr;
102 float grecordtime[RANKINGS_CNT];
103 string grecordholder[RANKINGS_CNT];
104 #ifdef UID
105 string grecorduid[RANKINGS_CNT];
106 #endif
107 float worst_time; // last ranked time
108 float have_recs; // have we already read the records from the database before?
109 float race_GetTime(float pos) {
110         if(g_cts)
111                 rr = CTS_RECORD;
112         else
113                 rr = RACE_RECORD;
114
115         if (!have_recs) { // I guess this is better than checking if the first array item is empty, rumor has it that arrays are slow
116                 float i;
117                 for(i=0;i<RANKINGS_CNT;++i) {
118                         grecordtime[i] = stof(db_get(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i))));
119                         grecordholder[i] = strzone(db_get(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i))));
120 #ifdef UID
121                         grecorduid[i] = strzone(db_get(ServerProgsDB, strcat(GetMapname(), rr, "uid", ftos(i))));
122 #endif
123                 }
124                 grecordtime[0] = stof(db_get(ServerProgsDB, strcat(GetMapname(), rr, "time")));
125                 grecordholder[0] = strzone(db_get(ServerProgsDB, strcat(GetMapname(), rr, "netname")));
126 #ifdef UID
127                 grecorduid[0] = strzone(db_get(ServerProgsDB, strcat(GetMapname(), rr, "uid")));
128 #endif
129                 worst_time = stof(db_get(ServerProgsDB, strcat(GetMapname(), rr, strcat("time", ftos(RANKINGS_CNT-1)))));
130                 have_recs = 1;
131         }
132
133         return grecordtime[pos-1];
134 }
135
136 string race_GetName(float pos) { // these other functions assume that race_GetTime has been called >= once before
137         return grecordholder[pos-1];
138 }
139
140 #ifdef UID
141 float race_CheckUID(string myuid, string net_name) { // return existing UID or player name ranking pos, else 0
142         float i;
143         for (i=RANKINGS_CNT-1;i>=0;--i)
144                 if(grecorduid[i] == myuid)
145                         return i+1;
146         for (i=RANKINGS_CNT-1;i>=0;--i)
147                 if(grecordholder[i] == net_name)
148                         return i+1;
149         return 0;
150 }
151 #endif
152
153 float race_CheckName(string net_name) { // Does the name already exist in rankings? In that case, where? (otherwise 0)
154         float i;
155         for (i=RANKINGS_CNT-1;i>=0;--i)
156                 if(grecordholder[i] == net_name)
157                         return i+1;
158         return 0;
159 }
160
161 float race_GetPos(float t) {
162         float i;
163
164         if(worst_time == 0)
165         for (i=0;i<RANKINGS_CNT;++i)
166                 if (grecordtime[i] == 0 || grecordtime[i] > t)
167                         return i+1;
168
169         for (i=0;i<RANKINGS_CNT;++i)
170                 if (grecordtime[i] > t)
171                         return i+1;
172         return 0;
173 }
174
175 void race_send_recordtime(float msg)
176 {
177         // send the server best time
178         WriteByte(msg, SVC_TEMPENTITY);
179         WriteByte(msg, TE_CSQC_RACE);
180         WriteByte(msg, RACE_NET_SERVER_RECORD);
181         WriteInt24_t(msg, race_GetTime(1));
182 }
183
184 void race_SendRankings(float pos, float prevpos, float del, float msg)
185 {
186         WriteByte(msg, SVC_TEMPENTITY);
187         WriteByte(msg, TE_CSQC_RACE);
188         WriteByte(msg, RACE_NET_SERVER_RANKINGS);
189         WriteShort(msg, pos);
190         WriteShort(msg, prevpos);
191         WriteShort(msg, del);
192         WriteString(msg, race_GetName(pos));
193         WriteInt24_t(msg, race_GetTime(pos));
194 }
195
196 void race_SendStatus(float id, entity e)
197 {
198         float msg;
199         if (id == 0)
200                 msg = MSG_ONE;
201         else
202                 msg = MSG_ALL;
203         msg_entity = e;
204         WRITESPECTATABLE_MSG_ONE_VARNAME(dummy3, {
205                 WriteByte(msg, SVC_TEMPENTITY);
206                 WriteByte(msg, TE_CSQC_RACE);
207                 WriteByte(msg, RACE_NET_SERVER_STATUS);
208                 WriteShort(msg, id);
209                 WriteString(msg, e.netname);
210         });
211 }
212
213 string race_PlaceName(float pos) {
214         if(pos == 1)
215                 return "1st";
216         else if(pos == 2)
217                 return "2nd";
218         else if(pos == 3)
219                 return "3rd";
220         else
221                 return strcat(ftos(pos), "th");
222 }
223
224 void race_SetTime(entity e, float t) {
225         if (worst_time && t > worst_time)
226                 return;
227
228         float oldrec;
229         string recorddifference;
230         float pos;
231         pos = race_GetPos(t);
232         float prevpos;
233
234         prevpos = race_CheckName(e.netname);
235 #ifdef UID
236         prevpos = race_CheckUID(e.uid, e.netname);
237 #endif
238         if (prevpos && (prevpos < pos || !pos))
239         {
240                 oldrec = race_GetTime(prevpos);
241                 recorddifference = strcat(" ^1[+", TIME_ENCODED_TOSTRING(t - oldrec), "]");
242                 bprint(e.netname, "^7 couldn't break their ", race_PlaceName(prevpos), " place record of ", TIME_ENCODED_TOSTRING(oldrec), recorddifference, "\n");
243
244                 return;
245         } else if (!pos) { // no ranking, time worse than the worst ranked
246                 recorddifference = strcat(" ^1[+", TIME_ENCODED_TOSTRING(t - grecordtime[RANKINGS_CNT-1]), "]");
247                 bprint(e.netname, "^7 couldn't break the ", race_PlaceName(RANKINGS_CNT), " place record of ", TIME_ENCODED_TOSTRING(grecordtime[RANKINGS_CNT-1]), recorddifference, "\n");
248                 return;
249         }
250
251         // move other rankings out of the way
252         float i;
253         if (prevpos) { // player improved his existing record
254                 if(prevpos == pos)
255                         oldrec = grecordtime[pos-1];
256                 else
257                         oldrec = grecordtime[pos-1];
258                 for (i=prevpos-1;i>pos-1;--i) {
259                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), ftos(grecordtime[i-1]));
260                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i)), grecordholder[i-1]);
261 #ifdef UID
262                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "uid", ftos(i)), grecorduid[i-1]);
263 #endif
264                         grecordtime[i] = grecordtime[i-1];
265
266                         if (grecordholder[i])
267                                 strunzone(grecordholder[i]);
268                         grecordholder[i] = strzone(grecordholder[i-1]);
269 #ifdef UID
270                         if (grecorduid[i])
271                                 strunzone(grecorduid[i]);
272                         grecorduid[i] = strzone(grecorduid[i-1]);
273 #endif
274                 }
275         } else { // player has no ranked record yet
276                 oldrec = grecordtime[pos-1];
277                 for (i=RANKINGS_CNT-1;i>pos-1;--i) {
278                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), ftos(grecordtime[i-1]));
279                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i)), grecordholder[i-1]);
280 #ifdef UID
281                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "uid", ftos(i)), grecorduid[i-1]);
282 #endif
283                         grecordtime[i] = grecordtime[i-1];
284
285                         if (grecordholder[i])
286                                 strunzone(grecordholder[i]);
287                         grecordholder[i] = strzone(grecordholder[i-1]);
288 #ifdef UID
289                         if (grecorduid[i])
290                                 strunzone(grecorduid[i]);
291                         grecorduid[i] = strzone(grecorduid[i-1]);
292 #endif
293                 }
294         }
295         
296         // store new ranking
297         if (pos == 1) {
298                 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time"), ftos(t));
299                 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname"), e.netname);
300 #ifdef UID
301                 db_put(ServerProgsDB, strcat(GetMapname(), rr, "uid"), e.uid);
302 #endif
303
304                 grecordtime[0] = t;
305
306                 if (grecordholder[0])
307                         strunzone(grecordholder[0]);
308                 grecordholder[0] = strzone(e.netname);
309 #ifdef UID
310                 if (grecorduid[0])
311                         strunzone(grecorduid[0]);
312                 grecorduid[0] = strzone(e.uid);
313 #endif
314                 write_recordmarker(e, time - TIME_DECODE(t), TIME_DECODE(t));
315                 race_send_recordtime(MSG_ALL);
316         } else {
317                 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(pos-1)), ftos(t));
318                 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(pos-1)), e.netname);
319 #ifdef UID
320                 db_put(ServerProgsDB, strcat(GetMapname(), rr, "uid", ftos(pos-1)), e.uid);
321 #endif
322
323                 grecordtime[pos-1] = t;
324
325                 if (grecordholder[pos-1])
326                         strunzone(grecordholder[pos-1]);
327                 grecordholder[pos-1] = strzone(e.netname);
328 #ifdef UID
329                 if (grecorduid[pos-1])
330                         strunzone(grecorduid[pos-1]);
331                 grecorduid[pos-1] = strzone(e.uid);
332 #endif
333         }
334
335         if (pos == RANKINGS_CNT)
336                 worst_time = t;
337
338         race_SendRankings(pos, prevpos, 0, MSG_ALL);
339         if(rankings_reply)
340                 strunzone(rankings_reply);
341         rankings_reply = strzone(getrankings());
342         if(pos == 1) {
343                 if(pos == prevpos) {
344                         recorddifference = strcat(" ^2[-", TIME_ENCODED_TOSTRING(oldrec - t), "]");
345                         bprint(e.netname, "^1 improved their 1st place record with ", TIME_ENCODED_TOSTRING(t), recorddifference, "\n");
346                         race_SendStatus(2, e); // "new record"
347                 } else {
348                         recorddifference = strcat(" ^2[-", TIME_ENCODED_TOSTRING(oldrec - t), "]");
349                         bprint(e.netname, "^1 broke ", grecordholder[pos], "^1's 1st place record with ", strcat(TIME_ENCODED_TOSTRING(t), recorddifference, "\n"));
350                         race_SendStatus(2, e); // "new record"
351                 }
352         } else {
353                 if(pos == prevpos) {
354                         recorddifference = strcat(" ^2[-", TIME_ENCODED_TOSTRING(oldrec - t), "]");
355                         bprint(e.netname, "^3 improved their ", race_PlaceName(pos), " ^3place record with ", TIME_ENCODED_TOSTRING(t), recorddifference, "\n");
356                         race_SendStatus(0, e); // "new time"
357                 } else if (oldrec == 0) {
358                         bprint(e.netname, "^2 set the ", race_PlaceName(pos), " ^2place record with ", TIME_ENCODED_TOSTRING(t), "\n");
359                         race_SendStatus(1, e); // "new rank"
360                 } else {
361                         recorddifference = strcat(" ^2[-", TIME_ENCODED_TOSTRING(oldrec - t), "]");
362                         bprint(e.netname, "^2 broke ", grecordholder[pos], "^2's ", race_PlaceName(pos), " ^2place record with ", strcat(TIME_ENCODED_TOSTRING(t), recorddifference, "\n"));
363                         race_SendStatus(1, e); // "new rank"
364                 }
365         }
366 }
367
368 void race_DeleteTime(float pos) {
369         float i;
370
371         for (i = pos-1; i <= RANKINGS_CNT-1; ++i) {
372                 if (i == 0) {
373                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "time"), ftos(grecordtime[1]));
374                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname"), grecordholder[1]);
375 #ifdef UID
376                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "uid"), grecorduid[1]);
377 #endif
378                         grecordtime[0] = grecordtime[1];
379                         if (grecordholder[i])
380                                 strunzone(grecordholder[0]);
381                         grecordholder[0] = strzone(grecordholder[1]);
382
383 #ifdef UID
384                         if (grecorduid[i])
385                                 strunzone(grecorduid[0]);
386                         grecorduid[0] = strzone(grecorduid[1]);
387 #endif
388                 }
389                 else if (i == RANKINGS_CNT-1) {
390                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), string_null);
391                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i)), string_null);
392 #ifdef UID
393                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "uid", ftos(i)), string_null);
394 #endif
395                         grecordtime[i] = 0;
396                         if (grecordholder[i])
397                                 strunzone(grecordholder[i]);
398                         grecordholder[i] = string_null;
399
400 #ifdef UID
401                         if (grecorduid[i])
402                                 strunzone(grecorduid[i]);
403                         grecorduid[i] = string_null;
404 #endif
405                 }
406                 else {
407                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), ftos(grecordtime[i+1]));
408                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i)), grecordholder[i+1]);
409 #ifdef UID
410                         db_put(ServerProgsDB, strcat(GetMapname(), rr, "uid", ftos(i)), grecorduid[i+1]);
411 #endif
412                         grecordtime[i] = grecordtime[i+1];
413                         if (grecordholder[i])
414                                 strunzone(grecordholder[i]);
415                         grecordholder[i] = strzone(grecordholder[i+1]);
416
417 #ifdef UID
418                         if (grecorduid[i])
419                                 strunzone(grecorduid[i]);
420                         grecorduid[i] = strzone(grecorduid[i+1]);
421 #endif
422                 }
423         }
424
425         race_SendRankings(pos, 0, 1, MSG_ALL);
426         if(pos == 1)
427                 race_send_recordtime(MSG_ALL);
428
429         if(rankings_reply)
430                 strunzone(rankings_reply);
431         rankings_reply = strzone(getrankings());
432
433         worst_time = 0;
434 }
435
436 void race_SendTime(entity e, float cp, float t, float tvalid)
437 {
438         float snew, l;
439         entity p;
440
441         if(g_race_qualifying)
442                 t += e.race_penalty_accumulator;
443
444         t = TIME_ENCODE(t); // make integer
445         // adding just 0.4 so it rounds down in the .5 case (matching the timer display)
446
447         if(tvalid)
448         if(cp == race_timed_checkpoint) // finish line
449         if not(e.race_completed)
450         {
451                 float s;
452                 if(g_race_qualifying)
453                 {
454                         s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
455                         if(!s || t < s)
456                                 PlayerScore_Add(e, SP_RACE_FASTEST, t - s);
457                 }
458                 else
459                 {
460                         s = PlayerScore_Add(e, SP_RACE_TIME, 0);
461                         snew = TIME_ENCODE(time - game_starttime);
462                         PlayerScore_Add(e, SP_RACE_TIME, snew - s);
463                         l = PlayerTeamScore_Add(e, SP_RACE_LAPS, ST_RACE_LAPS, 1);
464
465                         if(cvar("fraglimit"))
466                                 if(l >= cvar("fraglimit"))
467                                         race_StartCompleting();
468
469                         if(race_completing)
470                         {
471                                 e.race_completed = 1;
472                                 MAKE_INDEPENDENT_PLAYER(e);
473                                 bprint(e.netname, "^7 has finished the race.\n");
474                                 ClientData_Touch(e);
475                         }
476                 }
477         }
478
479         float recordtime;
480         string recordholder;
481         if(g_race_qualifying)
482         {
483                 if(tvalid)
484                 {
485                         recordtime = race_checkpoint_records[cp];
486                         recordholder = strcat1(race_checkpoint_recordholders[cp]); // make a tempstring copy, as we'll possibly strunzone it!
487                         if(recordholder == e.netname)
488                                 recordholder = "";
489
490                         if(t != 0)
491                         if(t < worst_time || worst_time == 0)
492                         {
493                                 if(cp == race_timed_checkpoint)
494                                 {
495                                         race_SetTime(e, t);
496                                 }
497
498                                 if(t < recordtime || recordtime == 0)
499                                 {
500                                         race_checkpoint_records[cp] = t;
501                                         if(race_checkpoint_recordholders[cp])
502                                                 strunzone(race_checkpoint_recordholders[cp]);
503                                         race_checkpoint_recordholders[cp] = strzone(e.netname);
504                                         if(g_race_qualifying)
505                                         {
506                                                 FOR_EACH_REALPLAYER(p)
507                                                         if(p.race_checkpoint == cp)
508                                                                 race_SendNextCheckpoint(p, 0);
509                                         }
510                                 }
511                         }
512                 }
513                 else
514                 {
515                         // dummies
516                         t = 0;
517                         recordtime = 0;
518                         recordholder = "";
519                 }
520
521                 msg_entity = e;
522                 if(g_race_qualifying)
523                 {
524                         WRITESPECTATABLE_MSG_ONE_VARNAME(dummy1, {
525                                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
526                                 WriteByte(MSG_ONE, TE_CSQC_RACE);
527                                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_QUALIFYING);
528                                 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
529                                 WriteInt24_t(MSG_ONE, t); // time to that intermediate
530                                 WriteInt24_t(MSG_ONE, recordtime); // previously best time
531                                 WriteString(MSG_ONE, recordholder); // record holder
532                         });
533                 }
534         }
535         else // RACE! Not Qualifying
536         {
537                 float lself, lother, othtime;
538                 entity oth;
539                 oth = race_checkpoint_lastplayers[cp];
540                 if(oth)
541                 {
542                         lself = PlayerScore_Add(e, SP_RACE_LAPS, 0);
543                         lother = race_checkpoint_lastlaps[cp];
544                         othtime = race_checkpoint_lasttimes[cp];
545                 }
546                 else
547                         lself = lother = othtime = 0;
548
549                 msg_entity = e;
550                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy2, {
551                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
552                         WriteByte(MSG_ONE, TE_CSQC_RACE);
553                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE);
554                         WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
555                         if(e == oth)
556                         {
557                                 WriteInt24_t(MSG_ONE, 0);
558                                 WriteByte(MSG_ONE, 0);
559                                 WriteString(MSG_ONE, "");
560                         }
561                         else
562                         {
563                                 WriteInt24_t(MSG_ONE, TIME_ENCODE(time - race_checkpoint_lasttimes[cp]));
564                                 WriteByte(MSG_ONE, lself - lother);
565                                 WriteString(MSG_ONE, oth.netname); // record holder
566                         }
567                 });
568
569                 race_checkpoint_lastplayers[cp] = e;
570                 race_checkpoint_lasttimes[cp] = time;
571                 race_checkpoint_lastlaps[cp] = lself;
572
573                 msg_entity = oth;
574                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy3, {
575                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
576                         WriteByte(MSG_ONE, TE_CSQC_RACE);
577                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE_BY_OPPONENT);
578                         WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
579                         if(e == oth)
580                         {
581                                 WriteInt24_t(MSG_ONE, 0);
582                                 WriteByte(MSG_ONE, 0);
583                                 WriteString(MSG_ONE, "");
584                         }
585                         else
586                         {
587                                 WriteInt24_t(MSG_ONE, TIME_ENCODE(time - othtime));
588                                 WriteByte(MSG_ONE, lother - lself);
589                                 WriteString(MSG_ONE, e.netname); // record holder
590                         }
591                 });
592         }
593 }
594
595 void race_ClearTime(entity e)
596 {
597         e.race_checkpoint = 0;
598         e.race_laptime = 0;
599         e.race_movetime = e.race_movetime_frac = e.race_movetime_count = 0;
600         e.race_penalty_accumulator = 0;
601         e.race_lastpenalty = world;
602
603         msg_entity = e;
604         WRITESPECTATABLE_MSG_ONE({
605                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
606                 WriteByte(MSG_ONE, TE_CSQC_RACE);
607                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_CLEAR); // next
608         });
609 }
610
611 void dumpsurface(entity e)
612 {
613         float n, si, ni;
614         vector norm, vec;
615         print("Surfaces of ", etos(e), ":\n");
616
617         print("TEST = ", ftos(getsurfacenearpoint(e, '0 0 0')), "\n");
618
619         for(si = 0; ; ++si)
620         {
621                 n = getsurfacenumpoints(e, si);
622                 if(n <= 0)
623                         break;
624                 print("  Surface ", ftos(si), ":\n");
625                 norm = getsurfacenormal(e, si);
626                 print("    Normal = ", vtos(norm), "\n");
627                 for(ni = 0; ni < n; ++ni)
628                 {
629                         vec = getsurfacepoint(e, si, ni);
630                         print("    Point ", ftos(ni), " = ", vtos(vec), " (", ftos(norm * vec), ")\n");
631                 }
632         }
633 }
634
635 void checkpoint_passed()
636 {
637         string oldmsg;
638         entity cp;
639
640         if(other.classname == "porto")
641         {
642                 // do not allow portalling through checkpoints
643                 trace_plane_normal = normalize(-1 * other.velocity);
644                 self = other;
645                 W_Porto_Fail(0);
646                 return;
647         }
648
649         /*
650          * Trigger targets
651          */
652         if not((self.spawnflags & 2) && (other.classname == "player"))
653         {
654                 activator = other;
655                 oldmsg = self.message;
656                 self.message = "";
657                 SUB_UseTargets();
658                 self.message = oldmsg;
659         }
660
661         if(other.classname != "player")
662                 return;
663
664         /*
665          * Remove unauthorized equipment
666          */
667         Portal_ClearAll(other);
668
669         other.porto_forbidden = 2; // decreased by 1 each StartFrame
670
671         if(defrag_ents) {
672                 if(self.race_checkpoint == -2) 
673                 {
674                         self.race_checkpoint = other.race_checkpoint;
675                 }
676
677                 float largest_cp_id;
678                 float cp_amount;
679                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
680                         cp_amount += 1;
681                         if(cp.race_checkpoint > largest_cp_id) // update the finish id if someone hit a new checkpoint
682                         {
683                                 largest_cp_id = cp.race_checkpoint;
684                                 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
685                                         cp.race_checkpoint = largest_cp_id + 1; // finish line
686                                 race_highest_checkpoint = largest_cp_id + 1;
687                                 race_timed_checkpoint = largest_cp_id + 1;
688
689                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
690                                         if(cp.race_checkpoint == -2) // set defragcpexists to -1 so that the cp id file will be rewritten when someone finishes
691                                                 defragcpexists = -1;
692                                 }       
693                         }
694                 }
695                 if(cp_amount == 0) {
696                         for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
697                                 cp.race_checkpoint = 1;
698                         race_highest_checkpoint = 1;
699                         race_timed_checkpoint = 1;
700                 }
701         }
702
703         if((other.race_checkpoint == -1 && self.race_checkpoint == 0) || (other.race_checkpoint == self.race_checkpoint))
704         {
705                 if(self.race_penalty)
706                 {
707                         if(other.race_lastpenalty != self)
708                         {
709                                 other.race_lastpenalty = self;
710                                 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
711                         }
712                 }
713
714                 if(other.race_penalty)
715                         return;
716
717                 /*
718                  * Trigger targets
719                  */
720                 if(self.spawnflags & 2)
721                 {
722                         activator = other;
723                         oldmsg = self.message;
724                         self.message = "";
725                         SUB_UseTargets();
726                         self.message = oldmsg;
727                 }
728
729                 if(other.race_respawn_checkpoint != self.race_checkpoint || !other.race_started)
730                         other.race_respawn_spotref = self; // this is not a spot but a CP, but spawnpoint selection will deal with that
731                 other.race_respawn_checkpoint = self.race_checkpoint;
732                 other.race_checkpoint = race_NextCheckpoint(self.race_checkpoint);
733                 other.race_started = 1;
734
735                 race_SendTime(other, self.race_checkpoint, other.race_movetime, !!other.race_laptime);
736
737                 if(!self.race_checkpoint) // start line
738                 {
739                         other.race_laptime = time;
740                         other.race_movetime = other.race_movetime_frac = other.race_movetime_count = 0;
741                         other.race_penalty_accumulator = 0;
742                         other.race_lastpenalty = world;
743                 }
744
745                 if(g_race_qualifying)
746                         race_SendNextCheckpoint(other, 0);
747
748                 if(defrag_ents && defragcpexists < 0 && self.classname == "target_stopTimer")
749                 {
750                         float fh;
751                         defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_WRITE);
752                         if(fh >= 0)
753                         {
754                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
755                                 fputs(fh, strcat(cp.targetname, " ", ftos(cp.race_checkpoint), "\n"));
756                         }
757                         fclose(fh);
758                 }
759         }
760         else if(other.race_checkpoint == race_NextCheckpoint(self.race_checkpoint))
761         {
762                 // ignored
763         }
764         else
765         {
766                 if(self.spawnflags & 4)
767                         Damage (other, self, self, 10000, DEATH_HURTTRIGGER, other.origin, '0 0 0');
768         }
769 }
770
771 void checkpoint_touch()
772 {
773         EXACTTRIGGER_TOUCH;
774         checkpoint_passed();
775 }
776
777 void checkpoint_use()
778 {
779         if(other.classname == "info_player_deathmatch") // a spawn, a spawn
780                 return;
781
782         other = activator;
783         checkpoint_passed();
784 }
785
786 float race_waypointsprite_visible_for_player(entity e)
787 {
788         if(e.race_checkpoint == -1 || self.owner.race_checkpoint == -2)
789                 return TRUE;
790         else if(e.race_checkpoint == self.owner.race_checkpoint)
791                 return TRUE;
792         else
793                 return FALSE;
794 }
795
796 float have_verified;
797 void trigger_race_checkpoint_verify()
798 {
799         entity oldself, cp;
800         float i, p;
801         float qual;
802
803         if(have_verified)
804                 return;
805         have_verified = 1;
806         
807         qual = g_race_qualifying;
808
809         oldself = self;
810         self = spawn();
811         self.classname = "player";
812
813         if(g_race)
814         {
815                 for(i = 0; i <= race_highest_checkpoint; ++i)
816                 {
817                         self.race_checkpoint = race_NextCheckpoint(i);
818
819                         // race only (middle of the race)
820                         g_race_qualifying = 0;
821                         self.race_place = 0;
822                         if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
823                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for respawning in race) - bailing out"));
824
825                         if(i == 0)
826                         {
827                                 // qualifying only
828                                 g_race_qualifying = 1;
829                                 self.race_place = race_lowest_place_spawn;
830                                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
831                                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
832                                 
833                                 // race only (initial spawn)
834                                 g_race_qualifying = 0;
835                                 for(p = 1; p <= race_highest_place_spawn; ++p)
836                                 {
837                                         self.race_place = p;
838                                         if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
839                                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for initially spawning in race) - bailing out"));
840                                 }
841                         }
842                 }
843         }
844         else if(!defrag_ents)
845         {
846                 // qualifying only
847                 self.race_checkpoint = race_NextCheckpoint(0);
848                 g_race_qualifying = 1;
849                 self.race_place = race_lowest_place_spawn;
850                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
851                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
852         }
853         else
854         {
855                 self.race_checkpoint = race_NextCheckpoint(0);
856                 g_race_qualifying = 1;
857                 self.race_place = 0; // there's only one spawn on defrag maps
858  
859                 // check if a defragcp file already exists, then read it and apply the checkpoint order
860                 float fh;
861                 float len;
862                 string l;
863
864                 defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_READ);
865                 if(fh >= 0)
866                 {
867                         while((l = fgets(fh)))
868                         {
869                                 len = tokenize_console(l);
870                                 if(len != 2) {
871                                         defragcpexists = -1; // something's wrong in the defrag cp file, set defragcpexists to -1 so that it will be rewritten when someone finishes
872                                         continue;
873                                 }
874                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
875                                         if(argv(0) == cp.targetname)
876                                                 cp.race_checkpoint = stof(argv(1));
877                         }
878                         fclose(fh);
879                 }
880         }
881
882         g_race_qualifying = qual;
883
884         if(race_timed_checkpoint) {
885                 if(defrag_ents) {
886                         for(cp = world; (cp = find(cp, classname, "target_startTimer"));)
887                                 WaypointSprite_UpdateSprites(cp.sprite, "race-start", "", "");
888                         for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
889                                 WaypointSprite_UpdateSprites(cp.sprite, "race-finish", "", "");
890
891                         for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
892                                 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
893                                         defragcpexists = -1;
894                         }
895
896                         if(defragcpexists != -1){
897                                 float largest_cp_id;
898                                 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
899                                         if(cp.race_checkpoint > largest_cp_id)
900                                                 largest_cp_id = cp.race_checkpoint;
901                                 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
902                                         cp.race_checkpoint = largest_cp_id + 1; // finish line
903                                 race_highest_checkpoint = largest_cp_id + 1;
904                                 race_timed_checkpoint = largest_cp_id + 1;
905                         } else {
906                                 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
907                                         cp.race_checkpoint = 255; // finish line
908                                 race_highest_checkpoint = 255;
909                                 race_timed_checkpoint = 255;
910                         }
911                 }
912                 else {
913                         for(cp = world; (cp = find(cp, classname, "trigger_race_checkpoint")); )
914                                 if(cp.sprite)
915                                 {
916                                         if(cp.race_checkpoint == 0)
917                                                 WaypointSprite_UpdateSprites(cp.sprite, "race-start", "", "");
918                                         else if(cp.race_checkpoint == race_timed_checkpoint)
919                                                 WaypointSprite_UpdateSprites(cp.sprite, "race-finish", "", "");
920                                 }
921                 }
922         }
923
924         if(defrag_ents) {
925                 entity trigger, targ;
926                 for(trigger = world; (trigger = find(trigger, classname, "trigger_multiple")); )
927                         for(targ = world; (targ = find(targ, targetname, trigger.target)); )
928                                 if (targ.classname == "target_checkpoint" || targ.classname == "target_startTimer" || targ.classname == "target_stopTimer") {
929                                         targ.wait = -2;
930                                         targ.delay = 0;
931
932                                         setsize(targ, trigger.mins, trigger.maxs);
933                                         setorigin(targ, trigger.origin);
934                                         remove(trigger);
935                                 }
936         }
937         remove(self);
938         self = oldself;
939 }
940
941 void spawnfunc_trigger_race_checkpoint()
942 {
943         vector o;
944         if(!g_race && !g_cts)
945         {
946                 remove(self);
947                 return;
948         }
949
950         EXACTTRIGGER_INIT;
951
952         self.use = checkpoint_use;
953         if not(self.spawnflags & 1)
954                 self.touch = checkpoint_touch;
955
956         o = (self.absmin + self.absmax) * 0.5;
957         tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o_z - self.absmin_z), MOVE_NORMAL, self);
958         waypoint_spawnforitem_force(self, trace_endpos);
959         self.nearestwaypointtimeout = time + 1000000000;
960
961         if(!self.message)
962                 self.message = "went backwards";
963         if (!self.message2)
964                 self.message2 = "was pushed backwards by";
965         if (!self.race_penalty_reason)
966                 self.race_penalty_reason = "missing a checkpoint";
967         
968         self.race_checkpoint = self.cnt;
969
970         if(self.race_checkpoint > race_highest_checkpoint)
971         {
972                 race_highest_checkpoint = self.race_checkpoint;
973                 if(self.spawnflags & 8)
974                         race_timed_checkpoint = self.race_checkpoint;
975                 else
976                         race_timed_checkpoint = 0;
977         }
978
979         if(!self.race_penalty)
980         {
981                 if(self.race_checkpoint)
982                         WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
983                 else
984                         WaypointSprite_SpawnFixed("race-finish", o, self, sprite);
985         }
986
987         self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
988
989         InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
990 }
991
992 void spawnfunc_target_checkpoint() // defrag entity
993 {
994         vector o;
995         if(!g_race && !g_cts)
996         {
997                 remove(self);
998                 return;
999         }
1000         defrag_ents = 1;
1001
1002         EXACTTRIGGER_INIT;
1003
1004         self.use = checkpoint_use;
1005         if not(self.spawnflags & 1)
1006                 self.touch = checkpoint_touch;
1007
1008         o = (self.absmin + self.absmax) * 0.5;
1009         tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o_z - self.absmin_z), MOVE_NORMAL, self);
1010         waypoint_spawnforitem_force(self, trace_endpos);
1011         self.nearestwaypointtimeout = time + 1000000000;
1012
1013         if(!self.message)
1014                 self.message = "went backwards";
1015         if (!self.message2)
1016                 self.message2 = "was pushed backwards by";
1017         if (!self.race_penalty_reason)
1018                 self.race_penalty_reason = "missing a checkpoint";
1019
1020         if(self.classname == "target_startTimer")
1021                 self.race_checkpoint = 0;
1022         else
1023                 self.race_checkpoint = -2;
1024
1025         race_timed_checkpoint = 1;
1026
1027         if(self.race_checkpoint == 0)
1028                 WaypointSprite_SpawnFixed("race-start", o, self, sprite);
1029         else
1030                 WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
1031
1032         self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
1033
1034         InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
1035 }
1036
1037 void spawnfunc_target_startTimer() { spawnfunc_target_checkpoint(); }
1038 void spawnfunc_target_stopTimer() { spawnfunc_target_checkpoint(); }
1039
1040 void race_AbandonRaceCheck(entity p)
1041 {
1042         if(race_completing && !p.race_completed)
1043         {
1044                 p.race_completed = 1;
1045                 MAKE_INDEPENDENT_PLAYER(p);
1046                 bprint(p.netname, "^7 has abandoned the race.\n");
1047                 ClientData_Touch(p);
1048         }
1049 }
1050
1051 void race_StartCompleting()
1052 {
1053         entity p;
1054         race_completing = 1;
1055         FOR_EACH_PLAYER(p)
1056                 if(p.deadflag != DEAD_NO)
1057                         race_AbandonRaceCheck(p);
1058 }
1059
1060 void race_PreparePlayer()
1061 {
1062         race_ClearTime(self);
1063         self.race_place = 0;
1064         self.race_started = 0;
1065         self.race_respawn_checkpoint = 0;
1066         self.race_respawn_spotref = world;
1067 }
1068
1069 void race_RetractPlayer()
1070 {
1071         if(!g_race && !g_cts)
1072                 return;
1073         if(self.race_respawn_checkpoint == 0 || self.race_respawn_checkpoint == race_timed_checkpoint)
1074                 race_ClearTime(self);
1075         self.race_checkpoint = self.race_respawn_checkpoint;
1076 }
1077
1078 void race_PreDie()
1079 {
1080         if(!g_race && !g_cts)
1081                 return;
1082
1083         race_AbandonRaceCheck(self);
1084 }
1085
1086 void race_PreSpawn()
1087 {
1088         if(!g_race && !g_cts)
1089                 return;
1090         if(self.killcount == -666 /* initial spawn */ || g_race_qualifying) // spawn
1091                 race_PreparePlayer();
1092         else // respawn
1093                 race_RetractPlayer();
1094
1095         race_AbandonRaceCheck(self);
1096 }
1097
1098 void race_PostSpawn(entity spot)
1099 {
1100         if(!g_race && !g_cts)
1101                 return;
1102
1103         if(spot.target == "")
1104                 // Emergency: this wasn't a real spawnpoint. Can this ever happen?
1105                 race_PreparePlayer();
1106
1107         // if we need to respawn, do it right
1108         self.race_respawn_checkpoint = self.race_checkpoint;
1109         self.race_respawn_spotref = spot;
1110
1111         self.race_place = 0;
1112 }
1113
1114 void race_PreSpawnObserver()
1115 {
1116         if(!g_race && !g_cts)
1117                 return;
1118         race_PreparePlayer();
1119         self.race_checkpoint = -1;
1120 }
1121
1122 void spawnfunc_info_player_race (void)
1123 {
1124         if(!g_race && !g_cts)
1125         {
1126                 remove(self);
1127                 return;
1128         }
1129         ++race_spawns;
1130         spawnfunc_info_player_deathmatch();
1131
1132         if(self.race_place > race_highest_place_spawn)
1133                 race_highest_place_spawn = self.race_place;
1134         if(self.race_place < race_lowest_place_spawn)
1135                 race_lowest_place_spawn = self.race_place;
1136 }
1137
1138 void race_ClearRecords()
1139 {
1140         float i;
1141         entity e;
1142
1143         for(i = 0; i < MAX_CHECKPOINTS; ++i)
1144         {
1145                 race_checkpoint_records[i] = 0;
1146                 if(race_checkpoint_recordholders[i])
1147                         strunzone(race_checkpoint_recordholders[i]);
1148                 race_checkpoint_recordholders[i] = string_null;
1149         }
1150
1151         e = self;
1152         FOR_EACH_CLIENT(self)
1153         {
1154                 float p;
1155                 p = self.race_place;
1156                 race_PreparePlayer();
1157                 self.race_place = p;
1158         }
1159         self = e;
1160 }
1161
1162 void race_ReadyRestart()
1163 {
1164         float s;
1165
1166         Score_NicePrint(world);
1167
1168         race_ClearRecords();
1169         PlayerScore_Sort(race_place);
1170
1171         entity e;
1172         FOR_EACH_CLIENT(e)
1173         {
1174                 if(e.race_place)
1175                 {
1176                         s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
1177                         if(!s)
1178                                 e.race_place = 0;
1179                 }
1180                 print(e.netname, " = ", ftos(e.race_place), "\n");
1181         }
1182
1183         if(g_race_qualifying == 2)
1184         {
1185                 g_race_qualifying = 0;
1186                 independent_players = 0;
1187                 cvar_set("fraglimit", ftos(race_fraglimit));
1188                 cvar_set("leadlimit", ftos(race_leadlimit));
1189                 cvar_set("timelimit", ftos(race_timelimit));
1190                 ScoreRules_race();
1191         }
1192 }
1193
1194 void race_ImposePenaltyTime(entity pl, float penalty, string reason)
1195 {
1196         if(g_race_qualifying)
1197         {
1198                 pl.race_penalty_accumulator += penalty;
1199                 msg_entity = pl;
1200                 WRITESPECTATABLE_MSG_ONE({
1201                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
1202                         WriteByte(MSG_ONE, TE_CSQC_RACE);
1203                         WriteByte(MSG_ONE, RACE_NET_PENALTY_QUALIFYING);
1204                         WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1205                         WriteString(MSG_ONE, reason);
1206                 });
1207         }
1208         else
1209         {
1210                 pl.race_penalty = time + penalty;
1211                 msg_entity = pl;
1212                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy, {
1213                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
1214                         WriteByte(MSG_ONE, TE_CSQC_RACE);
1215                         WriteByte(MSG_ONE, RACE_NET_PENALTY_RACE);
1216                         WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1217                         WriteString(MSG_ONE, reason);
1218                 });
1219         }
1220 }
1221
1222 void penalty_touch()
1223 {
1224         EXACTTRIGGER_TOUCH;
1225         if(other.race_lastpenalty != self)
1226         {
1227                 other.race_lastpenalty = self;
1228                 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
1229         }
1230 }
1231
1232 void penalty_use()
1233 {
1234         race_ImposePenaltyTime(activator, self.race_penalty, self.race_penalty_reason);
1235 }
1236
1237 void spawnfunc_trigger_race_penalty()
1238 {
1239         EXACTTRIGGER_INIT;
1240
1241         self.use = penalty_use;
1242         if not(self.spawnflags & 1)
1243                 self.touch = penalty_touch;
1244
1245         if (!self.race_penalty_reason)
1246                 self.race_penalty_reason = "missing a checkpoint";
1247         if (!self.race_penalty)
1248                 self.race_penalty = 5;
1249 }