Merge branch 'master' into terencehill/bot_waypoints
authorterencehill <piuntn@gmail.com>
Thu, 22 Aug 2019 00:07:59 +0000 (02:07 +0200)
committerterencehill <piuntn@gmail.com>
Thu, 22 Aug 2019 00:07:59 +0000 (02:07 +0200)
# Conflicts:
# qcsrc/server/bot/default/havocbot/havocbot.qc

1  2 
qcsrc/menu/xonotic/keybinder.qc
qcsrc/server/bot/api.qh
qcsrc/server/bot/default/havocbot/havocbot.qc
qcsrc/server/bot/default/waypoints.qc
xonotic-client.cfg

index b4a259d49fa4199e31e498a0596bf10604c82a13,6e59b8b1fef3825b6267d60821f564a1efe58942..4f6e4a04a6dbe3201e43430975b00ede72e53c6b
@@@ -68,9 -68,9 +68,9 @@@ void KeyBinds_Read(
        for(int imp = 1; imp <= 9; ++imp)
        {
                string w_list = "";
-               ADD_TO_W_LIST(!(it.spawnflags & WEP_FLAG_MUTATORBLOCKED) && !(it.spawnflags & WEP_FLAG_HIDDEN) && !(it.spawnflags & WEP_FLAG_SUPERWEAPON));
+               ADD_TO_W_LIST(!(it.spawnflags & (WEP_FLAG_MUTATORBLOCKED | WEP_FLAG_HIDDEN | WEP_FLAG_SPECIALATTACK | WEP_FLAG_SUPERWEAPON)));
                ADD_TO_W_LIST((it.spawnflags & WEP_FLAG_SUPERWEAPON) && !(it.spawnflags & WEP_FLAG_HIDDEN));
-               ADD_TO_W_LIST((it.spawnflags & WEP_FLAG_MUTATORBLOCKED) && !(it.spawnflags & WEP_FLAG_HIDDEN));
+               ADD_TO_W_LIST((it.spawnflags & WEP_FLAG_MUTATORBLOCKED) && !(it.spawnflags & (WEP_FLAG_HIDDEN | WEP_FLAG_SPECIALATTACK)));
                if(w_list)
                        KEYBIND_DEF(strcat("weapon_group_", itos(imp)), substring(w_list, 0, -4));
                if(imp == 0)
        KEYBIND_DEF("kill"                                  , _("respawn"));
        KEYBIND_DEF("quickmenu"                             , _("quick menu"));
        KEYBIND_DEF("menu_showsandboxtools"                 , _("sandbox menu"));
 +      KEYBIND_DEF("wpeditor_menu"                         , _("waypoint editor menu"));
        KEYBIND_DEF("+button8"                              , _("drag object"));
        KEYBIND_EMPTY_LINE();
  
diff --combined qcsrc/server/bot/api.qh
index 630f3205297b238c41f01e372ec7ee3ce6f20b38,35b52e3d9121de10f322853ea0d92d9c14a51e81..eddc712347a6f115c26fb7eb3fa466d5cb24e58a
@@@ -7,26 -7,14 +7,23 @@@
  const int WAYPOINTFLAG_GENERATED = BIT(23);
  const int WAYPOINTFLAG_ITEM = BIT(22);
  const int WAYPOINTFLAG_TELEPORT = BIT(21); // teleports, warpzones and jumppads
 -const int WAYPOINTFLAG_NORELINK = BIT(20);
 +//const int WAYPOINTFLAG_NORELINK = BIT(20); // deprecated, see explanation below. Do not recycle this bit.
  const int WAYPOINTFLAG_PERSONAL = BIT(19);
  const int WAYPOINTFLAG_PROTECTED = BIT(18);  // Useless WP detection never kills these.
  const int WAYPOINTFLAG_USEFUL = BIT(17);  // Useless WP detection temporary flag.
  const int WAYPOINTFLAG_DEAD_END = BIT(16);  // Useless WP detection temporary flag.
  const int WAYPOINTFLAG_LADDER = BIT(15);
  const int WAYPOINTFLAG_JUMP = BIT(14);
 +const int WAYPOINTFLAG_CUSTOM_JP = BIT(13);  // jumppad with different destination waypoint (changed in the editor)
 +const int WAYPOINTFLAG_CROUCH = BIT(12);
 +const int WAYPOINTFLAG_SUPPORT = BIT(11);
 +
 +// removed WAYPOINTFLAG_NORELINK since it breaks backward compatibility
 +// e.g. support waypoints would have no outgoing links in old Xonotic versions
 +// In general, old Xonotic versions should spawn a normal waypoint for each unknown waypoint type
 +const int WAYPOINTFLAG_NORELINK__DEPRECATED = BIT(20);
 +const int WPFLAGMASK_NORELINK = (WAYPOINTFLAG_TELEPORT | WAYPOINTFLAG_LADDER | WAYPOINTFLAG_JUMP | WAYPOINTFLAG_CUSTOM_JP | WAYPOINTFLAG_SUPPORT);
  
- entity kh_worldkeylist;
- .entity kh_worldkeynext;
  float bot_custom_weapon;
  float bot_weapons_close[Weapons_MAX];
  float bot_weapons_far[Weapons_MAX];
@@@ -60,8 -48,8 +57,8 @@@ float skill
  .float wp24mincost, wp25mincost, wp26mincost, wp27mincost, wp28mincost, wp29mincost, wp30mincost, wp31mincost;
  .float wpconsidered;
  .float wpcost;
 -.float wphardwired;
  .int wpflags;
 +.entity wphw00, wphw01, wphw02, wphw03, wphw04, wphw05, wphw06, wphw07;
  
  bool bot_aim(entity this, .entity weaponentity, float shotspeed, float shotspeedupward, float maxshottime, float applygravity);
  void bot_aim_reset(entity this);
@@@ -110,7 -98,6 +107,7 @@@ void navigation_goalrating_timeout_set(
  void navigation_goalrating_timeout_force(entity this);
  void navigation_goalrating_timeout_expire(entity this, float seconds);
  bool navigation_goalrating_timeout(entity this);
 +void navigation_goalrating_timeout_extend_if_needed(entity this, float seconds);
  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);
@@@ -131,10 -118,9 +128,10 @@@ void waypoint_spawnforitem(entity e)
  void waypoint_spawnforitem_force(entity e, vector org);
  void waypoint_spawnforteleporter(entity e, vector destination, float timetaken, entity tracetest_ent);
  void waypoint_spawnforteleporter_wz(entity e, entity tracetest_ent);
 -void waypoint_spawn_fromeditor(entity pl);
 +void waypoint_spawn_fromeditor(entity pl, bool at_crosshair, bool is_jump_wp, bool is_crouch_wp, bool is_support_wp);
  entity waypoint_spawn(vector m1, vector m2, float f);
  void waypoint_unreachable(entity pl);
 +void waypoint_start_hardwiredlink(entity pl, bool at_crosshair);
  
  void waypoint_getSymmetricalOrigin_cmd(entity caller, bool save, int arg_idx);
  void waypoint_getSymmetricalAxis_cmd(entity caller, bool save, int arg_idx);
index 9f6da64eb26abe234e4142e221f24ecd8d47a98c,2b2dfbf3cf5e86a4d9d3b19af56fd676be07c06b..6af0032368c0d4293ef8f9ac9099513f3a2efb07
@@@ -1,5 -1,7 +1,7 @@@
  #include "havocbot.qh"
  
+ #include "roles.qh"
  #include <server/defs.qh>
  #include <server/miscfunctions.qh>
  #include "../cvars.qh"
@@@ -24,8 -26,6 +26,6 @@@
  
  #include <lib/warpzone/common.qh>
  
- .float speed;
  void havocbot_ai(entity this)
  {
        if(this.draggedby)
  
  void havocbot_keyboard_movement(entity this, vector destorg)
  {
-       vector keyboard;
+       if(time <= this.havocbot_keyboardtime)
+               return;
  
-       if (time > this.havocbot_keyboardtime)
+       float sk = skill + this.bot_moveskill;
+       this.havocbot_keyboardtime =
+               max(
+                       this.havocbot_keyboardtime
+                               + 0.05 / max(1, sk + this.havocbot_keyboardskill)
+                               + random() * 0.025 / max(0.00025, skill + this.havocbot_keyboardskill)
+               , time);
+       vector keyboard = CS(this).movement / autocvar_sv_maxspeed;
+       float trigger = autocvar_bot_ai_keyboard_threshold;
+       // categorize forward movement
+       // at skill < 1.5 only forward
+       // at skill < 2.5 only individual directions
+       // at skill < 4.5 only individual directions, and forward diagonals
+       // at skill >= 4.5, all cases allowed
+       if (keyboard.x > trigger)
        {
-               float sk = skill + this.bot_moveskill;
-               this.havocbot_keyboardtime =
-                       max(
-                               this.havocbot_keyboardtime
-                                       + 0.05 / max(1, sk + this.havocbot_keyboardskill)
-                                       + random() * 0.025 / max(0.00025, skill + this.havocbot_keyboardskill)
-                       , time);
-               keyboard = CS(this).movement / autocvar_sv_maxspeed;
-               float trigger = autocvar_bot_ai_keyboard_threshold;
-               // categorize forward movement
-               // at skill < 1.5 only forward
-               // at skill < 2.5 only individual directions
-               // at skill < 4.5 only individual directions, and forward diagonals
-               // at skill >= 4.5, all cases allowed
-               if (keyboard.x > trigger)
-               {
-                       keyboard.x = 1;
-                       if (sk < 2.5)
-                               keyboard.y = 0;
-               }
-               else if (keyboard.x < -trigger && sk > 1.5)
-               {
-                       keyboard.x = -1;
-                       if (sk < 4.5)
-                               keyboard.y = 0;
-               }
-               else
-               {
-                       keyboard.x = 0;
-                       if (sk < 1.5)
-                               keyboard.y = 0;
-               }
+               keyboard.x = 1;
+               if (sk < 2.5)
+                       keyboard.y = 0;
+       }
+       else if (keyboard.x < -trigger && sk > 1.5)
+       {
+               keyboard.x = -1;
                if (sk < 4.5)
-                       keyboard.z = 0;
-               if (keyboard.y > trigger)
-                       keyboard.y = 1;
-               else if (keyboard.y < -trigger)
-                       keyboard.y = -1;
-               else
                        keyboard.y = 0;
+       }
+       else
+       {
+               keyboard.x = 0;
+               if (sk < 1.5)
+                       keyboard.y = 0;
+       }
+       if (sk < 4.5)
+               keyboard.z = 0;
  
-               if (keyboard.z > trigger)
-                       keyboard.z = 1;
-               else if (keyboard.z < -trigger)
-                       keyboard.z = -1;
-               else
-                       keyboard.z = 0;
+       if (keyboard.y > trigger)
+               keyboard.y = 1;
+       else if (keyboard.y < -trigger)
+               keyboard.y = -1;
+       else
+               keyboard.y = 0;
  
-               // make sure bots don't get stuck if havocbot_keyboardtime is very high
-               if (keyboard == '0 0 0')
-                       this.havocbot_keyboardtime = min(this.havocbot_keyboardtime, time + 0.2);
+       if (keyboard.z > trigger)
+               keyboard.z = 1;
+       else if (keyboard.z < -trigger)
+               keyboard.z = -1;
+       else
+               keyboard.z = 0;
  
-               this.havocbot_keyboard = keyboard * autocvar_sv_maxspeed;
-               if (this.havocbot_ducktime > time)
-                       PHYS_INPUT_BUTTON_CROUCH(this) = true;
+       // make sure bots don't get stuck if havocbot_keyboardtime is very high
+       if (keyboard == '0 0 0')
+               this.havocbot_keyboardtime = min(this.havocbot_keyboardtime, time + 0.2);
  
-               keyboard = this.havocbot_keyboard;
-               float blend = bound(0, vlen(destorg - this.origin) / autocvar_bot_ai_keyboard_distance, 1); // When getting close move with 360 degree
-               //dprint("movement ", vtos(CS(this).movement), " keyboard ", vtos(keyboard), " blend ", ftos(blend), "\n");
-               CS(this).movement = CS(this).movement + (keyboard - CS(this).movement) * blend;
-       }
+       this.havocbot_keyboard = keyboard * autocvar_sv_maxspeed;
+       if (this.havocbot_ducktime > time)
+               PHYS_INPUT_BUTTON_CROUCH(this) = true;
+       keyboard = this.havocbot_keyboard;
+       float blend = bound(0, vlen(destorg - this.origin) / autocvar_bot_ai_keyboard_distance, 1); // When getting close move with 360 degree
+       //dprint("movement ", vtos(CS(this).movement), " keyboard ", vtos(keyboard), " blend ", ftos(blend), "\n");
+       CS(this).movement = CS(this).movement + (keyboard - CS(this).movement) * blend;
  }
  
  void havocbot_bunnyhop(entity this, vector dir)
  {
-       float bunnyhopdistance;
-       vector deviation;
-       float maxspeed;
        // Don't jump when attacking
        if(this.aistatus & AI_STATUS_ATTACKING)
                return;
        if(IS_PLAYER(this.goalcurrent))
                return;
  
-       maxspeed = autocvar_sv_maxspeed;
-       if(this.aistatus & AI_STATUS_RUNNING && vdist(this.velocity, <, autocvar_sv_maxspeed * 0.75)
-               || this.aistatus & AI_STATUS_DANGER_AHEAD)
+       if((this.aistatus & AI_STATUS_RUNNING) && vdist(this.velocity, <, autocvar_sv_maxspeed * 0.75)
+               || (this.aistatus & AI_STATUS_DANGER_AHEAD))
        {
                this.aistatus &= ~AI_STATUS_RUNNING;
                PHYS_INPUT_BUTTON_JUMP(this) = false;
                return;
        }
  
 -      if(this.waterlevel > WATERLEVEL_WETFEET)
 +      if(this.waterlevel > WATERLEVEL_WETFEET || IS_DUCKED(this))
        {
                this.aistatus &= ~AI_STATUS_RUNNING;
                return;
        }
  
        vector gco = get_closer_dest(this.goalcurrent, this.origin);
-       bunnyhopdistance = vlen(this.origin - gco);
  
        // Run only to visible goals
        if(IS_ONGROUND(this))
-       if(vdist(vec2(this.velocity), >=, autocvar_sv_maxspeed)) // if -really- running
+       if(vdist(vec2(this.velocity), >=, autocvar_sv_maxspeed))
        if(checkpvs(this.origin + this.view_ofs, this.goalcurrent))
        {
-                       this.bot_lastseengoal = this.goalcurrent;
+               this.bot_lastseengoal = this.goalcurrent;
  
-                       // seen it before
-                       if(this.bot_timelastseengoal)
+               // seen it before
+               if(this.bot_timelastseengoal)
+               {
+                       // for a period of time
+                       if(time - this.bot_timelastseengoal > autocvar_bot_ai_bunnyhop_firstjumpdelay)
                        {
-                               // for a period of time
-                               if(time - this.bot_timelastseengoal > autocvar_bot_ai_bunnyhop_firstjumpdelay)
-                               {
-                                       float checkdistance;
-                                       checkdistance = true;
+                               bool checkdistance = true;
  
-                                       // don't run if it is too close
-                                       if(this.bot_canruntogoal==0)
-                                       {
-                                               if(bunnyhopdistance > autocvar_bot_ai_bunnyhop_startdistance)
-                                                       this.bot_canruntogoal = 1;
-                                               else
-                                                       this.bot_canruntogoal = -1;
-                                       }
+                               // don't run if it is too close
+                               if(this.bot_canruntogoal==0)
+                               {
+                                       if(vdist(this.origin - gco, >, autocvar_bot_ai_bunnyhop_startdistance))
+                                               this.bot_canruntogoal = 1;
+                                       else
+                                               this.bot_canruntogoal = -1;
+                               }
  
-                                       if(this.bot_canruntogoal != 1)
-                                               return;
+                               if(this.bot_canruntogoal != 1)
+                                       return;
  
-                                       if(this.aistatus & AI_STATUS_ROAMING)
-                                       if(this.goalcurrent.classname=="waypoint")
-                                       if (!(this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL))
-                                       if(fabs(gco.z - this.origin.z) < this.maxs.z - this.mins.z)
-                                       if(this.goalstack01 && !wasfreed(this.goalstack01))
-                                       if (!(this.goalstack01.wpflags & WAYPOINTFLAG_JUMP))
+                               if(this.aistatus & AI_STATUS_ROAMING)
+                               if(this.goalcurrent.classname == "waypoint")
 -                              if(!(this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL))
++                              if (!(this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL))
+                               if(fabs(gco.z - this.origin.z) < this.maxs.z - this.mins.z)
 -                              if(this.goalstack01 && !wasfreed(this.goalstack01))
++                              if (this.goalstack01 && !wasfreed(this.goalstack01))
++                              if (!(this.goalstack01.wpflags & WAYPOINTFLAG_JUMP))
+                               {
+                                       vector gno = (this.goalstack01.absmin + this.goalstack01.absmax) * 0.5;
+                                       vector deviation = vectoangles(gno - this.origin) - vectoangles(gco - this.origin);
+                                       while (deviation.y < -180) deviation.y = deviation.y + 360;
+                                       while (deviation.y > 180) deviation.y = deviation.y - 360;
+                                       if(fabs(deviation.y) < 20)
+                                       if(vlen2(this.origin - gco) < vlen2(this.origin - gno))
+                                       if(fabs(gno.z - gco.z) < this.maxs.z - this.mins.z)
                                        {
-                                               vector gno = (this.goalstack01.absmin + this.goalstack01.absmax) * 0.5;
-                                               deviation = vectoangles(gno - this.origin) - vectoangles(gco - this.origin);
-                                               while (deviation.y < -180) deviation.y = deviation.y + 360;
-                                               while (deviation.y > 180) deviation.y = deviation.y - 360;
-                                               if(fabs(deviation.y) < 20)
-                                               if(bunnyhopdistance < vlen(this.origin - gno))
-                                               if(fabs(gno.z - gco.z) < this.maxs.z - this.mins.z)
+                                               if(vdist(gco - gno, >, autocvar_bot_ai_bunnyhop_startdistance))
+                                               if(checkpvs(this.origin + this.view_ofs, this.goalstack01))
                                                {
-                                                       if(vdist(gco - gno, >, autocvar_bot_ai_bunnyhop_startdistance))
-                                                       if(checkpvs(this.origin + this.view_ofs, this.goalstack01))
-                                                       {
-                                                               checkdistance = false;
-                                                       }
+                                                       checkdistance = false;
                                                }
                                        }
+                               }
  
-                                       if(checkdistance)
-                                       {
-                                               this.aistatus &= ~AI_STATUS_RUNNING;
-                                               // increase stop distance in case the goal is on a slope or a lower platform
-                                               if(bunnyhopdistance > autocvar_bot_ai_bunnyhop_stopdistance + (this.origin.z - gco.z))
-                                                       PHYS_INPUT_BUTTON_JUMP(this) = true;
-                                       }
-                                       else
-                                       {
-                                               this.aistatus |= AI_STATUS_RUNNING;
+                               if(checkdistance)
+                               {
+                                       this.aistatus &= ~AI_STATUS_RUNNING;
+                                       // increase stop distance in case the goal is on a slope or a lower platform
+                                       if(vdist(this.origin - gco, >, autocvar_bot_ai_bunnyhop_stopdistance + (this.origin.z - gco.z)))
                                                PHYS_INPUT_BUTTON_JUMP(this) = true;
-                                       }
+                               }
+                               else
+                               {
+                                       this.aistatus |= AI_STATUS_RUNNING;
+                                       PHYS_INPUT_BUTTON_JUMP(this) = true;
                                }
                        }
-                       else
-                       {
-                               this.bot_timelastseengoal = time;
-                       }
+               }
+               else
+               {
+                       this.bot_timelastseengoal = time;
+               }
        }
        else
        {
                this.bot_timelastseengoal = 0;
        }
- #if 0
-       // Release jump button
-       if(!cvar("sv_pogostick"))
-       if((IS_ONGROUND(this)) == 0)
-       {
-               if(this.velocity.z < 0 || vlen(this.velocity)<maxspeed)
-                       PHYS_INPUT_BUTTON_JUMP(this) = false;
-               // Strafe
-               if(this.aistatus & AI_STATUS_RUNNING)
-               if(vlen(this.velocity)>maxspeed)
-               {
-                       deviation = vectoangles(dir) - vectoangles(this.velocity);
-                       while (deviation.y < -180) deviation.y = deviation.y + 360;
-                       while (deviation.y > 180) deviation.y = deviation.y - 360;
-                       if(fabs(deviation.y)>10)
-                               CS(this).movement_x = 0;
-                       if(deviation.y>10)
-                               CS(this).movement_y = maxspeed * -1;
-                       else if(deviation.y<10)
-                               CS(this).movement_y = maxspeed;
-               }
-       }
- #endif
  }
  
  // return true when bot isn't getting closer to the current goal
  bool havocbot_checkgoaldistance(entity this, vector gco)
  {
 +      if (this.bot_stop_moving_timeout > time)
 +              return false;
        float curr_dist_z = max(20, fabs(this.origin.z - gco.z));
        float curr_dist_2d = max(20, vlen(vec2(this.origin - gco)));
        float distance_time = this.goalcurrent_distance_time;
@@@ -479,11 -438,6 +441,11 @@@ void havocbot_movetogoal(entity this
        CS(this).movement = '0 0 0';
        maxspeed = autocvar_sv_maxspeed;
  
 +      if (this.goalcurrent.wpflags & WAYPOINTFLAG_CROUCH)
 +              PHYS_INPUT_BUTTON_CROUCH(this) = true;
 +      else
 +              PHYS_INPUT_BUTTON_CROUCH(this) = false;
 +
        PHYS_INPUT_BUTTON_JETPACK(this) = false;
        // Jetpack navigation
        if(this.navigation_jetpack_goal)
  
                        return;
                }
 -              else if(!this.jumppadcount && !this.goalcurrent.wphardwired
 +              else if(!this.jumppadcount && !waypoint_is_hardwiredlink(this.goalcurrent_prev, this.goalcurrent)
 +                      && !(this.goalcurrent_prev && this.goalcurrent_prev.wpflags & WAYPOINTFLAG_JUMP)
                        && GetResource(this, RES_HEALTH) + GetResource(this, RES_ARMOR) > ROCKETJUMP_DAMAGE())
                {
                        if(this.velocity.z < 0)
                        vector flat_diff = vec2(diff);
                        offset = max(32, current_speed * cos(deviation.y * DEG2RAD) * 0.3) * flatdir;
                        vector actual_destorg = this.origin + offset;
 -                      if (!this.goalstack01 || this.goalcurrent.wpflags & (WAYPOINTFLAG_TELEPORT | WAYPOINTFLAG_LADDER))
 +                      if (this.goalcurrent_prev && this.goalcurrent_prev.wpflags & WAYPOINTFLAG_JUMP)
 +                      {
 +                              if (time > this.bot_stop_moving_timeout
 +                                      && fabs(deviation.y) > 20 && current_speed > maxspeed * 0.4
 +                                      && vdist(vec2(this.origin - this.goalcurrent_prev.origin), <, 50))
 +                              {
 +                                      this.bot_stop_moving_timeout = time + 0.1;
 +                              }
 +                              if (current_speed > autocvar_sv_maxspeed * 0.9
 +                                      && vlen2(flat_diff) < vlen2(vec2(this.goalcurrent_prev.origin - destorg))
 +                                      && vdist(vec2(this.origin - this.goalcurrent_prev.origin), >, 50)
 +                                      && vdist(vec2(this.origin - this.goalcurrent_prev.origin), <, 150)
 +                              )
 +                              {
 +                                      PHYS_INPUT_BUTTON_JUMP(this) = true;
 +                                      // avoid changing route while bot is jumping a gap
 +                                      navigation_goalrating_timeout_extend_if_needed(this, 1.5);
 +                              }
 +                      }
 +                      else if (!this.goalstack01 || this.goalcurrent.wpflags & (WAYPOINTFLAG_TELEPORT | WAYPOINTFLAG_LADDER))
                        {
                                if (vlen2(flat_diff) < vlen2(offset))
                                {
                                turning = true;
                        }
  
 -                      LABEL(jump_check);
 +                      LABEL(jumpobstacle_check);
                        dir = flatdir = normalize(actual_destorg - this.origin);
  
 -                      if (turning || fabs(deviation.y) < 50) // don't even try to jump if deviation is too high
 +                      bool jump_forbidden = false;
 +                      if (!turning && fabs(deviation.y) > 50)
 +                              jump_forbidden = true;
 +                      else if (IS_DUCKED(this))
 +                      {
 +                              tracebox(this.origin, PL_MIN_CONST, PL_MAX_CONST, this.origin, false, this);
 +                              if (trace_startsolid)
 +                                      jump_forbidden = true;
 +                      }
 +
 +                      if (!jump_forbidden)
                        {
                                tracebox(this.origin, this.mins, this.maxs, actual_destorg, false, this);
                                if (trace_fraction < 1 && trace_plane_normal.z < 0.7)
                                                        actual_destorg = destorg;
                                                        turning = false;
                                                        this.bot_tracewalk_time = time + 0.25;
 -                                                      goto jump_check;
 +                                                      goto jumpobstacle_check;
                                                }
                                                s = trace_fraction;
                                                // don't artificially reduce max jump height in real-time
  
                        bool unreachable = false;
                        s = CONTENT_SOLID;
 -                      if(trace_fraction == 1 && this.jumppadcount == 0 && !this.goalcurrent.wphardwired )
 +                      if (trace_fraction == 1 && !this.jumppadcount
 +                              && !waypoint_is_hardwiredlink(this.goalcurrent_prev, this.goalcurrent)
 +                              && !(this.goalcurrent_prev && this.goalcurrent_prev.wpflags & WAYPOINTFLAG_JUMP) )
                        if((IS_ONGROUND(this)) || (this.aistatus & AI_STATUS_RUNNING) || (this.aistatus & AI_STATUS_ROAMING) || PHYS_INPUT_BUTTON_JUMP(this))
                        {
                                // Look downwards
                        }
  
                        // slow down if bot is in the air and goal is under it
 -                      if (!this.goalcurrent.wphardwired
 +                      if (!waypoint_is_hardwiredlink(this.goalcurrent_prev, this.goalcurrent)
                                && vdist(flat_diff, <, 250) && this.origin.z - destorg.z > 120
                                && (!IS_ONGROUND(this) || vdist(vec2(this.velocity), >, maxspeed * 0.3)))
                        {
index 0b06a9593c4afe46d04a4620735ac71ab3dc8651,6bc90770145614828e8c5653794e1d0facfaf1bb..24f6ba8cb76b75c0cd2ed6ba80071c49c5b5a50a
@@@ -13,7 -13,6 +13,7 @@@
  
  #include <common/constants.qh>
  #include <common/debug.qh>
 +#include <common/mapobjects/trigger/jumppads.qh>
  #include <common/net_linked.qh>
  #include <common/physics/player.qh>
  
@@@ -260,87 -259,6 +260,87 @@@ vector waypoint_getSymmetricalPoint(vec
        return new_org;
  }
  
 +void crosshair_trace_waypoints(entity pl);
 +void waypoint_lock(entity pl)
 +{
 +      crosshair_trace_waypoints(pl);
 +      pl.wp_locked = trace_ent;
 +}
 +
 +bool waypoint_has_hardwiredlinks(entity wp)
 +{
 +      if (!wp)
 +              return false;
 +      return (wp.wphw00 != NULL);
 +}
 +
 +bool waypoint_is_hardwiredlink(entity wp_from, entity wp_to)
 +{
 +      if (!(wp_from && wp_to))
 +              return false;
 +
 +      if (!wp_from.wphw00) return false; else if (wp_from.wphw00 == wp_to) return true;
 +      if (!wp_from.wphw01) return false; else if (wp_from.wphw01 == wp_to) return true;
 +      if (!wp_from.wphw02) return false; else if (wp_from.wphw02 == wp_to) return true;
 +      if (!wp_from.wphw03) return false; else if (wp_from.wphw03 == wp_to) return true;
 +      if (!wp_from.wphw04) return false; else if (wp_from.wphw04 == wp_to) return true;
 +      if (!wp_from.wphw05) return false; else if (wp_from.wphw05 == wp_to) return true;
 +      if (!wp_from.wphw06) return false; else if (wp_from.wphw06 == wp_to) return true;
 +      if (!wp_from.wphw07) return false; else if (wp_from.wphw07 == wp_to) return true;
 +
 +      return false;
 +}
 +
 +void waypoint_setupmodel(entity wp);
 +void waypoint_mark_hardwiredlink(entity wp_from, entity wp_to)
 +{
 +      if (!(wp_from && wp_to))
 +              return;
 +
 +      if (!wp_from.wphw00 || wp_from.wphw00 == wp_to) { wp_from.wphw00 = wp_to; waypoint_setupmodel(wp_from); return; }
 +      if (!wp_from.wphw01 || wp_from.wphw01 == wp_to) { wp_from.wphw01 = wp_to; return; }
 +      if (!wp_from.wphw02 || wp_from.wphw02 == wp_to) { wp_from.wphw02 = wp_to; return; }
 +      if (!wp_from.wphw03 || wp_from.wphw03 == wp_to) { wp_from.wphw03 = wp_to; return; }
 +      if (!wp_from.wphw04 || wp_from.wphw04 == wp_to) { wp_from.wphw04 = wp_to; return; }
 +      if (!wp_from.wphw05 || wp_from.wphw05 == wp_to) { wp_from.wphw05 = wp_to; return; }
 +      if (!wp_from.wphw06 || wp_from.wphw06 == wp_to) { wp_from.wphw06 = wp_to; return; }
 +      if (!wp_from.wphw07 || wp_from.wphw07 == wp_to) { wp_from.wphw07 = wp_to; return; }
 +
 +      return;
 +}
 +
 +void waypoint_unmark_hardwiredlink(entity wp_from, entity wp_to)
 +{
 +      if (!(wp_from && wp_to))
 +              return;
 +
 +      int removed = -1;
 +      if (removed < 0 && wp_from.wphw00 == wp_to) removed = 0;
 +      if (removed < 0 && wp_from.wphw01 == wp_to) removed = 1;
 +      if (removed < 0 && wp_from.wphw02 == wp_to) removed = 2;
 +      if (removed < 0 && wp_from.wphw03 == wp_to) removed = 3;
 +      if (removed < 0 && wp_from.wphw04 == wp_to) removed = 4;
 +      if (removed < 0 && wp_from.wphw05 == wp_to) removed = 5;
 +      if (removed < 0 && wp_from.wphw06 == wp_to) removed = 6;
 +      if (removed < 0 && wp_from.wphw07 == wp_to) removed = 7;
 +
 +      if (removed >= 0)
 +      {
 +              if (removed <= 0) wp_from.wphw00 = wp_from.wphw01;
 +              if (removed <= 1) wp_from.wphw01 = wp_from.wphw02;
 +              if (removed <= 2) wp_from.wphw02 = wp_from.wphw03;
 +              if (removed <= 3) wp_from.wphw03 = wp_from.wphw04;
 +              if (removed <= 4) wp_from.wphw04 = wp_from.wphw05;
 +              if (removed <= 5) wp_from.wphw05 = wp_from.wphw06;
 +              if (removed <= 6) wp_from.wphw06 = wp_from.wphw07;
 +              if (removed <= 7) wp_from.wphw07 = NULL;
 +              if (!wp_from.wphw00)
 +                      waypoint_setupmodel(wp_from);
 +      }
 +
 +      return;
 +}
 +
  void waypoint_setupmodel(entity wp)
  {
        if (autocvar_g_waypointeditor)
                setsize(wp, m1, m2);
                wp.effects = EF_LOWPRECISION;
                if (wp.wpflags & WAYPOINTFLAG_ITEM)
 -                      wp.colormod = '1 0 0';
 +                      wp.colormod = '1 0 0'; // red
                else if (wp.wpflags & WAYPOINTFLAG_GENERATED)
 -                      wp.colormod = '1 1 0';
 -              else if (wp.wphardwired)
 -                      wp.colormod = '0.5 0 1';
 +                      wp.colormod = '1 1 0'; // yellow
 +              else if (wp.wpflags & WAYPOINTFLAG_SUPPORT)
 +                      wp.colormod = '0 1 0'; // green
 +              else if (wp.wpflags & WAYPOINTFLAG_CUSTOM_JP)
 +                      wp.colormod = '1 0.5 0'; // orange
 +              else if (wp.wpflags & WAYPOINTFLAG_TELEPORT)
 +                      wp.colormod = '1 0.5 0'; // orange
 +              else if (wp.wpflags & WAYPOINTFLAG_LADDER)
 +                      wp.colormod = '1 0.5 0'; // orange
 +              else if (wp.wpflags & WAYPOINTFLAG_JUMP)
 +                      wp.colormod = '1 0.5 0'; // orange
 +              else if (wp.wpflags & WAYPOINTFLAG_CROUCH)
 +                      wp.colormod = '0 1 1'; // cyan
 +              else if (waypoint_has_hardwiredlinks(wp))
 +                      wp.colormod = '0.5 0 1'; // purple
                else
                        wp.colormod = '1 1 1';
        }
                wp.model = "";
  }
  
 +string waypoint_get_type_name(entity wp)
 +{
 +      if (wp.wpflags & WAYPOINTFLAG_ITEM) return "^1Item waypoint";
 +      else if (wp.wpflags & WAYPOINTFLAG_CROUCH) return "^5Crouch waypoint";
 +      else if (wp.wpflags & WAYPOINTFLAG_JUMP) return "^xf80Jump waypoint";
 +      else if (wp.wpflags & WAYPOINTFLAG_SUPPORT) return "^2Support waypoint";
 +      else if (waypoint_has_hardwiredlinks(wp)) return "^x80fHardwired waypoint";
 +      else if (wp.wpflags & WAYPOINTFLAG_LADDER) return "^3Ladder waypoint";
 +      else if (wp.wpflags & WAYPOINTFLAG_TELEPORT)
 +      {
 +              if (!wp.wpisbox) return "^3Warpzone waypoint";
 +              else if (wp.wpflags & WAYPOINTFLAG_CUSTOM_JP) return "^3Custom jumppad waypoint";
 +              else
 +              {
 +                      IL_EACH(g_jumppads, boxesoverlap(wp.absmin, wp.absmax, it.absmin, it.absmax),
 +                              { return "^3Jumppad waypoint"; });
 +                      return "^3Teleport waypoint";
 +              }
 +      }
 +
 +      return "^7Waypoint";
 +}
 +
 +entity waypoint_get(vector m1, vector m2)
 +{
 +      if (m1 == m2)
 +      {
 +              m1 -= '8 8 8';
 +              m2 += '8 8 8';
 +      }
 +      IL_EACH(g_waypoints, boxesoverlap(m1, m2, it.absmin, it.absmax), { return it; });
 +
 +      return NULL;
 +}
 +
 +.float createdtime;
  entity waypoint_spawn(vector m1, vector m2, float f)
  {
        if(!(f & (WAYPOINTFLAG_PERSONAL | WAYPOINTFLAG_GENERATED)) && m1 == m2)
        {
 -              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;
 -              });
 +              entity wp_found = waypoint_get(m1, m2);
 +              if (wp_found)
 +                      return wp_found;
        }
        // spawn only one destination waypoint for teleports teleporting player to the exact same spot
        // otherwise links loaded from file would be applied only to the first destination
        // waypoint since link format doesn't specify waypoint entities but just positions
 -      if((f & WAYPOINTFLAG_GENERATED) && !(f & (WAYPOINTFLAG_NORELINK | WAYPOINTFLAG_PERSONAL)) && m1 == m2)
 +      if((f & WAYPOINTFLAG_GENERATED) && !(f & (WPFLAGMASK_NORELINK | WAYPOINTFLAG_PERSONAL)) && m1 == m2)
        {
                IL_EACH(g_waypoints, boxesoverlap(m1, m2, it.absmin, it.absmax),
                {
        w.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
        w.wpflags = f;
        w.solid = SOLID_TRIGGER;
 +      w.createdtime = time;
        setorigin(w, (m1 + m2) * 0.5);
        setsize(w, m1 - w.origin, m2 - w.origin);
        if (w.size)
  
        if(!w.wpisbox)
        {
 -              setsize(w, PL_MIN_CONST - '1 1 0', PL_MAX_CONST + '1 1 0');
 +              if (f & WAYPOINTFLAG_CROUCH)
 +                      setsize(w, PL_CROUCH_MIN_CONST - '1 1 0', PL_CROUCH_MAX_CONST + '1 1 0');
 +              else
 +                      setsize(w, PL_MIN_CONST - '1 1 0', PL_MAX_CONST + '1 1 0');
                if(!move_out_of_solid(w))
                {
                        if(!(f & WAYPOINTFLAG_GENERATED))
        return w;
  }
  
 -void waypoint_spawn_fromeditor(entity pl)
 +float trigger_push_get_push_time(entity this, vector endpos);
 +void waypoint_addlink_for_custom_jumppad(entity wp_from, entity wp_to)
 +{
 +      entity jp = NULL;
 +      IL_EACH(g_jumppads, boxesoverlap(wp_from.absmin, wp_from.absmax, it.absmin, it.absmax),
 +      {
 +              jp = it;
 +              break;
 +      });
 +      if (!jp)
 +              return;
 +
 +      float cost = trigger_push_get_push_time(jp, wp_to.origin);
 +      wp_from.wp00 = wp_to;
 +      wp_from.wp00mincost = cost;
 +      jp.nearestwaypoint = wp_from;
 +      jp.nearestwaypointtimeout = -1;
 +}
 +
 +bool start_wp_is_spawned;
 +vector start_wp_origin;
 +bool start_wp_is_hardwired;
 +bool start_wp_is_support;
 +
 +void waypoint_clear_start_wp_globals(entity pl, bool warn)
 +{
 +      start_wp_is_spawned = false;
 +      start_wp_origin = '0 0 0';
 +      pl.wp_locked = NULL;
 +      start_wp_is_hardwired = false;
 +      start_wp_is_support = false;
 +      if (warn)
 +              LOG_INFO("^xf80Start waypoint has been cleared.\n");
 +}
 +
 +void waypoint_start_hardwiredlink(entity pl, bool at_crosshair)
 +{
 +      entity wp = pl.nearestwaypoint;
 +      if (at_crosshair)
 +      {
 +              crosshair_trace_waypoints(pl);
 +              wp = trace_ent;
 +      }
 +      string err = "";
 +      if (start_wp_is_spawned && !start_wp_is_hardwired)
 +              err = "can't hardwire while in the process of creating a special link";
 +      else if (!wp)
 +      {
 +              if (at_crosshair)
 +                      err = "couldn't find any waypoint at crosshair";
 +              else
 +                      err = "couldn't find any waypoint nearby";
 +      }
 +      else if (wp.wpflags & WPFLAGMASK_NORELINK)
 +              err = "can't hardwire a waypoint with special links";
 +
 +      if (err == "")
 +      {
 +              start_wp_is_hardwired = true;
 +              start_wp_is_spawned = true;
 +              start_wp_origin = wp.origin;
 +              pl.wp_locked = wp;
 +              LOG_INFOF("^x80fWaypoint %s marked as hardwired link origin.\n", vtos(wp.origin));
 +      }
 +      else
 +      {
 +              start_wp_is_hardwired = false;
 +              LOG_INFO("Error: ", err, "\n");
 +      }
 +}
 +
 +void waypoint_spawn_fromeditor(entity pl, bool at_crosshair, bool is_jump_wp, bool is_crouch_wp, bool is_support_wp)
  {
 -      entity e;
 +      if (WAYPOINT_VERSION < waypoint_version_loaded)
 +      {
 +              LOG_INFOF("^1Editing waypoints with a higher version number (%f) is not allowed.\n"
 +                      "Update Xonotic to make them editable.", waypoint_version_loaded);
 +              return;
 +      }
 +
 +      entity e = NULL, jp = NULL;
        vector org = pl.origin;
 +      if (at_crosshair)
 +      {
 +              crosshair_trace_waypoints(pl);
 +              org = trace_endpos;
 +              if (!trace_ent)
 +                      org.z -= PL_MIN_CONST.z;
 +              if (!(start_wp_is_hardwired || start_wp_is_support))
 +                      IL_EACH(g_jumppads, boxesoverlap(org + PL_MIN_CONST, org + PL_MAX_CONST, it.absmin, it.absmax),
 +                      {
 +                              jp = it;
 +                              break;
 +                      });
 +              if (!jp && !start_wp_is_spawned && trace_ent)
 +              {
 +                      if (trace_ent.wpflags & (WAYPOINTFLAG_JUMP))
 +                              is_jump_wp = true;
 +                      else if (trace_ent.wpflags & (WAYPOINTFLAG_SUPPORT))
 +                              is_support_wp = true;
 +              }
 +      }
 +      if (jp || is_jump_wp || is_support_wp)
 +      {
 +              if (start_wp_is_spawned)
 +                      start_wp_is_spawned = false;
 +              LOG_INFO("^xf80Spawning start waypoint...\n");
 +      }
        int ctf_flags = havocbot_symmetry_origin_order;
        bool sym = ((autocvar_g_waypointeditor_symmetrical > 0 && ctf_flags >= 2)
                   || (autocvar_g_waypointeditor_symmetrical < 0));
                ctf_flags = 2;
        int wp_num = ctf_flags;
  
 -      if(!PHYS_INPUT_BUTTON_CROUCH(pl))
 +      if(!PHYS_INPUT_BUTTON_CROUCH(pl) && !at_crosshair && !is_jump_wp && !is_support_wp)
        {
                // snap waypoint to item's origin if close enough
                IL_EACH(g_items, true,
                });
        }
  
 +      vector start_org = '0 0 0';
 +      if (start_wp_is_spawned)
 +      {
 +              if (!start_wp_is_hardwired)
 +                      LOG_INFO("^xf80Spawning destination waypoint...\n");
 +              start_org = start_wp_origin;
 +      }
 +
 +      // save org as it can be modified spawning symmetrycal waypoints
 +      vector initial_origin = '0 0 0';
 +      bool initial_origin_is_set = false;
 +
        LABEL(add_wp);
 -      e = waypoint_spawn(org, org, 0);
 +
 +      if (jp)
 +      {
 +              e = NULL;
 +              IL_EACH(g_waypoints, it.wpflags & WPFLAGMASK_NORELINK
 +                      && boxesoverlap(org + PL_MIN_CONST, org + PL_MAX_CONST, it.absmin, it.absmax),
 +              {
 +                      e = it; break;
 +              });
 +              if (!e)
 +                      e = waypoint_spawn(jp.absmin - PL_MAX_CONST + '1 1 1', jp.absmax - PL_MIN_CONST + '-1 -1 -1', WAYPOINTFLAG_TELEPORT);
 +              if (!pl.wp_locked)
 +                      pl.wp_locked = e;
 +      }
 +      else if (is_jump_wp || is_support_wp)
 +      {
 +              int type_flag = (is_jump_wp) ? WAYPOINTFLAG_JUMP : WAYPOINTFLAG_SUPPORT;
 +
 +              entity wp_found = waypoint_get(org, org);
 +              if (wp_found && !(wp_found.wpflags & type_flag))
 +              {
 +                      LOG_INFOF("Error: can't spawn a %s waypoint over an existent waypoint of a different type\n", (is_jump_wp) ? "Jump" : "Support");
 +                      return;
 +              }
 +              e = waypoint_spawn(org, org, type_flag);
 +              if (!pl.wp_locked)
 +                      pl.wp_locked = e;
 +      }
 +      else
 +              e = waypoint_spawn(org, org, (is_crouch_wp) ? WAYPOINTFLAG_CROUCH : 0);
        if(!e)
        {
                LOG_INFOF("Couldn't spawn waypoint at %v\n", org);
 +              if (start_wp_is_spawned)
 +                      waypoint_clear_start_wp_globals(pl, true);
                return;
        }
 -      waypoint_schedulerelink(e);
 -      bprint(strcat("Waypoint spawned at ", vtos(e.origin), "\n"));
 -      if(sym)
 +
 +      if (!initial_origin_is_set)
 +      {
 +              initial_origin = e.origin;
 +              initial_origin_is_set = true;
 +      }
 +
 +      entity start_wp = NULL;
 +      if (start_wp_is_spawned)
 +      {
 +              IL_EACH(g_waypoints, (start_wp_is_hardwired || it.wpflags & WPFLAGMASK_NORELINK)
 +                      && boxesoverlap(start_org, start_org, it.absmin, it.absmax),
 +              {
 +                      start_wp = it; break;
 +              });
 +              if(!start_wp)
 +              {
 +                      // should not happen
 +                      LOG_INFOF("Couldn't find start waypoint at %v\n", start_org);
 +                      waypoint_clear_start_wp_globals(pl, true);
 +                      return;
 +              }
 +              if (start_wp_is_hardwired)
 +              {
 +                      if (waypoint_is_hardwiredlink(start_wp, e))
 +                      {
 +                              waypoint_unmark_hardwiredlink(start_wp, e);
 +                              waypoint_removelink(start_wp, e);
 +                              string s = strcat(vtos(start_wp.origin), "*", vtos(e.origin));
 +                              LOG_INFOF("^x80fRemoved hardwired link %s.\n", s);
 +                      }
 +                      else
 +                      {
 +                              if (e.createdtime == time)
 +                              {
 +                                      LOG_INFO("Error: hardwired links can be created only between 2 existing (and unconnected) waypoints.\n");
 +                                      waypoint_remove(e);
 +                                      waypoint_clear_start_wp_globals(pl, true);
 +                                      waypoint_spawn_fromeditor(pl, at_crosshair, is_jump_wp, is_crouch_wp, is_support_wp);
 +                                      return;
 +                              }
 +                              if (start_wp == e)
 +                              {
 +                                      LOG_INFO("Error: start and destination waypoints coincide.\n");
 +                                      waypoint_clear_start_wp_globals(pl, true);
 +                                      return;
 +                              }
 +                              if (waypoint_islinked(start_wp, e))
 +                              {
 +                                      LOG_INFO("Error: waypoints are already linked.\n");
 +                                      waypoint_clear_start_wp_globals(pl, true);
 +                                      return;
 +                              }
 +                              waypoint_addlink(start_wp, e);
 +                              waypoint_mark_hardwiredlink(start_wp, e);
 +                              string s = strcat(vtos(start_wp.origin), "*", vtos(e.origin));
 +                              LOG_INFOF("^x80fAdded hardwired link %s.\n", s);
 +                      }
 +              }
 +              else
 +              {
 +                      if (start_wp_is_support)
 +                      {
 +                              if (e.SUPPORT_WP)
 +                              {
 +                                      LOG_INFOF("Waypoint %v has already a support waypoint, delete it first.\n", e.origin);
 +                                      waypoint_clear_start_wp_globals(pl, true);
 +                                      return;
 +                              }
 +                              // clear all links to e
 +                              IL_EACH(g_waypoints, it != e,
 +                              {
 +                                      if (waypoint_islinked(it, e) && !waypoint_is_hardwiredlink(it, e))
 +                                              waypoint_removelink(it, e);
 +                              });
 +                      }
 +                      waypoint_addlink(start_wp, e);
 +              }
 +      }
 +
 +      if (!(jp || is_jump_wp || is_support_wp || start_wp_is_hardwired))
 +              waypoint_schedulerelink(e);
 +
 +      string wp_type_str = waypoint_get_type_name(e);
 +
 +      bprint(strcat(wp_type_str, "^7 spawned at ", vtos(e.origin), "\n"));
 +
 +      if (start_wp_is_spawned)
 +      {
 +              pl.wp_locked = NULL;
 +              if (!start_wp_is_hardwired)
 +                      waypoint_schedulerelink(start_wp);
 +              if (start_wp.wpflags & WAYPOINTFLAG_TELEPORT)
 +              {
 +                      if (start_wp.wp00_original == start_wp.wp00)
 +                              start_wp.wpflags &= ~WAYPOINTFLAG_CUSTOM_JP;
 +                      else
 +                              start_wp.wpflags |= WAYPOINTFLAG_CUSTOM_JP;
 +              }
 +      }
 +
 +      if (sym)
        {
 -              org = waypoint_getSymmetricalPoint(e.origin, ctf_flags);
 +              org = waypoint_getSymmetricalPoint(org, ctf_flags);
 +              if (jp)
 +              {
 +                      IL_EACH(g_jumppads, boxesoverlap(org + PL_MIN_CONST, org + PL_MAX_CONST, it.absmin, it.absmax),
 +                      {
 +                              jp = it; break;
 +                      });
 +              }
 +              if (start_wp_is_spawned)
 +                      start_org = waypoint_getSymmetricalPoint(start_org, ctf_flags);
                if (vdist(org - pl.origin, >, 32))
                {
                        if(wp_num > 2)
                        goto add_wp;
                }
        }
 +      if (jp || is_jump_wp || is_support_wp)
 +      {
 +              if (!start_wp_is_spawned)
 +              {
 +                      // we've just created a custom jumppad waypoint
 +                      // the next one created by the user will be the destination waypoint
 +                      start_wp_is_spawned = true;
 +                      start_wp_origin = initial_origin;
 +                      if (is_support_wp)
 +                              start_wp_is_support = true;
 +              }
 +      }
 +      else if (start_wp_is_spawned)
 +      {
 +              waypoint_clear_start_wp_globals(pl, false);
 +      }
  }
  
  void waypoint_remove(entity wp)
  {
        IL_EACH(g_waypoints, it != wp,
        {
 +              if (it.SUPPORT_WP == wp)
 +              {
 +                      it.SUPPORT_WP = NULL;
 +                      waypoint_schedulerelink(it); // restore incoming links
 +              }
                if (waypoint_islinked(it, wp))
 +              {
 +                      if (waypoint_is_hardwiredlink(it, wp))
 +                              waypoint_unmark_hardwiredlink(it, wp);
                        waypoint_removelink(it, wp);
 +              }
        });
        delete(wp);
  }
  
  void waypoint_remove_fromeditor(entity pl)
  {
 +      if (WAYPOINT_VERSION < waypoint_version_loaded)
 +      {
 +              LOG_INFOF("^1Editing waypoints with a higher version number (%f) is not allowed.\n"
 +                      "Update Xonotic to make them editable.", waypoint_version_loaded);
 +              return;
 +      }
 +
        entity e = navigation_findnearestwaypoint(pl, false);
  
        int ctf_flags = havocbot_symmetry_origin_order;
  
        LABEL(remove_wp);
        if (!e) return;
 -      if (e.wpflags & WAYPOINTFLAG_GENERATED) return;
  
 -      if (e.wphardwired)
 +      if (e.wpflags & WAYPOINTFLAG_GENERATED)
 +      {
 +              if (start_wp_is_spawned)
 +                      waypoint_clear_start_wp_globals(pl, true);
 +              return;
 +      }
 +
 +      if (waypoint_has_hardwiredlinks(e))
        {
 -              LOG_INFO("^1Warning: ^7Removal of hardwired waypoints is not allowed in the editor. Please remove links from/to this waypoint (", vtos(e.origin), ") by hand from maps/", mapname, ".waypoints.hardwired\n");
 +              LOG_INFO("Can't remove a waypoint with hardwired links, remove links with \"wpeditor hardwire\" first\n");
                return;
        }
  
        }
  
        bprint(strcat("Waypoint removed at ", vtos(e.origin), "\n"));
 +      te_explosion(e.origin);
        waypoint_remove(e);
  
        if (sym && wp_sym)
                        sym = false;
                goto remove_wp;
        }
 +
 +      if (start_wp_is_spawned)
 +              waypoint_clear_start_wp_globals(pl, true);
  }
  
  void waypoint_removelink(entity from, entity to)
  {
 -      if (from == to || (from.wpflags & WAYPOINTFLAG_NORELINK))
 +      if (from == to || (from.wpflags & WPFLAGMASK_NORELINK && !(from.wpflags & (WAYPOINTFLAG_JUMP | WAYPOINTFLAG_SUPPORT))))
                return;
  
        entity fromwp31_prev = from.wp31;
@@@ -995,18 -567,12 +995,18 @@@ float waypoint_getlinearcost(float dist
                return dist / (autocvar_sv_maxspeed * 1.25);
        return dist / autocvar_sv_maxspeed;
  }
 +
  float waypoint_getlinearcost_underwater(float dist)
  {
        // NOTE: underwater speed factor is hardcoded in the engine too, see SV_WaterMove
        return dist / (autocvar_sv_maxspeed * 0.7);
  }
  
 +float waypoint_getlinearcost_crouched(float dist)
 +{
 +      return dist / (autocvar_sv_maxspeed * 0.5);
 +}
 +
  float waypoint_gettravelcost(vector from, vector to, entity from_ent, entity to_ent)
  {
        bool submerged_from = navigation_check_submerged_state(from_ent, from);
        if (submerged_from && submerged_to)
                return waypoint_getlinearcost_underwater(vlen(to - from));
  
 +      if (from_ent.wpflags & WAYPOINTFLAG_CROUCH && to_ent.wpflags & WAYPOINTFLAG_CROUCH)
 +              return waypoint_getlinearcost_crouched(vlen(to - from));
 +
        float c = waypoint_getlinearcost(vlen(to - from));
  
        float height = from.z - to.z;
        if(height > jumpheight_vec.z && autocvar_sv_gravity > 0)
        {
 -              float height_cost = sqrt(height / (autocvar_sv_gravity / 2));
 +              float height_cost; // fall cost
 +              if (boolean(from_ent.wpflags & WAYPOINTFLAG_JUMP))
 +                      height_cost = jumpheight_time + sqrt((height + jumpheight_vec.z) / (autocvar_sv_gravity / 2));
 +              else
 +                      height_cost = sqrt(height / (autocvar_sv_gravity / 2));
                c = waypoint_getlinearcost(vlen(vec2(to - from))); // xy distance cost
                if(height_cost > c)
                        c = height_cost;
        }
  
 +      // consider half path underwater
        if (submerged_from || submerged_to)
                return (c + waypoint_getlinearcost_underwater(vlen(to - from))) / 2;
 +
 +      // consider half path crouched
 +      if (from_ent.wpflags & WAYPOINTFLAG_CROUCH || to_ent.wpflags & WAYPOINTFLAG_CROUCH)
 +              return (c + waypoint_getlinearcost_crouched(vlen(to - from))) / 2;
 +
        return c;
  }
  
@@@ -1071,7 -624,7 +1071,7 @@@ void waypoint_addlink_customcost(entit
  {
        if (from == to || waypoint_islinked(from, to))
                return;
 -      if (c == -1 && (from.wpflags & WAYPOINTFLAG_NORELINK))
 +      if (c == -1 && (from.wpflags & WPFLAGMASK_NORELINK) && !(from.wpflags & (WAYPOINTFLAG_JUMP | WAYPOINTFLAG_SUPPORT)))
                return;
  
        if(c == -1)
  
  void waypoint_addlink(entity from, entity to)
  {
 -      waypoint_addlink_customcost(from, to, -1);
 +      if ((from.wpflags & WPFLAGMASK_NORELINK) && !(from.wpflags & (WAYPOINTFLAG_JUMP | WAYPOINTFLAG_SUPPORT)))
 +              waypoint_addlink_for_custom_jumppad(from, to);
 +      else
 +              waypoint_addlink_customcost(from, to, -1);
 +
 +      if (from.wpflags & WAYPOINTFLAG_SUPPORT)
 +              to.SUPPORT_WP = from;
  }
  
  // relink this spawnfunc_waypoint
@@@ -1143,10 -690,8 +1143,10 @@@ void waypoint_think(entity this
        {
                if (boxesoverlap(this.absmin, this.absmax, it.absmin, it.absmax))
                {
 -                      waypoint_addlink(this, it);
 -                      waypoint_addlink(it, this);
 +                      if (!(this.wpflags & WPFLAGMASK_NORELINK))
 +                              waypoint_addlink(this, it);
 +                      if (!(it.wpflags & WPFLAGMASK_NORELINK))
 +                              waypoint_addlink(it, this);
                }
                else
                {
  
                        dv = ev - sv;
                        dv.z = 0;
 -                      if(vdist(dv, >=, 1050)) // max search distance in XY
 +                      int maxdist = 1050;
 +                      vector m1 = PL_MIN_CONST;
 +                      vector m2 = PL_MAX_CONST;
 +
 +                      if (this.wpflags & WAYPOINTFLAG_CROUCH || it.wpflags & WAYPOINTFLAG_CROUCH)
 +                      {
 +                              m1 = PL_CROUCH_MIN_CONST;
 +                              m2 = PL_CROUCH_MAX_CONST;
 +                              // links from crouch wp to normal wp (and viceversa) are very short to avoid creating many links
 +                              // that would be wasted due to rough travel cost calculation (the longer link is, the higher cost is)
 +                              // links from crouch wp to crouch wp can be as long as normal links
 +                              if (!(this.wpflags & WAYPOINTFLAG_CROUCH && it.wpflags & WAYPOINTFLAG_CROUCH))
 +                                      maxdist = 100;
 +                      }
 +
 +                      if (vdist(dv, >=, maxdist)) // max search distance in XY
                        {
                                ++relink_lengthculled;
                                continue;
  
                        //traceline(this.origin, it.origin, false, NULL);
                        //if (trace_fraction == 1)
 -                      if (this.wpisbox)
 +                      if (this.wpisbox || this.wpflags & (WAYPOINTFLAG_JUMP | WAYPOINTFLAG_SUPPORT) // forbid outgoing links
 +                              || it.SUPPORT_WP) // forbid incoming links
 +                      {
                                relink_walkculled += 0.5;
 +                      }
                        else
                        {
 -                              if (tracewalk(this, sv, PL_MIN_CONST, PL_MAX_CONST, ev2, ev2_height, MOVE_NOMONSTERS))
 +                              if (tracewalk(this, sv, m1, m2, ev2, ev2_height, MOVE_NOMONSTERS))
                                        waypoint_addlink(this, it);
                                else
                                        relink_walkculled += 0.5;
                        }
  
 -                      if (it.wpisbox)
 +                      // reverse direction
 +                      if (it.wpisbox || it.wpflags & (WAYPOINTFLAG_JUMP | WAYPOINTFLAG_SUPPORT) // forbid incoming links
 +                              || this.SUPPORT_WP) // forbid outgoing links
 +                      {
                                relink_walkculled += 0.5;
 +                      }
                        else
                        {
 -                              if (tracewalk(this, ev, PL_MIN_CONST, PL_MAX_CONST, sv2, sv2_height, MOVE_NOMONSTERS))
 +                              if (tracewalk(this, ev, m1, m2, sv2, sv2_height, MOVE_NOMONSTERS))
                                        waypoint_addlink(it, this);
                                else
                                        relink_walkculled += 0.5;
        navigation_testtracewalk = 0;
        this.wplinked = true;
        this.dphitcontentsmask = dphitcontentsmask_save;
 +
 +      setthink(this, func_null);
 +      this.nextthink = 0;
  }
  
  void waypoint_clearlinks(entity wp)
@@@ -1255,7 -775,7 +1255,7 @@@ void waypoint_schedulerelink(entity wp
        wp.enemy = NULL;
        if (!(wp.wpflags & WAYPOINTFLAG_PERSONAL))
                wp.owner = NULL;
 -      if (!(wp.wpflags & WAYPOINTFLAG_NORELINK))
 +      if (!(wp.wpflags & WPFLAGMASK_NORELINK))
                waypoint_clearlinks(wp);
        // schedule an actual relink on next frame
        setthink(wp, waypoint_think);
@@@ -1283,7 -803,7 +1283,7 @@@ void waypoint_schedulerelinkall(
        {
                waypoint_schedulerelink(it);
        });
 -      waypoint_load_links_hardwired();
 +      waypoint_load_hardwiredlinks();
  }
  
  #define GET_GAMETYPE_EXTENSION() ((g_race) ? ".race" : "")
@@@ -1413,8 -933,6 +1413,8 @@@ bool waypoint_load_links(
  
                ++c;
                waypoint_addlink(wp_from, wp_to);
 +              if (wp_from.wp00_original && wp_from.wp00_original != wp_from.wp00)
 +                      wp_from.wpflags |= WAYPOINTFLAG_CUSTOM_JP;
        }
  
        fclose(file);
        return true;
  }
  
 -void waypoint_load_or_remove_links_hardwired(bool removal_mode)
 +void waypoint_load_hardwiredlinks()
  {
        string s;
        float file, tokens, c = 0, found;
  
        if (file < 0)
        {
 -              if(!removal_mode)
 -                      LOG_TRACE("waypoint links load from ", filename, " failed");
 +              LOG_TRACE("waypoint links load from ", filename, " failed");
                return;
        }
  
 +      bool is_special = false;
        while ((s = fgets(file)))
        {
                if(substring(s, 0, 2)=="//")
                if(substring(s, 0, 1)=="#")
                        continue;
  
 +              // special links start with *, so old xonotic versions don't load them
 +              is_special = false;
 +              if (substring(s, 0, 1) == "*")
 +              {
 +                      is_special = true;
 +                      s = substring(s, 1, -1);
 +              }
 +
                tokens = tokenizebyseparator(s, "*");
  
                if (tokens!=2)
  
                        if(!found)
                        {
 -                              if(!removal_mode)
 -                                      LOG_INFO("NOTICE: Can not find origin waypoint for the hardwired link ", s, ". Path skipped");
 +                              s = strcat(((is_special) ? "special link " : "hardwired link "), s);
 +                              LOG_INFO("NOTICE: Can not find origin waypoint of the ", s, ". Path skipped");
                                continue;
                        }
                }
  
                if(!found)
                {
 -                      if(!removal_mode)
 -                              LOG_INFO("NOTICE: Can not find destination waypoint for the hardwired link ", s, ". Path skipped");
 +                      s = strcat(((is_special) ? "special link " : "hardwired link "), s);
 +                      LOG_INFO("NOTICE: Can not find destination waypoint of the ", s, ". Path skipped");
                        continue;
                }
  
                ++c;
 -              if(removal_mode)
 +
 +              if (!is_special)
                {
 -                      waypoint_removelink(wp_from, wp_to);
 -                      continue;
 +                      waypoint_addlink(wp_from, wp_to);
 +                      waypoint_mark_hardwiredlink(wp_from, wp_to);
 +              } else if (wp_from.wpflags & WPFLAGMASK_NORELINK
 +                      && (wp_from.wpflags & (WAYPOINTFLAG_JUMP | WAYPOINTFLAG_SUPPORT)
 +                              || (wp_from.wpisbox && wp_from.wpflags & WAYPOINTFLAG_TELEPORT)))
 +              {
 +                      waypoint_addlink(wp_from, wp_to);
                }
 -
 -              waypoint_addlink(wp_from, wp_to);
 -              wp_from.wphardwired = true;
 -              wp_to.wphardwired = true;
 -              waypoint_setupmodel(wp_from);
 -              waypoint_setupmodel(wp_to);
        }
  
        fclose(file);
  
 -      LOG_TRACE(((removal_mode) ? "unloaded " : "loaded "),
 -              ftos(c), " waypoint links from maps/", mapname, ".waypoints.hardwired");
 +      LOG_TRACE("loaded ", ftos(c), " waypoint links from maps/", mapname, ".waypoints.hardwired");
  }
  
  entity waypoint_get_link(entity w, float i)
        }
  }
  
 +// Save all hardwired waypoint links to a file
 +void waypoint_save_hardwiredlinks()
 +{
 +      string gt_ext = GET_GAMETYPE_EXTENSION();
 +
 +      string filename = sprintf("maps/%s.waypoints.hardwired", strcat(mapname, gt_ext));
 +      int file = fopen(filename, FILE_WRITE);
 +      if (file < 0)
 +      {
 +              LOG_TRACE("waypoint hardwired links ", filename, " creation failed");
 +              return;
 +      }
 +
 +      // write hardwired links to file
 +      int count = 0;
 +      fputs(file, "// HARDWIRED LINKS\n");
 +      IL_EACH(g_waypoints, waypoint_has_hardwiredlinks(it),
 +      {
 +              for (int j = 0; j < 32; ++j)
 +              {
 +                      entity link = waypoint_get_link(it, j);
 +                      if (waypoint_is_hardwiredlink(it, link))
 +                      {
 +                              // NOTE: vtos rounds vector components to 1 decimal place
 +                              string s = strcat(vtos(it.origin), "*", vtos(link.origin), "\n");
 +                              fputs(file, s);
 +                              ++count;
 +                      }
 +              }
 +      });
 +
 +      // write special links to file
 +      int count2 = 0;
 +      fputs(file, "\n// SPECIAL LINKS\n");
 +      IL_EACH(g_waypoints, it.wpflags & (WAYPOINTFLAG_JUMP | WAYPOINTFLAG_SUPPORT | WAYPOINTFLAG_CUSTOM_JP),
 +      {
 +              for (int j = 0; j < 32; ++j)
 +              {
 +                      entity link = waypoint_get_link(it, j);
 +                      if (link)
 +                      {
 +                              // NOTE: vtos rounds vector components to 1 decimal place
 +                              string s = strcat("*", vtos(it.origin), "*", vtos(link.origin), "\n");
 +                              fputs(file, s);
 +                              ++count2;
 +                      }
 +              }
 +      });
 +
 +      fclose(file);
 +
 +      LOG_INFOF("saved %d hardwired links and %d special links to %s", count, count2, filename);
 +}
 +
  // Save all waypoint links to a file
  void waypoint_save_links()
  {
 -      // temporarily remove hardwired links so they don't get saved among normal links
 -      waypoint_remove_links_hardwired();
 -
        string gt_ext = GET_GAMETYPE_EXTENSION();
  
        string filename = sprintf("maps/%s.waypoints.cache", strcat(mapname, gt_ext));
                fputs(file, strcat("//", "WAYPOINT_TIME ", waypoint_time, "\n"));
  
        int c = 0;
 -      IL_EACH(g_waypoints, true,
 +      IL_EACH(g_waypoints, !(it.wpflags & (WAYPOINTFLAG_JUMP | WAYPOINTFLAG_SUPPORT | WAYPOINTFLAG_CUSTOM_JP)),
        {
                for(int j = 0; j < 32; ++j)
                {
                        entity link = waypoint_get_link(it, j);
 -                      if(link)
 +                      if (link && !waypoint_is_hardwiredlink(it, link))
                        {
                                // NOTE: vtos rounds vector components to 1 decimal place
                                string s = strcat(vtos(it.origin), "*", vtos(link.origin), "\n");
        botframe_cachedwaypointlinks = true;
  
        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()
  {
 +      if (WAYPOINT_VERSION < waypoint_version_loaded)
 +      {
 +              LOG_INFOF("^1Overwriting waypoints with a higher version number (%f) is not allowed.\n"
 +                      "Update Xonotic to make them editable.", waypoint_version_loaded);
 +              return;
 +      }
        string gt_ext = GET_GAMETYPE_EXTENSION();
  
        string filename = sprintf("maps/%s.waypoints", strcat(mapname, gt_ext));
        });
        fclose(file);
        waypoint_save_links();
 +      waypoint_save_hardwiredlinks();
 +
        botframe_loadedforcedlinks = false;
  
 +      waypoint_version_loaded = WAYPOINT_VERSION;
        LOG_INFOF("saved %d waypoints to %s", c, filename);
  }
  
  float waypoint_loadall()
  {
        string s;
 -      float file, cwp, cwb, fl;
 +      int file, cwp, cwb, fl;
        vector m1, m2;
        cwp = 0;
        cwb = 0;
                if (!s)
                        break;
                fl = stof(s);
 +              if (fl & WAYPOINTFLAG_NORELINK__DEPRECATED)
 +                      fl &= ~WAYPOINTFLAG_NORELINK__DEPRECATED;
                waypoint_spawn(m1, m2, fl);
                if (m1 == m2)
                        cwp = cwp + 1;
                        cwb = cwb + 1;
        }
        fclose(file);
 +      waypoint_version_loaded = ver;
        LOG_TRACE("loaded ", ftos(cwp), " waypoints and ", ftos(cwb), " wayboxes from maps/", mapname, ".waypoints");
  
        if (autocvar_g_waypointeditor && autocvar_g_waypointeditor_symmetrical_allowload)
        {
+               string sym_str = "";
                cvar_set("g_waypointeditor_symmetrical", ftos(sym));
                if (sym == 1 && sym_param3 < 2)
                        cvar_set("g_waypointeditor_symmetrical_order", "0"); // make sure this is reset if not loaded
                                cvar_set("g_waypointeditor_symmetrical_origin", params);
                        }
                        cvar_set("g_waypointeditor_symmetrical_order", ftos(sym_param3));
-                       LOG_INFO("Waypoint editor: loaded symmetry ", ftos(sym), " with origin ", params, " and order ", ftos(sym_param3));
+                       sym_str = strcat(ftos(sym), " with origin ", params, " and order ", ftos(sym_param3));
                }
                else if (sym == -2)
                {
                        string params = strcat(ftos(sym_param1), " ", ftos(sym_param2));
                        cvar_set("g_waypointeditor_symmetrical_axis", params);
-                       LOG_INFO("Waypoint editor: loaded symmetry ", ftos(sym), " with axis ", params);
+                       sym_str = strcat(ftos(sym), " with axis ", params);
                }
                else
-                       LOG_INFO("Waypoint editor: loaded symmetry ", ftos(sym));
+                       sym_str = ftos(sym);
+               if (sym_str != "")
+                       LOG_INFO("Waypoint editor: loaded symmetry ", sym_str);
                LOG_INFO(strcat("g_waypointeditor_symmetrical", " has been set to ", cvar_string("g_waypointeditor_symmetrical")));
        }
  
 +      if (WAYPOINT_VERSION < waypoint_version_loaded)
 +              LOG_INFOF("^1Editing waypoints with a higher version number (%f) is not allowed.\n"
 +                      "Update Xonotic to make them editable.", waypoint_version_loaded);
 +
        return cwp + cwb;
  }
  
@@@ -1944,10 -1393,9 +1947,10 @@@ void waypoint_spawnforteleporter_boxes(
  {
        entity w;
        entity dw;
 -      w = waypoint_spawn(org1, org2, WAYPOINTFLAG_GENERATED | teleport_flag | WAYPOINTFLAG_NORELINK);
 +      w = waypoint_spawn(org1, org2, WAYPOINTFLAG_GENERATED | teleport_flag);
        dw = waypoint_spawn(destination1, destination2, WAYPOINTFLAG_GENERATED);
        // one way link to the destination
 +      w.wp00_original = dw;
        w.wp00 = dw;
        w.wp00mincost = timetaken; // this is just for jump pads
        // the teleporter's nearest spawnfunc_waypoint is this one
@@@ -2024,7 -1472,7 +2027,7 @@@ void waypoint_showlink(entity wp1, enti
        if (!(wp1 && wp2))
                return;
  
 -      if (wp1.wphardwired && wp2.wphardwired)
 +      if (waypoint_is_hardwiredlink(wp1, wp2) || wp1.wpflags & (WAYPOINTFLAG_JUMP | WAYPOINTFLAG_SUPPORT | WAYPOINTFLAG_CUSTOM_JP))
                te_beam(NULL, wp1.origin, wp2.origin);
        else if (display_type == 1)
                te_lightning2(NULL, wp1.origin, wp2.origin);
@@@ -2041,22 -1489,22 +2044,22 @@@ void waypoint_showlinks_to(entity wp, i
  
  void waypoint_showlinks_from(entity wp, int display_type)
  {
 -      waypoint_showlink(wp.wp00, wp, display_type); waypoint_showlink(wp.wp16, wp, display_type);
 -      waypoint_showlink(wp.wp01, wp, display_type); waypoint_showlink(wp.wp17, wp, display_type);
 -      waypoint_showlink(wp.wp02, wp, display_type); waypoint_showlink(wp.wp18, wp, display_type);
 -      waypoint_showlink(wp.wp03, wp, display_type); waypoint_showlink(wp.wp19, wp, display_type);
 -      waypoint_showlink(wp.wp04, wp, display_type); waypoint_showlink(wp.wp20, wp, display_type);
 -      waypoint_showlink(wp.wp05, wp, display_type); waypoint_showlink(wp.wp21, wp, display_type);
 -      waypoint_showlink(wp.wp06, wp, display_type); waypoint_showlink(wp.wp22, wp, display_type);
 -      waypoint_showlink(wp.wp07, wp, display_type); waypoint_showlink(wp.wp23, wp, display_type);
 -      waypoint_showlink(wp.wp08, wp, display_type); waypoint_showlink(wp.wp24, wp, display_type);
 -      waypoint_showlink(wp.wp09, wp, display_type); waypoint_showlink(wp.wp25, wp, display_type);
 -      waypoint_showlink(wp.wp10, wp, display_type); waypoint_showlink(wp.wp26, wp, display_type);
 -      waypoint_showlink(wp.wp11, wp, display_type); waypoint_showlink(wp.wp27, wp, display_type);
 -      waypoint_showlink(wp.wp12, wp, display_type); waypoint_showlink(wp.wp28, wp, display_type);
 -      waypoint_showlink(wp.wp13, wp, display_type); waypoint_showlink(wp.wp29, wp, display_type);
 -      waypoint_showlink(wp.wp14, wp, display_type); waypoint_showlink(wp.wp30, wp, display_type);
 -      waypoint_showlink(wp.wp15, wp, display_type); waypoint_showlink(wp.wp31, wp, display_type);
 +      waypoint_showlink(wp, wp.wp00, display_type); waypoint_showlink(wp, wp.wp16, display_type);
 +      waypoint_showlink(wp, wp.wp01, display_type); waypoint_showlink(wp, wp.wp17, display_type);
 +      waypoint_showlink(wp, wp.wp02, display_type); waypoint_showlink(wp, wp.wp18, display_type);
 +      waypoint_showlink(wp, wp.wp03, display_type); waypoint_showlink(wp, wp.wp19, display_type);
 +      waypoint_showlink(wp, wp.wp04, display_type); waypoint_showlink(wp, wp.wp20, display_type);
 +      waypoint_showlink(wp, wp.wp05, display_type); waypoint_showlink(wp, wp.wp21, display_type);
 +      waypoint_showlink(wp, wp.wp06, display_type); waypoint_showlink(wp, wp.wp22, display_type);
 +      waypoint_showlink(wp, wp.wp07, display_type); waypoint_showlink(wp, wp.wp23, display_type);
 +      waypoint_showlink(wp, wp.wp08, display_type); waypoint_showlink(wp, wp.wp24, display_type);
 +      waypoint_showlink(wp, wp.wp09, display_type); waypoint_showlink(wp, wp.wp25, display_type);
 +      waypoint_showlink(wp, wp.wp10, display_type); waypoint_showlink(wp, wp.wp26, display_type);
 +      waypoint_showlink(wp, wp.wp11, display_type); waypoint_showlink(wp, wp.wp27, display_type);
 +      waypoint_showlink(wp, wp.wp12, display_type); waypoint_showlink(wp, wp.wp28, display_type);
 +      waypoint_showlink(wp, wp.wp13, display_type); waypoint_showlink(wp, wp.wp29, display_type);
 +      waypoint_showlink(wp, wp.wp14, display_type); waypoint_showlink(wp, wp.wp30, display_type);
 +      waypoint_showlink(wp, wp.wp15, display_type); waypoint_showlink(wp, wp.wp31, display_type);
  }
  
  void crosshair_trace_waypoints(entity pl)
                        setsize(it, '-16 -16 -16', '16 16 16');
        });
  
 -      crosshair_trace(pl);
 +      WarpZone_crosshair_trace(pl);
  
        IL_EACH(g_waypoints, true, {
                it.solid = SOLID_TRIGGER;
                if (!it.wpisbox)
                        setsize(it, '0 0 0', '0 0 0');
        });
 +
        if (trace_ent.classname != "waypoint")
                trace_ent = NULL;
 +      else if (!trace_ent.wpisbox)
 +              trace_endpos = trace_ent.origin;
  }
  
  void botframe_showwaypointlinks()
                        it.wp_aimed = NULL;
                if (wasfreed(it.wp_locked))
                        it.wp_locked = NULL;
 -              if (PHYS_INPUT_BUTTON_USE(it))
 -                      it.wp_locked = it.wp_aimed;
                entity head = it.wp_locked;
                if (!head)
                        head = navigation_findnearestwaypoint(it, false);
                it.nearestwaypointtimeout = time + 2; // while I'm at it...
                if (IS_ONGROUND(it) || it.waterlevel > WATERLEVEL_NONE || it.wp_locked)
                        display_type = 1; // default
 -              else if(head && (head.wphardwired))
 +              else if(waypoint_has_hardwiredlinks(head))
                        display_type = 2; // only hardwired
  
                if (display_type)
                                wp = trace_ent;
                                if (wp != it.wp_aimed)
                                {
 -                                      str = sprintf("\necho ^2WP info^7: entity: %d, flags: %d, origin: '%s'\n", etof(wp), wp.wpflags, vtos(wp.origin));
 +                                      string wp_type_str = waypoint_get_type_name(wp);
 +                                      str = sprintf("\necho Entity %d: %s^7, flags: %d, origin: %s\n", etof(wp), wp_type_str, wp.wpflags, vtos(wp.origin));
                                        if (wp.wpisbox)
 -                                              str = strcat(str, sprintf("echo \" absmin: '%s', absmax: '%s'\"\n", vtos(wp.absmin), vtos(wp.absmax)));
 +                                              str = strcat(str, sprintf("echo \" absmin: %s, absmax: %s\"\n", vtos(wp.absmin), vtos(wp.absmax)));
                                        stuffcmd(it, str);
 -                                      str = sprintf("entity: %d\nflags: %d\norigin: \'%s\'", etof(wp), wp.wpflags, vtos(wp.origin));
 +                                      str = sprintf("Entity %d: %s^7\nflags: %d\norigin: %s", etof(wp), wp_type_str, wp.wpflags, vtos(wp.origin));
                                        if (wp.wpisbox)
 -                                              str = strcat(str, sprintf(" \nabsmin: '%s'\nabsmax: '%s'", vtos(wp.absmin), vtos(wp.absmax)));
 +                                              str = strcat(str, sprintf(" \nabsmin: %s\nabsmax: %s", vtos(wp.absmin), vtos(wp.absmax)));
                                        debug_text_3d(wp.origin, str, 0, 7, '0 0 0');
                                }
                        }
@@@ -2202,7 -1648,7 +2205,7 @@@ float botframe_autowaypoints_fix_from(e
                }
  
                float bestdist = maxdist;
 -              IL_EACH(g_waypoints, it != wp && !(it.wpflags & WAYPOINTFLAG_NORELINK),
 +              IL_EACH(g_waypoints, it != wp && !(it.wpflags & WPFLAGMASK_NORELINK),
                {
                        float d = vlen(wp.origin - it.origin) + vlen(it.origin - porg);
                        if(d < bestdist)
diff --combined xonotic-client.cfg
index 65a819c416d4f49a578eb991779626811bc30ce5,c9a49f924a330b15c327e566d9617ecad3247eeb..e7a0ec1a0a3e20b9c0d4b308f73a390cf0e0f70b
@@@ -234,8 -234,6 +234,8 @@@ cl_movement 
  cl_movement_track_canjump 0
  cl_stairsmoothspeed 200
  
 +alias wpeditor_menu "quickmenu file \"\" wpeditor.txt"
 +
  alias g_waypointeditor_spawn         "wpeditor spawn"
  alias g_waypointeditor_remove        "wpeditor remove"
  alias g_waypointeditor_relinkall     "wpeditor relinkall"
@@@ -398,6 -396,7 +398,7 @@@ set g_waypointsprite_timealphaexponent 
  seta g_waypointsprite_turrets 1 "disable turret waypoints"
  seta g_waypointsprite_turrets_maxdist 5000 "max distance for turret waypoints"
  seta g_waypointsprite_turrets_text 0 "show the turret's name in the waypoint"
+ seta g_waypointsprite_turrets_onlyhurt 0 "only show the turret waypoint for a short period after being hurt"
  seta g_waypointsprite_uppercase 1
  seta g_waypointsprite_text 0 "Always show text instead of icons, setting this to 0 will still use text if the icon is unavailable"
  seta g_waypointsprite_iconsize 32