]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Merge branch 'terencehill/bot_waypoints' into terencehill/bot_ai
authorterencehill <piuntn@gmail.com>
Mon, 15 Jan 2018 16:57:12 +0000 (17:57 +0100)
committerterencehill <piuntn@gmail.com>
Mon, 15 Jan 2018 16:57:12 +0000 (17:57 +0100)
# Conflicts:
# qcsrc/server/mutators/mutator/gamemode_race.qc

1  2 
defaultServer.cfg
qcsrc/common/t_items.qc
qcsrc/common/t_items.qh
qcsrc/common/triggers/trigger/jumppads.qc
qcsrc/server/bot/api.qh
qcsrc/server/bot/default/bot.qc
qcsrc/server/bot/default/navigation.qc
qcsrc/server/bot/default/navigation.qh
qcsrc/server/bot/default/waypoints.qc
qcsrc/server/mutators/mutator/gamemode_cts.qc
qcsrc/server/mutators/mutator/gamemode_race.qc

diff --combined defaultServer.cfg
index 61aab3a4ec54ba5e2f65479be12e53fb1a14ff07,db91e385495a17e66d07aa3dfb8c955476085f2f..739746267769380cc8cd599a5f944bc91e477a9d
@@@ -116,8 -116,7 +116,8 @@@ set bot_debug_goalstack 0 "Visualize th
  set bot_wander_enable 1 "Have bots wander around if they are unable to reach any useful goal. Disable only for debugging purposes."
  // general bot AI cvars
  set bot_ai_thinkinterval 0.05
 -set bot_ai_strategyinterval 5 "How often a new objective is chosen"
 +set bot_ai_strategyinterval 7 "How often a new objective is chosen"
 +set bot_ai_strategyinterval_movingtarget 5.5 "How often a new objective is chosen when current objective can move"
  set bot_ai_enemydetectioninterval 2 "How often bots pick a new target"
  set bot_ai_enemydetectionradius 10000 "How far bots can see enemies"
  set bot_ai_dodgeupdateinterval 0.2 "How often scan for items to dodge. Currently not in use."
@@@ -203,7 -202,6 +203,6 @@@ set g_casings 2 "specifies which casing
  set g_norecoil 0 "if set to 1 shooting weapons won't make you crosshair to move upwards (recoil)"
  set g_maplist_mostrecent "" "contains the name of the maps that were most recently played"
  set g_maplist_mostrecent_count 3      "number of most recent maps that are blocked from being played again"
- set g_maplist "" "the list of maps to be cycled among (is autogenerated if empty)"
  set g_maplist_index 0 "this is used internally for saving position in maplist cycle"
  set g_maplist_selectrandom 0  "if 1, a random map will be chosen as next map - DEPRECATED in favor of g_maplist_shuffle"
  set g_maplist_shuffle 1       "new randomization method: like selectrandom, but avoid playing the same maps in short succession. This works by taking out the first element and inserting it into g_maplist with a bias to the end of the list"
