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