]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/race.qc
Move more REPLICATE calls to \common and from qh files to qc files (it fixes compilat...
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / race.qc
1 #include "race.qh"
2
3 #include <common/deathtypes/all.qh>
4 #include <common/gamemodes/_mod.qh>
5 #include <common/gamemodes/rules.qh>
6 #include <common/mapobjects/subs.qh>
7 #include <common/mapobjects/triggers.qh>
8 #include <common/mutators/mutator/waypoints/waypointsprites.qh>
9 #include <common/net_linked.qh>
10 #include <common/notifications/all.qh>
11 #include <common/playerstats.qh>
12 #include <common/state.qh>
13 #include <common/stats.qh>
14 #include <common/vehicles/sv_vehicles.qh>
15 #include <common/weapons/_all.qh>
16 #include <common/weapons/weapon/porto.qh>
17 #include <lib/warpzone/common.qh>
18 #include <lib/warpzone/util_server.qh>
19 #include <server/bot/api.qh>
20 #include <server/cheats.qh>
21 #include <server/client.qh>
22 #include <server/command/getreplies.qh>
23 #include <server/damage.qh>
24 #include <server/gamelog.qh>
25 #include <server/intermission.qh>
26 #include <server/main.qh>
27 #include <server/mutators/_mod.qh>
28 #include <server/portals.qh>
29 #include <server/scores.qh>
30 #include <server/spawnpoints.qh>
31 #include <server/weapons/common.qh>
32 #include <server/world.qh>
33
34 .string stored_netname; // TODO: store this information independently of race-based gamemodes
35
36 string uid2name(string myuid)
37 {
38         string s = db_get(ServerProgsDB, strcat("/uid2name/", myuid));
39
40         // FIXME remove this later after 0.6 release
41         // convert old style broken records to correct style
42         if(s == "")
43         {
44                 s = db_get(ServerProgsDB, strcat("uid2name", myuid));
45                 if(s != "")
46                 {
47                         db_put(ServerProgsDB, strcat("/uid2name/", myuid), s);
48                         db_remove(ServerProgsDB, strcat("uid2name", myuid));
49                 }
50         }
51
52         if(s == "")
53                 s = "^1Unregistered Player";
54         return s;
55 }
56
57 void write_recordmarker(entity pl, float tstart, float dt)
58 {
59     GameLogEcho(strcat(":recordset:", ftos(pl.playerid), ":", ftos(dt)));
60
61     // also write a marker into demo files for demotc-race-record-extractor to find
62     stuffcmd(pl,
63              strcat(
64                  strcat("//", strconv(2, 0, 0, GetGametype()), " RECORD SET ", TIME_ENCODED_TOSTRING(TIME_ENCODE(dt))),
65                  " ", ftos(tstart), " ", ftos(dt), "\n"));
66 }
67
68 IntrusiveList g_race_targets;
69 IntrusiveList g_racecheckpoints;
70
71 void race_InitSpectator()
72 {
73         if(g_race_qualifying)
74                 if(msg_entity.enemy.race_laptime)
75                         race_SendNextCheckpoint(msg_entity.enemy, 1);
76 }
77
78 float race_readTime(string map, float pos)
79 {
80         return stof(db_get(ServerProgsDB, strcat(map, record_type, "time", ftos(pos))));
81 }
82
83 string race_readUID(string map, float pos)
84 {
85         return db_get(ServerProgsDB, strcat(map, record_type, "crypto_idfp", ftos(pos)));
86 }
87
88 float race_readPos(string map, float t)
89 {
90         for(int i = 1; i <= RANKINGS_CNT; ++i)
91         {
92                 int mytime = race_readTime(map, i);
93                 if(!mytime || mytime > t)
94                         return i;
95         }
96
97         return 0; // pos is zero if unranked
98 }
99
100 void race_writeTime(string map, float t, string myuid)
101 {
102         float newpos;
103         newpos = race_readPos(map, t);
104
105         float i, prevpos = 0;
106         for(i = 1; i <= RANKINGS_CNT; ++i)
107         {
108                 if(race_readUID(map, i) == myuid)
109                         prevpos = i;
110         }
111         if (prevpos)
112         {
113                 // player improved his existing record, only have to iterate on ranks between new and old recs
114                 for (i = prevpos; i > newpos; --i)
115                 {
116                         db_put(ServerProgsDB, strcat(map, record_type, "time", ftos(i)), ftos(race_readTime(map, i - 1)));
117                         db_put(ServerProgsDB, strcat(map, record_type, "crypto_idfp", ftos(i)), race_readUID(map, i - 1));
118                 }
119         }
120         else
121         {
122                 // player has no ranked record yet
123                 for (i = RANKINGS_CNT; i > newpos; --i)
124                 {
125                         float other_time = race_readTime(map, i - 1);
126                         if (other_time) {
127                                 db_put(ServerProgsDB, strcat(map, record_type, "time", ftos(i)), ftos(other_time));
128                                 db_put(ServerProgsDB, strcat(map, record_type, "crypto_idfp", ftos(i)), race_readUID(map, i - 1));
129                         }
130                 }
131         }
132
133         // store new time itself
134         db_put(ServerProgsDB, strcat(map, record_type, "time", ftos(newpos)), ftos(t));
135         db_put(ServerProgsDB, strcat(map, record_type, "crypto_idfp", ftos(newpos)), myuid);
136 }
137
138 string race_readName(string map, float pos)
139 {
140         return uid2name(db_get(ServerProgsDB, strcat(map, record_type, "crypto_idfp", ftos(pos))));
141 }
142
143 void race_checkAndWriteName(entity player)
144 {
145         if(CS_CVAR(player).cvar_cl_allow_uidtracking == 1 && CS_CVAR(player).cvar_cl_allow_uid2name == 1)
146         {
147                 if (!player.stored_netname)
148                         player.stored_netname = strzone(uid2name(player.crypto_idfp));
149                 if(player.stored_netname != player.netname)
150                 {
151                         db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
152                         strcpy(player.stored_netname, player.netname);
153                 }
154         }
155 }
156
157
158 const float MAX_CHECKPOINTS = 255;
159
160 .float race_penalty;
161 .float race_penalty_accumulator;
162 .string race_penalty_reason;
163 .float race_checkpoint; // player: next checkpoint that has to be reached
164 .entity race_lastpenalty;
165
166 .entity sprite;
167
168 float race_checkpoint_records[MAX_CHECKPOINTS];
169 string race_checkpoint_recordholders[MAX_CHECKPOINTS];
170 float race_checkpoint_lasttimes[MAX_CHECKPOINTS];
171 float race_checkpoint_lastlaps[MAX_CHECKPOINTS];
172 entity race_checkpoint_lastplayers[MAX_CHECKPOINTS];
173
174 .float race_checkpoint_record[MAX_CHECKPOINTS];
175
176 float race_highest_checkpoint;
177 float race_timed_checkpoint;
178
179 float defrag_ents;
180 float defragcpexists;
181
182 float race_NextCheckpoint(float f)
183 {
184         if(f >= race_highest_checkpoint)
185                 return 0;
186         else
187                 return f + 1;
188 }
189
190 float race_PreviousCheckpoint(float f)
191 {
192         if(f == -1)
193                 return 0;
194         else if(f == 0)
195                 return race_highest_checkpoint;
196         else
197                 return f - 1;
198 }
199
200 // encode as:
201 //   0 = common start/finish
202 // 254 = start
203 // 255 = finish
204 float race_CheckpointNetworkID(float f)
205 {
206         if(race_timed_checkpoint)
207         {
208                 if(f == 0)
209                         return 254; // start
210                 else if(f == race_timed_checkpoint)
211                         return 255; // finish
212         }
213         return f;
214 }
215
216 void race_SendNextCheckpoint(entity e, float spec) // qualifying only
217 {
218         if(!e.race_laptime)
219                 return;
220
221         int cp = e.race_checkpoint;
222         float recordtime = race_checkpoint_records[cp];
223         float myrecordtime = e.race_checkpoint_record[cp];
224         string recordholder = race_checkpoint_recordholders[cp];
225         if(recordholder == e.netname)
226                 recordholder = "";
227
228         if(!IS_REAL_CLIENT(e))
229                 return;
230
231         if(!spec)
232                 msg_entity = e;
233         WRITESPECTATABLE_MSG_ONE(msg_entity, {
234                 WriteHeader(MSG_ONE, TE_CSQC_RACE);
235                 if(spec)
236                 {
237                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_NEXT_SPEC_QUALIFYING);
238                         //WriteCoord(MSG_ONE, e.race_laptime - e.race_penalty_accumulator);
239                         WriteCoord(MSG_ONE, time - e.race_movetime - e.race_penalty_accumulator);
240                 }
241                 else
242                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_NEXT_QUALIFYING);
243                 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player will be at next
244                 WriteInt24_t(MSG_ONE, recordtime);
245                 if(!spec)
246                         WriteInt24_t(MSG_ONE, myrecordtime);
247                 WriteString(MSG_ONE, recordholder);
248         });
249 }
250
251 void race_send_recordtime(float msg)
252 {
253         // send the server best time
254         WriteHeader(msg, TE_CSQC_RACE);
255         WriteByte(msg, RACE_NET_SERVER_RECORD);
256         WriteInt24_t(msg, race_readTime(GetMapname(), 1));
257 }
258
259 void race_send_speedaward(float msg)
260 {
261         // send the best speed of the round
262         WriteHeader(msg, TE_CSQC_RACE);
263         WriteByte(msg, RACE_NET_SPEED_AWARD);
264         WriteInt24_t(msg, floor(speedaward_speed+0.5));
265         WriteString(msg, speedaward_holder);
266 }
267
268 void race_send_speedaward_alltimebest(float msg)
269 {
270         // send the best speed
271         WriteHeader(msg, TE_CSQC_RACE);
272         WriteByte(msg, RACE_NET_SPEED_AWARD_BEST);
273         WriteInt24_t(msg, floor(speedaward_alltimebest+0.5));
274         WriteString(msg, speedaward_alltimebest_holder);
275 }
276
277 void race_send_rankings_cnt(float msg)
278 {
279         WriteHeader(msg, TE_CSQC_RACE);
280         WriteByte(msg, RACE_NET_RANKINGS_CNT);
281         int m = min(RANKINGS_CNT, autocvar_g_cts_send_rankings_cnt);
282         WriteByte(msg, m);
283 }
284
285 void race_SendRanking(float pos, float prevpos, float del, float msg)
286 {
287         WriteHeader(msg, TE_CSQC_RACE);
288         WriteByte(msg, RACE_NET_SERVER_RANKINGS);
289         WriteShort(msg, pos);
290         WriteShort(msg, prevpos);
291         WriteShort(msg, del);
292         WriteString(msg, race_readName(GetMapname(), pos));
293         WriteInt24_t(msg, race_readTime(GetMapname(), pos));
294 }
295
296 void race_SpeedAwardFrame(entity player)
297 {
298         if (IS_OBSERVER(player))
299                 return;
300
301         if(vdist(player.velocity - player.velocity_z * '0 0 1', >, speedaward_speed))
302         {
303                 speedaward_speed = vlen(player.velocity - player.velocity_z * '0 0 1');
304                 speedaward_holder = player.netname;
305                 speedaward_uid = player.crypto_idfp;
306                 speedaward_lastupdate = time;
307         }
308         if (speedaward_speed > speedaward_lastsent && (time - speedaward_lastupdate > 1 || intermission_running))
309         {
310                 race_send_speedaward(MSG_ALL);
311                 speedaward_lastsent = speedaward_speed;
312                 if (speedaward_speed > speedaward_alltimebest && speedaward_uid != "")
313                 {
314                         speedaward_alltimebest = speedaward_speed;
315                         speedaward_alltimebest_holder = speedaward_holder;
316                         speedaward_alltimebest_uid = speedaward_uid;
317                         db_put(ServerProgsDB, strcat(GetMapname(), record_type, "speed/speed"), ftos(speedaward_alltimebest));
318                         db_put(ServerProgsDB, strcat(GetMapname(), record_type, "speed/crypto_idfp"), speedaward_alltimebest_uid);
319                         race_send_speedaward_alltimebest(MSG_ALL);
320                 }
321         }
322 }
323
324 void race_SendAll(entity player, bool only_rankings)
325 {
326         if(!IS_REAL_CLIENT(player))
327                 return;
328
329         msg_entity = player;
330         if (!only_rankings)
331         {
332                 race_send_recordtime(MSG_ONE);
333                 race_send_speedaward(MSG_ONE);
334
335                 speedaward_alltimebest = stof(db_get(ServerProgsDB, strcat(GetMapname(), record_type, "speed/speed")));
336                 speedaward_alltimebest_holder = uid2name(db_get(ServerProgsDB, strcat(GetMapname(), record_type, "speed/crypto_idfp")));
337                 race_send_speedaward_alltimebest(MSG_ONE);
338         }
339
340         int m = min(RANKINGS_CNT, autocvar_g_cts_send_rankings_cnt);
341         race_send_rankings_cnt(MSG_ONE);
342         for (int i = 1; i <= m; ++i)
343                 race_SendRanking(i, 0, 0, MSG_ONE);
344 }
345
346 void race_SendStatus(float id, entity e)
347 {
348         if(!IS_REAL_CLIENT(e))
349                 return;
350
351         float msg;
352         if (id == 0)
353                 msg = MSG_ONE;
354         else
355                 msg = MSG_ALL;
356         msg_entity = e;
357         WRITESPECTATABLE_MSG_ONE(msg_entity, {
358                 WriteHeader(msg, TE_CSQC_RACE);
359                 WriteByte(msg, RACE_NET_SERVER_STATUS);
360                 WriteShort(msg, id);
361                 WriteString(msg, e.netname);
362         });
363 }
364
365 void race_setTime(string map, float t, string myuid, string mynetname, entity e, bool showmessage)
366 {
367         // netname only used TEMPORARILY for printing
368         int newpos = race_readPos(map, t);
369
370         int player_prevpos = 0;
371         for(int i = 1; i <= RANKINGS_CNT; ++i)
372         {
373                 if(race_readUID(map, i) == myuid)
374                         player_prevpos = i;
375         }
376
377         float oldrec;
378         string oldrec_holder;
379         if (player_prevpos && (player_prevpos < newpos || !newpos))
380         {
381                 oldrec = race_readTime(GetMapname(), player_prevpos);
382                 race_SendStatus(0, e); // "fail"
383                 if(showmessage)
384                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_RACE_FAIL_RANKED, mynetname, player_prevpos, t, oldrec);
385                 return;
386         }
387         else if (!newpos)
388         {
389                 // no ranking, time worse than the worst ranked
390                 oldrec = race_readTime(GetMapname(), RANKINGS_CNT);
391                 race_SendStatus(0, e); // "fail"
392                 if(showmessage)
393                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_RACE_FAIL_UNRANKED, mynetname, RANKINGS_CNT, t, oldrec);
394                 return;
395         }
396
397         // if we didn't hit a return yet, we have a new record!
398
399         // if the player does not have a UID we can unfortunately not store the record, as the rankings system relies on UIDs
400         if(myuid == "")
401         {
402                 if(showmessage)
403                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_RACE_NEW_MISSING_UID, mynetname, t);
404                 return;
405         }
406
407         if(uid2name(myuid) == "^1Unregistered Player")
408         {
409                 if(showmessage)
410                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_RACE_NEW_MISSING_NAME, mynetname, t);
411                 return;
412         }
413
414         oldrec = race_readTime(GetMapname(), newpos);
415         oldrec_holder = race_readName(GetMapname(), newpos);
416
417         // store new ranking
418         race_writeTime(GetMapname(), t, myuid);
419
420         if (newpos == 1 && showmessage)
421         {
422                 write_recordmarker(e, time - TIME_DECODE(t), TIME_DECODE(t));
423                 race_send_recordtime(MSG_ALL);
424         }
425
426         race_SendRanking(newpos, player_prevpos, 0, MSG_ALL);
427         strcpy(rankings_reply, getrankings());
428
429         if(newpos == player_prevpos)
430         {
431                 if(showmessage)
432                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_RACE_NEW_IMPROVED, mynetname, newpos, t, oldrec);
433                 if(newpos == 1) { race_SendStatus(3, e); } // "new server record"
434                 else { race_SendStatus(1, e); } // "new time"
435         }
436         else if(oldrec == 0)
437         {
438                 if(showmessage)
439                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_RACE_NEW_SET, mynetname, newpos, t);
440                 if(newpos == 1) { race_SendStatus(3, e); } // "new server record"
441                 else { race_SendStatus(2, e); } // "new rank"
442         }
443         else
444         {
445                 if(showmessage)
446                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_RACE_NEW_BROKEN, mynetname, oldrec_holder, newpos, t, oldrec);
447                 if(newpos == 1) { race_SendStatus(3, e); } // "new server record"
448                 else { race_SendStatus(2, e); } // "new rank"
449         }
450 }
451
452 void race_deleteTime(string map, float pos)
453 {
454         for(int i = pos; i <= RANKINGS_CNT; ++i)
455         {
456                 string therank = ftos(i);
457                 if (i == RANKINGS_CNT)
458                 {
459                         db_remove(ServerProgsDB, strcat(map, record_type, "time", therank));
460                         db_remove(ServerProgsDB, strcat(map, record_type, "crypto_idfp", therank));
461                 }
462                 else
463                 {
464                         db_put(ServerProgsDB, strcat(map, record_type, "time", therank), ftos(race_readTime(GetMapname(), i+1)));
465                         db_put(ServerProgsDB, strcat(map, record_type, "crypto_idfp", therank), race_readUID(GetMapname(), i+1));
466                 }
467         }
468
469         race_SendRanking(pos, 0, 1, MSG_ALL);
470         if(pos == 1)
471                 race_send_recordtime(MSG_ALL);
472
473         strcpy(rankings_reply, getrankings());
474 }
475
476 void race_SendTime(entity e, float cp, float t, float tvalid)
477 {
478         float snew, l;
479
480         if(g_race_qualifying)
481                 t += e.race_penalty_accumulator;
482
483         t = TIME_ENCODE(t); // make integer
484
485         if(tvalid)
486         if(cp == race_timed_checkpoint) // finish line
487         if (!CS(e).race_completed)
488         {
489                 float s;
490                 if(g_race_qualifying)
491                 {
492                         s = GameRules_scoring_add(e, RACE_FASTEST, 0);
493                         if(!s || t < s)
494                                 GameRules_scoring_add(e, RACE_FASTEST, t - s);
495                 }
496                 else
497                 {
498                         s = GameRules_scoring_add(e, RACE_FASTEST, 0);
499                         if(!s || t < s)
500                                 GameRules_scoring_add(e, RACE_FASTEST, t - s);
501
502                         s = GameRules_scoring_add(e, RACE_TIME, 0);
503                         snew = TIME_ENCODE(time - game_starttime);
504                         GameRules_scoring_add(e, RACE_TIME, snew - s);
505                         l = GameRules_scoring_add_team(e, RACE_LAPS, 1);
506
507                         if(autocvar_fraglimit)
508                                 if(l >= autocvar_fraglimit)
509                                         race_StartCompleting();
510
511                         if(race_completing)
512                         {
513                                 CS(e).race_completed = 1;
514                                 MAKE_INDEPENDENT_PLAYER(e);
515                                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_RACE_FINISHED, e.netname);
516                                 ClientData_Touch(e);
517                         }
518                 }
519         }
520
521         if(g_race_qualifying)
522         {
523                 float recordtime;
524                 string recordholder;
525
526                 if(tvalid)
527                 {
528                         recordtime = race_checkpoint_records[cp];
529                         float myrecordtime = e.race_checkpoint_record[cp];
530                         recordholder = strcat1(race_checkpoint_recordholders[cp]); // make a tempstring copy, as we'll possibly strunzone it!
531                         if(recordholder == e.netname)
532                                 recordholder = "";
533
534                         if(t != 0)
535                         {
536                                 if(cp == race_timed_checkpoint)
537                                 {
538                                         race_setTime(GetMapname(), t, e.crypto_idfp, e.netname, e, true);
539                                         MUTATOR_CALLHOOK(Race_FinalCheckpoint, e);
540                                 }
541                                 if(t < myrecordtime || myrecordtime == 0)
542                                         e.race_checkpoint_record[cp] = t; // resending done below
543
544                                 if(t < recordtime || recordtime == 0)
545                                 {
546                                         race_checkpoint_records[cp] = t;
547                                         strcpy(race_checkpoint_recordholders[cp], e.netname);
548                                         if(g_race_qualifying)
549                                                 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it) && it.race_checkpoint == cp, { race_SendNextCheckpoint(it, 0); });
550                                 }
551
552                         }
553                 }
554                 else
555                 {
556                         // dummies
557                         t = 0;
558                         recordtime = 0;
559                         recordholder = "";
560                 }
561
562                 if(IS_REAL_CLIENT(e))
563                 {
564                         if(g_race_qualifying)
565                         {
566                                 FOREACH_CLIENT(IS_REAL_CLIENT(it),
567                                 {
568                                         if(it == e || (IS_SPEC(it) && it.enemy == e))
569                                         {
570                                                 msg_entity = it;
571                                                 WriteHeader(MSG_ONE, TE_CSQC_RACE);
572                                                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_QUALIFYING);
573                                                 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
574                                                 WriteInt24_t(MSG_ONE, t); // time to that intermediate
575                                                 WriteInt24_t(MSG_ONE, recordtime); // previously best time
576                                                 WriteInt24_t(MSG_ONE, ((tvalid) ? it.race_checkpoint_record[cp] : 0)); // previously best time
577                                                 WriteString(MSG_ONE, recordholder); // record holder
578                                         }
579                                 });
580                         }
581                 }
582         }
583         else // RACE! Not Qualifying
584         {
585                 float mylaps, lother, othtime;
586                 entity oth = race_checkpoint_lastplayers[cp];
587                 if(oth)
588                 {
589                         mylaps = GameRules_scoring_add(e, RACE_LAPS, 0);
590                         lother = race_checkpoint_lastlaps[cp];
591                         othtime = race_checkpoint_lasttimes[cp];
592                 }
593                 else
594                         mylaps = lother = othtime = 0;
595
596                 if(IS_REAL_CLIENT(e))
597                 {
598                         msg_entity = e;
599                         WRITESPECTATABLE_MSG_ONE(msg_entity, {
600                                 WriteHeader(MSG_ONE, TE_CSQC_RACE);
601                                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE);
602                                 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
603                                 if(e == oth)
604                                 {
605                                         WriteInt24_t(MSG_ONE, 0);
606                                         WriteByte(MSG_ONE, 0);
607                                         WriteByte(MSG_ONE, 0);
608                                 }
609                                 else
610                                 {
611                                         WriteInt24_t(MSG_ONE, TIME_ENCODE(time - race_checkpoint_lasttimes[cp]));
612                                         WriteByte(MSG_ONE, mylaps - lother);
613                                         WriteByte(MSG_ONE, etof(oth)); // record holder
614                                 }
615                         });
616                 }
617
618                 race_checkpoint_lastplayers[cp] = e;
619                 race_checkpoint_lasttimes[cp] = time;
620                 race_checkpoint_lastlaps[cp] = mylaps;
621
622                 if(IS_REAL_CLIENT(oth))
623                 {
624                         msg_entity = oth;
625                         WRITESPECTATABLE_MSG_ONE(msg_entity, {
626                                 WriteHeader(MSG_ONE, TE_CSQC_RACE);
627                                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE_BY_OPPONENT);
628                                 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
629                                 if(e == oth)
630                                 {
631                                         WriteInt24_t(MSG_ONE, 0);
632                                         WriteByte(MSG_ONE, 0);
633                                         WriteByte(MSG_ONE, 0);
634                                 }
635                                 else
636                                 {
637                                         WriteInt24_t(MSG_ONE, TIME_ENCODE(time - othtime));
638                                         WriteByte(MSG_ONE, lother - mylaps);
639                                         WriteByte(MSG_ONE, etof(e) - 1); // record holder
640                                 }
641                         });
642                 }
643         }
644 }
645
646 void race_ClearTime(entity e)
647 {
648         e.race_checkpoint = 0;
649         e.race_laptime = 0;
650         e.race_movetime = e.race_movetime_frac = e.race_movetime_count = 0;
651         e.race_penalty_accumulator = 0;
652         e.race_lastpenalty = NULL;
653
654         if(!IS_REAL_CLIENT(e))
655                 return;
656
657         msg_entity = e;
658         WRITESPECTATABLE_MSG_ONE(msg_entity, {
659                 WriteHeader(MSG_ONE, TE_CSQC_RACE);
660                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_CLEAR); // next
661         });
662 }
663
664 void checkpoint_passed(entity this, entity player)
665 {
666         if(IS_VEHICLE(player) && player.owner)
667                 player = player.owner;
668
669         if(player.personal && autocvar_g_allow_checkpoints)
670                 return; // practice mode!
671
672         if(player.classname == "porto")
673         {
674                 // do not allow portalling through checkpoints
675                 trace_plane_normal = normalize(-1 * player.velocity);
676                 W_Porto_Fail(player, 0);
677                 return;
678         }
679
680         string oldmsg; // used twice
681
682         /*
683          * Trigger targets
684          */
685         if (!((this.spawnflags & 2) && (IS_PLAYER(player))))
686         {
687                 oldmsg = this.message;
688                 this.message = "";
689                 SUB_UseTargets(this, player, player);
690                 this.message = oldmsg;
691         }
692
693         if (!IS_PLAYER(player))
694                 return;
695
696         /*
697          * Remove unauthorized equipment
698          */
699         Portal_ClearAll(player);
700
701         player.porto_forbidden = 2; // decreased by 1 each StartFrame
702
703         if(defrag_ents)
704         {
705                 if(this.race_checkpoint == -2)
706                 {
707                         this.race_checkpoint = player.race_checkpoint;
708                 }
709
710                 int cp_amount = 0, largest_cp_id = 0;
711                 IL_EACH(g_race_targets, it.classname == "target_checkpoint",
712                 {
713                         cp_amount += 1;
714                         if(it.race_checkpoint > largest_cp_id) // update the finish id if someone hit a new checkpoint
715                         {
716                                 if(!largest_cp_id)
717                                 {
718                                         IL_EACH(g_race_targets, it.classname == "target_checkpoint",
719                                         {
720                                                 if(it.race_checkpoint == -2) // set defragcpexists to -1 so that the cp id file will be rewritten when someone finishes
721                                                         defragcpexists = -1;
722                                         });
723                                 }
724
725                                 largest_cp_id = it.race_checkpoint;
726                                 IL_EACH(g_race_targets, it.classname == "target_stopTimer",
727                                 {
728                                         it.race_checkpoint = largest_cp_id + 1; // finish line
729                                 });
730                                 race_highest_checkpoint = largest_cp_id + 1;
731                                 race_timed_checkpoint = largest_cp_id + 1;
732                         }
733                 });
734
735                 if(!cp_amount)
736                 {
737                         IL_EACH(g_race_targets, it.classname == "target_stopTimer",
738                         {
739                                 it.race_checkpoint = 1;
740                         });
741                         race_highest_checkpoint = 1;
742                         race_timed_checkpoint = 1;
743                 }
744         }
745
746         if((player.race_checkpoint == -1 && this.race_checkpoint == 0) || (player.race_checkpoint == this.race_checkpoint))
747         {
748                 if(this.race_penalty)
749                 {
750                         if(player.race_lastpenalty != this)
751                         {
752                                 player.race_lastpenalty = this;
753                                 race_ImposePenaltyTime(player, this.race_penalty, this.race_penalty_reason);
754                         }
755                 }
756
757                 if(player.race_penalty)
758                         return;
759
760                 /*
761                  * Trigger targets
762                  */
763                 if(this.spawnflags & 2)
764                 {
765                         oldmsg = this.message;
766                         this.message = "";
767                         SUB_UseTargets(this, player, player); // TODO: should we be using other for the trigger here?
768                         this.message = oldmsg;
769                 }
770
771                 if(player.race_respawn_checkpoint != this.race_checkpoint || !player.race_started)
772                         player.race_respawn_spotref = this; // this is not a spot but a CP, but spawnpoint selection will deal with that
773                 player.race_respawn_checkpoint = this.race_checkpoint;
774                 player.race_checkpoint = race_NextCheckpoint(this.race_checkpoint);
775                 player.race_started = 1;
776
777                 race_SendTime(player, this.race_checkpoint, player.race_movetime, boolean(player.race_laptime));
778
779                 if(!this.race_checkpoint) // start line
780                 {
781                         player.race_laptime = time;
782                         player.race_movetime = player.race_movetime_frac = player.race_movetime_count = 0;
783                         player.race_penalty_accumulator = 0;
784                         player.race_lastpenalty = NULL;
785                 }
786
787                 if(g_race_qualifying)
788                         race_SendNextCheckpoint(player, 0);
789
790                 if(defrag_ents && defragcpexists < 0 && this.classname == "target_stopTimer")
791                 {
792                         float fh;
793                         defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_WRITE);
794                         if(fh >= 0)
795                         {
796                                 IL_EACH(g_race_targets, it.classname == "target_checkpoint",
797                                 {
798                                         fputs(fh, strcat(it.targetname, " ", ftos(it.race_checkpoint), "\n"));
799                                 });
800                         }
801                         fclose(fh);
802                 }
803         }
804         else if(player.race_checkpoint == race_NextCheckpoint(this.race_checkpoint))
805         {
806                 // ignored
807         }
808         else
809         {
810                 if(this.spawnflags & 4)
811                         Damage (player, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, player.origin, '0 0 0');
812         }
813 }
814
815 void checkpoint_touch(entity this, entity toucher)
816 {
817         EXACTTRIGGER_TOUCH(this, toucher);
818         checkpoint_passed(this, toucher);
819 }
820
821 void checkpoint_use(entity this, entity actor, entity trigger)
822 {
823         if(trigger.classname == "info_player_deathmatch") // a spawn, a spawn
824                 return;
825
826         checkpoint_passed(this, actor);
827 }
828
829 bool race_waypointsprite_visible_for_player(entity this, entity player, entity view)
830 {
831         entity own = this.owner;
832         if(this.realowner)
833                 own = this.realowner; // target support
834
835         if(view.race_checkpoint == -1 || own.race_checkpoint == -2)
836                 return true;
837         else if(view.race_checkpoint == own.race_checkpoint)
838                 return true;
839         else
840                 return false;
841 }
842
843 void defrag_waypointsprites(entity targeted, entity checkpoint)
844 {
845         // bones_was_here: spawn a waypoint for every entity with a bmodel
846         // that directly or indirectly targets this checkpoint
847         // (anything a player could touch or shoot to activate this cp)
848
849         entity s = WP_RaceCheckpoint;
850         if (checkpoint.classname == "target_startTimer")
851                 s = WP_RaceStart;
852         else if (checkpoint.classname == "target_stopTimer")
853                 s = WP_RaceFinish;
854
855         for (entity t = findchain(target, targeted.targetname); t; t = t.chain)
856         {
857                 if (t.modelindex)
858                 {
859                         WaypointSprite_SpawnFixed(s, (t.absmin + t.absmax) * 0.5, t, sprite, RADARICON_NONE);
860                         t.sprite.realowner = checkpoint;
861                         t.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
862                 }
863
864                 if (t.targetname)
865                         defrag_waypointsprites(t, checkpoint);
866         }
867 }
868
869 void trigger_race_checkpoint_verify(entity this)
870 {
871         static bool have_verified;
872         if (have_verified) return;
873         have_verified = true;
874
875         bool qual = g_race_qualifying;
876
877         int pl_race_checkpoint = 0;
878         int pl_race_place = 0;
879
880         if (g_race) {
881                 for (int i = 0; i <= race_highest_checkpoint; ++i) {
882                         pl_race_checkpoint = race_NextCheckpoint(i);
883
884                         // race only (middle of the race)
885                         g_race_qualifying = false;
886                         pl_race_place = 0;
887                         if (!Spawn_FilterOutBadSpots(this, findchain(classname, "info_player_deathmatch"), 0, false, true)) {
888                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(pl_race_place), " (used for respawning in race) - bailing out"));
889                         }
890
891                         if (i == 0) {
892                                 // qualifying only
893                                 g_race_qualifying = 1;
894                                 pl_race_place = race_lowest_place_spawn;
895                                 if (!Spawn_FilterOutBadSpots(this, findchain(classname, "info_player_deathmatch"), 0, false, true)) {
896                                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(pl_race_place), " (used for qualifying) - bailing out"));
897                                 }
898
899                                 // race only (initial spawn)
900                                 g_race_qualifying = 0;
901                                 for (int p = 1; p <= race_highest_place_spawn; ++p) {
902                                         pl_race_place = p;
903                                         if (!Spawn_FilterOutBadSpots(this, findchain(classname, "info_player_deathmatch"), 0, false, true)) {
904                                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(pl_race_place), " (used for initially spawning in race) - bailing out"));
905                                         }
906                                 }
907                         }
908                 }
909         } else if (!defrag_ents) {
910                 // qualifying only
911                 pl_race_checkpoint = race_NextCheckpoint(0);
912                 g_race_qualifying = 1;
913                 pl_race_place = race_lowest_place_spawn;
914                 if (!Spawn_FilterOutBadSpots(this, findchain(classname, "info_player_deathmatch"), 0, false, true)) {
915                         error(strcat("Checkpoint 0 misses a spawnpoint with race_place==", ftos(pl_race_place), " (used for qualifying) - bailing out"));
916                 }
917         } else {
918                 pl_race_checkpoint = race_NextCheckpoint(0);
919                 g_race_qualifying = 1;
920                 pl_race_place = 0; // there's only one spawn on defrag maps
921
922                 // check if a defragcp file already exists, then read it and apply the checkpoint order
923                 float fh;
924                 float len;
925                 string l;
926
927                 defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_READ);
928                 if (fh >= 0) {
929                         while ((l = fgets(fh))) {
930                                 len = tokenize_console(l);
931                                 if (len != 2) {
932                                         defragcpexists = -1; // something's wrong in the defrag cp file, set defragcpexists to -1 so that it will be rewritten when someone finishes
933                                         continue;
934                                 }
935                                 for (entity cp = NULL; (cp = find(cp, classname, "target_checkpoint"));) {
936                                         if (argv(0) == cp.targetname) {
937                                                 cp.race_checkpoint = stof(argv(1));
938                                         }
939                                 }
940                         }
941                         fclose(fh);
942                 }
943         }
944
945         g_race_qualifying = qual;
946
947         if (race_timed_checkpoint) {
948                 if (defrag_ents) {
949                         IL_EACH(g_race_targets, it.classname == "target_checkpoint" || it.classname == "target_startTimer" || it.classname == "target_stopTimer",
950                         {
951                                 defrag_waypointsprites(it, it);
952
953                                 if(it.classname == "target_checkpoint") {
954                                         if(it.race_checkpoint == -2)
955                                                 defragcpexists = -1; // 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
956                                 }
957                         });
958                         if (defragcpexists != -1) {
959                                 float largest_cp_id = 0;
960                                 for (entity cp = NULL; (cp = find(cp, classname, "target_checkpoint"));) {
961                                         if (cp.race_checkpoint > largest_cp_id) {
962                                                 largest_cp_id = cp.race_checkpoint;
963                                         }
964                                 }
965                                 for (entity cp = NULL; (cp = find(cp, classname, "target_stopTimer"));) {
966                                         cp.race_checkpoint = largest_cp_id + 1; // finish line
967                                 }
968                                 race_highest_checkpoint = largest_cp_id + 1;
969                                 race_timed_checkpoint = largest_cp_id + 1;
970                         } else {
971                                 for (entity cp = NULL; (cp = find(cp, classname, "target_stopTimer"));) {
972                                         cp.race_checkpoint = 255; // finish line
973                                 }
974                                 race_highest_checkpoint = 255;
975                                 race_timed_checkpoint = 255;
976                         }
977                 } else {
978                         IL_EACH(g_racecheckpoints, it.sprite,
979                         {
980                                 if (it.race_checkpoint == 0) {
981                                         WaypointSprite_UpdateSprites(it.sprite, WP_RaceStart, WP_Null, WP_Null);
982                                 } else if (it.race_checkpoint == race_timed_checkpoint) {
983                                         WaypointSprite_UpdateSprites(it.sprite, WP_RaceFinish, WP_Null, WP_Null);
984                                 }
985                         });
986                 }
987         }
988
989         if (defrag_ents) { /* The following hack shall be removed when per-player trigger_multiple.wait is implemented for cts */
990                 for (entity trigger = NULL; (trigger = find(trigger, classname, "trigger_multiple")); ) {
991                         for (entity targ = NULL; (targ = find(targ, targetname, trigger.target)); ) {
992                                 if (targ.classname == "target_checkpoint" || targ.classname == "target_startTimer" || targ.classname == "target_stopTimer") {
993                                         trigger.wait = 0;
994                                         trigger.delay = 0;
995                                         targ.wait = 0;
996                                         targ.delay = 0;
997
998                     // These just make the game crash on some maps with oddly shaped triggers.
999                     // (on the other hand they used to fix the case when two players ran through a checkpoint at once,
1000                     // and often one of them just passed through without being registered. Hope it's fixed  in a better way now.
1001                     // (happened on item triggers too)
1002                     //
1003                                         //targ.wait = -2;
1004                                         //targ.delay = 0;
1005
1006                                         //setsize(targ, trigger.mins, trigger.maxs);
1007                                         //setorigin(targ, trigger.origin);
1008                                         //remove(trigger);
1009                                 }
1010             }
1011         }
1012         }
1013 }
1014
1015 vector trigger_race_checkpoint_spawn_evalfunc(entity this, entity player, entity spot, vector current)
1016 {
1017         if(g_race_qualifying)
1018         {
1019                 // spawn at first
1020                 if(this.race_checkpoint != 0)
1021                         return '-1 0 0';
1022                 if(spot.race_place != race_lowest_place_spawn)
1023                         return '-1 0 0';
1024         }
1025         else
1026         {
1027                 if(this.race_checkpoint != player.race_respawn_checkpoint)
1028                         return '-1 0 0';
1029                 // try reusing the previous spawn
1030                 if(this == player.race_respawn_spotref || spot == player.race_respawn_spotref)
1031                         current.x += SPAWN_PRIO_RACE_PREVIOUS_SPAWN;
1032                 if(this.race_checkpoint == 0)
1033                 {
1034                         int pl = player.race_place;
1035                         if(pl > race_highest_place_spawn)
1036                                 pl = 0;
1037                         if(pl == 0 && !player.race_started)
1038                                 pl = race_highest_place_spawn; // use last place if he has not even touched finish yet
1039                         if(spot.race_place != pl)
1040                                 return '-1 0 0';
1041                 }
1042         }
1043         return current;
1044 }
1045
1046 spawnfunc(trigger_race_checkpoint)
1047 {
1048         vector o;
1049         if(!g_race && !g_cts) { delete(this); return; }
1050
1051         EXACTTRIGGER_INIT;
1052
1053         this.use = checkpoint_use;
1054         if (!(this.spawnflags & 1))
1055                 settouch(this, checkpoint_touch);
1056
1057         o = (this.absmin + this.absmax) * 0.5;
1058         tracebox(o, PL_MIN_CONST, PL_MAX_CONST, o - '0 0 1' * (o.z - this.absmin.z), MOVE_NORMAL, this);
1059         waypoint_spawnforitem_force(this, trace_endpos);
1060         this.nearestwaypointtimeout = -1;
1061
1062         if(this.message == "")
1063                 this.message = "went backwards";
1064         if (this.message2 == "")
1065                 this.message2 = "was pushed backwards by";
1066         if (this.race_penalty_reason == "")
1067                 this.race_penalty_reason = "missing a checkpoint";
1068
1069         this.race_checkpoint = this.cnt;
1070
1071         if(this.race_checkpoint > race_highest_checkpoint)
1072         {
1073                 race_highest_checkpoint = this.race_checkpoint;
1074                 if(this.spawnflags & 8)
1075                         race_timed_checkpoint = this.race_checkpoint;
1076                 else
1077                         race_timed_checkpoint = 0;
1078         }
1079
1080         if(!this.race_penalty)
1081         {
1082                 if(this.race_checkpoint)
1083                         WaypointSprite_SpawnFixed(WP_RaceCheckpoint, o, this, sprite, RADARICON_NONE);
1084                 else
1085                         WaypointSprite_SpawnFixed(WP_RaceStartFinish, o, this, sprite, RADARICON_NONE);
1086         }
1087
1088         this.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
1089         this.spawn_evalfunc = trigger_race_checkpoint_spawn_evalfunc;
1090
1091         if (!g_racecheckpoints)
1092                 g_racecheckpoints = IL_NEW();
1093         IL_PUSH(g_racecheckpoints, this);
1094
1095         // trigger_race_checkpoint_verify checks this list too
1096         if (!g_race_targets)
1097                 g_race_targets = IL_NEW();
1098
1099         InitializeEntity(this, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
1100 }
1101
1102 void target_checkpoint_setup(entity this)
1103 {
1104         if(!g_race && !g_cts) { delete(this); return; }
1105         defrag_ents = 1;
1106
1107         // if this is targeted, then it probably isn't a trigger
1108         bool is_trigger = this.targetname == "";
1109
1110         if(is_trigger)
1111                 EXACTTRIGGER_INIT;
1112
1113         this.use = checkpoint_use;
1114         if (is_trigger && !(this.spawnflags & 1))
1115                 settouch(this, checkpoint_touch);
1116
1117         vector org = this.origin;
1118
1119         // bots should only pathfind to this if it is a valid touchable trigger
1120         if(is_trigger)
1121         {
1122                 org = (this.absmin + this.absmax) * 0.5;
1123                 tracebox(org, PL_MIN_CONST, PL_MAX_CONST, org - '0 0 1' * (org.z - this.absmin.z), MOVE_NORMAL, this);
1124                 waypoint_spawnforitem_force(this, trace_endpos);
1125                 this.nearestwaypointtimeout = -1;
1126         }
1127
1128         if(this.message == "")
1129                 this.message = "went backwards";
1130         if (this.message2 == "")
1131                 this.message2 = "was pushed backwards by";
1132         if (this.race_penalty_reason == "")
1133                 this.race_penalty_reason = "missing a checkpoint";
1134
1135         if(this.classname == "target_startTimer")
1136                 this.race_checkpoint = 0;
1137         else
1138                 this.race_checkpoint = -2;
1139
1140         race_timed_checkpoint = 1;
1141
1142         if (!g_race_targets)
1143                 g_race_targets = IL_NEW();
1144         IL_PUSH(g_race_targets, this);
1145
1146         // trigger_race_checkpoint_verify checks this list too
1147         if (!g_racecheckpoints)
1148                 g_racecheckpoints = IL_NEW();
1149
1150         InitializeEntity(this, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
1151 }
1152
1153 spawnfunc(target_checkpoint)
1154 {
1155         // xonotic defrag entity
1156         target_checkpoint_setup(this);
1157 }
1158
1159 // compatibility entity names
1160 spawnfunc(target_startTimer) { target_checkpoint_setup(this); }
1161 spawnfunc(target_stopTimer) { target_checkpoint_setup(this); }
1162
1163 void race_AbandonRaceCheck(entity p)
1164 {
1165         if(race_completing && !CS(p).race_completed)
1166         {
1167                 CS(p).race_completed = 1;
1168                 MAKE_INDEPENDENT_PLAYER(p);
1169                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_RACE_ABANDONED, p.netname);
1170                 ClientData_Touch(p);
1171         }
1172 }
1173
1174 void race_StartCompleting()
1175 {
1176         race_completing = 1;
1177         FOREACH_CLIENT(IS_PLAYER(it) && IS_DEAD(it), { race_AbandonRaceCheck(it); });
1178 }
1179
1180 void race_PreparePlayer(entity this)
1181 {
1182         race_ClearTime(this);
1183         this.race_place = 0;
1184         this.race_started = 0;
1185         this.race_respawn_checkpoint = 0;
1186         this.race_respawn_spotref = NULL;
1187 }
1188
1189 void race_RetractPlayer(entity this)
1190 {
1191         if(!g_race && !g_cts)
1192                 return;
1193         if(this.race_respawn_checkpoint == 0 || this.race_respawn_checkpoint == race_timed_checkpoint)
1194                 race_ClearTime(this);
1195         this.race_checkpoint = this.race_respawn_checkpoint;
1196 }
1197
1198 spawnfunc(info_player_race)
1199 {
1200         if(!g_race && !g_cts) { delete(this); return; }
1201         ++race_spawns;
1202         spawnfunc_info_player_deathmatch(this);
1203
1204         if(this.race_place > race_highest_place_spawn)
1205                 race_highest_place_spawn = this.race_place;
1206         if(this.race_place < race_lowest_place_spawn)
1207                 race_lowest_place_spawn = this.race_place;
1208 }
1209
1210 void race_ClearRecords()
1211 {
1212         for(int j = 0; j < MAX_CHECKPOINTS; ++j)
1213         {
1214                 race_checkpoint_records[j] = 0;
1215                 strfree(race_checkpoint_recordholders[j]);
1216         }
1217
1218         FOREACH_CLIENT(true, {
1219                 float p = it.race_place;
1220                 race_PreparePlayer(it);
1221                 it.race_place = p;
1222         });
1223 }
1224
1225 void race_ImposePenaltyTime(entity pl, float penalty, string reason)
1226 {
1227         if(g_race_qualifying)
1228         {
1229                 pl.race_penalty_accumulator += penalty;
1230                 if(IS_REAL_CLIENT(pl))
1231                 {
1232                         msg_entity = pl;
1233                         WRITESPECTATABLE_MSG_ONE(msg_entity, {
1234                                 WriteHeader(MSG_ONE, TE_CSQC_RACE);
1235                                 WriteByte(MSG_ONE, RACE_NET_PENALTY_QUALIFYING);
1236                                 WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1237                                 WriteString(MSG_ONE, reason);
1238                         });
1239                 }
1240         }
1241         else
1242         {
1243                 pl.race_penalty = time + penalty;
1244                 if(IS_REAL_CLIENT(pl))
1245                 {
1246                         msg_entity = pl;
1247                         WRITESPECTATABLE_MSG_ONE(msg_entity, {
1248                                 WriteHeader(MSG_ONE, TE_CSQC_RACE);
1249                                 WriteByte(MSG_ONE, RACE_NET_PENALTY_RACE);
1250                                 WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1251                                 WriteString(MSG_ONE, reason);
1252                         });
1253                 }
1254         }
1255 }
1256
1257 void penalty_touch(entity this, entity toucher)
1258 {
1259         EXACTTRIGGER_TOUCH(this, toucher);
1260         if(toucher.race_lastpenalty != this)
1261         {
1262                 toucher.race_lastpenalty = this;
1263                 race_ImposePenaltyTime(toucher, this.race_penalty, this.race_penalty_reason);
1264         }
1265 }
1266
1267 void penalty_use(entity this, entity actor, entity trigger)
1268 {
1269         race_ImposePenaltyTime(actor, this.race_penalty, this.race_penalty_reason);
1270 }
1271
1272 spawnfunc(trigger_race_penalty)
1273 {
1274         // TODO: find out why this wasnt done:
1275         //if(!g_cts && !g_race) { remove(this); return; }
1276
1277         EXACTTRIGGER_INIT;
1278
1279         this.use = penalty_use;
1280         if (!(this.spawnflags & 1))
1281                 settouch(this, penalty_touch);
1282
1283         if (this.race_penalty_reason == "")
1284                 this.race_penalty_reason = "missing a checkpoint";
1285         if (!this.race_penalty)
1286                 this.race_penalty = 5;
1287 }
1288
1289 float race_GetFractionalLapCount(entity e)
1290 {
1291         // interesting metrics (idea by KrimZon) to maybe sort players in the
1292         // scoreboard, immediately updates when overtaking
1293         //
1294         // requires the track to be built so you never get farther away from the
1295         // next checkpoint, though, and current Xonotic race maps are not built that
1296         // way
1297         //
1298         // also, this code is slow and would need optimization (i.e. "next CP"
1299         // links on CP entities)
1300
1301         float l;
1302         l = GameRules_scoring_add(e, RACE_LAPS, 0);
1303         if(CS(e).race_completed)
1304                 return l; // not fractional
1305
1306         vector o0, o1;
1307         float bestfraction, fraction;
1308         entity lastcp;
1309         float nextcpindex, lastcpindex;
1310
1311         nextcpindex = max(e.race_checkpoint, 0);
1312         lastcpindex = e.race_respawn_checkpoint;
1313         lastcp = e.race_respawn_spotref;
1314
1315         if(nextcpindex == lastcpindex)
1316                 return l; // finish
1317
1318         bestfraction = 1;
1319         IL_EACH(g_racecheckpoints, true,
1320         {
1321                 if(it.race_checkpoint != lastcpindex)
1322                         continue;
1323                 if(lastcp)
1324                         if(it != lastcp)
1325                                 continue;
1326                 o0 = (it.absmin + it.absmax) * 0.5;
1327                 IL_EACH(g_racecheckpoints, true,
1328                 {
1329                         if(it.race_checkpoint != nextcpindex)
1330                                 continue;
1331                         o1 = (it.absmin + it.absmax) * 0.5;
1332                         if(o0 == o1)
1333                                 continue;
1334                         fraction = bound(0.0001, vlen(e.origin - o1) / vlen(o0 - o1), 1);
1335                         if(fraction < bestfraction)
1336                                 bestfraction = fraction;
1337                 });
1338         });
1339
1340         // we are at CP "nextcpindex - bestfraction"
1341         // race_timed_checkpoint == 4: then nextcp==4 means 0.9999x, nextcp==0 means 0.0000x
1342         // race_timed_checkpoint == 0: then nextcp==0 means 0.9999x
1343         float c, nc;
1344         nc = race_highest_checkpoint + 1;
1345         c = ((nextcpindex - race_timed_checkpoint + nc + nc - 1) % nc) + 1 - bestfraction;
1346
1347         return l + c / nc;
1348 }