]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/bots.qc
Make multiple turrets a bit less of a cpuhog by spreading the thinks over different...
[divverent/nexuiz.git] / data / qcsrc / server / bots.qc
1 .float aistatus;
2 float AI_STATUS_ROAMING                         = 1;    // Bot is just crawling the map. No enemies at sight
3 float AI_STATUS_ATTACKING                       = 2;    // There are enemies at sight
4 float AI_STATUS_RUNNING                         = 4;    // Bot is bunny hopping
5 float AI_STATUS_DANGER_AHEAD                    = 8;    // There is lava/slime/trigger_hurt ahead
6 float AI_STATUS_OUT_JUMPPAD                     = 16;   // Trying to get out of a "vertical" jump pad
7 float AI_STATUS_OUT_WATER                       = 32;   // Trying to get out of water
8 float AI_STATUS_WAYPOINT_PERSONAL_LINKING       = 64;   // Waiting for the personal waypoint to be linked
9 float AI_STATUS_WAYPOINT_PERSONAL_GOING         = 128;  // Going to a personal waypoint
10 float AI_STATUS_WAYPOINT_PERSONAL_REACHED       = 256;  // Personal waypoint reached
11 float AI_STATUS_JETPACK_FLYING                  = 512;
12 float AI_STATUS_JETPACK_LANDING                 = 1024;
13
14 // utilities for path debugging
15 #ifdef DEBUG_TRACEWALK
16
17 float DEBUG_NODE_SUCCESS        = 1;
18 float DEBUG_NODE_WARNING        = 2;
19 float DEBUG_NODE_FAIL           = 3;
20
21 vector debuglastnode;
22
23 void debugresetnodes()
24 {
25         debuglastnode = '0 0 0';
26 }
27
28 void debugnode(vector node)
29 {
30         if not(self.classname=="player")
31                 return;
32
33         if(debuglastnode=='0 0 0')
34         {
35                 debuglastnode = node;
36                 return;
37         }
38
39         te_lightning2(world, node, debuglastnode);
40         debuglastnode = node;
41 }
42
43 void debugnodestatus(vector position, float status)
44 {
45         vector color;
46
47         switch (status)
48         {
49                 case DEBUG_NODE_SUCCESS:
50                         color = '0 15 0';
51                         break;
52                 case DEBUG_NODE_WARNING:
53                         color = '15 15 0';
54                         break;
55                 case DEBUG_NODE_FAIL:
56                         color = '15 0 0';
57                         break;
58                 default:
59                         color = '15 15 15';
60         }
61
62         te_customflash(position, 40,  2, color);
63 }
64
65 #endif
66
67 // rough simulation of walking from one point to another to test if a path
68 // can be traveled, used for waypoint linking and havocbot
69
70 vector stepheightvec;
71 float bot_navigation_movemode;
72 float navigation_testtracewalk;
73 float tracewalk(entity e, vector start, vector m1, vector m2, vector end, float movemode)
74 {
75         local vector org;
76         local vector move;
77         local vector dir;
78         local float dist;
79         local float totaldist;
80         local float stepdist;
81         local float yaw;
82         local float ignorehazards;
83         local float swimming;
84
85         #ifdef DEBUG_TRACEWALK
86                 debugresetnodes();
87                 debugnode(start);
88         #endif
89
90         move = end - start;
91         move_z = 0;
92         org = start;
93         dist = totaldist = vlen(move);
94         dir = normalize(move);
95         stepdist = 32;
96         ignorehazards = FALSE;
97
98         // Analyze starting point
99         traceline(start, start, MOVE_NORMAL, e);
100         if (trace_dpstartcontents & (DPCONTENTS_SLIME | DPCONTENTS_LAVA))
101                 ignorehazards = TRUE;
102         else
103         {
104                 traceline( start, start + '0 0 -65536', MOVE_NORMAL, e);
105                 if (trace_dpstartcontents & (DPCONTENTS_SLIME | DPCONTENTS_LAVA))
106                 {
107                         ignorehazards = TRUE;
108                         swimming = TRUE;
109                 }
110         }
111         tracebox(start, m1, m2, start, MOVE_NOMONSTERS, e);
112         if (trace_startsolid)
113         {
114                 // Bad start
115                 #ifdef DEBUG_TRACEWALK
116                         debugnodestatus(start, DEBUG_NODE_FAIL);
117                 #endif
118                 //print("tracewalk: ", vtos(start), " is a bad start\n");
119                 return FALSE;
120         }
121
122         // Movement loop
123         yaw = vectoyaw(move);
124         move = end - org;
125         for (;;)
126         {
127                 if (boxesoverlap(end, end, org + m1 + '-1 -1 -1', org + m2 + '1 1 1'))
128                 {
129                         // Succeeded
130                         #ifdef DEBUG_TRACEWALK
131                                 debugnodestatus(org, DEBUG_NODE_SUCCESS);
132                         #endif
133                         //print("tracewalk: ", vtos(start), " can reach ", vtos(end), "\n");
134                         return TRUE;
135                 }
136                 #ifdef DEBUG_TRACEWALK
137                         debugnode(org);
138                 #endif
139
140                 if (dist <= 0)
141                         break;
142                 if (stepdist > dist)
143                         stepdist = dist;
144                 dist = dist - stepdist;
145                 traceline(org, org, MOVE_NORMAL, e);
146                 if (!ignorehazards)
147                 {
148                         if (trace_dpstartcontents & (DPCONTENTS_SLIME | DPCONTENTS_LAVA))
149                         {
150                                 // hazards blocking path
151                                 #ifdef DEBUG_TRACEWALK
152                                         debugnodestatus(org, DEBUG_NODE_FAIL);
153                                 #endif
154                                 //print("tracewalk: ", vtos(start), " hits a hazard when trying to reach ", vtos(end), "\n");
155                                 return FALSE;
156                         }
157                 }
158                 if (trace_dpstartcontents & DPCONTENTS_LIQUIDSMASK)
159                 {
160                         move = normalize(end - org);
161                         tracebox(org, m1, m2, org + move * stepdist, movemode, e);
162
163                         #ifdef DEBUG_TRACEWALK
164                                 debugnode(trace_endpos);
165                         #endif
166
167                         if (trace_fraction < 1)
168                         {
169                                 swimming = TRUE;
170                                 org = trace_endpos - normalize(org - trace_endpos) * stepdist;
171                                 for(; org_z < end_z + self.maxs_z; org_z += stepdist)
172                                 {
173                                                 #ifdef DEBUG_TRACEWALK
174                                                         debugnode(org);
175                                                 #endif
176                                         if(pointcontents(org) == CONTENT_EMPTY)
177                                                         break;
178                                 }
179
180                                 if not (pointcontents(org + '0 0 1') == CONTENT_EMPTY)
181                                 {
182                                         #ifdef DEBUG_TRACEWALK
183                                                 debugnodestatus(org, DEBUG_NODE_FAIL);
184                                         #endif
185                                         return FALSE;
186                                         //print("tracewalk: ", vtos(start), " failed under water\n");
187                                 }
188                                 continue;
189
190                         }
191                         else
192                                 org = trace_endpos;
193                 }
194                 else
195                 {
196                         move = dir * stepdist + org;
197                         tracebox(org, m1, m2, move, movemode, e);
198
199                         #ifdef DEBUG_TRACEWALK
200                                 debugnode(trace_endpos);
201                         #endif
202
203                         // hit something
204                         if (trace_fraction < 1)
205                         {
206                                 // check if we can walk over this obstacle
207                                 tracebox(org + stepheightvec, m1, m2, move + stepheightvec, movemode, e);
208                                 if (trace_fraction < 1 || trace_startsolid)
209                                 {
210                                         #ifdef DEBUG_TRACEWALK
211                                                 debugnodestatus(trace_endpos, DEBUG_NODE_WARNING);
212                                         #endif
213
214                                         // check for doors
215                                         traceline( org, move, movemode, e);
216                                         if ( trace_ent.classname == "door_rotating" || trace_ent.classname == "door")
217                                         {
218                                                 local vector nextmove;
219                                                 move = trace_endpos;
220                                                 while(trace_ent.classname == "door_rotating" || trace_ent.classname == "door")
221                                                 {
222                                                         nextmove = move + (dir * stepdist);
223                                                         traceline( move, nextmove, movemode, e);
224                                                         move = nextmove;
225                                                 }
226                                         }
227                                         else
228                                         {
229                                                 #ifdef DEBUG_TRACEWALK
230                                                         debugnodestatus(trace_endpos, DEBUG_NODE_FAIL);
231                                                 #endif
232                                                 //print("tracewalk: ", vtos(start), " hit something when trying to reach ", vtos(end), "\n");
233                                                 //te_explosion(trace_endpos);
234                                                 //print(ftos(e.dphitcontentsmask), "\n");
235                                                 return FALSE; // failed
236                                         }
237                                 }
238                                 else
239                                         move = trace_endpos;
240                         }
241                         else
242                                 move = trace_endpos;
243
244                         // trace down from stepheight as far as possible and move there,
245                         // if this starts in solid we try again without the stepup, and
246                         // if that also fails we assume it is a wall
247                         // (this is the same logic as the Quake walkmove function used)
248                         tracebox(move, m1, m2, move + '0 0 -65536', movemode, e);
249
250                         // moved successfully
251                         if(swimming)
252                         {
253                                 local float c;
254                                 c = pointcontents(org + '0 0 1');
255                                 if not(c == CONTENT_WATER || c == CONTENT_LAVA || c == CONTENT_SLIME)
256                                         swimming = FALSE;
257                                 else
258                                         continue;
259                         }
260
261                         org = trace_endpos;
262                 }
263         }
264
265         //print("tracewalk: ", vtos(start), " did not arrive at ", vtos(end), " but at ", vtos(org), "\n");
266
267         // moved but didn't arrive at the intended destination
268         #ifdef DEBUG_TRACEWALK
269                 debugnodestatus(org, DEBUG_NODE_FAIL);
270         #endif
271
272         return FALSE;
273 };
274
275
276 // grenade tracing to decide the best pitch to fire at
277
278 entity tracetossent;
279 entity tracetossfaketarget;
280
281 // traces multiple trajectories to find one that will impact the target
282 // 'end' vector is the place it aims for,
283 // returns TRUE only if it hit targ (don't target non-solid entities)
284 vector findtrajectory_velocity;
285 float findtrajectorywithleading(vector org, vector m1, vector m2, entity targ, float shotspeed, float shotspeedupward, float maxtime, float shotdelay, entity ignore)
286 {
287         local float c, savesolid, shottime;
288         local vector dir, end, v;
289         if (shotspeed < 1)
290                 return FALSE; // could cause division by zero if calculated
291         if (targ.solid < SOLID_BBOX) // SOLID_NOT and SOLID_TRIGGER
292                 return FALSE; // could never hit it
293         if (!tracetossent)
294                 tracetossent = spawn();
295         tracetossent.owner = ignore;
296         setsize(tracetossent, m1, m2);
297         savesolid = targ.solid;
298         targ.solid = SOLID_NOT;
299         shottime = ((vlen(targ.origin - org) / shotspeed) + shotdelay);
300         v = targ.velocity * shottime + targ.origin;
301         tracebox(targ.origin, targ.mins, targ.maxs, v, FALSE, targ);
302         v = trace_endpos;
303         end = v + (targ.mins + targ.maxs) * 0.5;
304         if ((vlen(end - org) / shotspeed + 0.2) > maxtime)
305         {
306                 // out of range
307                 targ.solid = savesolid;
308                 return FALSE;
309         }
310
311         if (!tracetossfaketarget)
312                 tracetossfaketarget = spawn();
313         tracetossfaketarget.solid = savesolid;
314         tracetossfaketarget.movetype = targ.movetype;
315         setmodel(tracetossfaketarget, targ.model); // no low precision
316         tracetossfaketarget.model = targ.model;
317         tracetossfaketarget.modelindex = targ.modelindex;
318         setsize(tracetossfaketarget, targ.mins, targ.maxs);
319         setorigin(tracetossfaketarget, v);
320
321         c = 0;
322         dir = normalize(end - org);
323         while (c < 10) // 10 traces
324         {
325                 setorigin(tracetossent, org); // reset
326                 tracetossent.velocity = findtrajectory_velocity = normalize(dir) * shotspeed + shotspeedupward * '0 0 1';
327                 tracetoss(tracetossent, ignore); // love builtin functions...
328                 if (trace_ent == tracetossfaketarget) // done
329                 {
330                         targ.solid = savesolid;
331
332                         // make it disappear
333                         tracetossfaketarget.solid = SOLID_NOT;
334                         tracetossfaketarget.movetype = MOVETYPE_NONE;
335                         tracetossfaketarget.model = "";
336                         tracetossfaketarget.modelindex = 0;
337                         // relink to remove it from physics considerations
338                         setorigin(tracetossfaketarget, v);
339
340                         return TRUE;
341                 }
342                 dir_z = dir_z + 0.1; // aim up a little more
343                 c = c + 1;
344         }
345         targ.solid = savesolid;
346
347         // make it disappear
348         tracetossfaketarget.solid = SOLID_NOT;
349         tracetossfaketarget.movetype = MOVETYPE_NONE;
350         tracetossfaketarget.model = "";
351         tracetossfaketarget.modelindex = 0;
352         // relink to remove it from physics considerations
353         setorigin(tracetossfaketarget, v);
354
355         // leave a valid one even if it won't reach
356         findtrajectory_velocity = normalize(end - org) * shotspeed + shotspeedupward * '0 0 1';
357         return FALSE;
358 };
359
360
361
362 // lag simulation
363
364 .void(float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4) lag_func;
365
366 // upto 5 queued messages
367 .float lag1_time;
368 .float lag1_float1;
369 .float lag1_float2;
370 .entity lag1_entity1;
371 .vector lag1_vec1;
372 .vector lag1_vec2;
373 .vector lag1_vec3;
374 .vector lag1_vec4;
375
376 .float lag2_time;
377 .float lag2_float1;
378 .float lag2_float2;
379 .entity lag2_entity1;
380 .vector lag2_vec1;
381 .vector lag2_vec2;
382 .vector lag2_vec3;
383 .vector lag2_vec4;
384
385 .float lag3_time;
386 .float lag3_float1;
387 .float lag3_float2;
388 .entity lag3_entity1;
389 .vector lag3_vec1;
390 .vector lag3_vec2;
391 .vector lag3_vec3;
392 .vector lag3_vec4;
393
394 .float lag4_time;
395 .float lag4_float1;
396 .float lag4_float2;
397 .entity lag4_entity1;
398 .vector lag4_vec1;
399 .vector lag4_vec2;
400 .vector lag4_vec3;
401 .vector lag4_vec4;
402
403 .float lag5_time;
404 .float lag5_float1;
405 .float lag5_float2;
406 .entity lag5_entity1;
407 .vector lag5_vec1;
408 .vector lag5_vec2;
409 .vector lag5_vec3;
410 .vector lag5_vec4;
411
412 void lag_update()
413 {
414         if (self.lag1_time) if (time > self.lag1_time) {self.lag_func(self.lag1_time, self.lag1_float1, self.lag1_float2, self.lag1_entity1, self.lag1_vec1, self.lag1_vec2, self.lag1_vec3, self.lag1_vec4);self.lag1_time = 0;}
415         if (self.lag2_time) if (time > self.lag2_time) {self.lag_func(self.lag2_time, self.lag2_float1, self.lag2_float2, self.lag2_entity1, self.lag2_vec1, self.lag2_vec2, self.lag2_vec3, self.lag2_vec4);self.lag2_time = 0;}
416         if (self.lag3_time) if (time > self.lag3_time) {self.lag_func(self.lag3_time, self.lag3_float1, self.lag3_float2, self.lag3_entity1, self.lag3_vec1, self.lag3_vec2, self.lag3_vec3, self.lag3_vec4);self.lag3_time = 0;}
417         if (self.lag4_time) if (time > self.lag4_time) {self.lag_func(self.lag4_time, self.lag4_float1, self.lag4_float2, self.lag4_entity1, self.lag4_vec1, self.lag4_vec2, self.lag4_vec3, self.lag4_vec4);self.lag4_time = 0;}
418         if (self.lag5_time) if (time > self.lag5_time) {self.lag_func(self.lag5_time, self.lag5_float1, self.lag5_float2, self.lag5_entity1, self.lag5_vec1, self.lag5_vec2, self.lag5_vec3, self.lag5_vec4);self.lag5_time = 0;}
419 };
420
421 float lag_additem(float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4)
422 {
423         if (self.lag1_time == 0) {self.lag1_time = t;self.lag1_float1 = f1;self.lag1_float2 = f2;self.lag1_entity1 = e1;self.lag1_vec1 = v1;self.lag1_vec2 = v2;self.lag1_vec3 = v3;self.lag1_vec4 = v4;return TRUE;}
424         if (self.lag2_time == 0) {self.lag2_time = t;self.lag2_float1 = f1;self.lag2_float2 = f2;self.lag2_entity1 = e1;self.lag2_vec1 = v1;self.lag2_vec2 = v2;self.lag2_vec3 = v3;self.lag2_vec4 = v4;return TRUE;}
425         if (self.lag3_time == 0) {self.lag3_time = t;self.lag3_float1 = f1;self.lag3_float2 = f2;self.lag3_entity1 = e1;self.lag3_vec1 = v1;self.lag3_vec2 = v2;self.lag3_vec3 = v3;self.lag3_vec4 = v4;return TRUE;}
426         if (self.lag4_time == 0) {self.lag4_time = t;self.lag4_float1 = f1;self.lag4_float2 = f2;self.lag4_entity1 = e1;self.lag4_vec1 = v1;self.lag4_vec2 = v2;self.lag4_vec3 = v3;self.lag4_vec4 = v4;return TRUE;}
427         if (self.lag5_time == 0) {self.lag5_time = t;self.lag5_float1 = f1;self.lag5_float2 = f2;self.lag5_entity1 = e1;self.lag5_vec1 = v1;self.lag5_vec2 = v2;self.lag5_vec3 = v3;self.lag5_vec4 = v4;return TRUE;}
428         // no room for it (what is the best thing to do here??)
429         return FALSE;
430 };
431
432
433 // Random skill system
434 .float bot_thinkskill;
435 .float bot_mouseskill;
436 .float bot_predictionskill;
437 .float bot_offsetskill;
438
439
440 // spawnfunc_waypoint navigation system
441
442 // itemscore = (howmuchmoreIwant / howmuchIcanwant) / itemdistance
443 // waypointscore = 0.7 / waypointdistance
444
445 .entity wp00, wp01, wp02, wp03, wp04, wp05, wp06, wp07;
446 .entity wp08, wp09, wp10, wp11, wp12, wp13, wp14, wp15;
447 .entity wp16, wp17, wp18, wp19, wp20, wp21, wp22, wp23, wp24, wp25, wp26, wp27, wp28, wp29, wp30, wp31;
448 .float wp00mincost, wp01mincost, wp02mincost, wp03mincost, wp04mincost, wp05mincost, wp06mincost, wp07mincost;
449 .float wp08mincost, wp09mincost, wp10mincost, wp11mincost, wp12mincost, wp13mincost, wp14mincost, wp15mincost;
450 .float wp16mincost, wp17mincost, wp18mincost, wp19mincost, wp20mincost, wp21mincost, wp22mincost, wp23mincost, wp24mincost, wp25mincost, wp26mincost, wp27mincost, wp28mincost, wp29mincost, wp30mincost, wp31mincost;
451 .float wpfire, wpcost, wpconsidered;
452 .float wpisbox;
453 .float wpflags;
454 .vector wpnearestpoint;
455 .float wplinked;
456
457 // stack of current goals (the last one of which may be an item or other
458 // desirable object, the rest are typically waypoints to reach it)
459 .entity goalcurrent, goalstack01, goalstack02, goalstack03;
460 .entity goalstack04, goalstack05, goalstack06, goalstack07;
461 .entity goalstack08, goalstack09, goalstack10, goalstack11;
462 .entity goalstack12, goalstack13, goalstack14, goalstack15;
463 .entity goalstack16, goalstack17, goalstack18, goalstack19;
464 .entity goalstack20, goalstack21, goalstack22, goalstack23;
465 .entity goalstack24, goalstack25, goalstack26, goalstack27;
466 .entity goalstack28, goalstack29, goalstack30, goalstack31;
467
468 .entity nearestwaypoint;
469 .float nearestwaypointtimeout;
470
471 // used during navigation_goalrating_begin/end sessions
472 #define MAX_BESTGOALS 3
473 float bestgoalswindex;
474 float bestgoalsrindex;
475 float navigation_bestrating;
476 entity navigation_bestgoals[MAX_BESTGOALS];
477 .float navigation_hasgoals;
478
479 /////////////////////////////////////////////////////////////////////////////
480 // spawnfunc_waypoint management
481 /////////////////////////////////////////////////////////////////////////////
482
483 // waypoints with this flag are not saved, they are automatically generated
484 // waypoints like jump pads, teleporters, and items
485 float WAYPOINTFLAG_GENERATED = 8388608;
486 float WAYPOINTFLAG_ITEM = 4194304;
487 float WAYPOINTFLAG_TELEPORT = 2097152;
488 float WAYPOINTFLAG_NORELINK = 1048576;
489 float WAYPOINTFLAG_PERSONAL = 524288;
490
491 // add a new link to the spawnfunc_waypoint, replacing the furthest link it already has
492 void waypoint_addlink(entity from, entity to)
493 {
494         local float c;
495
496         if (from == to)
497                 return;
498         if (from.wpflags & WAYPOINTFLAG_NORELINK)
499                 return;
500
501         if (from.wp00 == to) return;if (from.wp01 == to) return;if (from.wp02 == to) return;if (from.wp03 == to) return;
502         if (from.wp04 == to) return;if (from.wp05 == to) return;if (from.wp06 == to) return;if (from.wp07 == to) return;
503         if (from.wp08 == to) return;if (from.wp09 == to) return;if (from.wp10 == to) return;if (from.wp11 == to) return;
504         if (from.wp12 == to) return;if (from.wp13 == to) return;if (from.wp14 == to) return;if (from.wp15 == to) return;
505         if (from.wp16 == to) return;if (from.wp17 == to) return;if (from.wp18 == to) return;if (from.wp19 == to) return;
506         if (from.wp20 == to) return;if (from.wp21 == to) return;if (from.wp22 == to) return;if (from.wp23 == to) return;
507         if (from.wp24 == to) return;if (from.wp25 == to) return;if (from.wp26 == to) return;if (from.wp27 == to) return;
508         if (from.wp28 == to) return;if (from.wp29 == to) return;if (from.wp30 == to) return;if (from.wp31 == to) return;
509
510         if (to.wpisbox || from.wpisbox)
511         {
512                 // if either is a box we have to find the nearest points on them to
513                 // calculate the distance properly
514                 local vector v1, v2, m1, m2;
515                 v1 = from.origin;
516                 m1 = to.absmin;
517                 m2 = to.absmax;
518                 v1_x = bound(m1_x, v1_x, m2_x);
519                 v1_y = bound(m1_y, v1_y, m2_y);
520                 v1_z = bound(m1_z, v1_z, m2_z);
521                 v2 = to.origin;
522                 m1 = from.absmin;
523                 m2 = from.absmax;
524                 v2_x = bound(m1_x, v2_x, m2_x);
525                 v2_y = bound(m1_y, v2_y, m2_y);
526                 v2_z = bound(m1_z, v2_z, m2_z);
527                 v2 = to.origin;
528                 c = vlen(v2 - v1);
529         }
530         else
531                 c = vlen(to.origin - from.origin);
532
533         if (from.wp31mincost < c) return;
534         if (from.wp30mincost < c) {from.wp31 = to;from.wp31mincost = c;return;} from.wp31 = from.wp30;from.wp31mincost = from.wp30mincost;
535         if (from.wp29mincost < c) {from.wp30 = to;from.wp30mincost = c;return;} from.wp30 = from.wp29;from.wp30mincost = from.wp29mincost;
536         if (from.wp28mincost < c) {from.wp29 = to;from.wp29mincost = c;return;} from.wp29 = from.wp28;from.wp29mincost = from.wp28mincost;
537         if (from.wp27mincost < c) {from.wp28 = to;from.wp28mincost = c;return;} from.wp28 = from.wp27;from.wp28mincost = from.wp27mincost;
538         if (from.wp26mincost < c) {from.wp27 = to;from.wp27mincost = c;return;} from.wp27 = from.wp26;from.wp27mincost = from.wp26mincost;
539         if (from.wp25mincost < c) {from.wp26 = to;from.wp26mincost = c;return;} from.wp26 = from.wp25;from.wp26mincost = from.wp25mincost;
540         if (from.wp24mincost < c) {from.wp25 = to;from.wp25mincost = c;return;} from.wp25 = from.wp24;from.wp25mincost = from.wp24mincost;
541         if (from.wp23mincost < c) {from.wp24 = to;from.wp24mincost = c;return;} from.wp24 = from.wp23;from.wp24mincost = from.wp23mincost;
542         if (from.wp22mincost < c) {from.wp23 = to;from.wp23mincost = c;return;} from.wp23 = from.wp22;from.wp23mincost = from.wp22mincost;
543         if (from.wp21mincost < c) {from.wp22 = to;from.wp22mincost = c;return;} from.wp22 = from.wp21;from.wp22mincost = from.wp21mincost;
544         if (from.wp20mincost < c) {from.wp21 = to;from.wp21mincost = c;return;} from.wp21 = from.wp20;from.wp21mincost = from.wp20mincost;
545         if (from.wp19mincost < c) {from.wp20 = to;from.wp20mincost = c;return;} from.wp20 = from.wp19;from.wp20mincost = from.wp19mincost;
546         if (from.wp18mincost < c) {from.wp19 = to;from.wp19mincost = c;return;} from.wp19 = from.wp18;from.wp19mincost = from.wp18mincost;
547         if (from.wp17mincost < c) {from.wp18 = to;from.wp18mincost = c;return;} from.wp18 = from.wp17;from.wp18mincost = from.wp17mincost;
548         if (from.wp16mincost < c) {from.wp17 = to;from.wp17mincost = c;return;} from.wp17 = from.wp16;from.wp17mincost = from.wp16mincost;
549         if (from.wp15mincost < c) {from.wp16 = to;from.wp16mincost = c;return;} from.wp16 = from.wp15;from.wp16mincost = from.wp15mincost;
550         if (from.wp14mincost < c) {from.wp15 = to;from.wp15mincost = c;return;} from.wp15 = from.wp14;from.wp15mincost = from.wp14mincost;
551         if (from.wp13mincost < c) {from.wp14 = to;from.wp14mincost = c;return;} from.wp14 = from.wp13;from.wp14mincost = from.wp13mincost;
552         if (from.wp12mincost < c) {from.wp13 = to;from.wp13mincost = c;return;} from.wp13 = from.wp12;from.wp13mincost = from.wp12mincost;
553         if (from.wp11mincost < c) {from.wp12 = to;from.wp12mincost = c;return;} from.wp12 = from.wp11;from.wp12mincost = from.wp11mincost;
554         if (from.wp10mincost < c) {from.wp11 = to;from.wp11mincost = c;return;} from.wp11 = from.wp10;from.wp11mincost = from.wp10mincost;
555         if (from.wp09mincost < c) {from.wp10 = to;from.wp10mincost = c;return;} from.wp10 = from.wp09;from.wp10mincost = from.wp09mincost;
556         if (from.wp08mincost < c) {from.wp09 = to;from.wp09mincost = c;return;} from.wp09 = from.wp08;from.wp09mincost = from.wp08mincost;
557         if (from.wp07mincost < c) {from.wp08 = to;from.wp08mincost = c;return;} from.wp08 = from.wp07;from.wp08mincost = from.wp07mincost;
558         if (from.wp06mincost < c) {from.wp07 = to;from.wp07mincost = c;return;} from.wp07 = from.wp06;from.wp07mincost = from.wp06mincost;
559         if (from.wp05mincost < c) {from.wp06 = to;from.wp06mincost = c;return;} from.wp06 = from.wp05;from.wp06mincost = from.wp05mincost;
560         if (from.wp04mincost < c) {from.wp05 = to;from.wp05mincost = c;return;} from.wp05 = from.wp04;from.wp05mincost = from.wp04mincost;
561         if (from.wp03mincost < c) {from.wp04 = to;from.wp04mincost = c;return;} from.wp04 = from.wp03;from.wp04mincost = from.wp03mincost;
562         if (from.wp02mincost < c) {from.wp03 = to;from.wp03mincost = c;return;} from.wp03 = from.wp02;from.wp03mincost = from.wp02mincost;
563         if (from.wp01mincost < c) {from.wp02 = to;from.wp02mincost = c;return;} from.wp02 = from.wp01;from.wp02mincost = from.wp01mincost;
564         if (from.wp00mincost < c) {from.wp01 = to;from.wp01mincost = c;return;} from.wp01 = from.wp00;from.wp01mincost = from.wp00mincost;
565         from.wp00 = to;from.wp00mincost = c;return;
566 };
567
568 // fields you can query using prvm_global server to get some statistics about waypoint linking culling
569 float relink_total, relink_walkculled, relink_pvsculled, relink_lengthculled;
570
571 // relink this spawnfunc_waypoint
572 // (precompile a list of all reachable waypoints from this spawnfunc_waypoint)
573 // (SLOW!)
574 void waypoint_think()
575 {
576         local entity e;
577         local vector sv, sm1, sm2, ev, em1, em2, dv;
578
579         stepheightvec = cvar("sv_stepheight") * '0 0 1';
580         bot_navigation_movemode = ((cvar("bot_navigation_ignoreplayers")) ? MOVE_NOMONSTERS : MOVE_NORMAL);
581
582         //dprint("waypoint_think wpisbox = ", ftos(self.wpisbox), "\n");
583         sm1 = self.origin + self.mins;
584         sm2 = self.origin + self.maxs;
585         for(e = world; (e = find(e, classname, "waypoint")); )
586         {
587                 if (boxesoverlap(self.absmin, self.absmax, e.absmin, e.absmax))
588                 {
589                         waypoint_addlink(self, e);
590                         waypoint_addlink(e, self);
591                 }
592                 else
593                 {
594                         ++relink_total;
595                         if(!checkpvs(self.origin, e))
596                         {
597                                 ++relink_pvsculled;
598                                 continue;
599                         }
600                         sv = e.origin;
601                         sv_x = bound(sm1_x, sv_x, sm2_x);
602                         sv_y = bound(sm1_y, sv_y, sm2_y);
603                         sv_z = bound(sm1_z, sv_z, sm2_z);
604                         ev = self.origin;
605                         em1 = e.origin + e.mins;
606                         em2 = e.origin + e.maxs;
607                         ev_x = bound(em1_x, ev_x, em2_x);
608                         ev_y = bound(em1_y, ev_y, em2_y);
609                         ev_z = bound(em1_z, ev_z, em2_z);
610                         dv = ev - sv;
611                         dv_z = 0;
612                         if (vlen(dv) >= 1050) // max search distance in XY
613                         {
614                                 ++relink_lengthculled;
615                                 continue;
616                         }
617                         navigation_testtracewalk = 0;
618                         if (!self.wpisbox)
619                         {
620                                 tracebox(sv - PL_MIN_z * '0 0 1', PL_MIN, PL_MAX, sv, FALSE, self);
621                                 if (!trace_startsolid)
622                                 {
623                                         //dprint("sv deviation", vtos(trace_endpos - sv), "\n");
624                                         sv = trace_endpos + '0 0 1';
625                                 }
626                         }
627                         if (!e.wpisbox)
628                         {
629                                 tracebox(ev - PL_MIN_z * '0 0 1', PL_MIN, PL_MAX, ev, FALSE, e);
630                                 if (!trace_startsolid)
631                                 {
632                                         //dprint("ev deviation", vtos(trace_endpos - ev), "\n");
633                                         ev = trace_endpos + '0 0 1';
634                                 }
635                         }
636                         //traceline(self.origin, e.origin, FALSE, world);
637                         //if (trace_fraction == 1)
638                         if (!self.wpisbox && tracewalk(self, sv, PL_MIN, PL_MAX, ev, MOVE_NOMONSTERS))
639                                 waypoint_addlink(self, e);
640                         else
641                                 relink_walkculled += 0.5;
642                         if (!e.wpisbox && tracewalk(e, ev, PL_MIN, PL_MAX, sv, MOVE_NOMONSTERS))
643                                 waypoint_addlink(e, self);
644                         else
645                                 relink_walkculled += 0.5;
646                 }
647         }
648         navigation_testtracewalk = 0;
649         self.wplinked = TRUE;
650 };
651
652 void waypoint_clearlinks(entity wp)
653 {
654         // clear links to other waypoints
655         local float f;
656         f = 10000000;
657         wp.wp00 = wp.wp01 = wp.wp02 = wp.wp03 = wp.wp04 = wp.wp05 = wp.wp06 = wp.wp07 = world;wp.wp00mincost = wp.wp01mincost = wp.wp02mincost = wp.wp03mincost = wp.wp04mincost = wp.wp05mincost = wp.wp06mincost = wp.wp07mincost = f;
658         wp.wp08 = wp.wp09 = wp.wp10 = wp.wp11 = wp.wp12 = wp.wp13 = wp.wp14 = wp.wp15 = world;wp.wp08mincost = wp.wp09mincost = wp.wp10mincost = wp.wp11mincost = wp.wp12mincost = wp.wp13mincost = wp.wp14mincost = wp.wp15mincost = f;
659         wp.wp16 = wp.wp17 = wp.wp18 = wp.wp19 = wp.wp20 = wp.wp21 = wp.wp22 = wp.wp23 = world;wp.wp16mincost = wp.wp17mincost = wp.wp18mincost = wp.wp19mincost = wp.wp20mincost = wp.wp21mincost = wp.wp22mincost = wp.wp23mincost = f;
660         wp.wp24 = wp.wp25 = wp.wp26 = wp.wp27 = wp.wp28 = wp.wp29 = wp.wp30 = wp.wp31 = world;wp.wp24mincost = wp.wp25mincost = wp.wp26mincost = wp.wp27mincost = wp.wp28mincost = wp.wp29mincost = wp.wp30mincost = wp.wp31mincost = f;
661         wp.wplinked = FALSE;
662 };
663
664 // tell a spawnfunc_waypoint to relink
665 void waypoint_schedulerelink(entity wp)
666 {
667         if (wp == world)
668                 return;
669         // TODO: add some sort of visible box in edit mode for box waypoints
670         if (cvar("g_waypointeditor"))
671         {
672                 local vector m1, m2;
673                 m1 = wp.mins;
674                 m2 = wp.maxs;
675                 setmodel(wp, "models/runematch/rune.mdl"); wp.effects = EF_LOWPRECISION;
676                 setsize(wp, m1, m2);
677                 if (wp.wpflags & WAYPOINTFLAG_ITEM)
678                         wp.colormod = '1 0 0';
679                 else if (wp.wpflags & WAYPOINTFLAG_GENERATED)
680                         wp.colormod = '1 1 0';
681                 else
682                         wp.colormod = '1 1 1';
683         }
684         else
685                 wp.model = "";
686         wp.wpisbox = vlen(wp.size) > 0;
687         wp.enemy = world;
688         if (!(wp.wpflags & WAYPOINTFLAG_PERSONAL))
689                 wp.owner = world;
690         if (!(wp.wpflags & WAYPOINTFLAG_NORELINK))
691                 waypoint_clearlinks(wp);
692         // schedule an actual relink on next frame
693         wp.think = waypoint_think;
694         wp.nextthink = time;
695         wp.effects = EF_LOWPRECISION;
696 }
697
698 // create a new spawnfunc_waypoint and automatically link it to other waypoints, and link
699 // them back to it as well
700 // (suitable for spawnfunc_waypoint editor)
701 entity waypoint_spawn(vector m1, vector m2, float f)
702 {
703         local entity w;
704         local vector org;
705         w = find(world, classname, "waypoint");
706
707         if not(f & WAYPOINTFLAG_PERSONAL)
708         while (w)
709         {
710                 // if a matching spawnfunc_waypoint already exists, don't add a duplicate
711                 if (boxesoverlap(m1, m2, w.absmin, w.absmax))
712                         return w;
713                 w = find(w, classname, "waypoint");
714         }
715
716         w = spawn();
717         w.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_PLAYERCLIP;
718         w.classname = "waypoint";
719         w.wpflags = f;
720         setorigin(w, (m1 + m2) * 0.5);
721         setsize(w, m1 - w.origin, m2 - w.origin);
722         if (vlen(w.size) > 0)
723                 w.wpisbox = TRUE;
724
725         if(!(f & WAYPOINTFLAG_GENERATED))
726         if(!w.wpisbox)
727         {
728                 traceline(w.origin + '0 0 1', w.origin + PL_MIN_z * '0 0 1' - '0 0 1', MOVE_NOMONSTERS, w);
729                 if (trace_fraction < 1)
730                         setorigin(w, trace_endpos - PL_MIN_z * '0 0 1');
731
732                 // check if the start position is stuck
733                 tracebox(w.origin, PL_MIN + '-1 -1 -1', PL_MAX + '1 1 1', w.origin, MOVE_NOMONSTERS, w);
734                 if (trace_startsolid)
735                 {
736                         org = w.origin + '0 0 26';
737                         tracebox(org, PL_MIN, PL_MAX, w.origin, MOVE_WORLDONLY, w);
738                         if(trace_startsolid)
739                         {
740                                 org = w.origin + '2 2 2';
741                                 tracebox(org, PL_MIN, PL_MAX, w.origin, MOVE_WORLDONLY, w);
742                                 if(trace_startsolid)
743                                 {
744                                         org = w.origin + '-2 -2 2';
745                                         tracebox(org, PL_MIN, PL_MAX, w.origin, MOVE_WORLDONLY, w);
746                                         if(trace_startsolid)
747                                         {
748                                                 org = w.origin + '-2 2 2';
749                                                 tracebox(org, PL_MIN, PL_MAX, w.origin, MOVE_WORLDONLY, w);
750                                                 if(trace_startsolid)
751                                                 {
752                                                         org = w.origin + '2 -2 2';
753                                                         tracebox(org, PL_MIN, PL_MAX, w.origin, MOVE_WORLDONLY, w);
754                                                         if(trace_startsolid)
755                                                         {
756                                                                 // this WP is in solid, refuse it
757                                                                 dprint("Killed a waypoint that was stuck in solid at ", vtos(org), "\n");
758                                                                 remove(w);
759                                                                 return world;
760                                                         }
761                                                 }
762                                         }
763                                 }
764                         }
765                         setorigin(w, org * 0.05 + trace_endpos * 0.95); // don't trust the trace fully
766                 }
767
768                 tracebox(w.origin, PL_MIN, PL_MAX, w.origin - '0 0 128', MOVE_WORLDONLY, w);
769                 if(trace_startsolid)
770                 {
771                         dprint("Killed a waypoint that was stuck in solid ", vtos(w.origin), "\n");
772                         remove(w);
773                         return world;
774                 }
775                 if (!trace_inwater)
776                 {
777                         if(trace_fraction == 1)
778                         {
779                                 dprint("Killed a waypoint that was stuck in air at ", vtos(w.origin), "\n");
780                                 remove(w);
781                                 return world;
782                         }
783                         trace_endpos_z += 0.1; // don't trust the trace fully
784 //                      dprint("Moved waypoint at ", vtos(w.origin), " by ", ftos(vlen(w.origin - trace_endpos)));
785 //                      dprint(" direction: ", vtos((trace_endpos - w.origin)), "\n");
786                         setorigin(w, trace_endpos);
787                 }
788         }
789
790         waypoint_clearlinks(w);
791         //waypoint_schedulerelink(w);
792
793         if (cvar("g_waypointeditor"))
794         {
795                 m1 = w.mins;
796                 m2 = w.maxs;
797                 setmodel(w, "models/runematch/rune.mdl"); w.effects = EF_LOWPRECISION;
798                 setsize(w, m1, m2);
799                 if (w.wpflags & WAYPOINTFLAG_ITEM)
800                         w.colormod = '1 0 0';
801                 else if (w.wpflags & WAYPOINTFLAG_GENERATED)
802                         w.colormod = '1 1 0';
803                 else
804                         w.colormod = '1 1 1';
805         }
806         else
807                 w.model = "";
808
809         return w;
810 };
811
812 // spawnfunc_waypoint map entity
813 void spawnfunc_waypoint()
814 {
815         setorigin(self, self.origin);
816         // schedule a relink after other waypoints have had a chance to spawn
817         waypoint_clearlinks(self);
818         //waypoint_schedulerelink(self);
819 };
820
821 // remove a spawnfunc_waypoint, and schedule all neighbors to relink
822 void waypoint_remove(entity e)
823 {
824         // tell all linked waypoints that they need to relink
825         waypoint_schedulerelink(e.wp00);
826         waypoint_schedulerelink(e.wp01);
827         waypoint_schedulerelink(e.wp02);
828         waypoint_schedulerelink(e.wp03);
829         waypoint_schedulerelink(e.wp04);
830         waypoint_schedulerelink(e.wp05);
831         waypoint_schedulerelink(e.wp06);
832         waypoint_schedulerelink(e.wp07);
833         waypoint_schedulerelink(e.wp08);
834         waypoint_schedulerelink(e.wp09);
835         waypoint_schedulerelink(e.wp10);
836         waypoint_schedulerelink(e.wp11);
837         waypoint_schedulerelink(e.wp12);
838         waypoint_schedulerelink(e.wp13);
839         waypoint_schedulerelink(e.wp14);
840         waypoint_schedulerelink(e.wp15);
841         waypoint_schedulerelink(e.wp16);
842         waypoint_schedulerelink(e.wp17);
843         waypoint_schedulerelink(e.wp18);
844         waypoint_schedulerelink(e.wp19);
845         waypoint_schedulerelink(e.wp20);
846         waypoint_schedulerelink(e.wp21);
847         waypoint_schedulerelink(e.wp22);
848         waypoint_schedulerelink(e.wp23);
849         waypoint_schedulerelink(e.wp24);
850         waypoint_schedulerelink(e.wp25);
851         waypoint_schedulerelink(e.wp26);
852         waypoint_schedulerelink(e.wp27);
853         waypoint_schedulerelink(e.wp28);
854         waypoint_schedulerelink(e.wp29);
855         waypoint_schedulerelink(e.wp30);
856         waypoint_schedulerelink(e.wp31);
857         // and now remove the spawnfunc_waypoint
858         remove(e);
859 };
860
861 // empties the map of waypoints
862 void waypoint_removeall()
863 {
864         local entity head, next;
865         head = findchain(classname, "waypoint");
866         while (head)
867         {
868                 next = head.chain;
869                 remove(head);
870                 head = next;
871         }
872 };
873
874 // tell all waypoints to relink
875 // (is this useful at all?)
876 void waypoint_schedulerelinkall()
877 {
878         local entity head;
879         relink_total = relink_walkculled = relink_pvsculled = relink_lengthculled = 0;
880         head = findchain(classname, "waypoint");
881         while (head)
882         {
883                 waypoint_schedulerelink(head);
884                 head = head.chain;
885         }
886 };
887
888 // Load waypoint links from file
889 float botframe_cachedwaypointlinks;
890 float waypoint_load_links()
891 {
892         local string filename, s;
893         local float file, tokens, c, found;
894         local entity wp_from, wp_to;
895         local vector wp_to_pos, wp_from_pos;
896         filename = strcat("maps/", mapname);
897         filename = strcat(filename, ".waypoints.cache");
898         file = fopen(filename, FILE_READ);
899
900         if (file < 0)
901         {
902                 dprint("waypoint links load from ");
903                 dprint(filename);
904                 dprint(" failed\n");
905                 return FALSE;
906         }
907
908         while (1)
909         {
910                 s = fgets(file);
911                 if (!s)
912                         break;
913
914                 tokens = tokenizebyseparator(s, "*");
915
916                 if (tokens!=2)
917                 {
918                         // bad file format
919                         fclose(file);
920                         return FALSE;
921                 }
922
923                 wp_from_pos     = stov(argv(0));
924                 wp_to_pos       = stov(argv(1));
925
926                 // Search "from" waypoint
927                 if(wp_from.origin!=wp_from_pos)
928                 {
929                         wp_from = findradius(wp_from_pos, 1);
930                         found = FALSE;
931                         while(wp_from)
932                         {
933                                 if(vlen(wp_from.origin-wp_from_pos)<1)
934                                 if(wp_from.classname == "waypoint")
935                                 {
936                                         found = TRUE;
937                                         break;
938                                 }
939                                 wp_from = wp_from.chain;
940                         }
941
942                         if(!found)
943                         {
944                                 // can't find that waypoint
945                                 fclose(file);
946                                 return FALSE;
947                         }
948                 }
949
950                 // Search "to" waypoint
951                 wp_to = findradius(wp_to_pos, 1);
952                 found = FALSE;
953                 while(wp_to)
954                 {
955                         if(vlen(wp_to.origin-wp_to_pos)<1)
956                         if(wp_to.classname == "waypoint")
957                         {
958                                 found = TRUE;
959                                 break;
960                         }
961                         wp_to = wp_to.chain;
962                 }
963
964                 if(!found)
965                 {
966                         // can't find that waypoint
967                         fclose(file);
968                         return FALSE;
969                 }
970
971                 ++c;
972                 waypoint_addlink(wp_from, wp_to);
973         }
974
975         fclose(file);
976
977         dprint("loaded ");
978         dprint(ftos(c));
979         dprint(" waypoint links from maps/");
980         dprint(mapname);
981         dprint(".waypoints.cache\n");
982
983         botframe_cachedwaypointlinks = TRUE;
984         return TRUE;
985 };
986
987 float botframe_loadedforcedlinks;
988 void waypoint_load_links_hardwired()
989 {
990         local string filename, s;
991         local float file, tokens, c, found;
992         local entity wp_from, wp_to;
993         local vector wp_to_pos, wp_from_pos;
994         filename = strcat("maps/", mapname);
995         filename = strcat(filename, ".waypoints.hardwired");
996         file = fopen(filename, FILE_READ);
997
998         botframe_loadedforcedlinks = TRUE;
999
1000         if (file < 0)
1001         {
1002                 dprint("waypoint links load from ");
1003                 dprint(filename);
1004                 dprint(" failed\n");
1005                 return;
1006         }
1007
1008         for (;;)
1009         {
1010                 s = fgets(file);
1011                 if (!s)
1012                         break;
1013
1014                 if(substring(s, 0, 2)=="//")
1015                         continue;
1016
1017                 if(substring(s, 0, 1)=="#")
1018                         continue;
1019
1020                 tokens = tokenizebyseparator(s, "*");
1021
1022                 if (tokens!=2)
1023                         continue;
1024
1025                 wp_from_pos     = stov(argv(0));
1026                 wp_to_pos       = stov(argv(1));
1027
1028                 // Search "from" waypoint
1029                 if(wp_from.origin!=wp_from_pos)
1030                 {
1031                         wp_from = findradius(wp_from_pos, 1);
1032                         found = FALSE;
1033                         while(wp_from)
1034                         {
1035                                 if(vlen(wp_from.origin-wp_from_pos)<1)
1036                                 if(wp_from.classname == "waypoint")
1037                                 {
1038                                         found = TRUE;
1039                                         break;
1040                                 }
1041                                 wp_from = wp_from.chain;
1042                         }
1043
1044                         if(!found)
1045                         {
1046                                 print(strcat("NOTICE: Can not find waypoint at ", vtos(wp_from_pos), ". Path skipped\n"));
1047                                 continue;
1048                         }
1049                 }
1050
1051                 // Search "to" waypoint
1052                 wp_to = findradius(wp_to_pos, 1);
1053                 found = FALSE;
1054                 while(wp_to)
1055                 {
1056                         if(vlen(wp_to.origin-wp_to_pos)<1)
1057                         if(wp_to.classname == "waypoint")
1058                         {
1059                                 found = TRUE;
1060                                 break;
1061                         }
1062                         wp_to = wp_to.chain;
1063                 }
1064
1065                 if(!found)
1066                 {
1067                         print(strcat("NOTICE: Can not find waypoint at ", vtos(wp_to_pos), ". Path skipped\n"));
1068                         continue;
1069                 }
1070
1071                 ++c;
1072                 waypoint_addlink(wp_from, wp_to);
1073         }
1074
1075         fclose(file);
1076
1077         dprint("loaded ");
1078         dprint(ftos(c));
1079         dprint(" waypoint links from maps/");
1080         dprint(mapname);
1081         dprint(".waypoints.hardwired\n");
1082 };
1083
1084
1085 // Save all waypoint links to a file
1086 void waypoint_save_links()
1087 {
1088         local string filename, s;
1089         local float file, c, i;
1090         local entity w, link;
1091         filename = strcat("maps/", mapname);
1092         filename = strcat(filename, ".waypoints.cache");
1093         file = fopen(filename, FILE_WRITE);
1094         if (file < 0)
1095         {
1096                 print("waypoint links save to ");
1097                 print(filename);
1098                 print(" failed\n");
1099         }
1100         c = 0;
1101         w = findchain(classname, "waypoint");
1102         while (w)
1103         {
1104                 for(i=0;i<32;++i)
1105                 {
1106                         // :S
1107                         switch(i)
1108                         {
1109                                 //      for i in $(seq -w 0 31); do echo "case $i:link = w.wp$i; break;"; done;
1110                                 case 00:link = w.wp00; break;
1111                                 case 01:link = w.wp01; break;
1112                                 case 02:link = w.wp02; break;
1113                                 case 03:link = w.wp03; break;
1114                                 case 04:link = w.wp04; break;
1115                                 case 05:link = w.wp05; break;
1116                                 case 06:link = w.wp06; break;
1117                                 case 07:link = w.wp07; break;
1118                                 case 08:link = w.wp08; break;
1119                                 case 09:link = w.wp09; break;
1120                                 case 10:link = w.wp10; break;
1121                                 case 11:link = w.wp11; break;
1122                                 case 12:link = w.wp12; break;
1123                                 case 13:link = w.wp13; break;
1124                                 case 14:link = w.wp14; break;
1125                                 case 15:link = w.wp15; break;
1126                                 case 16:link = w.wp16; break;
1127                                 case 17:link = w.wp17; break;
1128                                 case 18:link = w.wp18; break;
1129                                 case 19:link = w.wp19; break;
1130                                 case 20:link = w.wp20; break;
1131                                 case 21:link = w.wp21; break;
1132                                 case 22:link = w.wp22; break;
1133                                 case 23:link = w.wp23; break;
1134                                 case 24:link = w.wp24; break;
1135                                 case 25:link = w.wp25; break;
1136                                 case 26:link = w.wp26; break;
1137                                 case 27:link = w.wp27; break;
1138                                 case 28:link = w.wp28; break;
1139                                 case 29:link = w.wp29; break;
1140                                 case 30:link = w.wp30; break;
1141                                 case 31:link = w.wp31; break;
1142                         }
1143
1144                         if(link==world)
1145                                 continue;
1146
1147                         s = strcat(vtos(w.origin), "*", vtos(link.origin), "\n");
1148                         fputs(file, s);
1149                         ++c;
1150                 }
1151                 w = w.chain;
1152         }
1153         fclose(file);
1154         botframe_cachedwaypointlinks = TRUE;
1155
1156         print("saved ");
1157         print(ftos(c));
1158         print(" waypoints links to maps/");
1159         print(mapname);
1160         print(".waypoints.cache\n");
1161 };
1162
1163 // save waypoints to gamedir/data/maps/mapname.waypoints
1164 void waypoint_saveall()
1165 {
1166         local string filename, s;
1167         local float file, c;
1168         local entity w;
1169         filename = strcat("maps/", mapname);
1170         filename = strcat(filename, ".waypoints");
1171         file = fopen(filename, FILE_WRITE);
1172         if (file >= 0)
1173         {
1174                 c = 0;
1175                 w = findchain(classname, "waypoint");
1176                 while (w)
1177                 {
1178                         if (!(w.wpflags & WAYPOINTFLAG_GENERATED))
1179                         {
1180                                 s = strcat(vtos(w.origin + w.mins), "\n");
1181                                 s = strcat(s, vtos(w.origin + w.maxs));
1182                                 s = strcat(s, "\n");
1183                                 s = strcat(s, ftos(w.wpflags));
1184                                 s = strcat(s, "\n");
1185                                 fputs(file, s);
1186                                 c = c + 1;
1187                         }
1188                         w = w.chain;
1189                 }
1190                 fclose(file);
1191                 bprint("saved ");
1192                 bprint(ftos(c));
1193                 bprint(" waypoints to maps/");
1194                 bprint(mapname);
1195                 bprint(".waypoints\n");
1196         }
1197         else
1198         {
1199                 bprint("waypoint save to ");
1200                 bprint(filename);
1201                 bprint(" failed\n");
1202         }
1203         waypoint_save_links();
1204         botframe_loadedforcedlinks = FALSE;
1205 };
1206
1207 // load waypoints from file
1208 float waypoint_loadall()
1209 {
1210         local string filename, s;
1211         local float file, cwp, cwb, fl;
1212         local vector m1, m2;
1213         cwp = 0;
1214         cwb = 0;
1215         filename = strcat("maps/", mapname);
1216         filename = strcat(filename, ".waypoints");
1217         file = fopen(filename, FILE_READ);
1218         if (file >= 0)
1219         {
1220                 while (1)
1221                 {
1222                         s = fgets(file);
1223                         if (!s)
1224                                 break;
1225                         m1 = stov(s);
1226                         s = fgets(file);
1227                         if (!s)
1228                                 break;
1229                         m2 = stov(s);
1230                         s = fgets(file);
1231                         if (!s)
1232                                 break;
1233                         fl = stof(s);
1234                         waypoint_spawn(m1, m2, fl);
1235                         if (m1 == m2)
1236                                 cwp = cwp + 1;
1237                         else
1238                                 cwb = cwb + 1;
1239                 }
1240                 fclose(file);
1241                 dprint("loaded ");
1242                 dprint(ftos(cwp));
1243                 dprint(" waypoints and ");
1244                 dprint(ftos(cwb));
1245                 dprint(" wayboxes from maps/");
1246                 dprint(mapname);
1247                 dprint(".waypoints\n");
1248         }
1249         else
1250         {
1251                 dprint("waypoint load from ");
1252                 dprint(filename);
1253                 dprint(" failed\n");
1254         }
1255         return cwp + cwb;
1256 };
1257
1258 void waypoint_spawnforitem_force(entity e, vector org)
1259 {
1260         local entity w;
1261
1262         // Fix the waypoint altitude if necessary
1263         traceline(org, org + '0 0 -65535', TRUE, e);
1264         if(
1265                 org_z - trace_endpos_z > PL_MAX_z - PL_MIN_z + 10 // If middle of entiy is above player heigth
1266                 || org_z - trace_endpos_z < (PL_MAX_z - PL_MIN_z) * 0.5 // or below half player height
1267         )
1268                 org_z = trace_endpos_z + PL_MAX_z - PL_MIN_z;
1269
1270         // don't spawn an item spawnfunc_waypoint if it already exists
1271         w = findchain(classname, "waypoint");
1272         while (w)
1273         {
1274                 if (w.wpisbox)
1275                 {
1276                         if (boxesoverlap(org, org, w.absmin, w.absmax))
1277                         {
1278                                 e.nearestwaypoint = w;
1279                                 return;
1280                         }
1281                 }
1282                 else
1283                 {
1284                         if (vlen(w.origin - org) < 16)
1285                         {
1286                                 e.nearestwaypoint = w;
1287                                 return;
1288                         }
1289                 }
1290                 w = w.chain;
1291         }
1292         e.nearestwaypoint = waypoint_spawn(org, org, WAYPOINTFLAG_GENERATED | WAYPOINTFLAG_ITEM);
1293 }
1294
1295 void waypoint_spawnforitem(entity e)
1296 {
1297         if(!bot_waypoints_for_items)
1298                 return;
1299
1300         waypoint_spawnforitem_force(e, e.origin);
1301 };
1302
1303 void waypoint_spawnforteleporter(entity e, vector destination, float timetaken)
1304 {
1305         local entity w;
1306         local entity dw;
1307         w = waypoint_spawn(e.absmin, e.absmax, WAYPOINTFLAG_GENERATED | WAYPOINTFLAG_TELEPORT | WAYPOINTFLAG_NORELINK);
1308         dw = waypoint_spawn(destination, destination, WAYPOINTFLAG_GENERATED);
1309         // one way link to the destination
1310         w.wp00 = dw;
1311         w.wp00mincost = timetaken; // this is just for jump pads
1312         // the teleporter's nearest spawnfunc_waypoint is this one
1313         // (teleporters are not goals, so this is probably useless)
1314         e.nearestwaypoint = w;
1315         e.nearestwaypointtimeout = time + 1000000000;
1316 };
1317
1318 entity waypoint_spawnpersonal(vector position)
1319 {
1320         entity w;
1321
1322         // drop the waypoint to a proper location:
1323         //   first move it up by a player height
1324         //   then move it down to hit the floor with player bbox size
1325         traceline(position, position + '0 0 1' * (PL_MAX_z - PL_MIN_z), MOVE_NOMONSTERS, world);
1326         tracebox(trace_endpos, PL_MIN, PL_MAX, trace_endpos + '0 0 -1024', MOVE_NOMONSTERS, world);
1327         if(trace_fraction < 1)
1328                 position = trace_endpos;
1329
1330         w = waypoint_spawn(position, position, WAYPOINTFLAG_GENERATED | WAYPOINTFLAG_PERSONAL);
1331         w.nearestwaypoint = world;
1332         w.nearestwaypointtimeout = 0;
1333         w.owner = self;
1334
1335         waypoint_schedulerelink(w);
1336
1337         return w;
1338 };
1339
1340 /////////////////////////////////////////////////////////////////////////////
1341 // goal stack
1342 /////////////////////////////////////////////////////////////////////////////
1343
1344 // completely empty the goal stack, used when deciding where to go
1345 void navigation_clearroute()
1346 {
1347         //print("bot ", etos(self), " clear\n");
1348         self.navigation_hasgoals = FALSE;
1349         self.goalcurrent = world;
1350         self.goalstack01 = world;
1351         self.goalstack02 = world;
1352         self.goalstack03 = world;
1353         self.goalstack04 = world;
1354         self.goalstack05 = world;
1355         self.goalstack06 = world;
1356         self.goalstack07 = world;
1357         self.goalstack08 = world;
1358         self.goalstack09 = world;
1359         self.goalstack10 = world;
1360         self.goalstack11 = world;
1361         self.goalstack12 = world;
1362         self.goalstack13 = world;
1363         self.goalstack14 = world;
1364         self.goalstack15 = world;
1365         self.goalstack16 = world;
1366         self.goalstack17 = world;
1367         self.goalstack18 = world;
1368         self.goalstack19 = world;
1369         self.goalstack20 = world;
1370         self.goalstack21 = world;
1371         self.goalstack22 = world;
1372         self.goalstack23 = world;
1373         self.goalstack24 = world;
1374         self.goalstack25 = world;
1375         self.goalstack26 = world;
1376         self.goalstack27 = world;
1377         self.goalstack28 = world;
1378         self.goalstack29 = world;
1379         self.goalstack30 = world;
1380         self.goalstack31 = world;
1381 };
1382
1383 // add a new goal at the beginning of the stack
1384 // (in other words: add a new prerequisite before going to the later goals)
1385 void navigation_pushroute(entity e)
1386 {
1387         //print("bot ", etos(self), " push ", etos(e), "\n");
1388         self.goalstack31 = self.goalstack30;
1389         self.goalstack30 = self.goalstack29;
1390         self.goalstack29 = self.goalstack28;
1391         self.goalstack28 = self.goalstack27;
1392         self.goalstack27 = self.goalstack26;
1393         self.goalstack26 = self.goalstack25;
1394         self.goalstack25 = self.goalstack24;
1395         self.goalstack24 = self.goalstack23;
1396         self.goalstack23 = self.goalstack22;
1397         self.goalstack22 = self.goalstack21;
1398         self.goalstack21 = self.goalstack20;
1399         self.goalstack20 = self.goalstack19;
1400         self.goalstack19 = self.goalstack18;
1401         self.goalstack18 = self.goalstack17;
1402         self.goalstack17 = self.goalstack16;
1403         self.goalstack16 = self.goalstack15;
1404         self.goalstack15 = self.goalstack14;
1405         self.goalstack14 = self.goalstack13;
1406         self.goalstack13 = self.goalstack12;
1407         self.goalstack12 = self.goalstack11;
1408         self.goalstack11 = self.goalstack10;
1409         self.goalstack10 = self.goalstack09;
1410         self.goalstack09 = self.goalstack08;
1411         self.goalstack08 = self.goalstack07;
1412         self.goalstack07 = self.goalstack06;
1413         self.goalstack06 = self.goalstack05;
1414         self.goalstack05 = self.goalstack04;
1415         self.goalstack04 = self.goalstack03;
1416         self.goalstack03 = self.goalstack02;
1417         self.goalstack02 = self.goalstack01;
1418         self.goalstack01 = self.goalcurrent;
1419         self.goalcurrent = e;
1420 };
1421
1422 // remove first goal from stack
1423 // (in other words: remove a prerequisite for reaching the later goals)
1424 // (used when a spawnfunc_waypoint is reached)
1425 void navigation_poproute()
1426 {
1427         //print("bot ", etos(self), " pop\n");
1428         self.goalcurrent = self.goalstack01;
1429         self.goalstack01 = self.goalstack02;
1430         self.goalstack02 = self.goalstack03;
1431         self.goalstack03 = self.goalstack04;
1432         self.goalstack04 = self.goalstack05;
1433         self.goalstack05 = self.goalstack06;
1434         self.goalstack06 = self.goalstack07;
1435         self.goalstack07 = self.goalstack08;
1436         self.goalstack08 = self.goalstack09;
1437         self.goalstack09 = self.goalstack10;
1438         self.goalstack10 = self.goalstack11;
1439         self.goalstack11 = self.goalstack12;
1440         self.goalstack12 = self.goalstack13;
1441         self.goalstack13 = self.goalstack14;
1442         self.goalstack14 = self.goalstack15;
1443         self.goalstack15 = self.goalstack16;
1444         self.goalstack16 = self.goalstack17;
1445         self.goalstack17 = self.goalstack18;
1446         self.goalstack18 = self.goalstack19;
1447         self.goalstack19 = self.goalstack20;
1448         self.goalstack20 = self.goalstack21;
1449         self.goalstack21 = self.goalstack22;
1450         self.goalstack22 = self.goalstack23;
1451         self.goalstack23 = self.goalstack24;
1452         self.goalstack24 = self.goalstack25;
1453         self.goalstack25 = self.goalstack26;
1454         self.goalstack26 = self.goalstack27;
1455         self.goalstack27 = self.goalstack28;
1456         self.goalstack28 = self.goalstack29;
1457         self.goalstack29 = self.goalstack30;
1458         self.goalstack30 = self.goalstack31;
1459         self.goalstack31 = world;
1460 };
1461
1462 // find the spawnfunc_waypoint near a dynamic goal such as a dropped weapon
1463 entity navigation_findnearestwaypoint(entity ent, float walkfromwp)
1464 {
1465         local entity waylist, w, best;
1466         local float dist, bestdist;
1467         local vector v, org, pm1, pm2;
1468         pm1 = ent.origin + PL_MIN;
1469         pm2 = ent.origin + PL_MAX;
1470         waylist = findchain(classname, "waypoint");
1471
1472         // do two scans, because box test is cheaper
1473         w = waylist;
1474         while (w)
1475         {
1476                 // if object is touching spawnfunc_waypoint
1477                 if(w != ent)
1478                         if (boxesoverlap(pm1, pm2, w.absmin, w.absmax))
1479                                 return w;
1480                 w = w.chain;
1481         }
1482
1483         org = ent.origin + (ent.mins_z - PL_MIN_z) * '0 0 1';
1484         if(ent.tag_entity)
1485                 org = org + ent.tag_entity.origin;
1486         if (navigation_testtracewalk)
1487                 te_plasmaburn(org);
1488
1489         best = world;
1490         bestdist = 1050;
1491
1492         // box check failed, try walk
1493         w = waylist;
1494         while (w)
1495         {
1496                 // if object can walk from spawnfunc_waypoint
1497                 if(w != ent)
1498                 {
1499                         if (w.wpisbox)
1500                         {
1501                                 local vector wm1, wm2;
1502                                 wm1 = w.origin + w.mins;
1503                                 wm2 = w.origin + w.maxs;
1504                                 v_x = bound(wm1_x, org_x, wm2_x);
1505                                 v_y = bound(wm1_y, org_y, wm2_y);
1506                                 v_z = bound(wm1_z, org_z, wm2_z);
1507                         }
1508                         else
1509                                 v = w.origin;
1510                         dist = vlen(v - org);
1511                         if (bestdist > dist)
1512                         {
1513                                 traceline(v, org, TRUE, ent);
1514                                 if (trace_fraction == 1)
1515                                 {
1516                                         if (walkfromwp)
1517                                         {
1518                                                 //print("^1can I reach ", vtos(org), " from ", vtos(v), "?\n");
1519                                                 if (tracewalk(ent, v, PL_MIN, PL_MAX, org, bot_navigation_movemode))
1520                                                 {
1521                                                         bestdist = dist;
1522                                                         best = w;
1523                                                 }
1524                                         }
1525                                         else
1526                                         {
1527                                                 if (tracewalk(ent, org, PL_MIN, PL_MAX, v, bot_navigation_movemode))
1528                                                 {
1529                                                         bestdist = dist;
1530                                                         best = w;
1531                                                 }
1532                                         }
1533                                 }
1534                         }
1535                 }
1536                 w = w.chain;
1537         }
1538         return best;
1539 }
1540
1541 // finds the waypoints near the bot initiating a navigation query
1542 float navigation_markroutes_nearestwaypoints(entity waylist, float maxdist)
1543 {
1544         local entity head;
1545         local vector v, m1, m2, diff;
1546         local float c;
1547 //      navigation_testtracewalk = TRUE;
1548         c = 0;
1549         head = waylist;
1550         while (head)
1551         {
1552                 if (!head.wpconsidered)
1553                 {
1554                         if (head.wpisbox)
1555                         {
1556                                 m1 = head.origin + head.mins;
1557                                 m2 = head.origin + head.maxs;
1558                                 v = self.origin;
1559                                 v_x = bound(m1_x, v_x, m2_x);
1560                                 v_y = bound(m1_y, v_y, m2_y);
1561                                 v_z = bound(m1_z, v_z, m2_z);
1562                         }
1563                         else
1564                                 v = head.origin;
1565                         diff = v - self.origin;
1566                         diff_z = max(0, diff_z);
1567                         if (vlen(diff) < maxdist)
1568                         {
1569                                 head.wpconsidered = TRUE;
1570                                 if (tracewalk(self, self.origin, self.mins, self.maxs, v, bot_navigation_movemode))
1571                                 {
1572                                         head.wpnearestpoint = v;
1573                                         head.wpcost = vlen(v - self.origin) + head.dmg;
1574                                         head.wpfire = 1;
1575                                         head.enemy = world;
1576                                         c = c + 1;
1577                                 }
1578                         }
1579                 }
1580                 head = head.chain;
1581         }
1582         //navigation_testtracewalk = FALSE;
1583         return c;
1584 }
1585
1586 // updates a path link if a spawnfunc_waypoint link is better than the current one
1587 void navigation_markroutes_checkwaypoint(entity w, entity wp, float cost2, vector p)
1588 {
1589         local vector m1;
1590         local vector m2;
1591         local vector v;
1592         if (wp.wpisbox)
1593         {
1594                 m1 = wp.absmin;
1595                 m2 = wp.absmax;
1596                 v_x = bound(m1_x, p_x, m2_x);
1597                 v_y = bound(m1_y, p_y, m2_y);
1598                 v_z = bound(m1_z, p_z, m2_z);
1599         }
1600         else
1601                 v = wp.origin;
1602         cost2 = cost2 + vlen(v - p);
1603         if (wp.wpcost > cost2)
1604         {
1605                 wp.wpcost = cost2;
1606                 wp.enemy = w;
1607                 wp.wpfire = 1;
1608                 wp.wpnearestpoint = v;
1609         }
1610 };
1611
1612 // queries the entire spawnfunc_waypoint network for pathes leading away from the bot
1613 void navigation_markroutes()
1614 {
1615         local entity w, wp, waylist;
1616         local float searching, cost, cost2;
1617         local vector p;
1618         w = waylist = findchain(classname, "waypoint");
1619         while (w)
1620         {
1621                 w.wpconsidered = FALSE;
1622                 w.wpnearestpoint = '0 0 0';
1623                 w.wpcost = 10000000;
1624                 w.wpfire = 0;
1625                 w.enemy = world;
1626                 w = w.chain;
1627         }
1628
1629         // try a short range search for the nearest waypoints, and expand the search repeatedly if none are found
1630         // as this search is expensive we will use lower values if the bot is on the air
1631         local float i, increment, maxdistance;
1632         if(self.flags & FL_ONGROUND)
1633         {
1634                 increment = 750;
1635                 maxdistance = 50000;
1636         }
1637         else
1638         {
1639                 increment = 500;
1640                 maxdistance = 1500;
1641         }
1642
1643         for(i=increment;!navigation_markroutes_nearestwaypoints(waylist, i)&&i<maxdistance;i+=increment);
1644
1645         searching = TRUE;
1646         while (searching)
1647         {
1648                 searching = FALSE;
1649                 w = waylist;
1650                 while (w)
1651                 {
1652                         if (w.wpfire)
1653                         {
1654                                 searching = TRUE;
1655                                 w.wpfire = 0;
1656                                 cost = w.wpcost;
1657                                 p = w.wpnearestpoint;
1658                                 wp = w.wp00;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp00mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1659                                 wp = w.wp01;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp01mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1660                                 wp = w.wp02;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp02mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1661                                 wp = w.wp03;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp03mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1662                                 wp = w.wp04;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp04mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1663                                 wp = w.wp05;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp05mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1664                                 wp = w.wp06;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp06mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1665                                 wp = w.wp07;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp07mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1666                                 wp = w.wp08;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp08mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1667                                 wp = w.wp09;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp09mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1668                                 wp = w.wp10;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp10mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1669                                 wp = w.wp11;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp11mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1670                                 wp = w.wp12;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp12mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1671                                 wp = w.wp13;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp13mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1672                                 wp = w.wp14;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp14mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1673                                 wp = w.wp15;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp15mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1674                                 wp = w.wp16;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp16mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1675                                 wp = w.wp17;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp17mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1676                                 wp = w.wp18;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp18mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1677                                 wp = w.wp19;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp19mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1678                                 wp = w.wp20;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp20mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1679                                 wp = w.wp21;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp21mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1680                                 wp = w.wp22;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp22mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1681                                 wp = w.wp23;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp23mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1682                                 wp = w.wp24;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp24mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1683                                 wp = w.wp25;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp25mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1684                                 wp = w.wp26;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp26mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1685                                 wp = w.wp27;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp27mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1686                                 wp = w.wp28;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp28mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1687                                 wp = w.wp29;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp29mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1688                                 wp = w.wp30;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp30mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1689                                 wp = w.wp31;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp31mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p);
1690                                 }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
1691                         }
1692                         w = w.chain;
1693                 }
1694         }
1695 };
1696
1697 void navigation_bestgoals_reset()
1698 {
1699         local float i;
1700
1701         bestgoalswindex = 0;
1702         bestgoalsrindex = 0;
1703
1704         for(i=0;i>MAX_BESTGOALS-1;++i)
1705         {
1706                 navigation_bestgoals[i] = world;
1707         }
1708 }
1709
1710 void navigation_add_bestgoal(entity goal)
1711 {
1712         if(bestgoalsrindex>0)
1713         {
1714                 ++bestgoalsrindex;
1715
1716                 if(bestgoalsrindex==MAX_BESTGOALS)
1717                         bestgoalsrindex = 0;
1718         }
1719
1720         if(bestgoalswindex==MAX_BESTGOALS)
1721         {
1722                 bestgoalswindex=0;
1723                 if(bestgoalsrindex==0)
1724                         bestgoalsrindex=1;
1725         }
1726
1727         navigation_bestgoals[bestgoalswindex] = goal;
1728
1729         ++bestgoalswindex;
1730 }
1731
1732 entity navigation_get_bestgoal()
1733 {
1734         local entity ent;
1735
1736         ent = navigation_bestgoals[bestgoalsrindex];
1737         navigation_bestgoals[bestgoalsrindex] = world;
1738
1739         ++bestgoalsrindex;
1740
1741         if(bestgoalsrindex==MAX_BESTGOALS)
1742                 bestgoalsrindex = 0;
1743
1744         return ent;
1745 }
1746
1747 // fields required for jetpack navigation
1748 .entity navigation_jetpack_goal;
1749 .vector navigation_jetpack_point;
1750
1751 // updates the best goal according to a weighted calculation of travel cost and item value of a new proposed item
1752 .void() havocbot_role;
1753 void() havocbot_role_ctf_offense;
1754 void navigation_routerating(entity e, float f, float rangebias)
1755 {
1756         entity nwp;
1757         if (!e)
1758                 return;
1759
1760         // Evaluate path using jetpack
1761         if(g_jetpack)
1762         if(self.items & IT_JETPACK)
1763         if(cvar("bot_ai_navigation_jetpack"))
1764         if(vlen(self.origin - e.origin) > cvar("bot_ai_navigation_jetpack_mindistance"))
1765         {
1766                 vector pointa, pointb;
1767
1768         //      dprint("jetpack ai: evaluating path for ", e.classname,"\n");
1769
1770                 // Point A
1771                 traceline(self.origin, self.origin + '0 0 65535', MOVE_NORMAL, self);
1772                 pointa = trace_endpos - '0 0 1';
1773
1774                 // Point B
1775                 traceline(e.origin, e.origin + '0 0 65535', MOVE_NORMAL, e);
1776                 pointb = trace_endpos - '0 0 1';
1777
1778                 // Can I see these two points from the sky?
1779                 traceline(pointa, pointb, MOVE_NORMAL, self);
1780
1781                 if(trace_fraction==1)
1782                 {
1783                 //      dprint("jetpack ai: can bridge these two points\n");
1784
1785                         // Lower the altitude of these points as much as possible
1786                         local float zdistance, xydistance, cost, t, fuel;
1787                         local vector down, npa, npb;
1788
1789                         down = '0 0 -1' * (PL_MAX_z - PL_MIN_z) * 10;
1790
1791                         do{
1792                                 npa = pointa + down;
1793                                 npb = pointb + down;
1794
1795                                 if(npa_z<=self.absmax_z)
1796                                         break;
1797
1798                                 if(npb_z<=e.absmax_z)
1799                                         break;
1800
1801                                 traceline(npa, npb, MOVE_NORMAL, self);
1802                                 if(trace_fraction==1)
1803                                 {
1804                                         pointa = npa;
1805                                         pointb = npb;
1806                                 }
1807                         }
1808                         while(trace_fraction == 1);
1809
1810
1811                         // Rough estimation of fuel consumption
1812                         // (ignores acceleration and current xyz velocity)
1813                         xydistance = vlen(pointa - pointb);
1814                         zdistance = fabs(pointa_z - self.origin_z);
1815
1816                         t = zdistance / cvar("g_jetpack_maxspeed_up");
1817                         t += xydistance / cvar("g_jetpack_maxspeed_side");
1818                         fuel = t * cvar("g_jetpack_fuel") * 0.8;
1819
1820                 //      dprint("jetpack ai: required fuel ", ftos(fuel), " self.ammo_fuel ", ftos(self.ammo_fuel),"\n");
1821
1822                         // enough fuel ?
1823                         if(self.ammo_fuel>fuel)
1824                         {
1825                                 // Estimate cost
1826                                 // (as onground costs calculation is mostly based on distances, here we do the same establishing some relationship
1827                                 //  - between air and ground speeds)
1828
1829                                 cost = xydistance / (cvar("g_jetpack_maxspeed_side")/cvar("sv_maxspeed"));
1830                                 cost += zdistance / (cvar("g_jetpack_maxspeed_up")/cvar("sv_maxspeed"));
1831                                 cost *= 1.5;
1832
1833                                 // Compare against other goals
1834                                 f = f * rangebias / (rangebias + cost);
1835
1836                                 if (navigation_bestrating < f)
1837                                 {
1838                         //              dprint("jetpack path: added goal", e.classname, " (with rating ", ftos(f), ")\n");
1839                                         navigation_bestrating = f;
1840                                         navigation_add_bestgoal(e);
1841                                         self.navigation_jetpack_goal = e;
1842                                         self.navigation_jetpack_point = pointb;
1843                                 }
1844                                 return;
1845                         }
1846                 }
1847         }
1848
1849         //te_wizspike(e.origin);
1850         //bprint(etos(e));
1851         //bprint("\n");
1852         // update the cached spawnfunc_waypoint link on a dynamic item entity
1853         if(e.classname == "waypoint" && !(e.wpflags & WAYPOINTFLAG_PERSONAL))
1854         {
1855                 nwp = e;
1856         }
1857         else
1858         {
1859                 if (time > e.nearestwaypointtimeout)
1860                 {
1861                         e.nearestwaypoint = navigation_findnearestwaypoint(e, TRUE);
1862
1863                         // TODO: Cleaner solution, probably handling this timeout from ctf.qc
1864                         if(e.classname=="item_flag_team")
1865                                 e.nearestwaypointtimeout = time + 2;
1866                         else
1867                                 e.nearestwaypointtimeout = time + random() * 3 + 5;
1868                 }
1869                 nwp = e.nearestwaypoint;
1870         }
1871
1872         //dprint("-- checking ", e.classname, " (with cost ", ftos(nwp.wpcost), ")\n");
1873         if (nwp)
1874         if (nwp.wpcost < 10000000)
1875         {
1876                 //te_wizspike(nwp.wpnearestpoint);
1877         //      dprint(e.classname, " ", ftos(f), "/(1+", ftos((nwp.wpcost + vlen(e.origin - nwp.wpnearestpoint))), "/", ftos(rangebias), ") = ");
1878                 f = f * rangebias / (rangebias + (nwp.wpcost + vlen(e.origin - nwp.wpnearestpoint)));
1879                 //if (self.havocbot_role == havocbot_role_ctf_offense)
1880                 //dprint("considering ", e.classname, " (with rating ", ftos(f), ")\n");
1881                 //dprint(ftos(f));
1882                 if (navigation_bestrating < f)
1883                 {
1884                 //      dprint("ground path: added goal ", e.classname, " (with rating ", ftos(f), ")\n");
1885                         navigation_bestrating = f;
1886                         navigation_add_bestgoal(e);
1887                 }
1888         }
1889         //dprint("\n");
1890 };
1891
1892 // adds an item to the the goal stack with the path to a given item
1893 float navigation_routetogoal(entity e, vector startposition)
1894 {
1895         self.goalentity = e;
1896
1897         // if there is no goal, just exit
1898         if (!e)
1899                 return FALSE;
1900
1901         self.navigation_hasgoals = TRUE;
1902
1903         // put the entity on the goal stack
1904         navigation_pushroute(e);
1905
1906         if(g_jetpack)
1907         if(e==self.navigation_jetpack_goal)
1908                 return TRUE;
1909
1910         // if it can reach the goal there is nothing more to do
1911         if (tracewalk(self, startposition, PL_MIN, PL_MAX, e.origin, bot_navigation_movemode))
1912                 return TRUE;
1913
1914         // see if there are waypoints describing a path to the item
1915         if(e.classname != "waypoint" || (e.wpflags & WAYPOINTFLAG_PERSONAL))
1916                 e = e.nearestwaypoint;
1917         else
1918                 e = e.enemy; // we already have added it, so...
1919
1920         if(e == world)
1921                 return FALSE;
1922
1923         for (;;)
1924         {
1925                 // add the spawnfunc_waypoint to the path
1926                 navigation_pushroute(e);
1927                 e = e.enemy;
1928
1929                 if(e==world)
1930                         break;
1931         }
1932
1933         return FALSE;
1934 };
1935
1936 void navigation_routetogoals()
1937 {
1938         entity g1, g2;
1939
1940         navigation_clearroute();
1941
1942         g1 = navigation_get_bestgoal();
1943         for(;;)
1944         {
1945                 if(g2==world)
1946                         g2 = navigation_get_bestgoal();
1947
1948                 if(g2==world)
1949                 {
1950                         navigation_routetogoal(g1, self.origin);
1951                         return;
1952                 }
1953
1954                 if(navigation_routetogoal(g1, g2.origin))
1955                 {
1956                         g1 = g2;
1957                         g2 = world;
1958                         continue;
1959                 }
1960
1961                 navigation_clearroute();
1962                 g1 = g2;
1963                 g2 = world;
1964         }
1965 }
1966
1967 // removes any currently touching waypoints from the goal stack
1968 // (this is how bots detect if they have reached a goal)
1969 .float lastteleporttime;
1970
1971 void navigation_poptouchedgoals()
1972 {
1973         local vector org, m1, m2;
1974         org = self.origin;
1975         m1 = org + self.mins;
1976         m2 = org + self.maxs;
1977
1978         if(self.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT)
1979         {
1980                 if(self.lastteleporttime>0)
1981                 if(time-self.lastteleporttime<0.15)
1982                 {
1983                         navigation_poproute();
1984                         return;
1985                 }
1986         }
1987
1988         // Loose goal touching check when running
1989         if(self.aistatus & AI_STATUS_RUNNING)
1990         if(self.goalcurrent.classname=="waypoint")
1991         {
1992                 if(vlen(self.origin - self.goalcurrent.origin)<150)
1993                 {
1994                         traceline(self.origin + self.view_ofs , self.goalcurrent.origin, TRUE, world);
1995                         if(trace_fraction==1)
1996                         {
1997                                 // Detect personal waypoints
1998                                 if(self.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING)
1999                                 if(self.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL && self.goalcurrent.owner==self)
2000                                 {
2001                                         self.aistatus &~= AI_STATUS_WAYPOINT_PERSONAL_GOING;
2002                                         self.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED;
2003                                 }
2004
2005                                 navigation_poproute();
2006                         }
2007                 }
2008         }
2009
2010         while (self.goalcurrent && boxesoverlap(m1, m2, self.goalcurrent.absmin, self.goalcurrent.absmax))
2011         {
2012                 // Detect personal waypoints
2013                 if(self.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING)
2014                 if(self.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL && self.goalcurrent.owner==self)
2015                 {
2016                         self.aistatus &~= AI_STATUS_WAYPOINT_PERSONAL_GOING;
2017                         self.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED;
2018                 }
2019
2020                 navigation_poproute();
2021         }
2022 }
2023
2024 // begin a goal selection session (queries spawnfunc_waypoint network)
2025 void navigation_goalrating_start()
2026 {
2027         self.navigation_jetpack_goal = world;
2028         navigation_bestrating = -1;
2029         self.navigation_hasgoals = FALSE;
2030         navigation_bestgoals_reset();
2031         navigation_markroutes();
2032 };
2033
2034 // ends a goal selection session (updates goal stack to the best goal)
2035 void navigation_goalrating_end()
2036 {
2037         navigation_routetogoals();
2038 //      dprint("best goal ", self.goalcurrent.classname , "\n");
2039
2040         // Hack: if it can't walk to any goal just move blindly to the first visible waypoint
2041         if not (self.navigation_hasgoals)
2042         {
2043                 dprint(self.netname, " can't walk to any goal, going to a near waypoint\n");
2044
2045                 entity head;
2046
2047                 RandomSelection_Init();
2048                 head = findradius(self.origin,1000);
2049                 while(head)
2050                 {
2051                         if(head.classname=="waypoint")
2052                         if(!(head.wpflags & WAYPOINTFLAG_GENERATED))
2053                         if(vlen(self.origin-head.origin)>100)
2054                         if(checkpvs(self.view_ofs,head))
2055                                 RandomSelection_Add(head, 0, string_null, 1 + (vlen(self.origin-head.origin)<500), 0);
2056                         head = head.chain;
2057                 }
2058                 if(RandomSelection_chosen_ent)
2059                         navigation_routetogoal(RandomSelection_chosen_ent, self.origin);
2060
2061                 self.navigation_hasgoals = FALSE; // Reset this value
2062         }
2063 };
2064
2065
2066 //////////////////////////////////////////////////////////////////////////////
2067 // general bot functions
2068 //////////////////////////////////////////////////////////////////////////////
2069
2070 .float isbot; // true if this client is actually a bot
2071
2072 float skill;
2073 entity bot_list;
2074 .entity nextbot;
2075 .string netname_freeme;
2076 .string playermodel_freeme;
2077 .string playerskin_freeme;
2078
2079 float sv_maxspeed;
2080 .float createdtime;
2081
2082 .float bot_preferredcolors;
2083
2084 .float bot_attack;
2085 .float bot_dodge;
2086 .float bot_dodgerating;
2087
2088 //.float bot_painintensity;
2089 .float bot_firetimer;
2090 //.float bot_oldhealth;
2091 .void() bot_ai;
2092
2093 .entity bot_aimtarg;
2094 .float bot_aimlatency;
2095 .vector bot_aimselforigin;
2096 .vector bot_aimselfvelocity;
2097 .vector bot_aimtargorigin;
2098 .vector bot_aimtargvelocity;
2099
2100 .float bot_pickup;
2101 .float(entity player, entity item) bot_pickupevalfunc;
2102 .float bot_pickupbasevalue;
2103 .float bot_canfire;
2104 .float bot_strategytime;
2105
2106 // used for aiming currently
2107 // FIXME: make the weapon code use these and then replace the calculations here with a call to the weapon code
2108 vector shotorg;
2109 vector shotdir;
2110
2111 .float bot_forced_team;
2112 .float bot_config_loaded;
2113
2114 void bot_setnameandstuff()
2115 {
2116         string readfile, s;
2117         float file, tokens, prio;
2118         entity p;
2119
2120         string bot_name, bot_model, bot_skin, bot_shirt, bot_pants;
2121         string name, prefix, suffix;
2122
2123         if(cvar("g_campaign"))
2124         {
2125                 prefix = "";
2126                 suffix = "";
2127         }
2128         else
2129         {
2130                 prefix = cvar_string("bot_prefix");
2131                 suffix = cvar_string("bot_suffix");
2132         }
2133
2134         file = fopen(cvar_string("bot_config_file"), FILE_READ);
2135
2136         if(file < 0)
2137                 print(strcat("Error: Can not open the bot configuration file '",cvar_string("bot_config_file"),"'\n"));
2138         else
2139         {
2140                 RandomSelection_Init();
2141                 for(;;)
2142                 {
2143                         readfile = fgets(file);
2144                         if(!readfile)
2145                                 break;
2146                         if(substring(readfile, 0, 2) == "//")
2147                                 continue;
2148                         if(substring(readfile, 0, 1) == "#")
2149                                 continue;
2150                         tokens = tokenizebyseparator(readfile, "\t");
2151                         s = argv(0);
2152                         prio = 1;
2153                         FOR_EACH_CLIENT(p)
2154                         {
2155                                 if(strcat(prefix, s, suffix) == p.netname)
2156                                 {
2157                                         prio = 0;
2158                                         break;
2159                                 }
2160                         }
2161                         RandomSelection_Add(world, 0, readfile, 1, prio);
2162                 }
2163                 readfile = RandomSelection_chosen_string;
2164                 fclose(file);
2165         }
2166
2167         tokens = tokenizebyseparator(readfile, "\t");
2168         if(argv(0) != "") bot_name = argv(0);
2169         else bot_name = "Bot";
2170
2171         if(argv(1) != "") bot_model = argv(1);
2172         else bot_model = "marine";
2173
2174         if(argv(2) != "") bot_skin = argv(2);
2175         else bot_skin = "0";
2176
2177         if(argv(3) != "" && stof(argv(3)) >= 0) bot_shirt = argv(3);
2178         else bot_shirt = ftos(floor(random() * 15));
2179
2180         if(argv(4) != "" && stof(argv(4)) >= 0) bot_pants = argv(4);
2181         else bot_pants = ftos(floor(random() * 15));
2182
2183         self.bot_forced_team = stof(argv(5));
2184         self.bot_config_loaded = TRUE;
2185
2186         // this is really only a default, JoinBestTeam is called later
2187         setcolor(self, stof(bot_shirt) * 16 + stof(bot_pants));
2188         self.bot_preferredcolors = self.clientcolors;
2189
2190         // pick the name
2191         if (cvar("bot_usemodelnames"))
2192                 name = bot_model;
2193         else
2194                 name = bot_name;
2195
2196         // pick the model and skin
2197         if(substring(bot_model, -4, 1) != ".")
2198                 bot_model = strcat(bot_model, ".zym");
2199         self.playermodel = self.playermodel_freeme = strzone(strcat("models/player/", bot_model));
2200         self.playerskin = self.playerskin_freeme = strzone(bot_skin);
2201
2202         self.netname = self.netname_freeme = strzone(strcat(prefix, name, suffix));
2203 };
2204
2205 float bot_custom_weapon;
2206 float bot_distance_far;
2207 float bot_distance_close;
2208
2209 float bot_weapons_far[WEP_LAST];
2210 float bot_weapons_mid[WEP_LAST];
2211 float bot_weapons_close[WEP_LAST];
2212
2213 void bot_custom_weapon_priority_setup()
2214 {
2215         local float tokens, i, c, w;
2216
2217         bot_custom_weapon = FALSE;
2218
2219         if(     cvar_string("bot_ai_custom_weapon_priority_far") == "" ||
2220                 cvar_string("bot_ai_custom_weapon_priority_mid") == "" ||
2221                 cvar_string("bot_ai_custom_weapon_priority_close") == "" ||
2222                 cvar_string("bot_ai_custom_weapon_priority_distances") == ""
2223         )
2224                 return;
2225
2226         // Parse distances
2227         tokens = tokenizebyseparator(cvar_string("bot_ai_custom_weapon_priority_distances")," ");
2228
2229         if (tokens!=2)
2230                 return;
2231
2232         bot_distance_far = stof(argv(0));
2233         bot_distance_close = stof(argv(1));
2234
2235         if(bot_distance_far < bot_distance_close){
2236                 bot_distance_far = stof(argv(1));
2237                 bot_distance_close = stof(argv(0));
2238         }
2239
2240         // Initialize list of weapons
2241         bot_weapons_far[0] = -1;
2242         bot_weapons_mid[0] = -1;
2243         bot_weapons_close[0] = -1;
2244
2245         // Parse far distance weapon priorities
2246         tokens = tokenizebyseparator(cvar_string("bot_ai_custom_weapon_priority_far")," ");
2247
2248         c = 0;
2249         for(i=0; i < tokens && c < WEP_COUNT; ++i){
2250                 w = stof(argv(i));
2251                 if ( w >= WEP_FIRST && w <= WEP_LAST) {
2252                         bot_weapons_far[c] = w;
2253                         ++c;
2254                 }
2255         }
2256         if(c < WEP_COUNT)
2257                 bot_weapons_far[c] = -1;
2258
2259         // Parse mid distance weapon priorities
2260         tokens = tokenizebyseparator(cvar_string("bot_ai_custom_weapon_priority_mid")," ");
2261
2262         c = 0;
2263         for(i=0; i < tokens && c < WEP_COUNT; ++i){
2264                 w = stof(argv(i));
2265                 if ( w >= WEP_FIRST && w <= WEP_LAST) {
2266                         bot_weapons_mid[c] = w;
2267                         ++c;
2268                 }
2269         }
2270         if(c < WEP_COUNT)
2271                 bot_weapons_mid[c] = -1;
2272
2273         // Parse close distance weapon priorities
2274         tokens = tokenizebyseparator(cvar_string("bot_ai_custom_weapon_priority_close")," ");
2275
2276         c = 0;
2277         for(i=0; i < tokens && i < WEP_COUNT; ++i){
2278                 w = stof(argv(i));
2279                 if ( w >= WEP_FIRST && w <= WEP_LAST) {
2280                         bot_weapons_close[c] = w;
2281                         ++c;
2282                 }
2283         }
2284         if(c < WEP_COUNT)
2285                 bot_weapons_close[c] = -1;
2286
2287         bot_custom_weapon = TRUE;
2288 };
2289
2290
2291 void bot_endgame()
2292 {
2293         local entity e;
2294         //dprint("bot_endgame\n");
2295         e = bot_list;
2296         while (e)
2297         {
2298                 setcolor(e, e.bot_preferredcolors);
2299                 e = e.nextbot;
2300         }
2301         // if dynamic waypoints are ever implemented, save them here
2302 };
2303
2304 float bot_ignore_bots; // let bots not attack other bots (only works in non-teamplay)
2305 float bot_shouldattack(entity e)
2306 {
2307         if (e.team == self.team)
2308         {
2309                 if (e == self)
2310                         return FALSE;
2311                 if (teams_matter)
2312                 if (e.team != 0)
2313                         return FALSE;
2314         }
2315
2316         if(teams_matter)
2317         {
2318                 if(e.team==0)
2319                         return FALSE;
2320         }
2321         else if(bot_ignore_bots)
2322                 if(clienttype(e) == CLIENTTYPE_BOT)
2323                         return FALSE;
2324
2325         if (!e.takedamage)
2326                 return FALSE;
2327         if (e.deadflag)
2328                 return FALSE;
2329         if (e.BUTTON_CHAT)
2330                 return FALSE;
2331         if(g_minstagib)
2332         if(e.items & IT_STRENGTH)
2333                 return FALSE;
2334         if(e.flags & FL_NOTARGET)
2335                 return FALSE;
2336         return TRUE;
2337 };
2338
2339 void bot_lagfunc(float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4)
2340 {
2341         if(self.flags & FL_INWATER)
2342         {
2343                 self.bot_aimtarg = world;
2344                 return;
2345         }
2346         self.bot_aimtarg = e1;
2347         self.bot_aimlatency = self.ping; // FIXME?  Shouldn't this be in the lag item?
2348         self.bot_aimselforigin = v1;
2349         self.bot_aimselfvelocity = v2;
2350         self.bot_aimtargorigin = v3;
2351         self.bot_aimtargvelocity = v4;
2352         if(skill <= 0)
2353                 self.bot_canfire = (random() < 0.8);
2354         else if(skill <= 1)
2355                 self.bot_canfire = (random() < 0.9);
2356         else if(skill <= 2)
2357                 self.bot_canfire = (random() < 0.95);
2358         else
2359                 self.bot_canfire = 1;
2360 };
2361
2362 .float bot_nextthink;
2363 .float bot_badaimtime;
2364 .float bot_aimthinktime;
2365 .float bot_prevaimtime;
2366 .vector bot_mouseaim;
2367 .vector bot_badaimoffset;
2368 .vector bot_1st_order_aimfilter;
2369 .vector bot_2nd_order_aimfilter;
2370 .vector bot_3th_order_aimfilter;
2371 .vector bot_4th_order_aimfilter;
2372 .vector bot_5th_order_aimfilter;
2373 .vector bot_olddesiredang;
2374 float bot_aimdir(vector v, float maxfiredeviation)
2375 {
2376         local float dist, delta_t, blend;
2377         local vector desiredang, diffang;
2378
2379         //dprint("aim ", self.netname, ": old:", vtos(self.v_angle));
2380         // make sure v_angle is sane first
2381         self.v_angle_y = self.v_angle_y - floor(self.v_angle_y / 360) * 360;
2382         self.v_angle_z = 0;
2383
2384         // get the desired angles to aim at
2385         //dprint(" at:", vtos(v));
2386         v = normalize(v);
2387         //te_lightning2(world, self.origin + self.view_ofs, self.origin + self.view_ofs + v * 200);
2388         if (time >= self.bot_badaimtime)
2389         {
2390                 self.bot_badaimtime = max(self.bot_badaimtime + 0.3, time);
2391                 self.bot_badaimoffset = randomvec() * bound(0, 5 - 0.5 * (skill+self.bot_offsetskill), 5) * cvar("bot_ai_aimskill_offset");
2392         }
2393         desiredang = vectoangles(v) + self.bot_badaimoffset;
2394         //dprint(" desired:", vtos(desiredang));
2395         if (desiredang_x >= 180)
2396                 desiredang_x = desiredang_x - 360;
2397         desiredang_x = bound(-90, 0 - desiredang_x, 90);
2398         desiredang_z = self.v_angle_z;
2399         //dprint(" / ", vtos(desiredang));
2400
2401         //// pain throws off aim
2402         //if (self.bot_painintensity)
2403         //{
2404         //      // shake from pain
2405         //      desiredang = desiredang + randomvec() * self.bot_painintensity * 0.2;
2406         //}
2407
2408         // calculate turn angles
2409         diffang = (desiredang - self.bot_olddesiredang);
2410         // wrap yaw turn
2411         diffang_y = diffang_y - floor(diffang_y / 360) * 360;
2412         if (diffang_y >= 180)
2413                 diffang_y = diffang_y - 360;
2414         self.bot_olddesiredang = desiredang;
2415         //dprint(" diff:", vtos(diffang));
2416
2417         delta_t = time-self.bot_prevaimtime;
2418         self.bot_prevaimtime = time;
2419         // Here we will try to anticipate the comming aiming direction
2420         self.bot_1st_order_aimfilter= self.bot_1st_order_aimfilter
2421                 + (diffang * (1 / delta_t)    - self.bot_1st_order_aimfilter) * bound(0, cvar("bot_ai_aimskill_order_filter_1st"),1);
2422         self.bot_2nd_order_aimfilter= self.bot_2nd_order_aimfilter
2423                 + (self.bot_1st_order_aimfilter - self.bot_2nd_order_aimfilter) * bound(0, cvar("bot_ai_aimskill_order_filter_2nd"),1);
2424         self.bot_3th_order_aimfilter= self.bot_3th_order_aimfilter
2425                 + (self.bot_2nd_order_aimfilter - self.bot_3th_order_aimfilter) * bound(0, cvar("bot_ai_aimskill_order_filter_3th"),1);
2426         self.bot_4th_order_aimfilter= self.bot_4th_order_aimfilter
2427                 + (self.bot_3th_order_aimfilter - self.bot_4th_order_aimfilter) * bound(0, cvar("bot_ai_aimskill_order_filter_4th"),1);
2428         self.bot_5th_order_aimfilter= self.bot_5th_order_aimfilter
2429                 + (self.bot_4th_order_aimfilter - self.bot_5th_order_aimfilter) * bound(0, cvar("bot_ai_aimskill_order_filter_5th"),1);
2430
2431         //blend = (bound(0,skill,10)*0.1)*pow(1-bound(0,skill,10)*0.05,2.5)*5.656854249; //Plot formule before changing !
2432         blend = bound(0,skill,10)*0.1;
2433         desiredang = desiredang + blend *
2434         (
2435                   self.bot_1st_order_aimfilter * cvar("bot_ai_aimskill_order_mix_1st")
2436                 + self.bot_2nd_order_aimfilter * cvar("bot_ai_aimskill_order_mix_2nd")
2437                 + self.bot_3th_order_aimfilter * cvar("bot_ai_aimskill_order_mix_3th")
2438                 + self.bot_4th_order_aimfilter * cvar("bot_ai_aimskill_order_mix_4th")
2439                 + self.bot_5th_order_aimfilter * cvar("bot_ai_aimskill_order_mix_5th")
2440         );
2441
2442         // calculate turn angles
2443         diffang = desiredang - self.bot_mouseaim;
2444         // wrap yaw turn
2445         diffang_y = diffang_y - floor(diffang_y / 360) * 360;
2446         if (diffang_y >= 180)
2447                 diffang_y = diffang_y - 360;
2448         //dprint(" diff:", vtos(diffang));
2449
2450         if (time >= self.bot_aimthinktime)
2451         {
2452                 self.bot_aimthinktime = max(self.bot_aimthinktime + 0.5 - 0.05*(skill+self.bot_thinkskill), time);
2453                 self.bot_mouseaim = self.bot_mouseaim + diffang * (1-random()*0.1*bound(1,10-skill,10));
2454         }
2455
2456         //self.v_angle = self.v_angle + diffang * bound(0, r * frametime * (skill * 0.5 + 2), 1);
2457
2458         diffang = self.bot_mouseaim - desiredang;
2459         // wrap yaw turn
2460         diffang_y = diffang_y - floor(diffang_y / 360) * 360;
2461         if (diffang_y >= 180)
2462                 diffang_y = diffang_y - 360;
2463         desiredang = desiredang + diffang * bound(0,cvar("bot_ai_aimskill_think"),1);
2464
2465         // calculate turn angles
2466         diffang = desiredang - self.v_angle;
2467         // wrap yaw turn
2468         diffang_y = diffang_y - floor(diffang_y / 360) * 360;
2469         if (diffang_y >= 180)
2470                 diffang_y = diffang_y - 360;
2471         //dprint(" diff:", vtos(diffang));
2472
2473         // jitter tracking
2474         dist = vlen(diffang);
2475         //diffang = diffang + randomvec() * (dist * 0.05 * (3.5 - bound(0, skill, 3)));
2476
2477         // turn
2478         local float r, fixedrate, blendrate;
2479         fixedrate = cvar("bot_ai_aimskill_fixedrate") / bound(1,dist,1000);
2480         blendrate = cvar("bot_ai_aimskill_blendrate");
2481         r = max(fixedrate, blendrate);
2482         //self.v_angle = self.v_angle + diffang * bound(frametime, r * frametime * (2+skill*skill*0.05-random()*0.05*(10-skill)), 1);
2483         self.v_angle = self.v_angle + diffang * bound(delta_t, r * delta_t * (2+pow(skill+self.bot_mouseskill,3)*0.005-random()), 1);
2484         self.v_angle = self.v_angle * bound(0,cvar("bot_ai_aimskill_mouse"),1) + desiredang * bound(0,(1-cvar("bot_ai_aimskill_mouse")),1);
2485         //self.v_angle = self.v_angle + diffang * bound(0, r * frametime * (skill * 0.5 + 2), 1);
2486         //self.v_angle = self.v_angle + diffang * (1/ blendrate);
2487         self.v_angle_z = 0;
2488         self.v_angle_y = self.v_angle_y - floor(self.v_angle_y / 360) * 360;
2489         //dprint(" turn:", vtos(self.v_angle));
2490
2491         makevectors(self.v_angle);
2492         shotorg = self.origin + self.view_ofs;
2493         shotdir = v_forward;
2494
2495         //dprint(" dir:", vtos(v_forward));
2496         //te_lightning2(world, shotorg, shotorg + shotdir * 100);
2497
2498         // calculate turn angles again
2499         //diffang = desiredang - self.v_angle;
2500         //diffang_y = diffang_y - floor(diffang_y / 360) * 360;
2501         //if (diffang_y >= 180)
2502         //      diffang_y = diffang_y - 360;
2503
2504         //dprint("e ", vtos(diffang), " < ", ftos(maxfiredeviation), "\n");
2505
2506         // decide whether to fire this time
2507         // note the maxfiredeviation is in degrees so this has to convert to radians first
2508         //if ((normalize(v) * shotdir) >= cos(maxfiredeviation * (3.14159265358979323846 / 180)))
2509         if ((normalize(v) * shotdir) >= cos(maxfiredeviation * (3.14159265358979323846 / 180)))
2510         if (vlen(trace_endpos-shotorg) < 500+500*bound(0, skill, 10) || random()*random()>bound(0,skill*0.05,1))
2511                 self.bot_firetimer = time + bound(0.1, 0.5-skill*0.05, 0.5);
2512         //traceline(shotorg,shotorg+shotdir*1000,FALSE,world);
2513         //dprint(ftos(maxfiredeviation),"\n");
2514         //dprint(" diff:", vtos(diffang), "\n");
2515
2516         return self.bot_canfire && (time < self.bot_firetimer);
2517 };
2518
2519 vector bot_shotlead(vector targorigin, vector targvelocity, float shotspeed, float shotdelay)
2520 {
2521         // Try to add code here that predicts gravity effect here, no clue HOW to though ... well not yet atleast...
2522         return targorigin + targvelocity * (shotdelay + vlen(targorigin - shotorg) / shotspeed);
2523 };
2524
2525 float bot_aim(float shotspeed, float shotspeedupward, float maxshottime, float applygravity)
2526 {
2527         local float f, r;
2528         local vector v;
2529         /*
2530         eprint(self);
2531         dprint("bot_aim(", ftos(shotspeed));
2532         dprint(", ", ftos(shotspeedupward));
2533         dprint(", ", ftos(maxshottime));
2534         dprint(", ", ftos(applygravity));
2535         dprint(");\n");
2536         */
2537         shotspeed *= g_weaponspeedfactor;
2538         shotspeedupward *= g_weaponspeedfactor;
2539         if (!shotspeed)
2540         {
2541                 dprint("bot_aim: WARNING: weapon ", W_Name(self.weapon), " shotspeed is zero!\n");
2542                 shotspeed = 1000000;
2543         }
2544         if (!maxshottime)
2545         {
2546                 dprint("bot_aim: WARNING: weapon ", W_Name(self.weapon), " maxshottime is zero!\n");
2547                 maxshottime = 1;
2548         }
2549         makevectors(self.v_angle);
2550         shotorg = self.origin + self.view_ofs;
2551         shotdir = v_forward;
2552         v = bot_shotlead(self.bot_aimtargorigin, self.bot_aimtargvelocity, shotspeed, self.bot_aimlatency);
2553         local float distanceratio;
2554         distanceratio =sqrt(bound(0,skill,10000))*0.3*(vlen(v-shotorg)-100)/cvar("bot_ai_aimskill_firetolerance_distdegrees");
2555         distanceratio = bound(0,distanceratio,1);
2556         r =  (cvar("bot_ai_aimskill_firetolerance_maxdegrees")-cvar("bot_ai_aimskill_firetolerance_mindegrees"))
2557                 * (1-distanceratio) + cvar("bot_ai_aimskill_firetolerance_mindegrees");
2558         if (applygravity && self.bot_aimtarg)
2559         {
2560                 if (!findtrajectorywithleading(shotorg, '0 0 0', '0 0 0', self.bot_aimtarg, shotspeed, shotspeedupward, maxshottime, 0, self))
2561                         return FALSE;
2562                 f = bot_aimdir(findtrajectory_velocity - shotspeedupward * '0 0 1', r);
2563         }
2564         else
2565         {
2566                 f = bot_aimdir(v - shotorg, r);
2567                 //dprint("AIM: ");dprint(vtos(self.bot_aimtargorigin));dprint(" + ");dprint(vtos(self.bot_aimtargvelocity));dprint(" * ");dprint(ftos(self.bot_aimlatency + vlen(self.bot_aimtargorigin - shotorg) / shotspeed));dprint(" = ");dprint(vtos(v));dprint(" : aimdir = ");dprint(vtos(normalize(v - shotorg)));dprint(" : ");dprint(vtos(shotdir));dprint("\n");
2568                 traceline(shotorg, shotorg + shotdir * 10000, FALSE, self);
2569                 if (trace_ent.takedamage)
2570                 if (trace_fraction < 1)
2571                 if (!bot_shouldattack(trace_ent))
2572                         return FALSE;
2573                 traceline(shotorg, self.bot_aimtargorigin, FALSE, self);
2574                 if (trace_fraction < 1)
2575                 if (trace_ent != self.enemy)
2576                 if (!bot_shouldattack(trace_ent))
2577                         return FALSE;
2578         }
2579         if (r > maxshottime * shotspeed)
2580                 return FALSE;
2581         return f;
2582 };
2583
2584 // TODO: move this painintensity code to the player damage code
2585 void bot_think()
2586 {
2587         if (self.bot_nextthink > time)
2588                 return;
2589
2590         self.flags &~= FL_GODMODE;
2591         if(cvar("bot_god"))
2592                 self.flags |= FL_GODMODE;
2593
2594         self.bot_nextthink = self.bot_nextthink + cvar("bot_ai_thinkinterval");
2595         //if (self.bot_painintensity > 0)
2596         //      self.bot_painintensity = self.bot_painintensity - (skill + 1) * 40 * frametime;
2597
2598         //self.bot_painintensity = self.bot_painintensity + self.bot_oldhealth - self.health;
2599         //self.bot_painintensity = bound(0, self.bot_painintensity, 100);
2600
2601         if(time < game_starttime || ((cvar("g_campaign") && !campaign_bots_may_start)))
2602         {
2603                 self.nextthink = time + 0.5;
2604                 return;
2605         }
2606
2607         if (self.fixangle)
2608         {
2609                 self.v_angle = self.angles;
2610                 self.v_angle_z = 0;
2611                 self.fixangle = FALSE;
2612         }
2613
2614         self.dmg_take = 0;
2615         self.dmg_save = 0;
2616         self.dmg_inflictor = world;
2617
2618         // calculate an aiming latency based on the skill setting
2619         // (simulated network latency + naturally delayed reflexes)
2620         //self.ping = 0.7 - bound(0, 0.05 * skill, 0.5); // moved the reflexes to bot_aimdir (under the name 'think')
2621         // minimum ping 20+10 random
2622         self.ping = bound(0,0.07 - bound(0, skill * 0.005,0.05)+random()*0.01,0.65); // Now holds real lag to server, and higer skill players take a less laggy server
2623         // skill 10 = ping 0.2 (adrenaline)
2624         // skill 0 = ping 0.7 (slightly drunk)
2625
2626         // clear buttons
2627         self.BUTTON_ATCK = 0;
2628         self.button1 = 0;
2629         self.BUTTON_JUMP = 0;
2630         self.BUTTON_ATCK2 = 0;
2631         self.BUTTON_ZOOM = 0;
2632         self.BUTTON_CROUCH = 0;
2633         self.BUTTON_HOOK = 0;
2634         self.BUTTON_INFO = 0;
2635         self.button8 = 0;
2636         self.BUTTON_CHAT = 0;
2637         self.BUTTON_USE = 0;
2638
2639         // if dead, just wait until we can respawn
2640         if (self.deadflag)
2641         {
2642                 if (self.deadflag == DEAD_DEAD)
2643                 {
2644                         self.BUTTON_JUMP = 1; // press jump to respawn
2645                         self.bot_strategytime = 0;
2646                 }
2647         }
2648
2649         // now call the current bot AI (havocbot for example)
2650         self.bot_ai();
2651 };
2652
2653 entity bot_strategytoken;
2654 float bot_strategytoken_taken;
2655 entity player_list;
2656 .entity nextplayer;
2657 void bot_relinkplayerlist()
2658 {
2659         local entity e;
2660         local entity prevbot;
2661         player_count = 0;
2662         currentbots = 0;
2663         player_list = e = findchainflags(flags, FL_CLIENT);
2664         bot_list = world;
2665         prevbot = world;
2666         while (e)
2667         {
2668                 player_count = player_count + 1;
2669                 e.nextplayer = e.chain;
2670                 if (clienttype(e) == CLIENTTYPE_BOT)
2671                 {
2672                         if (prevbot)
2673                                 prevbot.nextbot = e;
2674                         else
2675                         {
2676                                 bot_list = e;
2677                                 bot_list.nextbot = world;
2678                         }
2679                         prevbot = e;
2680                         currentbots = currentbots + 1;
2681                 }
2682                 e = e.chain;
2683         }
2684         dprint(strcat("relink: ", ftos(currentbots), " bots seen.\n"));
2685         bot_strategytoken = bot_list;
2686         bot_strategytoken_taken = TRUE;
2687 };
2688
2689 void() havocbot_setupbot;
2690 float JoinBestTeam(entity pl, float only_return_best, float forcebestteam);
2691
2692 void bot_clientdisconnect()
2693 {
2694         if (clienttype(self) != CLIENTTYPE_BOT)
2695                 return;
2696         if(self.netname_freeme)
2697                 strunzone(self.netname_freeme);
2698         if(self.playermodel_freeme)
2699                 strunzone(self.playermodel_freeme);
2700         if(self.playerskin_freeme)
2701                 strunzone(self.playerskin_freeme);
2702         self.netname_freeme = string_null;
2703         self.playermodel_freeme = string_null;
2704         self.playerskin_freeme = string_null;
2705 }
2706
2707 void bot_clientconnect()
2708 {
2709         if (clienttype(self) != CLIENTTYPE_BOT)
2710                 return;
2711         self.bot_preferredcolors = self.clientcolors;
2712         self.bot_nextthink = time - random();
2713         self.lag_func = bot_lagfunc;
2714         self.isbot = TRUE;
2715         self.createdtime = self.nextthink;
2716
2717         if(!self.bot_config_loaded) // This is needed so team overrider doesn't break between matches
2718                 bot_setnameandstuff();
2719
2720         if(self.bot_forced_team==1)
2721                 self.team = COLOR_TEAM1;
2722         else if(self.bot_forced_team==2)
2723                 self.team = COLOR_TEAM2;
2724         else if(self.bot_forced_team==3)
2725                 self.team = COLOR_TEAM3;
2726         else if(self.bot_forced_team==4)
2727                 self.team = COLOR_TEAM4;
2728         else
2729                 JoinBestTeam(self, FALSE, TRUE);
2730
2731         havocbot_setupbot();
2732         self.bot_mouseskill=random()-0.5;
2733         self.bot_thinkskill=random()-0.5;
2734         self.bot_predictionskill=random()-0.5;
2735         self.bot_offsetskill=random()-0.5;
2736 };
2737
2738 entity bot_spawn()
2739 {
2740         local entity oldself, bot;
2741         bot = spawnclient();
2742         if (bot)
2743         {
2744                 currentbots = currentbots + 1;
2745                 oldself = self;
2746                 self = bot;
2747                 bot_setnameandstuff();
2748                 ClientConnect();
2749                 PutClientInServer();
2750                 self = oldself;
2751         }
2752         return bot;
2753 };
2754
2755 void CheckAllowedTeams(entity for_whom); void GetTeamCounts(entity other); float c1, c2, c3, c4;
2756 void bot_removefromlargestteam()
2757 {
2758         local float besttime, bestcount, thiscount;
2759         local entity best, head;
2760         CheckAllowedTeams(world);
2761         GetTeamCounts(world);
2762         head = findchainfloat(isbot, TRUE);
2763         if (!head)
2764                 return;
2765         best = head;
2766         besttime = head.createdtime;
2767         bestcount = 0;
2768         while (head)
2769         {
2770                 if(head.team == COLOR_TEAM1)
2771                         thiscount = c1;
2772                 else if(head.team == COLOR_TEAM2)
2773                         thiscount = c2;
2774                 else if(head.team == COLOR_TEAM3)
2775                         thiscount = c3;
2776                 else if(head.team == COLOR_TEAM4)
2777                         thiscount = c4;
2778                 else
2779                         thiscount = 0;
2780                 if (thiscount > bestcount)
2781                 {
2782                         bestcount = thiscount;
2783                         besttime = head.createdtime;
2784                         best = head;
2785                 }
2786                 else if (thiscount == bestcount && besttime < head.createdtime)
2787                 {
2788                         besttime = head.createdtime;
2789                         best = head;
2790                 }
2791                 head = head.chain;
2792         }
2793         currentbots = currentbots - 1;
2794         dropclient(best);
2795 };
2796
2797 void bot_removenewest()
2798 {
2799         local float besttime;
2800         local entity best, head;
2801
2802         if(teams_matter)
2803         {
2804                 bot_removefromlargestteam();
2805                 return;
2806         }
2807
2808         head = findchainfloat(isbot, TRUE);
2809         if (!head)
2810                 return;
2811         best = head;
2812         besttime = head.createdtime;
2813         while (head)
2814         {
2815                 if (besttime < head.createdtime)
2816                 {
2817                         besttime = head.createdtime;
2818                         best = head;
2819                 }
2820                 head = head.chain;
2821         }
2822         currentbots = currentbots - 1;
2823         dropclient(best);
2824 };
2825
2826 float botframe_waypointeditorlightningtime;
2827 void botframe_showwaypointlinks()
2828 {
2829         local entity player, head, w;
2830         if (time < botframe_waypointeditorlightningtime)
2831                 return;
2832         botframe_waypointeditorlightningtime = time + 0.5;
2833         player = find(world, classname, "player");
2834         while (player)
2835         {
2836                 if (!player.isbot)
2837                 if (player.flags & FL_ONGROUND || player.waterlevel > WATERLEVEL_NONE)
2838                 {
2839                         //navigation_testtracewalk = TRUE;
2840                         head = navigation_findnearestwaypoint(player, FALSE);
2841                 //      print("currently selected WP is ", etos(head), "\n");
2842                         //navigation_testtracewalk = FALSE;
2843                         if (head)
2844                         {
2845                                 w = head     ;if (w) te_lightning2(world, w.origin, player.origin);
2846                                 w = head.wp00;if (w) te_lightning2(world, w.origin, head.origin);
2847                                 w = head.wp01;if (w) te_lightning2(world, w.origin, head.origin);
2848                                 w = head.wp02;if (w) te_lightning2(world, w.origin, head.origin);
2849                                 w = head.wp03;if (w) te_lightning2(world, w.origin, head.origin);
2850                                 w = head.wp04;if (w) te_lightning2(world, w.origin, head.origin);
2851                                 w = head.wp05;if (w) te_lightning2(world, w.origin, head.origin);
2852                                 w = head.wp06;if (w) te_lightning2(world, w.origin, head.origin);
2853                                 w = head.wp07;if (w) te_lightning2(world, w.origin, head.origin);
2854                                 w = head.wp08;if (w) te_lightning2(world, w.origin, head.origin);
2855                                 w = head.wp09;if (w) te_lightning2(world, w.origin, head.origin);
2856                                 w = head.wp10;if (w) te_lightning2(world, w.origin, head.origin);
2857                                 w = head.wp11;if (w) te_lightning2(world, w.origin, head.origin);
2858                                 w = head.wp12;if (w) te_lightning2(world, w.origin, head.origin);
2859                                 w = head.wp13;if (w) te_lightning2(world, w.origin, head.origin);
2860                                 w = head.wp14;if (w) te_lightning2(world, w.origin, head.origin);
2861                                 w = head.wp15;if (w) te_lightning2(world, w.origin, head.origin);
2862                                 w = head.wp16;if (w) te_lightning2(world, w.origin, head.origin);
2863                                 w = head.wp17;if (w) te_lightning2(world, w.origin, head.origin);
2864                                 w = head.wp18;if (w) te_lightning2(world, w.origin, head.origin);
2865                                 w = head.wp19;if (w) te_lightning2(world, w.origin, head.origin);
2866                                 w = head.wp20;if (w) te_lightning2(world, w.origin, head.origin);
2867                                 w = head.wp21;if (w) te_lightning2(world, w.origin, head.origin);
2868                                 w = head.wp22;if (w) te_lightning2(world, w.origin, head.origin);
2869                                 w = head.wp23;if (w) te_lightning2(world, w.origin, head.origin);
2870                                 w = head.wp24;if (w) te_lightning2(world, w.origin, head.origin);
2871                                 w = head.wp25;if (w) te_lightning2(world, w.origin, head.origin);
2872                                 w = head.wp26;if (w) te_lightning2(world, w.origin, head.origin);
2873                                 w = head.wp27;if (w) te_lightning2(world, w.origin, head.origin);
2874                                 w = head.wp28;if (w) te_lightning2(world, w.origin, head.origin);
2875                                 w = head.wp29;if (w) te_lightning2(world, w.origin, head.origin);
2876                                 w = head.wp30;if (w) te_lightning2(world, w.origin, head.origin);
2877                                 w = head.wp31;if (w) te_lightning2(world, w.origin, head.origin);
2878                         }
2879                 }
2880                 player = find(player, classname, "player");
2881         }
2882 };
2883
2884 entity botframe_dangerwaypoint;
2885 void botframe_updatedangerousobjects(float maxupdate)
2886 {
2887         local entity head, bot_dodgelist;
2888         local vector m1, m2, v;
2889         local float c, d, danger;
2890         c = 0;
2891         bot_dodgelist = findchainfloat(bot_dodge, TRUE);
2892         botframe_dangerwaypoint = find(botframe_dangerwaypoint, classname, "waypoint");
2893         while (botframe_dangerwaypoint != world)
2894         {
2895                 danger = 0;
2896                 m1 = botframe_dangerwaypoint.mins;
2897                 m2 = botframe_dangerwaypoint.maxs;
2898                 head = bot_dodgelist;
2899                 while (head)
2900                 {
2901                         v = head.origin;
2902                         v_x = bound(m1_x, v_x, m2_x);
2903                         v_y = bound(m1_y, v_y, m2_y);
2904                         v_z = bound(m1_z, v_z, m2_z);
2905                         d = head.bot_dodgerating - vlen(head.origin - v);
2906                         if (d > 0)
2907                         {
2908                                 traceline(head.origin, v, TRUE, world);
2909                                 if (trace_fraction == 1)
2910                                         danger = danger + d;
2911                         }
2912                         head = head.chain;
2913                 }
2914                 botframe_dangerwaypoint.dmg = danger;
2915                 c = c + 1;
2916                 if (c >= maxupdate)
2917                         break;
2918                 botframe_dangerwaypoint = find(botframe_dangerwaypoint, classname, "waypoint");
2919         }
2920 };
2921
2922
2923 float botframe_spawnedwaypoints;
2924 float botframe_nextthink;
2925 float botframe_nextdangertime;
2926
2927 float autoskill_nextthink;
2928 .float totalfrags_lastcheck;
2929 void autoskill(float factor)
2930 {
2931         float bestbot;
2932         float bestplayer;
2933         entity head;
2934
2935         bestbot = -1;
2936         bestplayer = -1;
2937         FOR_EACH_PLAYER(head)
2938         {
2939                 if(clienttype(head) == CLIENTTYPE_REAL)
2940                         bestplayer = max(bestplayer, head.totalfrags - head.totalfrags_lastcheck);
2941                 else
2942                         bestbot = max(bestbot, head.totalfrags - head.totalfrags_lastcheck);
2943         }
2944
2945         dprint("autoskill: best player got ", ftos(bestplayer), ", ");
2946         dprint("best bot got ", ftos(bestbot), "; ");
2947         if(bestbot < 0 || bestplayer < 0)
2948         {
2949                 dprint("not doing anything\n");
2950                 // don't return, let it reset all counters below
2951         }
2952         else if(bestbot <= bestplayer * factor - 2)
2953         {
2954                 if(cvar("skill") < 17)
2955                 {
2956                         dprint("2 frags difference, increasing skill\n");
2957                         cvar_set("skill", ftos(cvar("skill") + 1));
2958                         bprint("^2SKILL UP!^7 Now at level ", ftos(cvar("skill")), "\n");
2959                 }
2960         }
2961         else if(bestbot >= bestplayer * factor + 2)
2962         {
2963                 if(cvar("skill") > 0)
2964                 {
2965                         dprint("2 frags difference, decreasing skill\n");
2966                         cvar_set("skill", ftos(cvar("skill") - 1));
2967                         bprint("^1SKILL DOWN!^7 Now at level ", ftos(cvar("skill")), "\n");
2968                 }
2969         }
2970         else
2971         {
2972                 dprint("not doing anything\n");
2973                 return;
2974                 // don't reset counters, wait for them to accumulate
2975         }
2976
2977         FOR_EACH_PLAYER(head)
2978                 head.totalfrags_lastcheck = head.totalfrags;
2979 }
2980
2981 float bot_cvar_nextthink;
2982 void bot_serverframe()
2983 {
2984         float realplayers, bots, activerealplayers;
2985         entity head;
2986
2987         if (intermission_running)
2988                 return;
2989
2990         if (time < 2)
2991                 return;
2992
2993         stepheightvec = cvar("sv_stepheight") * '0 0 1';
2994         bot_navigation_movemode = ((cvar("bot_navigation_ignoreplayers")) ? MOVE_NOMONSTERS : MOVE_NORMAL);
2995
2996         if(time > autoskill_nextthink)
2997         {
2998                 float a;
2999                 a = cvar("skill_auto");
3000                 if(a)
3001                         autoskill(a);
3002                 autoskill_nextthink = time + 5;
3003         }
3004
3005         activerealplayers = 0;
3006         realplayers = 0;
3007
3008         FOR_EACH_REALCLIENT(head)
3009         {
3010                 if(head.classname == "player" || g_lms || g_arena)
3011                         ++activerealplayers;
3012                 ++realplayers;
3013         }
3014
3015         // add/remove bots if needed to make sure there are at least
3016         // minplayers+bot_number, or remove all bots if no one is playing
3017         // But don't remove bots immediately on level change, as the real players
3018         // usually haven't rejoined yet
3019         bots_would_leave = FALSE;
3020         if ((realplayers || cvar("bot_join_empty") || (currentbots > 0 && time < 5)))
3021         {
3022                 float realminplayers, minplayers;
3023                 realminplayers = cvar("minplayers");
3024                 minplayers = max(0, floor(realminplayers));
3025
3026                 float realminbots, minbots;
3027                 if(teamplay && cvar("bot_vs_human"))
3028                         realminbots = ceil(fabs(cvar("bot_vs_human")) * activerealplayers);
3029                 else
3030                         realminbots = cvar("bot_number");
3031                 minbots = max(0, floor(realminbots));
3032
3033                 bots = min(max(minbots, minplayers - activerealplayers), maxclients - realplayers);
3034                 if(bots > minbots)
3035                         bots_would_leave = TRUE;
3036         }
3037         else
3038         {
3039                 // if there are no players, remove bots
3040                 bots = 0;
3041         }
3042
3043         bot_ignore_bots = cvar("bot_ignore_bots");
3044
3045         // only add one bot per frame to avoid utter chaos
3046         if(time > botframe_nextthink)
3047         {
3048                 //dprint(ftos(bots), " ? ", ftos(currentbots), "\n");
3049                 while (currentbots < bots)
3050                 {
3051                         if (bot_spawn() == world)
3052                         {
3053                                 bprint("Can not add bot, server full.\n");
3054                                 botframe_nextthink = time + 10;
3055                                 break;
3056                         }
3057                 }
3058                 while (currentbots > bots)
3059                         bot_removenewest();
3060         }
3061
3062         if(botframe_spawnedwaypoints)
3063         {
3064                 if(cvar("waypoint_benchmark"))
3065                         localcmd("quit\n");
3066         }
3067
3068         if (currentbots > 0 || cvar("g_waypointeditor"))
3069         if (botframe_spawnedwaypoints)
3070         {
3071                 if(botframe_cachedwaypointlinks)
3072                 {
3073                         if(!botframe_loadedforcedlinks)
3074                                 waypoint_load_links_hardwired();
3075                 }
3076                 else
3077                 {
3078                         // TODO: Make this check cleaner
3079                         local entity wp = findchain(classname, "waypoint");
3080                         if(time - wp.nextthink > 10)
3081                                 waypoint_save_links();
3082                 }
3083         }
3084         else
3085         {
3086                 botframe_spawnedwaypoints = TRUE;
3087                 waypoint_loadall();
3088                 if(!waypoint_load_links())
3089                         waypoint_schedulerelinkall();
3090         }
3091
3092         if (bot_list)
3093         {
3094                 // cycle the goal token from one bot to the next each frame
3095                 // (this prevents them from all doing spawnfunc_waypoint searches on the same
3096                 //  frame, which causes choppy framerates)
3097                 if (bot_strategytoken_taken)
3098                 {
3099                         bot_strategytoken_taken = FALSE;
3100                         if (bot_strategytoken)
3101                                 bot_strategytoken = bot_strategytoken.nextbot;
3102                         if (!bot_strategytoken)
3103                                 bot_strategytoken = bot_list;
3104                 }
3105
3106                 if (botframe_nextdangertime < time)
3107                 {
3108                         local float interval;
3109                         interval = cvar("bot_ai_dangerdetectioninterval");
3110                         if (botframe_nextdangertime < time - interval * 1.5)
3111                                 botframe_nextdangertime = time;
3112                         botframe_nextdangertime = botframe_nextdangertime + interval;
3113                         botframe_updatedangerousobjects(cvar("bot_ai_dangerdetectionupdates"));
3114                 }
3115         }
3116
3117         if (cvar("g_waypointeditor"))
3118                 botframe_showwaypointlinks();
3119
3120         if(time > bot_cvar_nextthink)
3121         {
3122                 if(currentbots>0)
3123                         bot_custom_weapon_priority_setup();
3124                 bot_cvar_nextthink = time + 5;
3125         }
3126 };