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];
105 string grecorduid[RANKINGS_CNT];
107 float worst_time; // last ranked time
108 float have_recs; // have we already read the records from the database before?
109 float race_GetTime(float pos) {
115 if (!have_recs) { // I guess this is better than checking if the first array item is empty, rumor has it that arrays are slow
117 for(i=0;i<RANKINGS_CNT;++i) {
118 grecordtime[i] = stof(db_get(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i))));
119 grecordholder[i] = strzone(db_get(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i))));
121 grecorduid[i] = strzone(db_get(ServerProgsDB, strcat(GetMapname(), rr, "uid", ftos(i))));
124 grecordtime[0] = stof(db_get(ServerProgsDB, strcat(GetMapname(), rr, "time")));
125 grecordholder[0] = strzone(db_get(ServerProgsDB, strcat(GetMapname(), rr, "netname")));
127 grecorduid[0] = strzone(db_get(ServerProgsDB, strcat(GetMapname(), rr, "uid")));
129 worst_time = stof(db_get(ServerProgsDB, strcat(GetMapname(), rr, strcat("time", ftos(RANKINGS_CNT-1)))));
133 return grecordtime[pos-1];
136 string race_GetName(float pos) { // these other functions assume that race_GetTime has been called >= once before
137 return grecordholder[pos-1];
141 float race_CheckUID(string myuid, string net_name) { // return existing UID or player name ranking pos, else 0
143 for (i=RANKINGS_CNT-1;i>=0;--i)
144 if(grecorduid[i] == myuid)
146 for (i=RANKINGS_CNT-1;i>=0;--i)
147 if(grecordholder[i] == net_name)
153 float race_CheckName(string net_name) { // Does the name already exist in rankings? In that case, where? (otherwise 0)
155 for (i=RANKINGS_CNT-1;i>=0;--i)
156 if(grecordholder[i] == net_name)
161 float race_GetPos(float t) {
165 for (i=0;i<RANKINGS_CNT;++i)
166 if (grecordtime[i] == 0 || grecordtime[i] > t)
169 for (i=0;i<RANKINGS_CNT;++i)
170 if (grecordtime[i] > t)
175 void race_send_recordtime(float msg)
177 // send the server best time
178 WriteByte(msg, SVC_TEMPENTITY);
179 WriteByte(msg, TE_CSQC_RACE);
180 WriteByte(msg, RACE_NET_SERVER_RECORD);
181 WriteInt24_t(msg, race_GetTime(1));
184 void race_SendRankings(float pos, float prevpos, float del, float msg)
186 WriteByte(msg, SVC_TEMPENTITY);
187 WriteByte(msg, TE_CSQC_RACE);
188 WriteByte(msg, RACE_NET_SERVER_RANKINGS);
189 WriteShort(msg, pos);
190 WriteShort(msg, prevpos);
191 WriteShort(msg, del);
192 WriteString(msg, race_GetName(pos));
193 WriteInt24_t(msg, race_GetTime(pos));
196 void race_SendStatus(float id, entity e)
204 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy3, {
205 WriteByte(msg, SVC_TEMPENTITY);
206 WriteByte(msg, TE_CSQC_RACE);
207 WriteByte(msg, RACE_NET_SERVER_STATUS);
209 WriteString(msg, e.netname);
213 string race_PlaceName(float pos) {
221 return strcat(ftos(pos), "th");
224 void race_SetTime(entity e, float t, float match_rec) {
226 pos = race_GetPos(t);
228 prevpos = race_CheckUID(e.uid, e.netname);
230 prevpos = race_CheckName(e.netname);
234 string recorddifference;
235 if (prevpos && (prevpos < pos || !pos))
237 oldrec = race_GetTime(prevpos);
238 recorddifference = strcat(" ^1[+", TIME_ENCODED_TOSTRING(t - oldrec), "]");
239 bprint(e.netname, "^7 couldn't break their ", race_PlaceName(prevpos), " place record of ", TIME_ENCODED_TOSTRING(oldrec), recorddifference, "\n");
240 race_SendStatus(0, e); // "fail"
242 } else if (!pos) { // no ranking, time worse than the worst ranked
243 recorddifference = strcat(" ^1[+", TIME_ENCODED_TOSTRING(t - grecordtime[RANKINGS_CNT-1]), "]");
244 bprint(e.netname, "^7 couldn't break the ", race_PlaceName(RANKINGS_CNT), " place record of ", TIME_ENCODED_TOSTRING(grecordtime[RANKINGS_CNT-1]), recorddifference, "\n");
245 race_SendStatus(0, e); // "fail"
249 // move other rankings out of the way
251 if (prevpos) { // player improved his existing record
253 oldrec = grecordtime[pos-1];
255 oldrec = grecordtime[pos-1];
256 for (i=prevpos-1;i>pos-1;--i) {
257 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), ftos(grecordtime[i-1]));
258 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i)), grecordholder[i-1]);
260 db_put(ServerProgsDB, strcat(GetMapname(), rr, "uid", ftos(i)), grecorduid[i-1]);
262 grecordtime[i] = grecordtime[i-1];
264 if (grecordholder[i])
265 strunzone(grecordholder[i]);
266 grecordholder[i] = strzone(grecordholder[i-1]);
269 strunzone(grecorduid[i]);
270 grecorduid[i] = strzone(grecorduid[i-1]);
273 } else { // player has no ranked record yet
274 oldrec = grecordtime[pos-1];
275 for (i=RANKINGS_CNT-1;i>pos-1;--i) {
276 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), ftos(grecordtime[i-1]));
277 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i)), grecordholder[i-1]);
279 db_put(ServerProgsDB, strcat(GetMapname(), rr, "uid", ftos(i)), grecorduid[i-1]);
281 grecordtime[i] = grecordtime[i-1];
283 if (grecordholder[i])
284 strunzone(grecordholder[i]);
285 grecordholder[i] = strzone(grecordholder[i-1]);
288 strunzone(grecorduid[i]);
289 grecorduid[i] = strzone(grecorduid[i-1]);
296 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time"), ftos(t));
297 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname"), e.netname);
299 db_put(ServerProgsDB, strcat(GetMapname(), rr, "uid"), e.uid);
304 if (grecordholder[0])
305 strunzone(grecordholder[0]);
306 grecordholder[0] = strzone(e.netname);
309 strunzone(grecorduid[0]);
310 grecorduid[0] = strzone(e.uid);
312 write_recordmarker(e, time - TIME_DECODE(t), TIME_DECODE(t));
313 race_send_recordtime(MSG_ALL);
315 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(pos-1)), ftos(t));
316 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(pos-1)), e.netname);
318 db_put(ServerProgsDB, strcat(GetMapname(), rr, "uid", ftos(pos-1)), e.uid);
321 grecordtime[pos-1] = t;
323 if (grecordholder[pos-1])
324 strunzone(grecordholder[pos-1]);
325 grecordholder[pos-1] = strzone(e.netname);
327 if (grecorduid[pos-1])
328 strunzone(grecorduid[pos-1]);
329 grecorduid[pos-1] = strzone(e.uid);
333 if (pos == RANKINGS_CNT)
336 race_SendRankings(pos, prevpos, 0, MSG_ALL);
338 strunzone(rankings_reply);
339 rankings_reply = strzone(getrankings());
342 recorddifference = strcat(" ^2[-", TIME_ENCODED_TOSTRING(oldrec - t), "]");
343 bprint(e.netname, "^1 improved their 1st place record with ", TIME_ENCODED_TOSTRING(t), recorddifference, "\n");
344 race_SendStatus(3, e); // "new server record"
346 recorddifference = strcat(" ^2[-", TIME_ENCODED_TOSTRING(oldrec - t), "]");
347 bprint(e.netname, "^1 broke ", grecordholder[pos], "^1's 1st place record with ", strcat(TIME_ENCODED_TOSTRING(t), recorddifference, "\n"));
348 race_SendStatus(3, e); // "new server record"
352 recorddifference = strcat(" ^2[-", TIME_ENCODED_TOSTRING(oldrec - t), "]");
353 bprint(e.netname, "^5 improved their ", race_PlaceName(pos), " ^5place record with ", TIME_ENCODED_TOSTRING(t), recorddifference, "\n");
354 race_SendStatus(1, e); // "new time"
355 } else if (oldrec == 0) {
356 bprint(e.netname, "^2 set the ", race_PlaceName(pos), " ^2place record with ", TIME_ENCODED_TOSTRING(t), "\n");
357 race_SendStatus(2, e); // "new rank"
359 recorddifference = strcat(" ^2[-", TIME_ENCODED_TOSTRING(oldrec - t), "]");
360 bprint(e.netname, "^2 broke ", grecordholder[pos], "^2's ", race_PlaceName(pos), " ^2place record with ", strcat(TIME_ENCODED_TOSTRING(t), recorddifference, "\n"));
361 race_SendStatus(2, e); // "new rank"
366 void race_DeleteTime(float pos) {
369 for (i = pos-1; i <= RANKINGS_CNT-1; ++i) {
371 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time"), ftos(grecordtime[1]));
372 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname"), grecordholder[1]);
374 db_put(ServerProgsDB, strcat(GetMapname(), rr, "uid"), grecorduid[1]);
376 grecordtime[0] = grecordtime[1];
377 if (grecordholder[i])
378 strunzone(grecordholder[0]);
379 grecordholder[0] = strzone(grecordholder[1]);
383 strunzone(grecorduid[0]);
384 grecorduid[0] = strzone(grecorduid[1]);
387 else if (i == RANKINGS_CNT-1) {
388 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), string_null);
389 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i)), string_null);
391 db_put(ServerProgsDB, strcat(GetMapname(), rr, "uid", ftos(i)), string_null);
394 if (grecordholder[i])
395 strunzone(grecordholder[i]);
396 grecordholder[i] = string_null;
400 strunzone(grecorduid[i]);
401 grecorduid[i] = string_null;
405 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), ftos(grecordtime[i+1]));
406 db_put(ServerProgsDB, strcat(GetMapname(), rr, "netname", ftos(i)), grecordholder[i+1]);
408 db_put(ServerProgsDB, strcat(GetMapname(), rr, "uid", ftos(i)), grecorduid[i+1]);
410 grecordtime[i] = grecordtime[i+1];
411 if (grecordholder[i])
412 strunzone(grecordholder[i]);
413 grecordholder[i] = strzone(grecordholder[i+1]);
417 strunzone(grecorduid[i]);
418 grecorduid[i] = strzone(grecorduid[i+1]);
423 race_SendRankings(pos, 0, 1, MSG_ALL);
425 race_send_recordtime(MSG_ALL);
428 strunzone(rankings_reply);
429 rankings_reply = strzone(getrankings());
434 void race_SendTime(entity e, float cp, float t, float tvalid)
439 if(g_race_qualifying)
440 t += e.race_penalty_accumulator;
442 t = TIME_ENCODE(t); // make integer
443 // adding just 0.4 so it rounds down in the .5 case (matching the timer display)
446 if(cp == race_timed_checkpoint) // finish line
447 if not(e.race_completed)
450 if(g_race_qualifying)
452 s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
454 PlayerScore_Add(e, SP_RACE_FASTEST, t - s);
458 s = PlayerScore_Add(e, SP_RACE_TIME, 0);
459 snew = TIME_ENCODE(time - game_starttime);
460 PlayerScore_Add(e, SP_RACE_TIME, snew - s);
461 l = PlayerTeamScore_Add(e, SP_RACE_LAPS, ST_RACE_LAPS, 1);
463 if(cvar("fraglimit"))
464 if(l >= cvar("fraglimit"))
465 race_StartCompleting();
469 e.race_completed = 1;
470 MAKE_INDEPENDENT_PLAYER(e);
471 bprint(e.netname, "^7 has finished the race.\n");
479 if(g_race_qualifying)
483 recordtime = race_checkpoint_records[cp];
484 recordholder = strcat1(race_checkpoint_recordholders[cp]); // make a tempstring copy, as we'll possibly strunzone it!
485 if(recordholder == e.netname)
489 if(cp == race_timed_checkpoint)
490 race_SetTime(e, t, recordtime);
492 if(t < recordtime || recordtime == 0)
494 race_checkpoint_records[cp] = t;
495 if(race_checkpoint_recordholders[cp])
496 strunzone(race_checkpoint_recordholders[cp]);
497 race_checkpoint_recordholders[cp] = strzone(e.netname);
498 if(g_race_qualifying)
500 FOR_EACH_REALPLAYER(p)
501 if(p.race_checkpoint == cp)
502 race_SendNextCheckpoint(p, 0);
516 if(g_race_qualifying)
518 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy1, {
519 WriteByte(MSG_ONE, SVC_TEMPENTITY);
520 WriteByte(MSG_ONE, TE_CSQC_RACE);
521 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_QUALIFYING);
522 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
523 WriteInt24_t(MSG_ONE, t); // time to that intermediate
524 WriteInt24_t(MSG_ONE, recordtime); // previously best time
525 WriteString(MSG_ONE, recordholder); // record holder
529 else // RACE! Not Qualifying
531 float lself, lother, othtime;
533 oth = race_checkpoint_lastplayers[cp];
536 lself = PlayerScore_Add(e, SP_RACE_LAPS, 0);
537 lother = race_checkpoint_lastlaps[cp];
538 othtime = race_checkpoint_lasttimes[cp];
541 lself = lother = othtime = 0;
544 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy2, {
545 WriteByte(MSG_ONE, SVC_TEMPENTITY);
546 WriteByte(MSG_ONE, TE_CSQC_RACE);
547 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE);
548 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
551 WriteInt24_t(MSG_ONE, 0);
552 WriteByte(MSG_ONE, 0);
553 WriteString(MSG_ONE, "");
557 WriteInt24_t(MSG_ONE, TIME_ENCODE(time - race_checkpoint_lasttimes[cp]));
558 WriteByte(MSG_ONE, lself - lother);
559 WriteString(MSG_ONE, oth.netname); // record holder
563 race_checkpoint_lastplayers[cp] = e;
564 race_checkpoint_lasttimes[cp] = time;
565 race_checkpoint_lastlaps[cp] = lself;
568 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy3, {
569 WriteByte(MSG_ONE, SVC_TEMPENTITY);
570 WriteByte(MSG_ONE, TE_CSQC_RACE);
571 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE_BY_OPPONENT);
572 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
575 WriteInt24_t(MSG_ONE, 0);
576 WriteByte(MSG_ONE, 0);
577 WriteString(MSG_ONE, "");
581 WriteInt24_t(MSG_ONE, TIME_ENCODE(time - othtime));
582 WriteByte(MSG_ONE, lother - lself);
583 WriteString(MSG_ONE, e.netname); // record holder
589 void race_ClearTime(entity e)
591 e.race_checkpoint = 0;
593 e.race_movetime = e.race_movetime_frac = e.race_movetime_count = 0;
594 e.race_penalty_accumulator = 0;
595 e.race_lastpenalty = world;
598 WRITESPECTATABLE_MSG_ONE({
599 WriteByte(MSG_ONE, SVC_TEMPENTITY);
600 WriteByte(MSG_ONE, TE_CSQC_RACE);
601 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_CLEAR); // next
605 void dumpsurface(entity e)
609 print("Surfaces of ", etos(e), ":\n");
611 print("TEST = ", ftos(getsurfacenearpoint(e, '0 0 0')), "\n");
615 n = getsurfacenumpoints(e, si);
618 print(" Surface ", ftos(si), ":\n");
619 norm = getsurfacenormal(e, si);
620 print(" Normal = ", vtos(norm), "\n");
621 for(ni = 0; ni < n; ++ni)
623 vec = getsurfacepoint(e, si, ni);
624 print(" Point ", ftos(ni), " = ", vtos(vec), " (", ftos(norm * vec), ")\n");
629 void checkpoint_passed()
634 if(other.classname == "porto")
636 // do not allow portalling through checkpoints
637 trace_plane_normal = normalize(-1 * other.velocity);
646 if not((self.spawnflags & 2) && (other.classname == "player"))
649 oldmsg = self.message;
652 self.message = oldmsg;
655 if(other.classname != "player")
659 * Remove unauthorized equipment
661 Portal_ClearAll(other);
663 other.porto_forbidden = 2; // decreased by 1 each StartFrame
666 if(self.race_checkpoint == -2)
668 self.race_checkpoint = other.race_checkpoint;
673 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
675 if(cp.race_checkpoint > largest_cp_id) // update the finish id if someone hit a new checkpoint
677 largest_cp_id = cp.race_checkpoint;
678 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
679 cp.race_checkpoint = largest_cp_id + 1; // finish line
680 race_highest_checkpoint = largest_cp_id + 1;
681 race_timed_checkpoint = largest_cp_id + 1;
683 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
684 if(cp.race_checkpoint == -2) // set defragcpexists to -1 so that the cp id file will be rewritten when someone finishes
690 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
691 cp.race_checkpoint = 1;
692 race_highest_checkpoint = 1;
693 race_timed_checkpoint = 1;
697 if((other.race_checkpoint == -1 && self.race_checkpoint == 0) || (other.race_checkpoint == self.race_checkpoint))
699 if(self.race_penalty)
701 if(other.race_lastpenalty != self)
703 other.race_lastpenalty = self;
704 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
708 if(other.race_penalty)
714 if(self.spawnflags & 2)
717 oldmsg = self.message;
720 self.message = oldmsg;
723 if(other.race_respawn_checkpoint != self.race_checkpoint || !other.race_started)
724 other.race_respawn_spotref = self; // this is not a spot but a CP, but spawnpoint selection will deal with that
725 other.race_respawn_checkpoint = self.race_checkpoint;
726 other.race_checkpoint = race_NextCheckpoint(self.race_checkpoint);
727 other.race_started = 1;
729 race_SendTime(other, self.race_checkpoint, other.race_movetime, !!other.race_laptime);
731 if(!self.race_checkpoint) // start line
733 other.race_laptime = time;
734 other.race_movetime = other.race_movetime_frac = other.race_movetime_count = 0;
735 other.race_penalty_accumulator = 0;
736 other.race_lastpenalty = world;
739 if(g_race_qualifying)
740 race_SendNextCheckpoint(other, 0);
742 if(defrag_ents && defragcpexists < 0 && self.classname == "target_stopTimer")
745 defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_WRITE);
748 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
749 fputs(fh, strcat(cp.targetname, " ", ftos(cp.race_checkpoint), "\n"));
754 else if(other.race_checkpoint == race_NextCheckpoint(self.race_checkpoint))
760 if(self.spawnflags & 4)
761 Damage (other, self, self, 10000, DEATH_HURTTRIGGER, other.origin, '0 0 0');
765 void checkpoint_touch()
771 void checkpoint_use()
773 if(other.classname == "info_player_deathmatch") // a spawn, a spawn
780 float race_waypointsprite_visible_for_player(entity e)
782 if(e.race_checkpoint == -1 || self.owner.race_checkpoint == -2)
784 else if(e.race_checkpoint == self.owner.race_checkpoint)
791 void trigger_race_checkpoint_verify()
801 qual = g_race_qualifying;
805 self.classname = "player";
809 for(i = 0; i <= race_highest_checkpoint; ++i)
811 self.race_checkpoint = race_NextCheckpoint(i);
813 // race only (middle of the race)
814 g_race_qualifying = 0;
816 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
817 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for respawning in race) - bailing out"));
822 g_race_qualifying = 1;
823 self.race_place = race_lowest_place_spawn;
824 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
825 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
827 // race only (initial spawn)
828 g_race_qualifying = 0;
829 for(p = 1; p <= race_highest_place_spawn; ++p)
832 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
833 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for initially spawning in race) - bailing out"));
838 else if(!defrag_ents)
841 self.race_checkpoint = race_NextCheckpoint(0);
842 g_race_qualifying = 1;
843 self.race_place = race_lowest_place_spawn;
844 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
845 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
849 self.race_checkpoint = race_NextCheckpoint(0);
850 g_race_qualifying = 1;
851 self.race_place = 0; // there's only one spawn on defrag maps
853 // check if a defragcp file already exists, then read it and apply the checkpoint order
858 defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_READ);
861 while((l = fgets(fh)))
863 len = tokenize_console(l);
865 defragcpexists = -1; // something's wrong in the defrag cp file, set defragcpexists to -1 so that it will be rewritten when someone finishes
868 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
869 if(argv(0) == cp.targetname)
870 cp.race_checkpoint = stof(argv(1));
876 g_race_qualifying = qual;
878 if(race_timed_checkpoint) {
880 for(cp = world; (cp = find(cp, classname, "target_startTimer"));)
881 WaypointSprite_UpdateSprites(cp.sprite, "race-start", "", "");
882 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
883 WaypointSprite_UpdateSprites(cp.sprite, "race-finish", "", "");
885 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
886 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
890 if(defragcpexists != -1){
892 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
893 if(cp.race_checkpoint > largest_cp_id)
894 largest_cp_id = cp.race_checkpoint;
895 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
896 cp.race_checkpoint = largest_cp_id + 1; // finish line
897 race_highest_checkpoint = largest_cp_id + 1;
898 race_timed_checkpoint = largest_cp_id + 1;
900 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
901 cp.race_checkpoint = 255; // finish line
902 race_highest_checkpoint = 255;
903 race_timed_checkpoint = 255;
907 for(cp = world; (cp = find(cp, classname, "trigger_race_checkpoint")); )
910 if(cp.race_checkpoint == 0)
911 WaypointSprite_UpdateSprites(cp.sprite, "race-start", "", "");
912 else if(cp.race_checkpoint == race_timed_checkpoint)
913 WaypointSprite_UpdateSprites(cp.sprite, "race-finish", "", "");
919 entity trigger, targ;
920 for(trigger = world; (trigger = find(trigger, classname, "trigger_multiple")); )
921 for(targ = world; (targ = find(targ, targetname, trigger.target)); )
922 if (targ.classname == "target_checkpoint" || targ.classname == "target_startTimer" || targ.classname == "target_stopTimer") {
926 setsize(targ, trigger.mins, trigger.maxs);
927 setorigin(targ, trigger.origin);
935 void spawnfunc_trigger_race_checkpoint()
938 if(!g_race && !g_cts)
946 self.use = checkpoint_use;
947 if not(self.spawnflags & 1)
948 self.touch = checkpoint_touch;
950 o = (self.absmin + self.absmax) * 0.5;
951 tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o_z - self.absmin_z), MOVE_NORMAL, self);
952 waypoint_spawnforitem_force(self, trace_endpos);
953 self.nearestwaypointtimeout = time + 1000000000;
956 self.message = "went backwards";
958 self.message2 = "was pushed backwards by";
959 if (!self.race_penalty_reason)
960 self.race_penalty_reason = "missing a checkpoint";
962 self.race_checkpoint = self.cnt;
964 if(self.race_checkpoint > race_highest_checkpoint)
966 race_highest_checkpoint = self.race_checkpoint;
967 if(self.spawnflags & 8)
968 race_timed_checkpoint = self.race_checkpoint;
970 race_timed_checkpoint = 0;
973 if(!self.race_penalty)
975 if(self.race_checkpoint)
976 WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
978 WaypointSprite_SpawnFixed("race-finish", o, self, sprite);
981 self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
983 InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
986 void spawnfunc_target_checkpoint() // defrag entity
989 if(!g_race && !g_cts)
998 self.use = checkpoint_use;
999 if not(self.spawnflags & 1)
1000 self.touch = checkpoint_touch;
1002 o = (self.absmin + self.absmax) * 0.5;
1003 tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o_z - self.absmin_z), MOVE_NORMAL, self);
1004 waypoint_spawnforitem_force(self, trace_endpos);
1005 self.nearestwaypointtimeout = time + 1000000000;
1008 self.message = "went backwards";
1010 self.message2 = "was pushed backwards by";
1011 if (!self.race_penalty_reason)
1012 self.race_penalty_reason = "missing a checkpoint";
1014 if(self.classname == "target_startTimer")
1015 self.race_checkpoint = 0;
1017 self.race_checkpoint = -2;
1019 race_timed_checkpoint = 1;
1021 if(self.race_checkpoint == 0)
1022 WaypointSprite_SpawnFixed("race-start", o, self, sprite);
1024 WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
1026 self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
1028 InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
1031 void spawnfunc_target_startTimer() { spawnfunc_target_checkpoint(); }
1032 void spawnfunc_target_stopTimer() { spawnfunc_target_checkpoint(); }
1034 void race_AbandonRaceCheck(entity p)
1036 if(race_completing && !p.race_completed)
1038 p.race_completed = 1;
1039 MAKE_INDEPENDENT_PLAYER(p);
1040 bprint(p.netname, "^7 has abandoned the race.\n");
1041 ClientData_Touch(p);
1045 void race_StartCompleting()
1048 race_completing = 1;
1050 if(p.deadflag != DEAD_NO)
1051 race_AbandonRaceCheck(p);
1054 void race_PreparePlayer()
1056 race_ClearTime(self);
1057 self.race_place = 0;
1058 self.race_started = 0;
1059 self.race_respawn_checkpoint = 0;
1060 self.race_respawn_spotref = world;
1063 void race_RetractPlayer()
1065 if(!g_race && !g_cts)
1067 if(self.race_respawn_checkpoint == 0 || self.race_respawn_checkpoint == race_timed_checkpoint)
1068 race_ClearTime(self);
1069 self.race_checkpoint = self.race_respawn_checkpoint;
1074 if(!g_race && !g_cts)
1077 race_AbandonRaceCheck(self);
1080 void race_PreSpawn()
1082 if(!g_race && !g_cts)
1084 if(self.killcount == -666 /* initial spawn */ || g_race_qualifying) // spawn
1085 race_PreparePlayer();
1087 race_RetractPlayer();
1089 race_AbandonRaceCheck(self);
1092 void race_PostSpawn(entity spot)
1094 if(!g_race && !g_cts)
1097 if(spot.target == "")
1098 // Emergency: this wasn't a real spawnpoint. Can this ever happen?
1099 race_PreparePlayer();
1101 // if we need to respawn, do it right
1102 self.race_respawn_checkpoint = self.race_checkpoint;
1103 self.race_respawn_spotref = spot;
1105 self.race_place = 0;
1108 void race_PreSpawnObserver()
1110 if(!g_race && !g_cts)
1112 race_PreparePlayer();
1113 self.race_checkpoint = -1;
1116 void spawnfunc_info_player_race (void)
1118 if(!g_race && !g_cts)
1124 spawnfunc_info_player_deathmatch();
1126 if(self.race_place > race_highest_place_spawn)
1127 race_highest_place_spawn = self.race_place;
1128 if(self.race_place < race_lowest_place_spawn)
1129 race_lowest_place_spawn = self.race_place;
1132 void race_ClearRecords()
1137 for(i = 0; i < MAX_CHECKPOINTS; ++i)
1139 race_checkpoint_records[i] = 0;
1140 if(race_checkpoint_recordholders[i])
1141 strunzone(race_checkpoint_recordholders[i]);
1142 race_checkpoint_recordholders[i] = string_null;
1146 FOR_EACH_CLIENT(self)
1149 p = self.race_place;
1150 race_PreparePlayer();
1151 self.race_place = p;
1156 void race_ReadyRestart()
1160 Score_NicePrint(world);
1162 race_ClearRecords();
1163 PlayerScore_Sort(race_place);
1170 s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
1174 print(e.netname, " = ", ftos(e.race_place), "\n");
1177 if(g_race_qualifying == 2)
1179 g_race_qualifying = 0;
1180 independent_players = 0;
1181 cvar_set("fraglimit", ftos(race_fraglimit));
1182 cvar_set("leadlimit", ftos(race_leadlimit));
1183 cvar_set("timelimit", ftos(race_timelimit));
1188 void race_ImposePenaltyTime(entity pl, float penalty, string reason)
1190 if(g_race_qualifying)
1192 pl.race_penalty_accumulator += penalty;
1194 WRITESPECTATABLE_MSG_ONE({
1195 WriteByte(MSG_ONE, SVC_TEMPENTITY);
1196 WriteByte(MSG_ONE, TE_CSQC_RACE);
1197 WriteByte(MSG_ONE, RACE_NET_PENALTY_QUALIFYING);
1198 WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1199 WriteString(MSG_ONE, reason);
1204 pl.race_penalty = time + penalty;
1206 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy, {
1207 WriteByte(MSG_ONE, SVC_TEMPENTITY);
1208 WriteByte(MSG_ONE, TE_CSQC_RACE);
1209 WriteByte(MSG_ONE, RACE_NET_PENALTY_RACE);
1210 WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1211 WriteString(MSG_ONE, reason);
1216 void penalty_touch()
1219 if(other.race_lastpenalty != self)
1221 other.race_lastpenalty = self;
1222 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
1228 race_ImposePenaltyTime(activator, self.race_penalty, self.race_penalty_reason);
1231 void spawnfunc_trigger_race_penalty()
1235 self.use = penalty_use;
1236 if not(self.spawnflags & 1)
1237 self.touch = penalty_touch;
1239 if (!self.race_penalty_reason)
1240 self.race_penalty_reason = "missing a checkpoint";
1241 if (!self.race_penalty)
1242 self.race_penalty = 5;
1245 float race_GetFractionalLapCount(entity e)
1247 // interesting metrics (idea by KrimZon) to maybe sort players in the
1248 // scoreboard, immediately updates when overtaking
1250 // requires the track to be built so you never get farther away from the
1251 // next checkpoint, though, and current Nexuiz race maps are not built that
1254 // also, this code is slow and would need optimization (i.e. "next CP"
1255 // links on CP entities)
1258 l = PlayerScore_Add(e, SP_RACE_LAPS, 0);
1259 if(e.race_completed)
1260 return l; // not fractional
1263 float bestfraction, fraction;
1264 entity lastcp, cp0, cp1;
1265 float nextcpindex, lastcpindex;
1267 nextcpindex = max(e.race_checkpoint, 0);
1268 lastcpindex = e.race_respawn_checkpoint;
1269 lastcp = e.race_respawn_spotref;
1271 if(nextcpindex == lastcpindex)
1275 for(cp0 = world; (cp0 = find(cp0, classname, "trigger_race_checkpoint")); )
1277 if(cp0.race_checkpoint != lastcpindex)
1282 o0 = (cp0.absmin + cp0.absmax) * 0.5;
1283 for(cp1 = world; (cp1 = find(cp1, classname, "trigger_race_checkpoint")); )
1285 if(cp1.race_checkpoint != nextcpindex)
1287 o1 = (cp1.absmin + cp1.absmax) * 0.5;
1290 fraction = bound(0.0001, vlen(e.origin - o1) / vlen(o0 - o1), 1);
1291 if(fraction < bestfraction)
1292 bestfraction = fraction;
1296 // we are at CP "nextcpindex - bestfraction"
1297 // race_timed_checkpoint == 4: then nextcp==4 means 0.9999x, nextcp==0 means 0.0000x
1298 // race_timed_checkpoint == 0: then nextcp==0 means 0.9999x
1300 nc = race_highest_checkpoint + 1;
1301 c = (mod(nextcpindex - race_timed_checkpoint + nc + nc - 1, nc) + 1) - bestfraction;