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