1 #define MAX_CHECKPOINTS 255
3 void spawnfunc_target_checkpoint();
6 .float race_penalty_accumulator;
7 .string race_penalty_reason;
8 .float race_checkpoint; // player: next checkpoint that has to be reached
10 .entity race_lastpenalty;
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];
20 float race_highest_checkpoint;
21 float race_timed_checkpoint;
26 float race_NextCheckpoint(float f)
28 if(f >= race_highest_checkpoint)
34 float race_PreviousCheckpoint(float f)
39 return race_highest_checkpoint;
45 // 0 = common start/finish
48 float race_CheckpointNetworkID(float f)
50 if(race_timed_checkpoint)
54 else if(f == race_timed_checkpoint)
60 void race_SendNextCheckpoint(entity e, float spec) // qualifying only
69 cp = e.race_checkpoint;
70 recordtime = race_checkpoint_records[cp];
71 recordholder = race_checkpoint_recordholders[cp];
72 if(recordholder == e.netname)
77 WRITESPECTATABLE_MSG_ONE({
78 WriteByte(MSG_ONE, SVC_TEMPENTITY);
79 WriteByte(MSG_ONE, TE_CSQC_RACE);
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);
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);
94 void race_InitSpectator()
97 if(msg_entity.enemy.race_laptime)
98 race_SendNextCheckpoint(msg_entity.enemy, 1);
102 float grecordtime[RANKINGS_CNT];
103 string grecordholder[RANKINGS_CNT];
104 string grecorduid[RANKINGS_CNT];
105 float worst_time; // last ranked time
106 float have_recs; // have we already read the records from the database before?
107 float race_GetTime(float pos) {
113 if (!have_recs) { // I guess this is better than checking if the first array item is empty, rumor has it that arrays are slow
115 for(i=0;i<RANKINGS_CNT;++i) {
116 grecordtime[i] = stof(db_get(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i))));
117 grecordholder[i] = strzone(db_get(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i))));
118 grecorduid[i] = strzone(db_get(ServerProgsDB, strcat(GetMapname(), rr, "crypto_idfp", ftos(i))));
120 grecordtime[0] = stof(db_get(ServerProgsDB, strcat(GetMapname(), rr, "time")));
121 grecordholder[0] = strzone(db_get(ServerProgsDB, strcat(GetMapname(), rr, "netname")));
122 grecorduid[0] = strzone(db_get(ServerProgsDB, strcat(GetMapname(), rr, "crypto_idfp")));
123 worst_time = stof(db_get(ServerProgsDB, strcat(GetMapname(), rr, strcat("time", ftos(RANKINGS_CNT-1)))));
127 return grecordtime[pos-1];
130 string race_GetName(float pos) { // these other functions assume that race_GetTime has been called >= once before
131 return grecordholder[pos-1];
134 float race_CheckUID(string myuid, string net_name) { // return existing UID or player name ranking pos, else 0
138 for (i=RANKINGS_CNT-1;i>=0;--i)
139 if(grecorduid[i] == myuid)
142 for (i=RANKINGS_CNT-1;i>=0;--i)
143 if(grecordholder[i] == net_name)
149 float race_CheckName(string net_name) { // Does the name already exist in rankings? In that case, where? (otherwise 0)
151 for (i=RANKINGS_CNT-1;i>=0;--i)
152 if(grecordholder[i] == net_name)
157 float race_GetPos(float t) {
161 for (i=0;i<RANKINGS_CNT;++i)
162 if (grecordtime[i] == 0 || grecordtime[i] > t)
165 for (i=0;i<RANKINGS_CNT;++i)
166 if (grecordtime[i] > t)
171 void race_send_recordtime(float msg)
173 // send the server best time
174 WriteByte(msg, SVC_TEMPENTITY);
175 WriteByte(msg, TE_CSQC_RACE);
176 WriteByte(msg, RACE_NET_SERVER_RECORD);
177 WriteInt24_t(msg, race_GetTime(1));
180 void race_SendRankings(float pos, float prevpos, float del, float msg)
182 WriteByte(msg, SVC_TEMPENTITY);
183 WriteByte(msg, TE_CSQC_RACE);
184 WriteByte(msg, RACE_NET_SERVER_RANKINGS);
185 WriteShort(msg, pos);
186 WriteShort(msg, prevpos);
187 WriteShort(msg, del);
188 WriteString(msg, race_GetName(pos));
189 WriteInt24_t(msg, race_GetTime(pos));
192 void race_SendStatus(float id, entity e)
200 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy3, {
201 WriteByte(msg, SVC_TEMPENTITY);
202 WriteByte(msg, TE_CSQC_RACE);
203 WriteByte(msg, RACE_NET_SERVER_STATUS);
205 WriteString(msg, e.netname);
209 string race_PlaceName(float pos) {
217 return strcat(ftos(pos), "th");
220 void race_SetTime(entity e, float t, float match_rec) {
222 pos = race_GetPos(t);
223 prevpos = race_CheckUID(e.crypto_idfp, e.netname);
226 string recorddifference;
227 if (prevpos && (prevpos < pos || !pos))
229 oldrec = race_GetTime(prevpos);
230 recorddifference = strcat(" ^1[+", TIME_ENCODED_TOSTRING(t - oldrec), "]");
231 bprint(e.netname, "^7 couldn't break their ", race_PlaceName(prevpos), " place record of ", TIME_ENCODED_TOSTRING(oldrec), recorddifference, "\n");
232 race_SendStatus(0, e); // "fail"
234 } else if (!pos) { // no ranking, time worse than the worst ranked
235 recorddifference = strcat(" ^1[+", TIME_ENCODED_TOSTRING(t - grecordtime[RANKINGS_CNT-1]), "]");
236 bprint(e.netname, "^7 couldn't break the ", race_PlaceName(RANKINGS_CNT), " place record of ", TIME_ENCODED_TOSTRING(grecordtime[RANKINGS_CNT-1]), recorddifference, "\n");
237 race_SendStatus(0, e); // "fail"
241 oldrec = grecordtime[pos-1];
243 // move other rankings out of the way
245 if (prevpos) { // player improved his existing record
246 for (i=prevpos-1;i>pos-1;--i) {
247 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), ftos(grecordtime[i-1]));
248 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i)), grecordholder[i-1]);
249 db_put(ServerProgsDB, strcat(GetMapname(), rr, "crypto_idfp", ftos(i)), grecorduid[i-1]);
250 grecordtime[i] = grecordtime[i-1];
252 if (grecordholder[i])
253 strunzone(grecordholder[i]);
254 grecordholder[i] = strzone(grecordholder[i-1]);
256 strunzone(grecorduid[i]);
257 grecorduid[i] = strzone(grecorduid[i-1]);
259 } else { // player has no ranked record yet
260 for (i=RANKINGS_CNT-1;i>pos-1;--i) {
261 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), ftos(grecordtime[i-1]));
262 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i)), grecordholder[i-1]);
263 db_put(ServerProgsDB, strcat(GetMapname(), rr, "crypto_idfp", ftos(i)), grecorduid[i-1]);
264 grecordtime[i] = grecordtime[i-1];
266 if (grecordholder[i])
267 strunzone(grecordholder[i]);
268 grecordholder[i] = strzone(grecordholder[i-1]);
270 strunzone(grecorduid[i]);
271 grecorduid[i] = strzone(grecorduid[i-1]);
277 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time"), ftos(t));
278 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname"), e.netname);
279 db_put(ServerProgsDB, strcat(GetMapname(), rr, "crypto_idfp"), e.crypto_idfp);
283 if (grecordholder[0])
284 strunzone(grecordholder[0]);
285 grecordholder[0] = strzone(e.netname);
287 strunzone(grecorduid[0]);
288 grecorduid[0] = strzone(e.crypto_idfp);
289 write_recordmarker(e, time - TIME_DECODE(t), TIME_DECODE(t));
290 race_send_recordtime(MSG_ALL);
292 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(pos-1)), ftos(t));
293 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(pos-1)), e.netname);
294 db_put(ServerProgsDB, strcat(GetMapname(), rr, "crypto_idfp", ftos(pos-1)), e.crypto_idfp);
296 grecordtime[pos-1] = t;
298 if (grecordholder[pos-1])
299 strunzone(grecordholder[pos-1]);
300 grecordholder[pos-1] = strzone(e.netname);
301 if (grecorduid[pos-1])
302 strunzone(grecorduid[pos-1]);
303 grecorduid[pos-1] = strzone(e.crypto_idfp);
306 if (pos == RANKINGS_CNT)
309 race_SendRankings(pos, prevpos, 0, MSG_ALL);
311 strunzone(rankings_reply);
312 rankings_reply = strzone(getrankings());
315 recorddifference = strcat(" ^2[-", TIME_ENCODED_TOSTRING(oldrec - t), "]");
316 bprint(e.netname, "^1 improved their 1st place record with ", TIME_ENCODED_TOSTRING(t), recorddifference, "\n");
317 } else if (oldrec == 0) {
318 bprint(e.netname, "^1 set the 1st place record with ", TIME_ENCODED_TOSTRING(t), "\n");
320 recorddifference = strcat(" ^2[-", TIME_ENCODED_TOSTRING(oldrec - t), "]");
321 bprint(e.netname, "^1 broke ", grecordholder[pos], "^1's 1st place record with ", strcat(TIME_ENCODED_TOSTRING(t), recorddifference, "\n"));
323 race_SendStatus(3, e); // "new server record"
326 recorddifference = strcat(" ^2[-", TIME_ENCODED_TOSTRING(oldrec - t), "]");
327 bprint(e.netname, "^5 improved their ", race_PlaceName(pos), " ^5place record with ", TIME_ENCODED_TOSTRING(t), recorddifference, "\n");
328 race_SendStatus(1, e); // "new time"
329 } else if (oldrec == 0) {
330 bprint(e.netname, "^2 set the ", race_PlaceName(pos), " ^2place record with ", TIME_ENCODED_TOSTRING(t), "\n");
331 race_SendStatus(2, e); // "new rank"
333 recorddifference = strcat(" ^2[-", TIME_ENCODED_TOSTRING(oldrec - t), "]");
334 bprint(e.netname, "^2 broke ", grecordholder[pos], "^2's ", race_PlaceName(pos), " ^2place record with ", strcat(TIME_ENCODED_TOSTRING(t), recorddifference, "\n"));
335 race_SendStatus(2, e); // "new rank"
340 void race_DeleteTime(float pos) {
343 for (i = pos-1; i <= RANKINGS_CNT-1; ++i) {
345 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time"), ftos(grecordtime[1]));
346 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname"), grecordholder[1]);
347 db_put(ServerProgsDB, strcat(GetMapname(), rr, "crypto_idfp"), grecorduid[1]);
348 grecordtime[0] = grecordtime[1];
349 if (grecordholder[i])
350 strunzone(grecordholder[0]);
351 grecordholder[0] = strzone(grecordholder[1]);
354 strunzone(grecorduid[0]);
355 grecorduid[0] = strzone(grecorduid[1]);
357 else if (i == RANKINGS_CNT-1) {
358 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), string_null);
359 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i)), string_null);
360 db_put(ServerProgsDB, strcat(GetMapname(), rr, "crypto_idfp", ftos(i)), string_null);
362 if (grecordholder[i])
363 strunzone(grecordholder[i]);
364 grecordholder[i] = string_null;
367 strunzone(grecorduid[i]);
368 grecorduid[i] = string_null;
371 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), ftos(grecordtime[i+1]));
372 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i)), grecordholder[i+1]);
373 db_put(ServerProgsDB, strcat(GetMapname(), rr, "crypto_idfp", ftos(i)), grecorduid[i+1]);
374 grecordtime[i] = grecordtime[i+1];
375 if (grecordholder[i])
376 strunzone(grecordholder[i]);
377 grecordholder[i] = strzone(grecordholder[i+1]);
380 strunzone(grecorduid[i]);
381 grecorduid[i] = strzone(grecorduid[i+1]);
385 race_SendRankings(pos, 0, 1, MSG_ALL);
387 race_send_recordtime(MSG_ALL);
390 strunzone(rankings_reply);
391 rankings_reply = strzone(getrankings());
396 void race_SendTime(entity e, float cp, float t, float tvalid)
401 if(g_race_qualifying)
402 t += e.race_penalty_accumulator;
404 t = TIME_ENCODE(t); // make integer
405 // adding just 0.4 so it rounds down in the .5 case (matching the timer display)
408 if(cp == race_timed_checkpoint) // finish line
409 if not(e.race_completed)
412 if(g_race_qualifying)
414 s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
416 PlayerScore_Add(e, SP_RACE_FASTEST, t - s);
420 s = PlayerScore_Add(e, SP_RACE_TIME, 0);
421 snew = TIME_ENCODE(time - game_starttime);
422 PlayerScore_Add(e, SP_RACE_TIME, snew - s);
423 l = PlayerTeamScore_Add(e, SP_RACE_LAPS, ST_RACE_LAPS, 1);
425 if(cvar("fraglimit"))
426 if(l >= cvar("fraglimit"))
427 race_StartCompleting();
431 e.race_completed = 1;
432 MAKE_INDEPENDENT_PLAYER(e);
433 bprint(e.netname, "^7 has finished the race.\n");
441 if(g_race_qualifying)
445 recordtime = race_checkpoint_records[cp];
446 recordholder = strcat1(race_checkpoint_recordholders[cp]); // make a tempstring copy, as we'll possibly strunzone it!
447 if(recordholder == e.netname)
451 if(cp == race_timed_checkpoint)
452 race_SetTime(e, t, recordtime);
454 if(t < recordtime || recordtime == 0)
456 race_checkpoint_records[cp] = t;
457 if(race_checkpoint_recordholders[cp])
458 strunzone(race_checkpoint_recordholders[cp]);
459 race_checkpoint_recordholders[cp] = strzone(e.netname);
460 if(g_race_qualifying)
462 FOR_EACH_REALPLAYER(p)
463 if(p.race_checkpoint == cp)
464 race_SendNextCheckpoint(p, 0);
478 if(g_race_qualifying)
480 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy1, {
481 WriteByte(MSG_ONE, SVC_TEMPENTITY);
482 WriteByte(MSG_ONE, TE_CSQC_RACE);
483 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_QUALIFYING);
484 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
485 WriteInt24_t(MSG_ONE, t); // time to that intermediate
486 WriteInt24_t(MSG_ONE, recordtime); // previously best time
487 WriteString(MSG_ONE, recordholder); // record holder
491 else // RACE! Not Qualifying
493 float lself, lother, othtime;
495 oth = race_checkpoint_lastplayers[cp];
498 lself = PlayerScore_Add(e, SP_RACE_LAPS, 0);
499 lother = race_checkpoint_lastlaps[cp];
500 othtime = race_checkpoint_lasttimes[cp];
503 lself = lother = othtime = 0;
506 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy2, {
507 WriteByte(MSG_ONE, SVC_TEMPENTITY);
508 WriteByte(MSG_ONE, TE_CSQC_RACE);
509 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE);
510 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
513 WriteInt24_t(MSG_ONE, 0);
514 WriteByte(MSG_ONE, 0);
515 WriteString(MSG_ONE, "");
519 WriteInt24_t(MSG_ONE, TIME_ENCODE(time - race_checkpoint_lasttimes[cp]));
520 WriteByte(MSG_ONE, lself - lother);
521 WriteString(MSG_ONE, oth.netname); // record holder
525 race_checkpoint_lastplayers[cp] = e;
526 race_checkpoint_lasttimes[cp] = time;
527 race_checkpoint_lastlaps[cp] = lself;
530 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy3, {
531 WriteByte(MSG_ONE, SVC_TEMPENTITY);
532 WriteByte(MSG_ONE, TE_CSQC_RACE);
533 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE_BY_OPPONENT);
534 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
537 WriteInt24_t(MSG_ONE, 0);
538 WriteByte(MSG_ONE, 0);
539 WriteString(MSG_ONE, "");
543 WriteInt24_t(MSG_ONE, TIME_ENCODE(time - othtime));
544 WriteByte(MSG_ONE, lother - lself);
545 WriteString(MSG_ONE, e.netname); // record holder
551 void race_ClearTime(entity e)
553 e.race_checkpoint = 0;
555 e.race_movetime = e.race_movetime_frac = e.race_movetime_count = 0;
556 e.race_penalty_accumulator = 0;
557 e.race_lastpenalty = world;
560 WRITESPECTATABLE_MSG_ONE({
561 WriteByte(MSG_ONE, SVC_TEMPENTITY);
562 WriteByte(MSG_ONE, TE_CSQC_RACE);
563 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_CLEAR); // next
567 void dumpsurface(entity e)
571 print("Surfaces of ", etos(e), ":\n");
573 print("TEST = ", ftos(getsurfacenearpoint(e, '0 0 0')), "\n");
577 n = getsurfacenumpoints(e, si);
580 print(" Surface ", ftos(si), ":\n");
581 norm = getsurfacenormal(e, si);
582 print(" Normal = ", vtos(norm), "\n");
583 for(ni = 0; ni < n; ++ni)
585 vec = getsurfacepoint(e, si, ni);
586 print(" Point ", ftos(ni), " = ", vtos(vec), " (", ftos(norm * vec), ")\n");
591 void checkpoint_passed()
596 if(other.classname == "porto")
598 // do not allow portalling through checkpoints
599 trace_plane_normal = normalize(-1 * other.velocity);
608 if not((self.spawnflags & 2) && (other.classname == "player"))
611 oldmsg = self.message;
614 self.message = oldmsg;
617 if(other.classname != "player")
621 * Remove unauthorized equipment
623 Portal_ClearAll(other);
625 other.porto_forbidden = 2; // decreased by 1 each StartFrame
628 if(self.race_checkpoint == -2)
630 self.race_checkpoint = other.race_checkpoint;
635 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
637 if(cp.race_checkpoint > largest_cp_id) // update the finish id if someone hit a new checkpoint
639 largest_cp_id = cp.race_checkpoint;
640 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
641 cp.race_checkpoint = largest_cp_id + 1; // finish line
642 race_highest_checkpoint = largest_cp_id + 1;
643 race_timed_checkpoint = largest_cp_id + 1;
645 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
646 if(cp.race_checkpoint == -2) // set defragcpexists to -1 so that the cp id file will be rewritten when someone finishes
652 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
653 cp.race_checkpoint = 1;
654 race_highest_checkpoint = 1;
655 race_timed_checkpoint = 1;
659 if((other.race_checkpoint == -1 && self.race_checkpoint == 0) || (other.race_checkpoint == self.race_checkpoint))
661 if(self.race_penalty)
663 if(other.race_lastpenalty != self)
665 other.race_lastpenalty = self;
666 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
670 if(other.race_penalty)
676 if(self.spawnflags & 2)
679 oldmsg = self.message;
682 self.message = oldmsg;
685 if(other.race_respawn_checkpoint != self.race_checkpoint || !other.race_started)
686 other.race_respawn_spotref = self; // this is not a spot but a CP, but spawnpoint selection will deal with that
687 other.race_respawn_checkpoint = self.race_checkpoint;
688 other.race_checkpoint = race_NextCheckpoint(self.race_checkpoint);
689 other.race_started = 1;
691 race_SendTime(other, self.race_checkpoint, other.race_movetime, !!other.race_laptime);
693 if(!self.race_checkpoint) // start line
695 other.race_laptime = time;
696 other.race_movetime = other.race_movetime_frac = other.race_movetime_count = 0;
697 other.race_penalty_accumulator = 0;
698 other.race_lastpenalty = world;
701 if(g_race_qualifying)
702 race_SendNextCheckpoint(other, 0);
704 if(defrag_ents && defragcpexists < 0 && self.classname == "target_stopTimer")
707 defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_WRITE);
710 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
711 fputs(fh, strcat(cp.targetname, " ", ftos(cp.race_checkpoint), "\n"));
716 else if(other.race_checkpoint == race_NextCheckpoint(self.race_checkpoint))
722 if(self.spawnflags & 4)
723 Damage (other, self, self, 10000, DEATH_HURTTRIGGER, other.origin, '0 0 0');
727 void checkpoint_touch()
733 void checkpoint_use()
735 if(other.classname == "info_player_deathmatch") // a spawn, a spawn
742 float race_waypointsprite_visible_for_player(entity e)
744 if(e.race_checkpoint == -1 || self.owner.race_checkpoint == -2)
746 else if(e.race_checkpoint == self.owner.race_checkpoint)
753 void trigger_race_checkpoint_verify()
763 qual = g_race_qualifying;
767 self.classname = "player";
771 for(i = 0; i <= race_highest_checkpoint; ++i)
773 self.race_checkpoint = race_NextCheckpoint(i);
775 // race only (middle of the race)
776 g_race_qualifying = 0;
778 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
779 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for respawning in race) - bailing out"));
784 g_race_qualifying = 1;
785 self.race_place = race_lowest_place_spawn;
786 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
787 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
789 // race only (initial spawn)
790 g_race_qualifying = 0;
791 for(p = 1; p <= race_highest_place_spawn; ++p)
794 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
795 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for initially spawning in race) - bailing out"));
800 else if(!defrag_ents)
803 self.race_checkpoint = race_NextCheckpoint(0);
804 g_race_qualifying = 1;
805 self.race_place = race_lowest_place_spawn;
806 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
807 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
811 self.race_checkpoint = race_NextCheckpoint(0);
812 g_race_qualifying = 1;
813 self.race_place = 0; // there's only one spawn on defrag maps
815 // check if a defragcp file already exists, then read it and apply the checkpoint order
820 defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_READ);
823 while((l = fgets(fh)))
825 len = tokenize_console(l);
827 defragcpexists = -1; // something's wrong in the defrag cp file, set defragcpexists to -1 so that it will be rewritten when someone finishes
830 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
831 if(argv(0) == cp.targetname)
832 cp.race_checkpoint = stof(argv(1));
838 g_race_qualifying = qual;
840 if(race_timed_checkpoint) {
842 for(cp = world; (cp = find(cp, classname, "target_startTimer"));)
843 WaypointSprite_UpdateSprites(cp.sprite, "race-start", "", "");
844 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
845 WaypointSprite_UpdateSprites(cp.sprite, "race-finish", "", "");
847 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
848 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
852 if(defragcpexists != -1){
854 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
855 if(cp.race_checkpoint > largest_cp_id)
856 largest_cp_id = cp.race_checkpoint;
857 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
858 cp.race_checkpoint = largest_cp_id + 1; // finish line
859 race_highest_checkpoint = largest_cp_id + 1;
860 race_timed_checkpoint = largest_cp_id + 1;
862 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
863 cp.race_checkpoint = 255; // finish line
864 race_highest_checkpoint = 255;
865 race_timed_checkpoint = 255;
869 for(cp = world; (cp = find(cp, classname, "trigger_race_checkpoint")); )
872 if(cp.race_checkpoint == 0)
873 WaypointSprite_UpdateSprites(cp.sprite, "race-start", "", "");
874 else if(cp.race_checkpoint == race_timed_checkpoint)
875 WaypointSprite_UpdateSprites(cp.sprite, "race-finish", "", "");
881 entity trigger, targ;
882 for(trigger = world; (trigger = find(trigger, classname, "trigger_multiple")); )
883 for(targ = world; (targ = find(targ, targetname, trigger.target)); )
884 if (targ.classname == "target_checkpoint" || targ.classname == "target_startTimer" || targ.classname == "target_stopTimer") {
888 setsize(targ, trigger.mins, trigger.maxs);
889 setorigin(targ, trigger.origin);
897 void spawnfunc_trigger_race_checkpoint()
900 if(!g_race && !g_cts)
908 self.use = checkpoint_use;
909 if not(self.spawnflags & 1)
910 self.touch = checkpoint_touch;
912 o = (self.absmin + self.absmax) * 0.5;
913 tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o_z - self.absmin_z), MOVE_NORMAL, self);
914 waypoint_spawnforitem_force(self, trace_endpos);
915 self.nearestwaypointtimeout = time + 1000000000;
918 self.message = "went backwards";
920 self.message2 = "was pushed backwards by";
921 if (!self.race_penalty_reason)
922 self.race_penalty_reason = "missing a checkpoint";
924 self.race_checkpoint = self.cnt;
926 if(self.race_checkpoint > race_highest_checkpoint)
928 race_highest_checkpoint = self.race_checkpoint;
929 if(self.spawnflags & 8)
930 race_timed_checkpoint = self.race_checkpoint;
932 race_timed_checkpoint = 0;
935 if(!self.race_penalty)
937 if(self.race_checkpoint)
938 WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
940 WaypointSprite_SpawnFixed("race-finish", o, self, sprite);
943 self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
945 InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
948 void spawnfunc_target_checkpoint() // defrag entity
951 if(!g_race && !g_cts)
960 self.use = checkpoint_use;
961 if not(self.spawnflags & 1)
962 self.touch = checkpoint_touch;
964 o = (self.absmin + self.absmax) * 0.5;
965 tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o_z - self.absmin_z), MOVE_NORMAL, self);
966 waypoint_spawnforitem_force(self, trace_endpos);
967 self.nearestwaypointtimeout = time + 1000000000;
970 self.message = "went backwards";
972 self.message2 = "was pushed backwards by";
973 if (!self.race_penalty_reason)
974 self.race_penalty_reason = "missing a checkpoint";
976 if(self.classname == "target_startTimer")
977 self.race_checkpoint = 0;
979 self.race_checkpoint = -2;
981 race_timed_checkpoint = 1;
983 if(self.race_checkpoint == 0)
984 WaypointSprite_SpawnFixed("race-start", o, self, sprite);
986 WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
988 self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
990 InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
993 void spawnfunc_target_startTimer() { spawnfunc_target_checkpoint(); }
994 void spawnfunc_target_stopTimer() { spawnfunc_target_checkpoint(); }
996 void race_AbandonRaceCheck(entity p)
998 if(race_completing && !p.race_completed)
1000 p.race_completed = 1;
1001 MAKE_INDEPENDENT_PLAYER(p);
1002 bprint(p.netname, "^7 has abandoned the race.\n");
1003 ClientData_Touch(p);
1007 void race_StartCompleting()
1010 race_completing = 1;
1012 if(p.deadflag != DEAD_NO)
1013 race_AbandonRaceCheck(p);
1016 void race_PreparePlayer()
1018 race_ClearTime(self);
1019 self.race_place = 0;
1020 self.race_started = 0;
1021 self.race_respawn_checkpoint = 0;
1022 self.race_respawn_spotref = world;
1025 void race_RetractPlayer()
1027 if(!g_race && !g_cts)
1029 if(self.race_respawn_checkpoint == 0 || self.race_respawn_checkpoint == race_timed_checkpoint)
1030 race_ClearTime(self);
1031 self.race_checkpoint = self.race_respawn_checkpoint;
1036 if(!g_race && !g_cts)
1039 race_AbandonRaceCheck(self);
1042 void race_PreSpawn()
1044 if(!g_race && !g_cts)
1046 if(self.killcount == -666 /* initial spawn */ || g_race_qualifying) // spawn
1047 race_PreparePlayer();
1049 race_RetractPlayer();
1051 race_AbandonRaceCheck(self);
1054 void race_PostSpawn(entity spot)
1056 if(!g_race && !g_cts)
1059 if(spot.target == "")
1060 // Emergency: this wasn't a real spawnpoint. Can this ever happen?
1061 race_PreparePlayer();
1063 // if we need to respawn, do it right
1064 self.race_respawn_checkpoint = self.race_checkpoint;
1065 self.race_respawn_spotref = spot;
1067 self.race_place = 0;
1070 void race_PreSpawnObserver()
1072 if(!g_race && !g_cts)
1074 race_PreparePlayer();
1075 self.race_checkpoint = -1;
1078 void spawnfunc_info_player_race (void)
1080 if(!g_race && !g_cts)
1086 spawnfunc_info_player_deathmatch();
1088 if(self.race_place > race_highest_place_spawn)
1089 race_highest_place_spawn = self.race_place;
1090 if(self.race_place < race_lowest_place_spawn)
1091 race_lowest_place_spawn = self.race_place;
1094 void race_ClearRecords()
1099 for(i = 0; i < MAX_CHECKPOINTS; ++i)
1101 race_checkpoint_records[i] = 0;
1102 if(race_checkpoint_recordholders[i])
1103 strunzone(race_checkpoint_recordholders[i]);
1104 race_checkpoint_recordholders[i] = string_null;
1108 FOR_EACH_CLIENT(self)
1111 p = self.race_place;
1112 race_PreparePlayer();
1113 self.race_place = p;
1118 void race_ReadyRestart()
1122 Score_NicePrint(world);
1124 race_ClearRecords();
1125 PlayerScore_Sort(race_place);
1132 s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
1136 print(e.netname, " = ", ftos(e.race_place), "\n");
1139 if(g_race_qualifying == 2)
1141 g_race_qualifying = 0;
1142 independent_players = 0;
1143 cvar_set("fraglimit", ftos(race_fraglimit));
1144 cvar_set("leadlimit", ftos(race_leadlimit));
1145 cvar_set("timelimit", ftos(race_timelimit));
1150 void race_ImposePenaltyTime(entity pl, float penalty, string reason)
1152 if(g_race_qualifying)
1154 pl.race_penalty_accumulator += penalty;
1156 WRITESPECTATABLE_MSG_ONE({
1157 WriteByte(MSG_ONE, SVC_TEMPENTITY);
1158 WriteByte(MSG_ONE, TE_CSQC_RACE);
1159 WriteByte(MSG_ONE, RACE_NET_PENALTY_QUALIFYING);
1160 WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1161 WriteString(MSG_ONE, reason);
1166 pl.race_penalty = time + penalty;
1168 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy, {
1169 WriteByte(MSG_ONE, SVC_TEMPENTITY);
1170 WriteByte(MSG_ONE, TE_CSQC_RACE);
1171 WriteByte(MSG_ONE, RACE_NET_PENALTY_RACE);
1172 WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1173 WriteString(MSG_ONE, reason);
1178 void penalty_touch()
1181 if(other.race_lastpenalty != self)
1183 other.race_lastpenalty = self;
1184 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
1190 race_ImposePenaltyTime(activator, self.race_penalty, self.race_penalty_reason);
1193 void spawnfunc_trigger_race_penalty()
1197 self.use = penalty_use;
1198 if not(self.spawnflags & 1)
1199 self.touch = penalty_touch;
1201 if (!self.race_penalty_reason)
1202 self.race_penalty_reason = "missing a checkpoint";
1203 if (!self.race_penalty)
1204 self.race_penalty = 5;
1207 float race_GetFractionalLapCount(entity e)
1209 // interesting metrics (idea by KrimZon) to maybe sort players in the
1210 // scoreboard, immediately updates when overtaking
1212 // requires the track to be built so you never get farther away from the
1213 // next checkpoint, though, and current Xonotic race maps are not built that
1216 // also, this code is slow and would need optimization (i.e. "next CP"
1217 // links on CP entities)
1220 l = PlayerScore_Add(e, SP_RACE_LAPS, 0);
1221 if(e.race_completed)
1222 return l; // not fractional
1225 float bestfraction, fraction;
1226 entity lastcp, cp0, cp1;
1227 float nextcpindex, lastcpindex;
1229 nextcpindex = max(e.race_checkpoint, 0);
1230 lastcpindex = e.race_respawn_checkpoint;
1231 lastcp = e.race_respawn_spotref;
1233 if(nextcpindex == lastcpindex)
1237 for(cp0 = world; (cp0 = find(cp0, classname, "trigger_race_checkpoint")); )
1239 if(cp0.race_checkpoint != lastcpindex)
1244 o0 = (cp0.absmin + cp0.absmax) * 0.5;
1245 for(cp1 = world; (cp1 = find(cp1, classname, "trigger_race_checkpoint")); )
1247 if(cp1.race_checkpoint != nextcpindex)
1249 o1 = (cp1.absmin + cp1.absmax) * 0.5;
1252 fraction = bound(0.0001, vlen(e.origin - o1) / vlen(o0 - o1), 1);
1253 if(fraction < bestfraction)
1254 bestfraction = fraction;
1258 // we are at CP "nextcpindex - bestfraction"
1259 // race_timed_checkpoint == 4: then nextcp==4 means 0.9999x, nextcp==0 means 0.0000x
1260 // race_timed_checkpoint == 0: then nextcp==0 means 0.9999x
1262 nc = race_highest_checkpoint + 1;
1263 c = (mod(nextcpindex - race_timed_checkpoint + nc + nc - 1, nc) + 1) - bestfraction;