]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/race.qc
Fix FL_WEAPON flag overlapping FL_JUMPRELEASED. This unintentional change was introdu...
[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), false)),
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         if(g_race_qualifying)
479                 t += e.race_penalty_accumulator;
480
481         t = TIME_ENCODE(t); // make integer
482
483         if(tvalid)
484         if(cp == race_timed_checkpoint) // finish line
485         if (!CS(e).race_completed)
486         {
487                 int s = GameRules_scoring_add(e, RACE_FASTEST, 0);
488                 if(!s || t < s)
489                         GameRules_scoring_add(e, RACE_FASTEST, t - s);
490                 if(!g_race_qualifying)
491                 {
492                         s = GameRules_scoring_add(e, RACE_TIME, 0);
493                         int snew = TIME_ENCODE(time - game_starttime);
494                         GameRules_scoring_add(e, RACE_TIME, snew - s);
495                         int l = GameRules_scoring_add_team(e, RACE_LAPS, 1);
496
497                         if(autocvar_fraglimit)
498                                 if(l >= autocvar_fraglimit)
499                                         race_StartCompleting();
500
501                         if(race_completing)
502                         {
503                                 CS(e).race_completed = 1;
504                                 MAKE_INDEPENDENT_PLAYER(e);
505                                 if(e.bot_attack)
506                                         IL_REMOVE(g_bot_targets, e);
507                                 e.bot_attack = false;
508                                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_RACE_FINISHED, e.netname);
509                                 ClientData_Touch(e);
510                         }
511                 }
512         }
513
514         if(g_race_qualifying)
515         {
516                 float recordtime;
517                 string recordholder;
518
519                 if(tvalid)
520                 {
521                         recordtime = race_checkpoint_records[cp];
522                         float myrecordtime = e.race_checkpoint_record[cp];
523                         recordholder = strcat1(race_checkpoint_recordholders[cp]); // make a tempstring copy, as we'll possibly strunzone it!
524                         if(recordholder == e.netname)
525                                 recordholder = "";
526
527                         if(t != 0)
528                         {
529                                 if(cp == race_timed_checkpoint)
530                                 {
531                                         race_setTime(GetMapname(), t, e.crypto_idfp, e.netname, e, true);
532                                         MUTATOR_CALLHOOK(Race_FinalCheckpoint, e);
533                                 }
534                                 if(t < myrecordtime || myrecordtime == 0)
535                                         e.race_checkpoint_record[cp] = t; // resending done below
536
537                                 if(t < recordtime || recordtime == 0)
538                                 {
539                                         race_checkpoint_records[cp] = t;
540                                         strcpy(race_checkpoint_recordholders[cp], e.netname);
541                                         if(g_race_qualifying)
542                                                 FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it) && it.race_checkpoint == cp, { race_SendNextCheckpoint(it, 0); });
543                                 }
544
545                         }
546                 }
547                 else
548                 {
549                         // dummies
550                         t = 0;
551                         recordtime = 0;
552                         recordholder = "";
553                 }
554
555                 if(IS_REAL_CLIENT(e))
556                 {
557                         if(g_race_qualifying)
558                         {
559                                 FOREACH_CLIENT(IS_REAL_CLIENT(it),
560                                 {
561                                         if(it == e || (IS_SPEC(it) && it.enemy == e))
562                                         {
563                                                 msg_entity = it;
564                                                 WriteHeader(MSG_ONE, TE_CSQC_RACE);
565                                                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_QUALIFYING);
566                                                 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
567                                                 WriteInt24_t(MSG_ONE, t); // time to that intermediate
568                                                 WriteInt24_t(MSG_ONE, recordtime); // previously best time
569                                                 WriteInt24_t(MSG_ONE, ((tvalid) ? it.race_checkpoint_record[cp] : 0)); // previously best time
570                                                 WriteString(MSG_ONE, recordholder); // record holder
571                                         }
572                                 });
573                         }
574                 }
575         }
576         else // RACE! Not Qualifying
577         {
578                 float mylaps, lother, othtime;
579                 entity oth = race_checkpoint_lastplayers[cp];
580                 if(oth)
581                 {
582                         mylaps = GameRules_scoring_add(e, RACE_LAPS, 0);
583                         lother = race_checkpoint_lastlaps[cp];
584                         othtime = race_checkpoint_lasttimes[cp];
585                 }
586                 else
587                         mylaps = lother = othtime = 0;
588
589                 if(IS_REAL_CLIENT(e))
590                 {
591                         msg_entity = e;
592                         WRITESPECTATABLE_MSG_ONE(msg_entity, {
593                                 WriteHeader(MSG_ONE, TE_CSQC_RACE);
594                                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE);
595                                 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
596                                 if(e == oth)
597                                 {
598                                         WriteInt24_t(MSG_ONE, 0);
599                                         WriteByte(MSG_ONE, 0);
600                                         WriteByte(MSG_ONE, 0);
601                                 }
602                                 else
603                                 {
604                                         WriteInt24_t(MSG_ONE, TIME_ENCODE(time - race_checkpoint_lasttimes[cp]));
605                                         WriteByte(MSG_ONE, mylaps - lother);
606                                         WriteByte(MSG_ONE, etof(oth)); // record holder
607                                 }
608                         });
609                 }
610
611                 race_checkpoint_lastplayers[cp] = e;
612                 race_checkpoint_lasttimes[cp] = time;
613                 race_checkpoint_lastlaps[cp] = mylaps;
614
615                 if(IS_REAL_CLIENT(oth))
616                 {
617                         msg_entity = oth;
618                         WRITESPECTATABLE_MSG_ONE(msg_entity, {
619                                 WriteHeader(MSG_ONE, TE_CSQC_RACE);
620                                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE_BY_OPPONENT);
621                                 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
622                                 if(e == oth)
623                                 {
624                                         WriteInt24_t(MSG_ONE, 0);
625                                         WriteByte(MSG_ONE, 0);
626                                         WriteByte(MSG_ONE, 0);
627                                 }
628                                 else
629                                 {
630                                         WriteInt24_t(MSG_ONE, TIME_ENCODE(time - othtime));
631                                         WriteByte(MSG_ONE, lother - mylaps);
632                                         WriteByte(MSG_ONE, etof(e) - 1); // record holder
633                                 }
634                         });
635                 }
636         }
637 }
638
639 void race_ClearTime(entity e)
640 {
641         e.race_checkpoint = 0;
642         e.race_laptime = 0;
643         e.race_movetime = e.race_movetime_frac = e.race_movetime_count = 0;
644         e.race_penalty_accumulator = 0;
645         e.race_lastpenalty = NULL;
646
647         if(!IS_REAL_CLIENT(e))
648                 return;
649
650         msg_entity = e;
651         WRITESPECTATABLE_MSG_ONE(msg_entity, {
652                 WriteHeader(MSG_ONE, TE_CSQC_RACE);
653                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_CLEAR); // next
654         });
655 }
656
657 void checkpoint_passed(entity this, entity player)
658 {
659         if(IS_VEHICLE(player) && player.owner)
660                 player = player.owner;
661
662         if(player.personal && autocvar_g_allow_checkpoints)
663                 return; // practice mode!
664
665         if(player.classname == "porto")
666         {
667                 // do not allow portalling through checkpoints
668                 trace_plane_normal = normalize(-1 * player.velocity);
669                 W_Porto_Fail(player, 0);
670                 return;
671         }
672
673         string oldmsg; // used twice
674
675         /*
676          * Trigger targets
677          */
678         if (!((this.spawnflags & 2) && (IS_PLAYER(player))))
679         {
680                 oldmsg = this.message;
681                 this.message = "";
682                 SUB_UseTargets(this, player, player);
683                 this.message = oldmsg;
684         }
685
686         if (!IS_PLAYER(player))
687                 return;
688
689         /*
690          * Remove unauthorized equipment
691          */
692         Portal_ClearAll(player);
693
694         player.porto_forbidden = 2; // decreased by 1 each StartFrame
695
696         if(defrag_ents)
697         {
698                 if(this.race_checkpoint == -2)
699                 {
700                         this.race_checkpoint = player.race_checkpoint;
701                 }
702
703                 int cp_amount = 0, largest_cp_id = 0;
704                 IL_EACH(g_race_targets, it.classname == "target_checkpoint",
705                 {
706                         cp_amount += 1;
707                         if(it.race_checkpoint > largest_cp_id) // update the finish id if someone hit a new checkpoint
708                         {
709                                 if(!largest_cp_id)
710                                 {
711                                         IL_EACH(g_race_targets, it.classname == "target_checkpoint",
712                                         {
713                                                 if(it.race_checkpoint == -2) // set defragcpexists to -1 so that the cp id file will be rewritten when someone finishes
714                                                         defragcpexists = -1;
715                                         });
716                                 }
717
718                                 largest_cp_id = it.race_checkpoint;
719                                 IL_EACH(g_race_targets, it.classname == "target_stopTimer",
720                                 {
721                                         it.race_checkpoint = largest_cp_id + 1; // finish line
722                                 });
723                                 race_highest_checkpoint = largest_cp_id + 1;
724                                 race_timed_checkpoint = largest_cp_id + 1;
725                         }
726                 });
727
728                 if(!cp_amount)
729                 {
730                         IL_EACH(g_race_targets, it.classname == "target_stopTimer",
731                         {
732                                 it.race_checkpoint = 1;
733                         });
734                         race_highest_checkpoint = 1;
735                         race_timed_checkpoint = 1;
736                 }
737         }
738
739         if((player.race_checkpoint == -1 && this.race_checkpoint == 0) || (player.race_checkpoint == this.race_checkpoint))
740         {
741                 if(this.race_penalty)
742                 {
743                         if(player.race_lastpenalty != this)
744                         {
745                                 player.race_lastpenalty = this;
746                                 race_ImposePenaltyTime(player, this.race_penalty, this.race_penalty_reason);
747                         }
748                 }
749
750                 if(player.race_penalty)
751                         return;
752
753                 /*
754                  * Trigger targets
755                  */
756                 if(this.spawnflags & 2)
757                 {
758                         oldmsg = this.message;
759                         this.message = "";
760                         SUB_UseTargets(this, player, player); // TODO: should we be using other for the trigger here?
761                         this.message = oldmsg;
762                 }
763
764                 if(player.race_respawn_checkpoint != this.race_checkpoint || !player.race_started)
765                         player.race_respawn_spotref = this; // this is not a spot but a CP, but spawnpoint selection will deal with that
766                 player.race_respawn_checkpoint = this.race_checkpoint;
767                 player.race_checkpoint = race_NextCheckpoint(this.race_checkpoint);
768                 player.race_started = 1;
769
770                 race_SendTime(player, this.race_checkpoint, player.race_movetime, boolean(player.race_laptime));
771
772                 if(!this.race_checkpoint) // start line
773                 {
774                         player.race_laptime = time;
775                         player.race_movetime = player.race_movetime_frac = player.race_movetime_count = 0;
776                         player.race_penalty_accumulator = 0;
777                         player.race_lastpenalty = NULL;
778                 }
779
780                 if(g_race_qualifying)
781                         race_SendNextCheckpoint(player, 0);
782
783                 if(defrag_ents && defragcpexists < 0 && this.classname == "target_stopTimer")
784                 {
785                         float fh;
786                         defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_WRITE);
787                         if(fh >= 0)
788                         {
789                                 IL_EACH(g_race_targets, it.classname == "target_checkpoint",
790                                 {
791                                         fputs(fh, strcat(it.targetname, " ", ftos(it.race_checkpoint), "\n"));
792                                 });
793                         }
794                         fclose(fh);
795                 }
796         }
797         else if(player.race_checkpoint == race_NextCheckpoint(this.race_checkpoint))
798         {
799                 // ignored
800         }
801         else
802         {
803                 if(this.spawnflags & 4)
804                         Damage (player, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, player.origin, '0 0 0');
805         }
806 }
807
808 void checkpoint_touch(entity this, entity toucher)
809 {
810         EXACTTRIGGER_TOUCH(this, toucher);
811         checkpoint_passed(this, toucher);
812 }
813
814 void checkpoint_use(entity this, entity actor, entity trigger)
815 {
816         if(trigger.classname == "info_player_deathmatch") // a spawn, a spawn
817                 return;
818
819         checkpoint_passed(this, actor);
820 }
821
822 bool race_waypointsprite_visible_for_player(entity this, entity player, entity view)
823 {
824         entity own = this.owner;
825         if(this.realowner)
826                 own = this.realowner; // target support
827
828         if(view.race_checkpoint == -1 || own.race_checkpoint == -2)
829                 return true;
830         else if(view.race_checkpoint == own.race_checkpoint)
831                 return true;
832         else
833                 return false;
834 }
835
836 void defrag_waypointsprites(entity targeted, entity checkpoint)
837 {
838         // bones_was_here: spawn a waypoint for every entity with a bmodel
839         // that directly or indirectly targets this checkpoint
840         // (anything a player could touch or shoot to activate this cp)
841
842         entity s = WP_RaceCheckpoint;
843         if (checkpoint.classname == "target_startTimer")
844                 s = WP_RaceStart;
845         else if (checkpoint.classname == "target_stopTimer")
846                 s = WP_RaceFinish;
847
848         for (entity t = findchain(target, targeted.targetname); t; t = t.chain)
849         {
850                 if (t.modelindex)
851                 {
852                         WaypointSprite_SpawnFixed(s, (t.absmin + t.absmax) * 0.5, t, sprite, RADARICON_NONE);
853                         t.sprite.realowner = checkpoint;
854                         t.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
855                 }
856
857                 if (t.targetname)
858                         defrag_waypointsprites(t, checkpoint);
859         }
860 }
861
862 void trigger_race_checkpoint_verify(entity this)
863 {
864         static bool have_verified;
865         if (have_verified) return;
866         have_verified = true;
867
868         int qual = g_race_qualifying;
869
870         int pl_race_checkpoint = 0;
871         int pl_race_place = 0;
872
873         if (g_race) {
874                 for (int i = 0; i <= race_highest_checkpoint; ++i) {
875                         pl_race_checkpoint = race_NextCheckpoint(i);
876
877                         // race only (middle of the race)
878                         g_race_qualifying = 0;
879                         pl_race_place = 0;
880                         if (!Spawn_FilterOutBadSpots(this, findchain(classname, "info_player_deathmatch"), 0, false, true)) {
881                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(pl_race_place), " (used for respawning in race) - bailing out"));
882                         }
883
884                         if (i == 0) {
885                                 // qualifying only
886                                 g_race_qualifying = 1;
887                                 pl_race_place = race_lowest_place_spawn;
888                                 if (!Spawn_FilterOutBadSpots(this, findchain(classname, "info_player_deathmatch"), 0, false, true)) {
889                                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(pl_race_place), " (used for qualifying) - bailing out"));
890                                 }
891
892                                 // race only (initial spawn)
893                                 g_race_qualifying = 0;
894                                 for (int p = 1; p <= race_highest_place_spawn; ++p) {
895                                         pl_race_place = p;
896                                         if (!Spawn_FilterOutBadSpots(this, findchain(classname, "info_player_deathmatch"), 0, false, true)) {
897                                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(pl_race_place), " (used for initially spawning in race) - bailing out"));
898                                         }
899                                 }
900                         }
901                 }
902         } else if (!defrag_ents) {
903                 // qualifying only
904                 pl_race_checkpoint = race_NextCheckpoint(0);
905                 g_race_qualifying = 1;
906                 pl_race_place = race_lowest_place_spawn;
907                 if (!Spawn_FilterOutBadSpots(this, findchain(classname, "info_player_deathmatch"), 0, false, true)) {
908                         error(strcat("Checkpoint 0 misses a spawnpoint with race_place==", ftos(pl_race_place), " (used for qualifying) - bailing out"));
909                 }
910         } else {
911                 pl_race_checkpoint = race_NextCheckpoint(0);
912                 g_race_qualifying = 1;
913                 pl_race_place = 0; // there's only one spawn on defrag maps
914
915                 // check if a defragcp file already exists, then read it and apply the checkpoint order
916                 float fh;
917                 float len;
918                 string l;
919
920                 defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_READ);
921                 if (fh >= 0) {
922                         while ((l = fgets(fh))) {
923                                 len = tokenize_console(l);
924                                 if (len != 2) {
925                                         defragcpexists = -1; // something's wrong in the defrag cp file, set defragcpexists to -1 so that it will be rewritten when someone finishes
926                                         continue;
927                                 }
928                                 for (entity cp = NULL; (cp = find(cp, classname, "target_checkpoint"));) {
929                                         if (argv(0) == cp.targetname) {
930                                                 cp.race_checkpoint = stof(argv(1));
931                                         }
932                                 }
933                         }
934                         fclose(fh);
935                 }
936         }
937
938         g_race_qualifying = qual;
939
940         if (race_timed_checkpoint) {
941                 if (defrag_ents) {
942                         IL_EACH(g_race_targets, it.classname == "target_checkpoint" || it.classname == "target_startTimer" || it.classname == "target_stopTimer",
943                         {
944                                 defrag_waypointsprites(it, it);
945
946                                 if(it.classname == "target_checkpoint") {
947                                         if(it.race_checkpoint == -2)
948                                                 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
949                                 }
950                         });
951                         if (defragcpexists != -1) {
952                                 float largest_cp_id = 0;
953                                 for (entity cp = NULL; (cp = find(cp, classname, "target_checkpoint"));) {
954                                         if (cp.race_checkpoint > largest_cp_id) {
955                                                 largest_cp_id = cp.race_checkpoint;
956                                         }
957                                 }
958                                 for (entity cp = NULL; (cp = find(cp, classname, "target_stopTimer"));) {
959                                         cp.race_checkpoint = largest_cp_id + 1; // finish line
960                                 }
961                                 race_highest_checkpoint = largest_cp_id + 1;
962                                 race_timed_checkpoint = largest_cp_id + 1;
963                         } else {
964                                 for (entity cp = NULL; (cp = find(cp, classname, "target_stopTimer"));) {
965                                         cp.race_checkpoint = 255; // finish line
966                                 }
967                                 race_highest_checkpoint = 255;
968                                 race_timed_checkpoint = 255;
969                         }
970                 } else {
971                         IL_EACH(g_racecheckpoints, it.sprite,
972                         {
973                                 if (it.race_checkpoint == 0) {
974                                         WaypointSprite_UpdateSprites(it.sprite, WP_RaceStart, WP_Null, WP_Null);
975                                 } else if (it.race_checkpoint == race_timed_checkpoint) {
976                                         WaypointSprite_UpdateSprites(it.sprite, WP_RaceFinish, WP_Null, WP_Null);
977                                 }
978                         });
979                 }
980         }
981
982         if (defrag_ents) { /* The following hack shall be removed when per-player trigger_multiple.wait is implemented for cts */
983                 for (entity trigger = NULL; (trigger = find(trigger, classname, "trigger_multiple")); ) {
984                         for (entity targ = NULL; (targ = find(targ, targetname, trigger.target)); ) {
985                                 if (targ.classname == "target_checkpoint" || targ.classname == "target_startTimer" || targ.classname == "target_stopTimer") {
986                                         trigger.wait = 0;
987                                         trigger.delay = 0;
988                                         targ.wait = 0;
989                                         targ.delay = 0;
990
991                     // These just make the game crash on some maps with oddly shaped triggers.
992                     // (on the other hand they used to fix the case when two players ran through a checkpoint at once,
993                     // and often one of them just passed through without being registered. Hope it's fixed  in a better way now.
994                     // (happened on item triggers too)
995                     //
996                                         //targ.wait = -2;
997                                         //targ.delay = 0;
998
999                                         //setsize(targ, trigger.mins, trigger.maxs);
1000                                         //setorigin(targ, trigger.origin);
1001                                         //remove(trigger);
1002                                 }
1003             }
1004         }
1005         }
1006 }
1007
1008 vector trigger_race_checkpoint_spawn_evalfunc(entity this, entity player, entity spot, vector current)
1009 {
1010         if(g_race_qualifying)
1011         {
1012                 // spawn at first
1013                 if(this.race_checkpoint != 0)
1014                         return '-1 0 0';
1015                 if(spot.race_place != race_lowest_place_spawn)
1016                         return '-1 0 0';
1017         }
1018         else
1019         {
1020                 if(this.race_checkpoint != player.race_respawn_checkpoint)
1021                         return '-1 0 0';
1022                 // try reusing the previous spawn
1023                 if(this == player.race_respawn_spotref || spot == player.race_respawn_spotref)
1024                         current.x += SPAWN_PRIO_RACE_PREVIOUS_SPAWN;
1025                 if(this.race_checkpoint == 0)
1026                 {
1027                         int pl = player.race_place;
1028                         if(pl > race_highest_place_spawn)
1029                                 pl = 0;
1030                         if(pl == 0 && !player.race_started)
1031                                 pl = race_highest_place_spawn; // use last place if he has not even touched finish yet
1032                         if(spot.race_place != pl)
1033                                 return '-1 0 0';
1034                 }
1035         }
1036         return current;
1037 }
1038
1039 spawnfunc(trigger_race_checkpoint)
1040 {
1041         vector o;
1042         if(!g_race && !g_cts) { delete(this); return; }
1043
1044         EXACTTRIGGER_INIT;
1045
1046         this.use = checkpoint_use;
1047         if (!(this.spawnflags & 1))
1048                 settouch(this, checkpoint_touch);
1049
1050         o = (this.absmin + this.absmax) * 0.5;
1051         tracebox(o, PL_MIN_CONST, PL_MAX_CONST, o - '0 0 1' * (o.z - this.absmin.z), MOVE_NORMAL, this);
1052         waypoint_spawnforitem_force(this, trace_endpos);
1053         this.nearestwaypointtimeout = -1;
1054
1055         if(this.message == "")
1056                 this.message = "went backwards";
1057         if (this.message2 == "")
1058                 this.message2 = "was pushed backwards by";
1059         if (this.race_penalty_reason == "")
1060                 this.race_penalty_reason = "missing a checkpoint";
1061
1062         this.race_checkpoint = this.cnt;
1063
1064         if(this.race_checkpoint > race_highest_checkpoint)
1065         {
1066                 race_highest_checkpoint = this.race_checkpoint;
1067                 if(this.spawnflags & 8)
1068                         race_timed_checkpoint = this.race_checkpoint;
1069                 else
1070                         race_timed_checkpoint = 0;
1071         }
1072
1073         if(!this.race_penalty)
1074         {
1075                 if(this.race_checkpoint)
1076                         WaypointSprite_SpawnFixed(WP_RaceCheckpoint, o, this, sprite, RADARICON_NONE);
1077                 else
1078                         WaypointSprite_SpawnFixed(WP_RaceStartFinish, o, this, sprite, RADARICON_NONE);
1079         }
1080
1081         this.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
1082         this.spawn_evalfunc = trigger_race_checkpoint_spawn_evalfunc;
1083
1084         if (!g_racecheckpoints)
1085                 g_racecheckpoints = IL_NEW();
1086         IL_PUSH(g_racecheckpoints, this);
1087
1088         // trigger_race_checkpoint_verify checks this list too
1089         if (!g_race_targets)
1090                 g_race_targets = IL_NEW();
1091
1092         InitializeEntity(this, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
1093 }
1094
1095 void target_checkpoint_setup(entity this)
1096 {
1097         if(!g_race && !g_cts) { delete(this); return; }
1098         defrag_ents = 1;
1099
1100         // if this is targeted, then it probably isn't a trigger
1101         bool is_trigger = this.targetname == "";
1102
1103         if(is_trigger)
1104                 EXACTTRIGGER_INIT;
1105
1106         this.use = checkpoint_use;
1107         if (is_trigger && !(this.spawnflags & 1))
1108                 settouch(this, checkpoint_touch);
1109
1110         vector org = this.origin;
1111
1112         // bots should only pathfind to this if it is a valid touchable trigger
1113         if(is_trigger)
1114         {
1115                 org = (this.absmin + this.absmax) * 0.5;
1116                 tracebox(org, PL_MIN_CONST, PL_MAX_CONST, org - '0 0 1' * (org.z - this.absmin.z), MOVE_NORMAL, this);
1117                 waypoint_spawnforitem_force(this, trace_endpos);
1118                 this.nearestwaypointtimeout = -1;
1119         }
1120
1121         if(this.message == "")
1122                 this.message = "went backwards";
1123         if (this.message2 == "")
1124                 this.message2 = "was pushed backwards by";
1125         if (this.race_penalty_reason == "")
1126                 this.race_penalty_reason = "missing a checkpoint";
1127
1128         if(this.classname == "target_startTimer")
1129                 this.race_checkpoint = 0;
1130         else
1131                 this.race_checkpoint = -2;
1132
1133         race_timed_checkpoint = 1;
1134
1135         if (!g_race_targets)
1136                 g_race_targets = IL_NEW();
1137         IL_PUSH(g_race_targets, this);
1138
1139         // trigger_race_checkpoint_verify checks this list too
1140         if (!g_racecheckpoints)
1141                 g_racecheckpoints = IL_NEW();
1142
1143         InitializeEntity(this, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
1144 }
1145
1146 spawnfunc(target_checkpoint)
1147 {
1148         // xonotic defrag entity
1149         target_checkpoint_setup(this);
1150 }
1151
1152 // compatibility entity names
1153 spawnfunc(target_startTimer) { target_checkpoint_setup(this); }
1154 spawnfunc(target_stopTimer) { target_checkpoint_setup(this); }
1155
1156 void race_AbandonRaceCheck(entity p)
1157 {
1158         if(race_completing && !CS(p).race_completed)
1159         {
1160                 CS(p).race_completed = 1;
1161                 MAKE_INDEPENDENT_PLAYER(p);
1162                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_RACE_ABANDONED, p.netname);
1163                 ClientData_Touch(p);
1164         }
1165 }
1166
1167 void race_StartCompleting()
1168 {
1169         race_completing = 1;
1170         FOREACH_CLIENT(IS_PLAYER(it) && IS_DEAD(it), { race_AbandonRaceCheck(it); });
1171 }
1172
1173 void race_PreparePlayer(entity this)
1174 {
1175         race_ClearTime(this);
1176         this.race_place = 0;
1177         this.race_started = 0;
1178         this.race_respawn_checkpoint = 0;
1179         this.race_respawn_spotref = NULL;
1180 }
1181
1182 void race_RetractPlayer(entity this)
1183 {
1184         if(!g_race && !g_cts)
1185                 return;
1186         if(this.race_respawn_checkpoint == 0 || this.race_respawn_checkpoint == race_timed_checkpoint)
1187                 race_ClearTime(this);
1188         this.race_checkpoint = this.race_respawn_checkpoint;
1189 }
1190
1191 spawnfunc(info_player_race)
1192 {
1193         if(!g_race && !g_cts) { delete(this); return; }
1194         ++race_spawns;
1195         spawnfunc_info_player_deathmatch(this);
1196
1197         if(this.race_place > race_highest_place_spawn)
1198                 race_highest_place_spawn = this.race_place;
1199         if(this.race_place < race_lowest_place_spawn)
1200                 race_lowest_place_spawn = this.race_place;
1201 }
1202
1203 void race_ClearRecords()
1204 {
1205         for(int j = 0; j < MAX_CHECKPOINTS; ++j)
1206         {
1207                 race_checkpoint_records[j] = 0;
1208                 strfree(race_checkpoint_recordholders[j]);
1209         }
1210
1211         FOREACH_CLIENT(true, {
1212                 float p = it.race_place;
1213                 race_PreparePlayer(it);
1214                 it.race_place = p;
1215         });
1216 }
1217
1218 void race_ImposePenaltyTime(entity pl, float penalty, string reason)
1219 {
1220         if(g_race_qualifying)
1221         {
1222                 pl.race_penalty_accumulator += penalty;
1223                 if(IS_REAL_CLIENT(pl))
1224                 {
1225                         msg_entity = pl;
1226                         WRITESPECTATABLE_MSG_ONE(msg_entity, {
1227                                 WriteHeader(MSG_ONE, TE_CSQC_RACE);
1228                                 WriteByte(MSG_ONE, RACE_NET_PENALTY_QUALIFYING);
1229                                 WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1230                                 WriteString(MSG_ONE, reason);
1231                         });
1232                 }
1233         }
1234         else
1235         {
1236                 pl.race_penalty = time + penalty;
1237                 if(IS_REAL_CLIENT(pl))
1238                 {
1239                         msg_entity = pl;
1240                         WRITESPECTATABLE_MSG_ONE(msg_entity, {
1241                                 WriteHeader(MSG_ONE, TE_CSQC_RACE);
1242                                 WriteByte(MSG_ONE, RACE_NET_PENALTY_RACE);
1243                                 WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1244                                 WriteString(MSG_ONE, reason);
1245                         });
1246                 }
1247         }
1248 }
1249
1250 void penalty_touch(entity this, entity toucher)
1251 {
1252         EXACTTRIGGER_TOUCH(this, toucher);
1253         if(toucher.race_lastpenalty != this)
1254         {
1255                 toucher.race_lastpenalty = this;
1256                 race_ImposePenaltyTime(toucher, this.race_penalty, this.race_penalty_reason);
1257         }
1258 }
1259
1260 void penalty_use(entity this, entity actor, entity trigger)
1261 {
1262         race_ImposePenaltyTime(actor, this.race_penalty, this.race_penalty_reason);
1263 }
1264
1265 spawnfunc(trigger_race_penalty)
1266 {
1267         // TODO: find out why this wasnt done:
1268         //if(!g_cts && !g_race) { remove(this); return; }
1269
1270         EXACTTRIGGER_INIT;
1271
1272         this.use = penalty_use;
1273         if (!(this.spawnflags & 1))
1274                 settouch(this, penalty_touch);
1275
1276         if (this.race_penalty_reason == "")
1277                 this.race_penalty_reason = "missing a checkpoint";
1278         if (!this.race_penalty)
1279                 this.race_penalty = 5;
1280 }
1281
1282 float race_GetFractionalLapCount(entity e)
1283 {
1284         // interesting metrics (idea by KrimZon) to maybe sort players in the
1285         // scoreboard, immediately updates when overtaking
1286         //
1287         // requires the track to be built so you never get farther away from the
1288         // next checkpoint, though, and current Xonotic race maps are not built that
1289         // way
1290         //
1291         // also, this code is slow and would need optimization (i.e. "next CP"
1292         // links on CP entities)
1293
1294         float l;
1295         l = GameRules_scoring_add(e, RACE_LAPS, 0);
1296         if(CS(e).race_completed)
1297                 return l; // not fractional
1298
1299         vector o0, o1;
1300         float bestfraction, fraction;
1301         entity lastcp;
1302         float nextcpindex, lastcpindex;
1303
1304         nextcpindex = max(e.race_checkpoint, 0);
1305         lastcpindex = e.race_respawn_checkpoint;
1306         lastcp = e.race_respawn_spotref;
1307
1308         if(nextcpindex == lastcpindex)
1309                 return l; // finish
1310
1311         bestfraction = 1;
1312         IL_EACH(g_racecheckpoints, true,
1313         {
1314                 if(it.race_checkpoint != lastcpindex)
1315                         continue;
1316                 if(lastcp)
1317                         if(it != lastcp)
1318                                 continue;
1319                 o0 = (it.absmin + it.absmax) * 0.5;
1320                 IL_EACH(g_racecheckpoints, true,
1321                 {
1322                         if(it.race_checkpoint != nextcpindex)
1323                                 continue;
1324                         o1 = (it.absmin + it.absmax) * 0.5;
1325                         if(o0 == o1)
1326                                 continue;
1327                         fraction = bound(0.0001, vlen(e.origin - o1) / vlen(o0 - o1), 1);
1328                         if(fraction < bestfraction)
1329                                 bestfraction = fraction;
1330                 });
1331         });
1332
1333         // we are at CP "nextcpindex - bestfraction"
1334         // race_timed_checkpoint == 4: then nextcp==4 means 0.9999x, nextcp==0 means 0.0000x
1335         // race_timed_checkpoint == 0: then nextcp==0 means 0.9999x
1336         float c, nc;
1337         nc = race_highest_checkpoint + 1;
1338         c = ((nextcpindex - race_timed_checkpoint + nc + nc - 1) % nc) + 1 - bestfraction;
1339
1340         return l + c / nc;
1341 }