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);
101 void race_send_recordtime(float msg)
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(1));
110 void race_SendRankings(float pos, float prevpos, float del, float msg)
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(pos));
119 WriteInt24_t(msg, race_readTime(pos));
122 void race_SendStatus(float id, 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);
135 WriteString(msg, e.netname);
139 void race_setTime(string map, float t, string myuid, string netname) { // netname only used TEMPORARILY for printing
140 float newpos, prevpos;
141 newpos = race_readPos(map, t);
144 for(i = 1; i <= RANKINGS_CNT; ++i)
146 if(race_readUID(map, i) == myuid)
151 string recorddifference;
152 if (prevpos && (prevpos < pos || !pos))
154 oldrec = race_readTime(prevpos);
155 recorddifference = strcat(" ^1[+", TIME_ENCODED_TOSTRING(t - oldrec), "]");
156 bprint(e.netname, "^7 couldn't break their ", race_placeName(prevpos), " place record of ", TIME_ENCODED_TOSTRING(oldrec), recorddifference, "\n");
157 race_SendStatus(0, e); // "fail"
159 } else if (!pos) { // no ranking, time worse than the worst ranked
160 recorddifference = strcat(" ^1[+", TIME_ENCODED_TOSTRING(t - grecordtime[RANKINGS_CNT-1]), "]");
161 bprint(e.netname, "^7 couldn't break the ", race_placeName(RANKINGS_CNT), " place record of ", TIME_ENCODED_TOSTRING(grecordtime[RANKINGS_CNT-1]), recorddifference, "\n");
162 race_SendStatus(0, e); // "fail"
166 oldrec = grecordtime[pos-1];
168 // move other rankings out of the way
170 if (prevpos) { // player improved his existing record, only have to iterate on ranks between new and old recs
171 for (i=prevpos;i>pos;--i) {
172 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), ftos(grecordtime[i-1]));
173 db_put(ServerProgsDB, strcat(GetMapname(), rr, "crypto_idfp", ftos(i)), grecorduid[i-1]);
175 } else { // player has no ranked record yet
176 for (i=RANKINGS_CNT;i>pos;--i) {
177 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), ftos(grecordtime[i]));
178 db_put(ServerProgsDB, strcat(GetMapname(), rr, "crypto_idfp", ftos(i)), grecorduid[i]);
184 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time"), ftos(t));
185 db_put(ServerProgsDB, strcat(GetMapname(), rr, "crypto_idfp"), e.crypto_idfp);
189 if (grecordholder[0])
190 strunzone(grecordholder[0]);
191 grecordholder[0] = strzone(e.netname);
193 strunzone(grecorduid[0]);
194 grecorduid[0] = strzone(e.crypto_idfp);
195 write_recordmarker(e, time - TIME_DECODE(t), TIME_DECODE(t));
196 race_send_recordtime(MSG_ALL);
198 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(pos-1)), ftos(t));
199 db_put(ServerProgsDB, strcat(GetMapname(), rr, "crypto_idfp", ftos(pos-1)), e.crypto_idfp);
201 grecordtime[pos-1] = t;
203 if (grecordholder[pos-1])
204 strunzone(grecordholder[pos-1]);
205 grecordholder[pos-1] = strzone(e.netname);
206 if (grecorduid[pos-1])
207 strunzone(grecorduid[pos-1]);
208 grecorduid[pos-1] = strzone(e.crypto_idfp);
211 if (pos == RANKINGS_CNT)
214 race_SendRankings(pos, prevpos, 0, MSG_ALL);
216 strunzone(rankings_reply);
217 rankings_reply = strzone(getrankings());
220 recorddifference = strcat(" ^2[-", TIME_ENCODED_TOSTRING(oldrec - t), "]");
221 bprint(e.netname, "^1 improved their 1st place record with ", TIME_ENCODED_TOSTRING(t), recorddifference, "\n");
222 } else if (oldrec == 0) {
223 bprint(e.netname, "^1 set the 1st place record with ", TIME_ENCODED_TOSTRING(t), "\n");
225 recorddifference = strcat(" ^2[-", TIME_ENCODED_TOSTRING(oldrec - t), "]");
226 bprint(e.netname, "^1 broke ", grecordholder[pos], "^1's 1st place record with ", strcat(TIME_ENCODED_TOSTRING(t), recorddifference, "\n"));
228 race_SendStatus(3, e); // "new server record"
231 recorddifference = strcat(" ^2[-", TIME_ENCODED_TOSTRING(oldrec - t), "]");
232 bprint(e.netname, "^5 improved their ", race_placeName(pos), " ^5place record with ", TIME_ENCODED_TOSTRING(t), recorddifference, "\n");
233 race_SendStatus(1, e); // "new time"
234 } else if (oldrec == 0) {
235 bprint(e.netname, "^2 set the ", race_placeName(pos), " ^2place record with ", TIME_ENCODED_TOSTRING(t), "\n");
236 race_SendStatus(2, e); // "new rank"
238 recorddifference = strcat(" ^2[-", TIME_ENCODED_TOSTRING(oldrec - t), "]");
239 bprint(e.netname, "^2 broke ", grecordholder[pos], "^2's ", race_placeName(pos), " ^2place record with ", strcat(TIME_ENCODED_TOSTRING(t), recorddifference, "\n"));
240 race_SendStatus(2, e); // "new rank"
245 void race_DeleteTime(float pos) {
248 for (i = pos-1; i <= RANKINGS_CNT-1; ++i) {
250 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time"), ftos(grecordtime[1]));
251 db_put(ServerProgsDB, strcat(GetMapname(), rr, "crypto_idfp"), grecorduid[1]);
252 grecordtime[0] = grecordtime[1];
253 if (grecordholder[i])
254 strunzone(grecordholder[0]);
255 grecordholder[0] = strzone(grecordholder[1]);
258 strunzone(grecorduid[0]);
259 grecorduid[0] = strzone(grecorduid[1]);
261 else if (i == RANKINGS_CNT-1) {
262 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), string_null);
263 db_put(ServerProgsDB, strcat(GetMapname(), rr, "crypto_idfp", ftos(i)), string_null);
265 if (grecordholder[i])
266 strunzone(grecordholder[i]);
267 grecordholder[i] = string_null;
270 strunzone(grecorduid[i]);
271 grecorduid[i] = string_null;
274 db_put(ServerProgsDB, strcat(GetMapname(), rr, "time", ftos(i)), ftos(grecordtime[i+1]));
275 db_put(ServerProgsDB, strcat(GetMapname(), rr, "crypto_idfp", ftos(i)), grecorduid[i+1]);
276 grecordtime[i] = grecordtime[i+1];
277 if (grecordholder[i])
278 strunzone(grecordholder[i]);
279 grecordholder[i] = strzone(grecordholder[i+1]);
282 strunzone(grecorduid[i]);
283 grecorduid[i] = strzone(grecorduid[i+1]);
287 race_SendRankings(pos, 0, 1, MSG_ALL);
289 race_send_recordtime(MSG_ALL);
292 strunzone(rankings_reply);
293 rankings_reply = strzone(getrankings());
298 void race_SendTime(entity e, float cp, float t, float tvalid)
303 if(g_race_qualifying)
304 t += e.race_penalty_accumulator;
306 t = TIME_ENCODE(t); // make integer
307 // adding just 0.4 so it rounds down in the .5 case (matching the timer display)
310 if(cp == race_timed_checkpoint) // finish line
311 if not(e.race_completed)
314 if(g_race_qualifying)
316 s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
318 PlayerScore_Add(e, SP_RACE_FASTEST, t - s);
322 s = PlayerScore_Add(e, SP_RACE_TIME, 0);
323 snew = TIME_ENCODE(time - game_starttime);
324 PlayerScore_Add(e, SP_RACE_TIME, snew - s);
325 l = PlayerTeamScore_Add(e, SP_RACE_LAPS, ST_RACE_LAPS, 1);
327 if(cvar("fraglimit"))
328 if(l >= cvar("fraglimit"))
329 race_StartCompleting();
333 e.race_completed = 1;
334 MAKE_INDEPENDENT_PLAYER(e);
335 bprint(e.netname, "^7 has finished the race.\n");
343 if(g_race_qualifying)
347 recordtime = race_checkpoint_records[cp];
348 recordholder = strcat1(race_checkpoint_recordholders[cp]); // make a tempstring copy, as we'll possibly strunzone it!
349 if(recordholder == e.netname)
353 if(cp == race_timed_checkpoint)
355 race_SetTime(e, t, recordtime);
356 if(g_cts && cvar("g_cts_finish_kill_delay"))
358 CTS_ClientKill(cvar("g_cts_finish_kill_delay"));
361 if(t < recordtime || recordtime == 0)
363 race_checkpoint_records[cp] = t;
364 if(race_checkpoint_recordholders[cp])
365 strunzone(race_checkpoint_recordholders[cp]);
366 race_checkpoint_recordholders[cp] = strzone(e.netname);
367 if(g_race_qualifying)
369 FOR_EACH_REALPLAYER(p)
370 if(p.race_checkpoint == cp)
371 race_SendNextCheckpoint(p, 0);
385 if(g_race_qualifying)
387 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy1, {
388 WriteByte(MSG_ONE, SVC_TEMPENTITY);
389 WriteByte(MSG_ONE, TE_CSQC_RACE);
390 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_QUALIFYING);
391 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
392 WriteInt24_t(MSG_ONE, t); // time to that intermediate
393 WriteInt24_t(MSG_ONE, recordtime); // previously best time
394 WriteString(MSG_ONE, recordholder); // record holder
398 else // RACE! Not Qualifying
400 float lself, lother, othtime;
402 oth = race_checkpoint_lastplayers[cp];
405 lself = PlayerScore_Add(e, SP_RACE_LAPS, 0);
406 lother = race_checkpoint_lastlaps[cp];
407 othtime = race_checkpoint_lasttimes[cp];
410 lself = lother = othtime = 0;
413 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy2, {
414 WriteByte(MSG_ONE, SVC_TEMPENTITY);
415 WriteByte(MSG_ONE, TE_CSQC_RACE);
416 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE);
417 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
420 WriteInt24_t(MSG_ONE, 0);
421 WriteByte(MSG_ONE, 0);
422 WriteString(MSG_ONE, "");
426 WriteInt24_t(MSG_ONE, TIME_ENCODE(time - race_checkpoint_lasttimes[cp]));
427 WriteByte(MSG_ONE, lself - lother);
428 WriteString(MSG_ONE, oth.netname); // record holder
432 race_checkpoint_lastplayers[cp] = e;
433 race_checkpoint_lasttimes[cp] = time;
434 race_checkpoint_lastlaps[cp] = lself;
437 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy3, {
438 WriteByte(MSG_ONE, SVC_TEMPENTITY);
439 WriteByte(MSG_ONE, TE_CSQC_RACE);
440 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE_BY_OPPONENT);
441 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
444 WriteInt24_t(MSG_ONE, 0);
445 WriteByte(MSG_ONE, 0);
446 WriteString(MSG_ONE, "");
450 WriteInt24_t(MSG_ONE, TIME_ENCODE(time - othtime));
451 WriteByte(MSG_ONE, lother - lself);
452 WriteString(MSG_ONE, e.netname); // record holder
458 void race_ClearTime(entity e)
460 e.race_checkpoint = 0;
462 e.race_movetime = e.race_movetime_frac = e.race_movetime_count = 0;
463 e.race_penalty_accumulator = 0;
464 e.race_lastpenalty = world;
467 WRITESPECTATABLE_MSG_ONE({
468 WriteByte(MSG_ONE, SVC_TEMPENTITY);
469 WriteByte(MSG_ONE, TE_CSQC_RACE);
470 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_CLEAR); // next
474 void dumpsurface(entity e)
478 print("Surfaces of ", etos(e), ":\n");
480 print("TEST = ", ftos(getsurfacenearpoint(e, '0 0 0')), "\n");
484 n = getsurfacenumpoints(e, si);
487 print(" Surface ", ftos(si), ":\n");
488 norm = getsurfacenormal(e, si);
489 print(" Normal = ", vtos(norm), "\n");
490 for(ni = 0; ni < n; ++ni)
492 vec = getsurfacepoint(e, si, ni);
493 print(" Point ", ftos(ni), " = ", vtos(vec), " (", ftos(norm * vec), ")\n");
498 void checkpoint_passed()
503 if(other.classname == "porto")
505 // do not allow portalling through checkpoints
506 trace_plane_normal = normalize(-1 * other.velocity);
515 if not((self.spawnflags & 2) && (other.classname == "player"))
518 oldmsg = self.message;
521 self.message = oldmsg;
524 if(other.classname != "player")
528 * Remove unauthorized equipment
530 Portal_ClearAll(other);
532 other.porto_forbidden = 2; // decreased by 1 each StartFrame
535 if(self.race_checkpoint == -2)
537 self.race_checkpoint = other.race_checkpoint;
542 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
544 if(cp.race_checkpoint > largest_cp_id) // update the finish id if someone hit a new checkpoint
546 largest_cp_id = cp.race_checkpoint;
547 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
548 cp.race_checkpoint = largest_cp_id + 1; // finish line
549 race_highest_checkpoint = largest_cp_id + 1;
550 race_timed_checkpoint = largest_cp_id + 1;
552 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
553 if(cp.race_checkpoint == -2) // set defragcpexists to -1 so that the cp id file will be rewritten when someone finishes
559 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
560 cp.race_checkpoint = 1;
561 race_highest_checkpoint = 1;
562 race_timed_checkpoint = 1;
566 if((other.race_checkpoint == -1 && self.race_checkpoint == 0) || (other.race_checkpoint == self.race_checkpoint))
568 if(self.race_penalty)
570 if(other.race_lastpenalty != self)
572 other.race_lastpenalty = self;
573 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
577 if(other.race_penalty)
583 if(self.spawnflags & 2)
586 oldmsg = self.message;
589 self.message = oldmsg;
592 if(other.race_respawn_checkpoint != self.race_checkpoint || !other.race_started)
593 other.race_respawn_spotref = self; // this is not a spot but a CP, but spawnpoint selection will deal with that
594 other.race_respawn_checkpoint = self.race_checkpoint;
595 other.race_checkpoint = race_NextCheckpoint(self.race_checkpoint);
596 other.race_started = 1;
598 race_SendTime(other, self.race_checkpoint, other.race_movetime, !!other.race_laptime);
600 if(!self.race_checkpoint) // start line
602 other.race_laptime = time;
603 other.race_movetime = other.race_movetime_frac = other.race_movetime_count = 0;
604 other.race_penalty_accumulator = 0;
605 other.race_lastpenalty = world;
608 if(g_race_qualifying)
609 race_SendNextCheckpoint(other, 0);
611 if(defrag_ents && defragcpexists < 0 && self.classname == "target_stopTimer")
614 defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_WRITE);
617 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
618 fputs(fh, strcat(cp.targetname, " ", ftos(cp.race_checkpoint), "\n"));
623 else if(other.race_checkpoint == race_NextCheckpoint(self.race_checkpoint))
629 if(self.spawnflags & 4)
630 Damage (other, self, self, 10000, DEATH_HURTTRIGGER, other.origin, '0 0 0');
634 void checkpoint_touch()
640 void checkpoint_use()
642 if(other.classname == "info_player_deathmatch") // a spawn, a spawn
649 float race_waypointsprite_visible_for_player(entity e)
651 if(e.race_checkpoint == -1 || self.owner.race_checkpoint == -2)
653 else if(e.race_checkpoint == self.owner.race_checkpoint)
660 void trigger_race_checkpoint_verify()
670 qual = g_race_qualifying;
674 self.classname = "player";
678 for(i = 0; i <= race_highest_checkpoint; ++i)
680 self.race_checkpoint = race_NextCheckpoint(i);
682 // race only (middle of the race)
683 g_race_qualifying = 0;
685 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
686 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for respawning in race) - bailing out"));
691 g_race_qualifying = 1;
692 self.race_place = race_lowest_place_spawn;
693 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
694 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
696 // race only (initial spawn)
697 g_race_qualifying = 0;
698 for(p = 1; p <= race_highest_place_spawn; ++p)
701 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
702 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for initially spawning in race) - bailing out"));
707 else if(!defrag_ents)
710 self.race_checkpoint = race_NextCheckpoint(0);
711 g_race_qualifying = 1;
712 self.race_place = race_lowest_place_spawn;
713 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE, FALSE))
714 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
718 self.race_checkpoint = race_NextCheckpoint(0);
719 g_race_qualifying = 1;
720 self.race_place = 0; // there's only one spawn on defrag maps
722 // check if a defragcp file already exists, then read it and apply the checkpoint order
727 defragcpexists = fh = fopen(strcat("maps/", GetMapname(), ".defragcp"), FILE_READ);
730 while((l = fgets(fh)))
732 len = tokenize_console(l);
734 defragcpexists = -1; // something's wrong in the defrag cp file, set defragcpexists to -1 so that it will be rewritten when someone finishes
737 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
738 if(argv(0) == cp.targetname)
739 cp.race_checkpoint = stof(argv(1));
745 g_race_qualifying = qual;
747 if(race_timed_checkpoint) {
749 for(cp = world; (cp = find(cp, classname, "target_startTimer"));)
750 WaypointSprite_UpdateSprites(cp.sprite, "race-start", "", "");
751 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
752 WaypointSprite_UpdateSprites(cp.sprite, "race-finish", "", "");
754 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));) {
755 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
759 if(defragcpexists != -1){
761 for(cp = world; (cp = find(cp, classname, "target_checkpoint"));)
762 if(cp.race_checkpoint > largest_cp_id)
763 largest_cp_id = cp.race_checkpoint;
764 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
765 cp.race_checkpoint = largest_cp_id + 1; // finish line
766 race_highest_checkpoint = largest_cp_id + 1;
767 race_timed_checkpoint = largest_cp_id + 1;
769 for(cp = world; (cp = find(cp, classname, "target_stopTimer"));)
770 cp.race_checkpoint = 255; // finish line
771 race_highest_checkpoint = 255;
772 race_timed_checkpoint = 255;
776 for(cp = world; (cp = find(cp, classname, "trigger_race_checkpoint")); )
779 if(cp.race_checkpoint == 0)
780 WaypointSprite_UpdateSprites(cp.sprite, "race-start", "", "");
781 else if(cp.race_checkpoint == race_timed_checkpoint)
782 WaypointSprite_UpdateSprites(cp.sprite, "race-finish", "", "");
788 entity trigger, targ;
789 for(trigger = world; (trigger = find(trigger, classname, "trigger_multiple")); )
790 for(targ = world; (targ = find(targ, targetname, trigger.target)); )
791 if (targ.classname == "target_checkpoint" || targ.classname == "target_startTimer" || targ.classname == "target_stopTimer") {
795 setsize(targ, trigger.mins, trigger.maxs);
796 setorigin(targ, trigger.origin);
804 void spawnfunc_trigger_race_checkpoint()
807 if(!g_race && !g_cts)
815 self.use = checkpoint_use;
816 if not(self.spawnflags & 1)
817 self.touch = checkpoint_touch;
819 o = (self.absmin + self.absmax) * 0.5;
820 tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o_z - self.absmin_z), MOVE_NORMAL, self);
821 waypoint_spawnforitem_force(self, trace_endpos);
822 self.nearestwaypointtimeout = time + 1000000000;
825 self.message = "went backwards";
827 self.message2 = "was pushed backwards by";
828 if (!self.race_penalty_reason)
829 self.race_penalty_reason = "missing a checkpoint";
831 self.race_checkpoint = self.cnt;
833 if(self.race_checkpoint > race_highest_checkpoint)
835 race_highest_checkpoint = self.race_checkpoint;
836 if(self.spawnflags & 8)
837 race_timed_checkpoint = self.race_checkpoint;
839 race_timed_checkpoint = 0;
842 if(!self.race_penalty)
844 if(self.race_checkpoint)
845 WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
847 WaypointSprite_SpawnFixed("race-finish", o, self, sprite);
850 self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
852 InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
855 void spawnfunc_target_checkpoint() // defrag entity
858 if(!g_race && !g_cts)
867 self.use = checkpoint_use;
868 if not(self.spawnflags & 1)
869 self.touch = checkpoint_touch;
871 o = (self.absmin + self.absmax) * 0.5;
872 tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o_z - self.absmin_z), MOVE_NORMAL, self);
873 waypoint_spawnforitem_force(self, trace_endpos);
874 self.nearestwaypointtimeout = time + 1000000000;
877 self.message = "went backwards";
879 self.message2 = "was pushed backwards by";
880 if (!self.race_penalty_reason)
881 self.race_penalty_reason = "missing a checkpoint";
883 if(self.classname == "target_startTimer")
884 self.race_checkpoint = 0;
886 self.race_checkpoint = -2;
888 race_timed_checkpoint = 1;
890 if(self.race_checkpoint == 0)
891 WaypointSprite_SpawnFixed("race-start", o, self, sprite);
893 WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
895 self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
897 InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
900 void spawnfunc_target_startTimer() { spawnfunc_target_checkpoint(); }
901 void spawnfunc_target_stopTimer() { spawnfunc_target_checkpoint(); }
903 void race_AbandonRaceCheck(entity p)
905 if(race_completing && !p.race_completed)
907 p.race_completed = 1;
908 MAKE_INDEPENDENT_PLAYER(p);
909 bprint(p.netname, "^7 has abandoned the race.\n");
914 void race_StartCompleting()
919 if(p.deadflag != DEAD_NO)
920 race_AbandonRaceCheck(p);
923 void race_PreparePlayer()
925 race_ClearTime(self);
927 self.race_started = 0;
928 self.race_respawn_checkpoint = 0;
929 self.race_respawn_spotref = world;
932 void race_RetractPlayer()
934 if(!g_race && !g_cts)
936 if(self.race_respawn_checkpoint == 0 || self.race_respawn_checkpoint == race_timed_checkpoint)
937 race_ClearTime(self);
938 self.race_checkpoint = self.race_respawn_checkpoint;
943 if(!g_race && !g_cts)
946 race_AbandonRaceCheck(self);
951 if(!g_race && !g_cts)
953 if(self.killcount == -666 /* initial spawn */ || g_race_qualifying) // spawn
954 race_PreparePlayer();
956 race_RetractPlayer();
958 race_AbandonRaceCheck(self);
961 void race_PostSpawn(entity spot)
963 if(!g_race && !g_cts)
966 if(spot.target == "")
967 // Emergency: this wasn't a real spawnpoint. Can this ever happen?
968 race_PreparePlayer();
970 // if we need to respawn, do it right
971 self.race_respawn_checkpoint = self.race_checkpoint;
972 self.race_respawn_spotref = spot;
977 void race_PreSpawnObserver()
979 if(!g_race && !g_cts)
981 race_PreparePlayer();
982 self.race_checkpoint = -1;
985 void spawnfunc_info_player_race (void)
987 if(!g_race && !g_cts)
993 spawnfunc_info_player_deathmatch();
995 if(self.race_place > race_highest_place_spawn)
996 race_highest_place_spawn = self.race_place;
997 if(self.race_place < race_lowest_place_spawn)
998 race_lowest_place_spawn = self.race_place;
1001 void race_ClearRecords()
1006 for(i = 0; i < MAX_CHECKPOINTS; ++i)
1008 race_checkpoint_records[i] = 0;
1009 if(race_checkpoint_recordholders[i])
1010 strunzone(race_checkpoint_recordholders[i]);
1011 race_checkpoint_recordholders[i] = string_null;
1015 FOR_EACH_CLIENT(self)
1018 p = self.race_place;
1019 race_PreparePlayer();
1020 self.race_place = p;
1025 void race_ReadyRestart()
1029 Score_NicePrint(world);
1031 race_ClearRecords();
1032 PlayerScore_Sort(race_place);
1039 s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
1043 print(e.netname, " = ", ftos(e.race_place), "\n");
1046 if(g_race_qualifying == 2)
1048 g_race_qualifying = 0;
1049 independent_players = 0;
1050 cvar_set("fraglimit", ftos(race_fraglimit));
1051 cvar_set("leadlimit", ftos(race_leadlimit));
1052 cvar_set("timelimit", ftos(race_timelimit));
1057 void race_ImposePenaltyTime(entity pl, float penalty, string reason)
1059 if(g_race_qualifying)
1061 pl.race_penalty_accumulator += penalty;
1063 WRITESPECTATABLE_MSG_ONE({
1064 WriteByte(MSG_ONE, SVC_TEMPENTITY);
1065 WriteByte(MSG_ONE, TE_CSQC_RACE);
1066 WriteByte(MSG_ONE, RACE_NET_PENALTY_QUALIFYING);
1067 WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1068 WriteString(MSG_ONE, reason);
1073 pl.race_penalty = time + penalty;
1075 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy, {
1076 WriteByte(MSG_ONE, SVC_TEMPENTITY);
1077 WriteByte(MSG_ONE, TE_CSQC_RACE);
1078 WriteByte(MSG_ONE, RACE_NET_PENALTY_RACE);
1079 WriteShort(MSG_ONE, TIME_ENCODE(penalty));
1080 WriteString(MSG_ONE, reason);
1085 void penalty_touch()
1088 if(other.race_lastpenalty != self)
1090 other.race_lastpenalty = self;
1091 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
1097 race_ImposePenaltyTime(activator, self.race_penalty, self.race_penalty_reason);
1100 void spawnfunc_trigger_race_penalty()
1104 self.use = penalty_use;
1105 if not(self.spawnflags & 1)
1106 self.touch = penalty_touch;
1108 if (!self.race_penalty_reason)
1109 self.race_penalty_reason = "missing a checkpoint";
1110 if (!self.race_penalty)
1111 self.race_penalty = 5;
1114 float race_GetFractionalLapCount(entity e)
1116 // interesting metrics (idea by KrimZon) to maybe sort players in the
1117 // scoreboard, immediately updates when overtaking
1119 // requires the track to be built so you never get farther away from the
1120 // next checkpoint, though, and current Xonotic race maps are not built that
1123 // also, this code is slow and would need optimization (i.e. "next CP"
1124 // links on CP entities)
1127 l = PlayerScore_Add(e, SP_RACE_LAPS, 0);
1128 if(e.race_completed)
1129 return l; // not fractional
1132 float bestfraction, fraction;
1133 entity lastcp, cp0, cp1;
1134 float nextcpindex, lastcpindex;
1136 nextcpindex = max(e.race_checkpoint, 0);
1137 lastcpindex = e.race_respawn_checkpoint;
1138 lastcp = e.race_respawn_spotref;
1140 if(nextcpindex == lastcpindex)
1144 for(cp0 = world; (cp0 = find(cp0, classname, "trigger_race_checkpoint")); )
1146 if(cp0.race_checkpoint != lastcpindex)
1151 o0 = (cp0.absmin + cp0.absmax) * 0.5;
1152 for(cp1 = world; (cp1 = find(cp1, classname, "trigger_race_checkpoint")); )
1154 if(cp1.race_checkpoint != nextcpindex)
1156 o1 = (cp1.absmin + cp1.absmax) * 0.5;
1159 fraction = bound(0.0001, vlen(e.origin - o1) / vlen(o0 - o1), 1);
1160 if(fraction < bestfraction)
1161 bestfraction = fraction;
1165 // we are at CP "nextcpindex - bestfraction"
1166 // race_timed_checkpoint == 4: then nextcp==4 means 0.9999x, nextcp==0 means 0.0000x
1167 // race_timed_checkpoint == 0: then nextcp==0 means 0.9999x
1169 nc = race_highest_checkpoint + 1;
1170 c = (mod(nextcpindex - race_timed_checkpoint + nc + nc - 1, nc) + 1) - bestfraction;