]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Merge branch 'master' into terencehill/bot_waypoints
authorterencehill <piuntn@gmail.com>
Wed, 31 Jul 2019 21:02:43 +0000 (23:02 +0200)
committerterencehill <piuntn@gmail.com>
Wed, 31 Jul 2019 21:02:43 +0000 (23:02 +0200)
1  2 
qcsrc/server/bot/default/havocbot/havocbot.qc
qcsrc/server/bot/default/waypoints.qc

index 17ec0c6fe02ef27651f97b7d68dc42a2fa06fa24,57b4c310e37ed33760c7500f81c5fa1d8089d7f3..9f6da64eb26abe234e4142e221f24ecd8d47a98c
@@@ -288,7 -288,7 +288,7 @@@ void havocbot_bunnyhop(entity this, vec
                return;
        }
  
 -      if(this.waterlevel > WATERLEVEL_WETFEET)
 +      if(this.waterlevel > WATERLEVEL_WETFEET || IS_DUCKED(this))
        {
                this.aistatus &= ~AI_STATUS_RUNNING;
                return;
                                        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))
                                        {
                                                vector gno = (this.goalstack01.absmin + this.goalstack01.absmax) * 0.5;
                                                deviation = vectoangles(gno - this.origin) - vectoangles(gco - this.origin);
  // 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 -476,6 +479,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)))
                        {
        if(skill+this.bot_moveskill >= autocvar_bot_ai_bunnyhop_skilloffset)
                havocbot_bunnyhop(this, dir);
  
-       if ((dir * v_up) >= autocvar_sv_jumpvelocity*0.5 && (IS_ONGROUND(this))) PHYS_INPUT_BUTTON_JUMP(this) = true;
-       if (((dodge * v_up) > 0) && random()*frametime >= 0.2*bound(0,(10-skill-this.bot_dodgeskill)*0.1,1)) PHYS_INPUT_BUTTON_JUMP(this) = true;
-       if (((dodge * v_up) < 0) && random()*frametime >= 0.5*bound(0,(10-skill-this.bot_dodgeskill)*0.1,1)) this.havocbot_ducktime=time+0.3/bound(0.1,skill+this.bot_dodgeskill,10);
+       if (dir * v_up >= autocvar_sv_jumpvelocity * 0.5 && IS_ONGROUND(this))
+               PHYS_INPUT_BUTTON_JUMP(this) = true;
+       if (dodge)
+       {
+               if (dodge * v_up > 0 && random() * frametime >= 0.2 * bound(0, (10 - skill - this.bot_dodgeskill) * 0.1, 1))
+                       PHYS_INPUT_BUTTON_JUMP(this) = true;
+               if (dodge * v_up < 0 && random() * frametime >= 0.5 * bound(0, (10 - skill - this.bot_dodgeskill) * 0.1, 1))
+                       this.havocbot_ducktime = time + 0.3 / bound(0.1, skill + this.bot_dodgeskill, 10);
+       }
  }
  
  entity havocbot_gettarget(entity this, bool secondary)
index 9d08e433cb14cb62fb5d11cfe35fba15355f5286,d69a89400f53be33f8f2079627df8f8ff51f850e..d2d0aa15c86a2aa02399c3058fb57f6eb7668e7d
@@@ -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,80 -259,6 +260,80 @@@ vector waypoint_getSymmetricalPoint(vec
        return new_org;
  }
  
 +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)
  {
 -      entity e;
 +      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)
 +{
 +      entity wp = pl.nearestwaypoint;
 +      if ((!start_wp_is_spawned || start_wp_is_hardwired) && wp && !(wp.wpflags & WPFLAGMASK_NORELINK))
 +      {
 +              start_wp_is_hardwired = true;
 +              start_wp_is_spawned = true;
 +              start_wp_origin = wp.origin;
 +              pl.wp_locked = wp;
 +              LOG_INFOF("^x80fNearest waypoint %s marked as hardwired link origin.\n", vtos(wp.origin));
 +      }
 +      else
 +              start_wp_is_hardwired = false;
 +}
 +
 +void waypoint_spawn_fromeditor(entity pl, bool at_crosshair, bool is_jump_wp, bool is_crouch_wp, bool is_support_wp)
 +{
 +      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(pl);
 +              org = trace_endpos - eZ * 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 || 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)
        {
 -              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");
 +              if (start_wp_is_spawned)
 +                      waypoint_clear_start_wp_globals(pl, true);
 +              return;
 +      }
 +
 +      if (waypoint_has_hardwiredlinks(e))
 +      {
 +              LOG_INFO("Can't remove a waypoint with hardwired links, remove them with \"wpeditor hardwire\" first\n");
                return;
        }
  
                        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;
@@@ -957,18 -567,12 +957,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;
  }
  
@@@ -1033,7 -624,7 +1033,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
@@@ -1105,10 -690,8 +1105,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)
@@@ -1217,7 -775,7 +1217,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);
@@@ -1245,7 -803,7 +1245,7 @@@ void waypoint_schedulerelinkall(
        {
                waypoint_schedulerelink(it);
        });
 -      waypoint_load_links_hardwired();
 +      waypoint_load_hardwiredlinks();
  }
  
  #define GET_GAMETYPE_EXTENSION() ((g_race) ? ".race" : "")
@@@ -1375,8 -933,6 +1375,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");
 +                              LOG_INFO("NOTICE: Can not find origin waypoint for the hardwired link ", s, ". Path skipped");
                                continue;
                        }
                }
  
                if(!found)
                {
 -                      if(!removal_mode)
 -                              LOG_INFO("NOTICE: Can not find destination waypoint for the hardwired link ", s, ". Path skipped");
 +                      LOG_INFO("NOTICE: Can not find destination waypoint for the hardwired link ", 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)
                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;
  }
  
@@@ -1904,10 -1390,9 +1904,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
@@@ -1984,7 -1469,7 +1984,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);
@@@ -2001,22 -1486,22 +2001,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)
@@@ -2059,7 -1544,7 +2059,7 @@@ void botframe_showwaypointlinks(
                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');
                                }
                        }
@@@ -2161,7 -1645,7 +2161,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)
  }
  
  // automatically create missing waypoints
- .entity botframe_autowaypoints_lastwp0, botframe_autowaypoints_lastwp1;
  void botframe_autowaypoints_fix(entity p, float walkfromwp, .entity fld)
  {
        float r = botframe_autowaypoints_fix_from(p, walkfromwp, p.(fld), fld);
@@@ -2355,6 -1838,8 +2354,8 @@@ LABEL(next
        });
  }
  
+ //.entity botframe_autowaypoints_lastwp0;
+ .entity botframe_autowaypoints_lastwp1;
  void botframe_autowaypoints()
  {
        FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it) && !IS_DEAD(it), {