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