a582594b02532976d051c91274c8b09afb640c6a
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / gamemode_ctf.qc
1 #include "../../common/movetypes/movetypes.qh"
2
3 void ctf_FakeTimeLimit(entity e, float t)
4 {
5         msg_entity = e;
6         WriteByte(MSG_ONE, 3); // svc_updatestat
7         WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
8         if(t < 0)
9                 WriteCoord(MSG_ONE, autocvar_timelimit);
10         else
11                 WriteCoord(MSG_ONE, (t + 1) / 60);
12 }
13
14 void ctf_EventLog(string mode, float flagteam, entity actor) // use an alias for easy changing and quick editing later
15 {
16         if(autocvar_sv_eventlog)
17                 GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
18 }
19
20 void ctf_CaptureRecord(entity flag, entity player)
21 {
22         float cap_record = ctf_captimerecord;
23         float cap_time = (time - flag.ctf_pickuptime);
24         string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
25
26         // notify about shit
27         if(!ctf_captimerecord) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_2(flag, CHOICE_CTF_CAPTURE_TIME_), player.netname, (cap_time * 100)); }
28         else if(cap_time < cap_record) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_2(flag, CHOICE_CTF_CAPTURE_BROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
29         else { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_2(flag, CHOICE_CTF_CAPTURE_UNBROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
30
31         // write that shit in the database
32         if((!ctf_captimerecord) || (cap_time < cap_record))
33         {
34                 ctf_captimerecord = cap_time;
35                 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
36                 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
37                 write_recordmarker(player, (time - cap_time), cap_time);
38         }
39 }
40
41 void ctf_FlagcarrierWaypoints(entity player)
42 {
43         WaypointSprite_Spawn("flagcarrier", 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, true, RADARICON_FLAG, WPCOLOR_FLAGCARRIER(player.team));
44         WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON) * 2);
45         WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
46         WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
47 }
48
49 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
50 {
51         float current_distance = vlen((('1 0 0' * to.x) + ('0 1 0' * to.y)) - (('1 0 0' * from.x) + ('0 1 0' * from.y))); // for the sake of this check, exclude Z axis
52         float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
53         float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
54         //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
55
56         vector targpos;
57         if(current_height) // make sure we can actually do this arcing path
58         {
59                 targpos = (to + ('0 0 1' * current_height));
60                 WarpZone_TraceLine(flag.move_origin, targpos, MOVE_NOMONSTERS, flag);
61                 if(trace_fraction < 1)
62                 {
63                         //print("normal arc line failed, trying to find new pos...");
64                         WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
65                         targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
66                         WarpZone_TraceLine(flag.move_origin, targpos, MOVE_NOMONSTERS, flag);
67                         if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
68                         /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
69                 }
70         }
71         else { targpos = to; }
72
73         //flag.move_angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
74
75         vector desired_direction = normalize(targpos - from);
76         if(turnrate) { flag.move_velocity = (normalize(normalize(flag.move_velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
77         else { flag.move_velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
78 }
79
80 float ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
81 {
82         if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
83         {
84                 // directional tracing only
85                 float spreadlimit;
86                 makevectors(passer_angle);
87
88                 // find the closest point on the enemy to the center of the attack
89                 float h; // hypotenuse, which is the distance between attacker to head
90                 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
91
92                 h = vlen(head_center - passer_center);
93                 a = h * (normalize(head_center - passer_center) * v_forward);
94
95                 vector nearest_on_line = (passer_center + a * v_forward);
96                 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
97
98                 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
99                 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
100
101                 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
102                         { return true; }
103                 else
104                         { return false; }
105         }
106         else { return true; }
107 }
108
109
110 // =======================
111 // CaptureShield Functions
112 // =======================
113
114 float ctf_CaptureShield_CheckStatus(entity p)
115 {
116         float s, se;
117         entity e;
118         float players_worseeq, players_total;
119
120         if(ctf_captureshield_max_ratio <= 0)
121                 return false;
122
123         s = PlayerScore_Add(p, SP_SCORE, 0);
124         if(s >= -ctf_captureshield_min_negscore)
125                 return false;
126
127         players_total = players_worseeq = 0;
128         FOR_EACH_PLAYER(e)
129         {
130                 if(DIFF_TEAM(e, p))
131                         continue;
132                 se = PlayerScore_Add(e, SP_SCORE, 0);
133                 if(se <= s)
134                         ++players_worseeq;
135                 ++players_total;
136         }
137
138         // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
139         // use this rule here
140
141         if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
142                 return false;
143
144         return true;
145 }
146
147 void ctf_CaptureShield_Update(entity player, float wanted_status)
148 {
149         float updated_status = ctf_CaptureShield_CheckStatus(player);
150         if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
151         {
152                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
153                 player.ctf_captureshielded = updated_status;
154         }
155 }
156
157 float ctf_CaptureShield_Customize()
158 {
159         if(!other.ctf_captureshielded) { return false; }
160         if(SAME_TEAM(self, other)) { return false; }
161
162         return true;
163 }
164
165 void ctf_CaptureShield_Touch()
166 {
167         if(!other.ctf_captureshielded) { return; }
168         if(SAME_TEAM(self, other)) { return; }
169
170         vector mymid = (self.absmin + self.absmax) * 0.5;
171         vector othermid = (other.absmin + other.absmax) * 0.5;
172
173         Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
174         if(IS_REAL_CLIENT(other)) { Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
175 }
176
177 void ctf_CaptureShield_Spawn(entity flag)
178 {
179         entity shield = spawn();
180
181         shield.enemy = self;
182         shield.team = self.team;
183         shield.touch = ctf_CaptureShield_Touch;
184         shield.customizeentityforclient = ctf_CaptureShield_Customize;
185         shield.classname = "ctf_captureshield";
186         shield.effects = EF_ADDITIVE;
187         shield.movetype = MOVETYPE_NOCLIP;
188         shield.solid = SOLID_TRIGGER;
189         shield.avelocity = '7 0 11';
190         shield.scale = 0.5;
191
192         setorigin(shield, self.origin);
193         setmodel(shield, "models/ctf/shield.md3");
194         setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
195 }
196
197
198 // ====================
199 // Drop/Pass/Throw Code
200 // ====================
201
202 void ctf_Handle_Drop(entity flag, entity player, float droptype)
203 {
204         // declarations
205         player = (player ? player : flag.pass_sender);
206
207         // main
208         flag.move_movetype = MOVETYPE_TOSS;
209         flag.takedamage = DAMAGE_YES;
210         flag.move_angles = '0 0 0';
211         flag.health = flag.max_flag_health;
212         flag.ctf_droptime = time;
213         flag.ctf_dropper = player;
214         flag.ctf_status = FLAG_DROPPED;
215
216         // messages and sounds
217         Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_LOST_), player.netname);
218         sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
219         ctf_EventLog("dropped", player.team, player);
220
221         // scoring
222         PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
223         PlayerScore_Add(player, SP_CTF_DROPS, 1);
224
225         // waypoints
226         if(autocvar_g_ctf_flag_dropped_waypoint)
227                 WaypointSprite_Spawn("flagdropped", 0, 0, flag, FLAG_WAYPOINT_OFFSET, world, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, true, RADARICON_FLAG, WPCOLOR_DROPPEDFLAG(flag.team));
228
229         if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
230         {
231                 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
232                 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
233         }
234
235         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
236
237         if(droptype == DROP_PASS)
238         {
239                 flag.pass_distance = 0;
240                 flag.pass_sender = world;
241                 flag.pass_target = world;
242         }
243 }
244
245 void ctf_Handle_Retrieve(entity flag, entity player)
246 {
247         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
248         entity sender = flag.pass_sender;
249
250         // transfer flag to player
251         flag.owner = player;
252         flag.owner.flagcarried = flag;
253
254         // reset flag
255         if(player.vehicle)
256         {
257                 setattachment(flag, player.vehicle, "");
258                 flag.move_origin = VEHICLE_FLAG_OFFSET;
259                 flag.scale = VEHICLE_FLAG_SCALE;
260         }
261         else
262         {
263                 setattachment(flag, player, "");
264                 flag.move_origin = VEHICLE_FLAG_OFFSET;
265         }
266         flag.move_movetype = MOVETYPE_NONE;
267         flag.takedamage = DAMAGE_NO;
268         flag.solid = SOLID_NOT;
269         flag.move_angles = '0 0 0';
270         flag.ctf_status = FLAG_CARRY;
271
272         // messages and sounds
273         sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
274         ctf_EventLog("receive", flag.team, player);
275
276         FOR_EACH_REALPLAYER(tmp_player)
277         {
278                 if(tmp_player == sender)
279                         Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PASS_SENT_), player.netname);
280                 else if(tmp_player == player)
281                         Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PASS_RECEIVED_), sender.netname);
282                 else if(SAME_TEAM(tmp_player, sender))
283                         Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PASS_OTHER_), sender.netname, player.netname);
284         }
285
286         // create new waypoint
287         ctf_FlagcarrierWaypoints(player);
288
289         sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
290         player.throw_antispam = sender.throw_antispam;
291
292         flag.pass_distance = 0;
293         flag.pass_sender = world;
294         flag.pass_target = world;
295 }
296
297 void ctf_Handle_Throw(entity player, entity receiver, float droptype)
298 {
299         entity flag = player.flagcarried;
300         vector targ_origin, flag_velocity;
301
302         if(!flag) { return; }
303         if((droptype == DROP_PASS) && !receiver) { return; }
304
305         if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
306
307         // reset the flag
308         setattachment(flag, world, "");
309         flag.move_origin = player.origin + FLAG_DROP_OFFSET;
310         flag.owner.flagcarried = world;
311         flag.owner = world;
312         flag.solid = SOLID_TRIGGER;
313         flag.ctf_dropper = player;
314         flag.ctf_droptime = time;
315
316         flag.move_flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
317         flag.flags = flag.move_flags;
318
319         switch(droptype)
320         {
321                 case DROP_PASS:
322                 {
323                         // warpzone support:
324                         // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
325                         // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
326                         WarpZone_RefSys_Copy(flag, receiver);
327                         WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
328                         targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
329
330                         flag.pass_distance = vlen((('1 0 0' * targ_origin.x) + ('0 1 0' * targ_origin.y)) - (('1 0 0' *  player.origin.x) + ('0 1 0' *  player.origin.y))); // for the sake of this check, exclude Z axis
331                         ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
332
333                         // main
334                         flag.move_movetype = MOVETYPE_FLY;
335                         flag.takedamage = DAMAGE_NO;
336                         flag.pass_sender = player;
337                         flag.pass_target = receiver;
338                         flag.ctf_status = FLAG_PASSING;
339
340                         // other
341                         sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
342                         WarpZone_TrailParticles(world, particleeffectnum(flag.passeffect), player.origin, targ_origin);
343                         ctf_EventLog("pass", flag.team, player);
344                         break;
345                 }
346
347                 case DROP_THROW:
348                 {
349                         makevectors((player.v_angle.y * '0 1 0') + (bound(autocvar_g_ctf_throw_angle_min, player.v_angle.x, autocvar_g_ctf_throw_angle_max) * '1 0 0'));
350
351                         flag_velocity = (('0 0 1' * autocvar_g_ctf_throw_velocity_up) + ((v_forward * autocvar_g_ctf_throw_velocity_forward) * ((player.items & IT_STRENGTH) ? autocvar_g_ctf_throw_strengthmultiplier : 1)));
352                         flag.move_velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, false);
353                         ctf_Handle_Drop(flag, player, droptype);
354                         break;
355                 }
356
357                 case DROP_RESET:
358                 {
359                         flag.move_velocity = '0 0 0'; // do nothing
360                         break;
361                 }
362
363                 default:
364                 case DROP_NORMAL:
365                 {
366                         flag.move_velocity = W_CalculateProjectileVelocity(player.velocity, (('0 0 1' * autocvar_g_ctf_drop_velocity_up) + ((('0 1 0' * crandom()) + ('1 0 0' * crandom())) * autocvar_g_ctf_drop_velocity_side)), false);
367                         ctf_Handle_Drop(flag, player, droptype);
368                         break;
369                 }
370         }
371
372         // kill old waypointsprite
373         WaypointSprite_Ping(player.wps_flagcarrier);
374         WaypointSprite_Kill(player.wps_flagcarrier);
375
376         if(player.wps_enemyflagcarrier)
377                 WaypointSprite_Kill(player.wps_enemyflagcarrier);
378
379         // captureshield
380         ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
381 }
382
383
384 // ==============
385 // Event Handlers
386 // ==============
387
388 void ctf_Handle_Capture(entity flag, entity toucher, float capturetype)
389 {
390         entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
391         entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
392         float old_time, new_time;
393
394         if (!player) { return; } // without someone to give the reward to, we can't possibly cap
395
396         nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
397
398         // messages and sounds
399         Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_2(enemy_flag, CENTER_CTF_CAPTURE_));
400         ctf_CaptureRecord(enemy_flag, player);
401         sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTEN_NONE);
402
403         switch(capturetype)
404         {
405                 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
406                 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
407                 default: break;
408         }
409
410         // scoring
411         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
412         PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
413
414         old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
415         new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
416         if(!old_time || new_time < old_time)
417                 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
418
419         // effects
420         pointparticles(particleeffectnum(flag.capeffect), flag.move_origin, '0 0 0', 1);
421         //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.move_origin - '0 0 15', -0.8, 0, 1);
422
423         // other
424         if(capturetype == CAPTURE_NORMAL)
425         {
426                 WaypointSprite_Kill(player.wps_flagcarrier);
427                 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
428
429                 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
430                         { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
431         }
432
433         // reset the flag
434         player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
435         ctf_RespawnFlag(enemy_flag);
436 }
437
438 void ctf_Handle_Return(entity flag, entity player)
439 {
440         // messages and sounds
441         if(player.flags & FL_MONSTER)
442         {
443                 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_RETURN_MONSTER_), player.monster_name);
444         }
445         else
446         {
447                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_RETURN_));
448                 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_RETURN_), player.netname);
449         }
450         sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
451         ctf_EventLog("return", flag.team, player);
452
453         // scoring
454         if(IS_PLAYER(player))
455         {
456                 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
457                 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
458
459                 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
460         }
461
462         TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
463
464         if(flag.ctf_dropper)
465         {
466                 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
467                 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
468                 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
469         }
470
471         // reset the flag
472         ctf_RespawnFlag(flag);
473 }
474
475 void ctf_Handle_Pickup(entity flag, entity player, float pickuptype)
476 {
477         // declarations
478         float pickup_dropped_score; // used to calculate dropped pickup score
479
480         // attach the flag to the player
481         flag.owner = player;
482         player.flagcarried = flag;
483         if(player.vehicle)
484         {
485                 setattachment(flag, player.vehicle, "");
486                 flag.move_origin = VEHICLE_FLAG_OFFSET;
487                 flag.scale = VEHICLE_FLAG_SCALE;
488         }
489         else
490         {
491                 setattachment(flag, player, "");
492                 flag.move_origin = FLAG_CARRY_OFFSET;
493         }
494
495         // flag setup
496         flag.move_movetype = MOVETYPE_NONE;
497         flag.takedamage = DAMAGE_NO;
498         flag.solid = SOLID_NOT;
499         flag.move_angles = '0 0 0';
500         flag.ctf_status = FLAG_CARRY;
501
502         switch(pickuptype)
503         {
504                 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
505                 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
506                 default: break;
507         }
508
509         // messages and sounds
510         Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_PICKUP_), player.netname);
511         Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_2(flag, CENTER_CTF_PICKUP_));
512         if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
513
514         Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, CHOICE_CTF_PICKUP_TEAM, Team_ColorCode(player.team), player.netname);
515         Send_Notification(NOTIF_TEAM, flag, MSG_CHOICE, CHOICE_CTF_PICKUP_ENEMY, Team_ColorCode(player.team), player.netname);
516
517         sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
518
519         // scoring
520         PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
521         nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
522         switch(pickuptype)
523         {
524                 case PICKUP_BASE:
525                 {
526                         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
527                         ctf_EventLog("steal", flag.team, player);
528                         break;
529                 }
530
531                 case PICKUP_DROPPED:
532                 {
533                         pickup_dropped_score = (autocvar_g_ctf_flag_return_time ? bound(0, ((flag.ctf_droptime + autocvar_g_ctf_flag_return_time) - time) / autocvar_g_ctf_flag_return_time, 1) : 1);
534                         pickup_dropped_score = floor((autocvar_g_ctf_score_pickup_dropped_late * (1 - pickup_dropped_score) + autocvar_g_ctf_score_pickup_dropped_early * pickup_dropped_score) + 0.5);
535                         dprint("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
536                         PlayerTeamScore_AddScore(player, pickup_dropped_score);
537                         ctf_EventLog("pickup", flag.team, player);
538                         break;
539                 }
540
541                 default: break;
542         }
543
544         // speedrunning
545         if(pickuptype == PICKUP_BASE)
546         {
547                 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
548                 if((player.speedrunning) && (ctf_captimerecord))
549                         ctf_FakeTimeLimit(player, time + ctf_captimerecord);
550         }
551
552         // effects
553         pointparticles(particleeffectnum(flag.toucheffect), player.origin, '0 0 0', 1);
554
555         // waypoints
556         if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
557         ctf_FlagcarrierWaypoints(player);
558         WaypointSprite_Ping(player.wps_flagcarrier);
559 }
560
561
562 // ===================
563 // Main Flag Functions
564 // ===================
565
566 void ctf_CheckFlagReturn(entity flag, float returntype)
567 {
568         if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
569         {
570                 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
571
572                 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
573                 {
574                         switch(returntype)
575                         {
576                                 case RETURN_DROPPED: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_DROPPED_)); break;
577                                 case RETURN_DAMAGE: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_DAMAGED_)); break;
578                                 case RETURN_SPEEDRUN: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_SPEEDRUN_), ctf_captimerecord); break;
579                                 case RETURN_NEEDKILL: Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_NEEDKILL_)); break;
580
581                                 default:
582                                 case RETURN_TIMEOUT:
583                                         { Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(flag, INFO_CTF_FLAGRETURN_TIMEOUT_)); break; }
584                         }
585                         sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
586                         ctf_EventLog("returned", flag.team, world);
587                         ctf_RespawnFlag(flag);
588                 }
589         }
590 }
591
592 void ctf_CheckStalemate(void)
593 {
594         // declarations
595         float stale_red_flags = 0, stale_blue_flags = 0;
596         entity tmp_entity;
597
598         entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
599
600         // build list of stale flags
601         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
602         {
603                 if(autocvar_g_ctf_stalemate)
604                 if(tmp_entity.ctf_status != FLAG_BASE)
605                 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time)
606                 {
607                         tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
608                         ctf_staleflaglist = tmp_entity;
609
610                         switch(tmp_entity.team)
611                         {
612                                 case NUM_TEAM_1: ++stale_red_flags; break;
613                                 case NUM_TEAM_2: ++stale_blue_flags; break;
614                         }
615                 }
616         }
617
618         if(stale_red_flags && stale_blue_flags)
619                 ctf_stalemate = true;
620         else if((!stale_red_flags && !stale_blue_flags) && autocvar_g_ctf_stalemate_endcondition == 2)
621                 { ctf_stalemate = false; wpforenemy_announced = false; }
622         else if((!stale_red_flags || !stale_blue_flags) && autocvar_g_ctf_stalemate_endcondition == 1)
623                 { ctf_stalemate = false; wpforenemy_announced = false; }
624
625         // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
626         if(ctf_stalemate)
627         {
628                 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
629                 {
630                         if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
631                                 WaypointSprite_Spawn("enemyflagcarrier", 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, world, tmp_entity.team, tmp_entity.owner, wps_enemyflagcarrier, true, RADARICON_FLAG, WPCOLOR_ENEMYFC(tmp_entity.owner.team));
632                 }
633
634                 if (!wpforenemy_announced)
635                 {
636                         FOR_EACH_REALPLAYER(tmp_entity)
637                                 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CENTER, ((tmp_entity.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER));
638
639                         wpforenemy_announced = true;
640                 }
641         }
642 }
643
644 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
645 {
646         self.move_velocity = self.velocity;
647         if(ITEM_DAMAGE_NEEDKILL(deathtype))
648         {
649                 // automatically kill the flag and return it
650                 self.health = 0;
651                 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
652                 return;
653         }
654         if(autocvar_g_ctf_flag_return_damage)
655         {
656                 // reduce health and check if it should be returned
657                 self.health = self.health - damage;
658                 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
659                 return;
660         }
661 }
662
663 void ctf_FlagUpdate()
664 {
665         // declarations
666         entity tmp_entity;
667
668         // captureshield
669         if(self == ctf_worldflaglist) // only for the first flag
670                 FOR_EACH_CLIENT(tmp_entity)
671                         ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
672
673         // sanity checks
674         if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
675                 dprint("wtf the flag got squashed?\n");
676                 tracebox(self.move_origin, FLAG_MIN, FLAG_MAX, self.move_origin, MOVE_NOMONSTERS, self);
677                 if(!trace_startsolid || self.noalign) // can we resize it without getting stuck?
678                         setsize(self, FLAG_MIN, FLAG_MAX); }
679
680         switch(self.ctf_status) // reset flag angles in case warpzones adjust it
681         {
682                 case FLAG_DROPPED:
683                 {
684                         self.move_angles = '0 0 0';
685                         break;
686                 }
687
688                 default: break;
689         }
690
691         // main think method
692         switch(self.ctf_status)
693         {
694                 case FLAG_BASE:
695                 {
696                         if(autocvar_g_ctf_dropped_capture_radius)
697                         {
698                                 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
699                                         if(tmp_entity.ctf_status == FLAG_DROPPED)
700                                         if(vlen(self.move_origin - tmp_entity.move_origin) < autocvar_g_ctf_dropped_capture_radius)
701                                         if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
702                                                 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
703                         }
704                         return;
705                 }
706
707                 case FLAG_DROPPED:
708                 {
709                         if(autocvar_g_ctf_flag_dropped_floatinwater)
710                         {
711                                 vector midpoint = ((self.absmin + self.absmax) * 0.5);
712                                 if(pointcontents(midpoint) == CONTENT_WATER)
713                                 {
714                                         self.move_velocity = self.move_velocity * 0.5;
715
716                                         if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
717                                                 { self.move_velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
718                                         else
719                                                 { self.move_movetype = MOVETYPE_FLY; }
720                                 }
721                                 else if(self.move_movetype == MOVETYPE_FLY) { self.move_movetype = MOVETYPE_TOSS; }
722                         }
723                         if(autocvar_g_ctf_flag_return_dropped)
724                         {
725                                 if((vlen(self.move_origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
726                                 {
727                                         self.health = 0;
728                                         ctf_CheckFlagReturn(self, RETURN_DROPPED);
729                                         return;
730                                 }
731                         }
732                         if(autocvar_g_ctf_flag_return_time)
733                         {
734                                 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
735                                 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
736                                 return;
737                         }
738                         return;
739                 }
740
741                 case FLAG_CARRY:
742                 {
743                         if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
744                         {
745                                 self.health = 0;
746                                 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
747
748                                 tmp_entity = self;
749                                 self = self.owner;
750                                 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
751                                 ImpulseCommands();
752                                 self = tmp_entity;
753                         }
754                         if(autocvar_g_ctf_stalemate)
755                         {
756                                 if(time >= wpforenemy_nextthink)
757                                 {
758                                         ctf_CheckStalemate();
759                                         wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
760                                 }
761                         }
762                         return;
763                 }
764
765                 case FLAG_PASSING:
766                 {
767                         vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
768                         targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
769                         WarpZone_TraceLine(self.move_origin, targ_origin, MOVE_NOMONSTERS, self);
770
771                         if((self.pass_target == world)
772                                 || (self.pass_target.deadflag != DEAD_NO)
773                                 || (self.pass_target.flagcarried)
774                                 || (vlen(self.move_origin - targ_origin) > autocvar_g_ctf_pass_radius)
775                                 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
776                                 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
777                         {
778                                 // give up, pass failed
779                                 ctf_Handle_Drop(self, world, DROP_PASS);
780                         }
781                         else
782                         {
783                                 // still a viable target, go for it
784                                 ctf_CalculatePassVelocity(self, targ_origin, self.move_origin, true);
785                         }
786                         return;
787                 }
788
789                 default: // this should never happen
790                 {
791                         dprint("ctf_FlagThink(): Flag exists with no status?\n");
792                         return;
793                 }
794         }
795 }
796
797 void ctf_FlagThink()
798 {
799         self.nextthink = time;
800
801         if(time >= self.ctf_thinkrate)
802         {
803                 self.ctf_thinkrate = time + FLAG_THINKRATE;
804                 ctf_FlagUpdate();
805         }
806
807         //Movetype_Physics_NoMatchServer();
808         Movetype_Physics_MatchTicrate(sys_frametime, 0);
809 }
810
811 void ctf_FlagTouch()
812 {
813         if(gameover) { return; }
814         if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
815
816         entity toucher = other;
817         float is_not_monster = (!(toucher.flags & FL_MONSTER));
818
819         // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
820         if(ITEM_TOUCH_NEEDKILL())
821         {
822                 self.health = 0;
823                 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
824                 return;
825         }
826
827         // special touch behaviors
828         if(toucher.frozen) { return; }
829         else if(toucher.vehicle_flags & VHF_ISVEHICLE)
830         {
831                 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
832                         toucher = toucher.owner; // the player is actually the vehicle owner, not other
833                 else
834                         return; // do nothing
835         }
836         else if(toucher.flags & FL_MONSTER)
837         {
838                 if(!autocvar_g_ctf_allow_monster_touch)
839                         return; // do nothing
840         }
841         else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
842         {
843                 if(time > self.wait) // if we haven't in a while, play a sound/effect
844                 {
845                         pointparticles(particleeffectnum(self.toucheffect), self.move_origin, '0 0 0', 1);
846                         sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTEN_NORM);
847                         self.wait = time + FLAG_TOUCHRATE;
848                 }
849                 return;
850         }
851         else if(toucher.deadflag != DEAD_NO) { return; }
852
853         switch(self.ctf_status)
854         {
855                 case FLAG_BASE:
856                 {
857                         if(SAME_TEAM(toucher, self) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, self) && is_not_monster)
858                                 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
859                         else if(DIFF_TEAM(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
860                                 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
861                         break;
862                 }
863
864                 case FLAG_DROPPED:
865                 {
866                         if(SAME_TEAM(toucher, self))
867                                 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
868                         else if(is_not_monster && (!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
869                                 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
870                         break;
871                 }
872
873                 case FLAG_CARRY:
874                 {
875                         dprint("Someone touched a flag even though it was being carried?\n");
876                         break;
877                 }
878
879                 case FLAG_PASSING:
880                 {
881                         if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
882                         {
883                                 if(DIFF_TEAM(toucher, self.pass_sender))
884                                         ctf_Handle_Return(self, toucher);
885                                 else
886                                         ctf_Handle_Retrieve(self, toucher);
887                         }
888                         break;
889                 }
890         }
891 }
892
893 .float last_respawn;
894 void ctf_RespawnFlag(entity flag)
895 {
896         // check for flag respawn being called twice in a row
897         if(flag.last_respawn > time - 0.5)
898                 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
899
900         flag.last_respawn = time;
901
902         // reset the player (if there is one)
903         if((flag.owner) && (flag.owner.flagcarried == flag))
904         {
905                 if(flag.owner.wps_enemyflagcarrier)
906                         WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
907
908                 WaypointSprite_Kill(flag.wps_flagcarrier);
909
910                 flag.owner.flagcarried = world;
911
912                 if(flag.speedrunning)
913                         ctf_FakeTimeLimit(flag.owner, -1);
914         }
915
916         if((flag.owner) && (flag.owner.vehicle))
917                 flag.scale = FLAG_SCALE;
918
919         if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
920                 { WaypointSprite_Kill(flag.wps_flagdropped); }
921
922         // reset the flag
923         setattachment(flag, world, "");
924         flag.move_origin = flag.ctf_spawnorigin;
925
926         flag.move_movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
927         flag.takedamage = DAMAGE_NO;
928         flag.health = flag.max_flag_health;
929         flag.solid = SOLID_TRIGGER;
930         flag.move_velocity = '0 0 0';
931         flag.move_angles = flag.mangle;
932         flag.move_flags = FL_ITEM | FL_NOTARGET;
933         flag.flags = flag.move_flags;
934
935         flag.ctf_status = FLAG_BASE;
936         flag.owner = world;
937         flag.pass_distance = 0;
938         flag.pass_sender = world;
939         flag.pass_target = world;
940         flag.ctf_dropper = world;
941         flag.ctf_pickuptime = 0;
942         flag.ctf_droptime = 0;
943
944         ctf_CheckStalemate();
945 }
946
947 void ctf_Reset()
948 {
949         if(self.owner)
950                 if(IS_PLAYER(self.owner))
951                         ctf_Handle_Throw(self.owner, world, DROP_RESET);
952
953         ctf_RespawnFlag(self);
954 }
955
956 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
957 {
958         // bot waypoints
959         waypoint_spawnforitem_force(self, self.origin);
960         self.nearestwaypointtimeout = 0; // activate waypointing again
961         self.bot_basewaypoint = self.nearestwaypoint;
962
963         // waypointsprites
964         // move_origin isnt accessible just yet
965         WaypointSprite_SpawnFixed(((self.team == NUM_TEAM_1) ? "redbase" : "bluebase"), self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, false));
966         WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, false));
967
968         // captureshield setup
969         ctf_CaptureShield_Spawn(self);
970
971         self.move_origin = self.origin;
972         self.move_angles = self.angles;
973         self.move_velocity = self.velocity;
974 }
975
976 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
977 {
978         // declarations
979         teamnumber = fabs(teamnumber - bound(0, autocvar_g_ctf_reverse, 1)); // if we were originally 1, this will become 0. If we were originally 0, this will become 1.
980         self = flag; // for later usage with droptofloor()
981
982         // main setup
983         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
984         ctf_worldflaglist = flag;
985
986         setattachment(flag, world, "");
987
988         flag.netname = ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag"); // Primarily only used for debugging or when showing nearby item name
989         flag.team = ((teamnumber) ? NUM_TEAM_1 : NUM_TEAM_2); // NUM_TEAM_1: color 4 team (red) - NUM_TEAM_2: color 13 team (blue)
990         flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough)
991         flag.classname = "item_flag_team";
992         flag.target = "###item###"; // wut?
993         flag.move_flags = FL_ITEM | FL_NOTARGET;
994         flag.flags = flag.move_flags;
995         flag.solid = SOLID_TRIGGER;
996         flag.takedamage = DAMAGE_NO;
997         flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
998         flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
999         flag.health = flag.max_flag_health;
1000         flag.event_damage = ctf_FlagDamage;
1001         flag.pushable = true;
1002         flag.teleportable = TELEPORT_NORMAL;
1003         flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1004         flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1005         flag.move_velocity = '0 0 0';
1006         flag.mangle = flag.angles;
1007         flag.reset = ctf_Reset;
1008         flag.touch = ctf_FlagTouch;
1009         flag.move_touch = flag.touch;
1010         flag.think = ctf_FlagThink;
1011         flag.nextthink = time + FLAG_THINKRATE;
1012         flag.ctf_status = FLAG_BASE;
1013         flag.move_time = time;
1014
1015         // appearence
1016         if(flag.model == "")       { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
1017         if(!flag.scale)            { flag.scale = FLAG_SCALE; }
1018         if(!flag.skin)             { flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin); }
1019         if(flag.toucheffect == "") { flag.toucheffect = ((teamnumber) ? "redflag_touch" : "blueflag_touch"); }
1020         if(flag.passeffect == "")  { flag.passeffect = ((teamnumber) ? "red_pass" : "blue_pass"); }
1021         if(flag.capeffect == "")   { flag.capeffect = ((teamnumber) ? "red_cap" : "blue_cap"); }
1022
1023         // sound
1024         if(flag.snd_flag_taken == "")    { flag.snd_flag_taken  = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
1025         if(flag.snd_flag_returned == "") { flag.snd_flag_returned = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
1026         if(flag.snd_flag_capture == "")  { flag.snd_flag_capture = ((teamnumber) ? "ctf/red_capture.wav" : "ctf/blue_capture.wav"); } // blue team scores by capturing the red flag
1027         if(flag.snd_flag_respawn == "")  { flag.snd_flag_respawn = "ctf/flag_respawn.wav"; } // if there is ever a team-based sound for this, update the code to match.
1028         if(flag.snd_flag_dropped == "")  { flag.snd_flag_dropped = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_dropped.wav"); }
1029         if(flag.snd_flag_touch == "")    { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
1030         if(flag.snd_flag_pass == "")     { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
1031
1032         // precache
1033         precache_sound(flag.snd_flag_taken);
1034         precache_sound(flag.snd_flag_returned);
1035         precache_sound(flag.snd_flag_capture);
1036         precache_sound(flag.snd_flag_respawn);
1037         precache_sound(flag.snd_flag_dropped);
1038         precache_sound(flag.snd_flag_touch);
1039         precache_sound(flag.snd_flag_pass);
1040         precache_model(flag.model);
1041         precache_model("models/ctf/shield.md3");
1042         precache_model("models/ctf/shockwavetransring.md3");
1043
1044         // appearence
1045         setmodel(flag, flag.model); // precision set below
1046         setsize(flag, FLAG_MIN, FLAG_MAX);
1047         setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1048
1049         if(autocvar_g_ctf_flag_glowtrails)
1050         {
1051                 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
1052                 flag.glow_size = 25;
1053                 flag.glow_trail = 1;
1054         }
1055
1056         flag.effects |= EF_LOWPRECISION;
1057         if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1058         if(autocvar_g_ctf_dynamiclights)   { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
1059
1060         // flag placement
1061         if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1062         {
1063                 flag.dropped_origin = flag.origin;
1064                 flag.noalign = true;
1065                 flag.move_movetype = MOVETYPE_NONE;
1066         }
1067         else // drop to floor, automatically find a platform and set that as spawn origin
1068         {
1069                 flag.noalign = false;
1070                 self = flag;
1071                 droptofloor();
1072                 flag.move_movetype = MOVETYPE_TOSS;
1073         }
1074
1075         InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1076 }
1077
1078
1079 // ================
1080 // Bot player logic
1081 // ================
1082
1083 // NOTE: LEGACY CODE, needs to be re-written!
1084
1085 void havocbot_calculate_middlepoint()
1086 {
1087         entity f;
1088         vector s = '0 0 0';
1089         vector fo = '0 0 0';
1090         float n = 0;
1091
1092         f = ctf_worldflaglist;
1093         while (f)
1094         {
1095                 fo = f.move_origin;
1096                 s = s + fo;
1097                 f = f.ctf_worldflagnext;
1098         }
1099         if(!n)
1100                 return;
1101         havocbot_ctf_middlepoint = s * (1.0 / n);
1102         havocbot_ctf_middlepoint_radius  = vlen(fo - havocbot_ctf_middlepoint);
1103 }
1104
1105
1106 entity havocbot_ctf_find_flag(entity bot)
1107 {
1108         entity f;
1109         f = ctf_worldflaglist;
1110         while (f)
1111         {
1112                 if (bot.team == f.team)
1113                         return f;
1114                 f = f.ctf_worldflagnext;
1115         }
1116         return world;
1117 }
1118
1119 entity havocbot_ctf_find_enemy_flag(entity bot)
1120 {
1121         entity f;
1122         f = ctf_worldflaglist;
1123         while (f)
1124         {
1125                 if (bot.team != f.team)
1126                         return f;
1127                 f = f.ctf_worldflagnext;
1128         }
1129         return world;
1130 }
1131
1132 float havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1133 {
1134         if (!teamplay)
1135                 return 0;
1136
1137         float c = 0;
1138         entity head;
1139
1140         FOR_EACH_PLAYER(head)
1141         {
1142                 if(head.team!=bot.team || head.deadflag != DEAD_NO || head == bot)
1143                         continue;
1144
1145                 if(vlen(head.origin - org) < tc_radius)
1146                         ++c;
1147         }
1148
1149         return c;
1150 }
1151
1152 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1153 {
1154         entity head;
1155         head = ctf_worldflaglist;
1156         while (head)
1157         {
1158                 if (self.team == head.team)
1159                         break;
1160                 head = head.ctf_worldflagnext;
1161         }
1162         if (head)
1163                 navigation_routerating(head, ratingscale, 10000);
1164 }
1165
1166 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1167 {
1168         entity head;
1169         head = ctf_worldflaglist;
1170         while (head)
1171         {
1172                 if (self.team == head.team)
1173                         break;
1174                 head = head.ctf_worldflagnext;
1175         }
1176         if (!head)
1177                 return;
1178
1179         navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1180 }
1181
1182 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1183 {
1184         entity head;
1185         head = ctf_worldflaglist;
1186         while (head)
1187         {
1188                 if (self.team != head.team)
1189                         break;
1190                 head = head.ctf_worldflagnext;
1191         }
1192         if (head)
1193                 navigation_routerating(head, ratingscale, 10000);
1194 }
1195
1196 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1197 {
1198         if (!bot_waypoints_for_items)
1199         {
1200                 havocbot_goalrating_ctf_enemyflag(ratingscale);
1201                 return;
1202         }
1203
1204         entity head;
1205
1206         head = havocbot_ctf_find_enemy_flag(self);
1207
1208         if (!head)
1209                 return;
1210
1211         navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1212 }
1213
1214 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1215 {
1216         entity mf;
1217
1218         mf = havocbot_ctf_find_flag(self);
1219
1220         if(mf.ctf_status == FLAG_BASE)
1221                 return;
1222
1223         if(mf.tag_entity)
1224                 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1225 }
1226
1227 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1228 {
1229         entity head;
1230         head = ctf_worldflaglist;
1231         while (head)
1232         {
1233                 // flag is out in the field
1234                 if(head.ctf_status != FLAG_BASE)
1235                 if(head.tag_entity==world)      // dropped
1236                 {
1237                         if(df_radius)
1238                         {
1239                                 if(vlen(org-head.move_origin)<df_radius)
1240                                         navigation_routerating(head, ratingscale, 10000);
1241                         }
1242                         else
1243                                 navigation_routerating(head, ratingscale, 10000);
1244                 }
1245
1246                 head = head.ctf_worldflagnext;
1247         }
1248 }
1249
1250 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1251 {
1252         entity head;
1253         float t;
1254         head = findchainfloat(bot_pickup, true);
1255         while (head)
1256         {
1257                 // gather health and armor only
1258                 if (head.solid)
1259                 if (head.health || head.armorvalue)
1260                 if (vlen(head.origin - org) < sradius)
1261                 {
1262                         // get the value of the item
1263                         t = head.bot_pickupevalfunc(self, head) * 0.0001;
1264                         if (t > 0)
1265                                 navigation_routerating(head, t * ratingscale, 500);
1266                 }
1267                 head = head.chain;
1268         }
1269 }
1270
1271 void havocbot_ctf_reset_role(entity bot)
1272 {
1273         float cdefense, cmiddle, coffense;
1274         entity mf, ef, head;
1275         float c;
1276
1277         if(bot.deadflag != DEAD_NO)
1278                 return;
1279
1280         if(vlen(havocbot_ctf_middlepoint)==0)
1281                 havocbot_calculate_middlepoint();
1282
1283         // Check ctf flags
1284         if (bot.flagcarried)
1285         {
1286                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1287                 return;
1288         }
1289
1290         mf = havocbot_ctf_find_flag(bot);
1291         ef = havocbot_ctf_find_enemy_flag(bot);
1292
1293         // Retrieve stolen flag
1294         if(mf.ctf_status!=FLAG_BASE)
1295         {
1296                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1297                 return;
1298         }
1299
1300         // If enemy flag is taken go to the middle to intercept pursuers
1301         if(ef.ctf_status!=FLAG_BASE)
1302         {
1303                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1304                 return;
1305         }
1306
1307         // if there is only me on the team switch to offense
1308         c = 0;
1309         FOR_EACH_PLAYER(head)
1310         if(SAME_TEAM(head, bot))
1311                 ++c;
1312
1313         if(c==1)
1314         {
1315                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1316                 return;
1317         }
1318
1319         // Evaluate best position to take
1320         // Count mates on middle position
1321         cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1322
1323         // Count mates on defense position
1324         cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1325
1326         // Count mates on offense position
1327         coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1328
1329         if(cdefense<=coffense)
1330                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1331         else if(coffense<=cmiddle)
1332                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1333         else
1334                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1335 }
1336
1337 void havocbot_role_ctf_carrier()
1338 {
1339         if(self.deadflag != DEAD_NO)
1340         {
1341                 havocbot_ctf_reset_role(self);
1342                 return;
1343         }
1344
1345         if (self.flagcarried == world)
1346         {
1347                 havocbot_ctf_reset_role(self);
1348                 return;
1349         }
1350
1351         if (self.bot_strategytime < time)
1352         {
1353                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1354
1355                 navigation_goalrating_start();
1356                 havocbot_goalrating_ctf_ourbase(50000);
1357
1358                 if(self.health<100)
1359                         havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1360
1361                 navigation_goalrating_end();
1362
1363                 if (self.navigation_hasgoals)
1364                         self.havocbot_cantfindflag = time + 10;
1365                 else if (time > self.havocbot_cantfindflag)
1366                 {
1367                         // Can't navigate to my own base, suicide!
1368                         // TODO: drop it and wander around
1369                         Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1370                         return;
1371                 }
1372         }
1373 }
1374
1375 void havocbot_role_ctf_escort()
1376 {
1377         entity mf, ef;
1378
1379         if(self.deadflag != DEAD_NO)
1380         {
1381                 havocbot_ctf_reset_role(self);
1382                 return;
1383         }
1384
1385         if (self.flagcarried)
1386         {
1387                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1388                 return;
1389         }
1390
1391         // If enemy flag is back on the base switch to previous role
1392         ef = havocbot_ctf_find_enemy_flag(self);
1393         if(ef.ctf_status==FLAG_BASE)
1394         {
1395                 self.havocbot_role = self.havocbot_previous_role;
1396                 self.havocbot_role_timeout = 0;
1397                 return;
1398         }
1399
1400         // If the flag carrier reached the base switch to defense
1401         mf = havocbot_ctf_find_flag(self);
1402         if(mf.ctf_status!=FLAG_BASE)
1403         if(vlen(ef.move_origin - mf.dropped_origin) < 300)
1404         {
1405                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1406                 return;
1407         }
1408
1409         // Set the role timeout if necessary
1410         if (!self.havocbot_role_timeout)
1411         {
1412                 self.havocbot_role_timeout = time + random() * 30 + 60;
1413         }
1414
1415         // If nothing happened just switch to previous role
1416         if (time > self.havocbot_role_timeout)
1417         {
1418                 self.havocbot_role = self.havocbot_previous_role;
1419                 self.havocbot_role_timeout = 0;
1420                 return;
1421         }
1422
1423         // Chase the flag carrier
1424         if (self.bot_strategytime < time)
1425         {
1426                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1427                 navigation_goalrating_start();
1428                 havocbot_goalrating_ctf_enemyflag(30000);
1429                 havocbot_goalrating_ctf_ourstolenflag(40000);
1430                 havocbot_goalrating_items(10000, self.origin, 10000);
1431                 navigation_goalrating_end();
1432         }
1433 }
1434
1435 void havocbot_role_ctf_offense()
1436 {
1437         entity mf, ef;
1438         vector pos;
1439
1440         if(self.deadflag != DEAD_NO)
1441         {
1442                 havocbot_ctf_reset_role(self);
1443                 return;
1444         }
1445
1446         if (self.flagcarried)
1447         {
1448                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1449                 return;
1450         }
1451
1452         // Check flags
1453         mf = havocbot_ctf_find_flag(self);
1454         ef = havocbot_ctf_find_enemy_flag(self);
1455
1456         // Own flag stolen
1457         if(mf.ctf_status!=FLAG_BASE)
1458         {
1459                 if(mf.tag_entity)
1460                         pos = mf.tag_entity.origin;
1461                 else
1462                         pos = mf.move_origin;
1463
1464                 // Try to get it if closer than the enemy base
1465                 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1466                 {
1467                         havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1468                         return;
1469                 }
1470         }
1471
1472         // Escort flag carrier
1473         if(ef.ctf_status!=FLAG_BASE)
1474         {
1475                 if(ef.tag_entity)
1476                         pos = ef.tag_entity.origin;
1477                 else
1478                         pos = ef.move_origin;
1479
1480                 if(vlen(pos-mf.dropped_origin)>700)
1481                 {
1482                         havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1483                         return;
1484                 }
1485         }
1486
1487         // About to fail, switch to middlefield
1488         if(self.health<50)
1489         {
1490                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1491                 return;
1492         }
1493
1494         // Set the role timeout if necessary
1495         if (!self.havocbot_role_timeout)
1496                 self.havocbot_role_timeout = time + 120;
1497
1498         if (time > self.havocbot_role_timeout)
1499         {
1500                 havocbot_ctf_reset_role(self);
1501                 return;
1502         }
1503
1504         if (self.bot_strategytime < time)
1505         {
1506                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1507                 navigation_goalrating_start();
1508                 havocbot_goalrating_ctf_ourstolenflag(50000);
1509                 havocbot_goalrating_ctf_enemybase(20000);
1510                 havocbot_goalrating_items(5000, self.origin, 1000);
1511                 havocbot_goalrating_items(1000, self.origin, 10000);
1512                 navigation_goalrating_end();
1513         }
1514 }
1515
1516 // Retriever (temporary role):
1517 void havocbot_role_ctf_retriever()
1518 {
1519         entity mf;
1520
1521         if(self.deadflag != DEAD_NO)
1522         {
1523                 havocbot_ctf_reset_role(self);
1524                 return;
1525         }
1526
1527         if (self.flagcarried)
1528         {
1529                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1530                 return;
1531         }
1532
1533         // If flag is back on the base switch to previous role
1534         mf = havocbot_ctf_find_flag(self);
1535         if(mf.ctf_status==FLAG_BASE)
1536         {
1537                 havocbot_ctf_reset_role(self);
1538                 return;
1539         }
1540
1541         if (!self.havocbot_role_timeout)
1542                 self.havocbot_role_timeout = time + 20;
1543
1544         if (time > self.havocbot_role_timeout)
1545         {
1546                 havocbot_ctf_reset_role(self);
1547                 return;
1548         }
1549
1550         if (self.bot_strategytime < time)
1551         {
1552                 float rt_radius;
1553                 rt_radius = 10000;
1554
1555                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1556                 navigation_goalrating_start();
1557                 havocbot_goalrating_ctf_ourstolenflag(50000);
1558                 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1559                 havocbot_goalrating_ctf_enemybase(30000);
1560                 havocbot_goalrating_items(500, self.origin, rt_radius);
1561                 navigation_goalrating_end();
1562         }
1563 }
1564
1565 void havocbot_role_ctf_middle()
1566 {
1567         entity mf;
1568
1569         if(self.deadflag != DEAD_NO)
1570         {
1571                 havocbot_ctf_reset_role(self);
1572                 return;
1573         }
1574
1575         if (self.flagcarried)
1576         {
1577                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1578                 return;
1579         }
1580
1581         mf = havocbot_ctf_find_flag(self);
1582         if(mf.ctf_status!=FLAG_BASE)
1583         {
1584                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1585                 return;
1586         }
1587
1588         if (!self.havocbot_role_timeout)
1589                 self.havocbot_role_timeout = time + 10;
1590
1591         if (time > self.havocbot_role_timeout)
1592         {
1593                 havocbot_ctf_reset_role(self);
1594                 return;
1595         }
1596
1597         if (self.bot_strategytime < time)
1598         {
1599                 vector org;
1600
1601                 org = havocbot_ctf_middlepoint;
1602                 org.z = self.origin.z;
1603
1604                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1605                 navigation_goalrating_start();
1606                 havocbot_goalrating_ctf_ourstolenflag(50000);
1607                 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1608                 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1609                 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1610                 havocbot_goalrating_items(2500, self.origin, 10000);
1611                 havocbot_goalrating_ctf_enemybase(2500);
1612                 navigation_goalrating_end();
1613         }
1614 }
1615
1616 void havocbot_role_ctf_defense()
1617 {
1618         entity mf;
1619
1620         if(self.deadflag != DEAD_NO)
1621         {
1622                 havocbot_ctf_reset_role(self);
1623                 return;
1624         }
1625
1626         if (self.flagcarried)
1627         {
1628                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1629                 return;
1630         }
1631
1632         // If own flag was captured
1633         mf = havocbot_ctf_find_flag(self);
1634         if(mf.ctf_status!=FLAG_BASE)
1635         {
1636                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1637                 return;
1638         }
1639
1640         if (!self.havocbot_role_timeout)
1641                 self.havocbot_role_timeout = time + 30;
1642
1643         if (time > self.havocbot_role_timeout)
1644         {
1645                 havocbot_ctf_reset_role(self);
1646                 return;
1647         }
1648         if (self.bot_strategytime < time)
1649         {
1650                 float mp_radius;
1651                 vector org;
1652
1653                 org = mf.dropped_origin;
1654                 mp_radius = havocbot_ctf_middlepoint_radius;
1655
1656                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1657                 navigation_goalrating_start();
1658
1659                 // if enemies are closer to our base, go there
1660                 entity head, closestplayer = world;
1661                 float distance, bestdistance = 10000;
1662                 FOR_EACH_PLAYER(head)
1663                 {
1664                         if(head.deadflag!=DEAD_NO)
1665                                 continue;
1666
1667                         distance = vlen(org - head.origin);
1668                         if(distance<bestdistance)
1669                         {
1670                                 closestplayer = head;
1671                                 bestdistance = distance;
1672                         }
1673                 }
1674
1675                 if(closestplayer)
1676                 if(DIFF_TEAM(closestplayer, self))
1677                 if(vlen(org - self.origin)>1000)
1678                 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1679                         havocbot_goalrating_ctf_ourbase(30000);
1680
1681                 havocbot_goalrating_ctf_ourstolenflag(20000);
1682                 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1683                 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1684                 havocbot_goalrating_items(10000, org, mp_radius);
1685                 havocbot_goalrating_items(5000, self.origin, 10000);
1686                 navigation_goalrating_end();
1687         }
1688 }
1689
1690 void havocbot_role_ctf_setrole(entity bot, float role)
1691 {
1692         dprint(strcat(bot.netname," switched to "));
1693         switch(role)
1694         {
1695                 case HAVOCBOT_CTF_ROLE_CARRIER:
1696                         dprint("carrier");
1697                         bot.havocbot_role = havocbot_role_ctf_carrier;
1698                         bot.havocbot_role_timeout = 0;
1699                         bot.havocbot_cantfindflag = time + 10;
1700                         bot.bot_strategytime = 0;
1701                         break;
1702                 case HAVOCBOT_CTF_ROLE_DEFENSE:
1703                         dprint("defense");
1704                         bot.havocbot_role = havocbot_role_ctf_defense;
1705                         bot.havocbot_role_timeout = 0;
1706                         break;
1707                 case HAVOCBOT_CTF_ROLE_MIDDLE:
1708                         dprint("middle");
1709                         bot.havocbot_role = havocbot_role_ctf_middle;
1710                         bot.havocbot_role_timeout = 0;
1711                         break;
1712                 case HAVOCBOT_CTF_ROLE_OFFENSE:
1713                         dprint("offense");
1714                         bot.havocbot_role = havocbot_role_ctf_offense;
1715                         bot.havocbot_role_timeout = 0;
1716                         break;
1717                 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1718                         dprint("retriever");
1719                         bot.havocbot_previous_role = bot.havocbot_role;
1720                         bot.havocbot_role = havocbot_role_ctf_retriever;
1721                         bot.havocbot_role_timeout = time + 10;
1722                         bot.bot_strategytime = 0;
1723                         break;
1724                 case HAVOCBOT_CTF_ROLE_ESCORT:
1725                         dprint("escort");
1726                         bot.havocbot_previous_role = bot.havocbot_role;
1727                         bot.havocbot_role = havocbot_role_ctf_escort;
1728                         bot.havocbot_role_timeout = time + 30;
1729                         bot.bot_strategytime = 0;
1730                         break;
1731         }
1732         dprint("\n");
1733 }
1734
1735
1736 // ==============
1737 // Hook Functions
1738 // ==============
1739
1740 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1741 {
1742         entity flag;
1743
1744         // initially clear items so they can be set as necessary later.
1745         self.items &= ~(IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST
1746                 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
1747
1748         // scan through all the flags and notify the client about them
1749         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1750         {
1751                 switch(flag.ctf_status)
1752                 {
1753                         case FLAG_PASSING:
1754                         case FLAG_CARRY:
1755                         {
1756                                 if((flag.owner == self) || (flag.pass_sender == self))
1757                                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
1758                                 else
1759                                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
1760                                 break;
1761                         }
1762                         case FLAG_DROPPED:
1763                         {
1764                                 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
1765                                 break;
1766                         }
1767                 }
1768         }
1769
1770         // item for stopping players from capturing the flag too often
1771         if(self.ctf_captureshielded)
1772                 self.items |= IT_CTF_SHIELDED;
1773
1774         // update the health of the flag carrier waypointsprite
1775         if(self.wps_flagcarrier)
1776                 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
1777
1778         return false;
1779 }
1780
1781 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1782 {
1783         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1784         {
1785                 if(frag_target == frag_attacker) // damage done to yourself
1786                 {
1787                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1788                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1789                 }
1790                 else // damage done to everyone else
1791                 {
1792                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1793                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1794                 }
1795         }
1796         else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && DIFF_TEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
1797         {
1798                 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON)))
1799                 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1800                 {
1801                         frag_target.wps_helpme_time = time;
1802                         WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1803                 }
1804                 // todo: add notification for when flag carrier needs help?
1805         }
1806         return false;
1807 }
1808
1809 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1810 {
1811         if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
1812         {
1813                 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1814                 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1815         }
1816
1817         if(frag_target.flagcarried)
1818                 { ctf_Handle_Throw(frag_target, world, DROP_NORMAL); }
1819
1820         return false;
1821 }
1822
1823 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1824 {
1825         frag_score = 0;
1826         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1827 }
1828
1829 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1830 {
1831         entity flag; // temporary entity for the search method
1832
1833         if(self.flagcarried)
1834                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1835
1836         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1837         {
1838                 if(flag.pass_sender == self) { flag.pass_sender = world; }
1839                 if(flag.pass_target == self) { flag.pass_target = world; }
1840                 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
1841         }
1842
1843         return false;
1844 }
1845
1846 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
1847 {
1848         if(self.flagcarried)
1849         if(!autocvar_g_ctf_portalteleport)
1850                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1851
1852         return false;
1853 }
1854
1855 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
1856 {
1857         if(MUTATOR_RETURNVALUE || gameover) { return false; }
1858
1859         entity player = self;
1860
1861         if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1862         {
1863                 // pass the flag to a team mate
1864                 if(autocvar_g_ctf_pass)
1865                 {
1866                         entity head, closest_target = world;
1867                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
1868
1869                         while(head) // find the closest acceptable target to pass to
1870                         {
1871                                 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
1872                                 if(head != player && SAME_TEAM(head, player))
1873                                 if(!head.speedrunning && !head.vehicle)
1874                                 {
1875                                         // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
1876                                         vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
1877                                         vector passer_center = CENTER_OR_VIEWOFS(player);
1878
1879                                         if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
1880                                         {
1881                                                 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
1882                                                 {
1883                                                         if(IS_BOT_CLIENT(head))
1884                                                         {
1885                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1886                                                                 ctf_Handle_Throw(head, player, DROP_PASS);
1887                                                         }
1888                                                         else
1889                                                         {
1890                                                                 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
1891                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1892                                                         }
1893                                                         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
1894                                                         return true;
1895                                                 }
1896                                                 else if(player.flagcarried)
1897                                                 {
1898                                                         if(closest_target)
1899                                                         {
1900                                                                 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
1901                                                                 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
1902                                                                         { closest_target = head; }
1903                                                         }
1904                                                         else { closest_target = head; }
1905                                                 }
1906                                         }
1907                                 }
1908                                 head = head.chain;
1909                         }
1910
1911                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
1912                 }
1913
1914                 // throw the flag in front of you
1915                 if(autocvar_g_ctf_throw && player.flagcarried)
1916                 {
1917                         if(player.throw_count == -1)
1918                         {
1919                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
1920                                 {
1921                                         player.throw_prevtime = time;
1922                                         player.throw_count = 1;
1923                                         ctf_Handle_Throw(player, world, DROP_THROW);
1924                                         return true;
1925                                 }
1926                                 else
1927                                 {
1928                                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
1929                                         return false;
1930                                 }
1931                         }
1932                         else
1933                         {
1934                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
1935                                 else { player.throw_count += 1; }
1936                                 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
1937
1938                                 player.throw_prevtime = time;
1939                                 ctf_Handle_Throw(player, world, DROP_THROW);
1940                                 return true;
1941                         }
1942                 }
1943         }
1944
1945         return false;
1946 }
1947
1948 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
1949 {
1950         if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
1951         {
1952                 self.wps_helpme_time = time;
1953                 WaypointSprite_HelpMePing(self.wps_flagcarrier);
1954         }
1955         else // create a normal help me waypointsprite
1956         {
1957                 WaypointSprite_Spawn("helpme", waypointsprite_deployed_lifetime, waypointsprite_limitedrange, self, FLAG_WAYPOINT_OFFSET, world, self.team, self, wps_helpme, false, RADARICON_HELPME, '1 0.5 0');
1958                 WaypointSprite_Ping(self.wps_helpme);
1959         }
1960
1961         return true;
1962 }
1963
1964 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
1965 {
1966         if(vh_player.flagcarried)
1967         {
1968                 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
1969                 {
1970                         ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
1971                 }
1972                 else
1973                 {
1974                         setattachment(vh_player.flagcarried, vh_vehicle, "");
1975                         vh_player.flagcarried.move_origin = VEHICLE_FLAG_OFFSET;
1976                         vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
1977                         //vh_player.flagcarried.move_angles = '0 0 0';
1978                 }
1979                 return true;
1980         }
1981
1982         return false;
1983 }
1984
1985 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
1986 {
1987         if(vh_player.flagcarried)
1988         {
1989                 setattachment(vh_player.flagcarried, vh_player, "");
1990                 vh_player.flagcarried.move_origin = VEHICLE_FLAG_OFFSET;
1991                 vh_player.flagcarried.scale = FLAG_SCALE;
1992                 vh_player.flagcarried.move_angles = '0 0 0';
1993                 return true;
1994         }
1995
1996         return false;
1997 }
1998
1999 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
2000 {
2001         if(self.flagcarried)
2002         {
2003                 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_2(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN_));
2004                 ctf_RespawnFlag(self.flagcarried);
2005                 return true;
2006         }
2007
2008         return false;
2009 }
2010
2011 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
2012 {
2013         entity flag; // temporary entity for the search method
2014
2015         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2016         {
2017                 switch(flag.ctf_status)
2018                 {
2019                         case FLAG_DROPPED:
2020                         case FLAG_PASSING:
2021                         {
2022                                 // lock the flag, game is over
2023                                 flag.move_movetype = MOVETYPE_NONE;
2024                                 flag.takedamage = DAMAGE_NO;
2025                                 flag.solid = SOLID_NOT;
2026                                 flag.nextthink = false; // stop thinking
2027
2028                                 //dprint("stopping the ", flag.netname, " from moving.\n");
2029                                 break;
2030                         }
2031
2032                         default:
2033                         case FLAG_BASE:
2034                         case FLAG_CARRY:
2035                         {
2036                                 // do nothing for these flags
2037                                 break;
2038                         }
2039                 }
2040         }
2041
2042         return false;
2043 }
2044
2045 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2046 {
2047         havocbot_ctf_reset_role(self);
2048         return true;
2049 }
2050
2051
2052 // ==========
2053 // Spawnfuncs
2054 // ==========
2055
2056 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
2057 CTF Starting point for a player in team one (Red).
2058 Keys: "angle" viewing angle when spawning. */
2059 void spawnfunc_info_player_team1()
2060 {
2061         if(g_assault) { remove(self); return; }
2062
2063         self.team = NUM_TEAM_1; // red
2064         spawnfunc_info_player_deathmatch();
2065 }
2066
2067
2068 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
2069 CTF Starting point for a player in team two (Blue).
2070 Keys: "angle" viewing angle when spawning. */
2071 void spawnfunc_info_player_team2()
2072 {
2073         if(g_assault) { remove(self); return; }
2074
2075         self.team = NUM_TEAM_2; // blue
2076         spawnfunc_info_player_deathmatch();
2077 }
2078
2079 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
2080 CTF Starting point for a player in team three (Yellow).
2081 Keys: "angle" viewing angle when spawning. */
2082 void spawnfunc_info_player_team3()
2083 {
2084         if(g_assault) { remove(self); return; }
2085
2086         self.team = NUM_TEAM_3; // yellow
2087         spawnfunc_info_player_deathmatch();
2088 }
2089
2090
2091 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
2092 CTF Starting point for a player in team four (Purple).
2093 Keys: "angle" viewing angle when spawning. */
2094 void spawnfunc_info_player_team4()
2095 {
2096         if(g_assault) { remove(self); return; }
2097
2098         self.team = NUM_TEAM_4; // purple
2099         spawnfunc_info_player_deathmatch();
2100 }
2101
2102 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2103 CTF flag for team one (Red).
2104 Keys:
2105 "angle" Angle the flag will point (minus 90 degrees)...
2106 "model" model to use, note this needs red and blue as skins 0 and 1...
2107 "noise" sound played when flag is picked up...
2108 "noise1" sound played when flag is returned by a teammate...
2109 "noise2" sound played when flag is captured...
2110 "noise3" sound played when flag is lost in the field and respawns itself...
2111 "noise4" sound played when flag is dropped by a player...
2112 "noise5" sound played when flag touches the ground... */
2113 void spawnfunc_item_flag_team1()
2114 {
2115         if(!g_ctf) { remove(self); return; }
2116
2117         ctf_FlagSetup(1, self); // 1 = red
2118 }
2119
2120 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2121 CTF flag for team two (Blue).
2122 Keys:
2123 "angle" Angle the flag will point (minus 90 degrees)...
2124 "model" model to use, note this needs red and blue as skins 0 and 1...
2125 "noise" sound played when flag is picked up...
2126 "noise1" sound played when flag is returned by a teammate...
2127 "noise2" sound played when flag is captured...
2128 "noise3" sound played when flag is lost in the field and respawns itself...
2129 "noise4" sound played when flag is dropped by a player...
2130 "noise5" sound played when flag touches the ground... */
2131 void spawnfunc_item_flag_team2()
2132 {
2133         if(!g_ctf) { remove(self); return; }
2134
2135         ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
2136 }
2137
2138 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2139 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2140 Note: If you use spawnfunc_ctf_team entities you must define at least 2!  However, unlike domination, you don't need to make a blank one too.
2141 Keys:
2142 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2143 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2144 void spawnfunc_ctf_team()
2145 {
2146         if(!g_ctf) { remove(self); return; }
2147
2148         self.classname = "ctf_team";
2149         self.team = self.cnt + 1;
2150 }
2151
2152 // compatibility for quake maps
2153 void spawnfunc_team_CTF_redflag()    { spawnfunc_item_flag_team1();    }
2154 void spawnfunc_team_CTF_blueflag()   { spawnfunc_item_flag_team2();    }
2155 void spawnfunc_team_CTF_redplayer()  { spawnfunc_info_player_team1();  }
2156 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2();  }
2157 void spawnfunc_team_CTF_redspawn()   { spawnfunc_info_player_team1();  }
2158 void spawnfunc_team_CTF_bluespawn()  { spawnfunc_info_player_team2();  }
2159
2160
2161 // ==============
2162 // Initialization
2163 // ==============
2164
2165 // scoreboard setup
2166 void ctf_ScoreRules()
2167 {
2168         ScoreRules_basics(2, SFL_SORT_PRIO_PRIMARY, 0, true);
2169         ScoreInfo_SetLabel_TeamScore  (ST_CTF_CAPS,     "caps",      SFL_SORT_PRIO_PRIMARY);
2170         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS,     "caps",      SFL_SORT_PRIO_SECONDARY);
2171         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME,  "captime",   SFL_LOWER_IS_BETTER | SFL_TIME);
2172         ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS,  "pickups",   0);
2173         ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS,  "fckills",   0);
2174         ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS,  "returns",   0);
2175         ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS,    "drops",     SFL_LOWER_IS_BETTER);
2176         ScoreRules_basics_end();
2177 }
2178
2179 // code from here on is just to support maps that don't have flag and team entities
2180 void ctf_SpawnTeam (string teamname, float teamcolor)
2181 {
2182         entity oldself;
2183         oldself = self;
2184         self = spawn();
2185         self.classname = "ctf_team";
2186         self.netname = teamname;
2187         self.cnt = teamcolor;
2188
2189         spawnfunc_ctf_team();
2190
2191         self = oldself;
2192 }
2193
2194 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2195 {
2196         // if no teams are found, spawn defaults
2197         if(find(world, classname, "ctf_team") == world)
2198         {
2199                 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2200                 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2201                 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2202         }
2203
2204         ctf_ScoreRules();
2205 }
2206
2207 void ctf_Initialize()
2208 {
2209         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2210
2211         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2212         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2213         ctf_captureshield_force = autocvar_g_ctf_shield_force;
2214
2215         InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2216 }
2217
2218
2219 MUTATOR_DEFINITION(gamemode_ctf)
2220 {
2221         MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2222         MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2223         MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2224         MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2225         MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2226         MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2227         MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2228         MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2229         MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2230         MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2231         MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2232         MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2233         MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2234         MUTATOR_HOOK(HavocBot_ChooseRole, ctf_BotRoles, CBC_ORDER_ANY);
2235
2236         MUTATOR_ONADD
2237         {
2238                 if(time > 1) // game loads at time 1
2239                         error("This is a game type and it cannot be added at runtime.");
2240                 ctf_Initialize();
2241         }
2242
2243         MUTATOR_ONROLLBACK_OR_REMOVE
2244         {
2245                 // we actually cannot roll back ctf_Initialize here
2246                 // BUT: we don't need to! If this gets called, adding always
2247                 // succeeds.
2248         }
2249
2250         MUTATOR_ONREMOVE
2251         {
2252                 print("This is a game type and it cannot be removed at runtime.");
2253                 return -1;
2254         }
2255
2256         return 0;
2257 }