diff --combined qcsrc/common/t_items.qc
index 7409a669b450a199f70b821bd7e8aa011ca3da4e,fa6d0f6f313510dd1bc6a8e1a6e921a425d7e1b0..feefd10985aa37763770520146704e307b78ae87
@@@ -533,7 -533,7 +533,7 @@@ void Item_Respawn (entity this
  
  void Item_RespawnCountdown (entity this)
  {
-       if(this.count >= ITEM_RESPAWN_TICKS)
+       if(this.item_respawncounter >= ITEM_RESPAWN_TICKS)
        {
                if(this.waypointsprite_attached)
                        WaypointSprite_Kill(this.waypointsprite_attached);
        else
        {
                this.nextthink = time + 1;
-               this.count += 1;
-               if(this.count == 1)
+               this.item_respawncounter += 1;
+               if(this.item_respawncounter == 1)
                {
                        do {
                                {
                        });
  
                        WaypointSprite_Ping(this.waypointsprite_attached);
-                       //WaypointSprite_UpdateHealth(this.waypointsprite_attached, this.count);
+                       //WaypointSprite_UpdateHealth(this.waypointsprite_attached, this.item_respawncounter);
                }
        }
  }
@@@ -607,7 -607,7 +607,7 @@@ void Item_ScheduleRespawnIn(entity e, f
                setthink(e, Item_RespawnCountdown);
                e.nextthink = time + max(0, t - ITEM_RESPAWN_TICKS);
                e.scheduledrespawntime = e.nextthink + ITEM_RESPAWN_TICKS;
-               e.count = 0;
+               e.item_respawncounter = 0;
                if(Item_ItemsTime_Allow(e.itemdef) || (e.weapons & WEPSET_SUPERWEAPONS))
                {
                        t = Item_ItemsTime_UpdateTime(e, e.scheduledrespawntime);
@@@ -1126,6 -1126,8 +1126,6 @@@ float ammo_pickupevalfunc(entity player
        return rating;
  }
  
 -.int item_group;
 -.int item_group_count;
  float healtharmor_pickupevalfunc(entity player, entity item)
  {
        float c = 0;
diff --combined qcsrc/common/t_items.qh
index fd1c9d248eba1b9aee5f11dcc41ab55bac6b2a86,3740c9a576d5ee045a8d80660fbbbc6170399cc1..5d815cef928419eebc8b281fbba3e2423d275cbe
@@@ -26,8 -26,6 +26,8 @@@ const int ISF_SIZE                            = BIT(7)
  
  #ifdef SVQC
  void StartItem(entity this, entity a);
 +.int item_group;
 +.int item_group_count;
  #endif
  
  #ifdef CSQC
@@@ -64,6 -62,8 +64,8 @@@ const float ITEM_RESPAWN_TICKS = 10
  .float max_armorvalue;
  .float pickup_anyway;
  
+ .float item_respawncounter;
  void Item_Show (entity e, float mode);
  
  void Item_Respawn (entity this);
index 5bb6d39877ac610803f0a9fb029db9d36420554b,11ea2f360aba51619735f23d109e857d9a554510..9e40cfd40178224fc9c48efe76cece7de982253a
@@@ -207,13 -207,16 +207,16 @@@ bool jumppad_push(entity this, entity t
                                        centerprint(targ, this.message);
                        }
                        else
+                       {
                                targ.lastteleporttime = time;
+                               targ.lastteleport_origin = targ.origin;
+                       }
  
                        if (!IS_DEAD(targ))
                                animdecide_setaction(targ, ANIMACTION_JUMP, true);
                }
                else
 -                      targ.jumppadcount = true;
 +                      targ.jumppadcount = 1;
  
                // reset tracking of who pushed you into a hazard (for kill credit)
                targ.pushltime = 0;
@@@ -278,23 -281,27 +281,27 @@@ bool trigger_push_testorigin(entity tra
        if(trace_startsolid)
                return false;
  
-       if(!jp.height)
+       if (!jp.height)
        {
                // since tracetoss starting from jumppad's origin often fails when target
                // is very close to real destination, start it directly from target's
                // origin instead
+               vector ofs = '0 0 0';
+               if (vdist(vec2(tracetest_ent.velocity), <, autocvar_sv_maxspeed))
+                       ofs = stepheightvec;
                tracetest_ent.velocity.z = 0;
-               setorigin(tracetest_ent, targ.origin + stepheightvec);
+               setorigin(tracetest_ent, targ.origin + ofs);
                tracetoss(tracetest_ent, tracetest_ent);
-               if(trace_startsolid)
+               if (trace_startsolid && ofs.z)
                {
-                       setorigin(tracetest_ent, targ.origin + stepheightvec / 2);
+                       setorigin(tracetest_ent, targ.origin + ofs / 2);
                        tracetoss(tracetest_ent, tracetest_ent);
-                       if(trace_startsolid)
+                       if (trace_startsolid && ofs.z)
                        {
                                setorigin(tracetest_ent, targ.origin);
                                tracetoss(tracetest_ent, tracetest_ent);
-                               if(trace_startsolid)
+                               if (trace_startsolid)
                                        return false;
                        }
                }
        return true;
  }
  #endif
 -void trigger_push_findtarget(entity this)
 +
 +/// if (item != NULL) returns true if the item can be reached by using this jumppad, false otherwise
 +/// if (item == NULL) tests jumppad's trajectory and eventually spawns waypoints for it (return value doesn't matter)
 +bool trigger_push_test(entity this, entity item)
  {
        // first calculate a typical start point for the jump
        vector org = (this.absmin + this.absmax) * 0.5;
-       org.z = this.absmax.z - PL_MIN_CONST.z;
+       org.z = this.absmax.z - PL_MIN_CONST.z - 10;
  
        if (this.target)
        {
                        setsize(e, PL_MIN_CONST, PL_MAX_CONST);
                        e.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
                        e.velocity = trigger_push_calculatevelocity(org, t, this.height, e);
 +
 +                      if(item)
 +                      {
 +                              setorigin(e, org);
 +                              tracetoss(e, e);
 +                              bool r = (trace_ent == item);
 +                              delete(e);
 +                              return r;
 +                      }
 +
                        vel = e.velocity;
                        vector best_target = '0 0 0';
                        vector best_org = '0 0 0';
  #endif
                }
  
 +              if(item)
 +                      return false;
 +
                if(!n)
                {
                        // no dest!
  #ifdef SVQC
                        objerror (this, "Jumppad with nonexistant target");
  #endif
 -                      return;
 +                      return false;
                }
                else if(n == 1)
                {
                setorigin(e, org);
                e.velocity = this.movedir;
                tracetoss(e, e);
 +              if(item)
 +              {
 +                      bool r = (trace_ent == item);
 +                      delete(e);
 +                      return r;
 +              }
                if (!(boxesoverlap(this.absmin, this.absmax + eZ * 50, trace_endpos + PL_MIN_CONST, trace_endpos + PL_MAX_CONST)))
                        waypoint_spawnforteleporter(this, trace_endpos, vlen(trace_endpos - org) / vlen(e.velocity), e);
                delete(e);
  
        defer(this, 0.1, trigger_push_updatelink);
  #endif
 +      return true;
 +}
 +
 +void trigger_push_findtarget(entity this)
 +{
 +      trigger_push_test(this, NULL);
  }
  
  #ifdef SVQC
@@@ -517,8 -496,6 +524,8 @@@ spawnfunc(trigger_push
  
        trigger_push_link(this); // link it now
  
 +      IL_PUSH(g_jumppads, this);
 +
        // this must be called to spawn the teleport waypoints for bots
        InitializeEntity(this, trigger_push_findtarget, INITPRIO_FINDTARGET);
  }
diff --combined qcsrc/server/bot/api.qh
index 3dab2c26bf94287485a592974c4e06e4ef9a0ba1,86442178c1e0068b4b24886fa7a79f2caefeed58..51ac148fc13c42c1185866cd87d6430165f61b8e
@@@ -6,7 -6,7 +6,7 @@@
  
  const int WAYPOINTFLAG_GENERATED = BIT(23);
  const int WAYPOINTFLAG_ITEM = BIT(22);
 -const int WAYPOINTFLAG_TELEPORT = BIT(21);
 +const int WAYPOINTFLAG_TELEPORT = BIT(21); // teleports, warpzones and jumppads
  const int WAYPOINTFLAG_NORELINK = BIT(20);
  const int WAYPOINTFLAG_PERSONAL = BIT(19);
  const int WAYPOINTFLAG_PROTECTED = BIT(18);  // Useless WP detection never kills these.
@@@ -23,7 -23,6 +23,7 @@@ float bot_weapons_far[Weapons_MAX]
  float bot_weapons_mid[Weapons_MAX];
  float skill;
  
 +.float bot_tracewalk_time;
  .float bot_attack;
  .float bot_dodgerating;
  .float bot_dodge;
  .float bot_moveskill; // moving technique
  .float bot_pickup;
  .float(entity player, entity item) bot_pickupevalfunc;
 -.float bot_strategytime;
  .string cleanname;
  .float havocbot_role_timeout;
 +.void(entity this) havocbot_role;
 +.void(entity this) havocbot_previous_role;
  .float isbot; // true if this client is actually a bot
  .float lastteleporttime;
+ .vector lastteleport_origin;
  .float navigation_hasgoals;
  .float nearestwaypointtimeout;
  .entity nearestwaypoint;
@@@ -74,16 -73,10 +75,16 @@@ void havocbot_goalrating_enemyplayers(e
  void havocbot_goalrating_items(entity this, float ratingscale, vector org, float sradius);
  void havocbot_goalrating_waypoints(entity this, float ratingscale, vector org, float sradius);
  
 +bool havocbot_goalrating_item_pickable_check_players(entity this, vector org, entity item, vector item_org);
 +
  vector havocbot_middlepoint;
  float havocbot_middlepoint_radius;
  vector havocbot_symmetryaxis_equation;
  
 +.float goalentity_lock_timeout;
 +.float ignoregoaltime;
 +.entity ignoregoal;
 +
  .entity bot_basewaypoint;
  .bool navigation_dynamicgoal;
  void navigation_dynamicgoal_init(entity this, bool initially_static);
@@@ -92,19 -85,10 +93,19 @@@ void navigation_dynamicgoal_unset(entit
  entity navigation_findnearestwaypoint(entity ent, float walkfromwp);
  void navigation_goalrating_end(entity this);
  void navigation_goalrating_start(entity this);
 +void navigation_goalrating_timeout_set(entity this);
 +void navigation_goalrating_timeout_force(entity this);
 +void navigation_goalrating_timeout_expire(entity this, float seconds);
 +bool navigation_goalrating_timeout(entity this);
 +bool navigation_goalrating_timeout_can_be_anticipated(entity this);
  void navigation_markroutes(entity this, entity fixed_source_waypoint);
  void navigation_markroutes_inverted(entity fixed_source_waypoint);
  void navigation_routerating(entity this, entity e, float f, float rangebias);
  
 +vector get_closer_dest(entity ent, vector org);
 +
 +void set_tracewalk_dest(entity ent, vector org, bool fix_player_dest);
 +vector set_tracewalk_dest_2(entity ent, vector org);
  bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float end_height, float movemode);
  
  void waypoint_remove_fromeditor(entity pl);
@@@ -119,3 -103,6 +120,3 @@@ void waypoint_spawnforteleporter_wz(ent
  void waypoint_spawn_fromeditor(entity pl);
  entity waypoint_spawn(vector m1, vector m2, float f);
  void waypoint_unreachable(entity pl);
 -
 -.entity goalcurrent;
 -void navigation_clearroute(entity this);
index 276d5b53158e50deb6583521e74a034908fd979b,a056b25a768de115a62c371f47a351bd6dba2eb6..af238029abd85042eb7e72e8f2de43f0a0c6dd94
@@@ -133,7 -133,7 +133,7 @@@ void bot_think(entity this
                if (this.deadflag == DEAD_DEAD)
                {
                        PHYS_INPUT_BUTTON_JUMP(this) = true; // press jump to respawn
 -                      this.bot_strategytime = 0;
 +                      navigation_goalrating_timeout_force(this);
                }
        }
        else if(this.aistatus & AI_STATUS_STUCK)
@@@ -759,8 -759,7 +759,7 @@@ void bot_serverframe(
        {
                botframe_spawnedwaypoints = true;
                waypoint_loadall();
-               if(!waypoint_load_links())
-                       waypoint_schedulerelinkall(); // link all the autogenerated waypoints (teleporters)
+               waypoint_load_links();
        }
  
        if (bot_list)
                //  frame, which causes choppy framerates)
                if (bot_strategytoken_taken)
                {
 +                      // give goal token to the first bot without goals; if all bots don't have
 +                      // any goal (or are dead/frozen) simply give it to the next one
                        bot_strategytoken_taken = false;
 -                      if (bot_strategytoken)
 -                              bot_strategytoken = bot_strategytoken.nextbot;
 -                      if (!bot_strategytoken)
 -                              bot_strategytoken = bot_list;
 +                      entity bot_strategytoken_save = bot_strategytoken;
 +                      while (true)
 +                      {
 +                              if (bot_strategytoken)
 +                                      bot_strategytoken = bot_strategytoken.nextbot;
 +                              if (!bot_strategytoken)
 +                                      bot_strategytoken = bot_list;
 +
 +                              if (!(IS_DEAD(bot_strategytoken) || STAT(FROZEN, bot_strategytoken))
 +                                      && !bot_strategytoken.goalcurrent)
 +                                      break;
 +
 +                              if (!bot_strategytoken_save) // break loop if all the bots are dead or frozen
 +                                      break;
 +                              if (bot_strategytoken == bot_strategytoken_save)
 +                                      bot_strategytoken_save = NULL; // looped through all the bots
 +                      }
                }
  
                if (botframe_nextdangertime < time)
index c7c3c553cd7931b9506e42fbc615faaa48e8fb14,8d592269341022695a551e8195eea9d19d88646e..3754897bda3fe5ff2554409d318933ad3e11e38a
  
  .float speed;
  
 +void navigation_goalrating_timeout_set(entity this)
 +{
 +      if(IS_MOVABLE(this.goalentity))
 +              this.bot_strategytime = time + autocvar_bot_ai_strategyinterval_movingtarget;
 +      else
 +              this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
 +}
 +
 +// use this when current goal must be discarded immediately
 +void navigation_goalrating_timeout_force(entity this)
 +{
 +      navigation_goalrating_timeout_expire(this, 0);
 +}
 +
 +// use this when current goal can be kept for a short while to increase the chance
 +// of bot touching a waypoint, which helps to find a new goal more efficiently
 +void navigation_goalrating_timeout_expire(entity this, float seconds)
 +{
 +      if (seconds <= 0)
 +              this.bot_strategytime = 0;
 +      else if (this.bot_strategytime > time + seconds)
 +              this.bot_strategytime = time + seconds;
 +}
 +
 +bool navigation_goalrating_timeout(entity this)
 +{
 +      return this.bot_strategytime < time;
 +}
 +
 +#define MAX_CHASE_DISTANCE 700
 +bool navigation_goalrating_timeout_can_be_anticipated(entity this)
 +{
 +      if(time > this.bot_strategytime - (IS_MOVABLE(this.goalentity) ? 3 : 2))
 +              return true;
 +
 +      if (this.goalentity.bot_pickup && time > this.bot_strategytime - 5)
 +      {
 +              vector gco = (this.goalentity.absmin + this.goalentity.absmax) * 0.5;
 +              if(!havocbot_goalrating_item_pickable_check_players(this, this.origin, this.goalentity, gco))
 +              {
 +                      this.ignoregoal = this.goalentity;
 +                      this.ignoregoaltime = time + autocvar_bot_ai_ignoregoal_timeout;
 +                      return true;
 +              }
 +      }
 +      return false;
 +}
 +
  void navigation_dynamicgoal_init(entity this, bool initially_static)
  {
        this.navigation_dynamicgoal = true;
@@@ -78,8 -30,6 +78,8 @@@
  void navigation_dynamicgoal_set(entity this)
  {
        this.nearestwaypointtimeout = time;
 +      if (this.nearestwaypoint)
 +              this.nearestwaypointtimeout += 2;
  }
  
  void navigation_dynamicgoal_unset(entity this)
        this.nearestwaypointtimeout = -1;
  }
  
 +// returns point of ent closer to org
 +vector get_closer_dest(entity ent, vector org)
 +{
 +      vector dest = '0 0 0';
 +      if ((ent.classname != "waypoint") || ent.wpisbox)
 +      {
 +              vector wm1 = ent.origin + ent.mins;
 +              vector wm2 = ent.origin + ent.maxs;
 +              dest.x = bound(wm1.x, org.x, wm2.x);
 +              dest.y = bound(wm1.y, org.y, wm2.y);
 +              dest.z = bound(wm1.z, org.z, wm2.z);
 +      }
 +      else
 +              dest = ent.origin;
 +      return dest;
 +}
 +
 +void set_tracewalk_dest(entity ent, vector org, bool fix_player_dest)
 +{
 +      if ((ent.classname != "waypoint") || ent.wpisbox)
 +      {
 +              vector wm1 = ent.origin + ent.mins;
 +              vector wm2 = ent.origin + ent.maxs;
 +              if (IS_PLAYER(ent) || IS_MONSTER(ent))
 +              {
 +                      // move destination point out of player bbox otherwise tracebox always fails
 +                      // (if bot_navigation_ignoreplayers is false)
 +                      wm1 += vec2(PL_MIN_CONST) + '-1 -1 0';
 +                      wm2 += vec2(PL_MAX_CONST) + '1 1 0';
 +              }
 +              // set destination point to x and y coords of ent that are closer to org
 +              // z coord is set to ent's min height
 +              tracewalk_dest.x = bound(wm1.x, org.x, wm2.x);
 +              tracewalk_dest.y = bound(wm1.y, org.y, wm2.y);
 +              tracewalk_dest.z = wm1.z;
 +              tracewalk_dest_height = wm2.z - wm1.z; // destination height
 +      }
 +      else
 +      {
 +              tracewalk_dest = ent.origin;
 +              tracewalk_dest_height = 0;
 +      }
 +      if (fix_player_dest && IS_PLAYER(ent) && !IS_ONGROUND(ent))
 +      {
 +              // snap player to the ground
 +              if (org.x == tracewalk_dest.x && org.y == tracewalk_dest.y)
 +              {
 +                      // bot is right under the player
 +                      tracebox(ent.origin, ent.mins, ent.maxs, ent.origin - '0 0 700', MOVE_NORMAL, ent);
 +                      tracewalk_dest = trace_endpos;
 +                      tracewalk_dest_height = 0;
 +              }
 +              else
 +              {
 +                      tracebox(tracewalk_dest, ent.mins, ent.maxs, tracewalk_dest - '0 0 700', MOVE_NORMAL, ent);
 +                      if (!trace_startsolid && tracewalk_dest.z - trace_endpos.z > 0)
 +                      {
 +                              tracewalk_dest_height = tracewalk_dest.z - trace_endpos.z;
 +                              tracewalk_dest.z = trace_endpos.z;
 +                      }
 +              }
 +      }
 +}
 +
 +// returns point of ent closer to org
 +vector set_tracewalk_dest_2(entity ent, vector org)
 +{
 +      vector closer_dest = '0 0 0';
 +      if ((ent.classname != "waypoint") || ent.wpisbox)
 +      {
 +              vector wm1 = ent.origin + ent.mins;
 +              vector wm2 = ent.origin + ent.maxs;
 +              closer_dest.x = bound(wm1.x, org.x, wm2.x);
 +              closer_dest.y = bound(wm1.y, org.y, wm2.y);
 +              closer_dest.z = bound(wm1.z, org.z, wm2.z);
 +              // set destination point to x and y coords of ent that are closer to org
 +              // z coord is set to ent's min height
 +              tracewalk_dest.x = closer_dest.x;
 +              tracewalk_dest.y = closer_dest.y;
 +              tracewalk_dest.z = wm1.z;
 +              tracewalk_dest_height = wm2.z - wm1.z; // destination height
 +      }
 +      else
 +      {
 +              closer_dest = ent.origin;
 +              tracewalk_dest = closer_dest;
 +              tracewalk_dest_height = 0;
 +      }
 +      return closer_dest;
 +}
 +
  bool navigation_check_submerged_state(entity ent, vector pos)
  {
        bool submerged;
@@@ -727,10 -586,9 +727,10 @@@ bool tracewalk(entity e, vector start, 
  void navigation_clearroute(entity this)
  {
        this.goalcurrent_prev = this.goalcurrent;
 -      this.goalcurrent_distance = 10000000;
 +      this.goalcurrent_distance_2d = FLOAT_MAX;
 +      this.goalcurrent_distance_z = FLOAT_MAX;
        this.goalcurrent_distance_time = 0;
 -      //print("bot ", etos(this), " clear\n");
 +      this.goalentity_lock_timeout = 0;
        this.goalentity = NULL;
        this.goalcurrent = NULL;
        this.goalstack01 = NULL;
  void navigation_pushroute(entity this, entity e)
  {
        this.goalcurrent_prev = this.goalcurrent;
 -      this.goalcurrent_distance = 10000000;
 +      this.goalcurrent_distance_2d = FLOAT_MAX;
 +      this.goalcurrent_distance_z = FLOAT_MAX;
        this.goalcurrent_distance_time = 0;
        //print("bot ", etos(this), " push ", etos(e), "\n");
        if(this.goalstack31 == this.goalentity)
  void navigation_poproute(entity this)
  {
        this.goalcurrent_prev = this.goalcurrent;
 -      this.goalcurrent_distance = 10000000;
 +      this.goalcurrent_distance_2d = FLOAT_MAX;
 +      this.goalcurrent_distance_z = FLOAT_MAX;
        this.goalcurrent_distance_time = 0;
        //print("bot ", etos(this), " pop\n");
        if(this.goalcurrent == this.goalentity)
 +      {
                this.goalentity = NULL;
 +              this.goalentity_lock_timeout = 0;
 +      }
        this.goalcurrent = this.goalstack01;
        this.goalstack01 = this.goalstack02;
        this.goalstack02 = this.goalstack03;
@@@ -902,14 -755,7 +902,14 @@@ entity navigation_findnearestwaypoint_w
        IL_EACH(g_waypoints, it != ent && it != except,
        {
                if(boxesoverlap(pm1, pm2, it.absmin, it.absmax))
 +              {
 +                      if(!autocvar_g_waypointeditor && walkfromwp && !ent.navigation_dynamicgoal)
 +                      {
 +                              waypoint_clearlinks(ent); // initialize wpXXmincost fields
 +                              navigation_item_addlink(it, ent);
 +                      }
                        return it;
 +              }
        });
  
        vector org = ent.origin;
                te_plasmaburn(org);
  
        entity best = NULL;
 -      vector v = '0 0 0', v2 = '0 0 0';
 -      float v2_height = 0;
 +      vector v = '0 0 0';
  
        if(ent.size && !IS_PLAYER(ent))
        {
                        if(walkfromwp && (it.wpflags & WAYPOINTFLAG_NORELINK))
                                continue;
  
 -                      SET_TRACEWALK_DESTCOORDS(it, org, v2, v2_height);
 -                      if(vdist(v2 - org, <, 1050))
 -                      if(tracewalk(ent, org, PL_MIN_CONST, PL_MAX_CONST, v2, v2_height, bot_navigation_movemode))
 +                      set_tracewalk_dest(ent, it.origin, false);
 +                      if (vdist(tracewalk_dest - it.origin, <, 1050)
 +                              && tracewalk(ent, it.origin, PL_MIN_CONST, PL_MAX_CONST,
 +                              tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode))
 +                      {
                                navigation_item_addlink(it, ent);
 +                      }
                });
        }
  
                if(walkfromwp && (it.wpflags & WAYPOINTFLAG_NORELINK))
                        continue;
                v = it.origin;
 -              if(walkfromwp)
 -                      SET_TRACEWALK_DESTCOORDS(ent, v, v2, v2_height);
 +
 +              if (walkfromwp)
 +              {
 +                      set_tracewalk_dest(ent, v, true);
 +                      if (trace_ent == ent)
 +                      {
 +                              bestdist = 0;
 +                              best = it;
 +                              break;
 +                      }
 +              }
                else
 -                      SET_TRACEWALK_DESTCOORDS(it, org, v2, v2_height);
 -              if(navigation_waypoint_will_link(v, org, ent, v2, v2_height, v2, v2_height, walkfromwp, bestdist))
 +                      set_tracewalk_dest(it, org, false);
 +
 +              if (navigation_waypoint_will_link(v, org, ent,
 +                      tracewalk_dest, tracewalk_dest_height,
 +                      tracewalk_dest, tracewalk_dest_height, walkfromwp, bestdist))
                {
 -                      bestdist = vlen(v - org);
 +                      if (walkfromwp)
 +                              bestdist = vlen(tracewalk_dest - v);
 +                      else
 +                              bestdist = vlen(v - org);
                        best = it;
                }
        });
 +      if(!best && !ent.navigation_dynamicgoal)
 +      {
 +              int solid_save = ent.solid;
 +              ent.solid = SOLID_BSP;
 +              IL_EACH(g_jumppads, true,
 +              {
 +                      if(trigger_push_test(it, ent))
 +                      {
 +                              best = it.nearestwaypoint;
 +                              break;
 +                      }
 +              });
 +              ent.solid = solid_save;
 +      }
        return best;
  }
  entity navigation_findnearestwaypoint(entity ent, float walkfromwp)
  // finds the waypoints near the bot initiating a navigation query
  float navigation_markroutes_nearestwaypoints(entity this, float maxdist)
  {
 -      vector v = '0 0 0';
        //navigation_testtracewalk = true;
        int c = 0;
 -      float v_height = 0;
        IL_EACH(g_waypoints, !it.wpconsidered,
        {
 -              SET_TRACEWALK_DESTCOORDS(it, this.origin, v, v_height);
 +              set_tracewalk_dest(it, this.origin, false);
  
 -              vector diff = v - this.origin;
 +              vector diff = tracewalk_dest - this.origin;
                diff.z = max(0, diff.z);
                if(vdist(diff, <, maxdist))
                {
                        it.wpconsidered = true;
 -                      if (tracewalk(this, this.origin, this.mins, this.maxs, v, v_height, bot_navigation_movemode))
 +                      if (tracewalk(this, this.origin, this.mins, this.maxs,
 +                              tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode))
                        {
 -                              it.wpnearestpoint = v;
 -                              it.wpcost = waypoint_gettravelcost(this.origin, v, this, it) + it.dmg;
 +                              it.wpnearestpoint = tracewalk_dest;
 +                              it.wpcost = waypoint_gettravelcost(this.origin, tracewalk_dest, this, it) + it.dmg;
                                it.wpfire = 1;
                                it.enemy = NULL;
                                c = c + 1;
@@@ -1361,11 -1177,29 +1361,11 @@@ void navigation_routerating(entity this
                if ((!e.nearestwaypoint || e.navigation_dynamicgoal)
                        && e.nearestwaypointtimeout >= 0 && time > e.nearestwaypointtimeout)
                {
 -                      nwp = navigation_findnearestwaypoint(e, true);
 -                      if(nwp)
 -                      {
 -                              e.nearestwaypoint = nwp;
 -
 -                              vector m1 = nwp.absmin, m2 = nwp.absmax;
 -                              m1.x = nwp.origin.x; m1.y = nwp.origin.y;
 -                              m2.x = nwp.origin.x; m2.y = nwp.origin.y;
 -                              vector ve = (e.absmin - e.absmax) * 0.5;
 -                              ve.x = bound(m1.x, ve.x, m2.x);
 -                              ve.y = bound(m1.y, ve.y, m2.y);
 -                              ve.z = bound(m1.z, ve.z, m2.z);
 -
 -                              m1 = e.absmin; m2 = e.absmax;
 -                              m1.x = e.origin.x; m1.y = e.origin.y;
 -                              m2.x = e.origin.x; m2.y = e.origin.y;
 -                              vector vnwp = nwp.origin;
 -                              vnwp.x = bound(m1.x, vnwp.x, m2.x);
 -                              vnwp.y = bound(m1.y, vnwp.y, m2.y);
 -                              vnwp.z = bound(m1.z, vnwp.z, m2.z);
 -                              e.nearestwaypoint_dist = vlen(ve - vnwp);
 -                      }
 +                      if(IS_BOT_CLIENT(e) && e.goalcurrent && e.goalcurrent.classname == "waypoint")
 +                              e.nearestwaypoint = nwp = e.goalcurrent;
                        else
 +                              e.nearestwaypoint = nwp = navigation_findnearestwaypoint(e, true);
 +                      if(!nwp)
                        {
                                LOG_DEBUG("FAILED to find a nearest waypoint to '", e.classname, "' #", etos(e));
  
        }
  
        LOG_DEBUG("-- checking ", e.classname, " (with cost ", ftos(nwp.wpcost), ")");
 -      if (nwp)
 -      if (nwp.wpcost < 10000000)
 +      if (nwp && nwp.wpcost < 10000000)
        {
                //te_wizspike(nwp.wpnearestpoint);
 -              float cost = nwp.wpcost + waypoint_gettravelcost(nwp.wpnearestpoint, goal_org, nwp, e);
 +              float nwptoitem_cost = 0;
 +              if(nwp.wpflags & WAYPOINTFLAG_TELEPORT)
 +                      nwptoitem_cost = nwp.wp00mincost;
 +              else
 +                      nwptoitem_cost = waypoint_gettravelcost(nwp.wpnearestpoint, goal_org, nwp, e);
 +              float cost = nwp.wpcost + nwptoitem_cost;
                LOG_DEBUG(e.classname, " ", ftos(f), "/(1+", ftos(cost), "/", ftos(rangebias), ") = ");
                f = f * rangebias / (rangebias + cost);
                LOG_DEBUG("considering ", e.classname, " (with rating ", ftos(f), ")");
@@@ -1446,13 -1276,11 +1446,13 @@@ bool navigation_routetogoal(entity this
                return true;
  
        // if it can reach the goal there is nothing more to do
 -      vector dest = '0 0 0';
 -      float dest_height = 0;
 -      SET_TRACEWALK_DESTCOORDS(e, startposition, dest, dest_height);
 -      if (tracewalk(this, startposition, STAT(PL_MIN, this), STAT(PL_MAX, this), dest, dest_height, bot_navigation_movemode))
 +      set_tracewalk_dest(e, startposition, true);
 +      if ((!IS_MOVABLE(this.goalcurrent) || vdist(tracewalk_dest - this.origin, <, MAX_CHASE_DISTANCE))
 +              && (trace_ent == this || tracewalk(this, startposition, STAT(PL_MIN, this), STAT(PL_MAX, this),
 +              tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode)))
 +      {
                return true;
 +      }
  
        entity nearest_wp = NULL;
        // see if there are waypoints describing a path to the item
        if(nearest_wp && nearest_wp.enemy)
        {
                // often path can be optimized by not adding the nearest waypoint
 -              if (this.goalentity.nearestwaypoint_dist < 8)
 -                      e = nearest_wp.enemy;
 -              else
 +              if (this.goalentity.navigation_dynamicgoal || autocvar_g_waypointeditor)
                {
 -                      if (this.goalentity.navigation_dynamicgoal || autocvar_g_waypointeditor)
 +                      if (nearest_wp.enemy.wpcost < autocvar_bot_ai_strategyinterval_movingtarget)
                        {
 -                              SET_TRACEWALK_DESTCOORDS(this.goalentity, nearest_wp.enemy.origin, dest, dest_height);
 -                              if(vdist(dest - nearest_wp.enemy.origin, <, 1050))
 -                              if(tracewalk(this, nearest_wp.enemy.origin, STAT(PL_MIN, this), STAT(PL_MAX, this), dest, dest_height, bot_navigation_movemode))
 +                              if (vdist(vec2(this.goalentity.origin - nearest_wp.origin), <, 32))
                                        e = nearest_wp.enemy;
 +                              else
 +                              {
 +                                      set_tracewalk_dest(this.goalentity, nearest_wp.enemy.origin, true);
 +                                      if (trace_ent == this || (vdist(tracewalk_dest - nearest_wp.enemy.origin, <, 1050)
 +                                              && vlen2(tracewalk_dest - nearest_wp.enemy.origin) < vlen2(nearest_wp.origin - nearest_wp.enemy.origin)
 +                                              && tracewalk(this, nearest_wp.enemy.origin, STAT(PL_MIN, this), STAT(PL_MAX, this),
 +                                              tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode)))
 +                                      {
 +                                              e = nearest_wp.enemy;
 +                                      }
 +                              }
                        }
 -                      else if(navigation_item_islinked(nearest_wp.enemy, this.goalentity))
 -                              e = nearest_wp.enemy;
                }
 +              else if(navigation_item_islinked(nearest_wp.enemy, this.goalentity))
 +                      e = nearest_wp.enemy;
        }
  
        for (;;)
        return false;
  }
  
 +// shorten path by removing intermediate goals
 +void navigation_shortenpath(entity this)
 +{
 +      if (!this.goalstack01 || wasfreed(this.goalstack01))
 +              return;
 +      if (this.bot_tracewalk_time > time)
 +              return;
 +      this.bot_tracewalk_time = max(time, this.bot_tracewalk_time) + 0.25;
 +
 +      bool cut_allowed = false;
 +      entity next = this.goalentity;
 +      // evaluate whether bot can discard current route and chase directly a player, trying to
 +      // keep waypoint route as long as possible, as it is safer and faster (bot can bunnyhop)
 +      if (IS_MOVABLE(next))
 +      {
 +              set_tracewalk_dest(next, this.origin, true);
 +              if (vdist(this.origin - tracewalk_dest, <, 200))
 +                      cut_allowed = true;
 +              else if (vdist(tracewalk_dest - this.origin, <, MAX_CHASE_DISTANCE)
 +                      && vdist(tracewalk_dest - this.goalcurrent.origin, >, 200)
 +                      && vdist(this.origin - this.goalcurrent.origin, >, 100)
 +                      && checkpvs(this.origin + this.view_ofs, next))
 +              {
 +                      if (vlen2(next.origin - this.origin) < vlen2(this.goalcurrent.origin - this.origin))
 +                              cut_allowed = true;
 +                      else
 +                      {
 +                              vector deviation = vectoangles(this.goalcurrent.origin - this.origin) - vectoangles(next.origin - this.origin);
 +                              while (deviation.y < -180) deviation.y += 360;
 +                              while (deviation.y > 180) deviation.y -= 360;
 +                              if (fabs(deviation.y) > 25)
 +                                      cut_allowed = true;
 +                      }
 +              }
 +              if (cut_allowed)
 +              {
 +                      if (trace_ent == this || tracewalk(this, this.origin, this.mins, this.maxs,
 +                              tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode))
 +                      {
 +                              LOG_DEBUG("path optimized for ", this.netname, ", route cleared");
 +                              do
 +                              {
 +                                      navigation_poproute(this);
 +                              }
 +                              while (this.goalcurrent != next);
 +                      }
 +                      return;
 +              }
 +      }
 +
 +      next = this.goalstack01;
 +      // if for some reason the bot is closer to the next goal, pop the current one
 +      if (!IS_MOVABLE(next) // already checked in the previous case
 +              && vlen2(this.goalcurrent.origin - next.origin) > vlen2(next.origin - this.origin)
 +              && checkpvs(this.origin + this.view_ofs, next))
 +      {
 +              set_tracewalk_dest(next, this.origin, true);
 +              cut_allowed = true;
 +      }
 +
 +      if (cut_allowed)
 +      {
 +              if (trace_ent == this || tracewalk(this, this.origin, this.mins, this.maxs,
 +                      tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode))
 +              {
 +                      LOG_DEBUG("path optimized for ", this.netname, ", removed a goal from the queue"); 
 +                      navigation_poproute(this);
 +              }
 +      }
 +}
 +
  // removes any currently touching waypoints from the goal stack
  // (this is how bots detect if they reached a goal)
 -void navigation_poptouchedgoals(entity this)
 +int navigation_poptouchedgoals(entity this)
  {
 +      int removed_goals = 0;
 +
 +      if(!this.goalcurrent)
 +              return removed_goals;
 +
        if(this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT)
        {
                // make sure jumppad is really hit, don't rely on distance based checks
                if(this.lastteleporttime > 0
                        && time - this.lastteleporttime < ((this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL) ? 2 : 0.15))
                {
+                       if (this.jumppadcount && !boxesoverlap(this.goalcurrent.absmin, this.goalcurrent.absmax,
+                               this.lastteleport_origin + STAT(PL_MIN, this), this.lastteleport_origin + STAT(PL_MAX, this)))
+                       {
+                               return;
+                       }
                        if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING)
                        if(this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL && this.goalcurrent.owner==this)
                        {
                                this.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED;
                        }
                        navigation_poproute(this);
 +                      this.lastteleporttime = 0;
 +                      ++removed_goals;
                }
                else
 -                      return;
 -      }
 -
 -      // If for some reason the bot is closer to the next goal, pop the current one
 -      if(this.goalstack01 && !wasfreed(this.goalstack01))
 -      if(vlen2(this.goalcurrent.origin - this.origin) > vlen2(this.goalstack01.origin - this.origin))
 -      if(checkpvs(this.origin + this.view_ofs, this.goalstack01))
 -      {
 -              vector dest = (this.goalstack01.absmin + this.goalstack01.absmax) * 0.5;
 -              dest.z = this.goalstack01.absmin.z;
 -              float dest_height = this.goalstack01.absmax.z - this.goalstack01.absmin.z;
 -              if(tracewalk(this, this.origin, this.mins, this.maxs, dest, dest_height, bot_navigation_movemode))
 -              {
 -                      LOG_DEBUG("path optimized for ", this.netname, ", removed a goal from the queue");
 -                      navigation_poproute(this);
 -                      if(this.goalcurrent && this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT)
 -                              return;
 -                      // TODO this may also be a nice idea to do "early" (e.g. by
 -                      // manipulating the vlen() comparisons) to shorten paths in
 -                      // general - this would make bots walk more "on rails" than
 -                      // "zigzagging" which they currently do with sufficiently
 -                      // random-like waypoints, and thus can make a nice bot
 -                      // personality property
 -              }
 +                      return removed_goals;
        }
  
        // Loose goal touching check when running
                                }
  
                                navigation_poproute(this);
 +                              ++removed_goals;
                                if(this.goalcurrent && this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT)
 -                                      return;
 +                                      return removed_goals;
                        }
                }
        }
                }
  
                navigation_poproute(this);
 +              ++removed_goals;
                if(this.goalcurrent && this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT)
 -                      return;
 +                      return removed_goals;
 +      }
 +      return removed_goals;
 +}
 +
 +entity navigation_get_really_close_waypoint(entity this)
 +{
 +      entity wp = this.goalcurrent;
 +      if(!wp)
 +              wp = this.goalcurrent_prev;
 +      if(!wp)
 +              return NULL;
 +      if(wp != this.goalcurrent_prev && vdist(wp.origin - this.origin, >, 50))
 +      {
 +              wp = this.goalcurrent_prev;
 +              if(!wp)
 +                      return NULL;
        }
 +      if(wp.classname != "waypoint")
 +      {
 +              wp = wp.nearestwaypoint;
 +              if(!wp)
 +                      return NULL;
 +      }
 +      if(vdist(wp.origin - this.origin, >, 50))
 +      {
 +              wp = NULL;
 +              IL_EACH(g_waypoints, !(it.wpflags & WAYPOINTFLAG_TELEPORT),
 +              {
 +                      if(vdist(it.origin - this.origin, <, 50))
 +                      {
 +                              wp = it;
 +                              break;
 +                      }
 +              });
 +              if(!wp)
 +                      return NULL;
 +      }
 +      if(wp.wpflags & WAYPOINTFLAG_TELEPORT)
 +              return NULL;
 +
 +      set_tracewalk_dest(wp, this.origin, false);
 +      if (!tracewalk(this, this.origin, STAT(PL_MIN, this), STAT(PL_MAX, this),
 +              tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode))
 +      {
 +              return NULL;
 +      }
 +      return wp;
  }
  
  // begin a goal selection session (queries spawnfunc_waypoint network)
@@@ -1716,10 -1440,9 +1722,10 @@@ void navigation_goalrating_start(entit
  
        this.navigation_jetpack_goal = NULL;
        navigation_bestrating = -1;
 +      entity wp = navigation_get_really_close_waypoint(this);
        navigation_clearroute(this);
        navigation_bestgoal = NULL;
 -      navigation_markroutes(this, NULL);
 +      navigation_markroutes(this, wp);
  }
  
  // ends a goal selection session (updates goal stack to the best goal)
@@@ -1799,9 -1522,10 +1805,9 @@@ void navigation_unstuck(entity this
                // evaluate the next goal on the queue
                float d = vlen2(this.origin - bot_waypoint_queue_goal.origin);
                LOG_DEBUG(this.netname, " evaluating ", bot_waypoint_queue_goal.classname, " with distance ", ftos(d));
 -              vector dest = (bot_waypoint_queue_goal.absmin + bot_waypoint_queue_goal.absmax) * 0.5;
 -              dest.z = bot_waypoint_queue_goal.absmin.z;
 -              float dest_height = bot_waypoint_queue_goal.absmax.z - bot_waypoint_queue_goal.absmin.z;
 -              if(tracewalk(bot_waypoint_queue_goal, this.origin, STAT(PL_MIN, this), STAT(PL_MAX, this), dest, dest_height, bot_navigation_movemode))
 +              set_tracewalk_dest(bot_waypoint_queue_goal, this.origin, false);
 +              if (tracewalk(bot_waypoint_queue_goal, this.origin, STAT(PL_MIN, this), STAT(PL_MAX, this),
 +                      tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode))
                {
                        if( d > bot_waypoint_queue_bestgoalrating)
                        {
                        {
                                LOG_DEBUG(this.netname, " stuck, reachable waypoint found, heading to it");
                                navigation_routetogoal(this, bot_waypoint_queue_bestgoal, this.origin);
 -                              this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
 +                              navigation_goalrating_timeout_set(this);
                                this.aistatus &= ~AI_STATUS_STUCK;
                        }
                        else
index c6d172cce9800bb87b040bd573a10ccbd40d31af,ccf2cecee9a0a14d152810a22585f862257d928c..75713e8eb3d8510ec8807646e85520f8efedd5d6
@@@ -25,13 -25,11 +25,13 @@@ entity navigation_bestgoal
  .entity goalstack28, goalstack29, goalstack30, goalstack31;
  
  .entity goalcurrent_prev;
 -.float goalcurrent_distance;
 +.float goalcurrent_distance_z;
 +.float goalcurrent_distance_2d;
  .float goalcurrent_distance_time;
  
 +.float goalentity_lock_timeout;
 +
  .entity nearestwaypoint;
 -.float nearestwaypoint_dist;
  .float nearestwaypointtimeout;
  
  /*
  #define navigation_item_addlink(from_wp, to_item) \
        waypoint_addlink_customcost(to_item, from_wp, waypoint_getlinkcost(from_wp, to_item))
  
 -// if ent is a box waypoint or an item v is set to coords of ent that are closer to org
 -#define SET_DESTCOORDS(ent, org, v) MACRO_BEGIN { \
 -      if ((ent.classname != "waypoint") || ent.wpisbox) { \
 -              vector wm1 = ent.origin + ent.mins; \
 -              vector wm2 = ent.origin + ent.maxs; \
 -              v.x = bound(wm1.x, org.x, wm2.x); \
 -              v.y = bound(wm1.y, org.y, wm2.y); \
 -              v.z = bound(wm1.z, org.z, wm2.z); \
 -      } else { \
 -              v = ent.origin; \
 -      } \
 -} MACRO_END
 -
 -// if ent is a box waypoint or an item v is set to coords of ent that are closer to org
 -// (but v.z is set to the lowest coord of ent), v_height is set to ent's height
 -#define SET_TRACEWALK_DESTCOORDS(ent, org, v, v_height) MACRO_BEGIN { \
 -      if ((ent.classname != "waypoint") || ent.wpisbox) { \
 -              vector wm1 = ent.origin + ent.mins; \
 -              vector wm2 = ent.origin + ent.maxs; \
 -              v.x = bound(wm1.x, org.x, wm2.x); \
 -              v.y = bound(wm1.y, org.y, wm2.y); \
 -              v.z = wm1.z; \
 -              v_height = wm2.z - wm1.z; \
 -      } else { \
 -              v = ent.origin; \
 -              v_height = 0; \
 -      } \
 -} MACRO_END
 -
 -// if ent is a box waypoint or an item v and v2 are set to coords of ent that are closer to org
 -// (but v2.z is set to the lowest coord of ent), v2_height is set to ent's height
 -#define SET_TRACEWALK_DESTCOORDS_2(ent, org, v, v2, v2_height) MACRO_BEGIN { \
 -      if ((ent.classname != "waypoint") || ent.wpisbox) { \
 -              vector wm1 = ent.origin + ent.mins; \
 -              vector wm2 = ent.origin + ent.maxs; \
 -              v.x = bound(wm1.x, org.x, wm2.x); \
 -              v.y = bound(wm1.y, org.y, wm2.y); \
 -              v.z = bound(wm1.z, org.z, wm2.z); \
 -              v2.x = v.x; \
 -              v2.y = v.y; \
 -              v2.z = wm1.z; \
 -              v2_height = wm2.z - wm1.z; \
 -      } else { \
 -              v = ent.origin; \
 -              v2 = v; \
 -              v2_height = 0; \
 -      } \
 -} MACRO_END
 +vector tracewalk_dest;
 +float tracewalk_dest_height;
  
  .entity wp_goal_prev0;
  .entity wp_goal_prev1;
  
  .float lastteleporttime;
+ .vector lastteleport_origin;
  
  .float blacklisted;
  
@@@ -109,13 -154,9 +110,13 @@@ void navigation_markroutes_checkwaypoin
  void navigation_markroutes(entity this, entity fixed_source_waypoint);
  void navigation_markroutes_inverted(entity fixed_source_waypoint);
  void navigation_routerating(entity this, entity e, float f, float rangebias);
 -void navigation_poptouchedgoals(entity this);
 +void navigation_shortenpath(entity this);
 +int navigation_poptouchedgoals(entity this);
  void navigation_goalrating_start(entity this);
  void navigation_goalrating_end(entity this);
 +void navigation_goalrating_timeout_set(entity this);
 +void navigation_goalrating_timeout_force(entity this);
 +bool navigation_goalrating_timeout(entity this);
  void navigation_unstuck(entity this);
  
  void botframe_updatedangerousobjects(float maxupdate);
index fed491b682ab6ae1a0ee8209f15be825735fa0ef,c20f6a92943ba6f2e169ed2777343230290a367e..fa82d926b63e92776cfd293dbebccf15ba20ad04
@@@ -179,19 -179,12 +179,12 @@@ void waypoint_setupmodel(entity wp
                wp.model = "";
  }
  
- // create a new spawnfunc_waypoint and automatically link it to other waypoints, and link
- // them back to it as well
- // (suitable for spawnfunc_waypoint editor)
  entity waypoint_spawn(vector m1, vector m2, float f)
  {
-       if(!(f & WAYPOINTFLAG_PERSONAL))
+       if(!(f & (WAYPOINTFLAG_PERSONAL | WAYPOINTFLAG_GENERATED)) && m1 == m2)
        {
-               vector em1 = m1, em2 = m2;
-               if (!(f & WAYPOINTFLAG_GENERATED) && m1 == m2)
-               {
-                       em1 = m1 - '8 8 8';
-                       em2 = m2 + '8 8 8';
-               }
+               vector em1 = m1 - '8 8 8';
+               vector em2 = m2 + '8 8 8';
                IL_EACH(g_waypoints, boxesoverlap(em1, em2, it.absmin, it.absmax),
                {
                        return it;
@@@ -583,12 -576,8 +576,12 @@@ void waypoint_think(entity this
                                continue;
                        }
  
 -                      SET_TRACEWALK_DESTCOORDS_2(this, it.origin, sv, sv2, sv2_height);
 -                      SET_TRACEWALK_DESTCOORDS_2(it, this.origin, ev, ev2, ev2_height);
 +                      sv = set_tracewalk_dest_2(this, it.origin);
 +                      sv2 = tracewalk_dest;
 +                      sv2_height = tracewalk_dest_height;
 +                      ev = set_tracewalk_dest_2(it, this.origin);
 +                      ev2 = tracewalk_dest;
 +                      ev2_height = tracewalk_dest_height;
  
                        dv = ev - sv;
                        dv.z = 0;
@@@ -686,22 -675,32 +679,32 @@@ void waypoint_schedulerelinkall(
        waypoint_load_links_hardwired();
  }
  
+ #define GET_GAMETYPE_EXTENSION() ((g_race) ? ".race" : "")
  // Load waypoint links from file
  bool waypoint_load_links()
  {
-       string filename, s;
+       string s;
        float file, tokens, c = 0, found;
        entity wp_from = NULL, wp_to;
        vector wp_to_pos, wp_from_pos;
-       filename = strcat("maps/", mapname);
-       filename = strcat(filename, ".waypoints.cache");
+       string gt_ext = GET_GAMETYPE_EXTENSION();
+       string filename = sprintf("maps/%s.waypoints.cache", strcat(mapname, gt_ext));
        file = fopen(filename, FILE_READ);
  
+       if (gt_ext != "" && file < 0)
+       {
+               // if race waypoint file doesn't exist load the default one
+               filename = sprintf("maps/%s.waypoints.cache", mapname);
+               file = fopen(filename, FILE_READ);
+       }
        if (file < 0)
        {
-               LOG_TRACE("waypoint links load from ");
-               LOG_TRACE(filename);
-               LOG_TRACE(" failed");
+               LOG_TRACE("waypoint links load from ", filename, " failed");
+               waypoint_schedulerelinkall();
                return false;
        }
  
                        else
                        {
                                if(ver < WAYPOINT_VERSION)
-                                       return false;
+                               {
+                                       LOG_TRACE("waypoint links for this map are outdated.");
+                                       if (g_assault)
+                                       {
+                                               LOG_TRACE("Assault waypoint links need to be manually updated in the editor");
+                                       }
+                                       else
+                                       {
+                                               LOG_TRACE("automatically updating...");
+                                               waypoint_schedulerelinkall();
+                                               fclose(file);
+                                               return false;
+                                       }
+                               }
                                parse_comments = false;
                        }
                }
                {
                        // bad file format
                        fclose(file);
+                       waypoint_schedulerelinkall(); // link all the autogenerated waypoints (teleporters)
                        return false;
                }
  
                                LOG_TRACE("waypoint_load_links: couldn't find 'from' waypoint at ", vtos(wp_from_pos));
                                continue;
                        }
                }
  
                // Search "to" waypoint
  
        fclose(file);
  
-       LOG_TRACE("loaded ", ftos(c), " waypoint links from maps/", mapname, ".waypoints.cache");
+       LOG_TRACE("loaded ", ftos(c), " waypoint links from ", filename);
+       bool scheduled = false;
+       IL_EACH(g_waypoints, it.wpflags & WAYPOINTFLAG_ITEM,
+       {
+               if (!it.wp00)
+               {
+                       waypoint_schedulerelink(it);
+                       scheduled = true;
+               }
+       });
+       if (scheduled)
+               return false;
  
        botframe_cachedwaypointlinks = true;
        return true;
  
  void waypoint_load_or_remove_links_hardwired(bool removal_mode)
  {
-       string filename, s;
+       string s;
        float file, tokens, c = 0, found;
        entity wp_from = NULL, wp_to;
        vector wp_to_pos, wp_from_pos;
-       filename = strcat("maps/", mapname);
-       filename = strcat(filename, ".waypoints.hardwired");
+       string gt_ext = GET_GAMETYPE_EXTENSION();
+       string filename = sprintf("maps/%s.waypoints.hardwired", strcat(mapname, gt_ext));
        file = fopen(filename, FILE_READ);
  
+       if (gt_ext != "" && file < 0)
+       {
+               // if race waypoint file doesn't exist load the default one
+               filename = sprintf("maps/%s.waypoints.hardwired", mapname);
+               file = fopen(filename, FILE_READ);
+       }
        botframe_loadedforcedlinks = true;
  
        if (file < 0)
  
        fclose(file);
  
-       if(!removal_mode)
-               LOG_TRACE("loaded ", ftos(c), " waypoint links from maps/", mapname, ".waypoints.hardwired");
+       LOG_TRACE(((removal_mode) ? "unloaded " : "loaded "),
+               ftos(c), " waypoint links from maps/", mapname, ".waypoints.hardwired");
  }
  
  entity waypoint_get_link(entity w, float i)
@@@ -940,7 -973,9 +977,9 @@@ void waypoint_save_links(
        // temporarily remove hardwired links so they don't get saved among normal links
        waypoint_remove_links_hardwired();
  
-       string filename = sprintf("maps/%s.waypoints.cache", mapname);
+       string gt_ext = GET_GAMETYPE_EXTENSION();
+       string filename = sprintf("maps/%s.waypoints.cache", strcat(mapname, gt_ext));
        int file = fopen(filename, FILE_WRITE);
        if (file < 0)
        {
                }
        });
        fclose(file);
        botframe_cachedwaypointlinks = true;
  
-       LOG_INFOF("saved %d waypoint links to maps/%s.waypoints.cache", c, mapname);
+       LOG_INFOF("saved %d waypoint links to %s", c, filename);
  
        waypoint_load_links_hardwired();
  }
  // save waypoints to gamedir/data/maps/mapname.waypoints
  void waypoint_saveall()
  {
-       string filename = sprintf("maps/%s.waypoints", mapname);
+       string gt_ext = GET_GAMETYPE_EXTENSION();
+       string filename = sprintf("maps/%s.waypoints", strcat(mapname, gt_ext));
        int file = fopen(filename, FILE_WRITE);
        if (file < 0)
        {
        waypoint_save_links();
        botframe_loadedforcedlinks = false;
  
-       LOG_INFOF("saved %d waypoints to maps/%s.waypoints", c, mapname);
+       LOG_INFOF("saved %d waypoints to %s", c, filename);
  }
  
  // load waypoints from file
  float waypoint_loadall()
  {
-       string filename, s;
+       string s;
        float file, cwp, cwb, fl;
        vector m1, m2;
        cwp = 0;
        cwb = 0;
-       filename = strcat("maps/", mapname);
-       filename = strcat(filename, ".waypoints");
+       string gt_ext = GET_GAMETYPE_EXTENSION();
+       string filename = sprintf("maps/%s.waypoints", strcat(mapname, gt_ext));
        file = fopen(filename, FILE_READ);
  
-       bool parse_comments = true;
-       float ver = 0;
+       if (gt_ext != "" && file < 0)
+       {
+               // if race waypoint file doesn't exist load the default one
+               filename = sprintf("maps/%s.waypoints", mapname);
+               file = fopen(filename, FILE_READ);
+       }
  
        if (file < 0)
        {
                return 0;
        }
  
+       bool parse_comments = true;
+       float ver = 0;
        while ((s = fgets(file)))
        {
                if(parse_comments)
                        else
                        {
                                if(floor(ver) < floor(WAYPOINT_VERSION))
+                               {
                                        LOG_TRACE("waypoints for this map are outdated");
+                                       LOG_TRACE("please update them in the editor");
+                               }
                                parse_comments = false;
                        }
                }
index ba2405b9db3837b67df1350876ccb5565b489f7d,0bfdc02fa2380ceadd6fdde783eed9a6e08e24a9..daaf8a969592e810af3531a1cdcded327b46a8fc
@@@ -13,21 -13,33 +13,34 @@@ void havocbot_role_cts(entity this
        if(IS_DEAD(this))
                return;
  
 -      if (this.bot_strategytime < time)
 +      if (navigation_goalrating_timeout(this))
        {
 -              this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
                navigation_goalrating_start(this);
  
+               bool raw_touch_check = true;
+               int cp = this.race_checkpoint;
+               LABEL(search_racecheckpoints)
                IL_EACH(g_racecheckpoints, true,
                {
-                       if(it.cnt == this.race_checkpoint)
-                               navigation_routerating(this, it, 1000000, 5000);
-                       else if(this.race_checkpoint == -1)
+                       if(it.cnt == cp || cp == -1)
+                       {
+                               // redirect bot to next goal if it touched the waypoint of an untouchable checkpoint
+                               // e.g. checkpoint in front of Stormkeep's warpzone
+                               // the same workaround is applied in Race game mode
+                               if (raw_touch_check && vdist(this.origin - it.nearestwaypoint.origin, <, 30))
+                               {
+                                       cp = race_NextCheckpoint(cp);
+                                       raw_touch_check = false;
+                                       goto search_racecheckpoints;
+                               }
                                navigation_routerating(this, it, 1000000, 5000);
+                       }
                });
  
                navigation_goalrating_end(this);
 +
 +              navigation_goalrating_timeout_set(this);
        }
  }
  
index c4ba8e91819b57f859cfe1fd3036b59ec72d88a7,9c2d60f38e7889991b0500fd22d7dcff660d4d52..690d23ca8bc3e2595df612f403185eba0975ee9c
@@@ -14,21 -14,33 +14,33 @@@ void havocbot_role_race(entity this
        if(IS_DEAD(this))
                return;
  
 -      if (this.bot_strategytime < time)
 +      if (navigation_goalrating_timeout(this))
        {
 -              this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
                navigation_goalrating_start(this);
  
+               bool raw_touch_check = true;
+               int cp = this.race_checkpoint;
+               LABEL(search_racecheckpoints)
                IL_EACH(g_racecheckpoints, true,
                {
-                       if(it.cnt == this.race_checkpoint)
-                               navigation_routerating(this, it, 1000000, 5000);
-                       else if(this.race_checkpoint == -1)
+                       if(it.cnt == cp || cp == -1)
+                       {
+                               // redirect bot to next goal if it touched the waypoint of an untouchable checkpoint
+                               // e.g. checkpoint in front of Stormkeep's warpzone
+                               // the same workaround is applied in CTS game mode
+                               if (raw_touch_check && vdist(this.origin - it.nearestwaypoint.origin, <, 30))
+                               {
+                                       cp = race_NextCheckpoint(cp);
+                                       raw_touch_check = false;
+                                       goto search_racecheckpoints;
+                               }
                                navigation_routerating(this, it, 1000000, 5000);
 -                      }
                });
  
                navigation_goalrating_end(this);
 +
 +              navigation_goalrating_timeout_set(this);
        }
  }