From: terencehill Date: Wed, 31 Jul 2019 21:02:43 +0000 (+0200) Subject: Merge branch 'master' into terencehill/bot_waypoints X-Git-Tag: xonotic-v0.8.5~1356^2~14 X-Git-Url: https://de.git.xonotic.org/?p=xonotic%2Fxonotic-data.pk3dir.git;a=commitdiff_plain;h=772fb683d951a622cc8520827096ec34fdea4763;hp=d3c2728fcdf32660fbc629615e8207701b03962c Merge branch 'master' into terencehill/bot_waypoints --- diff --git a/qcsrc/common/constants.qh b/qcsrc/common/constants.qh index 15c1366ea1..a7e7da546e 100644 --- a/qcsrc/common/constants.qh +++ b/qcsrc/common/constants.qh @@ -50,6 +50,8 @@ const int SERVERFLAG_PLAYERSTATS = 4; // a bit more constant const vector PL_MAX_CONST = '16 16 45'; const vector PL_MIN_CONST = '-16 -16 -24'; +const vector PL_CROUCH_MAX_CONST = '16 16 25'; +const vector PL_CROUCH_MIN_CONST = '-16 -16 -24'; // gametype vote flags const int GTV_FORBIDDEN = 0; // Cannot be voted diff --git a/qcsrc/common/mapobjects/trigger/jumppads.qc b/qcsrc/common/mapobjects/trigger/jumppads.qc index a61935eb8c..f438fbf01f 100644 --- a/qcsrc/common/mapobjects/trigger/jumppads.qc +++ b/qcsrc/common/mapobjects/trigger/jumppads.qc @@ -339,13 +339,57 @@ bool trigger_push_testorigin_for_item(entity tracetest_ent, entity item, vector } #endif +#ifdef SVQC +vector trigger_push_get_start_point(entity this) +{ + // calculate a typical start point for the jump + vector org = (this.absmin + this.absmax) * 0.5; + org.z = this.absmax.z - PL_MIN_CONST.z - 7; + return org; +} + +float trigger_push_get_push_time(entity this, vector endpos) +{ + vector org = trigger_push_get_start_point(this); + + float grav = PHYS_GRAVITY(NULL); + + entity t = this.enemy; + if (t) + { + entity e = spawn(); + setsize(e, PL_MIN_CONST, PL_MAX_CONST); + e.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP; + vector v = trigger_push_calculatevelocity(org, t, this.height, e); + vector v2 = trigger_push_calculatevelocity(endpos, t, this.height, e); + delete(e); + return (v.z + v2.z) / grav; + } + else if (!(this.target && this.target != "")) + { + if (!this.team) + { + vector v = this.movedir; + + float t = v.z / grav; + float jump_height = 1/2 * grav * (t ** 2); + float remaining_height = org.z + jump_height - endpos.z; + float v2_z = sqrt(2 * grav * remaining_height); + + return (v.z + v2_z) / grav; + } + } + return 0; +} +#endif + /// if (item != NULL) returns true if the item can be reached by using this jumppad, false otherwise /// if (item == NULL) tests jumppad's trajectory and eventually spawns waypoints for it (return value doesn't matter) bool trigger_push_test(entity this, entity item) { - // first calculate a typical start point for the jump - vector org = (this.absmin + this.absmax) * 0.5; - org.z = this.absmax.z - PL_MIN_CONST.z - 7; +#ifdef SVQC + vector org = trigger_push_get_start_point(this); +#endif if (this.target) { diff --git a/qcsrc/server/bot/api.qh b/qcsrc/server/bot/api.qh index 3f434dbecc..7a26fae494 100644 --- a/qcsrc/server/bot/api.qh +++ b/qcsrc/server/bot/api.qh @@ -7,13 +7,22 @@ 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; @@ -51,8 +60,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); @@ -101,6 +110,7 @@ void navigation_goalrating_timeout_set(entity this); void navigation_goalrating_timeout_force(entity this); void navigation_goalrating_timeout_expire(entity this, float seconds); bool navigation_goalrating_timeout(entity this); +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); @@ -121,9 +131,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); void waypoint_getSymmetricalOrigin_cmd(entity caller, bool save, int arg_idx); void waypoint_getSymmetricalAxis_cmd(entity caller, bool save, int arg_idx); diff --git a/qcsrc/server/bot/default/bot.qc b/qcsrc/server/bot/default/bot.qc index 90338d2021..40afed1799 100644 --- a/qcsrc/server/bot/default/bot.qc +++ b/qcsrc/server/bot/default/bot.qc @@ -580,6 +580,7 @@ void bot_calculate_stepheightvec() stepheightvec = autocvar_sv_stepheight * '0 0 1'; jumpheight_vec = (autocvar_sv_jumpvelocity ** 2) / (2 * autocvar_sv_gravity) * '0 0 1'; jumpstepheightvec = stepheightvec + jumpheight_vec * 0.85; // reduce it a bit to make the jumps easy + jumpheight_time = autocvar_sv_jumpvelocity / autocvar_sv_gravity; } bool bot_fixcount() @@ -756,7 +757,7 @@ void bot_serverframe() if(botframe_cachedwaypointlinks) { if(!botframe_loadedforcedlinks) - waypoint_load_links_hardwired(); + waypoint_load_hardwiredlinks(); } else { diff --git a/qcsrc/server/bot/default/havocbot/havocbot.qc b/qcsrc/server/bot/default/havocbot/havocbot.qc index 57b4c310e3..9f6da64eb2 100644 --- a/qcsrc/server/bot/default/havocbot/havocbot.qc +++ b/qcsrc/server/bot/default/havocbot/havocbot.qc @@ -288,7 +288,7 @@ void havocbot_bunnyhop(entity this, vector dir) return; } - if(this.waterlevel > WATERLEVEL_WETFEET) + if(this.waterlevel > WATERLEVEL_WETFEET || IS_DUCKED(this)) { this.aistatus &= ~AI_STATUS_RUNNING; return; @@ -336,6 +336,7 @@ void havocbot_bunnyhop(entity this, vector dir) 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); @@ -410,6 +411,8 @@ void havocbot_bunnyhop(entity this, vector dir) // 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; @@ -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) @@ -678,7 +686,8 @@ void havocbot_movetogoal(entity this) 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) @@ -993,7 +1002,26 @@ void havocbot_movetogoal(entity this) 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)) { @@ -1029,10 +1057,20 @@ void havocbot_movetogoal(entity this) 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) @@ -1048,7 +1086,7 @@ void havocbot_movetogoal(entity this) 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 @@ -1112,7 +1150,9 @@ void havocbot_movetogoal(entity this) 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 @@ -1159,7 +1199,7 @@ void havocbot_movetogoal(entity this) } // 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))) { diff --git a/qcsrc/server/bot/default/navigation.qc b/qcsrc/server/bot/default/navigation.qc index d63e158781..130d4dccaf 100644 --- a/qcsrc/server/bot/default/navigation.qc +++ b/qcsrc/server/bot/default/navigation.qc @@ -47,6 +47,11 @@ bool navigation_goalrating_timeout(entity this) return this.bot_strategytime < time; } +void navigation_goalrating_timeout_extend_if_needed(entity this, float seconds) +{ + this.bot_strategytime = max(this.bot_strategytime, time + seconds); +} + #define MAX_CHASE_DISTANCE 700 bool navigation_goalrating_timeout_can_be_anticipated(entity this) { @@ -917,7 +922,7 @@ entity navigation_findnearestwaypoint_withdist_except(entity ent, float walkfrom vector pm2 = ent.origin + ent.maxs; // do two scans, because box test is cheaper - IL_EACH(g_waypoints, it != ent && it != except && !(it.wpflags & WAYPOINTFLAG_TELEPORT), + IL_EACH(g_waypoints, it != ent && it != except && !(it.wpflags & (WAYPOINTFLAG_TELEPORT | WAYPOINTFLAG_JUMP)), { if(boxesoverlap(pm1, pm2, it.absmin, it.absmax)) { @@ -948,7 +953,7 @@ entity navigation_findnearestwaypoint_withdist_except(entity ent, float walkfrom waypoint_clearlinks(ent); // initialize wpXXmincost fields IL_EACH(g_waypoints, it != ent, { - if(walkfromwp && (it.wpflags & WAYPOINTFLAG_NORELINK)) + if (walkfromwp && (it.wpflags & WPFLAGMASK_NORELINK)) continue; set_tracewalk_dest(ent, it.origin, false); @@ -964,7 +969,7 @@ entity navigation_findnearestwaypoint_withdist_except(entity ent, float walkfrom // box check failed, try walk IL_EACH(g_waypoints, it != ent, { - if(walkfromwp && (it.wpflags & WAYPOINTFLAG_NORELINK)) + if (walkfromwp && (it.wpflags & WPFLAGMASK_NORELINK)) continue; v = it.origin; @@ -1577,7 +1582,7 @@ bool navigation_shortenpath(entity this) next = this.goalstack01; // if for some reason the bot is closer to the next goal, pop the current one - if (!IS_MOVABLE(next) // already checked in the previous case + if (!IS_MOVABLE(next) && !this.goalcurrent.wpflags & (WAYPOINTFLAG_TELEPORT | WAYPOINTFLAG_JUMP) && vlen2(this.goalcurrent.origin - next.origin) > vlen2(next.origin - this.origin) && checkpvs(this.origin + this.view_ofs, next)) { @@ -1771,7 +1776,7 @@ entity navigation_get_really_close_waypoint(entity this) if(vdist(wp.origin - this.origin, >, 50)) { wp = NULL; - IL_EACH(g_waypoints, !(it.wpflags & WAYPOINTFLAG_TELEPORT), + IL_EACH(g_waypoints, !(it.wpflags & (WAYPOINTFLAG_TELEPORT | WAYPOINTFLAG_JUMP)), { if(vdist(it.origin - this.origin, <, 50)) { diff --git a/qcsrc/server/bot/default/navigation.qh b/qcsrc/server/bot/default/navigation.qh index d002ae29db..026d326b9e 100644 --- a/qcsrc/server/bot/default/navigation.qh +++ b/qcsrc/server/bot/default/navigation.qh @@ -10,6 +10,7 @@ float navigation_testtracewalk; vector jumpstepheightvec; vector stepheightvec; vector jumpheight_vec; +float jumpheight_time; entity navigation_bestgoal; @@ -29,6 +30,8 @@ entity navigation_bestgoal; .float goalcurrent_distance_2d; .float goalcurrent_distance_time; +// final goal (item, object or player) is also saved in this field +.entity goalentity; .float goalentity_lock_timeout; .bool goalentity_shouldbefrozen; diff --git a/qcsrc/server/bot/default/waypoints.qc b/qcsrc/server/bot/default/waypoints.qc index d69a89400f..d2d0aa15c8 100644 --- a/qcsrc/server/bot/default/waypoints.qc +++ b/qcsrc/server/bot/default/waypoints.qc @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -259,6 +260,80 @@ vector waypoint_getSymmetricalPoint(vector org, int ctf_flags) 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) @@ -270,11 +345,23 @@ void waypoint_setupmodel(entity wp) 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'; } @@ -282,21 +369,54 @@ void waypoint_setupmodel(entity wp) 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), { @@ -309,6 +429,7 @@ entity waypoint_spawn(vector m1, vector m2, float f) 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) @@ -316,7 +437,10 @@ entity waypoint_spawn(vector m1, vector m2, float f) 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)) @@ -345,10 +469,84 @@ entity waypoint_spawn(vector m1, vector m2, float f) 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) +{ + 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) { - 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(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)); @@ -358,7 +556,7 @@ void waypoint_spawn_fromeditor(entity pl) 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, @@ -373,18 +571,169 @@ void waypoint_spawn_fromeditor(entity pl) }); } + 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) @@ -394,20 +743,52 @@ void waypoint_spawn_fromeditor(entity pl) 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; @@ -421,11 +802,17 @@ void waypoint_remove_fromeditor(entity pl) 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 them with \"wpeditor hardwire\" first\n"); return; } @@ -454,11 +841,14 @@ void waypoint_remove_fromeditor(entity pl) 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; @@ -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); @@ -581,19 +977,32 @@ float waypoint_gettravelcost(vector from, vector to, entity from_ent, entity to_ 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; } @@ -624,7 +1033,7 @@ void waypoint_addlink_customcost(entity from, entity to, float c) { 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) @@ -667,7 +1076,13 @@ void waypoint_addlink_customcost(entity from, entity to, float c) 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 @@ -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 { @@ -711,7 +1128,22 @@ void waypoint_think(entity this) 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; @@ -721,21 +1153,28 @@ void waypoint_think(entity this) //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; @@ -745,6 +1184,9 @@ void waypoint_think(entity this) navigation_testtracewalk = 0; this.wplinked = true; this.dphitcontentsmask = dphitcontentsmask_save; + + setthink(this, func_null); + this.nextthink = 0; } void waypoint_clearlinks(entity wp) @@ -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); @@ -803,7 +1245,7 @@ void waypoint_schedulerelinkall() { waypoint_schedulerelink(it); }); - waypoint_load_links_hardwired(); + waypoint_load_hardwiredlinks(); } #define GET_GAMETYPE_EXTENSION() ((g_race) ? ".race" : "") @@ -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); @@ -955,7 +1399,7 @@ bool waypoint_load_links() return true; } -void waypoint_load_or_remove_links_hardwired(bool removal_mode) +void waypoint_load_hardwiredlinks() { string s; float file, tokens, c = 0, found; @@ -978,11 +1422,11 @@ void waypoint_load_or_remove_links_hardwired(bool removal_mode) 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)=="//") @@ -991,6 +1435,14 @@ void waypoint_load_or_remove_links_hardwired(bool removal_mode) 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) @@ -1017,8 +1469,7 @@ void waypoint_load_or_remove_links_hardwired(bool removal_mode) 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; } } @@ -1039,29 +1490,27 @@ void waypoint_load_or_remove_links_hardwired(bool removal_mode) 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) @@ -1104,12 +1553,63 @@ 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)); @@ -1125,12 +1625,12 @@ void waypoint_save_links() 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"); @@ -1144,13 +1644,17 @@ void waypoint_save_links() 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)); @@ -1216,8 +1720,11 @@ void waypoint_saveall() }); 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); } @@ -1225,7 +1732,7 @@ void waypoint_saveall() float waypoint_loadall() { string s; - float file, cwp, cwb, fl; + int file, cwp, cwb, fl; vector m1, m2; cwp = 0; cwb = 0; @@ -1292,6 +1799,8 @@ float waypoint_loadall() 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; @@ -1299,6 +1808,7 @@ float waypoint_loadall() 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) @@ -1330,6 +1840,10 @@ float waypoint_loadall() 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; } @@ -1390,9 +1904,10 @@ void waypoint_spawnforteleporter_boxes(entity e, int teleport_flag, vector org1, { 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 @@ -1469,7 +1984,7 @@ void waypoint_showlink(entity wp1, entity wp2, int display_type) 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); @@ -1486,22 +2001,22 @@ void waypoint_showlinks_to(entity wp, int display_type) 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) @@ -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) @@ -1571,13 +2086,14 @@ void botframe_showwaypointlinks() 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'); } } @@ -1645,7 +2161,7 @@ float botframe_autowaypoints_fix_from(entity p, float walkfromwp, entity wp, .en } 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 --git a/qcsrc/server/bot/default/waypoints.qh b/qcsrc/server/bot/default/waypoints.qh index 0b69dcbb8b..25356446a4 100644 --- a/qcsrc/server/bot/default/waypoints.qh +++ b/qcsrc/server/bot/default/waypoints.qh @@ -6,7 +6,8 @@ // increase by 0.01 when changes require only waypoint relinking // increase by 1 when changes require to manually edit waypoints // max 2 decimal places, always specified -const float WAYPOINT_VERSION = 1.02; +const float WAYPOINT_VERSION = 1.04; +float waypoint_version_loaded; string waypoint_time; // fields you can query using prvm_global server to get some statistics about waypoint linking culling @@ -21,23 +22,42 @@ float botframe_cachedwaypointlinks; .entity wp00, wp01, wp02, wp03, wp04, wp05, wp06, wp07, wp08, wp09, wp10, wp11, wp12, wp13, wp14, wp15; .entity wp16, wp17, wp18, wp19, wp20, wp21, wp22, wp23, wp24, wp25, wp26, wp27, wp28, wp29, wp30, wp31; +// used by jumppads to store original destination wp, used in case it gets changed in the editor +.entity wp00_original; + .float wp00mincost, wp01mincost, wp02mincost, wp03mincost, wp04mincost, wp05mincost, wp06mincost, wp07mincost; .float wp08mincost, wp09mincost, wp10mincost, wp11mincost, wp12mincost, wp13mincost, wp14mincost, wp15mincost; .float wp16mincost, wp17mincost, wp18mincost, wp19mincost, wp20mincost, wp21mincost, wp22mincost, wp23mincost; .float wp24mincost, wp25mincost, wp26mincost, wp27mincost, wp28mincost, wp29mincost, wp30mincost, wp31mincost; -.float wpfire, wpcost, wpconsidered, wpisbox, wplinked, wphardwired; +// hardwired links are saved to the .wpXX fields among normal links +// AND to the supporting .wphwXX fields to allow identifying them precisely +// these links are not sorted, unlike normal links +.entity wphw00, wphw01, wphw02, wphw03, wphw04, wphw05, wphw06, wphw07; + +.float wpcost; +.int wpfire, wpconsidered, wpisbox, wplinked; .int wpflags; .entity wp_aimed; .entity wp_locked; .vector wpnearestpoint; +// holds reference to the support waypoint, if any +.entity goalentity; +#define SUPPORT_WP goalentity + /* * Functions */ spawnfunc(waypoint); + +bool waypoint_has_hardwiredlinks(entity wp); +bool waypoint_is_hardwiredlink(entity wp_from, entity wp_to); +void waypoint_mark_hardwiredlink(entity wp_from, entity wp_to); +void waypoint_unmark_hardwiredlink(entity wp_from, entity wp_to); + void waypoint_removelink(entity from, entity to); int waypoint_getlinknum(entity from, entity to); bool waypoint_islinked(entity from, entity to); @@ -66,11 +86,9 @@ void botframe_showwaypointlinks(); float waypoint_loadall(); bool waypoint_load_links(); -#define waypoint_load_links_hardwired() waypoint_load_or_remove_links_hardwired(false) -#define waypoint_remove_links_hardwired() waypoint_load_or_remove_links_hardwired(true) -void waypoint_load_or_remove_links_hardwired(bool removal_mode); +void waypoint_load_hardwiredlinks(); -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); entity waypoint_spawnpersonal(entity this, vector position); diff --git a/qcsrc/server/bot/null/bot_null.qc b/qcsrc/server/bot/null/bot_null.qc index bdca146c2e..a7f8e99f1d 100644 --- a/qcsrc/server/bot/null/bot_null.qc +++ b/qcsrc/server/bot/null/bot_null.qc @@ -39,6 +39,6 @@ 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) { return NULL; } #endif diff --git a/qcsrc/server/command/cmd.qc b/qcsrc/server/command/cmd.qc index 1395986b97..e599231c65 100644 --- a/qcsrc/server/command/cmd.qc +++ b/qcsrc/server/command/cmd.qc @@ -184,10 +184,11 @@ void ClientCommand_wpeditor(entity caller, int request, int argc) { if (argv(1) == "spawn") { + string s = argv(2); if (!IS_PLAYER(caller)) sprint(caller, "ERROR: this command works only if you are player\n"); else - waypoint_spawn_fromeditor(caller); + waypoint_spawn_fromeditor(caller, (s == "crosshair"), (s == "jump"), (s == "crouch"), (s == "support")); return; } else if (argv(1) == "remove") @@ -198,6 +199,11 @@ void ClientCommand_wpeditor(entity caller, int request, int argc) waypoint_remove_fromeditor(caller); return; } + else if (argv(1) == "hardwire") + { + waypoint_start_hardwiredlink(caller); + return; + } else if (argv(1) == "unreachable") { if (!IS_PLAYER(caller)) @@ -240,13 +246,22 @@ void ClientCommand_wpeditor(entity caller, int request, int argc) case CMD_REQUEST_USAGE: { sprint(caller, "\nUsage:^3 cmd wpeditor action\n"); - sprint(caller, " Where 'action' can be: spawn, remove, unreachable, saveall, relinkall,\n"); - sprint(caller, " symorigin get|set\n"); - sprint(caller, " symorigin get|set p1 p2 ... pX\n"); - sprint(caller, " symaxis get|set p1 p2\n"); - sprint(caller, " where p1 p2 ... pX are positions \"x y z\" (z can be omitted)\n"); - sprint(caller, " symorigin and symaxis commands are useful to determine origin/axis of symmetry" - " on maps without ctf flags or where flags aren't perfectly symmetrical\n"); + sprint(caller, " Where 'action' can be:\n"); + sprint(caller, " ^5spawn^7: spawns a waypoint at player's position\n"); + sprint(caller, " ^5remove^7: remove player's nearest waypoint\n"); + sprint(caller, " ^5unreachable^7: useful to reveal waypoints and items unreachable from the current position and spawnpoints without a nearest waypoint\n"); + sprint(caller, " ^5saveall^7: saves all waypoints and links to file\n"); + sprint(caller, " ^5relinkall^7: relink all waypoints as if they were respawned\n"); + sprint(caller, " ^5spawn crosshair^7: spawns a waypoint at crosshair's position (useful to spawn custom jumppad waypoints (spawn another waypoint to create destination))\n"); + sprint(caller, " ^5spawn jump^7: spawns a jump waypoint (spawn another waypoint to create destination)\n"); + sprint(caller, " ^5spawn crouch^7: spawns a crouch waypoint\n"); + sprint(caller, " ^5spawn support^7: spawns a support waypoint (spawn another waypoint to create destination from which all incoming links are removed), useful to replace links to preblematic jumppad/teleport waypoints\n"); + sprint(caller, " ^5hardwire^7: marks the nearest waypoint as origin of a new hardwired link (spawn another waypoint over an existing one to create destination)\n"); + sprint(caller, " ^5symorigin get|set\n"); + sprint(caller, " ^5symorigin get|set p1 p2 ... pX\n"); + sprint(caller, " ^5symaxis get|set p1 p2\n"); + sprint(caller, " ^7 where p1 p2 ... pX are positions (\"x y z\", z can be omitted) that you know are perfectly symmetrical"); + sprint(caller, " ^7 so you can determine origin/axis of symmetry of maps without ctf flags or where flags aren't perfectly symmetrical\n"); return; } }