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