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