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