From: TimePath Date: Sun, 7 Aug 2016 03:46:16 +0000 (+1000) Subject: Bots: define the API boundaries X-Git-Tag: xonotic-v0.8.2~745^2 X-Git-Url: https://git.rm.cloudns.org/?a=commitdiff_plain;h=b7d363a108963ca13647ee25b58c5a531366cf49;p=xonotic%2Fxonotic-data.pk3dir.git Bots: define the API boundaries --- diff --git a/qcsrc/common/monsters/monster.qh b/qcsrc/common/monsters/monster.qh index de24caa01..7b9638070 100644 --- a/qcsrc/common/monsters/monster.qh +++ b/qcsrc/common/monsters/monster.qh @@ -4,7 +4,7 @@ #ifdef SVQC #include "sv_monsters.qh" #include -#include +#include #include #include #include diff --git a/qcsrc/common/monsters/sv_monsters.qc b/qcsrc/common/monsters/sv_monsters.qc index 8508d5708..f03a51368 100644 --- a/qcsrc/common/monsters/sv_monsters.qc +++ b/qcsrc/common/monsters/sv_monsters.qc @@ -685,6 +685,8 @@ void Monster_CalculateVelocity(entity this, vector to, vector from, float turnra //this.angles = vectoangles(this.velocity); } +.entity draggedby; + void Monster_Move(entity this, float runspeed, float walkspeed, float stpspeed) { if(this.target2) { this.goalentity = find(NULL, targetname, this.target2); } diff --git a/qcsrc/common/t_items.qc b/qcsrc/common/t_items.qc index f7c816295..d180e1c1b 100644 --- a/qcsrc/common/t_items.qc +++ b/qcsrc/common/t_items.qc @@ -4,8 +4,7 @@ #if defined(SVQC) - #include "../server/bot/bot.qh" - #include "../server/bot/waypoints.qh" + #include "../server/bot/api.qh" #include diff --git a/qcsrc/common/triggers/func/breakable.qc b/qcsrc/common/triggers/func/breakable.qc index 6763701d3..29d6c6a26 100644 --- a/qcsrc/common/triggers/func/breakable.qc +++ b/qcsrc/common/triggers/func/breakable.qc @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include #include diff --git a/qcsrc/common/weapons/all.qh b/qcsrc/common/weapons/all.qh index fa9d64ab8..24ea41c64 100644 --- a/qcsrc/common/weapons/all.qh +++ b/qcsrc/common/weapons/all.qh @@ -26,15 +26,15 @@ WepSet ReadWepSet(); #include -#ifdef SVQC -#include -#endif - REGISTRY(Weapons, 72) // Increase as needed. Can be up to 72. #define Weapons_from(i) _Weapons_from(i, WEP_Null) REGISTER_REGISTRY(Weapons) STATIC_INIT(WeaponPickup) { FOREACH(Weapons, true, it.m_pickup = NEW(WeaponPickup, it)); } +#ifdef SVQC +#include +#endif + .WepSet m_wepset; #define WEPSET(id) (WEP_##id.m_wepset) #define WepSet_FromWeapon(it) ((it).m_wepset) diff --git a/qcsrc/server/autocvars.qh b/qcsrc/server/autocvars.qh index 188cc7131..a8d45bd52 100644 --- a/qcsrc/server/autocvars.qh +++ b/qcsrc/server/autocvars.qh @@ -5,61 +5,9 @@ bool autocvar__campaign_testrun; int autocvar__campaign_index; string autocvar__campaign_name; bool autocvar__sv_init; -float autocvar_bot_ai_aimskill_blendrate; -float autocvar_bot_ai_aimskill_firetolerance_distdegrees; -float autocvar_bot_ai_aimskill_firetolerance_maxdegrees; -float autocvar_bot_ai_aimskill_firetolerance_mindegrees; -float autocvar_bot_ai_aimskill_fixedrate; -float autocvar_bot_ai_aimskill_mouse; -float autocvar_bot_ai_aimskill_offset; -float autocvar_bot_ai_aimskill_order_filter_1st; -float autocvar_bot_ai_aimskill_order_filter_2nd; -float autocvar_bot_ai_aimskill_order_filter_3th; -float autocvar_bot_ai_aimskill_order_filter_4th; -float autocvar_bot_ai_aimskill_order_filter_5th; -float autocvar_bot_ai_aimskill_order_mix_1st; -float autocvar_bot_ai_aimskill_order_mix_2nd; -float autocvar_bot_ai_aimskill_order_mix_3th; -float autocvar_bot_ai_aimskill_order_mix_4th; -float autocvar_bot_ai_aimskill_order_mix_5th; -float autocvar_bot_ai_aimskill_think; -float autocvar_bot_ai_bunnyhop_firstjumpdelay; -float autocvar_bot_ai_bunnyhop_skilloffset; -float autocvar_bot_ai_bunnyhop_startdistance; -float autocvar_bot_ai_bunnyhop_stopdistance; -float autocvar_bot_ai_chooseweaponinterval; -string autocvar_bot_ai_custom_weapon_priority_close; -string autocvar_bot_ai_custom_weapon_priority_distances; -string autocvar_bot_ai_custom_weapon_priority_far; -string autocvar_bot_ai_custom_weapon_priority_mid; -float autocvar_bot_ai_dangerdetectioninterval; -float autocvar_bot_ai_dangerdetectionupdates; -float autocvar_bot_ai_enemydetectioninterval; -float autocvar_bot_ai_enemydetectionradius; -float autocvar_bot_ai_friends_aware_pickup_radius; -float autocvar_bot_ai_ignoregoal_timeout; -float autocvar_bot_ai_keyboard_distance; -float autocvar_bot_ai_keyboard_threshold; -float autocvar_bot_ai_navigation_jetpack; -float autocvar_bot_ai_navigation_jetpack_mindistance; float autocvar_bot_ai_strategyinterval; -float autocvar_bot_ai_thinkinterval; -bool autocvar_bot_ai_weapon_combo; -float autocvar_bot_ai_weapon_combo_threshold; -string autocvar_bot_config_file; -bool autocvar_bot_god; -bool autocvar_bot_ignore_bots; -bool autocvar_bot_join_empty; -bool autocvar_bot_navigation_ignoreplayers; -bool autocvar_bot_nofire; #define autocvar_bot_number cvar("bot_number") -#define autocvar_bot_prefix cvar_string("bot_prefix") -#define autocvar_bot_suffix cvar_string("bot_suffix") -bool autocvar_bot_usemodelnames; int autocvar_bot_vs_human; -bool autocvar_bot_debug_tracewalk; -bool autocvar_bot_debug_goalstack; -bool autocvar_bot_wander_enable; int autocvar_captureleadlimit_override; #define autocvar_capturelimit_override cvar("capturelimit_override") float autocvar_ekg; @@ -172,7 +120,6 @@ float autocvar_g_chat_flood_spl_tell; int autocvar_g_chat_nospectators; bool autocvar_g_chat_teamcolors; bool autocvar_g_chat_tellprivacy; -bool autocvar_g_debug_bot_commands; bool autocvar_g_forced_respawn; string autocvar_g_forced_team_blue; string autocvar_g_forced_team_otherwise; @@ -296,7 +243,6 @@ float autocvar_g_turrets_targetscan_maxdelay; float autocvar_g_turrets_targetscan_mindelay; bool autocvar_g_use_ammunition; bool autocvar_g_waypointeditor; -int autocvar_g_waypointeditor_auto; bool autocvar_g_waypoints_for_items; #define autocvar_g_weapon_stay cvar("g_weapon_stay") bool autocvar_g_weapon_throwable; @@ -321,7 +267,6 @@ int autocvar_rescan_pending; bool autocvar_samelevel; string autocvar_sessionid; #define autocvar_skill cvar("skill") -float autocvar_skill_auto; #define autocvar_slowmo cvar("slowmo") float autocvar_snd_soundradius; int autocvar_spawn_debug; @@ -429,7 +374,6 @@ float autocvar_timelimit_overtime; int autocvar_timelimit_overtimes; float autocvar_timelimit_suddendeath; #define autocvar_utf8_enable cvar("utf8_enable") -bool autocvar_waypoint_benchmark; float autocvar_sv_gameplayfix_gravityunaffectedbyticrate; bool autocvar_sv_gameplayfix_upwardvelocityclearsongroundflag; float autocvar_g_trueaim_minrange; diff --git a/qcsrc/server/bot/_mod.inc b/qcsrc/server/bot/_mod.inc index 787939110..8f0672e26 100644 --- a/qcsrc/server/bot/_mod.inc +++ b/qcsrc/server/bot/_mod.inc @@ -1,6 +1,2 @@ // generated file; do not modify -#include -#include -#include -#include -#include +#include diff --git a/qcsrc/server/bot/_mod.qh b/qcsrc/server/bot/_mod.qh index 802d39187..33f0b0229 100644 --- a/qcsrc/server/bot/_mod.qh +++ b/qcsrc/server/bot/_mod.qh @@ -1,6 +1,2 @@ // generated file; do not modify -#include -#include -#include -#include -#include +#include diff --git a/qcsrc/server/bot/aim.qc b/qcsrc/server/bot/aim.qc deleted file mode 100644 index b867b5f6d..000000000 --- a/qcsrc/server/bot/aim.qc +++ /dev/null @@ -1,393 +0,0 @@ -#include "aim.qh" - -#include "bot.qh" - -#include -#include - -#include "../weapons/weaponsystem.qh" - -#include "../mutators/all.qh" - -// traces multiple trajectories to find one that will impact the target -// 'end' vector is the place it aims for, -// returns true only if it hit targ (don't target non-solid entities) - -float findtrajectorywithleading(vector org, vector m1, vector m2, entity targ, float shotspeed, float shotspeedupward, float maxtime, float shotdelay, entity ignore) -{ - float c, savesolid, shottime; - vector dir, end, v, o; - if (shotspeed < 1) - return false; // could cause division by zero if calculated - if (targ.solid < SOLID_BBOX) // SOLID_NOT and SOLID_TRIGGER - return false; // could never hit it - if (!tracetossent) - tracetossent = new(tracetossent); - tracetossent.owner = ignore; - setsize(tracetossent, m1, m2); - savesolid = targ.solid; - targ.solid = SOLID_NOT; - o = (targ.absmin + targ.absmax) * 0.5; - shottime = ((vlen(o - org) / shotspeed) + shotdelay); - v = targ.velocity * shottime + o; - tracebox(o, targ.mins, targ.maxs, v, false, targ); - v = trace_endpos; - end = v + (targ.mins + targ.maxs) * 0.5; - if ((vlen(end - org) / shotspeed + 0.2) > maxtime) - { - // out of range - targ.solid = savesolid; - return false; - } - - if (!tracetossfaketarget) - tracetossfaketarget = new(tracetossfaketarget); - tracetossfaketarget.solid = savesolid; - set_movetype(tracetossfaketarget, targ.move_movetype); - _setmodel(tracetossfaketarget, targ.model); // no low precision - tracetossfaketarget.model = targ.model; - tracetossfaketarget.modelindex = targ.modelindex; - setsize(tracetossfaketarget, targ.mins, targ.maxs); - setorigin(tracetossfaketarget, v); - - c = 0; - dir = normalize(end - org); - while (c < 10) // 10 traces - { - setorigin(tracetossent, org); // reset - tracetossent.velocity = findtrajectory_velocity = normalize(dir) * shotspeed + shotspeedupward * '0 0 1'; - tracetoss(tracetossent, ignore); // love builtin functions... - if (trace_ent == tracetossfaketarget) // done - { - targ.solid = savesolid; - - // make it disappear - tracetossfaketarget.solid = SOLID_NOT; - set_movetype(tracetossfaketarget, MOVETYPE_NONE); - tracetossfaketarget.model = ""; - tracetossfaketarget.modelindex = 0; - // relink to remove it from physics considerations - setorigin(tracetossfaketarget, v); - - return true; - } - dir.z = dir.z + 0.1; // aim up a little more - c = c + 1; - } - targ.solid = savesolid; - - // make it disappear - tracetossfaketarget.solid = SOLID_NOT; - set_movetype(tracetossfaketarget, MOVETYPE_NONE); - tracetossfaketarget.model = ""; - tracetossfaketarget.modelindex = 0; - // relink to remove it from physics considerations - setorigin(tracetossfaketarget, v); - - // leave a valid one even if it won't reach - findtrajectory_velocity = normalize(end - org) * shotspeed + shotspeedupward * '0 0 1'; - return false; -} - -void lag_update(entity this) -{ - if (this.lag1_time) if (time > this.lag1_time) {this.lag_func(this, this.lag1_time, this.lag1_float1, this.lag1_float2, this.lag1_entity1, this.lag1_vec1, this.lag1_vec2, this.lag1_vec3, this.lag1_vec4);this.lag1_time = 0;} - if (this.lag2_time) if (time > this.lag2_time) {this.lag_func(this, this.lag2_time, this.lag2_float1, this.lag2_float2, this.lag2_entity1, this.lag2_vec1, this.lag2_vec2, this.lag2_vec3, this.lag2_vec4);this.lag2_time = 0;} - if (this.lag3_time) if (time > this.lag3_time) {this.lag_func(this, this.lag3_time, this.lag3_float1, this.lag3_float2, this.lag3_entity1, this.lag3_vec1, this.lag3_vec2, this.lag3_vec3, this.lag3_vec4);this.lag3_time = 0;} - if (this.lag4_time) if (time > this.lag4_time) {this.lag_func(this, this.lag4_time, this.lag4_float1, this.lag4_float2, this.lag4_entity1, this.lag4_vec1, this.lag4_vec2, this.lag4_vec3, this.lag4_vec4);this.lag4_time = 0;} - if (this.lag5_time) if (time > this.lag5_time) {this.lag_func(this, this.lag5_time, this.lag5_float1, this.lag5_float2, this.lag5_entity1, this.lag5_vec1, this.lag5_vec2, this.lag5_vec3, this.lag5_vec4);this.lag5_time = 0;} -} - -float lag_additem(entity this, float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4) -{ - if (this.lag1_time == 0) {this.lag1_time = t;this.lag1_float1 = f1;this.lag1_float2 = f2;this.lag1_entity1 = e1;this.lag1_vec1 = v1;this.lag1_vec2 = v2;this.lag1_vec3 = v3;this.lag1_vec4 = v4;return true;} - if (this.lag2_time == 0) {this.lag2_time = t;this.lag2_float1 = f1;this.lag2_float2 = f2;this.lag2_entity1 = e1;this.lag2_vec1 = v1;this.lag2_vec2 = v2;this.lag2_vec3 = v3;this.lag2_vec4 = v4;return true;} - if (this.lag3_time == 0) {this.lag3_time = t;this.lag3_float1 = f1;this.lag3_float2 = f2;this.lag3_entity1 = e1;this.lag3_vec1 = v1;this.lag3_vec2 = v2;this.lag3_vec3 = v3;this.lag3_vec4 = v4;return true;} - if (this.lag4_time == 0) {this.lag4_time = t;this.lag4_float1 = f1;this.lag4_float2 = f2;this.lag4_entity1 = e1;this.lag4_vec1 = v1;this.lag4_vec2 = v2;this.lag4_vec3 = v3;this.lag4_vec4 = v4;return true;} - if (this.lag5_time == 0) {this.lag5_time = t;this.lag5_float1 = f1;this.lag5_float2 = f2;this.lag5_entity1 = e1;this.lag5_vec1 = v1;this.lag5_vec2 = v2;this.lag5_vec3 = v3;this.lag5_vec4 = v4;return true;} - // no room for it (what is the best thing to do here??) - return false; -} - -bool bot_shouldattack(entity this, entity targ) -{ - if (targ.team == this.team) - { - if (targ == this) - return false; - if (teamplay) - if (targ.team != 0) - return false; - } - - if(STAT(FROZEN, targ)) - return false; - - if(teamplay) - { - if(targ.team==0) - return false; - } - else if(bot_ignore_bots) - if(IS_BOT_CLIENT(targ)) - return false; - - if (!targ.takedamage) - return false; - if (IS_DEAD(targ)) - return false; - if (PHYS_INPUT_BUTTON_CHAT(targ)) - return false; - if(targ.flags & FL_NOTARGET) - return false; - - if(MUTATOR_CALLHOOK(BotShouldAttack, this, targ)) - return false; - - return true; -} - -void bot_lagfunc(entity this, float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4) -{ - if(this.flags & FL_INWATER) - { - this.bot_aimtarg = NULL; - return; - } - this.bot_aimtarg = e1; - this.bot_aimlatency = this.ping; // FIXME? Shouldn't this be in the lag item? - //this.bot_aimorigin = v1; - //this.bot_aimvelocity = v2; - this.bot_aimtargorigin = v3; - this.bot_aimtargvelocity = v4; - if(skill <= 0) - this.bot_canfire = (random() < 0.8); - else if(skill <= 1) - this.bot_canfire = (random() < 0.9); - else if(skill <= 2) - this.bot_canfire = (random() < 0.95); - else - this.bot_canfire = 1; -} - -float bot_aimdir(entity this, vector v, float maxfiredeviation) -{ - float dist, delta_t, blend; - vector desiredang, diffang; - - //dprint("aim ", this.netname, ": old:", vtos(this.v_angle)); - // make sure v_angle is sane first - this.v_angle_y = this.v_angle.y - floor(this.v_angle.y / 360) * 360; - this.v_angle_z = 0; - - // get the desired angles to aim at - //dprint(" at:", vtos(v)); - v = normalize(v); - //te_lightning2(NULL, this.origin + this.view_ofs, this.origin + this.view_ofs + v * 200); - if (time >= this.bot_badaimtime) - { - this.bot_badaimtime = max(this.bot_badaimtime + 0.3, time); - this.bot_badaimoffset = randomvec() * bound(0, 5 - 0.5 * (skill+this.bot_offsetskill), 5) * autocvar_bot_ai_aimskill_offset; - } - desiredang = vectoangles(v) + this.bot_badaimoffset; - //dprint(" desired:", vtos(desiredang)); - if (desiredang.x >= 180) - desiredang.x = desiredang.x - 360; - desiredang.x = bound(-90, 0 - desiredang.x, 90); - desiredang.z = this.v_angle.z; - //dprint(" / ", vtos(desiredang)); - - //// pain throws off aim - //if (this.bot_painintensity) - //{ - // // shake from pain - // desiredang = desiredang + randomvec() * this.bot_painintensity * 0.2; - //} - - // calculate turn angles - diffang = (desiredang - this.bot_olddesiredang); - // wrap yaw turn - diffang.y = diffang.y - floor(diffang.y / 360) * 360; - if (diffang.y >= 180) - diffang.y = diffang.y - 360; - this.bot_olddesiredang = desiredang; - //dprint(" diff:", vtos(diffang)); - - delta_t = time-this.bot_prevaimtime; - this.bot_prevaimtime = time; - // Here we will try to anticipate the comming aiming direction - this.bot_1st_order_aimfilter= this.bot_1st_order_aimfilter - + (diffang * (1 / delta_t) - this.bot_1st_order_aimfilter) * bound(0, autocvar_bot_ai_aimskill_order_filter_1st,1); - this.bot_2nd_order_aimfilter= this.bot_2nd_order_aimfilter - + (this.bot_1st_order_aimfilter - this.bot_2nd_order_aimfilter) * bound(0, autocvar_bot_ai_aimskill_order_filter_2nd,1); - this.bot_3th_order_aimfilter= this.bot_3th_order_aimfilter - + (this.bot_2nd_order_aimfilter - this.bot_3th_order_aimfilter) * bound(0, autocvar_bot_ai_aimskill_order_filter_3th,1); - this.bot_4th_order_aimfilter= this.bot_4th_order_aimfilter - + (this.bot_3th_order_aimfilter - this.bot_4th_order_aimfilter) * bound(0, autocvar_bot_ai_aimskill_order_filter_4th,1); - this.bot_5th_order_aimfilter= this.bot_5th_order_aimfilter - + (this.bot_4th_order_aimfilter - this.bot_5th_order_aimfilter) * bound(0, autocvar_bot_ai_aimskill_order_filter_5th,1); - - //blend = (bound(0,skill,10)*0.1)*pow(1-bound(0,skill,10)*0.05,2.5)*5.656854249; //Plot formule before changing ! - blend = bound(0,skill+this.bot_aimskill,10)*0.1; - desiredang = desiredang + blend * - ( - this.bot_1st_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_1st - + this.bot_2nd_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_2nd - + this.bot_3th_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_3th - + this.bot_4th_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_4th - + this.bot_5th_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_5th - ); - - // calculate turn angles - diffang = desiredang - this.bot_mouseaim; - // wrap yaw turn - diffang.y = diffang.y - floor(diffang.y / 360) * 360; - if (diffang.y >= 180) - diffang.y = diffang.y - 360; - //dprint(" diff:", vtos(diffang)); - - if (time >= this.bot_aimthinktime) - { - this.bot_aimthinktime = max(this.bot_aimthinktime + 0.5 - 0.05*(skill+this.bot_thinkskill), time); - this.bot_mouseaim = this.bot_mouseaim + diffang * (1-random()*0.1*bound(1,10-(skill+this.bot_thinkskill),10)); - } - - //this.v_angle = this.v_angle + diffang * bound(0, r * frametime * (skill * 0.5 + 2), 1); - - diffang = this.bot_mouseaim - desiredang; - // wrap yaw turn - diffang.y = diffang.y - floor(diffang.y / 360) * 360; - if (diffang.y >= 180) - diffang.y = diffang.y - 360; - desiredang = desiredang + diffang * bound(0,autocvar_bot_ai_aimskill_think,1); - - // calculate turn angles - diffang = desiredang - this.v_angle; - // wrap yaw turn - diffang.y = diffang.y - floor(diffang.y / 360) * 360; - if (diffang.y >= 180) - diffang.y = diffang.y - 360; - //dprint(" diff:", vtos(diffang)); - - // jitter tracking - dist = vlen(diffang); - //diffang = diffang + randomvec() * (dist * 0.05 * (3.5 - bound(0, skill, 3))); - - // turn - float r, fixedrate, blendrate; - fixedrate = autocvar_bot_ai_aimskill_fixedrate / bound(1,dist,1000); - blendrate = autocvar_bot_ai_aimskill_blendrate; - r = max(fixedrate, blendrate); - //this.v_angle = this.v_angle + diffang * bound(frametime, r * frametime * (2+skill*skill*0.05-random()*0.05*(10-skill)), 1); - this.v_angle = this.v_angle + diffang * bound(delta_t, r * delta_t * (2+pow(skill+this.bot_mouseskill,3)*0.005-random()), 1); - this.v_angle = this.v_angle * bound(0,autocvar_bot_ai_aimskill_mouse,1) + desiredang * bound(0,(1-autocvar_bot_ai_aimskill_mouse),1); - //this.v_angle = this.v_angle + diffang * bound(0, r * frametime * (skill * 0.5 + 2), 1); - //this.v_angle = this.v_angle + diffang * (1/ blendrate); - this.v_angle_z = 0; - this.v_angle_y = this.v_angle.y - floor(this.v_angle.y / 360) * 360; - //dprint(" turn:", vtos(this.v_angle)); - - makevectors(this.v_angle); - shotorg = this.origin + this.view_ofs; - shotdir = v_forward; - - //dprint(" dir:", vtos(v_forward)); - //te_lightning2(NULL, shotorg, shotorg + shotdir * 100); - - // calculate turn angles again - //diffang = desiredang - this.v_angle; - //diffang_y = diffang_y - floor(diffang_y / 360) * 360; - //if (diffang_y >= 180) - // diffang_y = diffang_y - 360; - - //dprint("e ", vtos(diffang), " < ", ftos(maxfiredeviation), "\n"); - - // decide whether to fire this time - // note the maxfiredeviation is in degrees so this has to convert to radians first - //if ((normalize(v) * shotdir) >= cos(maxfiredeviation * (3.14159265358979323846 / 180))) - if ((normalize(v) * shotdir) >= cos(maxfiredeviation * (3.14159265358979323846 / 180))) - if(vdist(trace_endpos-shotorg, <, 500 + 500 * bound(0, skill + this.bot_aggresskill, 10)) || random()*random()>bound(0,(skill+this.bot_aggresskill)*0.05,1)) - this.bot_firetimer = time + bound(0.1, 0.5-(skill+this.bot_aggresskill)*0.05, 0.5); - //traceline(shotorg,shotorg+shotdir*1000,false,NULL); - //dprint(ftos(maxfiredeviation),"\n"); - //dprint(" diff:", vtos(diffang), "\n"); - - return this.bot_canfire && (time < this.bot_firetimer); -} - -vector bot_shotlead(vector targorigin, vector targvelocity, float shotspeed, float shotdelay) -{ - // Try to add code here that predicts gravity effect here, no clue HOW to though ... well not yet atleast... - return targorigin + targvelocity * (shotdelay + vlen(targorigin - shotorg) / shotspeed); -} - -bool bot_aim(entity this, float shotspeed, float shotspeedupward, float maxshottime, bool applygravity) -{ - float f, r, hf, distanceratio; - vector v; - /* - eprint(this); - dprint("bot_aim(", ftos(shotspeed)); - dprint(", ", ftos(shotspeedupward)); - dprint(", ", ftos(maxshottime)); - dprint(", ", ftos(applygravity)); - dprint(");\n"); - */ - - hf = this.dphitcontentsmask; - this.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_CORPSE; - - shotspeed *= W_WeaponSpeedFactor(this); - shotspeedupward *= W_WeaponSpeedFactor(this); - if (!shotspeed) - { - LOG_TRACE("bot_aim: WARNING: weapon ", PS(this).m_weapon.m_name, " shotspeed is zero!\n"); - shotspeed = 1000000; - } - if (!maxshottime) - { - LOG_TRACE("bot_aim: WARNING: weapon ", PS(this).m_weapon.m_name, " maxshottime is zero!\n"); - maxshottime = 1; - } - makevectors(this.v_angle); - shotorg = this.origin + this.view_ofs; - shotdir = v_forward; - v = bot_shotlead(this.bot_aimtargorigin, this.bot_aimtargvelocity, shotspeed, this.bot_aimlatency); - distanceratio = sqrt(bound(0,skill,10000))*0.3*(vlen(v-shotorg)-100)/autocvar_bot_ai_aimskill_firetolerance_distdegrees; - distanceratio = bound(0,distanceratio,1); - r = (autocvar_bot_ai_aimskill_firetolerance_maxdegrees-autocvar_bot_ai_aimskill_firetolerance_mindegrees) - * (1-distanceratio) + autocvar_bot_ai_aimskill_firetolerance_mindegrees; - if (applygravity && this.bot_aimtarg) - { - if (!findtrajectorywithleading(shotorg, '0 0 0', '0 0 0', this.bot_aimtarg, shotspeed, shotspeedupward, maxshottime, 0, this)) - { - this.dphitcontentsmask = hf; - return false; - } - - f = bot_aimdir(this, findtrajectory_velocity - shotspeedupward * '0 0 1', r); - } - else - { - f = bot_aimdir(this, v - shotorg, r); - //dprint("AIM: ");dprint(vtos(this.bot_aimtargorigin));dprint(" + ");dprint(vtos(this.bot_aimtargvelocity));dprint(" * ");dprint(ftos(this.bot_aimlatency + vlen(this.bot_aimtargorigin - shotorg) / shotspeed));dprint(" = ");dprint(vtos(v));dprint(" : aimdir = ");dprint(vtos(normalize(v - shotorg)));dprint(" : ");dprint(vtos(shotdir));dprint("\n"); - //traceline(shotorg, shotorg + shotdir * 10000, false, this); - //if (trace_ent.takedamage) - //if (trace_fraction < 1) - //if (!bot_shouldattack(this, trace_ent)) - // return false; - traceline(shotorg, this.bot_aimtargorigin, false, this); - if (trace_fraction < 1) - if (trace_ent != this.enemy) - if (!bot_shouldattack(this, trace_ent)) - { - this.dphitcontentsmask = hf; - return false; - } - } - - //if (r > maxshottime * shotspeed) - // return false; - this.dphitcontentsmask = hf; - return true; -} diff --git a/qcsrc/server/bot/aim.qh b/qcsrc/server/bot/aim.qh deleted file mode 100644 index dfe10e265..000000000 --- a/qcsrc/server/bot/aim.qh +++ /dev/null @@ -1,99 +0,0 @@ -#pragma once -/* - * Globals and Fields - */ - -entity tracetossent; -entity tracetossfaketarget; -vector findtrajectory_velocity; - - - -vector shotorg; -vector shotdir; - -// lag simulation -// upto 5 queued messages -.float lag1_time; -.float lag1_float1; -.float lag1_float2; -.entity lag1_entity1; -.vector lag1_vec1; -.vector lag1_vec2; -.vector lag1_vec3; -.vector lag1_vec4; - -.float lag2_time; -.float lag2_float1; -.float lag2_float2; -.entity lag2_entity1; -.vector lag2_vec1; -.vector lag2_vec2; -.vector lag2_vec3; -.vector lag2_vec4; - -.float lag3_time; -.float lag3_float1; -.float lag3_float2; -.entity lag3_entity1; -.vector lag3_vec1; -.vector lag3_vec2; -.vector lag3_vec3; -.vector lag3_vec4; - -.float lag4_time; -.float lag4_float1; -.float lag4_float2; -.entity lag4_entity1; -.vector lag4_vec1; -.vector lag4_vec2; -.vector lag4_vec3; -.vector lag4_vec4; - -.float lag5_time; -.float lag5_float1; -.float lag5_float2; -.entity lag5_entity1; -.vector lag5_vec1; -.vector lag5_vec2; -.vector lag5_vec3; -.vector lag5_vec4; - -.float bot_badaimtime; -.float bot_aimthinktime; -.float bot_prevaimtime; -.float bot_firetimer; -.float bot_aimlatency; - -.vector bot_mouseaim; -.vector bot_badaimoffset; -.vector bot_1st_order_aimfilter; -.vector bot_2nd_order_aimfilter; -.vector bot_3th_order_aimfilter; -.vector bot_4th_order_aimfilter; -.vector bot_5th_order_aimfilter; -.vector bot_olddesiredang; - -//.vector bot_aimorigin; -//.vector bot_aimvelocity; -.vector bot_aimtargorigin; -.vector bot_aimtargvelocity; - -.entity bot_aimtarg; - -/* - * Functions - */ - -float lag_additem(entity this, float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4); -void lag_update(entity this); -void bot_lagfunc(entity this, float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4); - -float bot_shouldattack(entity this, entity targ); -float bot_aimdir(entity this, vector v, float maxfiredeviation); -bool bot_aim(entity this, float shotspeed, float shotspeedupward, float maxshottime, bool applygravity); -float findtrajectorywithleading(vector org, vector m1, vector m2, entity targ, float shotspeed, float shotspeedupward, float maxtime, float shotdelay, entity ignore); - -vector bot_shotlead(vector targorigin, vector targvelocity, float shotspeed, float shotdelay); - -.void(entity this, float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4) lag_func; diff --git a/qcsrc/server/bot/api.qc b/qcsrc/server/bot/api.qc new file mode 100644 index 000000000..85b0e4656 --- /dev/null +++ b/qcsrc/server/bot/api.qc @@ -0,0 +1,49 @@ +#include "api.qh" + +#if 1 + +#include "default/_mod.inc" +#include "default/havocbot/_mod.inc" + +#else + +bool bot_aim(entity this, float shotspeed, float shotspeedupward, float maxshottime, float applygravity) { return false; } +void bot_clientconnect(entity this) { } +void bot_clientdisconnect(entity this) { } +void bot_cmdhelp(string scmd) { } +void bot_endgame() { } +bool bot_fixcount() { return true; } +void bot_list_commands() { } +void bot_queuecommand(entity bot, string cmdstring) { } +void bot_relinkplayerlist() { } +void bot_resetqueues() { } +void bot_serverframe() { } +bool bot_shouldattack(entity this, entity e) { return false; } +void bot_think(entity this) { } + +entity find_bot_by_name(string name) { return NULL; } +entity find_bot_by_number(float number) { return NULL; } + +void havocbot_goalrating_controlpoints(entity this, float ratingscale, vector org, float sradius) { } +void havocbot_goalrating_enemyplayers(entity this, float ratingscale, vector org, float sradius) { } +void havocbot_goalrating_items(entity this, float ratingscale, vector org, float sradius) { } + +entity navigation_findnearestwaypoint(entity ent, float walkfromwp) { return NULL; } +void navigation_goalrating_end(entity this) { } +void navigation_goalrating_start(entity this) { } +void navigation_markroutes(entity this, entity fixed_source_waypoint) { } +void navigation_markroutes_inverted(entity fixed_source_waypoint) { } +void navigation_routerating(entity this, entity e, float f, float rangebias) { } + +bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float movemode) { return false; } + +void waypoint_remove(entity e) { } +void waypoint_saveall() { } +void waypoint_schedulerelinkall() { } +void waypoint_schedulerelink(entity wp) { } +void waypoint_spawnforitem(entity e) { } +void waypoint_spawnforitem_force(entity e, vector org) { } +void waypoint_spawnforteleporter(entity e, vector destination, float timetaken) { } +void waypoint_spawnforteleporter_v(entity e, vector org, vector destination, float timetaken) { } +entity waypoint_spawn(vector m1, vector m2, float f) { return NULL; } +#endif diff --git a/qcsrc/server/bot/api.qh b/qcsrc/server/bot/api.qh new file mode 100644 index 000000000..9c525892f --- /dev/null +++ b/qcsrc/server/bot/api.qh @@ -0,0 +1,87 @@ +#pragma once + +#include + +const int WAYPOINTFLAG_GENERATED = BIT(23); +const int WAYPOINTFLAG_ITEM = BIT(22); +const int WAYPOINTFLAG_TELEPORT = BIT(21); +const int WAYPOINTFLAG_NORELINK = BIT(20); +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. + +entity kh_worldkeylist; +.entity kh_worldkeynext; + +float bot_custom_weapon; +float bot_weapons_close[Weapons_MAX]; +float bot_weapons_far[Weapons_MAX]; +float bot_weapons_mid[Weapons_MAX]; +float skill; + +.float bot_attack; +.float bot_dodgerating; +.float bot_dodge; +.float bot_forced_team; +.float bot_moveskill; // moving technique +.float bot_pickup; +.float(entity player, entity item) bot_pickupevalfunc; +.float bot_strategytime; +.string cleanname; +.float havocbot_role_timeout; +.float isbot; // true if this client is actually a bot +.float lastteleporttime; +.float navigation_hasgoals; +.float nearestwaypointtimeout; +.entity nearestwaypoint; +.float speed; +.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; +.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 wpconsidered; +.float wpcost; +.int wpflags; + +bool bot_aim(entity this, float shotspeed, float shotspeedupward, float maxshottime, float applygravity); +void bot_clientconnect(entity this); +void bot_clientdisconnect(entity this); +void bot_cmdhelp(string scmd); +void bot_endgame(); +bool bot_fixcount(); +void bot_list_commands(); +void bot_queuecommand(entity bot, string cmdstring); +void bot_relinkplayerlist(); +void bot_resetqueues(); +void bot_serverframe(); +bool bot_shouldattack(entity this, entity e); +void bot_think(entity this); + +entity find_bot_by_name(string name); +entity find_bot_by_number(float number); + +void havocbot_goalrating_controlpoints(entity this, float ratingscale, vector org, float sradius); +void havocbot_goalrating_enemyplayers(entity this, float ratingscale, vector org, float sradius); +void havocbot_goalrating_items(entity this, float ratingscale, vector org, float sradius); + +entity navigation_findnearestwaypoint(entity ent, float walkfromwp); +void navigation_goalrating_end(entity this); +void navigation_goalrating_start(entity this); +void navigation_markroutes(entity this, entity fixed_source_waypoint); +void navigation_markroutes_inverted(entity fixed_source_waypoint); +void navigation_routerating(entity this, entity e, float f, float rangebias); + +bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float movemode); + +void waypoint_remove(entity e); +void waypoint_saveall(); +void waypoint_schedulerelinkall(); +void waypoint_schedulerelink(entity wp); +void waypoint_spawnforitem(entity e); +void waypoint_spawnforitem_force(entity e, vector org); +void waypoint_spawnforteleporter(entity e, vector destination, float timetaken); +void waypoint_spawnforteleporter_v(entity e, vector org, vector destination, float timetaken); +entity waypoint_spawn(vector m1, vector m2, float f); diff --git a/qcsrc/server/bot/bot.qc b/qcsrc/server/bot/bot.qc deleted file mode 100644 index 58feef659..000000000 --- a/qcsrc/server/bot/bot.qc +++ /dev/null @@ -1,725 +0,0 @@ -#include "bot.qh" - -#include "aim.qh" -#include "navigation.qh" -#include "scripting.qh" -#include "waypoints.qh" - -#include "havocbot/havocbot.qh" -#include "havocbot/scripting.qh" - -#include "../teamplay.qh" - -#include "../antilag.qh" -#include "../autocvars.qh" -#include "../campaign.qh" -#include "../cl_client.qh" -#include "../constants.qh" -#include "../defs.qh" -#include "../race.qh" -#include - -#include "../mutators/all.qh" - -#include "../weapons/accuracy.qh" - -#include -#include -#include -#include -#include - -#include - -#include - -#include -#include - -entity bot_spawn() -{ - entity bot = spawnclient(); - if (bot) - { - currentbots = currentbots + 1; - bot_setnameandstuff(bot); - ClientConnect(bot); - PutClientInServer(bot); - } - return bot; -} - -void bot_think(entity this) -{ - if (this.bot_nextthink > time) - return; - - this.flags &= ~FL_GODMODE; - if(autocvar_bot_god) - this.flags |= FL_GODMODE; - - this.bot_nextthink = this.bot_nextthink + autocvar_bot_ai_thinkinterval * pow(0.5, this.bot_aiskill); - //if (this.bot_painintensity > 0) - // this.bot_painintensity = this.bot_painintensity - (skill + 1) * 40 * frametime; - - //this.bot_painintensity = this.bot_painintensity + this.bot_oldhealth - this.health; - //this.bot_painintensity = bound(0, this.bot_painintensity, 100); - - if (!IS_PLAYER(this) || (autocvar_g_campaign && !campaign_bots_may_start)) - { - this.bot_nextthink = time + 0.5; - return; - } - - if (this.fixangle) - { - this.v_angle = this.angles; - this.v_angle_z = 0; - this.fixangle = false; - } - - this.dmg_take = 0; - this.dmg_save = 0; - this.dmg_inflictor = NULL; - - // calculate an aiming latency based on the skill setting - // (simulated network latency + naturally delayed reflexes) - //this.ping = 0.7 - bound(0, 0.05 * skill, 0.5); // moved the reflexes to bot_aimdir (under the name 'think') - // minimum ping 20+10 random - this.ping = bound(0,0.07 - bound(0, (skill + this.bot_pingskill) * 0.005,0.05)+random()*0.01,0.65); // Now holds real lag to server, and higer skill players take a less laggy server - // skill 10 = ping 0.2 (adrenaline) - // skill 0 = ping 0.7 (slightly drunk) - - // clear buttons - PHYS_INPUT_BUTTON_ATCK(this) = false; - PHYS_INPUT_BUTTON_JUMP(this) = false; - PHYS_INPUT_BUTTON_ATCK2(this) = false; - PHYS_INPUT_BUTTON_ZOOM(this) = false; - PHYS_INPUT_BUTTON_CROUCH(this) = false; - PHYS_INPUT_BUTTON_HOOK(this) = false; - PHYS_INPUT_BUTTON_INFO(this) = false; - PHYS_INPUT_BUTTON_DRAG(this) = false; - PHYS_INPUT_BUTTON_CHAT(this) = false; - PHYS_INPUT_BUTTON_USE(this) = false; - - if (time < game_starttime) - { - // block the bot during the countdown to game start - this.movement = '0 0 0'; - this.bot_nextthink = game_starttime; - return; - } - - // if dead, just wait until we can respawn - if (IS_DEAD(this)) - { - if (this.deadflag == DEAD_DEAD) - { - PHYS_INPUT_BUTTON_JUMP(this) = true; // press jump to respawn - this.bot_strategytime = 0; - } - } - else if(this.aistatus & AI_STATUS_STUCK) - navigation_unstuck(this); - - // now call the current bot AI (havocbot for example) - this.bot_ai(this); -} - -void bot_setnameandstuff(entity this) -{ - string readfile, s; - float file, tokens, prio; - - string bot_name, bot_model, bot_skin, bot_shirt, bot_pants; - string name, prefix, suffix; - - if(autocvar_g_campaign) - { - prefix = ""; - suffix = ""; - } - else - { - prefix = autocvar_bot_prefix; - suffix = autocvar_bot_suffix; - } - - file = fopen(autocvar_bot_config_file, FILE_READ); - - if(file < 0) - { - LOG_INFO(strcat("Error: Can not open the bot configuration file '",autocvar_bot_config_file,"'\n")); - readfile = ""; - } - else - { - RandomSelection_Init(); - while((readfile = fgets(file))) - { - if(substring(readfile, 0, 2) == "//") - continue; - if(substring(readfile, 0, 1) == "#") - continue; - tokens = tokenizebyseparator(readfile, "\t"); - if(tokens == 0) - continue; - s = argv(0); - prio = 1; - FOREACH_CLIENT(IS_BOT_CLIENT(it), LAMBDA( - if(s == it.cleanname) - { - prio = 0; - break; - } - )); - RandomSelection_Add(NULL, 0, readfile, 1, prio); - } - readfile = RandomSelection_chosen_string; - fclose(file); - } - - tokens = tokenizebyseparator(readfile, "\t"); - if(argv(0) != "") bot_name = argv(0); - else bot_name = "Bot"; - - if(argv(1) != "") bot_model = argv(1); - else bot_model = ""; - - if(argv(2) != "") bot_skin = argv(2); - else bot_skin = "0"; - - if(argv(3) != "" && stof(argv(3)) >= 0) bot_shirt = argv(3); - else bot_shirt = ftos(floor(random() * 15)); - - if(argv(4) != "" && stof(argv(4)) >= 0) bot_pants = argv(4); - else bot_pants = ftos(floor(random() * 15)); - - this.bot_forced_team = stof(argv(5)); - - prio = 6; - - #define READSKILL(f,w,r) if(argv(prio) != "") this.f = stof(argv(prio)) * (w); else this.f = (!autocvar_g_campaign) * (2 * random() - 1) * (r) * (w); ++prio - //print(bot_name, ": ping=", argv(9), "\n"); - - READSKILL(havocbot_keyboardskill, 0.5, 0.5); // keyboard skill - READSKILL(bot_moveskill, 2, 0); // move skill - READSKILL(bot_dodgeskill, 2, 0); // dodge skill - - READSKILL(bot_pingskill, 0.5, 0); // ping skill - - READSKILL(bot_weaponskill, 2, 0); // weapon skill - READSKILL(bot_aggresskill, 1, 0); // aggre skill - READSKILL(bot_rangepreference, 1, 0); // read skill - - READSKILL(bot_aimskill, 2, 0); // aim skill - READSKILL(bot_offsetskill, 2, 0.5); // offset skill - READSKILL(bot_mouseskill, 1, 0.5); // mouse skill - - READSKILL(bot_thinkskill, 1, 0.5); // think skill - READSKILL(bot_aiskill, 2, 0); // "ai" skill - - this.bot_config_loaded = true; - - // this is really only a default, JoinBestTeam is called later - setcolor(this, stof(bot_shirt) * 16 + stof(bot_pants)); - this.bot_preferredcolors = this.clientcolors; - - // pick the name - if (autocvar_bot_usemodelnames) - name = bot_model; - else - name = bot_name; - - // number bots with identical names - int j = 0; - FOREACH_CLIENT(IS_BOT_CLIENT(it), LAMBDA( - if(it.cleanname == name) - ++j; - )); - if (j) - this.netname = this.netname_freeme = strzone(strcat(prefix, name, "(", ftos(j), ")", suffix)); - else - this.netname = this.netname_freeme = strzone(strcat(prefix, name, suffix)); - - this.cleanname = strzone(name); - - // pick the model and skin - if(substring(bot_model, -4, 1) != ".") - bot_model = strcat(bot_model, ".iqm"); - this.playermodel = this.playermodel_freeme = strzone(strcat("models/player/", bot_model)); - this.playerskin = this.playerskin_freeme = strzone(bot_skin); - - this.cvar_cl_accuracy_data_share = 1; // share the bots weapon accuracy data with the NULL - this.cvar_cl_accuracy_data_receive = 0; // don't receive any weapon accuracy data -} - -void bot_custom_weapon_priority_setup() -{ - float tokens, i, w; - - bot_custom_weapon = false; - - if( autocvar_bot_ai_custom_weapon_priority_far == "" || - autocvar_bot_ai_custom_weapon_priority_mid == "" || - autocvar_bot_ai_custom_weapon_priority_close == "" || - autocvar_bot_ai_custom_weapon_priority_distances == "" - ) - return; - - // Parse distances - tokens = tokenizebyseparator(autocvar_bot_ai_custom_weapon_priority_distances," "); - - if (tokens!=2) - return; - - bot_distance_far = stof(argv(0)); - bot_distance_close = stof(argv(1)); - - if(bot_distance_far < bot_distance_close){ - bot_distance_far = stof(argv(1)); - bot_distance_close = stof(argv(0)); - } - - // Initialize list of weapons - bot_weapons_far[0] = -1; - bot_weapons_mid[0] = -1; - bot_weapons_close[0] = -1; - - // Parse far distance weapon priorities - tokens = tokenizebyseparator(W_NumberWeaponOrder(autocvar_bot_ai_custom_weapon_priority_far)," "); - - int c = 0; - for(i=0; i < tokens && c < Weapons_COUNT; ++i){ - w = stof(argv(i)); - if ( w >= WEP_FIRST && w <= WEP_LAST) { - bot_weapons_far[c] = w; - ++c; - } - } - if(c < Weapons_COUNT) - bot_weapons_far[c] = -1; - - // Parse mid distance weapon priorities - tokens = tokenizebyseparator(W_NumberWeaponOrder(autocvar_bot_ai_custom_weapon_priority_mid)," "); - - c = 0; - for(i=0; i < tokens && c < Weapons_COUNT; ++i){ - w = stof(argv(i)); - if ( w >= WEP_FIRST && w <= WEP_LAST) { - bot_weapons_mid[c] = w; - ++c; - } - } - if(c < Weapons_COUNT) - bot_weapons_mid[c] = -1; - - // Parse close distance weapon priorities - tokens = tokenizebyseparator(W_NumberWeaponOrder(autocvar_bot_ai_custom_weapon_priority_close)," "); - - c = 0; - for(i=0; i < tokens && i < Weapons_COUNT; ++i){ - w = stof(argv(i)); - if ( w >= WEP_FIRST && w <= WEP_LAST) { - bot_weapons_close[c] = w; - ++c; - } - } - if(c < Weapons_COUNT) - bot_weapons_close[c] = -1; - - bot_custom_weapon = true; -} - -void bot_endgame() -{ - entity e; - //dprint("bot_endgame\n"); - e = bot_list; - while (e) - { - setcolor(e, e.bot_preferredcolors); - e = e.nextbot; - } - // if dynamic waypoints are ever implemented, save them here -} - -void bot_relinkplayerlist() -{ - player_count = 0; - currentbots = 0; - bot_list = NULL; - - entity prevbot = NULL; - FOREACH_CLIENT(true, - { - ++player_count; - - if(IS_BOT_CLIENT(it)) - { - if(prevbot) - prevbot.nextbot = it; - else - { - bot_list = it; - bot_list.nextbot = NULL; - } - prevbot = it; - ++currentbots; - } - }); - LOG_TRACE(strcat("relink: ", ftos(currentbots), " bots seen.\n")); - bot_strategytoken = bot_list; - bot_strategytoken_taken = true; -} - -void bot_clientdisconnect(entity this) -{ - if (!IS_BOT_CLIENT(this)) - return; - bot_clearqueue(this); - if(this.cleanname) - strunzone(this.cleanname); - if(this.netname_freeme) - strunzone(this.netname_freeme); - if(this.playermodel_freeme) - strunzone(this.playermodel_freeme); - if(this.playerskin_freeme) - strunzone(this.playerskin_freeme); - this.cleanname = string_null; - this.netname_freeme = string_null; - this.playermodel_freeme = string_null; - this.playerskin_freeme = string_null; - if(this.bot_cmd_current) - delete(this.bot_cmd_current); - if(bot_waypoint_queue_owner==this) - bot_waypoint_queue_owner = NULL; -} - -void bot_clientconnect(entity this) -{ - if (!IS_BOT_CLIENT(this)) return; - this.bot_preferredcolors = this.clientcolors; - this.bot_nextthink = time - random(); - this.lag_func = bot_lagfunc; - this.isbot = true; - this.createdtime = this.bot_nextthink; - - if(!this.bot_config_loaded) // This is needed so team overrider doesn't break between matches - bot_setnameandstuff(this); - - if(this.bot_forced_team==1) - this.team = NUM_TEAM_1; - else if(this.bot_forced_team==2) - this.team = NUM_TEAM_2; - else if(this.bot_forced_team==3) - this.team = NUM_TEAM_3; - else if(this.bot_forced_team==4) - this.team = NUM_TEAM_4; - else - JoinBestTeam(this, false, true); - - havocbot_setupbot(this); -} - -void bot_removefromlargestteam() -{ - CheckAllowedTeams(NULL); - GetTeamCounts(NULL); - - entity best = NULL; - float besttime = 0; - int bestcount = 0; - - int bcount = 0; - FOREACH_ENTITY_FLOAT(isbot, true, - { - ++bcount; - - if(!best) - { - best = it; - besttime = it.createdtime; - } - - int thiscount = 0; - - switch(it.team) - { - case NUM_TEAM_1: thiscount = c1; break; - case NUM_TEAM_2: thiscount = c2; break; - case NUM_TEAM_3: thiscount = c3; break; - case NUM_TEAM_4: thiscount = c4; break; - } - - if(thiscount > bestcount) - { - bestcount = thiscount; - besttime = it.createdtime; - best = it; - } - else if(thiscount == bestcount && besttime < it.createdtime) - { - besttime = it.createdtime; - best = it; - } - }); - if(!bcount) - return; // no bots to remove - currentbots = currentbots - 1; - dropclient(best); -} - -void bot_removenewest() -{ - if(teamplay) - { - bot_removefromlargestteam(); - return; - } - - float besttime = 0; - entity best = NULL; - int bcount = 0; - - FOREACH_ENTITY_FLOAT(isbot, true, - { - ++bcount; - - if(!best) - { - best = it; - besttime = it.createdtime; - } - - if(besttime < it.createdtime) - { - besttime = it.createdtime; - best = it; - } - }); - - if(!bcount) - return; // no bots to remove - - currentbots = currentbots - 1; - dropclient(best); -} - -void autoskill(float factor) -{ - float bestbot; - float bestplayer; - - bestbot = -1; - bestplayer = -1; - FOREACH_CLIENT(IS_PLAYER(it), LAMBDA( - if(IS_REAL_CLIENT(it)) - bestplayer = max(bestplayer, it.totalfrags - it.totalfrags_lastcheck); - else - bestbot = max(bestbot, it.totalfrags - it.totalfrags_lastcheck); - )); - - LOG_TRACE("autoskill: best player got ", ftos(bestplayer), ", "); - LOG_TRACE("best bot got ", ftos(bestbot), "; "); - if(bestbot < 0 || bestplayer < 0) - { - LOG_TRACE("not doing anything\n"); - // don't return, let it reset all counters below - } - else if(bestbot <= bestplayer * factor - 2) - { - if(autocvar_skill < 17) - { - LOG_TRACE("2 frags difference, increasing skill\n"); - cvar_set("skill", ftos(autocvar_skill + 1)); - bprint("^2SKILL UP!^7 Now at level ", ftos(autocvar_skill), "\n"); - } - } - else if(bestbot >= bestplayer * factor + 2) - { - if(autocvar_skill > 0) - { - LOG_TRACE("2 frags difference, decreasing skill\n"); - cvar_set("skill", ftos(autocvar_skill - 1)); - bprint("^1SKILL DOWN!^7 Now at level ", ftos(autocvar_skill), "\n"); - } - } - else - { - LOG_TRACE("not doing anything\n"); - return; - // don't reset counters, wait for them to accumulate - } - - FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(it.totalfrags_lastcheck = it.totalfrags)); -} - -void bot_calculate_stepheightvec() -{ - stepheightvec = autocvar_sv_stepheight * '0 0 1'; - jumpstepheightvec = stepheightvec + - ((autocvar_sv_jumpvelocity * autocvar_sv_jumpvelocity) / (2 * autocvar_sv_gravity)) * '0 0 0.85'; - // 0.75 factor is for safety to make the jumps easy -} - -float bot_fixcount() -{ - int activerealplayers = 0; - int realplayers = 0; - if (MUTATOR_CALLHOOK(Bot_FixCount, activerealplayers, realplayers)) { - activerealplayers = M_ARGV(0, int); - realplayers = M_ARGV(1, int); - } else { - FOREACH_CLIENT(IS_REAL_CLIENT(it), LAMBDA( - if(IS_PLAYER(it)) - ++activerealplayers; - ++realplayers; - )); - } - - int bots; - // add/remove bots if needed to make sure there are at least - // minplayers+bot_number, or remove all bots if no one is playing - // But don't remove bots immediately on level change, as the real players - // usually haven't rejoined yet - bots_would_leave = false; - if (teamplay && autocvar_bot_vs_human && AvailableTeams() == 2) - bots = min(ceil(fabs(autocvar_bot_vs_human) * activerealplayers), maxclients - realplayers); - else if ((realplayers || autocvar_bot_join_empty || (currentbots > 0 && time < 5))) - { - float realminplayers, minplayers; - realminplayers = autocvar_minplayers; - minplayers = max(0, floor(realminplayers)); - - float realminbots, minbots; - realminbots = autocvar_bot_number; - minbots = max(0, floor(realminbots)); - - bots = min(max(minbots, minplayers - activerealplayers), maxclients - realplayers); - if(bots > minbots) - bots_would_leave = true; - } - else - { - // if there are no players, remove bots - bots = 0; - } - - // only add one bot per frame to avoid utter chaos - if(time > botframe_nextthink) - { - //dprint(ftos(bots), " ? ", ftos(currentbots), "\n"); - while (currentbots < bots) - { - if (bot_spawn() == NULL) - { - bprint("Can not add bot, server full.\n"); - return false; - } - } - while (currentbots > bots) - bot_removenewest(); - } - - return true; -} - -void bot_serverframe() -{ - if (intermission_running) - return; - - if (time < 2) - return; - - bot_calculate_stepheightvec(); - bot_navigation_movemode = ((autocvar_bot_navigation_ignoreplayers) ? MOVE_NOMONSTERS : MOVE_NORMAL); - - if(time > autoskill_nextthink) - { - float a; - a = autocvar_skill_auto; - if(a) - autoskill(a); - autoskill_nextthink = time + 5; - } - - if(time > botframe_nextthink) - { - if(!bot_fixcount()) - botframe_nextthink = time + 10; - } - - bot_ignore_bots = autocvar_bot_ignore_bots; - - if(botframe_spawnedwaypoints) - { - if(autocvar_waypoint_benchmark) - localcmd("quit\n"); - } - - if (currentbots > 0 || autocvar_g_waypointeditor || autocvar_g_waypointeditor_auto) - if (botframe_spawnedwaypoints) - { - if(botframe_cachedwaypointlinks) - { - if(!botframe_loadedforcedlinks) - waypoint_load_links_hardwired(); - } - else - { - // TODO: Make this check cleaner - IL_EACH(g_waypoints, time - it.nextthink > 10, - { - waypoint_save_links(); - break; - }); - } - } - else - { - botframe_spawnedwaypoints = true; - waypoint_loadall(); - if(!waypoint_load_links()) - waypoint_schedulerelinkall(); - } - - if (bot_list) - { - // cycle the goal token from one bot to the next each frame - // (this prevents them from all doing spawnfunc_waypoint searches on the same - // frame, which causes choppy framerates) - if (bot_strategytoken_taken) - { - bot_strategytoken_taken = false; - if (bot_strategytoken) - bot_strategytoken = bot_strategytoken.nextbot; - if (!bot_strategytoken) - bot_strategytoken = bot_list; - } - - if (botframe_nextdangertime < time) - { - float interval; - interval = autocvar_bot_ai_dangerdetectioninterval; - if (botframe_nextdangertime < time - interval * 1.5) - botframe_nextdangertime = time; - botframe_nextdangertime = botframe_nextdangertime + interval; - botframe_updatedangerousobjects(autocvar_bot_ai_dangerdetectionupdates); - } - } - - if (autocvar_g_waypointeditor) - botframe_showwaypointlinks(); - - if (autocvar_g_waypointeditor_auto) - botframe_autowaypoints(); - - if(time > bot_cvar_nextthink) - { - if(currentbots>0) - bot_custom_weapon_priority_setup(); - bot_cvar_nextthink = time + 5; - } -} diff --git a/qcsrc/server/bot/bot.qh b/qcsrc/server/bot/bot.qh deleted file mode 100644 index e9dd92668..000000000 --- a/qcsrc/server/bot/bot.qh +++ /dev/null @@ -1,114 +0,0 @@ -#pragma once -/* - * Globals and Fields - */ - -const int AI_STATUS_ROAMING = BIT(0); // Bot is just crawling the map. No enemies at sight -const int AI_STATUS_ATTACKING = BIT(1); // There are enemies at sight -const int AI_STATUS_RUNNING = BIT(2); // Bot is bunny hopping -const int AI_STATUS_DANGER_AHEAD = BIT(3); // There is lava/slime/trigger_hurt ahead -const int AI_STATUS_OUT_JUMPPAD = BIT(4); // Trying to get out of a "vertical" jump pad -const int AI_STATUS_OUT_WATER = BIT(5); // Trying to get out of water -const int AI_STATUS_WAYPOINT_PERSONAL_LINKING = BIT(6); // Waiting for the personal waypoint to be linked -const int AI_STATUS_WAYPOINT_PERSONAL_GOING = BIT(7); // Going to a personal waypoint -const int AI_STATUS_WAYPOINT_PERSONAL_REACHED = BIT(8); // Personal waypoint reached -const int AI_STATUS_JETPACK_FLYING = BIT(9); -const int AI_STATUS_JETPACK_LANDING = BIT(10); -const int AI_STATUS_STUCK = BIT(11); // Cannot reach any goal - -.float isbot; // true if this client is actually a bot -.int aistatus; - -// Skill system -float skill; -float autoskill_nextthink; - -// havocbot_keyboardskill // keyboard movement -.float bot_moveskill; // moving technique -.float bot_dodgeskill; // dodging - -.float bot_pingskill; // ping offset - -.float bot_weaponskill; // weapon usage skill (combos, e.g.) -.float bot_aggresskill; // aggressivity, controls "think before fire" behaviour -.float bot_rangepreference; // weapon choice offset for range (>0 = prefer long range earlier "sniper", <0 = prefer short range "spammer") - -.float bot_aimskill; // aim accuracy -.float bot_offsetskill; // aim breakage -.float bot_mouseskill; // mouse "speed" - -.float bot_thinkskill; // target choice -.float bot_aiskill; // strategy choice - -.float totalfrags_lastcheck; - -// Custom weapon priorities -float bot_custom_weapon; -float bot_distance_far; -float bot_distance_close; - -float bot_weapons_far[Weapons_MAX]; -float bot_weapons_mid[Weapons_MAX]; -float bot_weapons_close[Weapons_MAX]; - -entity bot_list; -.entity nextbot; -.string cleanname; -.string netname_freeme; -.string playermodel_freeme; -.string playerskin_freeme; - -.float bot_nextthink; - -.float createdtime; -.float bot_preferredcolors; -.float bot_attack; -.float bot_dodge; -.float bot_dodgerating; - -.float bot_pickup; -.float bot_pickupbasevalue; -.float bot_canfire; -.float bot_strategytime; - -.float bot_forced_team; -.float bot_config_loaded; - -float bot_strategytoken_taken; -entity bot_strategytoken; - -float botframe_spawnedwaypoints; -float botframe_nextthink; -float botframe_nextdangertime; -float bot_cvar_nextthink; -float bot_ignore_bots; // let bots not attack other bots (only works in non-teamplay) - -/* - * Functions - */ - -entity bot_spawn(); -float bot_fixcount(); - -void bot_think(entity this); -void bot_setnameandstuff(entity this); -void bot_custom_weapon_priority_setup(); -void bot_endgame(); -void bot_relinkplayerlist(); -void bot_clientdisconnect(entity this); -void bot_clientconnect(entity this); -void bot_removefromlargestteam(); -void bot_removenewest(); -void autoskill(float factor); -void bot_serverframe(); - -.void(entity this) bot_ai; -.float(entity player, entity item) bot_pickupevalfunc; - -/* - * Imports - */ - -void(entity this) havocbot_setupbot; - -void bot_calculate_stepheightvec(); diff --git a/qcsrc/server/bot/default/_mod.inc b/qcsrc/server/bot/default/_mod.inc new file mode 100644 index 000000000..ec73502c3 --- /dev/null +++ b/qcsrc/server/bot/default/_mod.inc @@ -0,0 +1,7 @@ +// generated file; do not modify +#include +#include +#include +#include +#include +#include diff --git a/qcsrc/server/bot/default/_mod.qh b/qcsrc/server/bot/default/_mod.qh new file mode 100644 index 000000000..9252f195e --- /dev/null +++ b/qcsrc/server/bot/default/_mod.qh @@ -0,0 +1,7 @@ +// generated file; do not modify +#include +#include +#include +#include +#include +#include diff --git a/qcsrc/server/bot/default/aim.qc b/qcsrc/server/bot/default/aim.qc new file mode 100644 index 000000000..7c87db0ff --- /dev/null +++ b/qcsrc/server/bot/default/aim.qc @@ -0,0 +1,395 @@ +#include "aim.qh" + +#include "cvars.qh" + +#include "bot.qh" + +#include +#include + +#include "../../weapons/weaponsystem.qh" + +#include "../../mutators/all.qh" + +// traces multiple trajectories to find one that will impact the target +// 'end' vector is the place it aims for, +// returns true only if it hit targ (don't target non-solid entities) + +float findtrajectorywithleading(vector org, vector m1, vector m2, entity targ, float shotspeed, float shotspeedupward, float maxtime, float shotdelay, entity ignore) +{ + float c, savesolid, shottime; + vector dir, end, v, o; + if (shotspeed < 1) + return false; // could cause division by zero if calculated + if (targ.solid < SOLID_BBOX) // SOLID_NOT and SOLID_TRIGGER + return false; // could never hit it + if (!tracetossent) + tracetossent = new(tracetossent); + tracetossent.owner = ignore; + setsize(tracetossent, m1, m2); + savesolid = targ.solid; + targ.solid = SOLID_NOT; + o = (targ.absmin + targ.absmax) * 0.5; + shottime = ((vlen(o - org) / shotspeed) + shotdelay); + v = targ.velocity * shottime + o; + tracebox(o, targ.mins, targ.maxs, v, false, targ); + v = trace_endpos; + end = v + (targ.mins + targ.maxs) * 0.5; + if ((vlen(end - org) / shotspeed + 0.2) > maxtime) + { + // out of range + targ.solid = savesolid; + return false; + } + + if (!tracetossfaketarget) + tracetossfaketarget = new(tracetossfaketarget); + tracetossfaketarget.solid = savesolid; + set_movetype(tracetossfaketarget, targ.move_movetype); + _setmodel(tracetossfaketarget, targ.model); // no low precision + tracetossfaketarget.model = targ.model; + tracetossfaketarget.modelindex = targ.modelindex; + setsize(tracetossfaketarget, targ.mins, targ.maxs); + setorigin(tracetossfaketarget, v); + + c = 0; + dir = normalize(end - org); + while (c < 10) // 10 traces + { + setorigin(tracetossent, org); // reset + tracetossent.velocity = findtrajectory_velocity = normalize(dir) * shotspeed + shotspeedupward * '0 0 1'; + tracetoss(tracetossent, ignore); // love builtin functions... + if (trace_ent == tracetossfaketarget) // done + { + targ.solid = savesolid; + + // make it disappear + tracetossfaketarget.solid = SOLID_NOT; + set_movetype(tracetossfaketarget, MOVETYPE_NONE); + tracetossfaketarget.model = ""; + tracetossfaketarget.modelindex = 0; + // relink to remove it from physics considerations + setorigin(tracetossfaketarget, v); + + return true; + } + dir.z = dir.z + 0.1; // aim up a little more + c = c + 1; + } + targ.solid = savesolid; + + // make it disappear + tracetossfaketarget.solid = SOLID_NOT; + set_movetype(tracetossfaketarget, MOVETYPE_NONE); + tracetossfaketarget.model = ""; + tracetossfaketarget.modelindex = 0; + // relink to remove it from physics considerations + setorigin(tracetossfaketarget, v); + + // leave a valid one even if it won't reach + findtrajectory_velocity = normalize(end - org) * shotspeed + shotspeedupward * '0 0 1'; + return false; +} + +void lag_update(entity this) +{ + if (this.lag1_time) if (time > this.lag1_time) {this.lag_func(this, this.lag1_time, this.lag1_float1, this.lag1_float2, this.lag1_entity1, this.lag1_vec1, this.lag1_vec2, this.lag1_vec3, this.lag1_vec4);this.lag1_time = 0;} + if (this.lag2_time) if (time > this.lag2_time) {this.lag_func(this, this.lag2_time, this.lag2_float1, this.lag2_float2, this.lag2_entity1, this.lag2_vec1, this.lag2_vec2, this.lag2_vec3, this.lag2_vec4);this.lag2_time = 0;} + if (this.lag3_time) if (time > this.lag3_time) {this.lag_func(this, this.lag3_time, this.lag3_float1, this.lag3_float2, this.lag3_entity1, this.lag3_vec1, this.lag3_vec2, this.lag3_vec3, this.lag3_vec4);this.lag3_time = 0;} + if (this.lag4_time) if (time > this.lag4_time) {this.lag_func(this, this.lag4_time, this.lag4_float1, this.lag4_float2, this.lag4_entity1, this.lag4_vec1, this.lag4_vec2, this.lag4_vec3, this.lag4_vec4);this.lag4_time = 0;} + if (this.lag5_time) if (time > this.lag5_time) {this.lag_func(this, this.lag5_time, this.lag5_float1, this.lag5_float2, this.lag5_entity1, this.lag5_vec1, this.lag5_vec2, this.lag5_vec3, this.lag5_vec4);this.lag5_time = 0;} +} + +float lag_additem(entity this, float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4) +{ + if (this.lag1_time == 0) {this.lag1_time = t;this.lag1_float1 = f1;this.lag1_float2 = f2;this.lag1_entity1 = e1;this.lag1_vec1 = v1;this.lag1_vec2 = v2;this.lag1_vec3 = v3;this.lag1_vec4 = v4;return true;} + if (this.lag2_time == 0) {this.lag2_time = t;this.lag2_float1 = f1;this.lag2_float2 = f2;this.lag2_entity1 = e1;this.lag2_vec1 = v1;this.lag2_vec2 = v2;this.lag2_vec3 = v3;this.lag2_vec4 = v4;return true;} + if (this.lag3_time == 0) {this.lag3_time = t;this.lag3_float1 = f1;this.lag3_float2 = f2;this.lag3_entity1 = e1;this.lag3_vec1 = v1;this.lag3_vec2 = v2;this.lag3_vec3 = v3;this.lag3_vec4 = v4;return true;} + if (this.lag4_time == 0) {this.lag4_time = t;this.lag4_float1 = f1;this.lag4_float2 = f2;this.lag4_entity1 = e1;this.lag4_vec1 = v1;this.lag4_vec2 = v2;this.lag4_vec3 = v3;this.lag4_vec4 = v4;return true;} + if (this.lag5_time == 0) {this.lag5_time = t;this.lag5_float1 = f1;this.lag5_float2 = f2;this.lag5_entity1 = e1;this.lag5_vec1 = v1;this.lag5_vec2 = v2;this.lag5_vec3 = v3;this.lag5_vec4 = v4;return true;} + // no room for it (what is the best thing to do here??) + return false; +} + +bool bot_shouldattack(entity this, entity targ) +{ + if (targ.team == this.team) + { + if (targ == this) + return false; + if (teamplay) + if (targ.team != 0) + return false; + } + + if(STAT(FROZEN, targ)) + return false; + + if(teamplay) + { + if(targ.team==0) + return false; + } + else if(bot_ignore_bots) + if(IS_BOT_CLIENT(targ)) + return false; + + if (!targ.takedamage) + return false; + if (IS_DEAD(targ)) + return false; + if (PHYS_INPUT_BUTTON_CHAT(targ)) + return false; + if(targ.flags & FL_NOTARGET) + return false; + + if(MUTATOR_CALLHOOK(BotShouldAttack, this, targ)) + return false; + + return true; +} + +void bot_lagfunc(entity this, float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4) +{ + if(this.flags & FL_INWATER) + { + this.bot_aimtarg = NULL; + return; + } + this.bot_aimtarg = e1; + this.bot_aimlatency = this.ping; // FIXME? Shouldn't this be in the lag item? + //this.bot_aimorigin = v1; + //this.bot_aimvelocity = v2; + this.bot_aimtargorigin = v3; + this.bot_aimtargvelocity = v4; + if(skill <= 0) + this.bot_canfire = (random() < 0.8); + else if(skill <= 1) + this.bot_canfire = (random() < 0.9); + else if(skill <= 2) + this.bot_canfire = (random() < 0.95); + else + this.bot_canfire = 1; +} + +float bot_aimdir(entity this, vector v, float maxfiredeviation) +{ + float dist, delta_t, blend; + vector desiredang, diffang; + + //dprint("aim ", this.netname, ": old:", vtos(this.v_angle)); + // make sure v_angle is sane first + this.v_angle_y = this.v_angle.y - floor(this.v_angle.y / 360) * 360; + this.v_angle_z = 0; + + // get the desired angles to aim at + //dprint(" at:", vtos(v)); + v = normalize(v); + //te_lightning2(NULL, this.origin + this.view_ofs, this.origin + this.view_ofs + v * 200); + if (time >= this.bot_badaimtime) + { + this.bot_badaimtime = max(this.bot_badaimtime + 0.3, time); + this.bot_badaimoffset = randomvec() * bound(0, 5 - 0.5 * (skill+this.bot_offsetskill), 5) * autocvar_bot_ai_aimskill_offset; + } + desiredang = vectoangles(v) + this.bot_badaimoffset; + //dprint(" desired:", vtos(desiredang)); + if (desiredang.x >= 180) + desiredang.x = desiredang.x - 360; + desiredang.x = bound(-90, 0 - desiredang.x, 90); + desiredang.z = this.v_angle.z; + //dprint(" / ", vtos(desiredang)); + + //// pain throws off aim + //if (this.bot_painintensity) + //{ + // // shake from pain + // desiredang = desiredang + randomvec() * this.bot_painintensity * 0.2; + //} + + // calculate turn angles + diffang = (desiredang - this.bot_olddesiredang); + // wrap yaw turn + diffang.y = diffang.y - floor(diffang.y / 360) * 360; + if (diffang.y >= 180) + diffang.y = diffang.y - 360; + this.bot_olddesiredang = desiredang; + //dprint(" diff:", vtos(diffang)); + + delta_t = time-this.bot_prevaimtime; + this.bot_prevaimtime = time; + // Here we will try to anticipate the comming aiming direction + this.bot_1st_order_aimfilter= this.bot_1st_order_aimfilter + + (diffang * (1 / delta_t) - this.bot_1st_order_aimfilter) * bound(0, autocvar_bot_ai_aimskill_order_filter_1st,1); + this.bot_2nd_order_aimfilter= this.bot_2nd_order_aimfilter + + (this.bot_1st_order_aimfilter - this.bot_2nd_order_aimfilter) * bound(0, autocvar_bot_ai_aimskill_order_filter_2nd,1); + this.bot_3th_order_aimfilter= this.bot_3th_order_aimfilter + + (this.bot_2nd_order_aimfilter - this.bot_3th_order_aimfilter) * bound(0, autocvar_bot_ai_aimskill_order_filter_3th,1); + this.bot_4th_order_aimfilter= this.bot_4th_order_aimfilter + + (this.bot_3th_order_aimfilter - this.bot_4th_order_aimfilter) * bound(0, autocvar_bot_ai_aimskill_order_filter_4th,1); + this.bot_5th_order_aimfilter= this.bot_5th_order_aimfilter + + (this.bot_4th_order_aimfilter - this.bot_5th_order_aimfilter) * bound(0, autocvar_bot_ai_aimskill_order_filter_5th,1); + + //blend = (bound(0,skill,10)*0.1)*pow(1-bound(0,skill,10)*0.05,2.5)*5.656854249; //Plot formule before changing ! + blend = bound(0,skill+this.bot_aimskill,10)*0.1; + desiredang = desiredang + blend * + ( + this.bot_1st_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_1st + + this.bot_2nd_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_2nd + + this.bot_3th_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_3th + + this.bot_4th_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_4th + + this.bot_5th_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_5th + ); + + // calculate turn angles + diffang = desiredang - this.bot_mouseaim; + // wrap yaw turn + diffang.y = diffang.y - floor(diffang.y / 360) * 360; + if (diffang.y >= 180) + diffang.y = diffang.y - 360; + //dprint(" diff:", vtos(diffang)); + + if (time >= this.bot_aimthinktime) + { + this.bot_aimthinktime = max(this.bot_aimthinktime + 0.5 - 0.05*(skill+this.bot_thinkskill), time); + this.bot_mouseaim = this.bot_mouseaim + diffang * (1-random()*0.1*bound(1,10-(skill+this.bot_thinkskill),10)); + } + + //this.v_angle = this.v_angle + diffang * bound(0, r * frametime * (skill * 0.5 + 2), 1); + + diffang = this.bot_mouseaim - desiredang; + // wrap yaw turn + diffang.y = diffang.y - floor(diffang.y / 360) * 360; + if (diffang.y >= 180) + diffang.y = diffang.y - 360; + desiredang = desiredang + diffang * bound(0,autocvar_bot_ai_aimskill_think,1); + + // calculate turn angles + diffang = desiredang - this.v_angle; + // wrap yaw turn + diffang.y = diffang.y - floor(diffang.y / 360) * 360; + if (diffang.y >= 180) + diffang.y = diffang.y - 360; + //dprint(" diff:", vtos(diffang)); + + // jitter tracking + dist = vlen(diffang); + //diffang = diffang + randomvec() * (dist * 0.05 * (3.5 - bound(0, skill, 3))); + + // turn + float r, fixedrate, blendrate; + fixedrate = autocvar_bot_ai_aimskill_fixedrate / bound(1,dist,1000); + blendrate = autocvar_bot_ai_aimskill_blendrate; + r = max(fixedrate, blendrate); + //this.v_angle = this.v_angle + diffang * bound(frametime, r * frametime * (2+skill*skill*0.05-random()*0.05*(10-skill)), 1); + this.v_angle = this.v_angle + diffang * bound(delta_t, r * delta_t * (2+pow(skill+this.bot_mouseskill,3)*0.005-random()), 1); + this.v_angle = this.v_angle * bound(0,autocvar_bot_ai_aimskill_mouse,1) + desiredang * bound(0,(1-autocvar_bot_ai_aimskill_mouse),1); + //this.v_angle = this.v_angle + diffang * bound(0, r * frametime * (skill * 0.5 + 2), 1); + //this.v_angle = this.v_angle + diffang * (1/ blendrate); + this.v_angle_z = 0; + this.v_angle_y = this.v_angle.y - floor(this.v_angle.y / 360) * 360; + //dprint(" turn:", vtos(this.v_angle)); + + makevectors(this.v_angle); + shotorg = this.origin + this.view_ofs; + shotdir = v_forward; + + //dprint(" dir:", vtos(v_forward)); + //te_lightning2(NULL, shotorg, shotorg + shotdir * 100); + + // calculate turn angles again + //diffang = desiredang - this.v_angle; + //diffang_y = diffang_y - floor(diffang_y / 360) * 360; + //if (diffang_y >= 180) + // diffang_y = diffang_y - 360; + + //dprint("e ", vtos(diffang), " < ", ftos(maxfiredeviation), "\n"); + + // decide whether to fire this time + // note the maxfiredeviation is in degrees so this has to convert to radians first + //if ((normalize(v) * shotdir) >= cos(maxfiredeviation * (3.14159265358979323846 / 180))) + if ((normalize(v) * shotdir) >= cos(maxfiredeviation * (3.14159265358979323846 / 180))) + if(vdist(trace_endpos-shotorg, <, 500 + 500 * bound(0, skill + this.bot_aggresskill, 10)) || random()*random()>bound(0,(skill+this.bot_aggresskill)*0.05,1)) + this.bot_firetimer = time + bound(0.1, 0.5-(skill+this.bot_aggresskill)*0.05, 0.5); + //traceline(shotorg,shotorg+shotdir*1000,false,NULL); + //dprint(ftos(maxfiredeviation),"\n"); + //dprint(" diff:", vtos(diffang), "\n"); + + return this.bot_canfire && (time < this.bot_firetimer); +} + +vector bot_shotlead(vector targorigin, vector targvelocity, float shotspeed, float shotdelay) +{ + // Try to add code here that predicts gravity effect here, no clue HOW to though ... well not yet atleast... + return targorigin + targvelocity * (shotdelay + vlen(targorigin - shotorg) / shotspeed); +} + +bool bot_aim(entity this, float shotspeed, float shotspeedupward, float maxshottime, bool applygravity) +{ + float f, r, hf, distanceratio; + vector v; + /* + eprint(this); + dprint("bot_aim(", ftos(shotspeed)); + dprint(", ", ftos(shotspeedupward)); + dprint(", ", ftos(maxshottime)); + dprint(", ", ftos(applygravity)); + dprint(");\n"); + */ + + hf = this.dphitcontentsmask; + this.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_CORPSE; + + shotspeed *= W_WeaponSpeedFactor(this); + shotspeedupward *= W_WeaponSpeedFactor(this); + if (!shotspeed) + { + LOG_TRACE("bot_aim: WARNING: weapon ", PS(this).m_weapon.m_name, " shotspeed is zero!\n"); + shotspeed = 1000000; + } + if (!maxshottime) + { + LOG_TRACE("bot_aim: WARNING: weapon ", PS(this).m_weapon.m_name, " maxshottime is zero!\n"); + maxshottime = 1; + } + makevectors(this.v_angle); + shotorg = this.origin + this.view_ofs; + shotdir = v_forward; + v = bot_shotlead(this.bot_aimtargorigin, this.bot_aimtargvelocity, shotspeed, this.bot_aimlatency); + distanceratio = sqrt(bound(0,skill,10000))*0.3*(vlen(v-shotorg)-100)/autocvar_bot_ai_aimskill_firetolerance_distdegrees; + distanceratio = bound(0,distanceratio,1); + r = (autocvar_bot_ai_aimskill_firetolerance_maxdegrees-autocvar_bot_ai_aimskill_firetolerance_mindegrees) + * (1-distanceratio) + autocvar_bot_ai_aimskill_firetolerance_mindegrees; + if (applygravity && this.bot_aimtarg) + { + if (!findtrajectorywithleading(shotorg, '0 0 0', '0 0 0', this.bot_aimtarg, shotspeed, shotspeedupward, maxshottime, 0, this)) + { + this.dphitcontentsmask = hf; + return false; + } + + f = bot_aimdir(this, findtrajectory_velocity - shotspeedupward * '0 0 1', r); + } + else + { + f = bot_aimdir(this, v - shotorg, r); + //dprint("AIM: ");dprint(vtos(this.bot_aimtargorigin));dprint(" + ");dprint(vtos(this.bot_aimtargvelocity));dprint(" * ");dprint(ftos(this.bot_aimlatency + vlen(this.bot_aimtargorigin - shotorg) / shotspeed));dprint(" = ");dprint(vtos(v));dprint(" : aimdir = ");dprint(vtos(normalize(v - shotorg)));dprint(" : ");dprint(vtos(shotdir));dprint("\n"); + //traceline(shotorg, shotorg + shotdir * 10000, false, this); + //if (trace_ent.takedamage) + //if (trace_fraction < 1) + //if (!bot_shouldattack(this, trace_ent)) + // return false; + traceline(shotorg, this.bot_aimtargorigin, false, this); + if (trace_fraction < 1) + if (trace_ent != this.enemy) + if (!bot_shouldattack(this, trace_ent)) + { + this.dphitcontentsmask = hf; + return false; + } + } + + //if (r > maxshottime * shotspeed) + // return false; + this.dphitcontentsmask = hf; + return true; +} diff --git a/qcsrc/server/bot/default/aim.qh b/qcsrc/server/bot/default/aim.qh new file mode 100644 index 000000000..dfe10e265 --- /dev/null +++ b/qcsrc/server/bot/default/aim.qh @@ -0,0 +1,99 @@ +#pragma once +/* + * Globals and Fields + */ + +entity tracetossent; +entity tracetossfaketarget; +vector findtrajectory_velocity; + + + +vector shotorg; +vector shotdir; + +// lag simulation +// upto 5 queued messages +.float lag1_time; +.float lag1_float1; +.float lag1_float2; +.entity lag1_entity1; +.vector lag1_vec1; +.vector lag1_vec2; +.vector lag1_vec3; +.vector lag1_vec4; + +.float lag2_time; +.float lag2_float1; +.float lag2_float2; +.entity lag2_entity1; +.vector lag2_vec1; +.vector lag2_vec2; +.vector lag2_vec3; +.vector lag2_vec4; + +.float lag3_time; +.float lag3_float1; +.float lag3_float2; +.entity lag3_entity1; +.vector lag3_vec1; +.vector lag3_vec2; +.vector lag3_vec3; +.vector lag3_vec4; + +.float lag4_time; +.float lag4_float1; +.float lag4_float2; +.entity lag4_entity1; +.vector lag4_vec1; +.vector lag4_vec2; +.vector lag4_vec3; +.vector lag4_vec4; + +.float lag5_time; +.float lag5_float1; +.float lag5_float2; +.entity lag5_entity1; +.vector lag5_vec1; +.vector lag5_vec2; +.vector lag5_vec3; +.vector lag5_vec4; + +.float bot_badaimtime; +.float bot_aimthinktime; +.float bot_prevaimtime; +.float bot_firetimer; +.float bot_aimlatency; + +.vector bot_mouseaim; +.vector bot_badaimoffset; +.vector bot_1st_order_aimfilter; +.vector bot_2nd_order_aimfilter; +.vector bot_3th_order_aimfilter; +.vector bot_4th_order_aimfilter; +.vector bot_5th_order_aimfilter; +.vector bot_olddesiredang; + +//.vector bot_aimorigin; +//.vector bot_aimvelocity; +.vector bot_aimtargorigin; +.vector bot_aimtargvelocity; + +.entity bot_aimtarg; + +/* + * Functions + */ + +float lag_additem(entity this, float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4); +void lag_update(entity this); +void bot_lagfunc(entity this, float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4); + +float bot_shouldattack(entity this, entity targ); +float bot_aimdir(entity this, vector v, float maxfiredeviation); +bool bot_aim(entity this, float shotspeed, float shotspeedupward, float maxshottime, bool applygravity); +float findtrajectorywithleading(vector org, vector m1, vector m2, entity targ, float shotspeed, float shotspeedupward, float maxtime, float shotdelay, entity ignore); + +vector bot_shotlead(vector targorigin, vector targvelocity, float shotspeed, float shotdelay); + +.void(entity this, float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4) lag_func; diff --git a/qcsrc/server/bot/default/bot.qc b/qcsrc/server/bot/default/bot.qc new file mode 100644 index 000000000..753c1867e --- /dev/null +++ b/qcsrc/server/bot/default/bot.qc @@ -0,0 +1,727 @@ +#include "bot.qh" + +#include "cvars.qh" + +#include "aim.qh" +#include "navigation.qh" +#include "scripting.qh" +#include "waypoints.qh" + +#include "havocbot/havocbot.qh" +#include "havocbot/scripting.qh" + +#include "../../teamplay.qh" + +#include "../../antilag.qh" +#include "../../autocvars.qh" +#include "../../campaign.qh" +#include "../../cl_client.qh" +#include "../../constants.qh" +#include "../../defs.qh" +#include "../../race.qh" +#include + +#include "../../mutators/all.qh" + +#include "../../weapons/accuracy.qh" + +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include + +entity bot_spawn() +{ + entity bot = spawnclient(); + if (bot) + { + currentbots = currentbots + 1; + bot_setnameandstuff(bot); + ClientConnect(bot); + PutClientInServer(bot); + } + return bot; +} + +void bot_think(entity this) +{ + if (this.bot_nextthink > time) + return; + + this.flags &= ~FL_GODMODE; + if(autocvar_bot_god) + this.flags |= FL_GODMODE; + + this.bot_nextthink = this.bot_nextthink + autocvar_bot_ai_thinkinterval * pow(0.5, this.bot_aiskill); + //if (this.bot_painintensity > 0) + // this.bot_painintensity = this.bot_painintensity - (skill + 1) * 40 * frametime; + + //this.bot_painintensity = this.bot_painintensity + this.bot_oldhealth - this.health; + //this.bot_painintensity = bound(0, this.bot_painintensity, 100); + + if (!IS_PLAYER(this) || (autocvar_g_campaign && !campaign_bots_may_start)) + { + this.bot_nextthink = time + 0.5; + return; + } + + if (this.fixangle) + { + this.v_angle = this.angles; + this.v_angle_z = 0; + this.fixangle = false; + } + + this.dmg_take = 0; + this.dmg_save = 0; + this.dmg_inflictor = NULL; + + // calculate an aiming latency based on the skill setting + // (simulated network latency + naturally delayed reflexes) + //this.ping = 0.7 - bound(0, 0.05 * skill, 0.5); // moved the reflexes to bot_aimdir (under the name 'think') + // minimum ping 20+10 random + this.ping = bound(0,0.07 - bound(0, (skill + this.bot_pingskill) * 0.005,0.05)+random()*0.01,0.65); // Now holds real lag to server, and higer skill players take a less laggy server + // skill 10 = ping 0.2 (adrenaline) + // skill 0 = ping 0.7 (slightly drunk) + + // clear buttons + PHYS_INPUT_BUTTON_ATCK(this) = false; + PHYS_INPUT_BUTTON_JUMP(this) = false; + PHYS_INPUT_BUTTON_ATCK2(this) = false; + PHYS_INPUT_BUTTON_ZOOM(this) = false; + PHYS_INPUT_BUTTON_CROUCH(this) = false; + PHYS_INPUT_BUTTON_HOOK(this) = false; + PHYS_INPUT_BUTTON_INFO(this) = false; + PHYS_INPUT_BUTTON_DRAG(this) = false; + PHYS_INPUT_BUTTON_CHAT(this) = false; + PHYS_INPUT_BUTTON_USE(this) = false; + + if (time < game_starttime) + { + // block the bot during the countdown to game start + this.movement = '0 0 0'; + this.bot_nextthink = game_starttime; + return; + } + + // if dead, just wait until we can respawn + if (IS_DEAD(this)) + { + if (this.deadflag == DEAD_DEAD) + { + PHYS_INPUT_BUTTON_JUMP(this) = true; // press jump to respawn + this.bot_strategytime = 0; + } + } + else if(this.aistatus & AI_STATUS_STUCK) + navigation_unstuck(this); + + // now call the current bot AI (havocbot for example) + this.bot_ai(this); +} + +void bot_setnameandstuff(entity this) +{ + string readfile, s; + float file, tokens, prio; + + string bot_name, bot_model, bot_skin, bot_shirt, bot_pants; + string name, prefix, suffix; + + if(autocvar_g_campaign) + { + prefix = ""; + suffix = ""; + } + else + { + prefix = autocvar_bot_prefix; + suffix = autocvar_bot_suffix; + } + + file = fopen(autocvar_bot_config_file, FILE_READ); + + if(file < 0) + { + LOG_INFO(strcat("Error: Can not open the bot configuration file '",autocvar_bot_config_file,"'\n")); + readfile = ""; + } + else + { + RandomSelection_Init(); + while((readfile = fgets(file))) + { + if(substring(readfile, 0, 2) == "//") + continue; + if(substring(readfile, 0, 1) == "#") + continue; + tokens = tokenizebyseparator(readfile, "\t"); + if(tokens == 0) + continue; + s = argv(0); + prio = 1; + FOREACH_CLIENT(IS_BOT_CLIENT(it), LAMBDA( + if(s == it.cleanname) + { + prio = 0; + break; + } + )); + RandomSelection_Add(NULL, 0, readfile, 1, prio); + } + readfile = RandomSelection_chosen_string; + fclose(file); + } + + tokens = tokenizebyseparator(readfile, "\t"); + if(argv(0) != "") bot_name = argv(0); + else bot_name = "Bot"; + + if(argv(1) != "") bot_model = argv(1); + else bot_model = ""; + + if(argv(2) != "") bot_skin = argv(2); + else bot_skin = "0"; + + if(argv(3) != "" && stof(argv(3)) >= 0) bot_shirt = argv(3); + else bot_shirt = ftos(floor(random() * 15)); + + if(argv(4) != "" && stof(argv(4)) >= 0) bot_pants = argv(4); + else bot_pants = ftos(floor(random() * 15)); + + this.bot_forced_team = stof(argv(5)); + + prio = 6; + + #define READSKILL(f,w,r) if(argv(prio) != "") this.f = stof(argv(prio)) * (w); else this.f = (!autocvar_g_campaign) * (2 * random() - 1) * (r) * (w); ++prio + //print(bot_name, ": ping=", argv(9), "\n"); + + READSKILL(havocbot_keyboardskill, 0.5, 0.5); // keyboard skill + READSKILL(bot_moveskill, 2, 0); // move skill + READSKILL(bot_dodgeskill, 2, 0); // dodge skill + + READSKILL(bot_pingskill, 0.5, 0); // ping skill + + READSKILL(bot_weaponskill, 2, 0); // weapon skill + READSKILL(bot_aggresskill, 1, 0); // aggre skill + READSKILL(bot_rangepreference, 1, 0); // read skill + + READSKILL(bot_aimskill, 2, 0); // aim skill + READSKILL(bot_offsetskill, 2, 0.5); // offset skill + READSKILL(bot_mouseskill, 1, 0.5); // mouse skill + + READSKILL(bot_thinkskill, 1, 0.5); // think skill + READSKILL(bot_aiskill, 2, 0); // "ai" skill + + this.bot_config_loaded = true; + + // this is really only a default, JoinBestTeam is called later + setcolor(this, stof(bot_shirt) * 16 + stof(bot_pants)); + this.bot_preferredcolors = this.clientcolors; + + // pick the name + if (autocvar_bot_usemodelnames) + name = bot_model; + else + name = bot_name; + + // number bots with identical names + int j = 0; + FOREACH_CLIENT(IS_BOT_CLIENT(it), LAMBDA( + if(it.cleanname == name) + ++j; + )); + if (j) + this.netname = this.netname_freeme = strzone(strcat(prefix, name, "(", ftos(j), ")", suffix)); + else + this.netname = this.netname_freeme = strzone(strcat(prefix, name, suffix)); + + this.cleanname = strzone(name); + + // pick the model and skin + if(substring(bot_model, -4, 1) != ".") + bot_model = strcat(bot_model, ".iqm"); + this.playermodel = this.playermodel_freeme = strzone(strcat("models/player/", bot_model)); + this.playerskin = this.playerskin_freeme = strzone(bot_skin); + + this.cvar_cl_accuracy_data_share = 1; // share the bots weapon accuracy data with the NULL + this.cvar_cl_accuracy_data_receive = 0; // don't receive any weapon accuracy data +} + +void bot_custom_weapon_priority_setup() +{ + float tokens, i, w; + + bot_custom_weapon = false; + + if( autocvar_bot_ai_custom_weapon_priority_far == "" || + autocvar_bot_ai_custom_weapon_priority_mid == "" || + autocvar_bot_ai_custom_weapon_priority_close == "" || + autocvar_bot_ai_custom_weapon_priority_distances == "" + ) + return; + + // Parse distances + tokens = tokenizebyseparator(autocvar_bot_ai_custom_weapon_priority_distances," "); + + if (tokens!=2) + return; + + bot_distance_far = stof(argv(0)); + bot_distance_close = stof(argv(1)); + + if(bot_distance_far < bot_distance_close){ + bot_distance_far = stof(argv(1)); + bot_distance_close = stof(argv(0)); + } + + // Initialize list of weapons + bot_weapons_far[0] = -1; + bot_weapons_mid[0] = -1; + bot_weapons_close[0] = -1; + + // Parse far distance weapon priorities + tokens = tokenizebyseparator(W_NumberWeaponOrder(autocvar_bot_ai_custom_weapon_priority_far)," "); + + int c = 0; + for(i=0; i < tokens && c < Weapons_COUNT; ++i){ + w = stof(argv(i)); + if ( w >= WEP_FIRST && w <= WEP_LAST) { + bot_weapons_far[c] = w; + ++c; + } + } + if(c < Weapons_COUNT) + bot_weapons_far[c] = -1; + + // Parse mid distance weapon priorities + tokens = tokenizebyseparator(W_NumberWeaponOrder(autocvar_bot_ai_custom_weapon_priority_mid)," "); + + c = 0; + for(i=0; i < tokens && c < Weapons_COUNT; ++i){ + w = stof(argv(i)); + if ( w >= WEP_FIRST && w <= WEP_LAST) { + bot_weapons_mid[c] = w; + ++c; + } + } + if(c < Weapons_COUNT) + bot_weapons_mid[c] = -1; + + // Parse close distance weapon priorities + tokens = tokenizebyseparator(W_NumberWeaponOrder(autocvar_bot_ai_custom_weapon_priority_close)," "); + + c = 0; + for(i=0; i < tokens && i < Weapons_COUNT; ++i){ + w = stof(argv(i)); + if ( w >= WEP_FIRST && w <= WEP_LAST) { + bot_weapons_close[c] = w; + ++c; + } + } + if(c < Weapons_COUNT) + bot_weapons_close[c] = -1; + + bot_custom_weapon = true; +} + +void bot_endgame() +{ + entity e; + //dprint("bot_endgame\n"); + e = bot_list; + while (e) + { + setcolor(e, e.bot_preferredcolors); + e = e.nextbot; + } + // if dynamic waypoints are ever implemented, save them here +} + +void bot_relinkplayerlist() +{ + player_count = 0; + currentbots = 0; + bot_list = NULL; + + entity prevbot = NULL; + FOREACH_CLIENT(true, + { + ++player_count; + + if(IS_BOT_CLIENT(it)) + { + if(prevbot) + prevbot.nextbot = it; + else + { + bot_list = it; + bot_list.nextbot = NULL; + } + prevbot = it; + ++currentbots; + } + }); + LOG_TRACE(strcat("relink: ", ftos(currentbots), " bots seen.\n")); + bot_strategytoken = bot_list; + bot_strategytoken_taken = true; +} + +void bot_clientdisconnect(entity this) +{ + if (!IS_BOT_CLIENT(this)) + return; + bot_clearqueue(this); + if(this.cleanname) + strunzone(this.cleanname); + if(this.netname_freeme) + strunzone(this.netname_freeme); + if(this.playermodel_freeme) + strunzone(this.playermodel_freeme); + if(this.playerskin_freeme) + strunzone(this.playerskin_freeme); + this.cleanname = string_null; + this.netname_freeme = string_null; + this.playermodel_freeme = string_null; + this.playerskin_freeme = string_null; + if(this.bot_cmd_current) + delete(this.bot_cmd_current); + if(bot_waypoint_queue_owner==this) + bot_waypoint_queue_owner = NULL; +} + +void bot_clientconnect(entity this) +{ + if (!IS_BOT_CLIENT(this)) return; + this.bot_preferredcolors = this.clientcolors; + this.bot_nextthink = time - random(); + this.lag_func = bot_lagfunc; + this.isbot = true; + this.createdtime = this.bot_nextthink; + + if(!this.bot_config_loaded) // This is needed so team overrider doesn't break between matches + bot_setnameandstuff(this); + + if(this.bot_forced_team==1) + this.team = NUM_TEAM_1; + else if(this.bot_forced_team==2) + this.team = NUM_TEAM_2; + else if(this.bot_forced_team==3) + this.team = NUM_TEAM_3; + else if(this.bot_forced_team==4) + this.team = NUM_TEAM_4; + else + JoinBestTeam(this, false, true); + + havocbot_setupbot(this); +} + +void bot_removefromlargestteam() +{ + CheckAllowedTeams(NULL); + GetTeamCounts(NULL); + + entity best = NULL; + float besttime = 0; + int bestcount = 0; + + int bcount = 0; + FOREACH_ENTITY_FLOAT(isbot, true, + { + ++bcount; + + if(!best) + { + best = it; + besttime = it.createdtime; + } + + int thiscount = 0; + + switch(it.team) + { + case NUM_TEAM_1: thiscount = c1; break; + case NUM_TEAM_2: thiscount = c2; break; + case NUM_TEAM_3: thiscount = c3; break; + case NUM_TEAM_4: thiscount = c4; break; + } + + if(thiscount > bestcount) + { + bestcount = thiscount; + besttime = it.createdtime; + best = it; + } + else if(thiscount == bestcount && besttime < it.createdtime) + { + besttime = it.createdtime; + best = it; + } + }); + if(!bcount) + return; // no bots to remove + currentbots = currentbots - 1; + dropclient(best); +} + +void bot_removenewest() +{ + if(teamplay) + { + bot_removefromlargestteam(); + return; + } + + float besttime = 0; + entity best = NULL; + int bcount = 0; + + FOREACH_ENTITY_FLOAT(isbot, true, + { + ++bcount; + + if(!best) + { + best = it; + besttime = it.createdtime; + } + + if(besttime < it.createdtime) + { + besttime = it.createdtime; + best = it; + } + }); + + if(!bcount) + return; // no bots to remove + + currentbots = currentbots - 1; + dropclient(best); +} + +void autoskill(float factor) +{ + float bestbot; + float bestplayer; + + bestbot = -1; + bestplayer = -1; + FOREACH_CLIENT(IS_PLAYER(it), LAMBDA( + if(IS_REAL_CLIENT(it)) + bestplayer = max(bestplayer, it.totalfrags - it.totalfrags_lastcheck); + else + bestbot = max(bestbot, it.totalfrags - it.totalfrags_lastcheck); + )); + + LOG_TRACE("autoskill: best player got ", ftos(bestplayer), ", "); + LOG_TRACE("best bot got ", ftos(bestbot), "; "); + if(bestbot < 0 || bestplayer < 0) + { + LOG_TRACE("not doing anything\n"); + // don't return, let it reset all counters below + } + else if(bestbot <= bestplayer * factor - 2) + { + if(autocvar_skill < 17) + { + LOG_TRACE("2 frags difference, increasing skill\n"); + cvar_set("skill", ftos(autocvar_skill + 1)); + bprint("^2SKILL UP!^7 Now at level ", ftos(autocvar_skill), "\n"); + } + } + else if(bestbot >= bestplayer * factor + 2) + { + if(autocvar_skill > 0) + { + LOG_TRACE("2 frags difference, decreasing skill\n"); + cvar_set("skill", ftos(autocvar_skill - 1)); + bprint("^1SKILL DOWN!^7 Now at level ", ftos(autocvar_skill), "\n"); + } + } + else + { + LOG_TRACE("not doing anything\n"); + return; + // don't reset counters, wait for them to accumulate + } + + FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(it.totalfrags_lastcheck = it.totalfrags)); +} + +void bot_calculate_stepheightvec() +{ + stepheightvec = autocvar_sv_stepheight * '0 0 1'; + jumpstepheightvec = stepheightvec + + ((autocvar_sv_jumpvelocity * autocvar_sv_jumpvelocity) / (2 * autocvar_sv_gravity)) * '0 0 0.85'; + // 0.75 factor is for safety to make the jumps easy +} + +float bot_fixcount() +{ + int activerealplayers = 0; + int realplayers = 0; + if (MUTATOR_CALLHOOK(Bot_FixCount, activerealplayers, realplayers)) { + activerealplayers = M_ARGV(0, int); + realplayers = M_ARGV(1, int); + } else { + FOREACH_CLIENT(IS_REAL_CLIENT(it), LAMBDA( + if(IS_PLAYER(it)) + ++activerealplayers; + ++realplayers; + )); + } + + int bots; + // add/remove bots if needed to make sure there are at least + // minplayers+bot_number, or remove all bots if no one is playing + // But don't remove bots immediately on level change, as the real players + // usually haven't rejoined yet + bots_would_leave = false; + if (teamplay && autocvar_bot_vs_human && AvailableTeams() == 2) + bots = min(ceil(fabs(autocvar_bot_vs_human) * activerealplayers), maxclients - realplayers); + else if ((realplayers || autocvar_bot_join_empty || (currentbots > 0 && time < 5))) + { + float realminplayers, minplayers; + realminplayers = autocvar_minplayers; + minplayers = max(0, floor(realminplayers)); + + float realminbots, minbots; + realminbots = autocvar_bot_number; + minbots = max(0, floor(realminbots)); + + bots = min(max(minbots, minplayers - activerealplayers), maxclients - realplayers); + if(bots > minbots) + bots_would_leave = true; + } + else + { + // if there are no players, remove bots + bots = 0; + } + + // only add one bot per frame to avoid utter chaos + if(time > botframe_nextthink) + { + //dprint(ftos(bots), " ? ", ftos(currentbots), "\n"); + while (currentbots < bots) + { + if (bot_spawn() == NULL) + { + bprint("Can not add bot, server full.\n"); + return false; + } + } + while (currentbots > bots) + bot_removenewest(); + } + + return true; +} + +void bot_serverframe() +{ + if (intermission_running) + return; + + if (time < 2) + return; + + bot_calculate_stepheightvec(); + bot_navigation_movemode = ((autocvar_bot_navigation_ignoreplayers) ? MOVE_NOMONSTERS : MOVE_NORMAL); + + if(time > autoskill_nextthink) + { + float a; + a = autocvar_skill_auto; + if(a) + autoskill(a); + autoskill_nextthink = time + 5; + } + + if(time > botframe_nextthink) + { + if(!bot_fixcount()) + botframe_nextthink = time + 10; + } + + bot_ignore_bots = autocvar_bot_ignore_bots; + + if(botframe_spawnedwaypoints) + { + if(autocvar_waypoint_benchmark) + localcmd("quit\n"); + } + + if (currentbots > 0 || autocvar_g_waypointeditor || autocvar_g_waypointeditor_auto) + if (botframe_spawnedwaypoints) + { + if(botframe_cachedwaypointlinks) + { + if(!botframe_loadedforcedlinks) + waypoint_load_links_hardwired(); + } + else + { + // TODO: Make this check cleaner + IL_EACH(g_waypoints, time - it.nextthink > 10, + { + waypoint_save_links(); + break; + }); + } + } + else + { + botframe_spawnedwaypoints = true; + waypoint_loadall(); + if(!waypoint_load_links()) + waypoint_schedulerelinkall(); + } + + if (bot_list) + { + // cycle the goal token from one bot to the next each frame + // (this prevents them from all doing spawnfunc_waypoint searches on the same + // frame, which causes choppy framerates) + if (bot_strategytoken_taken) + { + bot_strategytoken_taken = false; + if (bot_strategytoken) + bot_strategytoken = bot_strategytoken.nextbot; + if (!bot_strategytoken) + bot_strategytoken = bot_list; + } + + if (botframe_nextdangertime < time) + { + float interval; + interval = autocvar_bot_ai_dangerdetectioninterval; + if (botframe_nextdangertime < time - interval * 1.5) + botframe_nextdangertime = time; + botframe_nextdangertime = botframe_nextdangertime + interval; + botframe_updatedangerousobjects(autocvar_bot_ai_dangerdetectionupdates); + } + } + + if (autocvar_g_waypointeditor) + botframe_showwaypointlinks(); + + if (autocvar_g_waypointeditor_auto) + botframe_autowaypoints(); + + if(time > bot_cvar_nextthink) + { + if(currentbots>0) + bot_custom_weapon_priority_setup(); + bot_cvar_nextthink = time + 5; + } +} diff --git a/qcsrc/server/bot/default/bot.qh b/qcsrc/server/bot/default/bot.qh new file mode 100644 index 000000000..095930760 --- /dev/null +++ b/qcsrc/server/bot/default/bot.qh @@ -0,0 +1,108 @@ +#pragma once +/* + * Globals and Fields + */ + +const int AI_STATUS_ROAMING = BIT(0); // Bot is just crawling the map. No enemies at sight +const int AI_STATUS_ATTACKING = BIT(1); // There are enemies at sight +const int AI_STATUS_RUNNING = BIT(2); // Bot is bunny hopping +const int AI_STATUS_DANGER_AHEAD = BIT(3); // There is lava/slime/trigger_hurt ahead +const int AI_STATUS_OUT_JUMPPAD = BIT(4); // Trying to get out of a "vertical" jump pad +const int AI_STATUS_OUT_WATER = BIT(5); // Trying to get out of water +const int AI_STATUS_WAYPOINT_PERSONAL_LINKING = BIT(6); // Waiting for the personal waypoint to be linked +const int AI_STATUS_WAYPOINT_PERSONAL_GOING = BIT(7); // Going to a personal waypoint +const int AI_STATUS_WAYPOINT_PERSONAL_REACHED = BIT(8); // Personal waypoint reached +const int AI_STATUS_JETPACK_FLYING = BIT(9); +const int AI_STATUS_JETPACK_LANDING = BIT(10); +const int AI_STATUS_STUCK = BIT(11); // Cannot reach any goal + +.float isbot; // true if this client is actually a bot +.int aistatus; + +// Skill system +float autoskill_nextthink; + +// havocbot_keyboardskill // keyboard movement +.float bot_moveskill; // moving technique +.float bot_dodgeskill; // dodging + +.float bot_pingskill; // ping offset + +.float bot_weaponskill; // weapon usage skill (combos, e.g.) +.float bot_aggresskill; // aggressivity, controls "think before fire" behaviour +.float bot_rangepreference; // weapon choice offset for range (>0 = prefer long range earlier "sniper", <0 = prefer short range "spammer") + +.float bot_aimskill; // aim accuracy +.float bot_offsetskill; // aim breakage +.float bot_mouseskill; // mouse "speed" + +.float bot_thinkskill; // target choice +.float bot_aiskill; // strategy choice + +.float totalfrags_lastcheck; + +// Custom weapon priorities +float bot_distance_far; +float bot_distance_close; + +entity bot_list; +.entity nextbot; +.string cleanname; +.string netname_freeme; +.string playermodel_freeme; +.string playerskin_freeme; + +.float bot_nextthink; + +.float createdtime; +.float bot_preferredcolors; +.float bot_attack; +.float bot_dodge; +.float bot_dodgerating; + +.float bot_pickup; +.float bot_pickupbasevalue; +.float bot_canfire; +.float bot_strategytime; + +.float bot_forced_team; +.float bot_config_loaded; + +float bot_strategytoken_taken; +entity bot_strategytoken; + +float botframe_spawnedwaypoints; +float botframe_nextthink; +float botframe_nextdangertime; +float bot_cvar_nextthink; +float bot_ignore_bots; // let bots not attack other bots (only works in non-teamplay) + +/* + * Functions + */ + +entity bot_spawn(); +float bot_fixcount(); + +void bot_think(entity this); +void bot_setnameandstuff(entity this); +void bot_custom_weapon_priority_setup(); +void bot_endgame(); +void bot_relinkplayerlist(); +void bot_clientdisconnect(entity this); +void bot_clientconnect(entity this); +void bot_removefromlargestteam(); +void bot_removenewest(); +void autoskill(float factor); +void bot_serverframe(); + +.void(entity this) bot_ai; +.float(entity player, entity item) bot_pickupevalfunc; + +/* + * Imports + */ + +void(entity this) havocbot_setupbot; + +void bot_calculate_stepheightvec(); diff --git a/qcsrc/server/bot/default/cvars.qc b/qcsrc/server/bot/default/cvars.qc new file mode 100644 index 000000000..779e7f0cf --- /dev/null +++ b/qcsrc/server/bot/default/cvars.qc @@ -0,0 +1 @@ +#include "cvars.qh" diff --git a/qcsrc/server/bot/default/cvars.qh b/qcsrc/server/bot/default/cvars.qh new file mode 100644 index 000000000..d612c7ab9 --- /dev/null +++ b/qcsrc/server/bot/default/cvars.qh @@ -0,0 +1,58 @@ +#pragma once + +float autocvar_bot_ai_aimskill_blendrate; +float autocvar_bot_ai_aimskill_firetolerance_distdegrees; +float autocvar_bot_ai_aimskill_firetolerance_maxdegrees; +float autocvar_bot_ai_aimskill_firetolerance_mindegrees; +float autocvar_bot_ai_aimskill_fixedrate; +float autocvar_bot_ai_aimskill_mouse; +float autocvar_bot_ai_aimskill_offset; +float autocvar_bot_ai_aimskill_order_filter_1st; +float autocvar_bot_ai_aimskill_order_filter_2nd; +float autocvar_bot_ai_aimskill_order_filter_3th; +float autocvar_bot_ai_aimskill_order_filter_4th; +float autocvar_bot_ai_aimskill_order_filter_5th; +float autocvar_bot_ai_aimskill_order_mix_1st; +float autocvar_bot_ai_aimskill_order_mix_2nd; +float autocvar_bot_ai_aimskill_order_mix_3th; +float autocvar_bot_ai_aimskill_order_mix_4th; +float autocvar_bot_ai_aimskill_order_mix_5th; +float autocvar_bot_ai_aimskill_think; +float autocvar_bot_ai_bunnyhop_firstjumpdelay; +float autocvar_bot_ai_bunnyhop_skilloffset; +float autocvar_bot_ai_bunnyhop_startdistance; +float autocvar_bot_ai_bunnyhop_stopdistance; +float autocvar_bot_ai_chooseweaponinterval; +string autocvar_bot_ai_custom_weapon_priority_close; +string autocvar_bot_ai_custom_weapon_priority_distances; +string autocvar_bot_ai_custom_weapon_priority_far; +string autocvar_bot_ai_custom_weapon_priority_mid; +float autocvar_bot_ai_dangerdetectioninterval; +float autocvar_bot_ai_dangerdetectionupdates; +float autocvar_bot_ai_enemydetectioninterval; +float autocvar_bot_ai_enemydetectionradius; +float autocvar_bot_ai_friends_aware_pickup_radius; +float autocvar_bot_ai_ignoregoal_timeout; +float autocvar_bot_ai_keyboard_distance; +float autocvar_bot_ai_keyboard_threshold; +float autocvar_bot_ai_navigation_jetpack; +float autocvar_bot_ai_navigation_jetpack_mindistance; +float autocvar_bot_ai_thinkinterval; +bool autocvar_bot_ai_weapon_combo; +float autocvar_bot_ai_weapon_combo_threshold; +string autocvar_bot_config_file; +bool autocvar_bot_god; +bool autocvar_bot_ignore_bots; +bool autocvar_bot_join_empty; +bool autocvar_bot_navigation_ignoreplayers; +bool autocvar_bot_nofire; +#define autocvar_bot_prefix cvar_string("bot_prefix") +#define autocvar_bot_suffix cvar_string("bot_suffix") +bool autocvar_bot_usemodelnames; +bool autocvar_bot_debug_tracewalk; +bool autocvar_bot_debug_goalstack; +bool autocvar_bot_wander_enable; +bool autocvar_g_debug_bot_commands; +int autocvar_g_waypointeditor_auto; +float autocvar_skill_auto; +bool autocvar_waypoint_benchmark; diff --git a/qcsrc/server/bot/default/havocbot/_mod.inc b/qcsrc/server/bot/default/havocbot/_mod.inc new file mode 100644 index 000000000..3ecfda161 --- /dev/null +++ b/qcsrc/server/bot/default/havocbot/_mod.inc @@ -0,0 +1,3 @@ +// generated file; do not modify +#include +#include diff --git a/qcsrc/server/bot/default/havocbot/_mod.qh b/qcsrc/server/bot/default/havocbot/_mod.qh new file mode 100644 index 000000000..01fcd46ef --- /dev/null +++ b/qcsrc/server/bot/default/havocbot/_mod.qh @@ -0,0 +1,3 @@ +// generated file; do not modify +#include +#include diff --git a/qcsrc/server/bot/default/havocbot/havocbot.qc b/qcsrc/server/bot/default/havocbot/havocbot.qc new file mode 100644 index 000000000..67b558eed --- /dev/null +++ b/qcsrc/server/bot/default/havocbot/havocbot.qc @@ -0,0 +1,1298 @@ +#include "havocbot.qh" + +#include "../cvars.qh" + +#include "../aim.qh" +#include "../bot.qh" +#include "../navigation.qh" +#include "../scripting.qh" +#include "../waypoints.qh" + +#include +#include +#include +#include + +#include + +#include + +.float speed; + +void havocbot_ai(entity this) +{ + if(this.draggedby) + return; + + if(bot_execute_commands(this)) + return; + + if (bot_strategytoken == this) + if (!bot_strategytoken_taken) + { + if(this.havocbot_blockhead) + { + this.havocbot_blockhead = false; + } + else + { + if (!this.jumppadcount) + this.havocbot_role(this); // little too far down the rabbit hole + } + + // TODO: tracewalk() should take care of this job (better path finding under water) + // if we don't have a goal and we're under water look for a waypoint near the "shore" and push it + if(IS_DEAD(this)) + if(!this.goalcurrent) + if(this.waterlevel == WATERLEVEL_SWIMMING || (this.aistatus & AI_STATUS_OUT_WATER)) + { + // Look for the closest waypoint out of water + entity newgoal = NULL; + IL_EACH(g_waypoints, vdist(it.origin - this.origin, <=, 10000), + { + if(it.origin.z < this.origin.z) + continue; + + if(it.origin.z - this.origin.z - this.view_ofs.z > 100) + continue; + + if (pointcontents(it.origin + it.maxs + '0 0 1') != CONTENT_EMPTY) + continue; + + traceline(this.origin + this.view_ofs, ((it.absmin + it.absmax) * 0.5), true, this); + + if(trace_fraction < 1) + continue; + + if(!newgoal || vlen2(it.origin - this.origin) < vlen2(newgoal.origin - this.origin)) + newgoal = it; + }); + + if(newgoal) + { + // te_wizspike(newgoal.origin); + navigation_pushroute(this, newgoal); + } + } + + // token has been used this frame + bot_strategytoken_taken = true; + } + + if(IS_DEAD(this)) + return; + + havocbot_chooseenemy(this); + if (this.bot_chooseweapontime < time ) + { + this.bot_chooseweapontime = time + autocvar_bot_ai_chooseweaponinterval; + havocbot_chooseweapon(this); + } + havocbot_aim(this); + lag_update(this); + if (this.bot_aimtarg) + { + this.aistatus |= AI_STATUS_ATTACKING; + this.aistatus &= ~AI_STATUS_ROAMING; + + if(this.weapons) + { + Weapon w = PS(this).m_weapon; + w.wr_aim(w, this); + if (autocvar_bot_nofire || IS_INDEPENDENT_PLAYER(this)) + { + PHYS_INPUT_BUTTON_ATCK(this) = false; + PHYS_INPUT_BUTTON_ATCK2(this) = false; + } + else + { + if(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_ATCK2(this)) + this.lastfiredweapon = PS(this).m_weapon.m_id; + } + } + else + { + if(IS_PLAYER(this.bot_aimtarg)) + bot_aimdir(this, this.bot_aimtarg.origin + this.bot_aimtarg.view_ofs - this.origin - this.view_ofs , -1); + } + } + else if (this.goalcurrent) + { + this.aistatus |= AI_STATUS_ROAMING; + this.aistatus &= ~AI_STATUS_ATTACKING; + + vector now,v,next;//,heading; + float aimdistance,skillblend,distanceblend,blend; + next = now = ( (this.goalcurrent.absmin + this.goalcurrent.absmax) * 0.5) - (this.origin + this.view_ofs); + aimdistance = vlen(now); + //heading = this.velocity; + //dprint(this.goalstack01.classname,etos(this.goalstack01),"\n"); + if( + this.goalstack01 != this && this.goalstack01 != NULL && ((this.aistatus & AI_STATUS_RUNNING) == 0) && + !(this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT) + ) + next = ((this.goalstack01.absmin + this.goalstack01.absmax) * 0.5) - (this.origin + this.view_ofs); + + skillblend=bound(0,(skill+this.bot_moveskill-2.5)*0.5,1); //lower skill player can't preturn + distanceblend=bound(0,aimdistance/autocvar_bot_ai_keyboard_distance,1); + blend = skillblend * (1-distanceblend); + //v = (now * (distanceblend) + next * (1-distanceblend)) * (skillblend) + now * (1-skillblend); + //v = now * (distanceblend) * (skillblend) + next * (1-distanceblend) * (skillblend) + now * (1-skillblend); + //v = now * ((1-skillblend) + (distanceblend) * (skillblend)) + next * (1-distanceblend) * (skillblend); + v = now + blend * (next - now); + //dprint(etos(this), " "); + //dprint(vtos(now), ":", vtos(next), "=", vtos(v), " (blend ", ftos(blend), ")\n"); + //v = now * (distanceblend) + next * (1-distanceblend); + if (this.waterlevel < WATERLEVEL_SWIMMING) + v.z = 0; + //dprint("walk at:", vtos(v), "\n"); + //te_lightning2(NULL, this.origin, this.goalcurrent.origin); + bot_aimdir(this, v, -1); + } + havocbot_movetogoal(this); + + // if the bot is not attacking, consider reloading weapons + if (!(this.aistatus & AI_STATUS_ATTACKING)) + { + // we are currently holding a weapon that's not fully loaded, reload it + if(skill >= 2) // bots can only reload the held weapon on purpose past this skill + if(this.clip_load < this.clip_size) + this.impulse = 20; // "press" the reload button, not sure if this is done right + + // if we're not reloading a weapon, switch to any weapon in our invnetory that's not fully loaded to reload it next + // the code above executes next frame, starting the reloading then + if(skill >= 5) // bots can only look for unloaded weapons past this skill + if(this.clip_load >= 0) // only if we're not reloading a weapon already + { + FOREACH(Weapons, it != WEP_Null, LAMBDA( + if((this.weapons & (it.m_wepset)) && (it.spawnflags & WEP_FLAG_RELOADABLE) && (this.weapon_load[it.m_id] < it.reloading_ammo)) + PS(this).m_switchweapon = it; + )); + } + } +} + +void havocbot_keyboard_movement(entity this, vector destorg) +{ + vector keyboard; + float blend, maxspeed; + float sk; + + sk = skill + this.bot_moveskill; + + maxspeed = autocvar_sv_maxspeed; + + if (time < this.havocbot_keyboardtime) + return; + + this.havocbot_keyboardtime = + max( + this.havocbot_keyboardtime + + 0.05/max(1, sk+this.havocbot_keyboardskill) + + random()*0.025/max(0.00025, skill+this.havocbot_keyboardskill) + , time); + keyboard = this.movement * (1.0 / maxspeed); + + float trigger, trigger1; + blend = bound(0,sk*0.1,1); + trigger = autocvar_bot_ai_keyboard_threshold; + trigger1 = 0 - trigger; + + // categorize forward movement + // at skill < 1.5 only forward + // at skill < 2.5 only individual directions + // at skill < 4.5 only individual directions, and forward diagonals + // at skill >= 4.5, all cases allowed + if (keyboard.x > trigger) + { + keyboard.x = 1; + if (sk < 2.5) + keyboard.y = 0; + } + else if (keyboard.x < trigger1 && sk > 1.5) + { + keyboard.x = -1; + if (sk < 4.5) + keyboard.y = 0; + } + else + { + keyboard.x = 0; + if (sk < 1.5) + keyboard.y = 0; + } + if (sk < 4.5) + keyboard.z = 0; + + if (keyboard.y > trigger) + keyboard.y = 1; + else if (keyboard.y < trigger1) + keyboard.y = -1; + else + keyboard.y = 0; + + if (keyboard.z > trigger) + keyboard.z = 1; + else if (keyboard.z < trigger1) + keyboard.z = -1; + else + keyboard.z = 0; + + this.havocbot_keyboard = keyboard * maxspeed; + if (this.havocbot_ducktime>time) PHYS_INPUT_BUTTON_CROUCH(this) = true; + + keyboard = this.havocbot_keyboard; + blend = bound(0,vlen(destorg-this.origin)/autocvar_bot_ai_keyboard_distance,1); // When getting close move with 360 degree + //dprint("movement ", vtos(this.movement), " keyboard ", vtos(keyboard), " blend ", ftos(blend), "\n"); + this.movement = this.movement + (keyboard - this.movement) * blend; +} + +void havocbot_bunnyhop(entity this, vector dir) +{ + float bunnyhopdistance; + vector deviation; + float maxspeed; + vector gco, gno; + + // Don't jump when attacking + if(this.aistatus & AI_STATUS_ATTACKING) + return; + + if(IS_PLAYER(this.goalcurrent)) + return; + + maxspeed = autocvar_sv_maxspeed; + + if(this.aistatus & AI_STATUS_DANGER_AHEAD) + { + this.aistatus &= ~AI_STATUS_RUNNING; + PHYS_INPUT_BUTTON_JUMP(this) = false; + this.bot_canruntogoal = 0; + this.bot_timelastseengoal = 0; + return; + } + + if(this.waterlevel > WATERLEVEL_WETFEET) + { + this.aistatus &= ~AI_STATUS_RUNNING; + return; + } + + if(this.bot_lastseengoal != this.goalcurrent && !(this.aistatus & AI_STATUS_RUNNING)) + { + this.bot_canruntogoal = 0; + this.bot_timelastseengoal = 0; + } + + gco = (this.goalcurrent.absmin + this.goalcurrent.absmax) * 0.5; + bunnyhopdistance = vlen(this.origin - gco); + + // Run only to visible goals + if(IS_ONGROUND(this)) + if(this.speed==maxspeed) + if(checkpvs(this.origin + this.view_ofs, this.goalcurrent)) + { + this.bot_lastseengoal = this.goalcurrent; + + // seen it before + if(this.bot_timelastseengoal) + { + // for a period of time + if(time - this.bot_timelastseengoal > autocvar_bot_ai_bunnyhop_firstjumpdelay) + { + float checkdistance; + checkdistance = true; + + // don't run if it is too close + if(this.bot_canruntogoal==0) + { + if(bunnyhopdistance > autocvar_bot_ai_bunnyhop_startdistance) + this.bot_canruntogoal = 1; + else + this.bot_canruntogoal = -1; + } + + if(this.bot_canruntogoal != 1) + return; + + if(this.aistatus & AI_STATUS_ROAMING) + if(this.goalcurrent.classname=="waypoint") + if (!(this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL)) + if(fabs(gco.z - this.origin.z) < this.maxs.z - this.mins.z) + if(this.goalstack01!=NULL) + { + gno = (this.goalstack01.absmin + this.goalstack01.absmax) * 0.5; + deviation = vectoangles(gno - this.origin) - vectoangles(gco - this.origin); + while (deviation.y < -180) deviation.y = deviation.y + 360; + while (deviation.y > 180) deviation.y = deviation.y - 360; + + if(fabs(deviation.y) < 20) + if(bunnyhopdistance < vlen(this.origin - gno)) + if(fabs(gno.z - gco.z) < this.maxs.z - this.mins.z) + { + if(vdist(gco - gno, >, autocvar_bot_ai_bunnyhop_startdistance)) + if(checkpvs(this.origin + this.view_ofs, this.goalstack01)) + { + checkdistance = false; + } + } + } + + if(checkdistance) + { + this.aistatus &= ~AI_STATUS_RUNNING; + if(bunnyhopdistance > autocvar_bot_ai_bunnyhop_stopdistance) + PHYS_INPUT_BUTTON_JUMP(this) = true; + } + else + { + this.aistatus |= AI_STATUS_RUNNING; + PHYS_INPUT_BUTTON_JUMP(this) = true; + } + } + } + else + { + this.bot_timelastseengoal = time; + } + } + else + { + this.bot_timelastseengoal = 0; + } + +#if 0 + // Release jump button + if(!cvar("sv_pogostick")) + if((IS_ONGROUND(this)) == 0) + { + if(this.velocity.z < 0 || vlen(this.velocity)maxspeed) + { + deviation = vectoangles(dir) - vectoangles(this.velocity); + while (deviation.y < -180) deviation.y = deviation.y + 360; + while (deviation.y > 180) deviation.y = deviation.y - 360; + + if(fabs(deviation.y)>10) + this.movement_x = 0; + + if(deviation.y>10) + this.movement_y = maxspeed * -1; + else if(deviation.y<10) + this.movement_y = maxspeed; + + } + } +#endif +} + +void havocbot_movetogoal(entity this) +{ + vector destorg; + vector diff; + vector dir; + vector flatdir; + vector m1; + vector m2; + vector evadeobstacle; + vector evadelava; + float s; + float maxspeed; + vector gco; + //float dist; + vector dodge; + //if (this.goalentity) + // te_lightning2(this, this.origin, (this.goalentity.absmin + this.goalentity.absmax) * 0.5); + this.movement = '0 0 0'; + maxspeed = autocvar_sv_maxspeed; + + // Jetpack navigation + if(this.goalcurrent) + if(this.navigation_jetpack_goal) + if(this.goalcurrent==this.navigation_jetpack_goal) + if(this.ammo_fuel) + { + if(autocvar_bot_debug_goalstack) + { + debuggoalstack(this); + te_wizspike(this.navigation_jetpack_point); + } + + // Take off + if (!(this.aistatus & AI_STATUS_JETPACK_FLYING)) + { + // Brake almost completely so it can get a good direction + if(vdist(this.velocity, >, 10)) + return; + this.aistatus |= AI_STATUS_JETPACK_FLYING; + } + + makevectors(this.v_angle.y * '0 1 0'); + dir = normalize(this.navigation_jetpack_point - this.origin); + + // Landing + if(this.aistatus & AI_STATUS_JETPACK_LANDING) + { + // Calculate brake distance in xy + float db, v, d; + vector dxy; + + dxy = this.origin - ( ( this.goalcurrent.absmin + this.goalcurrent.absmax ) * 0.5 ); dxy.z = 0; + d = vlen(dxy); + v = vlen(this.velocity - this.velocity.z * '0 0 1'); + db = (pow(v,2) / (autocvar_g_jetpack_acceleration_side * 2)) + 100; + // dprint("distance ", ftos(ceil(d)), " velocity ", ftos(ceil(v)), " brake at ", ftos(ceil(db)), "\n"); + if(d < db || d < 500) + { + // Brake + if(fabs(this.velocity.x)>maxspeed*0.3) + { + this.movement_x = dir * v_forward * -maxspeed; + return; + } + // Switch to normal mode + this.navigation_jetpack_goal = NULL; + this.aistatus &= ~AI_STATUS_JETPACK_LANDING; + this.aistatus &= ~AI_STATUS_JETPACK_FLYING; + return; + } + } + else if(checkpvs(this.origin,this.goalcurrent)) + { + // If I can see the goal switch to landing code + this.aistatus &= ~AI_STATUS_JETPACK_FLYING; + this.aistatus |= AI_STATUS_JETPACK_LANDING; + return; + } + + // Flying + PHYS_INPUT_BUTTON_HOOK(this) = true; + if(this.navigation_jetpack_point.z - STAT(PL_MAX, NULL).z + STAT(PL_MIN, NULL).z < this.origin.z) + { + this.movement_x = dir * v_forward * maxspeed; + this.movement_y = dir * v_right * maxspeed; + } + return; + } + + // Handling of jump pads + if(this.jumppadcount) + { + // If got stuck on the jump pad try to reach the farthest visible waypoint + if(this.aistatus & AI_STATUS_OUT_JUMPPAD) + { + if(fabs(this.velocity.z)<50) + { + entity newgoal = NULL; + IL_EACH(g_waypoints, vdist(it.origin - this.origin, <=, 1000), + { + traceline(this.origin + this.view_ofs, ((it.absmin + it.absmax) * 0.5), true, this); + + if(trace_fraction < 1) + continue; + + if(!newgoal || vlen2(it.origin - this.origin) > vlen2(newgoal.origin - this.origin)) + newgoal = it; + }); + + if(newgoal) + { + this.ignoregoal = this.goalcurrent; + this.ignoregoaltime = time + autocvar_bot_ai_ignoregoal_timeout; + navigation_clearroute(this); + navigation_routetogoal(this, newgoal, this.origin); + this.aistatus &= ~AI_STATUS_OUT_JUMPPAD; + } + } + else + return; + } + else + { + if(this.velocity.z>0) + { + float threshold; + vector velxy = this.velocity; velxy_z = 0; + threshold = maxspeed * 0.2; + if(vdist(velxy, <, threshold)) + { + LOG_TRACE("Warning: ", this.netname, " got stuck on a jumppad (velocity in xy is ", vtos(velxy), "), trying to get out of it now\n"); + this.aistatus |= AI_STATUS_OUT_JUMPPAD; + } + return; + } + + // Don't chase players while using a jump pad + if(IS_PLAYER(this.goalcurrent) || IS_PLAYER(this.goalstack01)) + return; + } + } + else if(this.aistatus & AI_STATUS_OUT_JUMPPAD) + this.aistatus &= ~AI_STATUS_OUT_JUMPPAD; + + // If there is a trigger_hurt right below try to use the jetpack or make a rocketjump + if(skill>6) + if (!(IS_ONGROUND(this))) + { + tracebox(this.origin, this.mins, this.maxs, this.origin + '0 0 -65536', MOVE_NOMONSTERS, this); + if(tracebox_hits_trigger_hurt(this.origin, this.mins, this.maxs, trace_endpos )) + if(this.items & IT_JETPACK) + { + tracebox(this.origin, this.mins, this.maxs, this.origin + '0 0 65536', MOVE_NOMONSTERS, this); + if(tracebox_hits_trigger_hurt(this.origin, this.mins, this.maxs, trace_endpos + '0 0 1' )) + { + if(this.velocity.z<0) + { + PHYS_INPUT_BUTTON_HOOK(this) = true; + } + } + else + PHYS_INPUT_BUTTON_HOOK(this) = true; + + // If there is no goal try to move forward + + if(this.goalcurrent==NULL) + dir = v_forward; + else + dir = normalize(( ( this.goalcurrent.absmin + this.goalcurrent.absmax ) * 0.5 ) - this.origin); + + vector xyvelocity = this.velocity; xyvelocity_z = 0; + float xyspeed = xyvelocity * dir; + + if(xyspeed < (maxspeed / 2)) + { + makevectors(this.v_angle.y * '0 1 0'); + tracebox(this.origin, this.mins, this.maxs, this.origin + (dir * maxspeed * 3), MOVE_NOMONSTERS, this); + if(trace_fraction==1) + { + this.movement_x = dir * v_forward * maxspeed; + this.movement_y = dir * v_right * maxspeed; + if (skill < 10) + havocbot_keyboard_movement(this, this.origin + dir * 100); + } + } + + this.havocbot_blockhead = true; + + return; + } + else if(this.health>WEP_CVAR(devastator, damage)*0.5) + { + if(this.velocity.z < 0) + if(client_hasweapon(this, WEP_DEVASTATOR, true, false)) + { + this.movement_x = maxspeed; + + if(this.rocketjumptime) + { + if(time > this.rocketjumptime) + { + PHYS_INPUT_BUTTON_ATCK2(this) = true; + this.rocketjumptime = 0; + } + return; + } + + PS(this).m_switchweapon = WEP_DEVASTATOR; + this.v_angle_x = 90; + PHYS_INPUT_BUTTON_ATCK(this) = true; + this.rocketjumptime = time + WEP_CVAR(devastator, detonatedelay); + return; + } + } + else + { + // If there is no goal try to move forward + if(this.goalcurrent==NULL) + this.movement_x = maxspeed; + } + } + + // If we are under water with no goals, swim up + if(this.waterlevel) + if(this.goalcurrent==NULL) + { + dir = '0 0 0'; + if(this.waterlevel>WATERLEVEL_SWIMMING) + dir.z = 1; + else if(this.velocity.z >= 0 && !(this.waterlevel == WATERLEVEL_WETFEET && this.watertype == CONTENT_WATER)) + PHYS_INPUT_BUTTON_JUMP(this) = true; + else + PHYS_INPUT_BUTTON_JUMP(this) = false; + makevectors(this.v_angle.y * '0 1 0'); + this.movement_x = dir * v_forward * maxspeed; + this.movement_y = dir * v_right * maxspeed; + this.movement_z = dir * v_up * maxspeed; + } + + // if there is nowhere to go, exit + if (this.goalcurrent == NULL) + return; + + if (this.goalcurrent) + navigation_poptouchedgoals(this); + + // if ran out of goals try to use an alternative goal or get a new strategy asap + if(this.goalcurrent == NULL) + { + this.bot_strategytime = 0; + return; + } + + + if(autocvar_bot_debug_goalstack) + debuggoalstack(this); + + m1 = this.goalcurrent.origin + this.goalcurrent.mins; + m2 = this.goalcurrent.origin + this.goalcurrent.maxs; + destorg = this.origin; + destorg.x = bound(m1_x, destorg.x, m2_x); + destorg.y = bound(m1_y, destorg.y, m2_y); + destorg.z = bound(m1_z, destorg.z, m2_z); + diff = destorg - this.origin; + //dist = vlen(diff); + dir = normalize(diff); + flatdir = diff;flatdir.z = 0; + flatdir = normalize(flatdir); + gco = (this.goalcurrent.absmin + this.goalcurrent.absmax) * 0.5; + + //if (this.bot_dodgevector_time < time) + { + // this.bot_dodgevector_time = time + cvar("bot_ai_dodgeupdateinterval"); + // this.bot_dodgevector_jumpbutton = 1; + evadeobstacle = '0 0 0'; + evadelava = '0 0 0'; + + if (this.waterlevel) + { + if(this.waterlevel>WATERLEVEL_SWIMMING) + { + // flatdir_z = 1; + this.aistatus |= AI_STATUS_OUT_WATER; + } + else + { + if(this.velocity.z >= 0 && !(this.watertype == CONTENT_WATER && gco.z < this.origin.z) && + ( !(this.waterlevel == WATERLEVEL_WETFEET && this.watertype == CONTENT_WATER) || this.aistatus & AI_STATUS_OUT_WATER)) + PHYS_INPUT_BUTTON_JUMP(this) = true; + else + PHYS_INPUT_BUTTON_JUMP(this) = false; + } + dir = normalize(flatdir); + makevectors(this.v_angle.y * '0 1 0'); + } + else + { + if(this.aistatus & AI_STATUS_OUT_WATER) + this.aistatus &= ~AI_STATUS_OUT_WATER; + + // jump if going toward an obstacle that doesn't look like stairs we + // can walk up directly + tracebox(this.origin, this.mins, this.maxs, this.origin + this.velocity * 0.2, false, this); + if (trace_fraction < 1) + if (trace_plane_normal.z < 0.7) + { + s = trace_fraction; + tracebox(this.origin + stepheightvec, this.mins, this.maxs, this.origin + this.velocity * 0.2 + stepheightvec, false, this); + if (trace_fraction < s + 0.01) + if (trace_plane_normal.z < 0.7) + { + s = trace_fraction; + tracebox(this.origin + jumpstepheightvec, this.mins, this.maxs, this.origin + this.velocity * 0.2 + jumpstepheightvec, false, this); + if (trace_fraction > s) + PHYS_INPUT_BUTTON_JUMP(this) = true; + } + } + + // avoiding dangers and obstacles + vector dst_ahead, dst_down; + makevectors(this.v_angle.y * '0 1 0'); + dst_ahead = this.origin + this.view_ofs + (this.velocity * 0.4) + (v_forward * 32 * 3); + dst_down = dst_ahead - '0 0 1500'; + + // Look ahead + traceline(this.origin + this.view_ofs, dst_ahead, true, NULL); + + // Check head-banging against walls + if(vdist(this.origin + this.view_ofs - trace_endpos, <, 25) && !(this.aistatus & AI_STATUS_OUT_WATER)) + { + PHYS_INPUT_BUTTON_JUMP(this) = true; + if(this.facingwalltime && time > this.facingwalltime) + { + this.ignoregoal = this.goalcurrent; + this.ignoregoaltime = time + autocvar_bot_ai_ignoregoal_timeout; + this.bot_strategytime = 0; + return; + } + else + { + this.facingwalltime = time + 0.05; + } + } + else + { + this.facingwalltime = 0; + + if(this.ignoregoal != NULL && time > this.ignoregoaltime) + { + this.ignoregoal = NULL; + this.ignoregoaltime = 0; + } + } + + // Check for water/slime/lava and dangerous edges + // (only when the bot is on the ground or jumping intentionally) + this.aistatus &= ~AI_STATUS_DANGER_AHEAD; + + if(trace_fraction == 1 && this.jumppadcount == 0 && !this.goalcurrent.wphardwired ) + if((IS_ONGROUND(this)) || (this.aistatus & AI_STATUS_RUNNING) || PHYS_INPUT_BUTTON_JUMP(this)) + { + // Look downwards + traceline(dst_ahead , dst_down, true, NULL); + // te_lightning2(NULL, this.origin, dst_ahead); // Draw "ahead" look + // te_lightning2(NULL, dst_ahead, dst_down); // Draw "downwards" look + if(trace_endpos.z < this.origin.z + this.mins.z) + { + s = pointcontents(trace_endpos + '0 0 1'); + if (s != CONTENT_SOLID) + if (s == CONTENT_LAVA || s == CONTENT_SLIME) + evadelava = normalize(this.velocity) * -1; + else if (s == CONTENT_SKY) + evadeobstacle = normalize(this.velocity) * -1; + else if (!boxesoverlap(dst_ahead - this.view_ofs + this.mins, dst_ahead - this.view_ofs + this.maxs, + this.goalcurrent.absmin, this.goalcurrent.absmax)) + { + // if ain't a safe goal with "holes" (like the jumpad on soylent) + // and there is a trigger_hurt below + if(tracebox_hits_trigger_hurt(dst_ahead, this.mins, this.maxs, trace_endpos)) + { + // Remove dangerous dynamic goals from stack + LOG_TRACE("bot ", this.netname, " avoided the goal ", this.goalcurrent.classname, " ", etos(this.goalcurrent), " because it led to a dangerous path; goal stack cleared\n"); + navigation_clearroute(this); + return; + } + } + } + } + + dir = flatdir; + evadeobstacle.z = 0; + evadelava.z = 0; + makevectors(this.v_angle.y * '0 1 0'); + + if(evadeobstacle!='0 0 0'||evadelava!='0 0 0') + this.aistatus |= AI_STATUS_DANGER_AHEAD; + } + + dodge = havocbot_dodge(this); + dodge = dodge * bound(0,0.5+(skill+this.bot_dodgeskill)*0.1,1); + evadelava = evadelava * bound(1,3-(skill+this.bot_dodgeskill),3); //Noobs fear lava a lot and take more distance from it + traceline(this.origin, ( ( this.enemy.absmin + this.enemy.absmax ) * 0.5 ), true, NULL); + if(IS_PLAYER(trace_ent)) + dir = dir * bound(0,(skill+this.bot_dodgeskill)/7,1); + + dir = normalize(dir + dodge + evadeobstacle + evadelava); + // this.bot_dodgevector = dir; + // this.bot_dodgevector_jumpbutton = PHYS_INPUT_BUTTON_JUMP(this); + } + + if(time < this.ladder_time) + { + if(this.goalcurrent.origin.z + this.goalcurrent.mins.z > this.origin.z + this.mins.z) + { + if(this.origin.z + this.mins.z < this.ladder_entity.origin.z + this.ladder_entity.maxs.z) + dir.z = 1; + } + else + { + if(this.origin.z + this.mins.z > this.ladder_entity.origin.z + this.ladder_entity.mins.z) + dir.z = -1; + } + } + + //dir = this.bot_dodgevector; + //if (this.bot_dodgevector_jumpbutton) + // PHYS_INPUT_BUTTON_JUMP(this) = true; + this.movement_x = dir * v_forward * maxspeed; + this.movement_y = dir * v_right * maxspeed; + this.movement_z = dir * v_up * maxspeed; + + // Emulate keyboard interface + if (skill < 10) + havocbot_keyboard_movement(this, destorg); + + // Bunnyhop! +// if(this.aistatus & AI_STATUS_ROAMING) + if(this.goalcurrent) + 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); +} + +void havocbot_chooseenemy(entity this) +{ + entity head, best, head2; + float rating, bestrating, hf; + vector eye, v; + if (autocvar_bot_nofire || IS_INDEPENDENT_PLAYER(this)) + { + this.enemy = NULL; + return; + } + if (this.enemy) + { + if (!bot_shouldattack(this, this.enemy)) + { + // enemy died or something, find a new target + this.enemy = NULL; + this.havocbot_chooseenemy_finished = time; + } + else if (this.havocbot_stickenemy) + { + // tracking last chosen enemy + // if enemy is visible + // and not really really far away + // and we're not severely injured + // then keep tracking for a half second into the future + traceline(this.origin+this.view_ofs, ( this.enemy.absmin + this.enemy.absmax ) * 0.5,false,NULL); + if (trace_ent == this.enemy || trace_fraction == 1) + if (vdist(((this.enemy.absmin + this.enemy.absmax) * 0.5) - this.origin, <, 1000)) + if (this.health > 30) + { + // remain tracking him for a shot while (case he went after a small corner or pilar + this.havocbot_chooseenemy_finished = time + 0.5; + return; + } + // enemy isn't visible, or is far away, or we're injured severely + // so stop preferring this enemy + // (it will still take a half second until a new one is chosen) + this.havocbot_stickenemy = 0; + } + } + if (time < this.havocbot_chooseenemy_finished) + return; + this.havocbot_chooseenemy_finished = time + autocvar_bot_ai_enemydetectioninterval; + eye = this.origin + this.view_ofs; + best = NULL; + bestrating = 100000000; + head = head2 = findchainfloat(bot_attack, true); + + // Backup hit flags + hf = this.dphitcontentsmask; + + // Search for enemies, if no enemy can be seen directly try to look through transparent objects + + this.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_CORPSE; + + bool scan_transparent = false; + bool scan_secondary_targets = false; + bool have_secondary_targets = false; + while(true) + { + scan_secondary_targets = false; +LABEL(scan_targets) + for( ; head; head = head.chain) + { + if(!scan_secondary_targets) + { + if(head.classname == "misc_breakablemodel") + { + have_secondary_targets = true; + continue; + } + } + else + { + if(head.classname != "misc_breakablemodel") + continue; + } + + v = (head.absmin + head.absmax) * 0.5; + rating = vlen(v - eye); + if (rating rating) + if (bot_shouldattack(this, head)) + { + traceline(eye, v, true, this); + if (trace_ent == head || trace_fraction >= 1) + { + best = head; + bestrating = rating; + } + } + } + + if(!best && have_secondary_targets && !scan_secondary_targets) + { + scan_secondary_targets = true; + // restart the loop + head = head2; + bestrating = 100000000; + goto scan_targets; + } + + // I want to do a second scan if no enemy was found or I don't have weapons + // TODO: Perform the scan when using the rifle (requires changes on the rifle code) + if(best || this.weapons) // || this.weapon == WEP_RIFLE.m_id + break; + if(scan_transparent) + break; + + // Set flags to see through transparent objects + this.dphitcontentsmask |= DPCONTENTS_OPAQUE; + + head = head2; + scan_transparent = true; + } + + // Restore hit flags + this.dphitcontentsmask = hf; + + this.enemy = best; + this.havocbot_stickenemy = true; + if(best && best.classname == "misc_breakablemodel") + this.havocbot_stickenemy = false; +} + +float havocbot_chooseweapon_checkreload(entity this, int new_weapon) +{ + // bots under this skill cannot find unloaded weapons to reload idly when not in combat, + // so skip this for them, or they'll never get to reload their weapons at all. + // this also allows bots under this skill to be more stupid, and reload more often during combat :) + if(skill < 5) + return false; + + // if this weapon is scheduled for reloading, don't switch to it during combat + if (this.weapon_load[new_weapon] < 0) + { + bool other_weapon_available = false; + FOREACH(Weapons, it != WEP_Null, LAMBDA( + if(it.wr_checkammo1(it, this) + it.wr_checkammo2(it, this)) + other_weapon_available = true; + )); + if(other_weapon_available) + return true; + } + + return false; +} + +void havocbot_chooseweapon(entity this) +{ + int i; + + // ;) + if(g_weaponarena_weapons == WEPSET(TUBA)) + { + PS(this).m_switchweapon = WEP_TUBA; + return; + } + + // TODO: clean this up by moving it to weapon code + if(this.enemy==NULL) + { + // If no weapon was chosen get the first available weapon + if(PS(this).m_weapon==WEP_Null) + FOREACH(Weapons, it != WEP_Null, LAMBDA( + if(client_hasweapon(this, it, true, false)) + { + PS(this).m_switchweapon = it; + return; + } + )); + return; + } + + // Do not change weapon during the next second after a combo + float f = time - this.lastcombotime; + if(f < 1) + return; + + float w; + float distance; distance=bound(10,vlen(this.origin-this.enemy.origin)-200,10000); + + // Should it do a weapon combo? + float af, ct, combo_time, combo; + + af = ATTACK_FINISHED(this, 0); + ct = autocvar_bot_ai_weapon_combo_threshold; + + // Bots with no skill will be 4 times more slower than "godlike" bots when doing weapon combos + // Ideally this 4 should be calculated as longest_weapon_refire / bot_ai_weapon_combo_threshold + combo_time = time + ct + (ct * ((-0.3*(skill+this.bot_weaponskill))+3)); + + combo = false; + + if(autocvar_bot_ai_weapon_combo) + if(PS(this).m_weapon.m_id == this.lastfiredweapon) + if(af > combo_time) + { + combo = true; + this.lastcombotime = time; + } + + distance *= pow(2, this.bot_rangepreference); + + // Custom weapon list based on distance to the enemy + if(bot_custom_weapon){ + + // Choose weapons for far distance + if ( distance > bot_distance_far ) { + for(i=0; i < Weapons_COUNT && bot_weapons_far[i] != -1 ; ++i){ + w = bot_weapons_far[i]; + if ( client_hasweapon(this, Weapons_from(w), true, false) ) + { + if ((PS(this).m_weapon.m_id == w && combo) || havocbot_chooseweapon_checkreload(this, w)) + continue; + PS(this).m_switchweapon = Weapons_from(w); + return; + } + } + } + + // Choose weapons for mid distance + if ( distance > bot_distance_close) { + for(i=0; i < Weapons_COUNT && bot_weapons_mid[i] != -1 ; ++i){ + w = bot_weapons_mid[i]; + if ( client_hasweapon(this, Weapons_from(w), true, false) ) + { + if ((PS(this).m_weapon.m_id == w && combo) || havocbot_chooseweapon_checkreload(this, w)) + continue; + PS(this).m_switchweapon = Weapons_from(w); + return; + } + } + } + + // Choose weapons for close distance + for(i=0; i < Weapons_COUNT && bot_weapons_close[i] != -1 ; ++i){ + w = bot_weapons_close[i]; + if ( client_hasweapon(this, Weapons_from(w), true, false) ) + { + if ((PS(this).m_weapon.m_id == w && combo) || havocbot_chooseweapon_checkreload(this, w)) + continue; + PS(this).m_switchweapon = Weapons_from(w); + return; + } + } + } +} + +void havocbot_aim(entity this) +{ + vector myvel, enemyvel; +// if(this.flags & FL_INWATER) +// return; + if (time < this.nextaim) + return; + this.nextaim = time + 0.1; + myvel = this.velocity; + if (!this.waterlevel) + myvel.z = 0; + if (this.enemy) + { + enemyvel = this.enemy.velocity; + if (!this.enemy.waterlevel) + enemyvel.z = 0; + lag_additem(this, time + this.ping, 0, 0, this.enemy, this.origin, myvel, (this.enemy.absmin + this.enemy.absmax) * 0.5, enemyvel); + } + else + lag_additem(this, time + this.ping, 0, 0, NULL, this.origin, myvel, ( this.goalcurrent.absmin + this.goalcurrent.absmax ) * 0.5, '0 0 0'); +} + +bool havocbot_moveto_refresh_route(entity this) +{ + // Refresh path to goal if necessary + entity wp; + wp = this.havocbot_personal_waypoint; + navigation_goalrating_start(this); + navigation_routerating(this, wp, 10000, 10000); + navigation_goalrating_end(this); + return this.navigation_hasgoals; +} + +float havocbot_moveto(entity this, vector pos) +{ + entity wp; + + if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) + { + // Step 4: Move to waypoint + if(this.havocbot_personal_waypoint==NULL) + { + LOG_TRACE("Error: ", this.netname, " trying to walk to a non existent personal waypoint\n"); + this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING; + return CMD_STATUS_ERROR; + } + + if (!bot_strategytoken_taken) + if(this.havocbot_personal_waypoint_searchtime= 30) + { + LOG_TRACE("Warning: can't walk to the personal waypoint located at ", vtos(this.havocbot_personal_waypoint.origin),"\n"); + this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_LINKING; + delete(this.havocbot_personal_waypoint); + return CMD_STATUS_ERROR; + } + else + LOG_TRACE(this.netname, " can't walk to its personal waypoint (after ", ftos(this.havocbot_personal_waypoint_failcounter), " failed attempts), trying later\n"); + } + } + + if(autocvar_bot_debug_goalstack) + debuggoalstack(this); + + // Heading + vector dir = ( ( this.goalcurrent.absmin + this.goalcurrent.absmax ) * 0.5 ) - (this.origin + this.view_ofs); + dir.z = 0; + bot_aimdir(this, dir, -1); + + // Go! + havocbot_movetogoal(this); + + if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_REACHED) + { + // Step 5: Waypoint reached + LOG_TRACE(this.netname, "'s personal waypoint reached\n"); + delete(this.havocbot_personal_waypoint); + this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_REACHED; + return CMD_STATUS_FINISHED; + } + + return CMD_STATUS_EXECUTING; + } + + // Step 2: Linking waypoint + if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_LINKING) + { + // Wait until it is linked + if(!this.havocbot_personal_waypoint.wplinked) + { + LOG_TRACE(this.netname, " waiting for personal waypoint to be linked\n"); + return CMD_STATUS_EXECUTING; + } + + this.havocbot_personal_waypoint_searchtime = time; // so we set the route next frame + this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_LINKING; + this.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_GOING; + + // Step 3: Route to waypoint + LOG_TRACE(this.netname, " walking to its personal waypoint\n"); + + return CMD_STATUS_EXECUTING; + } + + // Step 1: Spawning waypoint + wp = waypoint_spawnpersonal(this, pos); + if(wp==NULL) + { + LOG_TRACE("Error: Can't spawn personal waypoint at ",vtos(pos),"\n"); + return CMD_STATUS_ERROR; + } + + this.havocbot_personal_waypoint = wp; + this.havocbot_personal_waypoint_failcounter = 0; + this.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_LINKING; + + // if pos is inside a teleport, then let's mark it as teleport waypoint + FOREACH_ENTITY_CLASS("trigger_teleport", WarpZoneLib_BoxTouchesBrush(pos, pos, it, NULL), + { + wp.wpflags |= WAYPOINTFLAG_TELEPORT; + this.lastteleporttime = 0; + }); + +/* + if(wp.wpflags & WAYPOINTFLAG_TELEPORT) + print("routing to a teleporter\n"); + else + print("routing to a non-teleporter\n"); +*/ + + return CMD_STATUS_EXECUTING; +} + +float havocbot_resetgoal(entity this) +{ + navigation_clearroute(this); + return CMD_STATUS_FINISHED; +} + +void havocbot_setupbot(entity this) +{ + this.bot_ai = havocbot_ai; + this.cmd_moveto = havocbot_moveto; + this.cmd_resetgoal = havocbot_resetgoal; + + havocbot_chooserole(this); +} + +vector havocbot_dodge(entity this) +{ + // LordHavoc: disabled because this is too expensive + return '0 0 0'; +#if 0 + entity head; + vector dodge, v, n; + float danger, bestdanger, vl, d; + dodge = '0 0 0'; + bestdanger = -20; + // check for dangerous objects near bot or approaching bot + head = findchainfloat(bot_dodge, true); + while(head) + { + if (head.owner != this) + { + vl = vlen(head.velocity); + if (vl > autocvar_sv_maxspeed * 0.3) + { + n = normalize(head.velocity); + v = this.origin - head.origin; + d = v * n; + if (d > (0 - head.bot_dodgerating)) + if (d < (vl * 0.2 + head.bot_dodgerating)) + { + // calculate direction and distance from the flight path, by removing the forward axis + v = v - (n * (v * n)); + danger = head.bot_dodgerating - vlen(v); + if (bestdanger < danger) + { + bestdanger = danger; + // dodge to the side of the object + dodge = normalize(v); + } + } + } + else + { + danger = head.bot_dodgerating - vlen(head.origin - this.origin); + if (bestdanger < danger) + { + bestdanger = danger; + dodge = normalize(this.origin - head.origin); + } + } + } + head = head.chain; + } + return dodge; +#endif +} diff --git a/qcsrc/server/bot/default/havocbot/havocbot.qh b/qcsrc/server/bot/default/havocbot/havocbot.qh new file mode 100644 index 000000000..4a391b6e7 --- /dev/null +++ b/qcsrc/server/bot/default/havocbot/havocbot.qh @@ -0,0 +1,64 @@ +#pragma once + +/* + * Globals and Fields + */ + +.float havocbot_keyboardskill; +.float facingwalltime, ignoregoaltime; +.float lastfiredweapon; +.float lastcombotime; +.float havocbot_blockhead; + +.float havocbot_keyboardtime; +.float havocbot_ducktime; +.float bot_timelastseengoal; +.float bot_canruntogoal; +.float bot_chooseweapontime; +.float rocketjumptime; +.float nextaim; +.float havocbot_personal_waypoint_searchtime; +.float havocbot_personal_waypoint_failcounter; +.float havocbot_chooseenemy_finished; +.float havocbot_stickenemy; +.float havocbot_role_timeout; + +.entity ignoregoal; +.entity bot_lastseengoal; +.entity havocbot_personal_waypoint; + +.vector havocbot_keyboard; + +/* + * Functions + */ + +void havocbot_ai(entity this); +void havocbot_aim(entity this); +void havocbot_setupbot(entity this); +void havocbot_movetogoal(entity this); +void havocbot_chooserole(entity this); +void havocbot_chooseenemy(entity this); +void havocbot_chooseweapon(entity this); +void havocbot_bunnyhop(entity this, vector dir); +void havocbot_keyboard_movement(entity this, vector destorg); + +float havocbot_resetgoal(entity this); +float havocbot_moveto(entity this, vector pos); +float havocbot_moveto_refresh_route(entity this); + +vector havocbot_dodge(entity this); + +.void(entity this) havocbot_role; +.void(entity this) havocbot_previous_role; + +void(entity this, float ratingscale, vector org, float sradius) havocbot_goalrating_items; +void(entity this, float ratingscale, vector org, float sradius) havocbot_goalrating_enemyplayers; + +/* + * Imports + */ + +.entity draggedby; +.float ladder_time; +.entity ladder_entity; diff --git a/qcsrc/server/bot/default/havocbot/roles.qc b/qcsrc/server/bot/default/havocbot/roles.qc new file mode 100644 index 000000000..fffe6e4b9 --- /dev/null +++ b/qcsrc/server/bot/default/havocbot/roles.qc @@ -0,0 +1,210 @@ +#include "roles.qh" + +#include "havocbot.qh" + +#include "../cvars.qh" + +#include "../bot.qh" +#include "../navigation.qh" + +.float max_armorvalue; +.float havocbot_role_timeout; + +.void(entity this) havocbot_previous_role; +.void(entity this) havocbot_role; + +void havocbot_goalrating_items(entity this, float ratingscale, vector org, float sradius) +{ + float rating, d, discard, friend_distance, enemy_distance; + vector o; + ratingscale = ratingscale * 0.0001; // items are rated around 10000 already + + FOREACH_ENTITY_FLOAT(bot_pickup, true, + { + o = (it.absmin + it.absmax) * 0.5; + friend_distance = 10000; enemy_distance = 10000; + rating = 0; + + if(!it.solid || vdist(o - org, >, sradius) || (it == this.ignoregoal && time < this.ignoregoaltime) ) + continue; + + // Check if the item can be picked up safely + if(it.classname == "droppedweapon") + { + traceline(o, o + '0 0 -1500', true, NULL); + + d = pointcontents(trace_endpos + '0 0 1'); + if(d & CONTENT_WATER || d & CONTENT_SLIME || d & CONTENT_LAVA) + continue; + if(tracebox_hits_trigger_hurt(it.origin, it.mins, it.maxs, trace_endpos)) + continue; + } + else + { + // Ignore items under water + traceline(it.origin + it.maxs, it.origin + it.maxs, MOVE_NORMAL, it); + if(trace_dpstartcontents & DPCONTENTS_LIQUIDSMASK) + continue; + } + + if(teamplay) + { + discard = false; + + entity picker = it; + FOREACH_CLIENT(IS_PLAYER(it) && it != this && !IS_DEAD(it), + { + d = vlen(it.origin - o); // distance between player and item + + if ( it.team == this.team ) + { + if ( !IS_REAL_CLIENT(it) || discard ) + continue; + + if( d > friend_distance) + continue; + + friend_distance = d; + + discard = true; + + if( picker.health && it.health > this.health ) + continue; + + if( picker.armorvalue && it.armorvalue > this.armorvalue) + continue; + + if( picker.weapons ) + if( picker.weapons & ~it.weapons ) + continue; + + if (picker.ammo_shells && it.ammo_shells > this.ammo_shells) + continue; + + if (picker.ammo_nails && it.ammo_nails > this.ammo_nails) + continue; + + if (picker.ammo_rockets && it.ammo_rockets > this.ammo_rockets) + continue; + + if (picker.ammo_cells && it.ammo_cells > this.ammo_cells) + continue; + + if (picker.ammo_plasma && it.ammo_plasma > this.ammo_plasma) + continue; + + discard = false; + } + else + { + // If enemy only track distances + // TODO: track only if visible ? + if( d < enemy_distance ) + enemy_distance = d; + } + }); + + // Rate the item only if no one needs it, or if an enemy is closer to it + if ( (enemy_distance < friend_distance && vdist(o - org, <, enemy_distance)) || + (friend_distance > autocvar_bot_ai_friends_aware_pickup_radius ) || !discard ) + rating = it.bot_pickupevalfunc(this, it); + + } + else + rating = it.bot_pickupevalfunc(this, it); + + if(rating > 0) + navigation_routerating(this, it, rating * ratingscale, 2000); + }); +} + +void havocbot_goalrating_controlpoints(entity this, float ratingscale, vector org, float sradius) +{ + FOREACH_ENTITY_CLASS("dom_controlpoint", vdist((((it.absmin + it.absmax) * 0.5) - org), <, sradius), + { + if(it.cnt > -1) // this is just being fought + navigation_routerating(this, it, ratingscale, 5000); + else if(it.goalentity.cnt == 0) // unclaimed + navigation_routerating(this, it, ratingscale * 0.5, 5000); + else if(it.goalentity.team != this.team) // other team's point + navigation_routerating(this, it, ratingscale * 0.2, 5000); + }); +} + +void havocbot_goalrating_enemyplayers(entity this, float ratingscale, vector org, float sradius) +{ + if (autocvar_bot_nofire) + return; + + // don't chase players if we're under water + if(this.waterlevel>WATERLEVEL_WETFEET) + return; + + int t; + + FOREACH_CLIENT(IS_PLAYER(it) && bot_shouldattack(this, it), LAMBDA( + // TODO: Merge this logic with the bot_shouldattack function + if(vdist(it.origin - org, <, 100) || vdist(it.origin - org, >, sradius)) + continue; + + // rate only visible enemies + /* + traceline(this.origin + this.view_ofs, it.origin, MOVE_NOMONSTERS, this); + if (trace_fraction < 1 || trace_ent != it) + continue; + */ + + if((it.flags & FL_INWATER) || (it.flags & FL_PARTIALGROUND)) + continue; + + // not falling + if((IS_ONGROUND(it)) == 0) + { + traceline(it.origin, it.origin + '0 0 -1500', true, NULL); + t = pointcontents(trace_endpos + '0 0 1'); + if(t != CONTENT_SOLID ) + if(t & CONTENT_WATER || t & CONTENT_SLIME || t & CONTENT_LAVA) + continue; + if(tracebox_hits_trigger_hurt(it.origin, it.mins, it.maxs, trace_endpos)) + continue; + } + + // TODO: rate waypoints near the targetted player at that moment, instead of the player itthis + // adding a player as a goal seems to be quite dangerous, especially on space maps + // remove hack in navigation_poptouchedgoals() after performing this change + + t = (this.health + this.armorvalue ) / (it.health + it.armorvalue ); + navigation_routerating(this, it, t * ratingscale, 2000); + )); +} + +// legacy bot role for standard gamemodes +// go to best items +void havocbot_role_generic(entity this) +{ + if(IS_DEAD(this)) + return; + + if (this.bot_strategytime < time) + { + this.bot_strategytime = time + autocvar_bot_ai_strategyinterval; + navigation_goalrating_start(this); + havocbot_goalrating_items(this, 10000, this.origin, 10000); + havocbot_goalrating_enemyplayers(this, 20000, this.origin, 10000); + //havocbot_goalrating_waypoints(1, this.origin, 1000); + navigation_goalrating_end(this); + } +} + +void havocbot_chooserole_generic(entity this) +{ + this.havocbot_role = havocbot_role_generic; +} + +void havocbot_chooserole(entity this) +{ + LOG_TRACE("choosing a role...\n"); + this.bot_strategytime = 0; + if(!MUTATOR_CALLHOOK(HavocBot_ChooseRole, this)) + havocbot_chooserole_generic(this); +} diff --git a/qcsrc/server/bot/default/havocbot/roles.qh b/qcsrc/server/bot/default/havocbot/roles.qh new file mode 100644 index 000000000..5b1f2b530 --- /dev/null +++ b/qcsrc/server/bot/default/havocbot/roles.qh @@ -0,0 +1,2 @@ +#pragma once +void havocbot_goalrating_controlpoints(entity this, float ratingscale, vector org, float sradius); diff --git a/qcsrc/server/bot/default/havocbot/scripting.qh b/qcsrc/server/bot/default/havocbot/scripting.qh new file mode 100644 index 000000000..07cb4d6e6 --- /dev/null +++ b/qcsrc/server/bot/default/havocbot/scripting.qh @@ -0,0 +1,3 @@ +#pragma once + +void bot_clearqueue(entity bot); diff --git a/qcsrc/server/bot/default/navigation.qc b/qcsrc/server/bot/default/navigation.qc new file mode 100644 index 000000000..5ab07b1a8 --- /dev/null +++ b/qcsrc/server/bot/default/navigation.qc @@ -0,0 +1,1213 @@ +#include "navigation.qh" + +#include "cvars.qh" + +#include "bot.qh" +#include "waypoints.qh" + +#include + +#include + +#include +#include + +.float speed; + +// rough simulation of walking from one point to another to test if a path +// can be traveled, used for waypoint linking and havocbot + +bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float movemode) +{ + vector org; + vector move; + vector dir; + float dist; + float totaldist; + float stepdist; + float yaw; + float ignorehazards; + float swimming; + + if(autocvar_bot_debug_tracewalk) + { + debugresetnodes(); + debugnode(e, start); + } + + move = end - start; + move.z = 0; + org = start; + dist = totaldist = vlen(move); + dir = normalize(move); + stepdist = 32; + ignorehazards = false; + swimming = false; + + // Analyze starting point + traceline(start, start, MOVE_NORMAL, e); + if (trace_dpstartcontents & (DPCONTENTS_SLIME | DPCONTENTS_LAVA)) + ignorehazards = true; + else + { + traceline( start, start + '0 0 -65536', MOVE_NORMAL, e); + if (trace_dpstartcontents & (DPCONTENTS_SLIME | DPCONTENTS_LAVA)) + { + ignorehazards = true; + swimming = true; + } + } + tracebox(start, m1, m2, start, MOVE_NOMONSTERS, e); + if (trace_startsolid) + { + // Bad start + if(autocvar_bot_debug_tracewalk) + debugnodestatus(start, DEBUG_NODE_FAIL); + + //print("tracewalk: ", vtos(start), " is a bad start\n"); + return false; + } + + // Movement loop + yaw = vectoyaw(move); + move = end - org; + for (;;) + { + if (boxesoverlap(end, end, org + m1 + '-1 -1 -1', org + m2 + '1 1 1')) + { + // Succeeded + if(autocvar_bot_debug_tracewalk) + debugnodestatus(org, DEBUG_NODE_SUCCESS); + + //print("tracewalk: ", vtos(start), " can reach ", vtos(end), "\n"); + return true; + } + if(autocvar_bot_debug_tracewalk) + debugnode(e, org); + + if (dist <= 0) + break; + if (stepdist > dist) + stepdist = dist; + dist = dist - stepdist; + traceline(org, org, MOVE_NORMAL, e); + if (!ignorehazards) + { + if (trace_dpstartcontents & (DPCONTENTS_SLIME | DPCONTENTS_LAVA)) + { + // hazards blocking path + if(autocvar_bot_debug_tracewalk) + debugnodestatus(org, DEBUG_NODE_FAIL); + + //print("tracewalk: ", vtos(start), " hits a hazard when trying to reach ", vtos(end), "\n"); + return false; + } + } + if (trace_dpstartcontents & DPCONTENTS_LIQUIDSMASK) + { + move = normalize(end - org); + tracebox(org, m1, m2, org + move * stepdist, movemode, e); + + if(autocvar_bot_debug_tracewalk) + debugnode(e, trace_endpos); + + if (trace_fraction < 1) + { + swimming = true; + org = trace_endpos - normalize(org - trace_endpos) * stepdist; + for (; org.z < end.z + e.maxs.z; org.z += stepdist) + { + if(autocvar_bot_debug_tracewalk) + debugnode(e, org); + + if(pointcontents(org) == CONTENT_EMPTY) + break; + } + + if(pointcontents(org + '0 0 1') != CONTENT_EMPTY) + { + if(autocvar_bot_debug_tracewalk) + debugnodestatus(org, DEBUG_NODE_FAIL); + + return false; + //print("tracewalk: ", vtos(start), " failed under water\n"); + } + continue; + + } + else + org = trace_endpos; + } + else + { + move = dir * stepdist + org; + tracebox(org, m1, m2, move, movemode, e); + + if(autocvar_bot_debug_tracewalk) + debugnode(e, trace_endpos); + + // hit something + if (trace_fraction < 1) + { + // check if we can walk over this obstacle, possibly by jumpstepping + tracebox(org + stepheightvec, m1, m2, move + stepheightvec, movemode, e); + if (trace_fraction < 1 || trace_startsolid) + { + tracebox(org + jumpstepheightvec, m1, m2, move + jumpstepheightvec, movemode, e); + if (trace_fraction < 1 || trace_startsolid) + { + if(autocvar_bot_debug_tracewalk) + debugnodestatus(trace_endpos, DEBUG_NODE_WARNING); + + // check for doors + traceline( org, move, movemode, e); + if ( trace_ent.classname == "door_rotating" || trace_ent.classname == "door") + { + vector nextmove; + move = trace_endpos; + while(trace_ent.classname == "door_rotating" || trace_ent.classname == "door") + { + nextmove = move + (dir * stepdist); + traceline( move, nextmove, movemode, e); + move = nextmove; + } + } + else + { + if(autocvar_bot_debug_tracewalk) + debugnodestatus(trace_endpos, DEBUG_NODE_FAIL); + + //print("tracewalk: ", vtos(start), " hit something when trying to reach ", vtos(end), "\n"); + //te_explosion(trace_endpos); + //print(ftos(e.dphitcontentsmask), "\n"); + return false; // failed + } + } + else + move = trace_endpos; + } + else + move = trace_endpos; + } + else + move = trace_endpos; + + // trace down from stepheight as far as possible and move there, + // if this starts in solid we try again without the stepup, and + // if that also fails we assume it is a wall + // (this is the same logic as the Quake walkmove function used) + tracebox(move, m1, m2, move + '0 0 -65536', movemode, e); + + // moved successfully + if(swimming) + { + float c; + c = pointcontents(org + '0 0 1'); + if (!(c == CONTENT_WATER || c == CONTENT_LAVA || c == CONTENT_SLIME)) + swimming = false; + else + continue; + } + + org = trace_endpos; + } + } + + //print("tracewalk: ", vtos(start), " did not arrive at ", vtos(end), " but at ", vtos(org), "\n"); + + // moved but didn't arrive at the intended destination + if(autocvar_bot_debug_tracewalk) + debugnodestatus(org, DEBUG_NODE_FAIL); + + return false; +} + +///////////////////////////////////////////////////////////////////////////// +// goal stack +///////////////////////////////////////////////////////////////////////////// + +// completely empty the goal stack, used when deciding where to go +void navigation_clearroute(entity this) +{ + //print("bot ", etos(this), " clear\n"); + this.navigation_hasgoals = false; + this.goalcurrent = NULL; + this.goalstack01 = NULL; + this.goalstack02 = NULL; + this.goalstack03 = NULL; + this.goalstack04 = NULL; + this.goalstack05 = NULL; + this.goalstack06 = NULL; + this.goalstack07 = NULL; + this.goalstack08 = NULL; + this.goalstack09 = NULL; + this.goalstack10 = NULL; + this.goalstack11 = NULL; + this.goalstack12 = NULL; + this.goalstack13 = NULL; + this.goalstack14 = NULL; + this.goalstack15 = NULL; + this.goalstack16 = NULL; + this.goalstack17 = NULL; + this.goalstack18 = NULL; + this.goalstack19 = NULL; + this.goalstack20 = NULL; + this.goalstack21 = NULL; + this.goalstack22 = NULL; + this.goalstack23 = NULL; + this.goalstack24 = NULL; + this.goalstack25 = NULL; + this.goalstack26 = NULL; + this.goalstack27 = NULL; + this.goalstack28 = NULL; + this.goalstack29 = NULL; + this.goalstack30 = NULL; + this.goalstack31 = NULL; +} + +// add a new goal at the beginning of the stack +// (in other words: add a new prerequisite before going to the later goals) +// NOTE: when a waypoint is added, the WP gets pushed first, then the +// next-closest WP on the shortest path to the WP +// That means, if the stack overflows, the bot will know how to do the FIRST 32 +// steps to the goal, and then recalculate the path. +void navigation_pushroute(entity this, entity e) +{ + //print("bot ", etos(this), " push ", etos(e), "\n"); + this.goalstack31 = this.goalstack30; + this.goalstack30 = this.goalstack29; + this.goalstack29 = this.goalstack28; + this.goalstack28 = this.goalstack27; + this.goalstack27 = this.goalstack26; + this.goalstack26 = this.goalstack25; + this.goalstack25 = this.goalstack24; + this.goalstack24 = this.goalstack23; + this.goalstack23 = this.goalstack22; + this.goalstack22 = this.goalstack21; + this.goalstack21 = this.goalstack20; + this.goalstack20 = this.goalstack19; + this.goalstack19 = this.goalstack18; + this.goalstack18 = this.goalstack17; + this.goalstack17 = this.goalstack16; + this.goalstack16 = this.goalstack15; + this.goalstack15 = this.goalstack14; + this.goalstack14 = this.goalstack13; + this.goalstack13 = this.goalstack12; + this.goalstack12 = this.goalstack11; + this.goalstack11 = this.goalstack10; + this.goalstack10 = this.goalstack09; + this.goalstack09 = this.goalstack08; + this.goalstack08 = this.goalstack07; + this.goalstack07 = this.goalstack06; + this.goalstack06 = this.goalstack05; + this.goalstack05 = this.goalstack04; + this.goalstack04 = this.goalstack03; + this.goalstack03 = this.goalstack02; + this.goalstack02 = this.goalstack01; + this.goalstack01 = this.goalcurrent; + this.goalcurrent = e; +} + +// remove first goal from stack +// (in other words: remove a prerequisite for reaching the later goals) +// (used when a spawnfunc_waypoint is reached) +void navigation_poproute(entity this) +{ + //print("bot ", etos(this), " pop\n"); + this.goalcurrent = this.goalstack01; + this.goalstack01 = this.goalstack02; + this.goalstack02 = this.goalstack03; + this.goalstack03 = this.goalstack04; + this.goalstack04 = this.goalstack05; + this.goalstack05 = this.goalstack06; + this.goalstack06 = this.goalstack07; + this.goalstack07 = this.goalstack08; + this.goalstack08 = this.goalstack09; + this.goalstack09 = this.goalstack10; + this.goalstack10 = this.goalstack11; + this.goalstack11 = this.goalstack12; + this.goalstack12 = this.goalstack13; + this.goalstack13 = this.goalstack14; + this.goalstack14 = this.goalstack15; + this.goalstack15 = this.goalstack16; + this.goalstack16 = this.goalstack17; + this.goalstack17 = this.goalstack18; + this.goalstack18 = this.goalstack19; + this.goalstack19 = this.goalstack20; + this.goalstack20 = this.goalstack21; + this.goalstack21 = this.goalstack22; + this.goalstack22 = this.goalstack23; + this.goalstack23 = this.goalstack24; + this.goalstack24 = this.goalstack25; + this.goalstack25 = this.goalstack26; + this.goalstack26 = this.goalstack27; + this.goalstack27 = this.goalstack28; + this.goalstack28 = this.goalstack29; + this.goalstack29 = this.goalstack30; + this.goalstack30 = this.goalstack31; + this.goalstack31 = NULL; +} + +float navigation_waypoint_will_link(vector v, vector org, entity ent, float walkfromwp, float bestdist) +{ + float dist; + dist = vlen(v - org); + if (bestdist > dist) + { + traceline(v, org, true, ent); + if (trace_fraction == 1) + { + if (walkfromwp) + { + if (tracewalk(ent, v, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), org, bot_navigation_movemode)) + return true; + } + else + { + if (tracewalk(ent, org, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), v, bot_navigation_movemode)) + return true; + } + } + } + return false; +} + +// find the spawnfunc_waypoint near a dynamic goal such as a dropped weapon +entity navigation_findnearestwaypoint_withdist_except(entity ent, float walkfromwp, float bestdist, entity except) +{ + vector pm1 = ent.origin + ent.mins; + vector pm2 = ent.origin + ent.maxs; + + // do two scans, because box test is cheaper + IL_EACH(g_waypoints, it != ent && it != except, + { + if(boxesoverlap(pm1, pm2, it.absmin, it.absmax)) + return it; + }); + + vector org = ent.origin + 0.5 * (ent.mins + ent.maxs); + org.z = ent.origin.z + ent.mins.z - STAT(PL_MIN, NULL).z; // player height + // TODO possibly make other code have the same support for bboxes + if(ent.tag_entity) + org = org + ent.tag_entity.origin; + if (navigation_testtracewalk) + te_plasmaburn(org); + + entity best = NULL; + vector v; + + // box check failed, try walk + IL_EACH(g_waypoints, it != ent, + { + if(it.wpisbox) + { + vector wm1 = it.origin + it.mins; + vector wm2 = it.origin + it.maxs; + v.x = bound(wm1_x, org.x, wm2_x); + v.y = bound(wm1_y, org.y, wm2_y); + v.z = bound(wm1_z, org.z, wm2_z); + } + else + v = it.origin; + if(navigation_waypoint_will_link(v, org, ent, walkfromwp, bestdist)) + { + bestdist = vlen(v - org); + best = it; + } + }); + return best; +} +entity navigation_findnearestwaypoint(entity ent, float walkfromwp) +{ + entity wp = navigation_findnearestwaypoint_withdist_except(ent, walkfromwp, 1050, NULL); + if (autocvar_g_waypointeditor_auto) + { + entity wp2 = navigation_findnearestwaypoint_withdist_except(ent, walkfromwp, 1050, wp); + if (wp && !wp2) + wp.wpflags |= WAYPOINTFLAG_PROTECTED; + } + return wp; +} + +// finds the waypoints near the bot initiating a navigation query +float navigation_markroutes_nearestwaypoints(entity this, float maxdist) +{ + vector v, m1, m2; +// navigation_testtracewalk = true; + int c = 0; + IL_EACH(g_waypoints, !it.wpconsidered, + { + if (it.wpisbox) + { + m1 = it.origin + it.mins; + m2 = it.origin + it.maxs; + v = this.origin; + v.x = bound(m1_x, v.x, m2_x); + v.y = bound(m1_y, v.y, m2_y); + v.z = bound(m1_z, v.z, m2_z); + } + else + v = it.origin; + vector diff = v - this.origin; + diff.z = max(0, diff.z); + if(vdist(diff, <, maxdist)) + { + it.wpconsidered = true; + if (tracewalk(this, this.origin, this.mins, this.maxs, v, bot_navigation_movemode)) + { + it.wpnearestpoint = v; + it.wpcost = vlen(v - this.origin) + it.dmg; + it.wpfire = 1; + it.enemy = NULL; + c = c + 1; + } + } + }); + //navigation_testtracewalk = false; + return c; +} + +// updates a path link if a spawnfunc_waypoint link is better than the current one +void navigation_markroutes_checkwaypoint(entity w, entity wp, float cost2, vector p) +{ + vector m1; + vector m2; + vector v; + if (wp.wpisbox) + { + m1 = wp.absmin; + m2 = wp.absmax; + v.x = bound(m1_x, p.x, m2_x); + v.y = bound(m1_y, p.y, m2_y); + v.z = bound(m1_z, p.z, m2_z); + } + else + v = wp.origin; + cost2 = cost2 + vlen(v - p); + if (wp.wpcost > cost2) + { + wp.wpcost = cost2; + wp.enemy = w; + wp.wpfire = 1; + wp.wpnearestpoint = v; + } +} + +// queries the entire spawnfunc_waypoint network for pathes leading away from the bot +void navigation_markroutes(entity this, entity fixed_source_waypoint) +{ + float cost, cost2; + vector p; + + IL_EACH(g_waypoints, true, + { + it.wpconsidered = false; + it.wpnearestpoint = '0 0 0'; + it.wpcost = 10000000; + it.wpfire = 0; + it.enemy = NULL; + }); + + if(fixed_source_waypoint) + { + fixed_source_waypoint.wpconsidered = true; + fixed_source_waypoint.wpnearestpoint = fixed_source_waypoint.origin + 0.5 * (fixed_source_waypoint.mins + fixed_source_waypoint.maxs); + fixed_source_waypoint.wpcost = fixed_source_waypoint.dmg; + fixed_source_waypoint.wpfire = 1; + fixed_source_waypoint.enemy = NULL; + } + else + { + // try a short range search for the nearest waypoints, and expand the search repeatedly if none are found + // as this search is expensive we will use lower values if the bot is on the air + float increment, maxdistance; + if(IS_ONGROUND(this)) + { + increment = 750; + maxdistance = 50000; + } + else + { + increment = 500; + maxdistance = 1500; + } + + for(int j = increment; !navigation_markroutes_nearestwaypoints(this, j) && j < maxdistance; j += increment); + } + + bool searching = true; + while (searching) + { + searching = false; + IL_EACH(g_waypoints, it.wpfire, + { + searching = true; + it.wpfire = 0; + cost = it.wpcost; + p = it.wpnearestpoint; + entity wp; + wp = it.wp00;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp00mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp01;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp01mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp02;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp02mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp03;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp03mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp04;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp04mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp05;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp05mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp06;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp06mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp07;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp07mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp08;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp08mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp09;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp09mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp10;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp10mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp11;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp11mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp12;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp12mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp13;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp13mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp14;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp14mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp15;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp15mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp16;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp16mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp17;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp17mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp18;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp18mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp19;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp19mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp20;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp20mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp21;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp21mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp22;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp22mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp23;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp23mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp24;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp24mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp25;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp25mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp26;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp26mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp27;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp27mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp28;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp28mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp29;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp29mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp30;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp30mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + wp = it.wp31;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp31mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); + }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} + }); + } +} + +// queries the entire spawnfunc_waypoint network for pathes leading to the bot +void navigation_markroutes_inverted(entity fixed_source_waypoint) +{ + float cost, cost2; + vector p; + IL_EACH(g_waypoints, true, + { + it.wpconsidered = false; + it.wpnearestpoint = '0 0 0'; + it.wpcost = 10000000; + it.wpfire = 0; + it.enemy = NULL; + }); + + if(fixed_source_waypoint) + { + fixed_source_waypoint.wpconsidered = true; + fixed_source_waypoint.wpnearestpoint = fixed_source_waypoint.origin + 0.5 * (fixed_source_waypoint.mins + fixed_source_waypoint.maxs); + fixed_source_waypoint.wpcost = fixed_source_waypoint.dmg; // the cost to get from X to fixed_source_waypoint + fixed_source_waypoint.wpfire = 1; + fixed_source_waypoint.enemy = NULL; + } + else + { + error("need to start with a waypoint\n"); + } + + bool searching = true; + while (searching) + { + searching = false; + IL_EACH(g_waypoints, it.wpfire, + { + searching = true; + it.wpfire = 0; + cost = it.wpcost; // cost to walk from it to home + p = it.wpnearestpoint; + entity wp = it; + IL_EACH(g_waypoints, true, + { + if(wp != it.wp00) if(wp != it.wp01) if(wp != it.wp02) if(wp != it.wp03) + if(wp != it.wp04) if(wp != it.wp05) if(wp != it.wp06) if(wp != it.wp07) + if(wp != it.wp08) if(wp != it.wp09) if(wp != it.wp10) if(wp != it.wp11) + if(wp != it.wp12) if(wp != it.wp13) if(wp != it.wp14) if(wp != it.wp15) + if(wp != it.wp16) if(wp != it.wp17) if(wp != it.wp18) if(wp != it.wp19) + if(wp != it.wp20) if(wp != it.wp21) if(wp != it.wp22) if(wp != it.wp23) + if(wp != it.wp24) if(wp != it.wp25) if(wp != it.wp26) if(wp != it.wp27) + if(wp != it.wp28) if(wp != it.wp29) if(wp != it.wp30) if(wp != it.wp31) + continue; + cost2 = cost + it.dmg; + navigation_markroutes_checkwaypoint(wp, it, cost2, p); + }); + }); + } +} + +// updates the best goal according to a weighted calculation of travel cost and item value of a new proposed item +void navigation_routerating(entity this, entity e, float f, float rangebias) +{ + entity nwp; + vector o; + if (!e) + return; + + if(e.blacklisted) + return; + + o = (e.absmin + e.absmax) * 0.5; + + //print("routerating ", etos(e), " = ", ftos(f), " - ", ftos(rangebias), "\n"); + + // Evaluate path using jetpack + if(g_jetpack) + if(this.items & IT_JETPACK) + if(autocvar_bot_ai_navigation_jetpack) + if(vdist(this.origin - o, >, autocvar_bot_ai_navigation_jetpack_mindistance)) + { + vector pointa, pointb; + + LOG_DEBUG("jetpack ai: evaluating path for ", e.classname, "\n"); + + // Point A + traceline(this.origin, this.origin + '0 0 65535', MOVE_NORMAL, this); + pointa = trace_endpos - '0 0 1'; + + // Point B + traceline(o, o + '0 0 65535', MOVE_NORMAL, e); + pointb = trace_endpos - '0 0 1'; + + // Can I see these two points from the sky? + traceline(pointa, pointb, MOVE_NORMAL, this); + + if(trace_fraction==1) + { + LOG_DEBUG("jetpack ai: can bridge these two points\n"); + + // Lower the altitude of these points as much as possible + float zdistance, xydistance, cost, t, fuel; + vector down, npa, npb; + + down = '0 0 -1' * (STAT(PL_MAX, NULL).z - STAT(PL_MIN, NULL).z) * 10; + + do{ + npa = pointa + down; + npb = pointb + down; + + if(npa.z<=this.absmax.z) + break; + + if(npb.z<=e.absmax.z) + break; + + traceline(npa, npb, MOVE_NORMAL, this); + if(trace_fraction==1) + { + pointa = npa; + pointb = npb; + } + } + while(trace_fraction == 1); + + + // Rough estimation of fuel consumption + // (ignores acceleration and current xyz velocity) + xydistance = vlen(pointa - pointb); + zdistance = fabs(pointa.z - this.origin.z); + + t = zdistance / autocvar_g_jetpack_maxspeed_up; + t += xydistance / autocvar_g_jetpack_maxspeed_side; + fuel = t * autocvar_g_jetpack_fuel * 0.8; + + LOG_DEBUG(strcat("jetpack ai: required fuel ", ftos(fuel), " this.ammo_fuel ", ftos(this.ammo_fuel), "\n")); + + // enough fuel ? + if(this.ammo_fuel>fuel) + { + // Estimate cost + // (as onground costs calculation is mostly based on distances, here we do the same establishing some relationship + // - between air and ground speeds) + + cost = xydistance / (autocvar_g_jetpack_maxspeed_side/autocvar_sv_maxspeed); + cost += zdistance / (autocvar_g_jetpack_maxspeed_up/autocvar_sv_maxspeed); + cost *= 1.5; + + // Compare against other goals + f = f * rangebias / (rangebias + cost); + + if (navigation_bestrating < f) + { + LOG_DEBUG(strcat("jetpack path: added goal ", e.classname, " (with rating ", ftos(f), ")\n")); + navigation_bestrating = f; + navigation_bestgoal = e; + this.navigation_jetpack_goal = e; + this.navigation_jetpack_point = pointb; + } + return; + } + } + } + + //te_wizspike(e.origin); + //bprint(etos(e)); + //bprint("\n"); + // update the cached spawnfunc_waypoint link on a dynamic item entity + if(e.classname == "waypoint" && !(e.wpflags & WAYPOINTFLAG_PERSONAL)) + { + nwp = e; + } + else + { + float search; + + search = true; + + if(e.flags & FL_ITEM) + { + if (!(e.flags & FL_WEAPON)) + if(e.nearestwaypoint) + search = false; + } + else if (e.flags & FL_WEAPON) + { + if(e.classname != "droppedweapon") + if(e.nearestwaypoint) + search = false; + } + + if(search) + if (time > e.nearestwaypointtimeout) + { + nwp = navigation_findnearestwaypoint(e, true); + if(nwp) + e.nearestwaypoint = nwp; + else + { + LOG_DEBUG(strcat("FAILED to find a nearest waypoint to '", e.classname, "' #", etos(e), "\n")); + + if(e.flags & FL_ITEM) + e.blacklisted = true; + else if (e.flags & FL_WEAPON) + { + if(e.classname != "droppedweapon") + e.blacklisted = true; + } + + if(e.blacklisted) + { + LOG_DEBUG(strcat("The entity '", e.classname, "' is going to be excluded from path finding during this match\n")); + return; + } + } + + // TODO: Cleaner solution, probably handling this timeout from ctf.qc + if(e.classname=="item_flag_team") + e.nearestwaypointtimeout = time + 2; + else + e.nearestwaypointtimeout = time + random() * 3 + 5; + } + nwp = e.nearestwaypoint; + } + + LOG_DEBUG(strcat("-- checking ", e.classname, " (with cost ", ftos(nwp.wpcost), ")\n")); + if (nwp) + if (nwp.wpcost < 10000000) + { + //te_wizspike(nwp.wpnearestpoint); + LOG_DEBUG(strcat(e.classname, " ", ftos(f), "/(1+", ftos((nwp.wpcost + vlen(e.origin - nwp.wpnearestpoint))), "/", ftos(rangebias), ") = ")); + f = f * rangebias / (rangebias + (nwp.wpcost + vlen(o - nwp.wpnearestpoint))); + LOG_DEBUG(strcat("considering ", e.classname, " (with rating ", ftos(f), ")\n")); + if (navigation_bestrating < f) + { + LOG_DEBUG(strcat("ground path: added goal ", e.classname, " (with rating ", ftos(f), ")\n")); + navigation_bestrating = f; + navigation_bestgoal = e; + } + } +} + +// adds an item to the the goal stack with the path to a given item +bool navigation_routetogoal(entity this, entity e, vector startposition) +{ + this.goalentity = e; + + // if there is no goal, just exit + if (!e) + return false; + + this.navigation_hasgoals = true; + + // put the entity on the goal stack + //print("routetogoal ", etos(e), "\n"); + navigation_pushroute(this, e); + + if(g_jetpack) + if(e==this.navigation_jetpack_goal) + return true; + + // if it can reach the goal there is nothing more to do + if (tracewalk(this, startposition, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), (e.absmin + e.absmax) * 0.5, bot_navigation_movemode)) + return true; + + // see if there are waypoints describing a path to the item + if(e.classname != "waypoint" || (e.wpflags & WAYPOINTFLAG_PERSONAL)) + e = e.nearestwaypoint; + else + e = e.enemy; // we already have added it, so... + + if(e == NULL) + return false; + + for (;;) + { + // add the spawnfunc_waypoint to the path + navigation_pushroute(this, e); + e = e.enemy; + + if(e==NULL) + break; + } + + return false; +} + +// removes any currently touching waypoints from the goal stack +// (this is how bots detect if they reached a goal) +void navigation_poptouchedgoals(entity this) +{ + vector org, m1, m2; + org = this.origin; + m1 = org + this.mins; + m2 = org + this.maxs; + + if(this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT) + { + if(this.lastteleporttime>0) + if(time-this.lastteleporttime<(this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL)?2:0.15) + { + if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) + if(this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL && this.goalcurrent.owner==this) + { + this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING; + this.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED; + } + navigation_poproute(this); + return; + } + } + + // If for some reason the bot is closer to the next goal, pop the current one + if(this.goalstack01) + if(vlen2(this.goalcurrent.origin - this.origin) > vlen2(this.goalstack01.origin - this.origin)) + if(checkpvs(this.origin + this.view_ofs, this.goalstack01)) + if(tracewalk(this, this.origin, this.mins, this.maxs, (this.goalstack01.absmin + this.goalstack01.absmax) * 0.5, bot_navigation_movemode)) + { + LOG_DEBUG(strcat("path optimized for ", this.netname, ", removed a goal from the queue\n")); + navigation_poproute(this); + // TODO this may also be a nice idea to do "early" (e.g. by + // manipulating the vlen() comparisons) to shorten paths in + // general - this would make bots walk more "on rails" than + // "zigzagging" which they currently do with sufficiently + // random-like waypoints, and thus can make a nice bot + // personality property + } + + // HACK: remove players/bots as goals, they can lead a bot to unexpected places (cliffs, lava, etc) + // TODO: rate waypoints near the targetted player at that moment, instead of the player itthis + if(IS_PLAYER(this.goalcurrent)) + navigation_poproute(this); + + // aid for detecting jump pads better (distance based check fails sometimes) + if(this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT && this.jumppadcount > 0 ) + navigation_poproute(this); + + // Loose goal touching check when running + if(this.aistatus & AI_STATUS_RUNNING) + if(this.speed >= autocvar_sv_maxspeed) // if -really- running + if(this.goalcurrent.classname=="waypoint") + { + if(vdist(this.origin - this.goalcurrent.origin, <, 150)) + { + traceline(this.origin + this.view_ofs , this.goalcurrent.origin, true, NULL); + if(trace_fraction==1) + { + // Detect personal waypoints + if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) + if(this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL && this.goalcurrent.owner==this) + { + this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING; + this.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED; + } + + navigation_poproute(this); + } + } + } + + while (this.goalcurrent && boxesoverlap(m1, m2, this.goalcurrent.absmin, this.goalcurrent.absmax)) + { + // Detect personal waypoints + if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) + if(this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL && this.goalcurrent.owner==this) + { + this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING; + this.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED; + } + + navigation_poproute(this); + } +} + +// begin a goal selection session (queries spawnfunc_waypoint network) +void navigation_goalrating_start(entity this) +{ + if(this.aistatus & AI_STATUS_STUCK) + return; + + this.navigation_jetpack_goal = NULL; + navigation_bestrating = -1; + this.navigation_hasgoals = false; + navigation_clearroute(this); + navigation_bestgoal = NULL; + navigation_markroutes(this, NULL); +} + +// ends a goal selection session (updates goal stack to the best goal) +void navigation_goalrating_end(entity this) +{ + if(this.aistatus & AI_STATUS_STUCK) + return; + + navigation_routetogoal(this, navigation_bestgoal, this.origin); + LOG_DEBUG(strcat("best goal ", this.goalcurrent.classname , "\n")); + + // If the bot got stuck then try to reach the farthest waypoint + if (!this.navigation_hasgoals) + if (autocvar_bot_wander_enable) + { + if (!(this.aistatus & AI_STATUS_STUCK)) + { + LOG_DEBUG(strcat(this.netname, " cannot walk to any goal\n")); + this.aistatus |= AI_STATUS_STUCK; + } + + this.navigation_hasgoals = false; // Reset this value + } +} + +void botframe_updatedangerousobjects(float maxupdate) +{ + vector m1, m2, v, o; + float c, d, danger; + c = 0; + IL_EACH(g_waypoints, true, + { + danger = 0; + m1 = it.mins; + m2 = it.maxs; + FOREACH_ENTITY_FLOAT(bot_dodge, true, + { + v = it.origin; + v.x = bound(m1_x, v.x, m2_x); + v.y = bound(m1_y, v.y, m2_y); + v.z = bound(m1_z, v.z, m2_z); + o = (it.absmin + it.absmax) * 0.5; + d = it.bot_dodgerating - vlen(o - v); + if (d > 0) + { + traceline(o, v, true, NULL); + if (trace_fraction == 1) + danger = danger + d; + } + }); + it.dmg = danger; + c = c + 1; + if (c >= maxupdate) + break; + }); +} + +void navigation_unstuck(entity this) +{ + float search_radius = 1000; + + if (!autocvar_bot_wander_enable) + return; + + if (!bot_waypoint_queue_owner) + { + LOG_DEBUG(strcat(this.netname, " sutck, taking over the waypoints queue\n")); + bot_waypoint_queue_owner = this; + bot_waypoint_queue_bestgoal = NULL; + bot_waypoint_queue_bestgoalrating = 0; + } + + if(bot_waypoint_queue_owner!=this) + return; + + if (bot_waypoint_queue_goal) + { + // evaluate the next goal on the queue + float d = vlen(this.origin - bot_waypoint_queue_goal.origin); + LOG_DEBUG(strcat(this.netname, " evaluating ", bot_waypoint_queue_goal.classname, " with distance ", ftos(d), "\n")); + if(tracewalk(bot_waypoint_queue_goal, this.origin, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), bot_waypoint_queue_goal.origin, bot_navigation_movemode)) + { + if( d > bot_waypoint_queue_bestgoalrating) + { + bot_waypoint_queue_bestgoalrating = d; + bot_waypoint_queue_bestgoal = bot_waypoint_queue_goal; + } + } + bot_waypoint_queue_goal = bot_waypoint_queue_goal.bot_waypoint_queue_nextgoal; + + if (!bot_waypoint_queue_goal) + { + if (bot_waypoint_queue_bestgoal) + { + LOG_DEBUG(strcat(this.netname, " stuck, reachable waypoint found, heading to it\n")); + navigation_routetogoal(this, bot_waypoint_queue_bestgoal, this.origin); + this.bot_strategytime = time + autocvar_bot_ai_strategyinterval; + this.aistatus &= ~AI_STATUS_STUCK; + } + else + { + LOG_DEBUG(strcat(this.netname, " stuck, cannot walk to any waypoint at all\n")); + } + + bot_waypoint_queue_owner = NULL; + } + } + else + { + if(bot_strategytoken!=this) + return; + + // build a new queue + LOG_DEBUG(strcat(this.netname, " stuck, scanning reachable waypoints within ", ftos(search_radius)," qu\n")); + + entity first = NULL; + + FOREACH_ENTITY_RADIUS(this.origin, search_radius, it.classname == "waypoint" && !(it.wpflags & WAYPOINTFLAG_GENERATED), + { + if(bot_waypoint_queue_goal) + bot_waypoint_queue_goal.bot_waypoint_queue_nextgoal = it; + else + first = it; + + bot_waypoint_queue_goal = it; + bot_waypoint_queue_goal.bot_waypoint_queue_nextgoal = NULL; + }); + + if (first) + bot_waypoint_queue_goal = first; + else + { + LOG_DEBUG(strcat(this.netname, " stuck, cannot walk to any waypoint at all\n")); + bot_waypoint_queue_owner = NULL; + } + } +} + +// Support for debugging tracewalk visually + +void debugresetnodes() +{ + debuglastnode = '0 0 0'; +} + +void debugnode(entity this, vector node) +{ + if (!IS_PLAYER(this)) + return; + + if(debuglastnode=='0 0 0') + { + debuglastnode = node; + return; + } + + te_lightning2(NULL, node, debuglastnode); + debuglastnode = node; +} + +void debugnodestatus(vector position, float status) +{ + vector c; + + switch (status) + { + case DEBUG_NODE_SUCCESS: + c = '0 15 0'; + break; + case DEBUG_NODE_WARNING: + c = '15 15 0'; + break; + case DEBUG_NODE_FAIL: + c = '15 0 0'; + break; + default: + c = '15 15 15'; + } + + te_customflash(position, 40, 2, c); +} + +// Support for debugging the goal stack visually + +.float goalcounter; +.vector lastposition; + +// Debug the goal stack visually +void debuggoalstack(entity this) +{ + entity goal; + vector org, go; + + if(this.goalcounter==0)goal=this.goalcurrent; + else if(this.goalcounter==1)goal=this.goalstack01; + else if(this.goalcounter==2)goal=this.goalstack02; + else if(this.goalcounter==3)goal=this.goalstack03; + else if(this.goalcounter==4)goal=this.goalstack04; + else if(this.goalcounter==5)goal=this.goalstack05; + else if(this.goalcounter==6)goal=this.goalstack06; + else if(this.goalcounter==7)goal=this.goalstack07; + else if(this.goalcounter==8)goal=this.goalstack08; + else if(this.goalcounter==9)goal=this.goalstack09; + else if(this.goalcounter==10)goal=this.goalstack10; + else if(this.goalcounter==11)goal=this.goalstack11; + else if(this.goalcounter==12)goal=this.goalstack12; + else if(this.goalcounter==13)goal=this.goalstack13; + else if(this.goalcounter==14)goal=this.goalstack14; + else if(this.goalcounter==15)goal=this.goalstack15; + else if(this.goalcounter==16)goal=this.goalstack16; + else if(this.goalcounter==17)goal=this.goalstack17; + else if(this.goalcounter==18)goal=this.goalstack18; + else if(this.goalcounter==19)goal=this.goalstack19; + else if(this.goalcounter==20)goal=this.goalstack20; + else if(this.goalcounter==21)goal=this.goalstack21; + else if(this.goalcounter==22)goal=this.goalstack22; + else if(this.goalcounter==23)goal=this.goalstack23; + else if(this.goalcounter==24)goal=this.goalstack24; + else if(this.goalcounter==25)goal=this.goalstack25; + else if(this.goalcounter==26)goal=this.goalstack26; + else if(this.goalcounter==27)goal=this.goalstack27; + else if(this.goalcounter==28)goal=this.goalstack28; + else if(this.goalcounter==29)goal=this.goalstack29; + else if(this.goalcounter==30)goal=this.goalstack30; + else if(this.goalcounter==31)goal=this.goalstack31; + else goal=NULL; + + if(goal==NULL) + { + this.goalcounter = 0; + this.lastposition='0 0 0'; + return; + } + + if(this.lastposition=='0 0 0') + org = this.origin; + else + org = this.lastposition; + + + go = ( goal.absmin + goal.absmax ) * 0.5; + te_lightning2(NULL, org, go); + this.lastposition = go; + + this.goalcounter++; +} diff --git a/qcsrc/server/bot/default/navigation.qh b/qcsrc/server/bot/default/navigation.qh new file mode 100644 index 000000000..ad0177665 --- /dev/null +++ b/qcsrc/server/bot/default/navigation.qh @@ -0,0 +1,77 @@ +#pragma once +/* + * Globals and Fields + */ + +float navigation_bestrating; +float bot_navigation_movemode; +float navigation_testtracewalk; + +vector jumpstepheightvec; +vector stepheightvec; + +entity navigation_bestgoal; + +// stack of current goals (the last one of which may be an item or other +// desirable object, the rest are typically waypoints to reach it) +.entity goalcurrent, goalstack01, goalstack02, goalstack03; +.entity goalstack04, goalstack05, goalstack06, goalstack07; +.entity goalstack08, goalstack09, goalstack10, goalstack11; +.entity goalstack12, goalstack13, goalstack14, goalstack15; +.entity goalstack16, goalstack17, goalstack18, goalstack19; +.entity goalstack20, goalstack21, goalstack22, goalstack23; +.entity goalstack24, goalstack25, goalstack26, goalstack27; +.entity goalstack28, goalstack29, goalstack30, goalstack31; +.entity nearestwaypoint; + +.float nearestwaypointtimeout; +.float navigation_hasgoals; +.float lastteleporttime; + +.float blacklisted; + +.entity navigation_jetpack_goal; +.vector navigation_jetpack_point; + +const float DEBUG_NODE_SUCCESS = 1; +const float DEBUG_NODE_WARNING = 2; +const float DEBUG_NODE_FAIL = 3; +vector debuglastnode; + +entity bot_waypoint_queue_owner; // Owner of the temporary list of goals +entity bot_waypoint_queue_goal; // Head of the temporary list of goals +.entity bot_waypoint_queue_nextgoal; +entity bot_waypoint_queue_bestgoal; +float bot_waypoint_queue_bestgoalrating; + +/* + * Functions + */ + +void debugresetnodes(); +void debugnode(entity this, vector node); +void debugnodestatus(vector position, float status); + +void debuggoalstack(entity this); + +float tracewalk(entity e, vector start, vector m1, vector m2, vector end, float movemode); + +float navigation_markroutes_nearestwaypoints(entity this, float maxdist); +float navigation_routetogoal(entity this, entity e, vector startposition); + +void navigation_clearroute(entity this); +void navigation_pushroute(entity this, entity e); +void navigation_poproute(entity this); +void navigation_markroutes_checkwaypoint(entity w, entity wp, float cost2, vector p); +void navigation_markroutes(entity this, entity fixed_source_waypoint); +void navigation_markroutes_inverted(entity fixed_source_waypoint); +void navigation_routerating(entity this, entity e, float f, float rangebias); +void navigation_poptouchedgoals(entity this); +void navigation_goalrating_start(entity this); +void navigation_goalrating_end(entity this); +void navigation_unstuck(entity this); + +void botframe_updatedangerousobjects(float maxupdate); + +entity navigation_findnearestwaypoint(entity ent, float walkfromwp); +float navigation_waypoint_will_link(vector v, vector org, entity ent, float walkfromwp, float bestdist); diff --git a/qcsrc/server/bot/default/scripting.qc b/qcsrc/server/bot/default/scripting.qc new file mode 100644 index 000000000..8b2f9e6d1 --- /dev/null +++ b/qcsrc/server/bot/default/scripting.qc @@ -0,0 +1,1342 @@ +#include "scripting.qh" + +#include "cvars.qh" + +#include +#include + +#include "bot.qh" + +.int state; + +.float bot_cmdqueuebuf_allocated; +.float bot_cmdqueuebuf; +.float bot_cmdqueuebuf_start; +.float bot_cmdqueuebuf_end; + +void bot_clearqueue(entity bot) +{ + if(!bot.bot_cmdqueuebuf_allocated) + return; + buf_del(bot.bot_cmdqueuebuf); + bot.bot_cmdqueuebuf_allocated = false; + LOG_TRACE("bot ", bot.netname, " queue cleared\n"); +} + +void bot_queuecommand(entity bot, string cmdstring) +{ + if(!bot.bot_cmdqueuebuf_allocated) + { + bot.bot_cmdqueuebuf = buf_create(); + bot.bot_cmdqueuebuf_allocated = true; + bot.bot_cmdqueuebuf_start = 0; + bot.bot_cmdqueuebuf_end = 0; + } + + bufstr_set(bot.bot_cmdqueuebuf, bot.bot_cmdqueuebuf_end, cmdstring); + + // if the command was a "sound" command, precache the sound NOW + // this prevents lagging! + { + float sp; + string parm; + string cmdstr; + + sp = strstrofs(cmdstring, " ", 0); + if(sp >= 0) + { + parm = substring(cmdstring, sp + 1, -1); + cmdstr = substring(cmdstring, 0, sp); + if(cmdstr == "sound") + { + // find the LAST word + for (;;) + { + sp = strstrofs(parm, " ", 0); + if(sp < 0) + break; + parm = substring(parm, sp + 1, -1); + } + precache_sound(parm); + } + } + } + + bot.bot_cmdqueuebuf_end += 1; +} + +void bot_dequeuecommand(entity bot, float idx) +{ + if(!bot.bot_cmdqueuebuf_allocated) + error("dequeuecommand but no queue allocated"); + if(idx < bot.bot_cmdqueuebuf_start) + error("dequeueing a command in the past"); + if(idx >= bot.bot_cmdqueuebuf_end) + error("dequeueing a command in the future"); + bufstr_set(bot.bot_cmdqueuebuf, idx, ""); + if(idx == bot.bot_cmdqueuebuf_start) + bot.bot_cmdqueuebuf_start += 1; + if(bot.bot_cmdqueuebuf_start >= bot.bot_cmdqueuebuf_end) + bot_clearqueue(bot); +} + +string bot_readcommand(entity bot, float idx) +{ + if(!bot.bot_cmdqueuebuf_allocated) + error("readcommand but no queue allocated"); + if(idx < bot.bot_cmdqueuebuf_start) + error("reading a command in the past"); + if(idx >= bot.bot_cmdqueuebuf_end) + error("reading a command in the future"); + return bufstr_get(bot.bot_cmdqueuebuf, idx); +} + +bool bot_havecommand(entity this, int idx) +{ + if(!this.bot_cmdqueuebuf_allocated) + return false; + if(idx < this.bot_cmdqueuebuf_start) + return false; + if(idx >= this.bot_cmdqueuebuf_end) + return false; + return true; +} + +const int MAX_BOT_PLACES = 4; +.float bot_places_count; +.entity bot_places[MAX_BOT_PLACES]; +.string bot_placenames[MAX_BOT_PLACES]; +entity bot_getplace(entity this, string placename) +{ + entity e; + if(substring(placename, 0, 1) == "@") + { + int i, p; + placename = substring(placename, 1, -1); + string s, s2; + for(i = 0; i < this.bot_places_count; ++i) + if(this.(bot_placenames[i]) == placename) + return this.(bot_places[i]); + // now: i == this.bot_places_count + s = s2 = cvar_string(placename); + p = strstrofs(s2, " ", 0); + if(p >= 0) + { + s = substring(s2, 0, p); + //print("places: ", placename, " -> ", cvar_string(placename), "\n"); + cvar_set(placename, strcat(substring(s2, p+1, -1), " ", s)); + //print("places: ", placename, " := ", cvar_string(placename), "\n"); + } + e = find(NULL, targetname, s); + if(!e) + LOG_INFO("invalid place ", s, "\n"); + if(i < MAX_BOT_PLACES) + { + this.(bot_placenames[i]) = strzone(placename); + this.(bot_places[i]) = e; + this.bot_places_count += 1; + } + return e; + } + else + { + e = find(NULL, targetname, placename); + if(!e) + LOG_INFO("invalid place ", placename, "\n"); + return e; + } +} + + +// Initialize global commands list +// NOTE: New commands should be initialized here +void bot_commands_init() +{ + bot_cmd_string[BOT_CMD_NULL] = ""; + bot_cmd_parm_type[BOT_CMD_NULL] = BOT_CMD_PARAMETER_NONE; + + bot_cmd_string[BOT_CMD_PAUSE] = "pause"; + bot_cmd_parm_type[BOT_CMD_PAUSE] = BOT_CMD_PARAMETER_NONE; + + bot_cmd_string[BOT_CMD_CONTINUE] = "continue"; + bot_cmd_parm_type[BOT_CMD_CONTINUE] = BOT_CMD_PARAMETER_NONE; + + bot_cmd_string[BOT_CMD_WAIT] = "wait"; + bot_cmd_parm_type[BOT_CMD_WAIT] = BOT_CMD_PARAMETER_FLOAT; + + bot_cmd_string[BOT_CMD_TURN] = "turn"; + bot_cmd_parm_type[BOT_CMD_TURN] = BOT_CMD_PARAMETER_FLOAT; + + bot_cmd_string[BOT_CMD_MOVETO] = "moveto"; + bot_cmd_parm_type[BOT_CMD_MOVETO] = BOT_CMD_PARAMETER_VECTOR; + + bot_cmd_string[BOT_CMD_MOVETOTARGET] = "movetotarget"; + bot_cmd_parm_type[BOT_CMD_MOVETOTARGET] = BOT_CMD_PARAMETER_STRING; + + bot_cmd_string[BOT_CMD_RESETGOAL] = "resetgoal"; + bot_cmd_parm_type[BOT_CMD_RESETGOAL] = BOT_CMD_PARAMETER_NONE; + + bot_cmd_string[BOT_CMD_CC] = "cc"; + bot_cmd_parm_type[BOT_CMD_CC] = BOT_CMD_PARAMETER_STRING; + + bot_cmd_string[BOT_CMD_IF] = "if"; + bot_cmd_parm_type[BOT_CMD_IF] = BOT_CMD_PARAMETER_STRING; + + bot_cmd_string[BOT_CMD_ELSE] = "else"; + bot_cmd_parm_type[BOT_CMD_ELSE] = BOT_CMD_PARAMETER_NONE; + + bot_cmd_string[BOT_CMD_FI] = "fi"; + bot_cmd_parm_type[BOT_CMD_FI] = BOT_CMD_PARAMETER_NONE; + + bot_cmd_string[BOT_CMD_RESETAIM] = "resetaim"; + bot_cmd_parm_type[BOT_CMD_RESETAIM] = BOT_CMD_PARAMETER_NONE; + + bot_cmd_string[BOT_CMD_AIM] = "aim"; + bot_cmd_parm_type[BOT_CMD_AIM] = BOT_CMD_PARAMETER_STRING; + + bot_cmd_string[BOT_CMD_AIMTARGET] = "aimtarget"; + bot_cmd_parm_type[BOT_CMD_AIMTARGET] = BOT_CMD_PARAMETER_STRING; + + bot_cmd_string[BOT_CMD_PRESSKEY] = "presskey"; + bot_cmd_parm_type[BOT_CMD_PRESSKEY] = BOT_CMD_PARAMETER_STRING; + + bot_cmd_string[BOT_CMD_RELEASEKEY] = "releasekey"; + bot_cmd_parm_type[BOT_CMD_RELEASEKEY] = BOT_CMD_PARAMETER_STRING; + + bot_cmd_string[BOT_CMD_SELECTWEAPON] = "selectweapon"; + bot_cmd_parm_type[BOT_CMD_SELECTWEAPON] = BOT_CMD_PARAMETER_FLOAT; + + bot_cmd_string[BOT_CMD_IMPULSE] = "impulse"; + bot_cmd_parm_type[BOT_CMD_IMPULSE] = BOT_CMD_PARAMETER_FLOAT; + + bot_cmd_string[BOT_CMD_WAIT_UNTIL] = "wait_until"; + bot_cmd_parm_type[BOT_CMD_WAIT_UNTIL] = BOT_CMD_PARAMETER_FLOAT; + + bot_cmd_string[BOT_CMD_BARRIER] = "barrier"; + bot_cmd_parm_type[BOT_CMD_BARRIER] = BOT_CMD_PARAMETER_NONE; + + bot_cmd_string[BOT_CMD_CONSOLE] = "console"; + bot_cmd_parm_type[BOT_CMD_CONSOLE] = BOT_CMD_PARAMETER_STRING; + + bot_cmd_string[BOT_CMD_SOUND] = "sound"; + bot_cmd_parm_type[BOT_CMD_SOUND] = BOT_CMD_PARAMETER_STRING; + + bot_cmd_string[BOT_CMD_DEBUG_ASSERT_CANFIRE] = "debug_assert_canfire"; + bot_cmd_parm_type[BOT_CMD_DEBUG_ASSERT_CANFIRE] = BOT_CMD_PARAMETER_FLOAT; + + bot_cmds_initialized = true; +} + +// Returns first bot with matching name +entity find_bot_by_name(string name) +{ + entity bot; + + bot = findchainflags(flags, FL_CLIENT); + while (bot) + { + if(IS_BOT_CLIENT(bot)) + if(bot.netname==name) + return bot; + + bot = bot.chain; + } + + return NULL; +} + +// Returns a bot by number on list +entity find_bot_by_number(float number) +{ + entity bot; + float c = 0; + + if(!number) + return NULL; + + bot = findchainflags(flags, FL_CLIENT); // TODO: doesn't findchainflags loop backwards through entities? + while (bot) + { + if(IS_BOT_CLIENT(bot)) + { + if(++c==number) + return bot; + } + bot = bot.chain; + } + + return NULL; +} + +float bot_decodecommand(string cmdstring) +{ + float cmd_parm_type; + float sp; + string parm; + + sp = strstrofs(cmdstring, " ", 0); + if(sp < 0) + { + parm = ""; + } + else + { + parm = substring(cmdstring, sp + 1, -1); + cmdstring = substring(cmdstring, 0, sp); + } + + if(!bot_cmds_initialized) + bot_commands_init(); + + int i; + for(i=1;i \n")); + + LOG_INFO("Description: "); + switch(i) + { + case BOT_CMD_PAUSE: + LOG_INFO("Stops the bot completely. Any command other than 'continue' will be ignored."); + break; + case BOT_CMD_CONTINUE: + LOG_INFO("Disable paused status"); + break; + case BOT_CMD_WAIT: + LOG_INFO("Pause command parsing and bot ai for N seconds. Pressed key will remain pressed"); + break; + case BOT_CMD_WAIT_UNTIL: + LOG_INFO("Pause command parsing and bot ai until time is N from the last barrier. Pressed key will remain pressed"); + break; + case BOT_CMD_BARRIER: + LOG_INFO("Waits till all bots that have a command queue reach this command. Pressed key will remain pressed"); + break; + case BOT_CMD_TURN: + LOG_INFO("Look to the right or left N degrees. For turning to the left use positive numbers."); + break; + case BOT_CMD_MOVETO: + LOG_INFO("Walk to an specific coordinate on the map. Usage: moveto \"x y z\""); + break; + case BOT_CMD_MOVETOTARGET: + LOG_INFO("Walk to the specific target on the map"); + break; + case BOT_CMD_RESETGOAL: + LOG_INFO("Resets the goal stack"); + break; + case BOT_CMD_CC: + LOG_INFO("Execute client command. Examples: cc \"say something\"; cc god; cc \"name newnickname\"; cc kill;"); + break; + case BOT_CMD_IF: + LOG_INFO("Perform simple conditional execution.\n"); + LOG_INFO("Syntax: \n"); + LOG_INFO(" sv_cmd .. if \"condition\"\n"); + LOG_INFO(" sv_cmd .. \n"); + LOG_INFO(" sv_cmd .. \n"); + LOG_INFO(" sv_cmd .. else\n"); + LOG_INFO(" sv_cmd .. \n"); + LOG_INFO(" sv_cmd .. \n"); + LOG_INFO(" sv_cmd .. fi\n"); + LOG_INFO("Conditions: a=b, a>b, a50; if health>cvar.g_balance_laser_primary_damage; if flagcarrier;"); + break; + case BOT_CMD_RESETAIM: + LOG_INFO("Points the aim to the coordinates x,y 0,0"); + break; + case BOT_CMD_AIM: + LOG_INFO("Move the aim x/y (horizontal/vertical) degrees relatives to the bot\n"); + LOG_INFO("There is a 3rd optional parameter telling in how many seconds the aim has to reach the new position\n"); + LOG_INFO("Examples: aim \"90 0\" // Turn 90 degrees inmediately (positive numbers move to the left/up)\n"); + LOG_INFO(" aim \"0 90 2\" // Will gradually look to the sky in the next two seconds"); + break; + case BOT_CMD_AIMTARGET: + LOG_INFO("Points the aim to given target"); + break; + case BOT_CMD_PRESSKEY: + LOG_INFO("Press one of the following keys: forward, backward, left, right, jump, crouch, attack1, attack2, use\n"); + LOG_INFO("Multiple keys can be pressed at time (with many presskey calls) and it will remain pressed until the command \"releasekey\" is called"); + LOG_INFO("Note: The script will not return the control to the bot ai until all keys are released"); + break; + case BOT_CMD_RELEASEKEY: + LOG_INFO("Release previoulsy used keys. Use the parameter \"all\" to release all keys"); + break; + case BOT_CMD_SOUND: + LOG_INFO("play sound file at bot location"); + break; + case BOT_CMD_DEBUG_ASSERT_CANFIRE: + LOG_INFO("verify the state of the weapon entity"); + break; + default: + LOG_INFO("This command has no description yet."); + break; + } + LOG_INFO("\n"); + } +} + +void bot_list_commands() +{ + int i; + string ptype; + + if(!bot_cmds_initialized) + bot_commands_init(); + + LOG_INFO("List of all available commands:\n"); + LOG_INFO(" Command - Parameter Type\n"); + + for(i=1;i \n")); + } +} + +// Commands code +.int bot_exec_status; + +float bot_cmd_cc(entity this) +{ + SV_ParseClientCommand(this, bot_cmd.bot_cmd_parm_string); + return CMD_STATUS_FINISHED; +} + +float bot_cmd_impulse(entity this) +{ + this.impulse = bot_cmd.bot_cmd_parm_float; + return CMD_STATUS_FINISHED; +} + +float bot_cmd_continue(entity this) +{ + this.bot_exec_status &= ~BOT_EXEC_STATUS_PAUSED; + return CMD_STATUS_FINISHED; +} + +.float bot_cmd_wait_time; +float bot_cmd_wait(entity this) +{ + if(this.bot_exec_status & BOT_EXEC_STATUS_WAITING) + { + if(time>=this.bot_cmd_wait_time) + { + this.bot_exec_status &= ~BOT_EXEC_STATUS_WAITING; + return CMD_STATUS_FINISHED; + } + else + return CMD_STATUS_EXECUTING; + } + + this.bot_cmd_wait_time = time + bot_cmd.bot_cmd_parm_float; + this.bot_exec_status |= BOT_EXEC_STATUS_WAITING; + return CMD_STATUS_EXECUTING; +} + +float bot_cmd_wait_until(entity this) +{ + if(time < bot_cmd.bot_cmd_parm_float + bot_barriertime) + { + this.bot_exec_status |= BOT_EXEC_STATUS_WAITING; + return CMD_STATUS_EXECUTING; + } + this.bot_exec_status &= ~BOT_EXEC_STATUS_WAITING; + return CMD_STATUS_FINISHED; +} + +float bot_cmd_barrier(entity this) +{ + // 0 = no barrier, 1 = waiting, 2 = waiting finished + + if(this.bot_barrier == 0) // initialization + { + this.bot_barrier = 1; + + //this.colormod = '4 4 0'; + } + + if(this.bot_barrier == 1) // find other bots + { + FOREACH_CLIENT(it.isbot, LAMBDA( + if(it.bot_cmdqueuebuf_allocated) + if(it.bot_barrier != 1) + return CMD_STATUS_EXECUTING; // not all are at the barrier yet + )); + + // all bots hit the barrier! + + // acknowledge barrier + FOREACH_CLIENT(it.isbot, LAMBDA(it.bot_barrier = 2)); + + bot_barriertime = time; + } + + // if we get here, the barrier is finished + // so end it... + this.bot_barrier = 0; + //this.colormod = '0 0 0'; + + return CMD_STATUS_FINISHED; +} + +float bot_cmd_turn(entity this) +{ + this.v_angle_y = this.v_angle.y + bot_cmd.bot_cmd_parm_float; + this.v_angle_y = this.v_angle.y - floor(this.v_angle.y / 360) * 360; + return CMD_STATUS_FINISHED; +} + +float bot_cmd_select_weapon(entity this) +{ + float id = bot_cmd.bot_cmd_parm_float; + + if(id < WEP_FIRST || id > WEP_LAST) + return CMD_STATUS_ERROR; + + if(client_hasweapon(this, Weapons_from(id), true, false)) + PS(this).m_switchweapon = Weapons_from(id); + else + return CMD_STATUS_ERROR; + + return CMD_STATUS_FINISHED; +} + +.int bot_cmd_condition_status; + +const int CMD_CONDITION_NONE = 0; +const int CMD_CONDITION_true = 1; +const int CMD_CONDITION_false = 2; +const int CMD_CONDITION_true_BLOCK = 4; +const int CMD_CONDITION_false_BLOCK = 8; + +float bot_cmd_eval(entity this, string expr) +{ + // Search for numbers + if(strstrofs("0123456789", substring(expr, 0, 1), 0) >= 0) + { + return stof(expr); + } + + // Search for cvars + if(substring(expr, 0, 5)=="cvar.") + { + return cvar(substring(expr, 5, strlen(expr))); + } + + // Search for fields + switch(expr) + { + case "health": + return this.health; + case "speed": + return vlen(this.velocity); + case "flagcarrier": + return ((this.flagcarried!=NULL)); + } + + LOG_INFO(strcat("ERROR: Unable to convert the expression '",expr,"' into a numeric value\n")); + return 0; +} + +float bot_cmd_if(entity this) +{ + string expr, val_a, val_b; + float cmpofs; + + if(this.bot_cmd_condition_status != CMD_CONDITION_NONE) + { + // Only one "if" block is allowed at time + LOG_INFO("ERROR: Only one conditional block can be processed at time"); + bot_clearqueue(this); + return CMD_STATUS_ERROR; + } + + this.bot_cmd_condition_status |= CMD_CONDITION_true_BLOCK; + + // search for operators + expr = bot_cmd.bot_cmd_parm_string; + + cmpofs = strstrofs(expr,"=",0); + + if(cmpofs>0) + { + val_a = substring(expr,0,cmpofs); + val_b = substring(expr,cmpofs+1,strlen(expr)); + + if(bot_cmd_eval(this, val_a)==bot_cmd_eval(this, val_b)) + this.bot_cmd_condition_status |= CMD_CONDITION_true; + else + this.bot_cmd_condition_status |= CMD_CONDITION_false; + + return CMD_STATUS_FINISHED; + } + + cmpofs = strstrofs(expr,">",0); + + if(cmpofs>0) + { + val_a = substring(expr,0,cmpofs); + val_b = substring(expr,cmpofs+1,strlen(expr)); + + if(bot_cmd_eval(this, val_a)>bot_cmd_eval(this, val_b)) + this.bot_cmd_condition_status |= CMD_CONDITION_true; + else + this.bot_cmd_condition_status |= CMD_CONDITION_false; + + return CMD_STATUS_FINISHED; + } + + cmpofs = strstrofs(expr,"<",0); + + if(cmpofs>0) + { + val_a = substring(expr,0,cmpofs); + val_b = substring(expr,cmpofs+1,strlen(expr)); + + if(bot_cmd_eval(this, val_a)=this.bot_cmd_aim_endtime) + { + this.bot_cmd_aim_endtime = 0; + return CMD_STATUS_FINISHED; + } + else + return CMD_STATUS_EXECUTING; + } + + // New aiming direction + string parms; + float tokens, step; + + parms = bot_cmd.bot_cmd_parm_string; + + tokens = tokenizebyseparator(parms, " "); + + if(tokens<2||tokens>3) + return CMD_STATUS_ERROR; + + step = (tokens == 3) ? stof(argv(2)) : 0; + + if(step == 0) + { + this.v_angle_x -= stof(argv(1)); + this.v_angle_y += stof(argv(0)); + return CMD_STATUS_FINISHED; + } + + this.bot_cmd_aim_begin = this.v_angle; + + this.bot_cmd_aim_end_x = this.v_angle.x - stof(argv(1)); + this.bot_cmd_aim_end_y = this.v_angle.y + stof(argv(0)); + this.bot_cmd_aim_end_z = 0; + + this.bot_cmd_aim_begintime = time; + this.bot_cmd_aim_endtime = time + step; + + return CMD_STATUS_EXECUTING; +} + +float bot_cmd_aimtarget(entity this) +{ + if(this.bot_cmd_aim_endtime) + { + return bot_cmd_aim(this); + } + + entity e; + string parms; + vector v; + float tokens, step; + + parms = bot_cmd.bot_cmd_parm_string; + + tokens = tokenizebyseparator(parms, " "); + + e = bot_getplace(this, argv(0)); + if(!e) + return CMD_STATUS_ERROR; + + v = e.origin + (e.mins + e.maxs) * 0.5; + + if(tokens==1) + { + this.v_angle = vectoangles(v - (this.origin + this.view_ofs)); + this.v_angle_x = -this.v_angle.x; + return CMD_STATUS_FINISHED; + } + + if(tokens<1||tokens>2) + return CMD_STATUS_ERROR; + + step = stof(argv(1)); + + this.bot_cmd_aim_begin = this.v_angle; + this.bot_cmd_aim_end = vectoangles(v - (this.origin + this.view_ofs)); + this.bot_cmd_aim_end_x = -this.bot_cmd_aim_end.x; + + this.bot_cmd_aim_begintime = time; + this.bot_cmd_aim_endtime = time + step; + + return CMD_STATUS_EXECUTING; +} + +.int bot_cmd_keys; + +const int BOT_CMD_KEY_NONE = 0; +const int BOT_CMD_KEY_FORWARD = BIT(0); +const int BOT_CMD_KEY_BACKWARD = BIT(1); +const int BOT_CMD_KEY_RIGHT = BIT(2); +const int BOT_CMD_KEY_LEFT = BIT(3); +const int BOT_CMD_KEY_JUMP = BIT(4); +const int BOT_CMD_KEY_ATTACK1 = BIT(5); +const int BOT_CMD_KEY_ATTACK2 = BIT(6); +const int BOT_CMD_KEY_USE = BIT(7); +const int BOT_CMD_KEY_HOOK = BIT(8); +const int BOT_CMD_KEY_CROUCH = BIT(9); +const int BOT_CMD_KEY_CHAT = BIT(10); + +bool bot_presskeys(entity this) +{ + this.movement = '0 0 0'; + PHYS_INPUT_BUTTON_JUMP(this) = false; + PHYS_INPUT_BUTTON_CROUCH(this) = false; + PHYS_INPUT_BUTTON_ATCK(this) = false; + PHYS_INPUT_BUTTON_ATCK2(this) = false; + PHYS_INPUT_BUTTON_USE(this) = false; + PHYS_INPUT_BUTTON_HOOK(this) = false; + PHYS_INPUT_BUTTON_CHAT(this) = false; + + if(this.bot_cmd_keys == BOT_CMD_KEY_NONE) + return false; + + if(this.bot_cmd_keys & BOT_CMD_KEY_FORWARD) + this.movement_x = autocvar_sv_maxspeed; + else if(this.bot_cmd_keys & BOT_CMD_KEY_BACKWARD) + this.movement_x = -autocvar_sv_maxspeed; + + if(this.bot_cmd_keys & BOT_CMD_KEY_RIGHT) + this.movement_y = autocvar_sv_maxspeed; + else if(this.bot_cmd_keys & BOT_CMD_KEY_LEFT) + this.movement_y = -autocvar_sv_maxspeed; + + if(this.bot_cmd_keys & BOT_CMD_KEY_JUMP) + PHYS_INPUT_BUTTON_JUMP(this) = true; + + if(this.bot_cmd_keys & BOT_CMD_KEY_CROUCH) + PHYS_INPUT_BUTTON_CROUCH(this) = true; + + if(this.bot_cmd_keys & BOT_CMD_KEY_ATTACK1) + PHYS_INPUT_BUTTON_ATCK(this) = true; + + if(this.bot_cmd_keys & BOT_CMD_KEY_ATTACK2) + PHYS_INPUT_BUTTON_ATCK2(this) = true; + + if(this.bot_cmd_keys & BOT_CMD_KEY_USE) + PHYS_INPUT_BUTTON_USE(this) = true; + + if(this.bot_cmd_keys & BOT_CMD_KEY_HOOK) + PHYS_INPUT_BUTTON_HOOK(this) = true; + + if(this.bot_cmd_keys & BOT_CMD_KEY_CHAT) + PHYS_INPUT_BUTTON_CHAT(this) = true; + + return true; +} + + +float bot_cmd_keypress_handler(entity this, string key, float enabled) +{ + switch(key) + { + case "all": + if(enabled) + this.bot_cmd_keys = power2of(20) - 1; // >:) + else + this.bot_cmd_keys = BOT_CMD_KEY_NONE; + case "forward": + if(enabled) + { + this.bot_cmd_keys |= BOT_CMD_KEY_FORWARD; + this.bot_cmd_keys &= ~BOT_CMD_KEY_BACKWARD; + } + else + this.bot_cmd_keys &= ~BOT_CMD_KEY_FORWARD; + break; + case "backward": + if(enabled) + { + this.bot_cmd_keys |= BOT_CMD_KEY_BACKWARD; + this.bot_cmd_keys &= ~BOT_CMD_KEY_FORWARD; + } + else + this.bot_cmd_keys &= ~BOT_CMD_KEY_BACKWARD; + break; + case "left": + if(enabled) + { + this.bot_cmd_keys |= BOT_CMD_KEY_LEFT; + this.bot_cmd_keys &= ~BOT_CMD_KEY_RIGHT; + } + else + this.bot_cmd_keys &= ~BOT_CMD_KEY_LEFT; + break; + case "right": + if(enabled) + { + this.bot_cmd_keys |= BOT_CMD_KEY_RIGHT; + this.bot_cmd_keys &= ~BOT_CMD_KEY_LEFT; + } + else + this.bot_cmd_keys &= ~BOT_CMD_KEY_RIGHT; + break; + case "jump": + if(enabled) + this.bot_cmd_keys |= BOT_CMD_KEY_JUMP; + else + this.bot_cmd_keys &= ~BOT_CMD_KEY_JUMP; + break; + case "crouch": + if(enabled) + this.bot_cmd_keys |= BOT_CMD_KEY_CROUCH; + else + this.bot_cmd_keys &= ~BOT_CMD_KEY_CROUCH; + break; + case "attack1": + if(enabled) + this.bot_cmd_keys |= BOT_CMD_KEY_ATTACK1; + else + this.bot_cmd_keys &= ~BOT_CMD_KEY_ATTACK1; + break; + case "attack2": + if(enabled) + this.bot_cmd_keys |= BOT_CMD_KEY_ATTACK2; + else + this.bot_cmd_keys &= ~BOT_CMD_KEY_ATTACK2; + break; + case "use": + if(enabled) + this.bot_cmd_keys |= BOT_CMD_KEY_USE; + else + this.bot_cmd_keys &= ~BOT_CMD_KEY_USE; + break; + case "hook": + if(enabled) + this.bot_cmd_keys |= BOT_CMD_KEY_HOOK; + else + this.bot_cmd_keys &= ~BOT_CMD_KEY_HOOK; + break; + case "chat": + if(enabled) + this.bot_cmd_keys |= BOT_CMD_KEY_CHAT; + else + this.bot_cmd_keys &= ~BOT_CMD_KEY_CHAT; + break; + default: + break; + } + + return CMD_STATUS_FINISHED; +} + +float bot_cmd_presskey(entity this) +{ + string key; + + key = bot_cmd.bot_cmd_parm_string; + + bot_cmd_keypress_handler(this, key,true); + + return CMD_STATUS_FINISHED; +} + +float bot_cmd_releasekey(entity this) +{ + string key; + + key = bot_cmd.bot_cmd_parm_string; + + return bot_cmd_keypress_handler(this, key,false); +} + +float bot_cmd_pause(entity this) +{ + PHYS_INPUT_BUTTON_DRAG(this) = false; + PHYS_INPUT_BUTTON_USE(this) = false; + PHYS_INPUT_BUTTON_ATCK(this) = false; + PHYS_INPUT_BUTTON_JUMP(this) = false; + PHYS_INPUT_BUTTON_HOOK(this) = false; + PHYS_INPUT_BUTTON_CHAT(this) = false; + PHYS_INPUT_BUTTON_ATCK2(this) = false; + PHYS_INPUT_BUTTON_CROUCH(this) = false; + + this.movement = '0 0 0'; + this.bot_cmd_keys = BOT_CMD_KEY_NONE; + + this.bot_exec_status |= BOT_EXEC_STATUS_PAUSED; + return CMD_STATUS_FINISHED; +} + +float bot_cmd_moveto(entity this) +{ + return this.cmd_moveto(this, bot_cmd.bot_cmd_parm_vector); +} + +float bot_cmd_movetotarget(entity this) +{ + entity e; + e = bot_getplace(this, bot_cmd.bot_cmd_parm_string); + if(!e) + return CMD_STATUS_ERROR; + return this.cmd_moveto(this, e.origin + (e.mins + e.maxs) * 0.5); +} + +float bot_cmd_resetgoal(entity this) +{ + return this.cmd_resetgoal(this); +} + + +float bot_cmd_sound(entity this) +{ + string f; + f = bot_cmd.bot_cmd_parm_string; + + float n = tokenizebyseparator(f, " "); + + string sample = f; + float chan = CH_WEAPON_B; + float vol = VOL_BASE; + float atten = ATTEN_MIN; + + if(n >= 1) + sample = argv(n - 1); + if(n >= 2) + chan = stof(argv(0)); + if(n >= 3) + vol = stof(argv(1)); + if(n >= 4) + atten = stof(argv(2)); + + precache_sound(f); + _sound(this, chan, sample, vol, atten); + + return CMD_STATUS_FINISHED; +} + +.entity tuba_note; +float bot_cmd_debug_assert_canfire(entity this) +{ + float f = bot_cmd.bot_cmd_parm_float; + + int slot = 0; + .entity weaponentity = weaponentities[slot]; + if(this.(weaponentity).state != WS_READY) + { + if(f) + { + this.colormod = '0 8 8'; + LOG_INFO("Bot ", this.netname, " using ", this.weaponname, " wants to fire, inhibited by weaponentity state\n"); + } + } + else if(ATTACK_FINISHED(this, slot) > time) + { + if(f) + { + this.colormod = '8 0 8'; + LOG_INFO("Bot ", this.netname, " using ", this.weaponname, " wants to fire, inhibited by ATTACK_FINISHED (", ftos(ATTACK_FINISHED(this, slot) - time), " seconds left)\n"); + } + } + else if(this.tuba_note) + { + if(f) + { + this.colormod = '8 0 0'; + LOG_INFO("Bot ", this.netname, " using ", this.weaponname, " wants to fire, bot still has an active tuba note\n"); + } + } + else + { + if(!f) + { + this.colormod = '8 8 0'; + LOG_INFO("Bot ", this.netname, " using ", this.weaponname, " thinks it has fired, but apparently did not; ATTACK_FINISHED says ", ftos(ATTACK_FINISHED(this, slot) - time), " seconds left\n"); + } + } + + return CMD_STATUS_FINISHED; +} + +// + +void bot_command_executed(entity this, bool rm) +{ + entity cmd; + + cmd = bot_cmd; + + if(rm) + bot_dequeuecommand(this, this.bot_cmd_execution_index); + + this.bot_cmd_execution_index++; +} + +void bot_setcurrentcommand(entity this) +{ + bot_cmd = NULL; + + if(!this.bot_cmd_current) + { + this.bot_cmd_current = new_pure(bot_cmd); + this.bot_cmd_current.is_bot_cmd = true; + } + + bot_cmd = this.bot_cmd_current; + if(bot_cmd.bot_cmd_index != this.bot_cmd_execution_index || this.bot_cmd_execution_index == 0) + { + if(bot_havecommand(this, this.bot_cmd_execution_index)) + { + string cmdstring; + cmdstring = bot_readcommand(this, this.bot_cmd_execution_index); + if(bot_decodecommand(cmdstring)) + { + bot_cmd.owner = this; + bot_cmd.bot_cmd_index = this.bot_cmd_execution_index; + } + else + { + // Invalid command, remove from queue + bot_cmd = NULL; + bot_dequeuecommand(this, this.bot_cmd_execution_index); + this.bot_cmd_execution_index++; + } + } + else + bot_cmd = NULL; + } +} + +void bot_resetqueues() +{ + FOREACH_CLIENT(it.isbot, LAMBDA( + it.bot_cmd_execution_index = 0; + bot_clearqueue(it); + // also, cancel all barriers + it.bot_barrier = 0; + for(int i = 0; i < it.bot_places_count; ++i) + { + strunzone(it.(bot_placenames[i])); + it.(bot_placenames[i]) = string_null; + } + it.bot_places_count = 0; + )); + + bot_barriertime = time; +} + +// Here we map commands to functions and deal with complex interactions between commands and execution states +// NOTE: Of course you need to include your commands here too :) +float bot_execute_commands_once(entity this) +{ + float status, ispressingkey; + + // Find command + bot_setcurrentcommand(this); + + // if we have no bot command, better return + // old logic kept pressing previously pressed keys, but that has problems + // (namely, it means you cannot make a bot "normal" ever again) + // to keep a bot walking for a while, use the "wait" bot command + if(bot_cmd == NULL) + return false; + + // Ignore all commands except continue when the bot is paused + if(this.bot_exec_status & BOT_EXEC_STATUS_PAUSED) + if(bot_cmd.bot_cmd_type!=BOT_CMD_CONTINUE) + { + if(bot_cmd.bot_cmd_type!=BOT_CMD_NULL) + { + bot_command_executed(this, true); + LOG_INFO( "WARNING: Commands are ignored while the bot is paused. Use the command 'continue' instead.\n"); + } + return 1; + } + + // Keep pressing keys raised by the "presskey" command + ispressingkey = boolean(bot_presskeys(this)); + + // Handle conditions + if (!(bot_cmd.bot_cmd_type==BOT_CMD_FI||bot_cmd.bot_cmd_type==BOT_CMD_ELSE)) + if(this.bot_cmd_condition_status & CMD_CONDITION_true && this.bot_cmd_condition_status & CMD_CONDITION_false_BLOCK) + { + bot_command_executed(this, true); + return -1; + } + else if(this.bot_cmd_condition_status & CMD_CONDITION_false && this.bot_cmd_condition_status & CMD_CONDITION_true_BLOCK) + { + bot_command_executed(this, true); + return -1; + } + + // Map commands to functions + switch(bot_cmd.bot_cmd_type) + { + case BOT_CMD_NULL: + return ispressingkey; + //break; + case BOT_CMD_PAUSE: + status = bot_cmd_pause(this); + break; + case BOT_CMD_CONTINUE: + status = bot_cmd_continue(this); + break; + case BOT_CMD_WAIT: + status = bot_cmd_wait(this); + break; + case BOT_CMD_WAIT_UNTIL: + status = bot_cmd_wait_until(this); + break; + case BOT_CMD_TURN: + status = bot_cmd_turn(this); + break; + case BOT_CMD_MOVETO: + status = bot_cmd_moveto(this); + break; + case BOT_CMD_MOVETOTARGET: + status = bot_cmd_movetotarget(this); + break; + case BOT_CMD_RESETGOAL: + status = bot_cmd_resetgoal(this); + break; + case BOT_CMD_CC: + status = bot_cmd_cc(this); + break; + case BOT_CMD_IF: + status = bot_cmd_if(this); + break; + case BOT_CMD_ELSE: + status = bot_cmd_else(this); + break; + case BOT_CMD_FI: + status = bot_cmd_fi(this); + break; + case BOT_CMD_RESETAIM: + status = bot_cmd_resetaim(this); + break; + case BOT_CMD_AIM: + status = bot_cmd_aim(this); + break; + case BOT_CMD_AIMTARGET: + status = bot_cmd_aimtarget(this); + break; + case BOT_CMD_PRESSKEY: + status = bot_cmd_presskey(this); + break; + case BOT_CMD_RELEASEKEY: + status = bot_cmd_releasekey(this); + break; + case BOT_CMD_SELECTWEAPON: + status = bot_cmd_select_weapon(this); + break; + case BOT_CMD_IMPULSE: + status = bot_cmd_impulse(this); + break; + case BOT_CMD_BARRIER: + status = bot_cmd_barrier(this); + break; + case BOT_CMD_CONSOLE: + localcmd(strcat(bot_cmd.bot_cmd_parm_string, "\n")); + status = CMD_STATUS_FINISHED; + break; + case BOT_CMD_SOUND: + status = bot_cmd_sound(this); + break; + case BOT_CMD_DEBUG_ASSERT_CANFIRE: + status = bot_cmd_debug_assert_canfire(this); + break; + default: + LOG_INFO(strcat("ERROR: Invalid command on queue with id '",ftos(bot_cmd.bot_cmd_type),"'\n")); + return 0; + } + + if (status==CMD_STATUS_ERROR) + LOG_INFO(strcat("ERROR: The command '",bot_cmd_string[bot_cmd.bot_cmd_type],"' returned an error status\n")); + + // Move execution pointer + if(status==CMD_STATUS_EXECUTING) + { + return 1; + } + else + { + if(autocvar_g_debug_bot_commands) + { + string parms; + + switch(bot_cmd_parm_type[bot_cmd.bot_cmd_type]) + { + case BOT_CMD_PARAMETER_FLOAT: + parms = ftos(bot_cmd.bot_cmd_parm_float); + break; + case BOT_CMD_PARAMETER_STRING: + parms = bot_cmd.bot_cmd_parm_string; + break; + case BOT_CMD_PARAMETER_VECTOR: + parms = vtos(bot_cmd.bot_cmd_parm_vector); + break; + default: + parms = ""; + break; + } + clientcommand(this,strcat("say ^7", bot_cmd_string[bot_cmd.bot_cmd_type]," ",parms,"\n")); + } + + bot_command_executed(this, true); + } + + if(status == CMD_STATUS_FINISHED) + return -1; + + return CMD_STATUS_ERROR; +} + +// This function should be (the only) called directly from the bot ai loop +int bot_execute_commands(entity this) +{ + int f; + do + { + f = bot_execute_commands_once(this); + } + while(f < 0); + return f; +} diff --git a/qcsrc/server/bot/default/scripting.qh b/qcsrc/server/bot/default/scripting.qh new file mode 100644 index 000000000..cb6cd8731 --- /dev/null +++ b/qcsrc/server/bot/default/scripting.qh @@ -0,0 +1,81 @@ +#pragma once + +#define BOT_EXEC_STATUS_IDLE 0 +#define BOT_EXEC_STATUS_PAUSED 1 +#define BOT_EXEC_STATUS_WAITING 2 + +#define CMD_STATUS_EXECUTING 0 +#define CMD_STATUS_FINISHED 1 +#define CMD_STATUS_ERROR 2 + + +// NOTE: New commands should be added here. Do not forget to update BOT_CMD_COUNTER +const int BOT_CMD_NULL = 0; +const int BOT_CMD_PAUSE = 1; +const int BOT_CMD_CONTINUE = 2; +const int BOT_CMD_WAIT = 3; +const int BOT_CMD_TURN = 4; +const int BOT_CMD_MOVETO = 5; +const int BOT_CMD_RESETGOAL = 6; // Not implemented yet +const int BOT_CMD_CC = 7; +const int BOT_CMD_IF = 8; +const int BOT_CMD_ELSE = 9; +const int BOT_CMD_FI = 10; +const int BOT_CMD_RESETAIM = 11; +const int BOT_CMD_AIM = 12; +const int BOT_CMD_PRESSKEY = 13; +const int BOT_CMD_RELEASEKEY = 14; +const int BOT_CMD_SELECTWEAPON = 15; +const int BOT_CMD_IMPULSE = 16; +const int BOT_CMD_WAIT_UNTIL = 17; +const int BOT_CMD_MOVETOTARGET = 18; +const int BOT_CMD_AIMTARGET = 19; +const int BOT_CMD_BARRIER = 20; +const int BOT_CMD_CONSOLE = 21; +const int BOT_CMD_SOUND = 22; +const int BOT_CMD_DEBUG_ASSERT_CANFIRE = 23; +const int BOT_CMD_WHILE = 24; // TODO: Not implemented yet +const int BOT_CMD_WEND = 25; // TODO: Not implemented yet +const int BOT_CMD_CHASE = 26; // TODO: Not implemented yet + +const int BOT_CMD_COUNTER = 24; // Update this value if you add/remove a command + +// NOTE: Following commands should be implemented on the bot ai +// If a new command should be handled by the target ai(s) please declare it here +.float(entity, vector) cmd_moveto; +.float(entity) cmd_resetgoal; + +// +const int BOT_CMD_PARAMETER_NONE = 0; +const int BOT_CMD_PARAMETER_FLOAT = 1; +const int BOT_CMD_PARAMETER_STRING = 2; +const int BOT_CMD_PARAMETER_VECTOR = 3; + +float bot_cmds_initialized; +int bot_cmd_parm_type[BOT_CMD_COUNTER]; +string bot_cmd_string[BOT_CMD_COUNTER]; + +// Bots command queue +entity bot_cmd; // global current command +.entity bot_cmd_current; // current command of this bot + +.float is_bot_cmd; // Tells if the entity is a bot command +.float bot_cmd_index; // Position of the command in the queue +.int bot_cmd_type; // If of command (see the BOT_CMD_* defines) +.float bot_cmd_parm_float; // Field to store a float parameter +.string bot_cmd_parm_string; // Field to store a string parameter +.vector bot_cmd_parm_vector; // Field to store a vector parameter + +float bot_barriertime; +.float bot_barrier; + +.float bot_cmd_execution_index; // Position in the queue of the command to be executed + + +void bot_resetqueues(); +void bot_queuecommand(entity bot, string cmdstring); +void bot_cmdhelp(string scmd); +void bot_list_commands(); +float bot_execute_commands(entity this); +entity find_bot_by_name(string name); +entity find_bot_by_number(float number); diff --git a/qcsrc/server/bot/default/waypoints.qc b/qcsrc/server/bot/default/waypoints.qc new file mode 100644 index 000000000..84995d1c7 --- /dev/null +++ b/qcsrc/server/bot/default/waypoints.qc @@ -0,0 +1,1104 @@ +#include "waypoints.qh" + +#include "cvars.qh" + +#include "bot.qh" +#include "navigation.qh" + +#include + +#include "../../antilag.qh" + +#include + +#include +#include + +// create a new spawnfunc_waypoint and automatically link it to other waypoints, and link +// them back to it as well +// (suitable for spawnfunc_waypoint editor) +entity waypoint_spawn(vector m1, vector m2, float f) +{ + if(!(f & WAYPOINTFLAG_PERSONAL)) + { + IL_EACH(g_waypoints, boxesoverlap(m1, m2, it.absmin, it.absmax), + { + return it; + }); + } + + entity w = new(waypoint); + IL_PUSH(g_waypoints, w); + w.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP; + w.wpflags = f; + w.solid = SOLID_TRIGGER; + setorigin(w, (m1 + m2) * 0.5); + setsize(w, m1 - w.origin, m2 - w.origin); + if (vlen(w.size) > 0) + w.wpisbox = true; + + if(!w.wpisbox) + { + setsize(w, STAT(PL_MIN, NULL) - '1 1 0', STAT(PL_MAX, NULL) + '1 1 0'); + if(!move_out_of_solid(w)) + { + if(!(f & WAYPOINTFLAG_GENERATED)) + { + LOG_TRACE("Killed a waypoint that was stuck in solid at ", vtos(w.origin), "\n"); + delete(w); + return NULL; + } + else + { + if(autocvar_developer) + { + LOG_INFO("A generated waypoint is stuck in solid at ", vtos(w.origin), "\n"); + backtrace("Waypoint stuck"); + } + } + } + setsize(w, '0 0 0', '0 0 0'); + } + + waypoint_clearlinks(w); + //waypoint_schedulerelink(w); + + if (autocvar_g_waypointeditor) + { + m1 = w.mins; + m2 = w.maxs; + setmodel(w, MDL_WAYPOINT); w.effects = EF_LOWPRECISION; + setsize(w, m1, m2); + if (w.wpflags & WAYPOINTFLAG_ITEM) + w.colormod = '1 0 0'; + else if (w.wpflags & WAYPOINTFLAG_GENERATED) + w.colormod = '1 1 0'; + else + w.colormod = '1 1 1'; + } + else + w.model = ""; + + return w; +} + +// add a new link to the spawnfunc_waypoint, replacing the furthest link it already has +void waypoint_addlink(entity from, entity to) +{ + float c; + + if (from == to) + return; + if (from.wpflags & WAYPOINTFLAG_NORELINK) + return; + + if (from.wp00 == to) return;if (from.wp01 == to) return;if (from.wp02 == to) return;if (from.wp03 == to) return; + if (from.wp04 == to) return;if (from.wp05 == to) return;if (from.wp06 == to) return;if (from.wp07 == to) return; + if (from.wp08 == to) return;if (from.wp09 == to) return;if (from.wp10 == to) return;if (from.wp11 == to) return; + if (from.wp12 == to) return;if (from.wp13 == to) return;if (from.wp14 == to) return;if (from.wp15 == to) return; + if (from.wp16 == to) return;if (from.wp17 == to) return;if (from.wp18 == to) return;if (from.wp19 == to) return; + if (from.wp20 == to) return;if (from.wp21 == to) return;if (from.wp22 == to) return;if (from.wp23 == to) return; + if (from.wp24 == to) return;if (from.wp25 == to) return;if (from.wp26 == to) return;if (from.wp27 == to) return; + if (from.wp28 == to) return;if (from.wp29 == to) return;if (from.wp30 == to) return;if (from.wp31 == to) return; + + if (to.wpisbox || from.wpisbox) + { + // if either is a box we have to find the nearest points on them to + // calculate the distance properly + vector v1, v2, m1, m2; + v1 = from.origin; + m1 = to.absmin; + m2 = to.absmax; + v1_x = bound(m1_x, v1_x, m2_x); + v1_y = bound(m1_y, v1_y, m2_y); + v1_z = bound(m1_z, v1_z, m2_z); + v2 = to.origin; + m1 = from.absmin; + m2 = from.absmax; + v2_x = bound(m1_x, v2_x, m2_x); + v2_y = bound(m1_y, v2_y, m2_y); + v2_z = bound(m1_z, v2_z, m2_z); + v2 = to.origin; + c = vlen(v2 - v1); + } + else + c = vlen(to.origin - from.origin); + + if (from.wp31mincost < c) return; + if (from.wp30mincost < c) {from.wp31 = to;from.wp31mincost = c;return;} from.wp31 = from.wp30;from.wp31mincost = from.wp30mincost; + if (from.wp29mincost < c) {from.wp30 = to;from.wp30mincost = c;return;} from.wp30 = from.wp29;from.wp30mincost = from.wp29mincost; + if (from.wp28mincost < c) {from.wp29 = to;from.wp29mincost = c;return;} from.wp29 = from.wp28;from.wp29mincost = from.wp28mincost; + if (from.wp27mincost < c) {from.wp28 = to;from.wp28mincost = c;return;} from.wp28 = from.wp27;from.wp28mincost = from.wp27mincost; + if (from.wp26mincost < c) {from.wp27 = to;from.wp27mincost = c;return;} from.wp27 = from.wp26;from.wp27mincost = from.wp26mincost; + if (from.wp25mincost < c) {from.wp26 = to;from.wp26mincost = c;return;} from.wp26 = from.wp25;from.wp26mincost = from.wp25mincost; + if (from.wp24mincost < c) {from.wp25 = to;from.wp25mincost = c;return;} from.wp25 = from.wp24;from.wp25mincost = from.wp24mincost; + if (from.wp23mincost < c) {from.wp24 = to;from.wp24mincost = c;return;} from.wp24 = from.wp23;from.wp24mincost = from.wp23mincost; + if (from.wp22mincost < c) {from.wp23 = to;from.wp23mincost = c;return;} from.wp23 = from.wp22;from.wp23mincost = from.wp22mincost; + if (from.wp21mincost < c) {from.wp22 = to;from.wp22mincost = c;return;} from.wp22 = from.wp21;from.wp22mincost = from.wp21mincost; + if (from.wp20mincost < c) {from.wp21 = to;from.wp21mincost = c;return;} from.wp21 = from.wp20;from.wp21mincost = from.wp20mincost; + if (from.wp19mincost < c) {from.wp20 = to;from.wp20mincost = c;return;} from.wp20 = from.wp19;from.wp20mincost = from.wp19mincost; + if (from.wp18mincost < c) {from.wp19 = to;from.wp19mincost = c;return;} from.wp19 = from.wp18;from.wp19mincost = from.wp18mincost; + if (from.wp17mincost < c) {from.wp18 = to;from.wp18mincost = c;return;} from.wp18 = from.wp17;from.wp18mincost = from.wp17mincost; + if (from.wp16mincost < c) {from.wp17 = to;from.wp17mincost = c;return;} from.wp17 = from.wp16;from.wp17mincost = from.wp16mincost; + if (from.wp15mincost < c) {from.wp16 = to;from.wp16mincost = c;return;} from.wp16 = from.wp15;from.wp16mincost = from.wp15mincost; + if (from.wp14mincost < c) {from.wp15 = to;from.wp15mincost = c;return;} from.wp15 = from.wp14;from.wp15mincost = from.wp14mincost; + if (from.wp13mincost < c) {from.wp14 = to;from.wp14mincost = c;return;} from.wp14 = from.wp13;from.wp14mincost = from.wp13mincost; + if (from.wp12mincost < c) {from.wp13 = to;from.wp13mincost = c;return;} from.wp13 = from.wp12;from.wp13mincost = from.wp12mincost; + if (from.wp11mincost < c) {from.wp12 = to;from.wp12mincost = c;return;} from.wp12 = from.wp11;from.wp12mincost = from.wp11mincost; + if (from.wp10mincost < c) {from.wp11 = to;from.wp11mincost = c;return;} from.wp11 = from.wp10;from.wp11mincost = from.wp10mincost; + if (from.wp09mincost < c) {from.wp10 = to;from.wp10mincost = c;return;} from.wp10 = from.wp09;from.wp10mincost = from.wp09mincost; + if (from.wp08mincost < c) {from.wp09 = to;from.wp09mincost = c;return;} from.wp09 = from.wp08;from.wp09mincost = from.wp08mincost; + if (from.wp07mincost < c) {from.wp08 = to;from.wp08mincost = c;return;} from.wp08 = from.wp07;from.wp08mincost = from.wp07mincost; + if (from.wp06mincost < c) {from.wp07 = to;from.wp07mincost = c;return;} from.wp07 = from.wp06;from.wp07mincost = from.wp06mincost; + if (from.wp05mincost < c) {from.wp06 = to;from.wp06mincost = c;return;} from.wp06 = from.wp05;from.wp06mincost = from.wp05mincost; + if (from.wp04mincost < c) {from.wp05 = to;from.wp05mincost = c;return;} from.wp05 = from.wp04;from.wp05mincost = from.wp04mincost; + if (from.wp03mincost < c) {from.wp04 = to;from.wp04mincost = c;return;} from.wp04 = from.wp03;from.wp04mincost = from.wp03mincost; + if (from.wp02mincost < c) {from.wp03 = to;from.wp03mincost = c;return;} from.wp03 = from.wp02;from.wp03mincost = from.wp02mincost; + if (from.wp01mincost < c) {from.wp02 = to;from.wp02mincost = c;return;} from.wp02 = from.wp01;from.wp02mincost = from.wp01mincost; + if (from.wp00mincost < c) {from.wp01 = to;from.wp01mincost = c;return;} from.wp01 = from.wp00;from.wp01mincost = from.wp00mincost; + from.wp00 = to;from.wp00mincost = c;return; +} + +// relink this spawnfunc_waypoint +// (precompile a list of all reachable waypoints from this spawnfunc_waypoint) +// (SLOW!) +void waypoint_think(entity this) +{ + vector sv, sm1, sm2, ev, em1, em2, dv; + + bot_calculate_stepheightvec(); + + bot_navigation_movemode = ((autocvar_bot_navigation_ignoreplayers) ? MOVE_NOMONSTERS : MOVE_NORMAL); + + //dprint("waypoint_think wpisbox = ", ftos(this.wpisbox), "\n"); + sm1 = this.origin + this.mins; + sm2 = this.origin + this.maxs; + IL_EACH(g_waypoints, true, + { + if (boxesoverlap(this.absmin, this.absmax, it.absmin, it.absmax)) + { + waypoint_addlink(this, it); + waypoint_addlink(it, this); + } + else + { + ++relink_total; + if(!checkpvs(this.origin, it)) + { + ++relink_pvsculled; + continue; + } + sv = it.origin; + sv.x = bound(sm1_x, sv.x, sm2_x); + sv.y = bound(sm1_y, sv.y, sm2_y); + sv.z = bound(sm1_z, sv.z, sm2_z); + ev = this.origin; + em1 = it.origin + it.mins; + em2 = it.origin + it.maxs; + ev.x = bound(em1_x, ev.x, em2_x); + ev.y = bound(em1_y, ev.y, em2_y); + ev.z = bound(em1_z, ev.z, em2_z); + dv = ev - sv; + dv.z = 0; + if(vdist(dv, >=, 1050)) // max search distance in XY + { + ++relink_lengthculled; + continue; + } + navigation_testtracewalk = 0; + if (!this.wpisbox) + { + tracebox(sv - STAT(PL_MIN, NULL).z * '0 0 1', STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), sv, false, this); + if (!trace_startsolid) + { + //dprint("sv deviation", vtos(trace_endpos - sv), "\n"); + sv = trace_endpos + '0 0 1'; + } + } + if (!it.wpisbox) + { + tracebox(ev - STAT(PL_MIN, NULL).z * '0 0 1', STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), ev, false, it); + if (!trace_startsolid) + { + //dprint("ev deviation", vtos(trace_endpos - ev), "\n"); + ev = trace_endpos + '0 0 1'; + } + } + //traceline(this.origin, it.origin, false, NULL); + //if (trace_fraction == 1) + if (!this.wpisbox && tracewalk(this, sv, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), ev, MOVE_NOMONSTERS)) + waypoint_addlink(this, it); + else + relink_walkculled += 0.5; + if (!it.wpisbox && tracewalk(it, ev, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), sv, MOVE_NOMONSTERS)) + waypoint_addlink(it, this); + else + relink_walkculled += 0.5; + } + }); + navigation_testtracewalk = 0; + this.wplinked = true; +} + +void waypoint_clearlinks(entity wp) +{ + // clear links to other waypoints + float f; + f = 10000000; + wp.wp00 = wp.wp01 = wp.wp02 = wp.wp03 = wp.wp04 = wp.wp05 = wp.wp06 = wp.wp07 = NULL; + wp.wp08 = wp.wp09 = wp.wp10 = wp.wp11 = wp.wp12 = wp.wp13 = wp.wp14 = wp.wp15 = NULL; + wp.wp16 = wp.wp17 = wp.wp18 = wp.wp19 = wp.wp20 = wp.wp21 = wp.wp22 = wp.wp23 = NULL; + wp.wp24 = wp.wp25 = wp.wp26 = wp.wp27 = wp.wp28 = wp.wp29 = wp.wp30 = wp.wp31 = NULL; + + wp.wp00mincost = wp.wp01mincost = wp.wp02mincost = wp.wp03mincost = wp.wp04mincost = wp.wp05mincost = wp.wp06mincost = wp.wp07mincost = f; + wp.wp08mincost = wp.wp09mincost = wp.wp10mincost = wp.wp11mincost = wp.wp12mincost = wp.wp13mincost = wp.wp14mincost = wp.wp15mincost = f; + wp.wp16mincost = wp.wp17mincost = wp.wp18mincost = wp.wp19mincost = wp.wp20mincost = wp.wp21mincost = wp.wp22mincost = wp.wp23mincost = f; + wp.wp24mincost = wp.wp25mincost = wp.wp26mincost = wp.wp27mincost = wp.wp28mincost = wp.wp29mincost = wp.wp30mincost = wp.wp31mincost = f; + + wp.wplinked = false; +} + +// tell a spawnfunc_waypoint to relink +void waypoint_schedulerelink(entity wp) +{ + if (wp == NULL) + return; + // TODO: add some sort of visible box in edit mode for box waypoints + if (autocvar_g_waypointeditor) + { + vector m1, m2; + m1 = wp.mins; + m2 = wp.maxs; + setmodel(wp, MDL_WAYPOINT); wp.effects = EF_LOWPRECISION; + setsize(wp, m1, m2); + if (wp.wpflags & WAYPOINTFLAG_ITEM) + wp.colormod = '1 0 0'; + else if (wp.wpflags & WAYPOINTFLAG_GENERATED) + wp.colormod = '1 1 0'; + else + wp.colormod = '1 1 1'; + } + else + wp.model = ""; + wp.wpisbox = vlen(wp.size) > 0; + wp.enemy = NULL; + if (!(wp.wpflags & WAYPOINTFLAG_PERSONAL)) + wp.owner = NULL; + if (!(wp.wpflags & WAYPOINTFLAG_NORELINK)) + waypoint_clearlinks(wp); + // schedule an actual relink on next frame + setthink(wp, waypoint_think); + wp.nextthink = time; + wp.effects = EF_LOWPRECISION; +} + +// spawnfunc_waypoint map entity +spawnfunc(waypoint) +{ + IL_PUSH(g_waypoints, this); + + setorigin(this, this.origin); + // schedule a relink after other waypoints have had a chance to spawn + waypoint_clearlinks(this); + //waypoint_schedulerelink(this); +} + +// remove a spawnfunc_waypoint, and schedule all neighbors to relink +void waypoint_remove(entity e) +{ + // tell all linked waypoints that they need to relink + waypoint_schedulerelink(e.wp00); + waypoint_schedulerelink(e.wp01); + waypoint_schedulerelink(e.wp02); + waypoint_schedulerelink(e.wp03); + waypoint_schedulerelink(e.wp04); + waypoint_schedulerelink(e.wp05); + waypoint_schedulerelink(e.wp06); + waypoint_schedulerelink(e.wp07); + waypoint_schedulerelink(e.wp08); + waypoint_schedulerelink(e.wp09); + waypoint_schedulerelink(e.wp10); + waypoint_schedulerelink(e.wp11); + waypoint_schedulerelink(e.wp12); + waypoint_schedulerelink(e.wp13); + waypoint_schedulerelink(e.wp14); + waypoint_schedulerelink(e.wp15); + waypoint_schedulerelink(e.wp16); + waypoint_schedulerelink(e.wp17); + waypoint_schedulerelink(e.wp18); + waypoint_schedulerelink(e.wp19); + waypoint_schedulerelink(e.wp20); + waypoint_schedulerelink(e.wp21); + waypoint_schedulerelink(e.wp22); + waypoint_schedulerelink(e.wp23); + waypoint_schedulerelink(e.wp24); + waypoint_schedulerelink(e.wp25); + waypoint_schedulerelink(e.wp26); + waypoint_schedulerelink(e.wp27); + waypoint_schedulerelink(e.wp28); + waypoint_schedulerelink(e.wp29); + waypoint_schedulerelink(e.wp30); + waypoint_schedulerelink(e.wp31); + // and now remove the spawnfunc_waypoint + delete(e); +} + +// empties the map of waypoints +void waypoint_removeall() +{ + IL_EACH(g_waypoints, true, + { + delete(it); + }); +} + +// tell all waypoints to relink +// (is this useful at all?) +void waypoint_schedulerelinkall() +{ + relink_total = relink_walkculled = relink_pvsculled = relink_lengthculled = 0; + IL_EACH(g_waypoints, true, + { + waypoint_schedulerelink(it); + }); +} + +// Load waypoint links from file +float waypoint_load_links() +{ + string filename, s; + float file, tokens, c = 0, found; + entity wp_from = NULL, wp_to; + vector wp_to_pos, wp_from_pos; + filename = strcat("maps/", mapname); + filename = strcat(filename, ".waypoints.cache"); + file = fopen(filename, FILE_READ); + + if (file < 0) + { + LOG_TRACE("waypoint links load from "); + LOG_TRACE(filename); + LOG_TRACE(" failed\n"); + return false; + } + + while ((s = fgets(file))) + { + tokens = tokenizebyseparator(s, "*"); + + if (tokens!=2) + { + // bad file format + fclose(file); + return false; + } + + wp_from_pos = stov(argv(0)); + wp_to_pos = stov(argv(1)); + + // Search "from" waypoint + if(!wp_from || wp_from.origin!=wp_from_pos) + { + wp_from = findradius(wp_from_pos, 1); + found = false; + while(wp_from) + { + if(vdist(wp_from.origin - wp_from_pos, <, 1)) + if(wp_from.classname == "waypoint") + { + found = true; + break; + } + wp_from = wp_from.chain; + } + + if(!found) + { + LOG_TRACE("waypoint_load_links: couldn't find 'from' waypoint at ", vtos(wp_from.origin),"\n"); + continue; + } + + } + + // Search "to" waypoint + wp_to = findradius(wp_to_pos, 1); + found = false; + while(wp_to) + { + if(vdist(wp_to.origin - wp_to_pos, <, 1)) + if(wp_to.classname == "waypoint") + { + found = true; + break; + } + wp_to = wp_to.chain; + } + + if(!found) + { + LOG_TRACE("waypoint_load_links: couldn't find 'to' waypoint at ", vtos(wp_to.origin),"\n"); + continue; + } + + ++c; + waypoint_addlink(wp_from, wp_to); + } + + fclose(file); + + LOG_TRACE("loaded ", ftos(c), " waypoint links from maps/", mapname, ".waypoints.cache\n"); + + botframe_cachedwaypointlinks = true; + return true; +} + +void waypoint_load_links_hardwired() +{ + string filename, s; + float file, tokens, c = 0, found; + entity wp_from = NULL, wp_to; + vector wp_to_pos, wp_from_pos; + filename = strcat("maps/", mapname); + filename = strcat(filename, ".waypoints.hardwired"); + file = fopen(filename, FILE_READ); + + botframe_loadedforcedlinks = true; + + if (file < 0) + { + LOG_TRACE("waypoint links load from ", filename, " failed\n"); + return; + } + + while ((s = fgets(file))) + { + if(substring(s, 0, 2)=="//") + continue; + + if(substring(s, 0, 1)=="#") + continue; + + tokens = tokenizebyseparator(s, "*"); + + if (tokens!=2) + continue; + + wp_from_pos = stov(argv(0)); + wp_to_pos = stov(argv(1)); + + // Search "from" waypoint + if(!wp_from || wp_from.origin!=wp_from_pos) + { + wp_from = findradius(wp_from_pos, 5); + found = false; + while(wp_from) + { + if(vdist(wp_from.origin - wp_from_pos, <, 5)) + if(wp_from.classname == "waypoint") + { + found = true; + break; + } + wp_from = wp_from.chain; + } + + if(!found) + { + LOG_INFO(strcat("NOTICE: Can not find waypoint at ", vtos(wp_from_pos), ". Path skipped\n")); + continue; + } + } + + // Search "to" waypoint + wp_to = findradius(wp_to_pos, 5); + found = false; + while(wp_to) + { + if(vdist(wp_to.origin - wp_to_pos, <, 5)) + if(wp_to.classname == "waypoint") + { + found = true; + break; + } + wp_to = wp_to.chain; + } + + if(!found) + { + LOG_INFO(strcat("NOTICE: Can not find waypoint at ", vtos(wp_to_pos), ". Path skipped\n")); + continue; + } + + ++c; + waypoint_addlink(wp_from, wp_to); + wp_from.wphardwired = true; + wp_to.wphardwired = true; + } + + fclose(file); + + LOG_TRACE("loaded ", ftos(c), " waypoint links from maps/", mapname, ".waypoints.hardwired\n"); +} + +entity waypoint_get_link(entity w, float i) +{ + switch(i) + { + case 0:return w.wp00; + case 1:return w.wp01; + case 2:return w.wp02; + case 3:return w.wp03; + case 4:return w.wp04; + case 5:return w.wp05; + case 6:return w.wp06; + case 7:return w.wp07; + case 8:return w.wp08; + case 9:return w.wp09; + case 10:return w.wp10; + case 11:return w.wp11; + case 12:return w.wp12; + case 13:return w.wp13; + case 14:return w.wp14; + case 15:return w.wp15; + case 16:return w.wp16; + case 17:return w.wp17; + case 18:return w.wp18; + case 19:return w.wp19; + case 20:return w.wp20; + case 21:return w.wp21; + case 22:return w.wp22; + case 23:return w.wp23; + case 24:return w.wp24; + case 25:return w.wp25; + case 26:return w.wp26; + case 27:return w.wp27; + case 28:return w.wp28; + case 29:return w.wp29; + case 30:return w.wp30; + case 31:return w.wp31; + default:return NULL; + } +} + +// Save all waypoint links to a file +void waypoint_save_links() +{ + string filename = sprintf("maps/%s.waypoints.cache", mapname); + int file = fopen(filename, FILE_WRITE); + if (file < 0) + { + LOG_INFOF("waypoint link save to %s failed\n", filename); + return; + } + + int c = 0; + IL_EACH(g_waypoints, true, + { + for(int j = 0; j < 32; ++j) + { + entity link = waypoint_get_link(it, j); + if(link) + { + string s = strcat(vtos(it.origin), "*", vtos(link.origin), "\n"); + fputs(file, s); + ++c; + } + } + }); + fclose(file); + botframe_cachedwaypointlinks = true; + + LOG_INFOF("saved %d waypoint links to maps/%s.waypoints.cache\n", c, mapname); +} + +// save waypoints to gamedir/data/maps/mapname.waypoints +void waypoint_saveall() +{ + string filename = sprintf("maps/%s.waypoints", mapname); + int file = fopen(filename, FILE_WRITE); + if (file < 0) + { + waypoint_save_links(); // save anyway? + botframe_loadedforcedlinks = false; + + LOG_INFOF("waypoint links: save to %s failed\n", filename); + return; + } + + int c = 0; + IL_EACH(g_waypoints, true, + { + if(it.wpflags & WAYPOINTFLAG_GENERATED) + continue; + + for(int j = 0; j < 32; ++j) + { + string s; + s = strcat(vtos(it.origin + it.mins), "\n"); + s = strcat(s, vtos(it.origin + it.maxs)); + s = strcat(s, "\n"); + s = strcat(s, ftos(it.wpflags)); + s = strcat(s, "\n"); + fputs(file, s); + ++c; + } + }); + fclose(file); + waypoint_save_links(); + botframe_loadedforcedlinks = false; + + LOG_INFOF("saved %d waypoints to maps/%s.waypoints\n", c, mapname); +} + +// load waypoints from file +float waypoint_loadall() +{ + string filename, s; + float file, cwp, cwb, fl; + vector m1, m2; + cwp = 0; + cwb = 0; + filename = strcat("maps/", mapname); + filename = strcat(filename, ".waypoints"); + file = fopen(filename, FILE_READ); + if (file >= 0) + { + while ((s = fgets(file))) + { + m1 = stov(s); + s = fgets(file); + if (!s) + break; + m2 = stov(s); + s = fgets(file); + if (!s) + break; + fl = stof(s); + waypoint_spawn(m1, m2, fl); + if (m1 == m2) + cwp = cwp + 1; + else + cwb = cwb + 1; + } + fclose(file); + LOG_TRACE("loaded ", ftos(cwp), " waypoints and ", ftos(cwb), " wayboxes from maps/", mapname, ".waypoints\n"); + } + else + { + LOG_TRACE("waypoint load from ", filename, " failed\n"); + } + return cwp + cwb; +} + +vector waypoint_fixorigin(vector position) +{ + tracebox(position + '0 0 1' * (1 - STAT(PL_MIN, NULL).z), STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), position + '0 0 -512', MOVE_NOMONSTERS, NULL); + if(trace_fraction < 1) + position = trace_endpos; + //traceline(position, position + '0 0 -512', MOVE_NOMONSTERS, NULL); + //print("position is ", ftos(trace_endpos_z - position_z), " above solid\n"); + return position; +} + +void waypoint_spawnforitem_force(entity e, vector org) +{ + // Fix the waypoint altitude if necessary + org = waypoint_fixorigin(org); + + // don't spawn an item spawnfunc_waypoint if it already exists + IL_EACH(g_waypoints, true, + { + if(it.wpisbox) + { + if(boxesoverlap(org, org, it.absmin, it.absmax)) + { + e.nearestwaypoint = it; + return; + } + } + else + { + if(vdist(it.origin - org, <, 16)) + { + e.nearestwaypoint = it; + return; + } + } + }); + + e.nearestwaypoint = waypoint_spawn(org, org, WAYPOINTFLAG_GENERATED | WAYPOINTFLAG_ITEM); +} + +void waypoint_spawnforitem(entity e) +{ + if(!bot_waypoints_for_items) + return; + + waypoint_spawnforitem_force(e, e.origin); +} + +void waypoint_spawnforteleporter_boxes(entity e, vector org1, vector org2, vector destination1, vector destination2, float timetaken) +{ + entity w; + entity dw; + w = waypoint_spawn(org1, org2, WAYPOINTFLAG_GENERATED | WAYPOINTFLAG_TELEPORT | WAYPOINTFLAG_NORELINK); + dw = waypoint_spawn(destination1, destination2, WAYPOINTFLAG_GENERATED); + // one way link to the destination + w.wp00 = dw; + w.wp00mincost = timetaken; // this is just for jump pads + // the teleporter's nearest spawnfunc_waypoint is this one + // (teleporters are not goals, so this is probably useless) + e.nearestwaypoint = w; + e.nearestwaypointtimeout = time + 1000000000; +} + +void waypoint_spawnforteleporter_v(entity e, vector org, vector destination, float timetaken) +{ + org = waypoint_fixorigin(org); + destination = waypoint_fixorigin(destination); + waypoint_spawnforteleporter_boxes(e, org, org, destination, destination, timetaken); +} + +void waypoint_spawnforteleporter(entity e, vector destination, float timetaken) +{ + destination = waypoint_fixorigin(destination); + waypoint_spawnforteleporter_boxes(e, e.absmin, e.absmax, destination, destination, timetaken); +} + +entity waypoint_spawnpersonal(entity this, vector position) +{ + entity w; + + // drop the waypoint to a proper location: + // first move it up by a player height + // then move it down to hit the floor with player bbox size + position = waypoint_fixorigin(position); + + w = waypoint_spawn(position, position, WAYPOINTFLAG_GENERATED | WAYPOINTFLAG_PERSONAL); + w.nearestwaypoint = NULL; + w.nearestwaypointtimeout = 0; + w.owner = this; + + waypoint_schedulerelink(w); + + return w; +} + +void botframe_showwaypointlinks() +{ + if (time < botframe_waypointeditorlightningtime) + return; + botframe_waypointeditorlightningtime = time + 0.5; + FOREACH_CLIENT(IS_PLAYER(it) && !it.isbot, + { + if(IS_ONGROUND(it) || it.waterlevel > WATERLEVEL_NONE) + { + //navigation_testtracewalk = true; + entity head = navigation_findnearestwaypoint(it, false); + // print("currently selected WP is ", etos(head), "\n"); + //navigation_testtracewalk = false; + if (head) + { + entity w; + w = head ;if (w) te_lightning2(NULL, w.origin, it.origin); + w = head.wp00;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp01;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp02;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp03;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp04;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp05;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp06;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp07;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp08;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp09;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp10;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp11;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp12;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp13;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp14;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp15;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp16;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp17;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp18;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp19;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp20;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp21;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp22;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp23;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp24;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp25;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp26;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp27;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp28;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp29;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp30;if (w) te_lightning2(NULL, w.origin, head.origin); + w = head.wp31;if (w) te_lightning2(NULL, w.origin, head.origin); + } + } + }); +} + +float botframe_autowaypoints_fixdown(vector v) +{ + tracebox(v, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), v + '0 0 -64', MOVE_NOMONSTERS, NULL); + if(trace_fraction >= 1) + return 0; + return 1; +} + +float botframe_autowaypoints_createwp(vector v, entity p, .entity fld, float f) +{ + IL_EACH(g_waypoints, boxesoverlap(v - '32 32 32', v + '32 32 32', it.absmin, it.absmax), + { + // if a matching spawnfunc_waypoint already exists, don't add a duplicate + return 0; + }); + + waypoint_schedulerelink(p.(fld) = waypoint_spawn(v, v, f)); + return 1; +} + +// return value: +// 1 = WP created +// 0 = no action needed +// -1 = temp fail, try from world too +// -2 = permanent fail, do not retry +float botframe_autowaypoints_fix_from(entity p, float walkfromwp, entity wp, .entity fld) +{ + // make it possible to go from p to wp, if we can + // if wp is NULL, nearest is chosen + + entity w; + vector porg; + float t, tmin, tmax; + vector o; + vector save; + + if(!botframe_autowaypoints_fixdown(p.origin)) + return -2; + porg = trace_endpos; + + if(wp) + { + // if any WP w fulfills wp -> w -> porg and w is closer than wp, then switch from wp to w + + // if wp -> porg, then OK + float maxdist; + if(navigation_waypoint_will_link(wp.origin, porg, p, walkfromwp, 1050)) + { + // we may find a better one + maxdist = vlen(wp.origin - porg); + } + else + { + // accept any "good" + maxdist = 2100; + } + + float bestdist = maxdist; + IL_EACH(g_waypoints, it != wp && !(it.wpflags & WAYPOINTFLAG_NORELINK), + { + float d = vlen(wp.origin - it.origin) + vlen(it.origin - porg); + if(d < bestdist) + if(navigation_waypoint_will_link(wp.origin, it.origin, p, walkfromwp, 1050)) + if(navigation_waypoint_will_link(it.origin, porg, p, walkfromwp, 1050)) + { + bestdist = d; + p.(fld) = it; + } + }); + if(bestdist < maxdist) + { + LOG_INFO("update chain to new nearest WP ", etos(p.(fld)), "\n"); + return 0; + } + + if(bestdist < 2100) + { + // we know maxdist < 2100 + // so wp -> porg is still valid + // all is good + p.(fld) = wp; + return 0; + } + + // otherwise, no existing WP can fix our issues + } + else + { + save = p.origin; + setorigin(p, porg); + w = navigation_findnearestwaypoint(p, walkfromwp); + setorigin(p, save); + if(w) + { + p.(fld) = w; + return 0; + } + } + + tmin = 0; + tmax = 1; + for (;;) + { + if(tmax - tmin < 0.001) + { + // did not get a good candidate + return -1; + } + + t = (tmin + tmax) * 0.5; + o = antilag_takebackorigin(p, CS(p), time - t); + if(!botframe_autowaypoints_fixdown(o)) + return -2; + o = trace_endpos; + + if(wp) + { + if(!navigation_waypoint_will_link(wp.origin, o, p, walkfromwp, 1050)) + { + // we cannot walk from wp.origin to o + // get closer to tmax + tmin = t; + continue; + } + } + else + { + save = p.origin; + setorigin(p, o); + w = navigation_findnearestwaypoint(p, walkfromwp); + setorigin(p, save); + if(!w) + { + // we cannot walk from any WP to o + // get closer to tmax + tmin = t; + continue; + } + } + + // if we get here, o is valid regarding waypoints + // check if o is connected right to the player + // we break if it succeeds, as that means o is a good waypoint location + if(navigation_waypoint_will_link(o, porg, p, walkfromwp, 1050)) + break; + + // o is no good, we need to get closer to the player + tmax = t; + } + + LOG_INFO("spawning a waypoint for connecting to ", etos(wp), "\n"); + botframe_autowaypoints_createwp(o, p, fld, 0); + return 1; +} + +// 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); + if(r != -1) + return; + r = botframe_autowaypoints_fix_from(p, walkfromwp, NULL, fld); + if(r != -1) + return; + + LOG_INFO("emergency: got no good nearby WP to build a link from, starting a new chain\n"); + if(!botframe_autowaypoints_fixdown(p.origin)) + return; // shouldn't happen, caught above + botframe_autowaypoints_createwp(trace_endpos, p, fld, WAYPOINTFLAG_PROTECTED); +} + +void botframe_deleteuselesswaypoints() +{ + FOREACH_ENTITY_FLOAT(bot_pickup, true, + { + // NOTE: this protects waypoints if they're the ONLY nearest + // waypoint. That's the intention. + navigation_findnearestwaypoint(it, false); // Walk TO item. + navigation_findnearestwaypoint(it, true); // Walk FROM item. + }); + IL_EACH(g_waypoints, true, + { + it.wpflags |= WAYPOINTFLAG_DEAD_END; + it.wpflags &= ~WAYPOINTFLAG_USEFUL; + // WP is useful if: + if (it.wpflags & WAYPOINTFLAG_ITEM) + it.wpflags |= WAYPOINTFLAG_USEFUL; + if (it.wpflags & WAYPOINTFLAG_TELEPORT) + it.wpflags |= WAYPOINTFLAG_USEFUL; + if (it.wpflags & WAYPOINTFLAG_PROTECTED) + it.wpflags |= WAYPOINTFLAG_USEFUL; + // b) WP is closest WP for an item/spawnpoint/other entity + // This has been done above by protecting these WPs. + }); + // c) There are w1, w, w2 so that w1 -> w, w -> w2 and not w1 -> w2. + IL_EACH(g_waypoints, !(it.wpflags & WAYPOINTFLAG_PERSONAL), + { + for (int m = 0; m < 32; ++m) + { + entity w = waypoint_get_link(it, m); + if (!w) + break; + if (w.wpflags & WAYPOINTFLAG_PERSONAL) + continue; + if (w.wpflags & WAYPOINTFLAG_USEFUL) + continue; + for (int j = 0; j < 32; ++j) + { + entity w2 = waypoint_get_link(w, j); + if (!w2) + break; + if (it == w2) + continue; + if (w2.wpflags & WAYPOINTFLAG_PERSONAL) + continue; + // If we got here, it != w2 exist with it -> w + // and w -> w2. That means the waypoint is not + // a dead end. + w.wpflags &= ~WAYPOINTFLAG_DEAD_END; + for (int k = 0; k < 32; ++k) + { + if (waypoint_get_link(it, k) == w2) + continue; + // IF WE GET HERE, w is proven useful + // to get from it to w2! + w.wpflags |= WAYPOINTFLAG_USEFUL; + goto next; + } + } +LABEL(next) + } + }); + // d) The waypoint is a dead end. Dead end waypoints must be kept as + // they are needed to complete routes while autowaypointing. + + IL_EACH(g_waypoints, !(it.wpflags & (WAYPOINTFLAG_USEFUL | WAYPOINTFLAG_DEAD_END)), + { + LOG_INFOF("Removed a waypoint at %v. Try again for more!\n", it.origin); + te_explosion(it.origin); + waypoint_remove(it); + break; + }); + + IL_EACH(g_waypoints, true, + { + it.wpflags &= ~(WAYPOINTFLAG_USEFUL | WAYPOINTFLAG_DEAD_END); // temp flag + }); +} + +void botframe_autowaypoints() +{ + FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it) && !IS_DEAD(it), LAMBDA( + // going back is broken, so only fix waypoints to walk TO the player + //botframe_autowaypoints_fix(p, false, botframe_autowaypoints_lastwp0); + botframe_autowaypoints_fix(it, true, botframe_autowaypoints_lastwp1); + //te_explosion(p.botframe_autowaypoints_lastwp0.origin); + )); + + if (autocvar_g_waypointeditor_auto >= 2) { + botframe_deleteuselesswaypoints(); + } +} + diff --git a/qcsrc/server/bot/default/waypoints.qh b/qcsrc/server/bot/default/waypoints.qh new file mode 100644 index 000000000..38d57a04a --- /dev/null +++ b/qcsrc/server/bot/default/waypoints.qh @@ -0,0 +1,58 @@ +#pragma once +/* + * Globals and Fields + */ + +// fields you can query using prvm_global server to get some statistics about waypoint linking culling +float relink_total, relink_walkculled, relink_pvsculled, relink_lengthculled; + +float botframe_waypointeditorlightningtime; +float botframe_loadedforcedlinks; +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; + +// itemscore = (howmuchmoreIwant / howmuchIcanwant) / itemdistance +.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; +.int wpflags; + +.vector wpnearestpoint; + +/* + * Functions + */ + +spawnfunc(waypoint); +void waypoint_addlink(entity from, entity to); +void waypoint_think(entity this); +void waypoint_clearlinks(entity wp); +void waypoint_schedulerelink(entity wp); + +void waypoint_remove(entity e); +void waypoint_removeall(); +void waypoint_schedulerelinkall(); +void waypoint_load_links_hardwired(); +void waypoint_save_links(); +void waypoint_saveall(); + +void waypoint_spawnforitem_force(entity e, vector org); +void waypoint_spawnforitem(entity e); +void waypoint_spawnforteleporter(entity e, vector destination, float timetaken); +void waypoint_spawnforteleporter_v(entity e, vector org, vector destination, float timetaken); +void botframe_showwaypointlinks(); + +float waypoint_loadall(); +float waypoint_load_links(); + +entity waypoint_spawn(vector m1, vector m2, float f); +entity waypoint_spawnpersonal(entity this, vector position); + +vector waypoint_fixorigin(vector position); + +void botframe_autowaypoints(); diff --git a/qcsrc/server/bot/havocbot/_mod.inc b/qcsrc/server/bot/havocbot/_mod.inc deleted file mode 100644 index a6270bc13..000000000 --- a/qcsrc/server/bot/havocbot/_mod.inc +++ /dev/null @@ -1,3 +0,0 @@ -// generated file; do not modify -#include -#include diff --git a/qcsrc/server/bot/havocbot/_mod.qh b/qcsrc/server/bot/havocbot/_mod.qh deleted file mode 100644 index 4b62d1b8e..000000000 --- a/qcsrc/server/bot/havocbot/_mod.qh +++ /dev/null @@ -1,3 +0,0 @@ -// generated file; do not modify -#include -#include diff --git a/qcsrc/server/bot/havocbot/havocbot.qc b/qcsrc/server/bot/havocbot/havocbot.qc deleted file mode 100644 index ceb9b296e..000000000 --- a/qcsrc/server/bot/havocbot/havocbot.qc +++ /dev/null @@ -1,1296 +0,0 @@ -#include "havocbot.qh" - -#include "../aim.qh" -#include "../bot.qh" -#include "../navigation.qh" -#include "../scripting.qh" -#include "../waypoints.qh" - -#include -#include -#include -#include - -#include - -#include - -.float speed; - -void havocbot_ai(entity this) -{ - if(this.draggedby) - return; - - if(bot_execute_commands(this)) - return; - - if (bot_strategytoken == this) - if (!bot_strategytoken_taken) - { - if(this.havocbot_blockhead) - { - this.havocbot_blockhead = false; - } - else - { - if (!this.jumppadcount) - this.havocbot_role(this); // little too far down the rabbit hole - } - - // TODO: tracewalk() should take care of this job (better path finding under water) - // if we don't have a goal and we're under water look for a waypoint near the "shore" and push it - if(IS_DEAD(this)) - if(!this.goalcurrent) - if(this.waterlevel == WATERLEVEL_SWIMMING || (this.aistatus & AI_STATUS_OUT_WATER)) - { - // Look for the closest waypoint out of water - entity newgoal = NULL; - IL_EACH(g_waypoints, vdist(it.origin - this.origin, <=, 10000), - { - if(it.origin.z < this.origin.z) - continue; - - if(it.origin.z - this.origin.z - this.view_ofs.z > 100) - continue; - - if (pointcontents(it.origin + it.maxs + '0 0 1') != CONTENT_EMPTY) - continue; - - traceline(this.origin + this.view_ofs, ((it.absmin + it.absmax) * 0.5), true, this); - - if(trace_fraction < 1) - continue; - - if(!newgoal || vlen2(it.origin - this.origin) < vlen2(newgoal.origin - this.origin)) - newgoal = it; - }); - - if(newgoal) - { - // te_wizspike(newgoal.origin); - navigation_pushroute(this, newgoal); - } - } - - // token has been used this frame - bot_strategytoken_taken = true; - } - - if(IS_DEAD(this)) - return; - - havocbot_chooseenemy(this); - if (this.bot_chooseweapontime < time ) - { - this.bot_chooseweapontime = time + autocvar_bot_ai_chooseweaponinterval; - havocbot_chooseweapon(this); - } - havocbot_aim(this); - lag_update(this); - if (this.bot_aimtarg) - { - this.aistatus |= AI_STATUS_ATTACKING; - this.aistatus &= ~AI_STATUS_ROAMING; - - if(this.weapons) - { - Weapon w = PS(this).m_weapon; - w.wr_aim(w, this); - if (autocvar_bot_nofire || IS_INDEPENDENT_PLAYER(this)) - { - PHYS_INPUT_BUTTON_ATCK(this) = false; - PHYS_INPUT_BUTTON_ATCK2(this) = false; - } - else - { - if(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_ATCK2(this)) - this.lastfiredweapon = PS(this).m_weapon.m_id; - } - } - else - { - if(IS_PLAYER(this.bot_aimtarg)) - bot_aimdir(this, this.bot_aimtarg.origin + this.bot_aimtarg.view_ofs - this.origin - this.view_ofs , -1); - } - } - else if (this.goalcurrent) - { - this.aistatus |= AI_STATUS_ROAMING; - this.aistatus &= ~AI_STATUS_ATTACKING; - - vector now,v,next;//,heading; - float aimdistance,skillblend,distanceblend,blend; - next = now = ( (this.goalcurrent.absmin + this.goalcurrent.absmax) * 0.5) - (this.origin + this.view_ofs); - aimdistance = vlen(now); - //heading = this.velocity; - //dprint(this.goalstack01.classname,etos(this.goalstack01),"\n"); - if( - this.goalstack01 != this && this.goalstack01 != NULL && ((this.aistatus & AI_STATUS_RUNNING) == 0) && - !(this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT) - ) - next = ((this.goalstack01.absmin + this.goalstack01.absmax) * 0.5) - (this.origin + this.view_ofs); - - skillblend=bound(0,(skill+this.bot_moveskill-2.5)*0.5,1); //lower skill player can't preturn - distanceblend=bound(0,aimdistance/autocvar_bot_ai_keyboard_distance,1); - blend = skillblend * (1-distanceblend); - //v = (now * (distanceblend) + next * (1-distanceblend)) * (skillblend) + now * (1-skillblend); - //v = now * (distanceblend) * (skillblend) + next * (1-distanceblend) * (skillblend) + now * (1-skillblend); - //v = now * ((1-skillblend) + (distanceblend) * (skillblend)) + next * (1-distanceblend) * (skillblend); - v = now + blend * (next - now); - //dprint(etos(this), " "); - //dprint(vtos(now), ":", vtos(next), "=", vtos(v), " (blend ", ftos(blend), ")\n"); - //v = now * (distanceblend) + next * (1-distanceblend); - if (this.waterlevel < WATERLEVEL_SWIMMING) - v.z = 0; - //dprint("walk at:", vtos(v), "\n"); - //te_lightning2(NULL, this.origin, this.goalcurrent.origin); - bot_aimdir(this, v, -1); - } - havocbot_movetogoal(this); - - // if the bot is not attacking, consider reloading weapons - if (!(this.aistatus & AI_STATUS_ATTACKING)) - { - // we are currently holding a weapon that's not fully loaded, reload it - if(skill >= 2) // bots can only reload the held weapon on purpose past this skill - if(this.clip_load < this.clip_size) - this.impulse = 20; // "press" the reload button, not sure if this is done right - - // if we're not reloading a weapon, switch to any weapon in our invnetory that's not fully loaded to reload it next - // the code above executes next frame, starting the reloading then - if(skill >= 5) // bots can only look for unloaded weapons past this skill - if(this.clip_load >= 0) // only if we're not reloading a weapon already - { - FOREACH(Weapons, it != WEP_Null, LAMBDA( - if((this.weapons & (it.m_wepset)) && (it.spawnflags & WEP_FLAG_RELOADABLE) && (this.weapon_load[it.m_id] < it.reloading_ammo)) - PS(this).m_switchweapon = it; - )); - } - } -} - -void havocbot_keyboard_movement(entity this, vector destorg) -{ - vector keyboard; - float blend, maxspeed; - float sk; - - sk = skill + this.bot_moveskill; - - maxspeed = autocvar_sv_maxspeed; - - if (time < this.havocbot_keyboardtime) - return; - - this.havocbot_keyboardtime = - max( - this.havocbot_keyboardtime - + 0.05/max(1, sk+this.havocbot_keyboardskill) - + random()*0.025/max(0.00025, skill+this.havocbot_keyboardskill) - , time); - keyboard = this.movement * (1.0 / maxspeed); - - float trigger, trigger1; - blend = bound(0,sk*0.1,1); - trigger = autocvar_bot_ai_keyboard_threshold; - trigger1 = 0 - trigger; - - // categorize forward movement - // at skill < 1.5 only forward - // at skill < 2.5 only individual directions - // at skill < 4.5 only individual directions, and forward diagonals - // at skill >= 4.5, all cases allowed - if (keyboard.x > trigger) - { - keyboard.x = 1; - if (sk < 2.5) - keyboard.y = 0; - } - else if (keyboard.x < trigger1 && sk > 1.5) - { - keyboard.x = -1; - if (sk < 4.5) - keyboard.y = 0; - } - else - { - keyboard.x = 0; - if (sk < 1.5) - keyboard.y = 0; - } - if (sk < 4.5) - keyboard.z = 0; - - if (keyboard.y > trigger) - keyboard.y = 1; - else if (keyboard.y < trigger1) - keyboard.y = -1; - else - keyboard.y = 0; - - if (keyboard.z > trigger) - keyboard.z = 1; - else if (keyboard.z < trigger1) - keyboard.z = -1; - else - keyboard.z = 0; - - this.havocbot_keyboard = keyboard * maxspeed; - if (this.havocbot_ducktime>time) PHYS_INPUT_BUTTON_CROUCH(this) = true; - - keyboard = this.havocbot_keyboard; - blend = bound(0,vlen(destorg-this.origin)/autocvar_bot_ai_keyboard_distance,1); // When getting close move with 360 degree - //dprint("movement ", vtos(this.movement), " keyboard ", vtos(keyboard), " blend ", ftos(blend), "\n"); - this.movement = this.movement + (keyboard - this.movement) * blend; -} - -void havocbot_bunnyhop(entity this, vector dir) -{ - float bunnyhopdistance; - vector deviation; - float maxspeed; - vector gco, gno; - - // Don't jump when attacking - if(this.aistatus & AI_STATUS_ATTACKING) - return; - - if(IS_PLAYER(this.goalcurrent)) - return; - - maxspeed = autocvar_sv_maxspeed; - - if(this.aistatus & AI_STATUS_DANGER_AHEAD) - { - this.aistatus &= ~AI_STATUS_RUNNING; - PHYS_INPUT_BUTTON_JUMP(this) = false; - this.bot_canruntogoal = 0; - this.bot_timelastseengoal = 0; - return; - } - - if(this.waterlevel > WATERLEVEL_WETFEET) - { - this.aistatus &= ~AI_STATUS_RUNNING; - return; - } - - if(this.bot_lastseengoal != this.goalcurrent && !(this.aistatus & AI_STATUS_RUNNING)) - { - this.bot_canruntogoal = 0; - this.bot_timelastseengoal = 0; - } - - gco = (this.goalcurrent.absmin + this.goalcurrent.absmax) * 0.5; - bunnyhopdistance = vlen(this.origin - gco); - - // Run only to visible goals - if(IS_ONGROUND(this)) - if(this.speed==maxspeed) - if(checkpvs(this.origin + this.view_ofs, this.goalcurrent)) - { - this.bot_lastseengoal = this.goalcurrent; - - // seen it before - if(this.bot_timelastseengoal) - { - // for a period of time - if(time - this.bot_timelastseengoal > autocvar_bot_ai_bunnyhop_firstjumpdelay) - { - float checkdistance; - checkdistance = true; - - // don't run if it is too close - if(this.bot_canruntogoal==0) - { - if(bunnyhopdistance > autocvar_bot_ai_bunnyhop_startdistance) - this.bot_canruntogoal = 1; - else - this.bot_canruntogoal = -1; - } - - if(this.bot_canruntogoal != 1) - return; - - if(this.aistatus & AI_STATUS_ROAMING) - if(this.goalcurrent.classname=="waypoint") - if (!(this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL)) - if(fabs(gco.z - this.origin.z) < this.maxs.z - this.mins.z) - if(this.goalstack01!=NULL) - { - gno = (this.goalstack01.absmin + this.goalstack01.absmax) * 0.5; - deviation = vectoangles(gno - this.origin) - vectoangles(gco - this.origin); - while (deviation.y < -180) deviation.y = deviation.y + 360; - while (deviation.y > 180) deviation.y = deviation.y - 360; - - if(fabs(deviation.y) < 20) - if(bunnyhopdistance < vlen(this.origin - gno)) - if(fabs(gno.z - gco.z) < this.maxs.z - this.mins.z) - { - if(vdist(gco - gno, >, autocvar_bot_ai_bunnyhop_startdistance)) - if(checkpvs(this.origin + this.view_ofs, this.goalstack01)) - { - checkdistance = false; - } - } - } - - if(checkdistance) - { - this.aistatus &= ~AI_STATUS_RUNNING; - if(bunnyhopdistance > autocvar_bot_ai_bunnyhop_stopdistance) - PHYS_INPUT_BUTTON_JUMP(this) = true; - } - else - { - this.aistatus |= AI_STATUS_RUNNING; - PHYS_INPUT_BUTTON_JUMP(this) = true; - } - } - } - else - { - this.bot_timelastseengoal = time; - } - } - else - { - this.bot_timelastseengoal = 0; - } - -#if 0 - // Release jump button - if(!cvar("sv_pogostick")) - if((IS_ONGROUND(this)) == 0) - { - if(this.velocity.z < 0 || vlen(this.velocity)maxspeed) - { - deviation = vectoangles(dir) - vectoangles(this.velocity); - while (deviation.y < -180) deviation.y = deviation.y + 360; - while (deviation.y > 180) deviation.y = deviation.y - 360; - - if(fabs(deviation.y)>10) - this.movement_x = 0; - - if(deviation.y>10) - this.movement_y = maxspeed * -1; - else if(deviation.y<10) - this.movement_y = maxspeed; - - } - } -#endif -} - -void havocbot_movetogoal(entity this) -{ - vector destorg; - vector diff; - vector dir; - vector flatdir; - vector m1; - vector m2; - vector evadeobstacle; - vector evadelava; - float s; - float maxspeed; - vector gco; - //float dist; - vector dodge; - //if (this.goalentity) - // te_lightning2(this, this.origin, (this.goalentity.absmin + this.goalentity.absmax) * 0.5); - this.movement = '0 0 0'; - maxspeed = autocvar_sv_maxspeed; - - // Jetpack navigation - if(this.goalcurrent) - if(this.navigation_jetpack_goal) - if(this.goalcurrent==this.navigation_jetpack_goal) - if(this.ammo_fuel) - { - if(autocvar_bot_debug_goalstack) - { - debuggoalstack(this); - te_wizspike(this.navigation_jetpack_point); - } - - // Take off - if (!(this.aistatus & AI_STATUS_JETPACK_FLYING)) - { - // Brake almost completely so it can get a good direction - if(vdist(this.velocity, >, 10)) - return; - this.aistatus |= AI_STATUS_JETPACK_FLYING; - } - - makevectors(this.v_angle.y * '0 1 0'); - dir = normalize(this.navigation_jetpack_point - this.origin); - - // Landing - if(this.aistatus & AI_STATUS_JETPACK_LANDING) - { - // Calculate brake distance in xy - float db, v, d; - vector dxy; - - dxy = this.origin - ( ( this.goalcurrent.absmin + this.goalcurrent.absmax ) * 0.5 ); dxy.z = 0; - d = vlen(dxy); - v = vlen(this.velocity - this.velocity.z * '0 0 1'); - db = (pow(v,2) / (autocvar_g_jetpack_acceleration_side * 2)) + 100; - // dprint("distance ", ftos(ceil(d)), " velocity ", ftos(ceil(v)), " brake at ", ftos(ceil(db)), "\n"); - if(d < db || d < 500) - { - // Brake - if(fabs(this.velocity.x)>maxspeed*0.3) - { - this.movement_x = dir * v_forward * -maxspeed; - return; - } - // Switch to normal mode - this.navigation_jetpack_goal = NULL; - this.aistatus &= ~AI_STATUS_JETPACK_LANDING; - this.aistatus &= ~AI_STATUS_JETPACK_FLYING; - return; - } - } - else if(checkpvs(this.origin,this.goalcurrent)) - { - // If I can see the goal switch to landing code - this.aistatus &= ~AI_STATUS_JETPACK_FLYING; - this.aistatus |= AI_STATUS_JETPACK_LANDING; - return; - } - - // Flying - PHYS_INPUT_BUTTON_HOOK(this) = true; - if(this.navigation_jetpack_point.z - STAT(PL_MAX, NULL).z + STAT(PL_MIN, NULL).z < this.origin.z) - { - this.movement_x = dir * v_forward * maxspeed; - this.movement_y = dir * v_right * maxspeed; - } - return; - } - - // Handling of jump pads - if(this.jumppadcount) - { - // If got stuck on the jump pad try to reach the farthest visible waypoint - if(this.aistatus & AI_STATUS_OUT_JUMPPAD) - { - if(fabs(this.velocity.z)<50) - { - entity newgoal = NULL; - IL_EACH(g_waypoints, vdist(it.origin - this.origin, <=, 1000), - { - traceline(this.origin + this.view_ofs, ((it.absmin + it.absmax) * 0.5), true, this); - - if(trace_fraction < 1) - continue; - - if(!newgoal || vlen2(it.origin - this.origin) > vlen2(newgoal.origin - this.origin)) - newgoal = it; - }); - - if(newgoal) - { - this.ignoregoal = this.goalcurrent; - this.ignoregoaltime = time + autocvar_bot_ai_ignoregoal_timeout; - navigation_clearroute(this); - navigation_routetogoal(this, newgoal, this.origin); - this.aistatus &= ~AI_STATUS_OUT_JUMPPAD; - } - } - else - return; - } - else - { - if(this.velocity.z>0) - { - float threshold; - vector velxy = this.velocity; velxy_z = 0; - threshold = maxspeed * 0.2; - if(vdist(velxy, <, threshold)) - { - LOG_TRACE("Warning: ", this.netname, " got stuck on a jumppad (velocity in xy is ", vtos(velxy), "), trying to get out of it now\n"); - this.aistatus |= AI_STATUS_OUT_JUMPPAD; - } - return; - } - - // Don't chase players while using a jump pad - if(IS_PLAYER(this.goalcurrent) || IS_PLAYER(this.goalstack01)) - return; - } - } - else if(this.aistatus & AI_STATUS_OUT_JUMPPAD) - this.aistatus &= ~AI_STATUS_OUT_JUMPPAD; - - // If there is a trigger_hurt right below try to use the jetpack or make a rocketjump - if(skill>6) - if (!(IS_ONGROUND(this))) - { - tracebox(this.origin, this.mins, this.maxs, this.origin + '0 0 -65536', MOVE_NOMONSTERS, this); - if(tracebox_hits_trigger_hurt(this.origin, this.mins, this.maxs, trace_endpos )) - if(this.items & IT_JETPACK) - { - tracebox(this.origin, this.mins, this.maxs, this.origin + '0 0 65536', MOVE_NOMONSTERS, this); - if(tracebox_hits_trigger_hurt(this.origin, this.mins, this.maxs, trace_endpos + '0 0 1' )) - { - if(this.velocity.z<0) - { - PHYS_INPUT_BUTTON_HOOK(this) = true; - } - } - else - PHYS_INPUT_BUTTON_HOOK(this) = true; - - // If there is no goal try to move forward - - if(this.goalcurrent==NULL) - dir = v_forward; - else - dir = normalize(( ( this.goalcurrent.absmin + this.goalcurrent.absmax ) * 0.5 ) - this.origin); - - vector xyvelocity = this.velocity; xyvelocity_z = 0; - float xyspeed = xyvelocity * dir; - - if(xyspeed < (maxspeed / 2)) - { - makevectors(this.v_angle.y * '0 1 0'); - tracebox(this.origin, this.mins, this.maxs, this.origin + (dir * maxspeed * 3), MOVE_NOMONSTERS, this); - if(trace_fraction==1) - { - this.movement_x = dir * v_forward * maxspeed; - this.movement_y = dir * v_right * maxspeed; - if (skill < 10) - havocbot_keyboard_movement(this, this.origin + dir * 100); - } - } - - this.havocbot_blockhead = true; - - return; - } - else if(this.health>WEP_CVAR(devastator, damage)*0.5) - { - if(this.velocity.z < 0) - if(client_hasweapon(this, WEP_DEVASTATOR, true, false)) - { - this.movement_x = maxspeed; - - if(this.rocketjumptime) - { - if(time > this.rocketjumptime) - { - PHYS_INPUT_BUTTON_ATCK2(this) = true; - this.rocketjumptime = 0; - } - return; - } - - PS(this).m_switchweapon = WEP_DEVASTATOR; - this.v_angle_x = 90; - PHYS_INPUT_BUTTON_ATCK(this) = true; - this.rocketjumptime = time + WEP_CVAR(devastator, detonatedelay); - return; - } - } - else - { - // If there is no goal try to move forward - if(this.goalcurrent==NULL) - this.movement_x = maxspeed; - } - } - - // If we are under water with no goals, swim up - if(this.waterlevel) - if(this.goalcurrent==NULL) - { - dir = '0 0 0'; - if(this.waterlevel>WATERLEVEL_SWIMMING) - dir.z = 1; - else if(this.velocity.z >= 0 && !(this.waterlevel == WATERLEVEL_WETFEET && this.watertype == CONTENT_WATER)) - PHYS_INPUT_BUTTON_JUMP(this) = true; - else - PHYS_INPUT_BUTTON_JUMP(this) = false; - makevectors(this.v_angle.y * '0 1 0'); - this.movement_x = dir * v_forward * maxspeed; - this.movement_y = dir * v_right * maxspeed; - this.movement_z = dir * v_up * maxspeed; - } - - // if there is nowhere to go, exit - if (this.goalcurrent == NULL) - return; - - if (this.goalcurrent) - navigation_poptouchedgoals(this); - - // if ran out of goals try to use an alternative goal or get a new strategy asap - if(this.goalcurrent == NULL) - { - this.bot_strategytime = 0; - return; - } - - - if(autocvar_bot_debug_goalstack) - debuggoalstack(this); - - m1 = this.goalcurrent.origin + this.goalcurrent.mins; - m2 = this.goalcurrent.origin + this.goalcurrent.maxs; - destorg = this.origin; - destorg.x = bound(m1_x, destorg.x, m2_x); - destorg.y = bound(m1_y, destorg.y, m2_y); - destorg.z = bound(m1_z, destorg.z, m2_z); - diff = destorg - this.origin; - //dist = vlen(diff); - dir = normalize(diff); - flatdir = diff;flatdir.z = 0; - flatdir = normalize(flatdir); - gco = (this.goalcurrent.absmin + this.goalcurrent.absmax) * 0.5; - - //if (this.bot_dodgevector_time < time) - { - // this.bot_dodgevector_time = time + cvar("bot_ai_dodgeupdateinterval"); - // this.bot_dodgevector_jumpbutton = 1; - evadeobstacle = '0 0 0'; - evadelava = '0 0 0'; - - if (this.waterlevel) - { - if(this.waterlevel>WATERLEVEL_SWIMMING) - { - // flatdir_z = 1; - this.aistatus |= AI_STATUS_OUT_WATER; - } - else - { - if(this.velocity.z >= 0 && !(this.watertype == CONTENT_WATER && gco.z < this.origin.z) && - ( !(this.waterlevel == WATERLEVEL_WETFEET && this.watertype == CONTENT_WATER) || this.aistatus & AI_STATUS_OUT_WATER)) - PHYS_INPUT_BUTTON_JUMP(this) = true; - else - PHYS_INPUT_BUTTON_JUMP(this) = false; - } - dir = normalize(flatdir); - makevectors(this.v_angle.y * '0 1 0'); - } - else - { - if(this.aistatus & AI_STATUS_OUT_WATER) - this.aistatus &= ~AI_STATUS_OUT_WATER; - - // jump if going toward an obstacle that doesn't look like stairs we - // can walk up directly - tracebox(this.origin, this.mins, this.maxs, this.origin + this.velocity * 0.2, false, this); - if (trace_fraction < 1) - if (trace_plane_normal.z < 0.7) - { - s = trace_fraction; - tracebox(this.origin + stepheightvec, this.mins, this.maxs, this.origin + this.velocity * 0.2 + stepheightvec, false, this); - if (trace_fraction < s + 0.01) - if (trace_plane_normal.z < 0.7) - { - s = trace_fraction; - tracebox(this.origin + jumpstepheightvec, this.mins, this.maxs, this.origin + this.velocity * 0.2 + jumpstepheightvec, false, this); - if (trace_fraction > s) - PHYS_INPUT_BUTTON_JUMP(this) = true; - } - } - - // avoiding dangers and obstacles - vector dst_ahead, dst_down; - makevectors(this.v_angle.y * '0 1 0'); - dst_ahead = this.origin + this.view_ofs + (this.velocity * 0.4) + (v_forward * 32 * 3); - dst_down = dst_ahead - '0 0 1500'; - - // Look ahead - traceline(this.origin + this.view_ofs, dst_ahead, true, NULL); - - // Check head-banging against walls - if(vdist(this.origin + this.view_ofs - trace_endpos, <, 25) && !(this.aistatus & AI_STATUS_OUT_WATER)) - { - PHYS_INPUT_BUTTON_JUMP(this) = true; - if(this.facingwalltime && time > this.facingwalltime) - { - this.ignoregoal = this.goalcurrent; - this.ignoregoaltime = time + autocvar_bot_ai_ignoregoal_timeout; - this.bot_strategytime = 0; - return; - } - else - { - this.facingwalltime = time + 0.05; - } - } - else - { - this.facingwalltime = 0; - - if(this.ignoregoal != NULL && time > this.ignoregoaltime) - { - this.ignoregoal = NULL; - this.ignoregoaltime = 0; - } - } - - // Check for water/slime/lava and dangerous edges - // (only when the bot is on the ground or jumping intentionally) - this.aistatus &= ~AI_STATUS_DANGER_AHEAD; - - if(trace_fraction == 1 && this.jumppadcount == 0 && !this.goalcurrent.wphardwired ) - if((IS_ONGROUND(this)) || (this.aistatus & AI_STATUS_RUNNING) || PHYS_INPUT_BUTTON_JUMP(this)) - { - // Look downwards - traceline(dst_ahead , dst_down, true, NULL); - // te_lightning2(NULL, this.origin, dst_ahead); // Draw "ahead" look - // te_lightning2(NULL, dst_ahead, dst_down); // Draw "downwards" look - if(trace_endpos.z < this.origin.z + this.mins.z) - { - s = pointcontents(trace_endpos + '0 0 1'); - if (s != CONTENT_SOLID) - if (s == CONTENT_LAVA || s == CONTENT_SLIME) - evadelava = normalize(this.velocity) * -1; - else if (s == CONTENT_SKY) - evadeobstacle = normalize(this.velocity) * -1; - else if (!boxesoverlap(dst_ahead - this.view_ofs + this.mins, dst_ahead - this.view_ofs + this.maxs, - this.goalcurrent.absmin, this.goalcurrent.absmax)) - { - // if ain't a safe goal with "holes" (like the jumpad on soylent) - // and there is a trigger_hurt below - if(tracebox_hits_trigger_hurt(dst_ahead, this.mins, this.maxs, trace_endpos)) - { - // Remove dangerous dynamic goals from stack - LOG_TRACE("bot ", this.netname, " avoided the goal ", this.goalcurrent.classname, " ", etos(this.goalcurrent), " because it led to a dangerous path; goal stack cleared\n"); - navigation_clearroute(this); - return; - } - } - } - } - - dir = flatdir; - evadeobstacle.z = 0; - evadelava.z = 0; - makevectors(this.v_angle.y * '0 1 0'); - - if(evadeobstacle!='0 0 0'||evadelava!='0 0 0') - this.aistatus |= AI_STATUS_DANGER_AHEAD; - } - - dodge = havocbot_dodge(this); - dodge = dodge * bound(0,0.5+(skill+this.bot_dodgeskill)*0.1,1); - evadelava = evadelava * bound(1,3-(skill+this.bot_dodgeskill),3); //Noobs fear lava a lot and take more distance from it - traceline(this.origin, ( ( this.enemy.absmin + this.enemy.absmax ) * 0.5 ), true, NULL); - if(IS_PLAYER(trace_ent)) - dir = dir * bound(0,(skill+this.bot_dodgeskill)/7,1); - - dir = normalize(dir + dodge + evadeobstacle + evadelava); - // this.bot_dodgevector = dir; - // this.bot_dodgevector_jumpbutton = PHYS_INPUT_BUTTON_JUMP(this); - } - - if(time < this.ladder_time) - { - if(this.goalcurrent.origin.z + this.goalcurrent.mins.z > this.origin.z + this.mins.z) - { - if(this.origin.z + this.mins.z < this.ladder_entity.origin.z + this.ladder_entity.maxs.z) - dir.z = 1; - } - else - { - if(this.origin.z + this.mins.z > this.ladder_entity.origin.z + this.ladder_entity.mins.z) - dir.z = -1; - } - } - - //dir = this.bot_dodgevector; - //if (this.bot_dodgevector_jumpbutton) - // PHYS_INPUT_BUTTON_JUMP(this) = true; - this.movement_x = dir * v_forward * maxspeed; - this.movement_y = dir * v_right * maxspeed; - this.movement_z = dir * v_up * maxspeed; - - // Emulate keyboard interface - if (skill < 10) - havocbot_keyboard_movement(this, destorg); - - // Bunnyhop! -// if(this.aistatus & AI_STATUS_ROAMING) - if(this.goalcurrent) - 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); -} - -void havocbot_chooseenemy(entity this) -{ - entity head, best, head2; - float rating, bestrating, hf; - vector eye, v; - if (autocvar_bot_nofire || IS_INDEPENDENT_PLAYER(this)) - { - this.enemy = NULL; - return; - } - if (this.enemy) - { - if (!bot_shouldattack(this, this.enemy)) - { - // enemy died or something, find a new target - this.enemy = NULL; - this.havocbot_chooseenemy_finished = time; - } - else if (this.havocbot_stickenemy) - { - // tracking last chosen enemy - // if enemy is visible - // and not really really far away - // and we're not severely injured - // then keep tracking for a half second into the future - traceline(this.origin+this.view_ofs, ( this.enemy.absmin + this.enemy.absmax ) * 0.5,false,NULL); - if (trace_ent == this.enemy || trace_fraction == 1) - if (vdist(((this.enemy.absmin + this.enemy.absmax) * 0.5) - this.origin, <, 1000)) - if (this.health > 30) - { - // remain tracking him for a shot while (case he went after a small corner or pilar - this.havocbot_chooseenemy_finished = time + 0.5; - return; - } - // enemy isn't visible, or is far away, or we're injured severely - // so stop preferring this enemy - // (it will still take a half second until a new one is chosen) - this.havocbot_stickenemy = 0; - } - } - if (time < this.havocbot_chooseenemy_finished) - return; - this.havocbot_chooseenemy_finished = time + autocvar_bot_ai_enemydetectioninterval; - eye = this.origin + this.view_ofs; - best = NULL; - bestrating = 100000000; - head = head2 = findchainfloat(bot_attack, true); - - // Backup hit flags - hf = this.dphitcontentsmask; - - // Search for enemies, if no enemy can be seen directly try to look through transparent objects - - this.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_CORPSE; - - bool scan_transparent = false; - bool scan_secondary_targets = false; - bool have_secondary_targets = false; - while(true) - { - scan_secondary_targets = false; -LABEL(scan_targets) - for( ; head; head = head.chain) - { - if(!scan_secondary_targets) - { - if(head.classname == "misc_breakablemodel") - { - have_secondary_targets = true; - continue; - } - } - else - { - if(head.classname != "misc_breakablemodel") - continue; - } - - v = (head.absmin + head.absmax) * 0.5; - rating = vlen(v - eye); - if (rating rating) - if (bot_shouldattack(this, head)) - { - traceline(eye, v, true, this); - if (trace_ent == head || trace_fraction >= 1) - { - best = head; - bestrating = rating; - } - } - } - - if(!best && have_secondary_targets && !scan_secondary_targets) - { - scan_secondary_targets = true; - // restart the loop - head = head2; - bestrating = 100000000; - goto scan_targets; - } - - // I want to do a second scan if no enemy was found or I don't have weapons - // TODO: Perform the scan when using the rifle (requires changes on the rifle code) - if(best || this.weapons) // || this.weapon == WEP_RIFLE.m_id - break; - if(scan_transparent) - break; - - // Set flags to see through transparent objects - this.dphitcontentsmask |= DPCONTENTS_OPAQUE; - - head = head2; - scan_transparent = true; - } - - // Restore hit flags - this.dphitcontentsmask = hf; - - this.enemy = best; - this.havocbot_stickenemy = true; - if(best && best.classname == "misc_breakablemodel") - this.havocbot_stickenemy = false; -} - -float havocbot_chooseweapon_checkreload(entity this, int new_weapon) -{ - // bots under this skill cannot find unloaded weapons to reload idly when not in combat, - // so skip this for them, or they'll never get to reload their weapons at all. - // this also allows bots under this skill to be more stupid, and reload more often during combat :) - if(skill < 5) - return false; - - // if this weapon is scheduled for reloading, don't switch to it during combat - if (this.weapon_load[new_weapon] < 0) - { - bool other_weapon_available = false; - FOREACH(Weapons, it != WEP_Null, LAMBDA( - if(it.wr_checkammo1(it, this) + it.wr_checkammo2(it, this)) - other_weapon_available = true; - )); - if(other_weapon_available) - return true; - } - - return false; -} - -void havocbot_chooseweapon(entity this) -{ - int i; - - // ;) - if(g_weaponarena_weapons == WEPSET(TUBA)) - { - PS(this).m_switchweapon = WEP_TUBA; - return; - } - - // TODO: clean this up by moving it to weapon code - if(this.enemy==NULL) - { - // If no weapon was chosen get the first available weapon - if(PS(this).m_weapon==WEP_Null) - FOREACH(Weapons, it != WEP_Null, LAMBDA( - if(client_hasweapon(this, it, true, false)) - { - PS(this).m_switchweapon = it; - return; - } - )); - return; - } - - // Do not change weapon during the next second after a combo - float f = time - this.lastcombotime; - if(f < 1) - return; - - float w; - float distance; distance=bound(10,vlen(this.origin-this.enemy.origin)-200,10000); - - // Should it do a weapon combo? - float af, ct, combo_time, combo; - - af = ATTACK_FINISHED(this, 0); - ct = autocvar_bot_ai_weapon_combo_threshold; - - // Bots with no skill will be 4 times more slower than "godlike" bots when doing weapon combos - // Ideally this 4 should be calculated as longest_weapon_refire / bot_ai_weapon_combo_threshold - combo_time = time + ct + (ct * ((-0.3*(skill+this.bot_weaponskill))+3)); - - combo = false; - - if(autocvar_bot_ai_weapon_combo) - if(PS(this).m_weapon.m_id == this.lastfiredweapon) - if(af > combo_time) - { - combo = true; - this.lastcombotime = time; - } - - distance *= pow(2, this.bot_rangepreference); - - // Custom weapon list based on distance to the enemy - if(bot_custom_weapon){ - - // Choose weapons for far distance - if ( distance > bot_distance_far ) { - for(i=0; i < Weapons_COUNT && bot_weapons_far[i] != -1 ; ++i){ - w = bot_weapons_far[i]; - if ( client_hasweapon(this, Weapons_from(w), true, false) ) - { - if ((PS(this).m_weapon.m_id == w && combo) || havocbot_chooseweapon_checkreload(this, w)) - continue; - PS(this).m_switchweapon = Weapons_from(w); - return; - } - } - } - - // Choose weapons for mid distance - if ( distance > bot_distance_close) { - for(i=0; i < Weapons_COUNT && bot_weapons_mid[i] != -1 ; ++i){ - w = bot_weapons_mid[i]; - if ( client_hasweapon(this, Weapons_from(w), true, false) ) - { - if ((PS(this).m_weapon.m_id == w && combo) || havocbot_chooseweapon_checkreload(this, w)) - continue; - PS(this).m_switchweapon = Weapons_from(w); - return; - } - } - } - - // Choose weapons for close distance - for(i=0; i < Weapons_COUNT && bot_weapons_close[i] != -1 ; ++i){ - w = bot_weapons_close[i]; - if ( client_hasweapon(this, Weapons_from(w), true, false) ) - { - if ((PS(this).m_weapon.m_id == w && combo) || havocbot_chooseweapon_checkreload(this, w)) - continue; - PS(this).m_switchweapon = Weapons_from(w); - return; - } - } - } -} - -void havocbot_aim(entity this) -{ - vector myvel, enemyvel; -// if(this.flags & FL_INWATER) -// return; - if (time < this.nextaim) - return; - this.nextaim = time + 0.1; - myvel = this.velocity; - if (!this.waterlevel) - myvel.z = 0; - if (this.enemy) - { - enemyvel = this.enemy.velocity; - if (!this.enemy.waterlevel) - enemyvel.z = 0; - lag_additem(this, time + this.ping, 0, 0, this.enemy, this.origin, myvel, (this.enemy.absmin + this.enemy.absmax) * 0.5, enemyvel); - } - else - lag_additem(this, time + this.ping, 0, 0, NULL, this.origin, myvel, ( this.goalcurrent.absmin + this.goalcurrent.absmax ) * 0.5, '0 0 0'); -} - -bool havocbot_moveto_refresh_route(entity this) -{ - // Refresh path to goal if necessary - entity wp; - wp = this.havocbot_personal_waypoint; - navigation_goalrating_start(this); - navigation_routerating(this, wp, 10000, 10000); - navigation_goalrating_end(this); - return this.navigation_hasgoals; -} - -float havocbot_moveto(entity this, vector pos) -{ - entity wp; - - if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) - { - // Step 4: Move to waypoint - if(this.havocbot_personal_waypoint==NULL) - { - LOG_TRACE("Error: ", this.netname, " trying to walk to a non existent personal waypoint\n"); - this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING; - return CMD_STATUS_ERROR; - } - - if (!bot_strategytoken_taken) - if(this.havocbot_personal_waypoint_searchtime= 30) - { - LOG_TRACE("Warning: can't walk to the personal waypoint located at ", vtos(this.havocbot_personal_waypoint.origin),"\n"); - this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_LINKING; - delete(this.havocbot_personal_waypoint); - return CMD_STATUS_ERROR; - } - else - LOG_TRACE(this.netname, " can't walk to its personal waypoint (after ", ftos(this.havocbot_personal_waypoint_failcounter), " failed attempts), trying later\n"); - } - } - - if(autocvar_bot_debug_goalstack) - debuggoalstack(this); - - // Heading - vector dir = ( ( this.goalcurrent.absmin + this.goalcurrent.absmax ) * 0.5 ) - (this.origin + this.view_ofs); - dir.z = 0; - bot_aimdir(this, dir, -1); - - // Go! - havocbot_movetogoal(this); - - if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_REACHED) - { - // Step 5: Waypoint reached - LOG_TRACE(this.netname, "'s personal waypoint reached\n"); - delete(this.havocbot_personal_waypoint); - this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_REACHED; - return CMD_STATUS_FINISHED; - } - - return CMD_STATUS_EXECUTING; - } - - // Step 2: Linking waypoint - if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_LINKING) - { - // Wait until it is linked - if(!this.havocbot_personal_waypoint.wplinked) - { - LOG_TRACE(this.netname, " waiting for personal waypoint to be linked\n"); - return CMD_STATUS_EXECUTING; - } - - this.havocbot_personal_waypoint_searchtime = time; // so we set the route next frame - this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_LINKING; - this.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_GOING; - - // Step 3: Route to waypoint - LOG_TRACE(this.netname, " walking to its personal waypoint\n"); - - return CMD_STATUS_EXECUTING; - } - - // Step 1: Spawning waypoint - wp = waypoint_spawnpersonal(this, pos); - if(wp==NULL) - { - LOG_TRACE("Error: Can't spawn personal waypoint at ",vtos(pos),"\n"); - return CMD_STATUS_ERROR; - } - - this.havocbot_personal_waypoint = wp; - this.havocbot_personal_waypoint_failcounter = 0; - this.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_LINKING; - - // if pos is inside a teleport, then let's mark it as teleport waypoint - FOREACH_ENTITY_CLASS("trigger_teleport", WarpZoneLib_BoxTouchesBrush(pos, pos, it, NULL), - { - wp.wpflags |= WAYPOINTFLAG_TELEPORT; - this.lastteleporttime = 0; - }); - -/* - if(wp.wpflags & WAYPOINTFLAG_TELEPORT) - print("routing to a teleporter\n"); - else - print("routing to a non-teleporter\n"); -*/ - - return CMD_STATUS_EXECUTING; -} - -float havocbot_resetgoal(entity this) -{ - navigation_clearroute(this); - return CMD_STATUS_FINISHED; -} - -void havocbot_setupbot(entity this) -{ - this.bot_ai = havocbot_ai; - this.cmd_moveto = havocbot_moveto; - this.cmd_resetgoal = havocbot_resetgoal; - - havocbot_chooserole(this); -} - -vector havocbot_dodge(entity this) -{ - // LordHavoc: disabled because this is too expensive - return '0 0 0'; -#if 0 - entity head; - vector dodge, v, n; - float danger, bestdanger, vl, d; - dodge = '0 0 0'; - bestdanger = -20; - // check for dangerous objects near bot or approaching bot - head = findchainfloat(bot_dodge, true); - while(head) - { - if (head.owner != this) - { - vl = vlen(head.velocity); - if (vl > autocvar_sv_maxspeed * 0.3) - { - n = normalize(head.velocity); - v = this.origin - head.origin; - d = v * n; - if (d > (0 - head.bot_dodgerating)) - if (d < (vl * 0.2 + head.bot_dodgerating)) - { - // calculate direction and distance from the flight path, by removing the forward axis - v = v - (n * (v * n)); - danger = head.bot_dodgerating - vlen(v); - if (bestdanger < danger) - { - bestdanger = danger; - // dodge to the side of the object - dodge = normalize(v); - } - } - } - else - { - danger = head.bot_dodgerating - vlen(head.origin - this.origin); - if (bestdanger < danger) - { - bestdanger = danger; - dodge = normalize(this.origin - head.origin); - } - } - } - head = head.chain; - } - return dodge; -#endif -} diff --git a/qcsrc/server/bot/havocbot/havocbot.qh b/qcsrc/server/bot/havocbot/havocbot.qh deleted file mode 100644 index 4a391b6e7..000000000 --- a/qcsrc/server/bot/havocbot/havocbot.qh +++ /dev/null @@ -1,64 +0,0 @@ -#pragma once - -/* - * Globals and Fields - */ - -.float havocbot_keyboardskill; -.float facingwalltime, ignoregoaltime; -.float lastfiredweapon; -.float lastcombotime; -.float havocbot_blockhead; - -.float havocbot_keyboardtime; -.float havocbot_ducktime; -.float bot_timelastseengoal; -.float bot_canruntogoal; -.float bot_chooseweapontime; -.float rocketjumptime; -.float nextaim; -.float havocbot_personal_waypoint_searchtime; -.float havocbot_personal_waypoint_failcounter; -.float havocbot_chooseenemy_finished; -.float havocbot_stickenemy; -.float havocbot_role_timeout; - -.entity ignoregoal; -.entity bot_lastseengoal; -.entity havocbot_personal_waypoint; - -.vector havocbot_keyboard; - -/* - * Functions - */ - -void havocbot_ai(entity this); -void havocbot_aim(entity this); -void havocbot_setupbot(entity this); -void havocbot_movetogoal(entity this); -void havocbot_chooserole(entity this); -void havocbot_chooseenemy(entity this); -void havocbot_chooseweapon(entity this); -void havocbot_bunnyhop(entity this, vector dir); -void havocbot_keyboard_movement(entity this, vector destorg); - -float havocbot_resetgoal(entity this); -float havocbot_moveto(entity this, vector pos); -float havocbot_moveto_refresh_route(entity this); - -vector havocbot_dodge(entity this); - -.void(entity this) havocbot_role; -.void(entity this) havocbot_previous_role; - -void(entity this, float ratingscale, vector org, float sradius) havocbot_goalrating_items; -void(entity this, float ratingscale, vector org, float sradius) havocbot_goalrating_enemyplayers; - -/* - * Imports - */ - -.entity draggedby; -.float ladder_time; -.entity ladder_entity; diff --git a/qcsrc/server/bot/havocbot/roles.qc b/qcsrc/server/bot/havocbot/roles.qc deleted file mode 100644 index 9ea6a3d90..000000000 --- a/qcsrc/server/bot/havocbot/roles.qc +++ /dev/null @@ -1,208 +0,0 @@ -#include "roles.qh" - -#include "havocbot.qh" - -#include "../bot.qh" -#include "../navigation.qh" - -.float max_armorvalue; -.float havocbot_role_timeout; - -.void(entity this) havocbot_previous_role; -.void(entity this) havocbot_role; - -void havocbot_goalrating_items(entity this, float ratingscale, vector org, float sradius) -{ - float rating, d, discard, friend_distance, enemy_distance; - vector o; - ratingscale = ratingscale * 0.0001; // items are rated around 10000 already - - FOREACH_ENTITY_FLOAT(bot_pickup, true, - { - o = (it.absmin + it.absmax) * 0.5; - friend_distance = 10000; enemy_distance = 10000; - rating = 0; - - if(!it.solid || vdist(o - org, >, sradius) || (it == this.ignoregoal && time < this.ignoregoaltime) ) - continue; - - // Check if the item can be picked up safely - if(it.classname == "droppedweapon") - { - traceline(o, o + '0 0 -1500', true, NULL); - - d = pointcontents(trace_endpos + '0 0 1'); - if(d & CONTENT_WATER || d & CONTENT_SLIME || d & CONTENT_LAVA) - continue; - if(tracebox_hits_trigger_hurt(it.origin, it.mins, it.maxs, trace_endpos)) - continue; - } - else - { - // Ignore items under water - traceline(it.origin + it.maxs, it.origin + it.maxs, MOVE_NORMAL, it); - if(trace_dpstartcontents & DPCONTENTS_LIQUIDSMASK) - continue; - } - - if(teamplay) - { - discard = false; - - entity picker = it; - FOREACH_CLIENT(IS_PLAYER(it) && it != this && !IS_DEAD(it), - { - d = vlen(it.origin - o); // distance between player and item - - if ( it.team == this.team ) - { - if ( !IS_REAL_CLIENT(it) || discard ) - continue; - - if( d > friend_distance) - continue; - - friend_distance = d; - - discard = true; - - if( picker.health && it.health > this.health ) - continue; - - if( picker.armorvalue && it.armorvalue > this.armorvalue) - continue; - - if( picker.weapons ) - if( picker.weapons & ~it.weapons ) - continue; - - if (picker.ammo_shells && it.ammo_shells > this.ammo_shells) - continue; - - if (picker.ammo_nails && it.ammo_nails > this.ammo_nails) - continue; - - if (picker.ammo_rockets && it.ammo_rockets > this.ammo_rockets) - continue; - - if (picker.ammo_cells && it.ammo_cells > this.ammo_cells) - continue; - - if (picker.ammo_plasma && it.ammo_plasma > this.ammo_plasma) - continue; - - discard = false; - } - else - { - // If enemy only track distances - // TODO: track only if visible ? - if( d < enemy_distance ) - enemy_distance = d; - } - }); - - // Rate the item only if no one needs it, or if an enemy is closer to it - if ( (enemy_distance < friend_distance && vdist(o - org, <, enemy_distance)) || - (friend_distance > autocvar_bot_ai_friends_aware_pickup_radius ) || !discard ) - rating = it.bot_pickupevalfunc(this, it); - - } - else - rating = it.bot_pickupevalfunc(this, it); - - if(rating > 0) - navigation_routerating(this, it, rating * ratingscale, 2000); - }); -} - -void havocbot_goalrating_controlpoints(entity this, float ratingscale, vector org, float sradius) -{ - FOREACH_ENTITY_CLASS("dom_controlpoint", vdist((((it.absmin + it.absmax) * 0.5) - org), <, sradius), - { - if(it.cnt > -1) // this is just being fought - navigation_routerating(this, it, ratingscale, 5000); - else if(it.goalentity.cnt == 0) // unclaimed - navigation_routerating(this, it, ratingscale * 0.5, 5000); - else if(it.goalentity.team != this.team) // other team's point - navigation_routerating(this, it, ratingscale * 0.2, 5000); - }); -} - -void havocbot_goalrating_enemyplayers(entity this, float ratingscale, vector org, float sradius) -{ - if (autocvar_bot_nofire) - return; - - // don't chase players if we're under water - if(this.waterlevel>WATERLEVEL_WETFEET) - return; - - int t; - - FOREACH_CLIENT(IS_PLAYER(it) && bot_shouldattack(this, it), LAMBDA( - // TODO: Merge this logic with the bot_shouldattack function - if(vdist(it.origin - org, <, 100) || vdist(it.origin - org, >, sradius)) - continue; - - // rate only visible enemies - /* - traceline(this.origin + this.view_ofs, it.origin, MOVE_NOMONSTERS, this); - if (trace_fraction < 1 || trace_ent != it) - continue; - */ - - if((it.flags & FL_INWATER) || (it.flags & FL_PARTIALGROUND)) - continue; - - // not falling - if((IS_ONGROUND(it)) == 0) - { - traceline(it.origin, it.origin + '0 0 -1500', true, NULL); - t = pointcontents(trace_endpos + '0 0 1'); - if(t != CONTENT_SOLID ) - if(t & CONTENT_WATER || t & CONTENT_SLIME || t & CONTENT_LAVA) - continue; - if(tracebox_hits_trigger_hurt(it.origin, it.mins, it.maxs, trace_endpos)) - continue; - } - - // TODO: rate waypoints near the targetted player at that moment, instead of the player itthis - // adding a player as a goal seems to be quite dangerous, especially on space maps - // remove hack in navigation_poptouchedgoals() after performing this change - - t = (this.health + this.armorvalue ) / (it.health + it.armorvalue ); - navigation_routerating(this, it, t * ratingscale, 2000); - )); -} - -// legacy bot role for standard gamemodes -// go to best items -void havocbot_role_generic(entity this) -{ - if(IS_DEAD(this)) - return; - - if (this.bot_strategytime < time) - { - this.bot_strategytime = time + autocvar_bot_ai_strategyinterval; - navigation_goalrating_start(this); - havocbot_goalrating_items(this, 10000, this.origin, 10000); - havocbot_goalrating_enemyplayers(this, 20000, this.origin, 10000); - //havocbot_goalrating_waypoints(1, this.origin, 1000); - navigation_goalrating_end(this); - } -} - -void havocbot_chooserole_generic(entity this) -{ - this.havocbot_role = havocbot_role_generic; -} - -void havocbot_chooserole(entity this) -{ - LOG_TRACE("choosing a role...\n"); - this.bot_strategytime = 0; - if(!MUTATOR_CALLHOOK(HavocBot_ChooseRole, this)) - havocbot_chooserole_generic(this); -} diff --git a/qcsrc/server/bot/havocbot/roles.qh b/qcsrc/server/bot/havocbot/roles.qh deleted file mode 100644 index 5b1f2b530..000000000 --- a/qcsrc/server/bot/havocbot/roles.qh +++ /dev/null @@ -1,2 +0,0 @@ -#pragma once -void havocbot_goalrating_controlpoints(entity this, float ratingscale, vector org, float sradius); diff --git a/qcsrc/server/bot/havocbot/scripting.qh b/qcsrc/server/bot/havocbot/scripting.qh deleted file mode 100644 index 07cb4d6e6..000000000 --- a/qcsrc/server/bot/havocbot/scripting.qh +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -void bot_clearqueue(entity bot); diff --git a/qcsrc/server/bot/navigation.qc b/qcsrc/server/bot/navigation.qc deleted file mode 100644 index c9418e2e3..000000000 --- a/qcsrc/server/bot/navigation.qc +++ /dev/null @@ -1,1211 +0,0 @@ -#include "navigation.qh" - -#include "bot.qh" -#include "waypoints.qh" - -#include - -#include - -#include -#include - -.float speed; - -// rough simulation of walking from one point to another to test if a path -// can be traveled, used for waypoint linking and havocbot - -bool tracewalk(entity e, vector start, vector m1, vector m2, vector end, float movemode) -{ - vector org; - vector move; - vector dir; - float dist; - float totaldist; - float stepdist; - float yaw; - float ignorehazards; - float swimming; - - if(autocvar_bot_debug_tracewalk) - { - debugresetnodes(); - debugnode(e, start); - } - - move = end - start; - move.z = 0; - org = start; - dist = totaldist = vlen(move); - dir = normalize(move); - stepdist = 32; - ignorehazards = false; - swimming = false; - - // Analyze starting point - traceline(start, start, MOVE_NORMAL, e); - if (trace_dpstartcontents & (DPCONTENTS_SLIME | DPCONTENTS_LAVA)) - ignorehazards = true; - else - { - traceline( start, start + '0 0 -65536', MOVE_NORMAL, e); - if (trace_dpstartcontents & (DPCONTENTS_SLIME | DPCONTENTS_LAVA)) - { - ignorehazards = true; - swimming = true; - } - } - tracebox(start, m1, m2, start, MOVE_NOMONSTERS, e); - if (trace_startsolid) - { - // Bad start - if(autocvar_bot_debug_tracewalk) - debugnodestatus(start, DEBUG_NODE_FAIL); - - //print("tracewalk: ", vtos(start), " is a bad start\n"); - return false; - } - - // Movement loop - yaw = vectoyaw(move); - move = end - org; - for (;;) - { - if (boxesoverlap(end, end, org + m1 + '-1 -1 -1', org + m2 + '1 1 1')) - { - // Succeeded - if(autocvar_bot_debug_tracewalk) - debugnodestatus(org, DEBUG_NODE_SUCCESS); - - //print("tracewalk: ", vtos(start), " can reach ", vtos(end), "\n"); - return true; - } - if(autocvar_bot_debug_tracewalk) - debugnode(e, org); - - if (dist <= 0) - break; - if (stepdist > dist) - stepdist = dist; - dist = dist - stepdist; - traceline(org, org, MOVE_NORMAL, e); - if (!ignorehazards) - { - if (trace_dpstartcontents & (DPCONTENTS_SLIME | DPCONTENTS_LAVA)) - { - // hazards blocking path - if(autocvar_bot_debug_tracewalk) - debugnodestatus(org, DEBUG_NODE_FAIL); - - //print("tracewalk: ", vtos(start), " hits a hazard when trying to reach ", vtos(end), "\n"); - return false; - } - } - if (trace_dpstartcontents & DPCONTENTS_LIQUIDSMASK) - { - move = normalize(end - org); - tracebox(org, m1, m2, org + move * stepdist, movemode, e); - - if(autocvar_bot_debug_tracewalk) - debugnode(e, trace_endpos); - - if (trace_fraction < 1) - { - swimming = true; - org = trace_endpos - normalize(org - trace_endpos) * stepdist; - for (; org.z < end.z + e.maxs.z; org.z += stepdist) - { - if(autocvar_bot_debug_tracewalk) - debugnode(e, org); - - if(pointcontents(org) == CONTENT_EMPTY) - break; - } - - if(pointcontents(org + '0 0 1') != CONTENT_EMPTY) - { - if(autocvar_bot_debug_tracewalk) - debugnodestatus(org, DEBUG_NODE_FAIL); - - return false; - //print("tracewalk: ", vtos(start), " failed under water\n"); - } - continue; - - } - else - org = trace_endpos; - } - else - { - move = dir * stepdist + org; - tracebox(org, m1, m2, move, movemode, e); - - if(autocvar_bot_debug_tracewalk) - debugnode(e, trace_endpos); - - // hit something - if (trace_fraction < 1) - { - // check if we can walk over this obstacle, possibly by jumpstepping - tracebox(org + stepheightvec, m1, m2, move + stepheightvec, movemode, e); - if (trace_fraction < 1 || trace_startsolid) - { - tracebox(org + jumpstepheightvec, m1, m2, move + jumpstepheightvec, movemode, e); - if (trace_fraction < 1 || trace_startsolid) - { - if(autocvar_bot_debug_tracewalk) - debugnodestatus(trace_endpos, DEBUG_NODE_WARNING); - - // check for doors - traceline( org, move, movemode, e); - if ( trace_ent.classname == "door_rotating" || trace_ent.classname == "door") - { - vector nextmove; - move = trace_endpos; - while(trace_ent.classname == "door_rotating" || trace_ent.classname == "door") - { - nextmove = move + (dir * stepdist); - traceline( move, nextmove, movemode, e); - move = nextmove; - } - } - else - { - if(autocvar_bot_debug_tracewalk) - debugnodestatus(trace_endpos, DEBUG_NODE_FAIL); - - //print("tracewalk: ", vtos(start), " hit something when trying to reach ", vtos(end), "\n"); - //te_explosion(trace_endpos); - //print(ftos(e.dphitcontentsmask), "\n"); - return false; // failed - } - } - else - move = trace_endpos; - } - else - move = trace_endpos; - } - else - move = trace_endpos; - - // trace down from stepheight as far as possible and move there, - // if this starts in solid we try again without the stepup, and - // if that also fails we assume it is a wall - // (this is the same logic as the Quake walkmove function used) - tracebox(move, m1, m2, move + '0 0 -65536', movemode, e); - - // moved successfully - if(swimming) - { - float c; - c = pointcontents(org + '0 0 1'); - if (!(c == CONTENT_WATER || c == CONTENT_LAVA || c == CONTENT_SLIME)) - swimming = false; - else - continue; - } - - org = trace_endpos; - } - } - - //print("tracewalk: ", vtos(start), " did not arrive at ", vtos(end), " but at ", vtos(org), "\n"); - - // moved but didn't arrive at the intended destination - if(autocvar_bot_debug_tracewalk) - debugnodestatus(org, DEBUG_NODE_FAIL); - - return false; -} - -///////////////////////////////////////////////////////////////////////////// -// goal stack -///////////////////////////////////////////////////////////////////////////// - -// completely empty the goal stack, used when deciding where to go -void navigation_clearroute(entity this) -{ - //print("bot ", etos(this), " clear\n"); - this.navigation_hasgoals = false; - this.goalcurrent = NULL; - this.goalstack01 = NULL; - this.goalstack02 = NULL; - this.goalstack03 = NULL; - this.goalstack04 = NULL; - this.goalstack05 = NULL; - this.goalstack06 = NULL; - this.goalstack07 = NULL; - this.goalstack08 = NULL; - this.goalstack09 = NULL; - this.goalstack10 = NULL; - this.goalstack11 = NULL; - this.goalstack12 = NULL; - this.goalstack13 = NULL; - this.goalstack14 = NULL; - this.goalstack15 = NULL; - this.goalstack16 = NULL; - this.goalstack17 = NULL; - this.goalstack18 = NULL; - this.goalstack19 = NULL; - this.goalstack20 = NULL; - this.goalstack21 = NULL; - this.goalstack22 = NULL; - this.goalstack23 = NULL; - this.goalstack24 = NULL; - this.goalstack25 = NULL; - this.goalstack26 = NULL; - this.goalstack27 = NULL; - this.goalstack28 = NULL; - this.goalstack29 = NULL; - this.goalstack30 = NULL; - this.goalstack31 = NULL; -} - -// add a new goal at the beginning of the stack -// (in other words: add a new prerequisite before going to the later goals) -// NOTE: when a waypoint is added, the WP gets pushed first, then the -// next-closest WP on the shortest path to the WP -// That means, if the stack overflows, the bot will know how to do the FIRST 32 -// steps to the goal, and then recalculate the path. -void navigation_pushroute(entity this, entity e) -{ - //print("bot ", etos(this), " push ", etos(e), "\n"); - this.goalstack31 = this.goalstack30; - this.goalstack30 = this.goalstack29; - this.goalstack29 = this.goalstack28; - this.goalstack28 = this.goalstack27; - this.goalstack27 = this.goalstack26; - this.goalstack26 = this.goalstack25; - this.goalstack25 = this.goalstack24; - this.goalstack24 = this.goalstack23; - this.goalstack23 = this.goalstack22; - this.goalstack22 = this.goalstack21; - this.goalstack21 = this.goalstack20; - this.goalstack20 = this.goalstack19; - this.goalstack19 = this.goalstack18; - this.goalstack18 = this.goalstack17; - this.goalstack17 = this.goalstack16; - this.goalstack16 = this.goalstack15; - this.goalstack15 = this.goalstack14; - this.goalstack14 = this.goalstack13; - this.goalstack13 = this.goalstack12; - this.goalstack12 = this.goalstack11; - this.goalstack11 = this.goalstack10; - this.goalstack10 = this.goalstack09; - this.goalstack09 = this.goalstack08; - this.goalstack08 = this.goalstack07; - this.goalstack07 = this.goalstack06; - this.goalstack06 = this.goalstack05; - this.goalstack05 = this.goalstack04; - this.goalstack04 = this.goalstack03; - this.goalstack03 = this.goalstack02; - this.goalstack02 = this.goalstack01; - this.goalstack01 = this.goalcurrent; - this.goalcurrent = e; -} - -// remove first goal from stack -// (in other words: remove a prerequisite for reaching the later goals) -// (used when a spawnfunc_waypoint is reached) -void navigation_poproute(entity this) -{ - //print("bot ", etos(this), " pop\n"); - this.goalcurrent = this.goalstack01; - this.goalstack01 = this.goalstack02; - this.goalstack02 = this.goalstack03; - this.goalstack03 = this.goalstack04; - this.goalstack04 = this.goalstack05; - this.goalstack05 = this.goalstack06; - this.goalstack06 = this.goalstack07; - this.goalstack07 = this.goalstack08; - this.goalstack08 = this.goalstack09; - this.goalstack09 = this.goalstack10; - this.goalstack10 = this.goalstack11; - this.goalstack11 = this.goalstack12; - this.goalstack12 = this.goalstack13; - this.goalstack13 = this.goalstack14; - this.goalstack14 = this.goalstack15; - this.goalstack15 = this.goalstack16; - this.goalstack16 = this.goalstack17; - this.goalstack17 = this.goalstack18; - this.goalstack18 = this.goalstack19; - this.goalstack19 = this.goalstack20; - this.goalstack20 = this.goalstack21; - this.goalstack21 = this.goalstack22; - this.goalstack22 = this.goalstack23; - this.goalstack23 = this.goalstack24; - this.goalstack24 = this.goalstack25; - this.goalstack25 = this.goalstack26; - this.goalstack26 = this.goalstack27; - this.goalstack27 = this.goalstack28; - this.goalstack28 = this.goalstack29; - this.goalstack29 = this.goalstack30; - this.goalstack30 = this.goalstack31; - this.goalstack31 = NULL; -} - -float navigation_waypoint_will_link(vector v, vector org, entity ent, float walkfromwp, float bestdist) -{ - float dist; - dist = vlen(v - org); - if (bestdist > dist) - { - traceline(v, org, true, ent); - if (trace_fraction == 1) - { - if (walkfromwp) - { - if (tracewalk(ent, v, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), org, bot_navigation_movemode)) - return true; - } - else - { - if (tracewalk(ent, org, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), v, bot_navigation_movemode)) - return true; - } - } - } - return false; -} - -// find the spawnfunc_waypoint near a dynamic goal such as a dropped weapon -entity navigation_findnearestwaypoint_withdist_except(entity ent, float walkfromwp, float bestdist, entity except) -{ - vector pm1 = ent.origin + ent.mins; - vector pm2 = ent.origin + ent.maxs; - - // do two scans, because box test is cheaper - IL_EACH(g_waypoints, it != ent && it != except, - { - if(boxesoverlap(pm1, pm2, it.absmin, it.absmax)) - return it; - }); - - vector org = ent.origin + 0.5 * (ent.mins + ent.maxs); - org.z = ent.origin.z + ent.mins.z - STAT(PL_MIN, NULL).z; // player height - // TODO possibly make other code have the same support for bboxes - if(ent.tag_entity) - org = org + ent.tag_entity.origin; - if (navigation_testtracewalk) - te_plasmaburn(org); - - entity best = NULL; - vector v; - - // box check failed, try walk - IL_EACH(g_waypoints, it != ent, - { - if(it.wpisbox) - { - vector wm1 = it.origin + it.mins; - vector wm2 = it.origin + it.maxs; - v.x = bound(wm1_x, org.x, wm2_x); - v.y = bound(wm1_y, org.y, wm2_y); - v.z = bound(wm1_z, org.z, wm2_z); - } - else - v = it.origin; - if(navigation_waypoint_will_link(v, org, ent, walkfromwp, bestdist)) - { - bestdist = vlen(v - org); - best = it; - } - }); - return best; -} -entity navigation_findnearestwaypoint(entity ent, float walkfromwp) -{ - entity wp = navigation_findnearestwaypoint_withdist_except(ent, walkfromwp, 1050, NULL); - if (autocvar_g_waypointeditor_auto) - { - entity wp2 = navigation_findnearestwaypoint_withdist_except(ent, walkfromwp, 1050, wp); - if (wp && !wp2) - wp.wpflags |= WAYPOINTFLAG_PROTECTED; - } - return wp; -} - -// finds the waypoints near the bot initiating a navigation query -float navigation_markroutes_nearestwaypoints(entity this, float maxdist) -{ - vector v, m1, m2; -// navigation_testtracewalk = true; - int c = 0; - IL_EACH(g_waypoints, !it.wpconsidered, - { - if (it.wpisbox) - { - m1 = it.origin + it.mins; - m2 = it.origin + it.maxs; - v = this.origin; - v.x = bound(m1_x, v.x, m2_x); - v.y = bound(m1_y, v.y, m2_y); - v.z = bound(m1_z, v.z, m2_z); - } - else - v = it.origin; - vector diff = v - this.origin; - diff.z = max(0, diff.z); - if(vdist(diff, <, maxdist)) - { - it.wpconsidered = true; - if (tracewalk(this, this.origin, this.mins, this.maxs, v, bot_navigation_movemode)) - { - it.wpnearestpoint = v; - it.wpcost = vlen(v - this.origin) + it.dmg; - it.wpfire = 1; - it.enemy = NULL; - c = c + 1; - } - } - }); - //navigation_testtracewalk = false; - return c; -} - -// updates a path link if a spawnfunc_waypoint link is better than the current one -void navigation_markroutes_checkwaypoint(entity w, entity wp, float cost2, vector p) -{ - vector m1; - vector m2; - vector v; - if (wp.wpisbox) - { - m1 = wp.absmin; - m2 = wp.absmax; - v.x = bound(m1_x, p.x, m2_x); - v.y = bound(m1_y, p.y, m2_y); - v.z = bound(m1_z, p.z, m2_z); - } - else - v = wp.origin; - cost2 = cost2 + vlen(v - p); - if (wp.wpcost > cost2) - { - wp.wpcost = cost2; - wp.enemy = w; - wp.wpfire = 1; - wp.wpnearestpoint = v; - } -} - -// queries the entire spawnfunc_waypoint network for pathes leading away from the bot -void navigation_markroutes(entity this, entity fixed_source_waypoint) -{ - float cost, cost2; - vector p; - - IL_EACH(g_waypoints, true, - { - it.wpconsidered = false; - it.wpnearestpoint = '0 0 0'; - it.wpcost = 10000000; - it.wpfire = 0; - it.enemy = NULL; - }); - - if(fixed_source_waypoint) - { - fixed_source_waypoint.wpconsidered = true; - fixed_source_waypoint.wpnearestpoint = fixed_source_waypoint.origin + 0.5 * (fixed_source_waypoint.mins + fixed_source_waypoint.maxs); - fixed_source_waypoint.wpcost = fixed_source_waypoint.dmg; - fixed_source_waypoint.wpfire = 1; - fixed_source_waypoint.enemy = NULL; - } - else - { - // try a short range search for the nearest waypoints, and expand the search repeatedly if none are found - // as this search is expensive we will use lower values if the bot is on the air - float increment, maxdistance; - if(IS_ONGROUND(this)) - { - increment = 750; - maxdistance = 50000; - } - else - { - increment = 500; - maxdistance = 1500; - } - - for(int j = increment; !navigation_markroutes_nearestwaypoints(this, j) && j < maxdistance; j += increment); - } - - bool searching = true; - while (searching) - { - searching = false; - IL_EACH(g_waypoints, it.wpfire, - { - searching = true; - it.wpfire = 0; - cost = it.wpcost; - p = it.wpnearestpoint; - entity wp; - wp = it.wp00;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp00mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp01;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp01mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp02;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp02mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp03;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp03mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp04;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp04mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp05;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp05mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp06;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp06mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp07;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp07mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp08;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp08mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp09;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp09mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp10;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp10mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp11;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp11mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp12;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp12mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp13;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp13mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp14;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp14mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp15;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp15mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp16;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp16mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp17;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp17mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp18;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp18mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp19;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp19mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp20;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp20mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp21;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp21mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp22;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp22mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp23;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp23mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp24;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp24mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp25;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp25mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp26;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp26mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp27;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp27mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp28;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp28mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp29;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp29mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp30;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp30mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - wp = it.wp31;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + it.wp31mincost) navigation_markroutes_checkwaypoint(it, wp, cost2, p); - }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} - }); - } -} - -// queries the entire spawnfunc_waypoint network for pathes leading to the bot -void navigation_markroutes_inverted(entity fixed_source_waypoint) -{ - float cost, cost2; - vector p; - IL_EACH(g_waypoints, true, - { - it.wpconsidered = false; - it.wpnearestpoint = '0 0 0'; - it.wpcost = 10000000; - it.wpfire = 0; - it.enemy = NULL; - }); - - if(fixed_source_waypoint) - { - fixed_source_waypoint.wpconsidered = true; - fixed_source_waypoint.wpnearestpoint = fixed_source_waypoint.origin + 0.5 * (fixed_source_waypoint.mins + fixed_source_waypoint.maxs); - fixed_source_waypoint.wpcost = fixed_source_waypoint.dmg; // the cost to get from X to fixed_source_waypoint - fixed_source_waypoint.wpfire = 1; - fixed_source_waypoint.enemy = NULL; - } - else - { - error("need to start with a waypoint\n"); - } - - bool searching = true; - while (searching) - { - searching = false; - IL_EACH(g_waypoints, it.wpfire, - { - searching = true; - it.wpfire = 0; - cost = it.wpcost; // cost to walk from it to home - p = it.wpnearestpoint; - entity wp = it; - IL_EACH(g_waypoints, true, - { - if(wp != it.wp00) if(wp != it.wp01) if(wp != it.wp02) if(wp != it.wp03) - if(wp != it.wp04) if(wp != it.wp05) if(wp != it.wp06) if(wp != it.wp07) - if(wp != it.wp08) if(wp != it.wp09) if(wp != it.wp10) if(wp != it.wp11) - if(wp != it.wp12) if(wp != it.wp13) if(wp != it.wp14) if(wp != it.wp15) - if(wp != it.wp16) if(wp != it.wp17) if(wp != it.wp18) if(wp != it.wp19) - if(wp != it.wp20) if(wp != it.wp21) if(wp != it.wp22) if(wp != it.wp23) - if(wp != it.wp24) if(wp != it.wp25) if(wp != it.wp26) if(wp != it.wp27) - if(wp != it.wp28) if(wp != it.wp29) if(wp != it.wp30) if(wp != it.wp31) - continue; - cost2 = cost + it.dmg; - navigation_markroutes_checkwaypoint(wp, it, cost2, p); - }); - }); - } -} - -// updates the best goal according to a weighted calculation of travel cost and item value of a new proposed item -void navigation_routerating(entity this, entity e, float f, float rangebias) -{ - entity nwp; - vector o; - if (!e) - return; - - if(e.blacklisted) - return; - - o = (e.absmin + e.absmax) * 0.5; - - //print("routerating ", etos(e), " = ", ftos(f), " - ", ftos(rangebias), "\n"); - - // Evaluate path using jetpack - if(g_jetpack) - if(this.items & IT_JETPACK) - if(autocvar_bot_ai_navigation_jetpack) - if(vdist(this.origin - o, >, autocvar_bot_ai_navigation_jetpack_mindistance)) - { - vector pointa, pointb; - - LOG_DEBUG("jetpack ai: evaluating path for ", e.classname, "\n"); - - // Point A - traceline(this.origin, this.origin + '0 0 65535', MOVE_NORMAL, this); - pointa = trace_endpos - '0 0 1'; - - // Point B - traceline(o, o + '0 0 65535', MOVE_NORMAL, e); - pointb = trace_endpos - '0 0 1'; - - // Can I see these two points from the sky? - traceline(pointa, pointb, MOVE_NORMAL, this); - - if(trace_fraction==1) - { - LOG_DEBUG("jetpack ai: can bridge these two points\n"); - - // Lower the altitude of these points as much as possible - float zdistance, xydistance, cost, t, fuel; - vector down, npa, npb; - - down = '0 0 -1' * (STAT(PL_MAX, NULL).z - STAT(PL_MIN, NULL).z) * 10; - - do{ - npa = pointa + down; - npb = pointb + down; - - if(npa.z<=this.absmax.z) - break; - - if(npb.z<=e.absmax.z) - break; - - traceline(npa, npb, MOVE_NORMAL, this); - if(trace_fraction==1) - { - pointa = npa; - pointb = npb; - } - } - while(trace_fraction == 1); - - - // Rough estimation of fuel consumption - // (ignores acceleration and current xyz velocity) - xydistance = vlen(pointa - pointb); - zdistance = fabs(pointa.z - this.origin.z); - - t = zdistance / autocvar_g_jetpack_maxspeed_up; - t += xydistance / autocvar_g_jetpack_maxspeed_side; - fuel = t * autocvar_g_jetpack_fuel * 0.8; - - LOG_DEBUG(strcat("jetpack ai: required fuel ", ftos(fuel), " this.ammo_fuel ", ftos(this.ammo_fuel), "\n")); - - // enough fuel ? - if(this.ammo_fuel>fuel) - { - // Estimate cost - // (as onground costs calculation is mostly based on distances, here we do the same establishing some relationship - // - between air and ground speeds) - - cost = xydistance / (autocvar_g_jetpack_maxspeed_side/autocvar_sv_maxspeed); - cost += zdistance / (autocvar_g_jetpack_maxspeed_up/autocvar_sv_maxspeed); - cost *= 1.5; - - // Compare against other goals - f = f * rangebias / (rangebias + cost); - - if (navigation_bestrating < f) - { - LOG_DEBUG(strcat("jetpack path: added goal ", e.classname, " (with rating ", ftos(f), ")\n")); - navigation_bestrating = f; - navigation_bestgoal = e; - this.navigation_jetpack_goal = e; - this.navigation_jetpack_point = pointb; - } - return; - } - } - } - - //te_wizspike(e.origin); - //bprint(etos(e)); - //bprint("\n"); - // update the cached spawnfunc_waypoint link on a dynamic item entity - if(e.classname == "waypoint" && !(e.wpflags & WAYPOINTFLAG_PERSONAL)) - { - nwp = e; - } - else - { - float search; - - search = true; - - if(e.flags & FL_ITEM) - { - if (!(e.flags & FL_WEAPON)) - if(e.nearestwaypoint) - search = false; - } - else if (e.flags & FL_WEAPON) - { - if(e.classname != "droppedweapon") - if(e.nearestwaypoint) - search = false; - } - - if(search) - if (time > e.nearestwaypointtimeout) - { - nwp = navigation_findnearestwaypoint(e, true); - if(nwp) - e.nearestwaypoint = nwp; - else - { - LOG_DEBUG(strcat("FAILED to find a nearest waypoint to '", e.classname, "' #", etos(e), "\n")); - - if(e.flags & FL_ITEM) - e.blacklisted = true; - else if (e.flags & FL_WEAPON) - { - if(e.classname != "droppedweapon") - e.blacklisted = true; - } - - if(e.blacklisted) - { - LOG_DEBUG(strcat("The entity '", e.classname, "' is going to be excluded from path finding during this match\n")); - return; - } - } - - // TODO: Cleaner solution, probably handling this timeout from ctf.qc - if(e.classname=="item_flag_team") - e.nearestwaypointtimeout = time + 2; - else - e.nearestwaypointtimeout = time + random() * 3 + 5; - } - nwp = e.nearestwaypoint; - } - - LOG_DEBUG(strcat("-- checking ", e.classname, " (with cost ", ftos(nwp.wpcost), ")\n")); - if (nwp) - if (nwp.wpcost < 10000000) - { - //te_wizspike(nwp.wpnearestpoint); - LOG_DEBUG(strcat(e.classname, " ", ftos(f), "/(1+", ftos((nwp.wpcost + vlen(e.origin - nwp.wpnearestpoint))), "/", ftos(rangebias), ") = ")); - f = f * rangebias / (rangebias + (nwp.wpcost + vlen(o - nwp.wpnearestpoint))); - LOG_DEBUG(strcat("considering ", e.classname, " (with rating ", ftos(f), ")\n")); - if (navigation_bestrating < f) - { - LOG_DEBUG(strcat("ground path: added goal ", e.classname, " (with rating ", ftos(f), ")\n")); - navigation_bestrating = f; - navigation_bestgoal = e; - } - } -} - -// adds an item to the the goal stack with the path to a given item -bool navigation_routetogoal(entity this, entity e, vector startposition) -{ - this.goalentity = e; - - // if there is no goal, just exit - if (!e) - return false; - - this.navigation_hasgoals = true; - - // put the entity on the goal stack - //print("routetogoal ", etos(e), "\n"); - navigation_pushroute(this, e); - - if(g_jetpack) - if(e==this.navigation_jetpack_goal) - return true; - - // if it can reach the goal there is nothing more to do - if (tracewalk(this, startposition, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), (e.absmin + e.absmax) * 0.5, bot_navigation_movemode)) - return true; - - // see if there are waypoints describing a path to the item - if(e.classname != "waypoint" || (e.wpflags & WAYPOINTFLAG_PERSONAL)) - e = e.nearestwaypoint; - else - e = e.enemy; // we already have added it, so... - - if(e == NULL) - return false; - - for (;;) - { - // add the spawnfunc_waypoint to the path - navigation_pushroute(this, e); - e = e.enemy; - - if(e==NULL) - break; - } - - return false; -} - -// removes any currently touching waypoints from the goal stack -// (this is how bots detect if they reached a goal) -void navigation_poptouchedgoals(entity this) -{ - vector org, m1, m2; - org = this.origin; - m1 = org + this.mins; - m2 = org + this.maxs; - - if(this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT) - { - if(this.lastteleporttime>0) - if(time-this.lastteleporttime<(this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL)?2:0.15) - { - if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) - if(this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL && this.goalcurrent.owner==this) - { - this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING; - this.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED; - } - navigation_poproute(this); - return; - } - } - - // If for some reason the bot is closer to the next goal, pop the current one - if(this.goalstack01) - if(vlen2(this.goalcurrent.origin - this.origin) > vlen2(this.goalstack01.origin - this.origin)) - if(checkpvs(this.origin + this.view_ofs, this.goalstack01)) - if(tracewalk(this, this.origin, this.mins, this.maxs, (this.goalstack01.absmin + this.goalstack01.absmax) * 0.5, bot_navigation_movemode)) - { - LOG_DEBUG(strcat("path optimized for ", this.netname, ", removed a goal from the queue\n")); - navigation_poproute(this); - // TODO this may also be a nice idea to do "early" (e.g. by - // manipulating the vlen() comparisons) to shorten paths in - // general - this would make bots walk more "on rails" than - // "zigzagging" which they currently do with sufficiently - // random-like waypoints, and thus can make a nice bot - // personality property - } - - // HACK: remove players/bots as goals, they can lead a bot to unexpected places (cliffs, lava, etc) - // TODO: rate waypoints near the targetted player at that moment, instead of the player itthis - if(IS_PLAYER(this.goalcurrent)) - navigation_poproute(this); - - // aid for detecting jump pads better (distance based check fails sometimes) - if(this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT && this.jumppadcount > 0 ) - navigation_poproute(this); - - // Loose goal touching check when running - if(this.aistatus & AI_STATUS_RUNNING) - if(this.speed >= autocvar_sv_maxspeed) // if -really- running - if(this.goalcurrent.classname=="waypoint") - { - if(vdist(this.origin - this.goalcurrent.origin, <, 150)) - { - traceline(this.origin + this.view_ofs , this.goalcurrent.origin, true, NULL); - if(trace_fraction==1) - { - // Detect personal waypoints - if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) - if(this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL && this.goalcurrent.owner==this) - { - this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING; - this.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED; - } - - navigation_poproute(this); - } - } - } - - while (this.goalcurrent && boxesoverlap(m1, m2, this.goalcurrent.absmin, this.goalcurrent.absmax)) - { - // Detect personal waypoints - if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) - if(this.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL && this.goalcurrent.owner==this) - { - this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING; - this.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED; - } - - navigation_poproute(this); - } -} - -// begin a goal selection session (queries spawnfunc_waypoint network) -void navigation_goalrating_start(entity this) -{ - if(this.aistatus & AI_STATUS_STUCK) - return; - - this.navigation_jetpack_goal = NULL; - navigation_bestrating = -1; - this.navigation_hasgoals = false; - navigation_clearroute(this); - navigation_bestgoal = NULL; - navigation_markroutes(this, NULL); -} - -// ends a goal selection session (updates goal stack to the best goal) -void navigation_goalrating_end(entity this) -{ - if(this.aistatus & AI_STATUS_STUCK) - return; - - navigation_routetogoal(this, navigation_bestgoal, this.origin); - LOG_DEBUG(strcat("best goal ", this.goalcurrent.classname , "\n")); - - // If the bot got stuck then try to reach the farthest waypoint - if (!this.navigation_hasgoals) - if (autocvar_bot_wander_enable) - { - if (!(this.aistatus & AI_STATUS_STUCK)) - { - LOG_DEBUG(strcat(this.netname, " cannot walk to any goal\n")); - this.aistatus |= AI_STATUS_STUCK; - } - - this.navigation_hasgoals = false; // Reset this value - } -} - -void botframe_updatedangerousobjects(float maxupdate) -{ - vector m1, m2, v, o; - float c, d, danger; - c = 0; - IL_EACH(g_waypoints, true, - { - danger = 0; - m1 = it.mins; - m2 = it.maxs; - FOREACH_ENTITY_FLOAT(bot_dodge, true, - { - v = it.origin; - v.x = bound(m1_x, v.x, m2_x); - v.y = bound(m1_y, v.y, m2_y); - v.z = bound(m1_z, v.z, m2_z); - o = (it.absmin + it.absmax) * 0.5; - d = it.bot_dodgerating - vlen(o - v); - if (d > 0) - { - traceline(o, v, true, NULL); - if (trace_fraction == 1) - danger = danger + d; - } - }); - it.dmg = danger; - c = c + 1; - if (c >= maxupdate) - break; - }); -} - -void navigation_unstuck(entity this) -{ - float search_radius = 1000; - - if (!autocvar_bot_wander_enable) - return; - - if (!bot_waypoint_queue_owner) - { - LOG_DEBUG(strcat(this.netname, " sutck, taking over the waypoints queue\n")); - bot_waypoint_queue_owner = this; - bot_waypoint_queue_bestgoal = NULL; - bot_waypoint_queue_bestgoalrating = 0; - } - - if(bot_waypoint_queue_owner!=this) - return; - - if (bot_waypoint_queue_goal) - { - // evaluate the next goal on the queue - float d = vlen(this.origin - bot_waypoint_queue_goal.origin); - LOG_DEBUG(strcat(this.netname, " evaluating ", bot_waypoint_queue_goal.classname, " with distance ", ftos(d), "\n")); - if(tracewalk(bot_waypoint_queue_goal, this.origin, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), bot_waypoint_queue_goal.origin, bot_navigation_movemode)) - { - if( d > bot_waypoint_queue_bestgoalrating) - { - bot_waypoint_queue_bestgoalrating = d; - bot_waypoint_queue_bestgoal = bot_waypoint_queue_goal; - } - } - bot_waypoint_queue_goal = bot_waypoint_queue_goal.bot_waypoint_queue_nextgoal; - - if (!bot_waypoint_queue_goal) - { - if (bot_waypoint_queue_bestgoal) - { - LOG_DEBUG(strcat(this.netname, " stuck, reachable waypoint found, heading to it\n")); - navigation_routetogoal(this, bot_waypoint_queue_bestgoal, this.origin); - this.bot_strategytime = time + autocvar_bot_ai_strategyinterval; - this.aistatus &= ~AI_STATUS_STUCK; - } - else - { - LOG_DEBUG(strcat(this.netname, " stuck, cannot walk to any waypoint at all\n")); - } - - bot_waypoint_queue_owner = NULL; - } - } - else - { - if(bot_strategytoken!=this) - return; - - // build a new queue - LOG_DEBUG(strcat(this.netname, " stuck, scanning reachable waypoints within ", ftos(search_radius)," qu\n")); - - entity first = NULL; - - FOREACH_ENTITY_RADIUS(this.origin, search_radius, it.classname == "waypoint" && !(it.wpflags & WAYPOINTFLAG_GENERATED), - { - if(bot_waypoint_queue_goal) - bot_waypoint_queue_goal.bot_waypoint_queue_nextgoal = it; - else - first = it; - - bot_waypoint_queue_goal = it; - bot_waypoint_queue_goal.bot_waypoint_queue_nextgoal = NULL; - }); - - if (first) - bot_waypoint_queue_goal = first; - else - { - LOG_DEBUG(strcat(this.netname, " stuck, cannot walk to any waypoint at all\n")); - bot_waypoint_queue_owner = NULL; - } - } -} - -// Support for debugging tracewalk visually - -void debugresetnodes() -{ - debuglastnode = '0 0 0'; -} - -void debugnode(entity this, vector node) -{ - if (!IS_PLAYER(this)) - return; - - if(debuglastnode=='0 0 0') - { - debuglastnode = node; - return; - } - - te_lightning2(NULL, node, debuglastnode); - debuglastnode = node; -} - -void debugnodestatus(vector position, float status) -{ - vector c; - - switch (status) - { - case DEBUG_NODE_SUCCESS: - c = '0 15 0'; - break; - case DEBUG_NODE_WARNING: - c = '15 15 0'; - break; - case DEBUG_NODE_FAIL: - c = '15 0 0'; - break; - default: - c = '15 15 15'; - } - - te_customflash(position, 40, 2, c); -} - -// Support for debugging the goal stack visually - -.float goalcounter; -.vector lastposition; - -// Debug the goal stack visually -void debuggoalstack(entity this) -{ - entity goal; - vector org, go; - - if(this.goalcounter==0)goal=this.goalcurrent; - else if(this.goalcounter==1)goal=this.goalstack01; - else if(this.goalcounter==2)goal=this.goalstack02; - else if(this.goalcounter==3)goal=this.goalstack03; - else if(this.goalcounter==4)goal=this.goalstack04; - else if(this.goalcounter==5)goal=this.goalstack05; - else if(this.goalcounter==6)goal=this.goalstack06; - else if(this.goalcounter==7)goal=this.goalstack07; - else if(this.goalcounter==8)goal=this.goalstack08; - else if(this.goalcounter==9)goal=this.goalstack09; - else if(this.goalcounter==10)goal=this.goalstack10; - else if(this.goalcounter==11)goal=this.goalstack11; - else if(this.goalcounter==12)goal=this.goalstack12; - else if(this.goalcounter==13)goal=this.goalstack13; - else if(this.goalcounter==14)goal=this.goalstack14; - else if(this.goalcounter==15)goal=this.goalstack15; - else if(this.goalcounter==16)goal=this.goalstack16; - else if(this.goalcounter==17)goal=this.goalstack17; - else if(this.goalcounter==18)goal=this.goalstack18; - else if(this.goalcounter==19)goal=this.goalstack19; - else if(this.goalcounter==20)goal=this.goalstack20; - else if(this.goalcounter==21)goal=this.goalstack21; - else if(this.goalcounter==22)goal=this.goalstack22; - else if(this.goalcounter==23)goal=this.goalstack23; - else if(this.goalcounter==24)goal=this.goalstack24; - else if(this.goalcounter==25)goal=this.goalstack25; - else if(this.goalcounter==26)goal=this.goalstack26; - else if(this.goalcounter==27)goal=this.goalstack27; - else if(this.goalcounter==28)goal=this.goalstack28; - else if(this.goalcounter==29)goal=this.goalstack29; - else if(this.goalcounter==30)goal=this.goalstack30; - else if(this.goalcounter==31)goal=this.goalstack31; - else goal=NULL; - - if(goal==NULL) - { - this.goalcounter = 0; - this.lastposition='0 0 0'; - return; - } - - if(this.lastposition=='0 0 0') - org = this.origin; - else - org = this.lastposition; - - - go = ( goal.absmin + goal.absmax ) * 0.5; - te_lightning2(NULL, org, go); - this.lastposition = go; - - this.goalcounter++; -} diff --git a/qcsrc/server/bot/navigation.qh b/qcsrc/server/bot/navigation.qh deleted file mode 100644 index ad0177665..000000000 --- a/qcsrc/server/bot/navigation.qh +++ /dev/null @@ -1,77 +0,0 @@ -#pragma once -/* - * Globals and Fields - */ - -float navigation_bestrating; -float bot_navigation_movemode; -float navigation_testtracewalk; - -vector jumpstepheightvec; -vector stepheightvec; - -entity navigation_bestgoal; - -// stack of current goals (the last one of which may be an item or other -// desirable object, the rest are typically waypoints to reach it) -.entity goalcurrent, goalstack01, goalstack02, goalstack03; -.entity goalstack04, goalstack05, goalstack06, goalstack07; -.entity goalstack08, goalstack09, goalstack10, goalstack11; -.entity goalstack12, goalstack13, goalstack14, goalstack15; -.entity goalstack16, goalstack17, goalstack18, goalstack19; -.entity goalstack20, goalstack21, goalstack22, goalstack23; -.entity goalstack24, goalstack25, goalstack26, goalstack27; -.entity goalstack28, goalstack29, goalstack30, goalstack31; -.entity nearestwaypoint; - -.float nearestwaypointtimeout; -.float navigation_hasgoals; -.float lastteleporttime; - -.float blacklisted; - -.entity navigation_jetpack_goal; -.vector navigation_jetpack_point; - -const float DEBUG_NODE_SUCCESS = 1; -const float DEBUG_NODE_WARNING = 2; -const float DEBUG_NODE_FAIL = 3; -vector debuglastnode; - -entity bot_waypoint_queue_owner; // Owner of the temporary list of goals -entity bot_waypoint_queue_goal; // Head of the temporary list of goals -.entity bot_waypoint_queue_nextgoal; -entity bot_waypoint_queue_bestgoal; -float bot_waypoint_queue_bestgoalrating; - -/* - * Functions - */ - -void debugresetnodes(); -void debugnode(entity this, vector node); -void debugnodestatus(vector position, float status); - -void debuggoalstack(entity this); - -float tracewalk(entity e, vector start, vector m1, vector m2, vector end, float movemode); - -float navigation_markroutes_nearestwaypoints(entity this, float maxdist); -float navigation_routetogoal(entity this, entity e, vector startposition); - -void navigation_clearroute(entity this); -void navigation_pushroute(entity this, entity e); -void navigation_poproute(entity this); -void navigation_markroutes_checkwaypoint(entity w, entity wp, float cost2, vector p); -void navigation_markroutes(entity this, entity fixed_source_waypoint); -void navigation_markroutes_inverted(entity fixed_source_waypoint); -void navigation_routerating(entity this, entity e, float f, float rangebias); -void navigation_poptouchedgoals(entity this); -void navigation_goalrating_start(entity this); -void navigation_goalrating_end(entity this); -void navigation_unstuck(entity this); - -void botframe_updatedangerousobjects(float maxupdate); - -entity navigation_findnearestwaypoint(entity ent, float walkfromwp); -float navigation_waypoint_will_link(vector v, vector org, entity ent, float walkfromwp, float bestdist); diff --git a/qcsrc/server/bot/scripting.qc b/qcsrc/server/bot/scripting.qc deleted file mode 100644 index 291b22f5f..000000000 --- a/qcsrc/server/bot/scripting.qc +++ /dev/null @@ -1,1340 +0,0 @@ -#include "scripting.qh" - -#include -#include - -#include "bot.qh" - -.int state; - -.float bot_cmdqueuebuf_allocated; -.float bot_cmdqueuebuf; -.float bot_cmdqueuebuf_start; -.float bot_cmdqueuebuf_end; - -void bot_clearqueue(entity bot) -{ - if(!bot.bot_cmdqueuebuf_allocated) - return; - buf_del(bot.bot_cmdqueuebuf); - bot.bot_cmdqueuebuf_allocated = false; - LOG_TRACE("bot ", bot.netname, " queue cleared\n"); -} - -void bot_queuecommand(entity bot, string cmdstring) -{ - if(!bot.bot_cmdqueuebuf_allocated) - { - bot.bot_cmdqueuebuf = buf_create(); - bot.bot_cmdqueuebuf_allocated = true; - bot.bot_cmdqueuebuf_start = 0; - bot.bot_cmdqueuebuf_end = 0; - } - - bufstr_set(bot.bot_cmdqueuebuf, bot.bot_cmdqueuebuf_end, cmdstring); - - // if the command was a "sound" command, precache the sound NOW - // this prevents lagging! - { - float sp; - string parm; - string cmdstr; - - sp = strstrofs(cmdstring, " ", 0); - if(sp >= 0) - { - parm = substring(cmdstring, sp + 1, -1); - cmdstr = substring(cmdstring, 0, sp); - if(cmdstr == "sound") - { - // find the LAST word - for (;;) - { - sp = strstrofs(parm, " ", 0); - if(sp < 0) - break; - parm = substring(parm, sp + 1, -1); - } - precache_sound(parm); - } - } - } - - bot.bot_cmdqueuebuf_end += 1; -} - -void bot_dequeuecommand(entity bot, float idx) -{ - if(!bot.bot_cmdqueuebuf_allocated) - error("dequeuecommand but no queue allocated"); - if(idx < bot.bot_cmdqueuebuf_start) - error("dequeueing a command in the past"); - if(idx >= bot.bot_cmdqueuebuf_end) - error("dequeueing a command in the future"); - bufstr_set(bot.bot_cmdqueuebuf, idx, ""); - if(idx == bot.bot_cmdqueuebuf_start) - bot.bot_cmdqueuebuf_start += 1; - if(bot.bot_cmdqueuebuf_start >= bot.bot_cmdqueuebuf_end) - bot_clearqueue(bot); -} - -string bot_readcommand(entity bot, float idx) -{ - if(!bot.bot_cmdqueuebuf_allocated) - error("readcommand but no queue allocated"); - if(idx < bot.bot_cmdqueuebuf_start) - error("reading a command in the past"); - if(idx >= bot.bot_cmdqueuebuf_end) - error("reading a command in the future"); - return bufstr_get(bot.bot_cmdqueuebuf, idx); -} - -bool bot_havecommand(entity this, int idx) -{ - if(!this.bot_cmdqueuebuf_allocated) - return false; - if(idx < this.bot_cmdqueuebuf_start) - return false; - if(idx >= this.bot_cmdqueuebuf_end) - return false; - return true; -} - -const int MAX_BOT_PLACES = 4; -.float bot_places_count; -.entity bot_places[MAX_BOT_PLACES]; -.string bot_placenames[MAX_BOT_PLACES]; -entity bot_getplace(entity this, string placename) -{ - entity e; - if(substring(placename, 0, 1) == "@") - { - int i, p; - placename = substring(placename, 1, -1); - string s, s2; - for(i = 0; i < this.bot_places_count; ++i) - if(this.(bot_placenames[i]) == placename) - return this.(bot_places[i]); - // now: i == this.bot_places_count - s = s2 = cvar_string(placename); - p = strstrofs(s2, " ", 0); - if(p >= 0) - { - s = substring(s2, 0, p); - //print("places: ", placename, " -> ", cvar_string(placename), "\n"); - cvar_set(placename, strcat(substring(s2, p+1, -1), " ", s)); - //print("places: ", placename, " := ", cvar_string(placename), "\n"); - } - e = find(NULL, targetname, s); - if(!e) - LOG_INFO("invalid place ", s, "\n"); - if(i < MAX_BOT_PLACES) - { - this.(bot_placenames[i]) = strzone(placename); - this.(bot_places[i]) = e; - this.bot_places_count += 1; - } - return e; - } - else - { - e = find(NULL, targetname, placename); - if(!e) - LOG_INFO("invalid place ", placename, "\n"); - return e; - } -} - - -// Initialize global commands list -// NOTE: New commands should be initialized here -void bot_commands_init() -{ - bot_cmd_string[BOT_CMD_NULL] = ""; - bot_cmd_parm_type[BOT_CMD_NULL] = BOT_CMD_PARAMETER_NONE; - - bot_cmd_string[BOT_CMD_PAUSE] = "pause"; - bot_cmd_parm_type[BOT_CMD_PAUSE] = BOT_CMD_PARAMETER_NONE; - - bot_cmd_string[BOT_CMD_CONTINUE] = "continue"; - bot_cmd_parm_type[BOT_CMD_CONTINUE] = BOT_CMD_PARAMETER_NONE; - - bot_cmd_string[BOT_CMD_WAIT] = "wait"; - bot_cmd_parm_type[BOT_CMD_WAIT] = BOT_CMD_PARAMETER_FLOAT; - - bot_cmd_string[BOT_CMD_TURN] = "turn"; - bot_cmd_parm_type[BOT_CMD_TURN] = BOT_CMD_PARAMETER_FLOAT; - - bot_cmd_string[BOT_CMD_MOVETO] = "moveto"; - bot_cmd_parm_type[BOT_CMD_MOVETO] = BOT_CMD_PARAMETER_VECTOR; - - bot_cmd_string[BOT_CMD_MOVETOTARGET] = "movetotarget"; - bot_cmd_parm_type[BOT_CMD_MOVETOTARGET] = BOT_CMD_PARAMETER_STRING; - - bot_cmd_string[BOT_CMD_RESETGOAL] = "resetgoal"; - bot_cmd_parm_type[BOT_CMD_RESETGOAL] = BOT_CMD_PARAMETER_NONE; - - bot_cmd_string[BOT_CMD_CC] = "cc"; - bot_cmd_parm_type[BOT_CMD_CC] = BOT_CMD_PARAMETER_STRING; - - bot_cmd_string[BOT_CMD_IF] = "if"; - bot_cmd_parm_type[BOT_CMD_IF] = BOT_CMD_PARAMETER_STRING; - - bot_cmd_string[BOT_CMD_ELSE] = "else"; - bot_cmd_parm_type[BOT_CMD_ELSE] = BOT_CMD_PARAMETER_NONE; - - bot_cmd_string[BOT_CMD_FI] = "fi"; - bot_cmd_parm_type[BOT_CMD_FI] = BOT_CMD_PARAMETER_NONE; - - bot_cmd_string[BOT_CMD_RESETAIM] = "resetaim"; - bot_cmd_parm_type[BOT_CMD_RESETAIM] = BOT_CMD_PARAMETER_NONE; - - bot_cmd_string[BOT_CMD_AIM] = "aim"; - bot_cmd_parm_type[BOT_CMD_AIM] = BOT_CMD_PARAMETER_STRING; - - bot_cmd_string[BOT_CMD_AIMTARGET] = "aimtarget"; - bot_cmd_parm_type[BOT_CMD_AIMTARGET] = BOT_CMD_PARAMETER_STRING; - - bot_cmd_string[BOT_CMD_PRESSKEY] = "presskey"; - bot_cmd_parm_type[BOT_CMD_PRESSKEY] = BOT_CMD_PARAMETER_STRING; - - bot_cmd_string[BOT_CMD_RELEASEKEY] = "releasekey"; - bot_cmd_parm_type[BOT_CMD_RELEASEKEY] = BOT_CMD_PARAMETER_STRING; - - bot_cmd_string[BOT_CMD_SELECTWEAPON] = "selectweapon"; - bot_cmd_parm_type[BOT_CMD_SELECTWEAPON] = BOT_CMD_PARAMETER_FLOAT; - - bot_cmd_string[BOT_CMD_IMPULSE] = "impulse"; - bot_cmd_parm_type[BOT_CMD_IMPULSE] = BOT_CMD_PARAMETER_FLOAT; - - bot_cmd_string[BOT_CMD_WAIT_UNTIL] = "wait_until"; - bot_cmd_parm_type[BOT_CMD_WAIT_UNTIL] = BOT_CMD_PARAMETER_FLOAT; - - bot_cmd_string[BOT_CMD_BARRIER] = "barrier"; - bot_cmd_parm_type[BOT_CMD_BARRIER] = BOT_CMD_PARAMETER_NONE; - - bot_cmd_string[BOT_CMD_CONSOLE] = "console"; - bot_cmd_parm_type[BOT_CMD_CONSOLE] = BOT_CMD_PARAMETER_STRING; - - bot_cmd_string[BOT_CMD_SOUND] = "sound"; - bot_cmd_parm_type[BOT_CMD_SOUND] = BOT_CMD_PARAMETER_STRING; - - bot_cmd_string[BOT_CMD_DEBUG_ASSERT_CANFIRE] = "debug_assert_canfire"; - bot_cmd_parm_type[BOT_CMD_DEBUG_ASSERT_CANFIRE] = BOT_CMD_PARAMETER_FLOAT; - - bot_cmds_initialized = true; -} - -// Returns first bot with matching name -entity find_bot_by_name(string name) -{ - entity bot; - - bot = findchainflags(flags, FL_CLIENT); - while (bot) - { - if(IS_BOT_CLIENT(bot)) - if(bot.netname==name) - return bot; - - bot = bot.chain; - } - - return NULL; -} - -// Returns a bot by number on list -entity find_bot_by_number(float number) -{ - entity bot; - float c = 0; - - if(!number) - return NULL; - - bot = findchainflags(flags, FL_CLIENT); // TODO: doesn't findchainflags loop backwards through entities? - while (bot) - { - if(IS_BOT_CLIENT(bot)) - { - if(++c==number) - return bot; - } - bot = bot.chain; - } - - return NULL; -} - -float bot_decodecommand(string cmdstring) -{ - float cmd_parm_type; - float sp; - string parm; - - sp = strstrofs(cmdstring, " ", 0); - if(sp < 0) - { - parm = ""; - } - else - { - parm = substring(cmdstring, sp + 1, -1); - cmdstring = substring(cmdstring, 0, sp); - } - - if(!bot_cmds_initialized) - bot_commands_init(); - - int i; - for(i=1;i \n")); - - LOG_INFO("Description: "); - switch(i) - { - case BOT_CMD_PAUSE: - LOG_INFO("Stops the bot completely. Any command other than 'continue' will be ignored."); - break; - case BOT_CMD_CONTINUE: - LOG_INFO("Disable paused status"); - break; - case BOT_CMD_WAIT: - LOG_INFO("Pause command parsing and bot ai for N seconds. Pressed key will remain pressed"); - break; - case BOT_CMD_WAIT_UNTIL: - LOG_INFO("Pause command parsing and bot ai until time is N from the last barrier. Pressed key will remain pressed"); - break; - case BOT_CMD_BARRIER: - LOG_INFO("Waits till all bots that have a command queue reach this command. Pressed key will remain pressed"); - break; - case BOT_CMD_TURN: - LOG_INFO("Look to the right or left N degrees. For turning to the left use positive numbers."); - break; - case BOT_CMD_MOVETO: - LOG_INFO("Walk to an specific coordinate on the map. Usage: moveto \"x y z\""); - break; - case BOT_CMD_MOVETOTARGET: - LOG_INFO("Walk to the specific target on the map"); - break; - case BOT_CMD_RESETGOAL: - LOG_INFO("Resets the goal stack"); - break; - case BOT_CMD_CC: - LOG_INFO("Execute client command. Examples: cc \"say something\"; cc god; cc \"name newnickname\"; cc kill;"); - break; - case BOT_CMD_IF: - LOG_INFO("Perform simple conditional execution.\n"); - LOG_INFO("Syntax: \n"); - LOG_INFO(" sv_cmd .. if \"condition\"\n"); - LOG_INFO(" sv_cmd .. \n"); - LOG_INFO(" sv_cmd .. \n"); - LOG_INFO(" sv_cmd .. else\n"); - LOG_INFO(" sv_cmd .. \n"); - LOG_INFO(" sv_cmd .. \n"); - LOG_INFO(" sv_cmd .. fi\n"); - LOG_INFO("Conditions: a=b, a>b, a50; if health>cvar.g_balance_laser_primary_damage; if flagcarrier;"); - break; - case BOT_CMD_RESETAIM: - LOG_INFO("Points the aim to the coordinates x,y 0,0"); - break; - case BOT_CMD_AIM: - LOG_INFO("Move the aim x/y (horizontal/vertical) degrees relatives to the bot\n"); - LOG_INFO("There is a 3rd optional parameter telling in how many seconds the aim has to reach the new position\n"); - LOG_INFO("Examples: aim \"90 0\" // Turn 90 degrees inmediately (positive numbers move to the left/up)\n"); - LOG_INFO(" aim \"0 90 2\" // Will gradually look to the sky in the next two seconds"); - break; - case BOT_CMD_AIMTARGET: - LOG_INFO("Points the aim to given target"); - break; - case BOT_CMD_PRESSKEY: - LOG_INFO("Press one of the following keys: forward, backward, left, right, jump, crouch, attack1, attack2, use\n"); - LOG_INFO("Multiple keys can be pressed at time (with many presskey calls) and it will remain pressed until the command \"releasekey\" is called"); - LOG_INFO("Note: The script will not return the control to the bot ai until all keys are released"); - break; - case BOT_CMD_RELEASEKEY: - LOG_INFO("Release previoulsy used keys. Use the parameter \"all\" to release all keys"); - break; - case BOT_CMD_SOUND: - LOG_INFO("play sound file at bot location"); - break; - case BOT_CMD_DEBUG_ASSERT_CANFIRE: - LOG_INFO("verify the state of the weapon entity"); - break; - default: - LOG_INFO("This command has no description yet."); - break; - } - LOG_INFO("\n"); - } -} - -void bot_list_commands() -{ - int i; - string ptype; - - if(!bot_cmds_initialized) - bot_commands_init(); - - LOG_INFO("List of all available commands:\n"); - LOG_INFO(" Command - Parameter Type\n"); - - for(i=1;i \n")); - } -} - -// Commands code -.int bot_exec_status; - -float bot_cmd_cc(entity this) -{ - SV_ParseClientCommand(this, bot_cmd.bot_cmd_parm_string); - return CMD_STATUS_FINISHED; -} - -float bot_cmd_impulse(entity this) -{ - this.impulse = bot_cmd.bot_cmd_parm_float; - return CMD_STATUS_FINISHED; -} - -float bot_cmd_continue(entity this) -{ - this.bot_exec_status &= ~BOT_EXEC_STATUS_PAUSED; - return CMD_STATUS_FINISHED; -} - -.float bot_cmd_wait_time; -float bot_cmd_wait(entity this) -{ - if(this.bot_exec_status & BOT_EXEC_STATUS_WAITING) - { - if(time>=this.bot_cmd_wait_time) - { - this.bot_exec_status &= ~BOT_EXEC_STATUS_WAITING; - return CMD_STATUS_FINISHED; - } - else - return CMD_STATUS_EXECUTING; - } - - this.bot_cmd_wait_time = time + bot_cmd.bot_cmd_parm_float; - this.bot_exec_status |= BOT_EXEC_STATUS_WAITING; - return CMD_STATUS_EXECUTING; -} - -float bot_cmd_wait_until(entity this) -{ - if(time < bot_cmd.bot_cmd_parm_float + bot_barriertime) - { - this.bot_exec_status |= BOT_EXEC_STATUS_WAITING; - return CMD_STATUS_EXECUTING; - } - this.bot_exec_status &= ~BOT_EXEC_STATUS_WAITING; - return CMD_STATUS_FINISHED; -} - -float bot_cmd_barrier(entity this) -{ - // 0 = no barrier, 1 = waiting, 2 = waiting finished - - if(this.bot_barrier == 0) // initialization - { - this.bot_barrier = 1; - - //this.colormod = '4 4 0'; - } - - if(this.bot_barrier == 1) // find other bots - { - FOREACH_CLIENT(it.isbot, LAMBDA( - if(it.bot_cmdqueuebuf_allocated) - if(it.bot_barrier != 1) - return CMD_STATUS_EXECUTING; // not all are at the barrier yet - )); - - // all bots hit the barrier! - - // acknowledge barrier - FOREACH_CLIENT(it.isbot, LAMBDA(it.bot_barrier = 2)); - - bot_barriertime = time; - } - - // if we get here, the barrier is finished - // so end it... - this.bot_barrier = 0; - //this.colormod = '0 0 0'; - - return CMD_STATUS_FINISHED; -} - -float bot_cmd_turn(entity this) -{ - this.v_angle_y = this.v_angle.y + bot_cmd.bot_cmd_parm_float; - this.v_angle_y = this.v_angle.y - floor(this.v_angle.y / 360) * 360; - return CMD_STATUS_FINISHED; -} - -float bot_cmd_select_weapon(entity this) -{ - float id = bot_cmd.bot_cmd_parm_float; - - if(id < WEP_FIRST || id > WEP_LAST) - return CMD_STATUS_ERROR; - - if(client_hasweapon(this, Weapons_from(id), true, false)) - PS(this).m_switchweapon = Weapons_from(id); - else - return CMD_STATUS_ERROR; - - return CMD_STATUS_FINISHED; -} - -.int bot_cmd_condition_status; - -const int CMD_CONDITION_NONE = 0; -const int CMD_CONDITION_true = 1; -const int CMD_CONDITION_false = 2; -const int CMD_CONDITION_true_BLOCK = 4; -const int CMD_CONDITION_false_BLOCK = 8; - -float bot_cmd_eval(entity this, string expr) -{ - // Search for numbers - if(strstrofs("0123456789", substring(expr, 0, 1), 0) >= 0) - { - return stof(expr); - } - - // Search for cvars - if(substring(expr, 0, 5)=="cvar.") - { - return cvar(substring(expr, 5, strlen(expr))); - } - - // Search for fields - switch(expr) - { - case "health": - return this.health; - case "speed": - return vlen(this.velocity); - case "flagcarrier": - return ((this.flagcarried!=NULL)); - } - - LOG_INFO(strcat("ERROR: Unable to convert the expression '",expr,"' into a numeric value\n")); - return 0; -} - -float bot_cmd_if(entity this) -{ - string expr, val_a, val_b; - float cmpofs; - - if(this.bot_cmd_condition_status != CMD_CONDITION_NONE) - { - // Only one "if" block is allowed at time - LOG_INFO("ERROR: Only one conditional block can be processed at time"); - bot_clearqueue(this); - return CMD_STATUS_ERROR; - } - - this.bot_cmd_condition_status |= CMD_CONDITION_true_BLOCK; - - // search for operators - expr = bot_cmd.bot_cmd_parm_string; - - cmpofs = strstrofs(expr,"=",0); - - if(cmpofs>0) - { - val_a = substring(expr,0,cmpofs); - val_b = substring(expr,cmpofs+1,strlen(expr)); - - if(bot_cmd_eval(this, val_a)==bot_cmd_eval(this, val_b)) - this.bot_cmd_condition_status |= CMD_CONDITION_true; - else - this.bot_cmd_condition_status |= CMD_CONDITION_false; - - return CMD_STATUS_FINISHED; - } - - cmpofs = strstrofs(expr,">",0); - - if(cmpofs>0) - { - val_a = substring(expr,0,cmpofs); - val_b = substring(expr,cmpofs+1,strlen(expr)); - - if(bot_cmd_eval(this, val_a)>bot_cmd_eval(this, val_b)) - this.bot_cmd_condition_status |= CMD_CONDITION_true; - else - this.bot_cmd_condition_status |= CMD_CONDITION_false; - - return CMD_STATUS_FINISHED; - } - - cmpofs = strstrofs(expr,"<",0); - - if(cmpofs>0) - { - val_a = substring(expr,0,cmpofs); - val_b = substring(expr,cmpofs+1,strlen(expr)); - - if(bot_cmd_eval(this, val_a)=this.bot_cmd_aim_endtime) - { - this.bot_cmd_aim_endtime = 0; - return CMD_STATUS_FINISHED; - } - else - return CMD_STATUS_EXECUTING; - } - - // New aiming direction - string parms; - float tokens, step; - - parms = bot_cmd.bot_cmd_parm_string; - - tokens = tokenizebyseparator(parms, " "); - - if(tokens<2||tokens>3) - return CMD_STATUS_ERROR; - - step = (tokens == 3) ? stof(argv(2)) : 0; - - if(step == 0) - { - this.v_angle_x -= stof(argv(1)); - this.v_angle_y += stof(argv(0)); - return CMD_STATUS_FINISHED; - } - - this.bot_cmd_aim_begin = this.v_angle; - - this.bot_cmd_aim_end_x = this.v_angle.x - stof(argv(1)); - this.bot_cmd_aim_end_y = this.v_angle.y + stof(argv(0)); - this.bot_cmd_aim_end_z = 0; - - this.bot_cmd_aim_begintime = time; - this.bot_cmd_aim_endtime = time + step; - - return CMD_STATUS_EXECUTING; -} - -float bot_cmd_aimtarget(entity this) -{ - if(this.bot_cmd_aim_endtime) - { - return bot_cmd_aim(this); - } - - entity e; - string parms; - vector v; - float tokens, step; - - parms = bot_cmd.bot_cmd_parm_string; - - tokens = tokenizebyseparator(parms, " "); - - e = bot_getplace(this, argv(0)); - if(!e) - return CMD_STATUS_ERROR; - - v = e.origin + (e.mins + e.maxs) * 0.5; - - if(tokens==1) - { - this.v_angle = vectoangles(v - (this.origin + this.view_ofs)); - this.v_angle_x = -this.v_angle.x; - return CMD_STATUS_FINISHED; - } - - if(tokens<1||tokens>2) - return CMD_STATUS_ERROR; - - step = stof(argv(1)); - - this.bot_cmd_aim_begin = this.v_angle; - this.bot_cmd_aim_end = vectoangles(v - (this.origin + this.view_ofs)); - this.bot_cmd_aim_end_x = -this.bot_cmd_aim_end.x; - - this.bot_cmd_aim_begintime = time; - this.bot_cmd_aim_endtime = time + step; - - return CMD_STATUS_EXECUTING; -} - -.int bot_cmd_keys; - -const int BOT_CMD_KEY_NONE = 0; -const int BOT_CMD_KEY_FORWARD = BIT(0); -const int BOT_CMD_KEY_BACKWARD = BIT(1); -const int BOT_CMD_KEY_RIGHT = BIT(2); -const int BOT_CMD_KEY_LEFT = BIT(3); -const int BOT_CMD_KEY_JUMP = BIT(4); -const int BOT_CMD_KEY_ATTACK1 = BIT(5); -const int BOT_CMD_KEY_ATTACK2 = BIT(6); -const int BOT_CMD_KEY_USE = BIT(7); -const int BOT_CMD_KEY_HOOK = BIT(8); -const int BOT_CMD_KEY_CROUCH = BIT(9); -const int BOT_CMD_KEY_CHAT = BIT(10); - -bool bot_presskeys(entity this) -{ - this.movement = '0 0 0'; - PHYS_INPUT_BUTTON_JUMP(this) = false; - PHYS_INPUT_BUTTON_CROUCH(this) = false; - PHYS_INPUT_BUTTON_ATCK(this) = false; - PHYS_INPUT_BUTTON_ATCK2(this) = false; - PHYS_INPUT_BUTTON_USE(this) = false; - PHYS_INPUT_BUTTON_HOOK(this) = false; - PHYS_INPUT_BUTTON_CHAT(this) = false; - - if(this.bot_cmd_keys == BOT_CMD_KEY_NONE) - return false; - - if(this.bot_cmd_keys & BOT_CMD_KEY_FORWARD) - this.movement_x = autocvar_sv_maxspeed; - else if(this.bot_cmd_keys & BOT_CMD_KEY_BACKWARD) - this.movement_x = -autocvar_sv_maxspeed; - - if(this.bot_cmd_keys & BOT_CMD_KEY_RIGHT) - this.movement_y = autocvar_sv_maxspeed; - else if(this.bot_cmd_keys & BOT_CMD_KEY_LEFT) - this.movement_y = -autocvar_sv_maxspeed; - - if(this.bot_cmd_keys & BOT_CMD_KEY_JUMP) - PHYS_INPUT_BUTTON_JUMP(this) = true; - - if(this.bot_cmd_keys & BOT_CMD_KEY_CROUCH) - PHYS_INPUT_BUTTON_CROUCH(this) = true; - - if(this.bot_cmd_keys & BOT_CMD_KEY_ATTACK1) - PHYS_INPUT_BUTTON_ATCK(this) = true; - - if(this.bot_cmd_keys & BOT_CMD_KEY_ATTACK2) - PHYS_INPUT_BUTTON_ATCK2(this) = true; - - if(this.bot_cmd_keys & BOT_CMD_KEY_USE) - PHYS_INPUT_BUTTON_USE(this) = true; - - if(this.bot_cmd_keys & BOT_CMD_KEY_HOOK) - PHYS_INPUT_BUTTON_HOOK(this) = true; - - if(this.bot_cmd_keys & BOT_CMD_KEY_CHAT) - PHYS_INPUT_BUTTON_CHAT(this) = true; - - return true; -} - - -float bot_cmd_keypress_handler(entity this, string key, float enabled) -{ - switch(key) - { - case "all": - if(enabled) - this.bot_cmd_keys = power2of(20) - 1; // >:) - else - this.bot_cmd_keys = BOT_CMD_KEY_NONE; - case "forward": - if(enabled) - { - this.bot_cmd_keys |= BOT_CMD_KEY_FORWARD; - this.bot_cmd_keys &= ~BOT_CMD_KEY_BACKWARD; - } - else - this.bot_cmd_keys &= ~BOT_CMD_KEY_FORWARD; - break; - case "backward": - if(enabled) - { - this.bot_cmd_keys |= BOT_CMD_KEY_BACKWARD; - this.bot_cmd_keys &= ~BOT_CMD_KEY_FORWARD; - } - else - this.bot_cmd_keys &= ~BOT_CMD_KEY_BACKWARD; - break; - case "left": - if(enabled) - { - this.bot_cmd_keys |= BOT_CMD_KEY_LEFT; - this.bot_cmd_keys &= ~BOT_CMD_KEY_RIGHT; - } - else - this.bot_cmd_keys &= ~BOT_CMD_KEY_LEFT; - break; - case "right": - if(enabled) - { - this.bot_cmd_keys |= BOT_CMD_KEY_RIGHT; - this.bot_cmd_keys &= ~BOT_CMD_KEY_LEFT; - } - else - this.bot_cmd_keys &= ~BOT_CMD_KEY_RIGHT; - break; - case "jump": - if(enabled) - this.bot_cmd_keys |= BOT_CMD_KEY_JUMP; - else - this.bot_cmd_keys &= ~BOT_CMD_KEY_JUMP; - break; - case "crouch": - if(enabled) - this.bot_cmd_keys |= BOT_CMD_KEY_CROUCH; - else - this.bot_cmd_keys &= ~BOT_CMD_KEY_CROUCH; - break; - case "attack1": - if(enabled) - this.bot_cmd_keys |= BOT_CMD_KEY_ATTACK1; - else - this.bot_cmd_keys &= ~BOT_CMD_KEY_ATTACK1; - break; - case "attack2": - if(enabled) - this.bot_cmd_keys |= BOT_CMD_KEY_ATTACK2; - else - this.bot_cmd_keys &= ~BOT_CMD_KEY_ATTACK2; - break; - case "use": - if(enabled) - this.bot_cmd_keys |= BOT_CMD_KEY_USE; - else - this.bot_cmd_keys &= ~BOT_CMD_KEY_USE; - break; - case "hook": - if(enabled) - this.bot_cmd_keys |= BOT_CMD_KEY_HOOK; - else - this.bot_cmd_keys &= ~BOT_CMD_KEY_HOOK; - break; - case "chat": - if(enabled) - this.bot_cmd_keys |= BOT_CMD_KEY_CHAT; - else - this.bot_cmd_keys &= ~BOT_CMD_KEY_CHAT; - break; - default: - break; - } - - return CMD_STATUS_FINISHED; -} - -float bot_cmd_presskey(entity this) -{ - string key; - - key = bot_cmd.bot_cmd_parm_string; - - bot_cmd_keypress_handler(this, key,true); - - return CMD_STATUS_FINISHED; -} - -float bot_cmd_releasekey(entity this) -{ - string key; - - key = bot_cmd.bot_cmd_parm_string; - - return bot_cmd_keypress_handler(this, key,false); -} - -float bot_cmd_pause(entity this) -{ - PHYS_INPUT_BUTTON_DRAG(this) = false; - PHYS_INPUT_BUTTON_USE(this) = false; - PHYS_INPUT_BUTTON_ATCK(this) = false; - PHYS_INPUT_BUTTON_JUMP(this) = false; - PHYS_INPUT_BUTTON_HOOK(this) = false; - PHYS_INPUT_BUTTON_CHAT(this) = false; - PHYS_INPUT_BUTTON_ATCK2(this) = false; - PHYS_INPUT_BUTTON_CROUCH(this) = false; - - this.movement = '0 0 0'; - this.bot_cmd_keys = BOT_CMD_KEY_NONE; - - this.bot_exec_status |= BOT_EXEC_STATUS_PAUSED; - return CMD_STATUS_FINISHED; -} - -float bot_cmd_moveto(entity this) -{ - return this.cmd_moveto(this, bot_cmd.bot_cmd_parm_vector); -} - -float bot_cmd_movetotarget(entity this) -{ - entity e; - e = bot_getplace(this, bot_cmd.bot_cmd_parm_string); - if(!e) - return CMD_STATUS_ERROR; - return this.cmd_moveto(this, e.origin + (e.mins + e.maxs) * 0.5); -} - -float bot_cmd_resetgoal(entity this) -{ - return this.cmd_resetgoal(this); -} - - -float bot_cmd_sound(entity this) -{ - string f; - f = bot_cmd.bot_cmd_parm_string; - - float n = tokenizebyseparator(f, " "); - - string sample = f; - float chan = CH_WEAPON_B; - float vol = VOL_BASE; - float atten = ATTEN_MIN; - - if(n >= 1) - sample = argv(n - 1); - if(n >= 2) - chan = stof(argv(0)); - if(n >= 3) - vol = stof(argv(1)); - if(n >= 4) - atten = stof(argv(2)); - - precache_sound(f); - _sound(this, chan, sample, vol, atten); - - return CMD_STATUS_FINISHED; -} - -.entity tuba_note; -float bot_cmd_debug_assert_canfire(entity this) -{ - float f = bot_cmd.bot_cmd_parm_float; - - int slot = 0; - .entity weaponentity = weaponentities[slot]; - if(this.(weaponentity).state != WS_READY) - { - if(f) - { - this.colormod = '0 8 8'; - LOG_INFO("Bot ", this.netname, " using ", this.weaponname, " wants to fire, inhibited by weaponentity state\n"); - } - } - else if(ATTACK_FINISHED(this, slot) > time) - { - if(f) - { - this.colormod = '8 0 8'; - LOG_INFO("Bot ", this.netname, " using ", this.weaponname, " wants to fire, inhibited by ATTACK_FINISHED (", ftos(ATTACK_FINISHED(this, slot) - time), " seconds left)\n"); - } - } - else if(this.tuba_note) - { - if(f) - { - this.colormod = '8 0 0'; - LOG_INFO("Bot ", this.netname, " using ", this.weaponname, " wants to fire, bot still has an active tuba note\n"); - } - } - else - { - if(!f) - { - this.colormod = '8 8 0'; - LOG_INFO("Bot ", this.netname, " using ", this.weaponname, " thinks it has fired, but apparently did not; ATTACK_FINISHED says ", ftos(ATTACK_FINISHED(this, slot) - time), " seconds left\n"); - } - } - - return CMD_STATUS_FINISHED; -} - -// - -void bot_command_executed(entity this, bool rm) -{ - entity cmd; - - cmd = bot_cmd; - - if(rm) - bot_dequeuecommand(this, this.bot_cmd_execution_index); - - this.bot_cmd_execution_index++; -} - -void bot_setcurrentcommand(entity this) -{ - bot_cmd = NULL; - - if(!this.bot_cmd_current) - { - this.bot_cmd_current = new_pure(bot_cmd); - this.bot_cmd_current.is_bot_cmd = true; - } - - bot_cmd = this.bot_cmd_current; - if(bot_cmd.bot_cmd_index != this.bot_cmd_execution_index || this.bot_cmd_execution_index == 0) - { - if(bot_havecommand(this, this.bot_cmd_execution_index)) - { - string cmdstring; - cmdstring = bot_readcommand(this, this.bot_cmd_execution_index); - if(bot_decodecommand(cmdstring)) - { - bot_cmd.owner = this; - bot_cmd.bot_cmd_index = this.bot_cmd_execution_index; - } - else - { - // Invalid command, remove from queue - bot_cmd = NULL; - bot_dequeuecommand(this, this.bot_cmd_execution_index); - this.bot_cmd_execution_index++; - } - } - else - bot_cmd = NULL; - } -} - -void bot_resetqueues() -{ - FOREACH_CLIENT(it.isbot, LAMBDA( - it.bot_cmd_execution_index = 0; - bot_clearqueue(it); - // also, cancel all barriers - it.bot_barrier = 0; - for(int i = 0; i < it.bot_places_count; ++i) - { - strunzone(it.(bot_placenames[i])); - it.(bot_placenames[i]) = string_null; - } - it.bot_places_count = 0; - )); - - bot_barriertime = time; -} - -// Here we map commands to functions and deal with complex interactions between commands and execution states -// NOTE: Of course you need to include your commands here too :) -float bot_execute_commands_once(entity this) -{ - float status, ispressingkey; - - // Find command - bot_setcurrentcommand(this); - - // if we have no bot command, better return - // old logic kept pressing previously pressed keys, but that has problems - // (namely, it means you cannot make a bot "normal" ever again) - // to keep a bot walking for a while, use the "wait" bot command - if(bot_cmd == NULL) - return false; - - // Ignore all commands except continue when the bot is paused - if(this.bot_exec_status & BOT_EXEC_STATUS_PAUSED) - if(bot_cmd.bot_cmd_type!=BOT_CMD_CONTINUE) - { - if(bot_cmd.bot_cmd_type!=BOT_CMD_NULL) - { - bot_command_executed(this, true); - LOG_INFO( "WARNING: Commands are ignored while the bot is paused. Use the command 'continue' instead.\n"); - } - return 1; - } - - // Keep pressing keys raised by the "presskey" command - ispressingkey = boolean(bot_presskeys(this)); - - // Handle conditions - if (!(bot_cmd.bot_cmd_type==BOT_CMD_FI||bot_cmd.bot_cmd_type==BOT_CMD_ELSE)) - if(this.bot_cmd_condition_status & CMD_CONDITION_true && this.bot_cmd_condition_status & CMD_CONDITION_false_BLOCK) - { - bot_command_executed(this, true); - return -1; - } - else if(this.bot_cmd_condition_status & CMD_CONDITION_false && this.bot_cmd_condition_status & CMD_CONDITION_true_BLOCK) - { - bot_command_executed(this, true); - return -1; - } - - // Map commands to functions - switch(bot_cmd.bot_cmd_type) - { - case BOT_CMD_NULL: - return ispressingkey; - //break; - case BOT_CMD_PAUSE: - status = bot_cmd_pause(this); - break; - case BOT_CMD_CONTINUE: - status = bot_cmd_continue(this); - break; - case BOT_CMD_WAIT: - status = bot_cmd_wait(this); - break; - case BOT_CMD_WAIT_UNTIL: - status = bot_cmd_wait_until(this); - break; - case BOT_CMD_TURN: - status = bot_cmd_turn(this); - break; - case BOT_CMD_MOVETO: - status = bot_cmd_moveto(this); - break; - case BOT_CMD_MOVETOTARGET: - status = bot_cmd_movetotarget(this); - break; - case BOT_CMD_RESETGOAL: - status = bot_cmd_resetgoal(this); - break; - case BOT_CMD_CC: - status = bot_cmd_cc(this); - break; - case BOT_CMD_IF: - status = bot_cmd_if(this); - break; - case BOT_CMD_ELSE: - status = bot_cmd_else(this); - break; - case BOT_CMD_FI: - status = bot_cmd_fi(this); - break; - case BOT_CMD_RESETAIM: - status = bot_cmd_resetaim(this); - break; - case BOT_CMD_AIM: - status = bot_cmd_aim(this); - break; - case BOT_CMD_AIMTARGET: - status = bot_cmd_aimtarget(this); - break; - case BOT_CMD_PRESSKEY: - status = bot_cmd_presskey(this); - break; - case BOT_CMD_RELEASEKEY: - status = bot_cmd_releasekey(this); - break; - case BOT_CMD_SELECTWEAPON: - status = bot_cmd_select_weapon(this); - break; - case BOT_CMD_IMPULSE: - status = bot_cmd_impulse(this); - break; - case BOT_CMD_BARRIER: - status = bot_cmd_barrier(this); - break; - case BOT_CMD_CONSOLE: - localcmd(strcat(bot_cmd.bot_cmd_parm_string, "\n")); - status = CMD_STATUS_FINISHED; - break; - case BOT_CMD_SOUND: - status = bot_cmd_sound(this); - break; - case BOT_CMD_DEBUG_ASSERT_CANFIRE: - status = bot_cmd_debug_assert_canfire(this); - break; - default: - LOG_INFO(strcat("ERROR: Invalid command on queue with id '",ftos(bot_cmd.bot_cmd_type),"'\n")); - return 0; - } - - if (status==CMD_STATUS_ERROR) - LOG_INFO(strcat("ERROR: The command '",bot_cmd_string[bot_cmd.bot_cmd_type],"' returned an error status\n")); - - // Move execution pointer - if(status==CMD_STATUS_EXECUTING) - { - return 1; - } - else - { - if(autocvar_g_debug_bot_commands) - { - string parms; - - switch(bot_cmd_parm_type[bot_cmd.bot_cmd_type]) - { - case BOT_CMD_PARAMETER_FLOAT: - parms = ftos(bot_cmd.bot_cmd_parm_float); - break; - case BOT_CMD_PARAMETER_STRING: - parms = bot_cmd.bot_cmd_parm_string; - break; - case BOT_CMD_PARAMETER_VECTOR: - parms = vtos(bot_cmd.bot_cmd_parm_vector); - break; - default: - parms = ""; - break; - } - clientcommand(this,strcat("say ^7", bot_cmd_string[bot_cmd.bot_cmd_type]," ",parms,"\n")); - } - - bot_command_executed(this, true); - } - - if(status == CMD_STATUS_FINISHED) - return -1; - - return CMD_STATUS_ERROR; -} - -// This function should be (the only) called directly from the bot ai loop -int bot_execute_commands(entity this) -{ - int f; - do - { - f = bot_execute_commands_once(this); - } - while(f < 0); - return f; -} diff --git a/qcsrc/server/bot/scripting.qh b/qcsrc/server/bot/scripting.qh deleted file mode 100644 index cb6cd8731..000000000 --- a/qcsrc/server/bot/scripting.qh +++ /dev/null @@ -1,81 +0,0 @@ -#pragma once - -#define BOT_EXEC_STATUS_IDLE 0 -#define BOT_EXEC_STATUS_PAUSED 1 -#define BOT_EXEC_STATUS_WAITING 2 - -#define CMD_STATUS_EXECUTING 0 -#define CMD_STATUS_FINISHED 1 -#define CMD_STATUS_ERROR 2 - - -// NOTE: New commands should be added here. Do not forget to update BOT_CMD_COUNTER -const int BOT_CMD_NULL = 0; -const int BOT_CMD_PAUSE = 1; -const int BOT_CMD_CONTINUE = 2; -const int BOT_CMD_WAIT = 3; -const int BOT_CMD_TURN = 4; -const int BOT_CMD_MOVETO = 5; -const int BOT_CMD_RESETGOAL = 6; // Not implemented yet -const int BOT_CMD_CC = 7; -const int BOT_CMD_IF = 8; -const int BOT_CMD_ELSE = 9; -const int BOT_CMD_FI = 10; -const int BOT_CMD_RESETAIM = 11; -const int BOT_CMD_AIM = 12; -const int BOT_CMD_PRESSKEY = 13; -const int BOT_CMD_RELEASEKEY = 14; -const int BOT_CMD_SELECTWEAPON = 15; -const int BOT_CMD_IMPULSE = 16; -const int BOT_CMD_WAIT_UNTIL = 17; -const int BOT_CMD_MOVETOTARGET = 18; -const int BOT_CMD_AIMTARGET = 19; -const int BOT_CMD_BARRIER = 20; -const int BOT_CMD_CONSOLE = 21; -const int BOT_CMD_SOUND = 22; -const int BOT_CMD_DEBUG_ASSERT_CANFIRE = 23; -const int BOT_CMD_WHILE = 24; // TODO: Not implemented yet -const int BOT_CMD_WEND = 25; // TODO: Not implemented yet -const int BOT_CMD_CHASE = 26; // TODO: Not implemented yet - -const int BOT_CMD_COUNTER = 24; // Update this value if you add/remove a command - -// NOTE: Following commands should be implemented on the bot ai -// If a new command should be handled by the target ai(s) please declare it here -.float(entity, vector) cmd_moveto; -.float(entity) cmd_resetgoal; - -// -const int BOT_CMD_PARAMETER_NONE = 0; -const int BOT_CMD_PARAMETER_FLOAT = 1; -const int BOT_CMD_PARAMETER_STRING = 2; -const int BOT_CMD_PARAMETER_VECTOR = 3; - -float bot_cmds_initialized; -int bot_cmd_parm_type[BOT_CMD_COUNTER]; -string bot_cmd_string[BOT_CMD_COUNTER]; - -// Bots command queue -entity bot_cmd; // global current command -.entity bot_cmd_current; // current command of this bot - -.float is_bot_cmd; // Tells if the entity is a bot command -.float bot_cmd_index; // Position of the command in the queue -.int bot_cmd_type; // If of command (see the BOT_CMD_* defines) -.float bot_cmd_parm_float; // Field to store a float parameter -.string bot_cmd_parm_string; // Field to store a string parameter -.vector bot_cmd_parm_vector; // Field to store a vector parameter - -float bot_barriertime; -.float bot_barrier; - -.float bot_cmd_execution_index; // Position in the queue of the command to be executed - - -void bot_resetqueues(); -void bot_queuecommand(entity bot, string cmdstring); -void bot_cmdhelp(string scmd); -void bot_list_commands(); -float bot_execute_commands(entity this); -entity find_bot_by_name(string name); -entity find_bot_by_number(float number); diff --git a/qcsrc/server/bot/waypoints.qc b/qcsrc/server/bot/waypoints.qc deleted file mode 100644 index 89078f5c8..000000000 --- a/qcsrc/server/bot/waypoints.qc +++ /dev/null @@ -1,1102 +0,0 @@ -#include "waypoints.qh" - -#include "bot.qh" -#include "navigation.qh" - -#include - -#include "../antilag.qh" - -#include - -#include -#include - -// create a new spawnfunc_waypoint and automatically link it to other waypoints, and link -// them back to it as well -// (suitable for spawnfunc_waypoint editor) -entity waypoint_spawn(vector m1, vector m2, float f) -{ - if(!(f & WAYPOINTFLAG_PERSONAL)) - { - IL_EACH(g_waypoints, boxesoverlap(m1, m2, it.absmin, it.absmax), - { - return it; - }); - } - - entity w = new(waypoint); - IL_PUSH(g_waypoints, w); - w.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP; - w.wpflags = f; - w.solid = SOLID_TRIGGER; - setorigin(w, (m1 + m2) * 0.5); - setsize(w, m1 - w.origin, m2 - w.origin); - if (vlen(w.size) > 0) - w.wpisbox = true; - - if(!w.wpisbox) - { - setsize(w, STAT(PL_MIN, NULL) - '1 1 0', STAT(PL_MAX, NULL) + '1 1 0'); - if(!move_out_of_solid(w)) - { - if(!(f & WAYPOINTFLAG_GENERATED)) - { - LOG_TRACE("Killed a waypoint that was stuck in solid at ", vtos(w.origin), "\n"); - delete(w); - return NULL; - } - else - { - if(autocvar_developer) - { - LOG_INFO("A generated waypoint is stuck in solid at ", vtos(w.origin), "\n"); - backtrace("Waypoint stuck"); - } - } - } - setsize(w, '0 0 0', '0 0 0'); - } - - waypoint_clearlinks(w); - //waypoint_schedulerelink(w); - - if (autocvar_g_waypointeditor) - { - m1 = w.mins; - m2 = w.maxs; - setmodel(w, MDL_WAYPOINT); w.effects = EF_LOWPRECISION; - setsize(w, m1, m2); - if (w.wpflags & WAYPOINTFLAG_ITEM) - w.colormod = '1 0 0'; - else if (w.wpflags & WAYPOINTFLAG_GENERATED) - w.colormod = '1 1 0'; - else - w.colormod = '1 1 1'; - } - else - w.model = ""; - - return w; -} - -// add a new link to the spawnfunc_waypoint, replacing the furthest link it already has -void waypoint_addlink(entity from, entity to) -{ - float c; - - if (from == to) - return; - if (from.wpflags & WAYPOINTFLAG_NORELINK) - return; - - if (from.wp00 == to) return;if (from.wp01 == to) return;if (from.wp02 == to) return;if (from.wp03 == to) return; - if (from.wp04 == to) return;if (from.wp05 == to) return;if (from.wp06 == to) return;if (from.wp07 == to) return; - if (from.wp08 == to) return;if (from.wp09 == to) return;if (from.wp10 == to) return;if (from.wp11 == to) return; - if (from.wp12 == to) return;if (from.wp13 == to) return;if (from.wp14 == to) return;if (from.wp15 == to) return; - if (from.wp16 == to) return;if (from.wp17 == to) return;if (from.wp18 == to) return;if (from.wp19 == to) return; - if (from.wp20 == to) return;if (from.wp21 == to) return;if (from.wp22 == to) return;if (from.wp23 == to) return; - if (from.wp24 == to) return;if (from.wp25 == to) return;if (from.wp26 == to) return;if (from.wp27 == to) return; - if (from.wp28 == to) return;if (from.wp29 == to) return;if (from.wp30 == to) return;if (from.wp31 == to) return; - - if (to.wpisbox || from.wpisbox) - { - // if either is a box we have to find the nearest points on them to - // calculate the distance properly - vector v1, v2, m1, m2; - v1 = from.origin; - m1 = to.absmin; - m2 = to.absmax; - v1_x = bound(m1_x, v1_x, m2_x); - v1_y = bound(m1_y, v1_y, m2_y); - v1_z = bound(m1_z, v1_z, m2_z); - v2 = to.origin; - m1 = from.absmin; - m2 = from.absmax; - v2_x = bound(m1_x, v2_x, m2_x); - v2_y = bound(m1_y, v2_y, m2_y); - v2_z = bound(m1_z, v2_z, m2_z); - v2 = to.origin; - c = vlen(v2 - v1); - } - else - c = vlen(to.origin - from.origin); - - if (from.wp31mincost < c) return; - if (from.wp30mincost < c) {from.wp31 = to;from.wp31mincost = c;return;} from.wp31 = from.wp30;from.wp31mincost = from.wp30mincost; - if (from.wp29mincost < c) {from.wp30 = to;from.wp30mincost = c;return;} from.wp30 = from.wp29;from.wp30mincost = from.wp29mincost; - if (from.wp28mincost < c) {from.wp29 = to;from.wp29mincost = c;return;} from.wp29 = from.wp28;from.wp29mincost = from.wp28mincost; - if (from.wp27mincost < c) {from.wp28 = to;from.wp28mincost = c;return;} from.wp28 = from.wp27;from.wp28mincost = from.wp27mincost; - if (from.wp26mincost < c) {from.wp27 = to;from.wp27mincost = c;return;} from.wp27 = from.wp26;from.wp27mincost = from.wp26mincost; - if (from.wp25mincost < c) {from.wp26 = to;from.wp26mincost = c;return;} from.wp26 = from.wp25;from.wp26mincost = from.wp25mincost; - if (from.wp24mincost < c) {from.wp25 = to;from.wp25mincost = c;return;} from.wp25 = from.wp24;from.wp25mincost = from.wp24mincost; - if (from.wp23mincost < c) {from.wp24 = to;from.wp24mincost = c;return;} from.wp24 = from.wp23;from.wp24mincost = from.wp23mincost; - if (from.wp22mincost < c) {from.wp23 = to;from.wp23mincost = c;return;} from.wp23 = from.wp22;from.wp23mincost = from.wp22mincost; - if (from.wp21mincost < c) {from.wp22 = to;from.wp22mincost = c;return;} from.wp22 = from.wp21;from.wp22mincost = from.wp21mincost; - if (from.wp20mincost < c) {from.wp21 = to;from.wp21mincost = c;return;} from.wp21 = from.wp20;from.wp21mincost = from.wp20mincost; - if (from.wp19mincost < c) {from.wp20 = to;from.wp20mincost = c;return;} from.wp20 = from.wp19;from.wp20mincost = from.wp19mincost; - if (from.wp18mincost < c) {from.wp19 = to;from.wp19mincost = c;return;} from.wp19 = from.wp18;from.wp19mincost = from.wp18mincost; - if (from.wp17mincost < c) {from.wp18 = to;from.wp18mincost = c;return;} from.wp18 = from.wp17;from.wp18mincost = from.wp17mincost; - if (from.wp16mincost < c) {from.wp17 = to;from.wp17mincost = c;return;} from.wp17 = from.wp16;from.wp17mincost = from.wp16mincost; - if (from.wp15mincost < c) {from.wp16 = to;from.wp16mincost = c;return;} from.wp16 = from.wp15;from.wp16mincost = from.wp15mincost; - if (from.wp14mincost < c) {from.wp15 = to;from.wp15mincost = c;return;} from.wp15 = from.wp14;from.wp15mincost = from.wp14mincost; - if (from.wp13mincost < c) {from.wp14 = to;from.wp14mincost = c;return;} from.wp14 = from.wp13;from.wp14mincost = from.wp13mincost; - if (from.wp12mincost < c) {from.wp13 = to;from.wp13mincost = c;return;} from.wp13 = from.wp12;from.wp13mincost = from.wp12mincost; - if (from.wp11mincost < c) {from.wp12 = to;from.wp12mincost = c;return;} from.wp12 = from.wp11;from.wp12mincost = from.wp11mincost; - if (from.wp10mincost < c) {from.wp11 = to;from.wp11mincost = c;return;} from.wp11 = from.wp10;from.wp11mincost = from.wp10mincost; - if (from.wp09mincost < c) {from.wp10 = to;from.wp10mincost = c;return;} from.wp10 = from.wp09;from.wp10mincost = from.wp09mincost; - if (from.wp08mincost < c) {from.wp09 = to;from.wp09mincost = c;return;} from.wp09 = from.wp08;from.wp09mincost = from.wp08mincost; - if (from.wp07mincost < c) {from.wp08 = to;from.wp08mincost = c;return;} from.wp08 = from.wp07;from.wp08mincost = from.wp07mincost; - if (from.wp06mincost < c) {from.wp07 = to;from.wp07mincost = c;return;} from.wp07 = from.wp06;from.wp07mincost = from.wp06mincost; - if (from.wp05mincost < c) {from.wp06 = to;from.wp06mincost = c;return;} from.wp06 = from.wp05;from.wp06mincost = from.wp05mincost; - if (from.wp04mincost < c) {from.wp05 = to;from.wp05mincost = c;return;} from.wp05 = from.wp04;from.wp05mincost = from.wp04mincost; - if (from.wp03mincost < c) {from.wp04 = to;from.wp04mincost = c;return;} from.wp04 = from.wp03;from.wp04mincost = from.wp03mincost; - if (from.wp02mincost < c) {from.wp03 = to;from.wp03mincost = c;return;} from.wp03 = from.wp02;from.wp03mincost = from.wp02mincost; - if (from.wp01mincost < c) {from.wp02 = to;from.wp02mincost = c;return;} from.wp02 = from.wp01;from.wp02mincost = from.wp01mincost; - if (from.wp00mincost < c) {from.wp01 = to;from.wp01mincost = c;return;} from.wp01 = from.wp00;from.wp01mincost = from.wp00mincost; - from.wp00 = to;from.wp00mincost = c;return; -} - -// relink this spawnfunc_waypoint -// (precompile a list of all reachable waypoints from this spawnfunc_waypoint) -// (SLOW!) -void waypoint_think(entity this) -{ - vector sv, sm1, sm2, ev, em1, em2, dv; - - bot_calculate_stepheightvec(); - - bot_navigation_movemode = ((autocvar_bot_navigation_ignoreplayers) ? MOVE_NOMONSTERS : MOVE_NORMAL); - - //dprint("waypoint_think wpisbox = ", ftos(this.wpisbox), "\n"); - sm1 = this.origin + this.mins; - sm2 = this.origin + this.maxs; - IL_EACH(g_waypoints, true, - { - if (boxesoverlap(this.absmin, this.absmax, it.absmin, it.absmax)) - { - waypoint_addlink(this, it); - waypoint_addlink(it, this); - } - else - { - ++relink_total; - if(!checkpvs(this.origin, it)) - { - ++relink_pvsculled; - continue; - } - sv = it.origin; - sv.x = bound(sm1_x, sv.x, sm2_x); - sv.y = bound(sm1_y, sv.y, sm2_y); - sv.z = bound(sm1_z, sv.z, sm2_z); - ev = this.origin; - em1 = it.origin + it.mins; - em2 = it.origin + it.maxs; - ev.x = bound(em1_x, ev.x, em2_x); - ev.y = bound(em1_y, ev.y, em2_y); - ev.z = bound(em1_z, ev.z, em2_z); - dv = ev - sv; - dv.z = 0; - if(vdist(dv, >=, 1050)) // max search distance in XY - { - ++relink_lengthculled; - continue; - } - navigation_testtracewalk = 0; - if (!this.wpisbox) - { - tracebox(sv - STAT(PL_MIN, NULL).z * '0 0 1', STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), sv, false, this); - if (!trace_startsolid) - { - //dprint("sv deviation", vtos(trace_endpos - sv), "\n"); - sv = trace_endpos + '0 0 1'; - } - } - if (!it.wpisbox) - { - tracebox(ev - STAT(PL_MIN, NULL).z * '0 0 1', STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), ev, false, it); - if (!trace_startsolid) - { - //dprint("ev deviation", vtos(trace_endpos - ev), "\n"); - ev = trace_endpos + '0 0 1'; - } - } - //traceline(this.origin, it.origin, false, NULL); - //if (trace_fraction == 1) - if (!this.wpisbox && tracewalk(this, sv, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), ev, MOVE_NOMONSTERS)) - waypoint_addlink(this, it); - else - relink_walkculled += 0.5; - if (!it.wpisbox && tracewalk(it, ev, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), sv, MOVE_NOMONSTERS)) - waypoint_addlink(it, this); - else - relink_walkculled += 0.5; - } - }); - navigation_testtracewalk = 0; - this.wplinked = true; -} - -void waypoint_clearlinks(entity wp) -{ - // clear links to other waypoints - float f; - f = 10000000; - wp.wp00 = wp.wp01 = wp.wp02 = wp.wp03 = wp.wp04 = wp.wp05 = wp.wp06 = wp.wp07 = NULL; - wp.wp08 = wp.wp09 = wp.wp10 = wp.wp11 = wp.wp12 = wp.wp13 = wp.wp14 = wp.wp15 = NULL; - wp.wp16 = wp.wp17 = wp.wp18 = wp.wp19 = wp.wp20 = wp.wp21 = wp.wp22 = wp.wp23 = NULL; - wp.wp24 = wp.wp25 = wp.wp26 = wp.wp27 = wp.wp28 = wp.wp29 = wp.wp30 = wp.wp31 = NULL; - - wp.wp00mincost = wp.wp01mincost = wp.wp02mincost = wp.wp03mincost = wp.wp04mincost = wp.wp05mincost = wp.wp06mincost = wp.wp07mincost = f; - wp.wp08mincost = wp.wp09mincost = wp.wp10mincost = wp.wp11mincost = wp.wp12mincost = wp.wp13mincost = wp.wp14mincost = wp.wp15mincost = f; - wp.wp16mincost = wp.wp17mincost = wp.wp18mincost = wp.wp19mincost = wp.wp20mincost = wp.wp21mincost = wp.wp22mincost = wp.wp23mincost = f; - wp.wp24mincost = wp.wp25mincost = wp.wp26mincost = wp.wp27mincost = wp.wp28mincost = wp.wp29mincost = wp.wp30mincost = wp.wp31mincost = f; - - wp.wplinked = false; -} - -// tell a spawnfunc_waypoint to relink -void waypoint_schedulerelink(entity wp) -{ - if (wp == NULL) - return; - // TODO: add some sort of visible box in edit mode for box waypoints - if (autocvar_g_waypointeditor) - { - vector m1, m2; - m1 = wp.mins; - m2 = wp.maxs; - setmodel(wp, MDL_WAYPOINT); wp.effects = EF_LOWPRECISION; - setsize(wp, m1, m2); - if (wp.wpflags & WAYPOINTFLAG_ITEM) - wp.colormod = '1 0 0'; - else if (wp.wpflags & WAYPOINTFLAG_GENERATED) - wp.colormod = '1 1 0'; - else - wp.colormod = '1 1 1'; - } - else - wp.model = ""; - wp.wpisbox = vlen(wp.size) > 0; - wp.enemy = NULL; - if (!(wp.wpflags & WAYPOINTFLAG_PERSONAL)) - wp.owner = NULL; - if (!(wp.wpflags & WAYPOINTFLAG_NORELINK)) - waypoint_clearlinks(wp); - // schedule an actual relink on next frame - setthink(wp, waypoint_think); - wp.nextthink = time; - wp.effects = EF_LOWPRECISION; -} - -// spawnfunc_waypoint map entity -spawnfunc(waypoint) -{ - IL_PUSH(g_waypoints, this); - - setorigin(this, this.origin); - // schedule a relink after other waypoints have had a chance to spawn - waypoint_clearlinks(this); - //waypoint_schedulerelink(this); -} - -// remove a spawnfunc_waypoint, and schedule all neighbors to relink -void waypoint_remove(entity e) -{ - // tell all linked waypoints that they need to relink - waypoint_schedulerelink(e.wp00); - waypoint_schedulerelink(e.wp01); - waypoint_schedulerelink(e.wp02); - waypoint_schedulerelink(e.wp03); - waypoint_schedulerelink(e.wp04); - waypoint_schedulerelink(e.wp05); - waypoint_schedulerelink(e.wp06); - waypoint_schedulerelink(e.wp07); - waypoint_schedulerelink(e.wp08); - waypoint_schedulerelink(e.wp09); - waypoint_schedulerelink(e.wp10); - waypoint_schedulerelink(e.wp11); - waypoint_schedulerelink(e.wp12); - waypoint_schedulerelink(e.wp13); - waypoint_schedulerelink(e.wp14); - waypoint_schedulerelink(e.wp15); - waypoint_schedulerelink(e.wp16); - waypoint_schedulerelink(e.wp17); - waypoint_schedulerelink(e.wp18); - waypoint_schedulerelink(e.wp19); - waypoint_schedulerelink(e.wp20); - waypoint_schedulerelink(e.wp21); - waypoint_schedulerelink(e.wp22); - waypoint_schedulerelink(e.wp23); - waypoint_schedulerelink(e.wp24); - waypoint_schedulerelink(e.wp25); - waypoint_schedulerelink(e.wp26); - waypoint_schedulerelink(e.wp27); - waypoint_schedulerelink(e.wp28); - waypoint_schedulerelink(e.wp29); - waypoint_schedulerelink(e.wp30); - waypoint_schedulerelink(e.wp31); - // and now remove the spawnfunc_waypoint - delete(e); -} - -// empties the map of waypoints -void waypoint_removeall() -{ - IL_EACH(g_waypoints, true, - { - delete(it); - }); -} - -// tell all waypoints to relink -// (is this useful at all?) -void waypoint_schedulerelinkall() -{ - relink_total = relink_walkculled = relink_pvsculled = relink_lengthculled = 0; - IL_EACH(g_waypoints, true, - { - waypoint_schedulerelink(it); - }); -} - -// Load waypoint links from file -float waypoint_load_links() -{ - string filename, s; - float file, tokens, c = 0, found; - entity wp_from = NULL, wp_to; - vector wp_to_pos, wp_from_pos; - filename = strcat("maps/", mapname); - filename = strcat(filename, ".waypoints.cache"); - file = fopen(filename, FILE_READ); - - if (file < 0) - { - LOG_TRACE("waypoint links load from "); - LOG_TRACE(filename); - LOG_TRACE(" failed\n"); - return false; - } - - while ((s = fgets(file))) - { - tokens = tokenizebyseparator(s, "*"); - - if (tokens!=2) - { - // bad file format - fclose(file); - return false; - } - - wp_from_pos = stov(argv(0)); - wp_to_pos = stov(argv(1)); - - // Search "from" waypoint - if(!wp_from || wp_from.origin!=wp_from_pos) - { - wp_from = findradius(wp_from_pos, 1); - found = false; - while(wp_from) - { - if(vdist(wp_from.origin - wp_from_pos, <, 1)) - if(wp_from.classname == "waypoint") - { - found = true; - break; - } - wp_from = wp_from.chain; - } - - if(!found) - { - LOG_TRACE("waypoint_load_links: couldn't find 'from' waypoint at ", vtos(wp_from.origin),"\n"); - continue; - } - - } - - // Search "to" waypoint - wp_to = findradius(wp_to_pos, 1); - found = false; - while(wp_to) - { - if(vdist(wp_to.origin - wp_to_pos, <, 1)) - if(wp_to.classname == "waypoint") - { - found = true; - break; - } - wp_to = wp_to.chain; - } - - if(!found) - { - LOG_TRACE("waypoint_load_links: couldn't find 'to' waypoint at ", vtos(wp_to.origin),"\n"); - continue; - } - - ++c; - waypoint_addlink(wp_from, wp_to); - } - - fclose(file); - - LOG_TRACE("loaded ", ftos(c), " waypoint links from maps/", mapname, ".waypoints.cache\n"); - - botframe_cachedwaypointlinks = true; - return true; -} - -void waypoint_load_links_hardwired() -{ - string filename, s; - float file, tokens, c = 0, found; - entity wp_from = NULL, wp_to; - vector wp_to_pos, wp_from_pos; - filename = strcat("maps/", mapname); - filename = strcat(filename, ".waypoints.hardwired"); - file = fopen(filename, FILE_READ); - - botframe_loadedforcedlinks = true; - - if (file < 0) - { - LOG_TRACE("waypoint links load from ", filename, " failed\n"); - return; - } - - while ((s = fgets(file))) - { - if(substring(s, 0, 2)=="//") - continue; - - if(substring(s, 0, 1)=="#") - continue; - - tokens = tokenizebyseparator(s, "*"); - - if (tokens!=2) - continue; - - wp_from_pos = stov(argv(0)); - wp_to_pos = stov(argv(1)); - - // Search "from" waypoint - if(!wp_from || wp_from.origin!=wp_from_pos) - { - wp_from = findradius(wp_from_pos, 5); - found = false; - while(wp_from) - { - if(vdist(wp_from.origin - wp_from_pos, <, 5)) - if(wp_from.classname == "waypoint") - { - found = true; - break; - } - wp_from = wp_from.chain; - } - - if(!found) - { - LOG_INFO(strcat("NOTICE: Can not find waypoint at ", vtos(wp_from_pos), ". Path skipped\n")); - continue; - } - } - - // Search "to" waypoint - wp_to = findradius(wp_to_pos, 5); - found = false; - while(wp_to) - { - if(vdist(wp_to.origin - wp_to_pos, <, 5)) - if(wp_to.classname == "waypoint") - { - found = true; - break; - } - wp_to = wp_to.chain; - } - - if(!found) - { - LOG_INFO(strcat("NOTICE: Can not find waypoint at ", vtos(wp_to_pos), ". Path skipped\n")); - continue; - } - - ++c; - waypoint_addlink(wp_from, wp_to); - wp_from.wphardwired = true; - wp_to.wphardwired = true; - } - - fclose(file); - - LOG_TRACE("loaded ", ftos(c), " waypoint links from maps/", mapname, ".waypoints.hardwired\n"); -} - -entity waypoint_get_link(entity w, float i) -{ - switch(i) - { - case 0:return w.wp00; - case 1:return w.wp01; - case 2:return w.wp02; - case 3:return w.wp03; - case 4:return w.wp04; - case 5:return w.wp05; - case 6:return w.wp06; - case 7:return w.wp07; - case 8:return w.wp08; - case 9:return w.wp09; - case 10:return w.wp10; - case 11:return w.wp11; - case 12:return w.wp12; - case 13:return w.wp13; - case 14:return w.wp14; - case 15:return w.wp15; - case 16:return w.wp16; - case 17:return w.wp17; - case 18:return w.wp18; - case 19:return w.wp19; - case 20:return w.wp20; - case 21:return w.wp21; - case 22:return w.wp22; - case 23:return w.wp23; - case 24:return w.wp24; - case 25:return w.wp25; - case 26:return w.wp26; - case 27:return w.wp27; - case 28:return w.wp28; - case 29:return w.wp29; - case 30:return w.wp30; - case 31:return w.wp31; - default:return NULL; - } -} - -// Save all waypoint links to a file -void waypoint_save_links() -{ - string filename = sprintf("maps/%s.waypoints.cache", mapname); - int file = fopen(filename, FILE_WRITE); - if (file < 0) - { - LOG_INFOF("waypoint link save to %s failed\n", filename); - return; - } - - int c = 0; - IL_EACH(g_waypoints, true, - { - for(int j = 0; j < 32; ++j) - { - entity link = waypoint_get_link(it, j); - if(link) - { - string s = strcat(vtos(it.origin), "*", vtos(link.origin), "\n"); - fputs(file, s); - ++c; - } - } - }); - fclose(file); - botframe_cachedwaypointlinks = true; - - LOG_INFOF("saved %d waypoint links to maps/%s.waypoints.cache\n", c, mapname); -} - -// save waypoints to gamedir/data/maps/mapname.waypoints -void waypoint_saveall() -{ - string filename = sprintf("maps/%s.waypoints", mapname); - int file = fopen(filename, FILE_WRITE); - if (file < 0) - { - waypoint_save_links(); // save anyway? - botframe_loadedforcedlinks = false; - - LOG_INFOF("waypoint links: save to %s failed\n", filename); - return; - } - - int c = 0; - IL_EACH(g_waypoints, true, - { - if(it.wpflags & WAYPOINTFLAG_GENERATED) - continue; - - for(int j = 0; j < 32; ++j) - { - string s; - s = strcat(vtos(it.origin + it.mins), "\n"); - s = strcat(s, vtos(it.origin + it.maxs)); - s = strcat(s, "\n"); - s = strcat(s, ftos(it.wpflags)); - s = strcat(s, "\n"); - fputs(file, s); - ++c; - } - }); - fclose(file); - waypoint_save_links(); - botframe_loadedforcedlinks = false; - - LOG_INFOF("saved %d waypoints to maps/%s.waypoints\n", c, mapname); -} - -// load waypoints from file -float waypoint_loadall() -{ - string filename, s; - float file, cwp, cwb, fl; - vector m1, m2; - cwp = 0; - cwb = 0; - filename = strcat("maps/", mapname); - filename = strcat(filename, ".waypoints"); - file = fopen(filename, FILE_READ); - if (file >= 0) - { - while ((s = fgets(file))) - { - m1 = stov(s); - s = fgets(file); - if (!s) - break; - m2 = stov(s); - s = fgets(file); - if (!s) - break; - fl = stof(s); - waypoint_spawn(m1, m2, fl); - if (m1 == m2) - cwp = cwp + 1; - else - cwb = cwb + 1; - } - fclose(file); - LOG_TRACE("loaded ", ftos(cwp), " waypoints and ", ftos(cwb), " wayboxes from maps/", mapname, ".waypoints\n"); - } - else - { - LOG_TRACE("waypoint load from ", filename, " failed\n"); - } - return cwp + cwb; -} - -vector waypoint_fixorigin(vector position) -{ - tracebox(position + '0 0 1' * (1 - STAT(PL_MIN, NULL).z), STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), position + '0 0 -512', MOVE_NOMONSTERS, NULL); - if(trace_fraction < 1) - position = trace_endpos; - //traceline(position, position + '0 0 -512', MOVE_NOMONSTERS, NULL); - //print("position is ", ftos(trace_endpos_z - position_z), " above solid\n"); - return position; -} - -void waypoint_spawnforitem_force(entity e, vector org) -{ - // Fix the waypoint altitude if necessary - org = waypoint_fixorigin(org); - - // don't spawn an item spawnfunc_waypoint if it already exists - IL_EACH(g_waypoints, true, - { - if(it.wpisbox) - { - if(boxesoverlap(org, org, it.absmin, it.absmax)) - { - e.nearestwaypoint = it; - return; - } - } - else - { - if(vdist(it.origin - org, <, 16)) - { - e.nearestwaypoint = it; - return; - } - } - }); - - e.nearestwaypoint = waypoint_spawn(org, org, WAYPOINTFLAG_GENERATED | WAYPOINTFLAG_ITEM); -} - -void waypoint_spawnforitem(entity e) -{ - if(!bot_waypoints_for_items) - return; - - waypoint_spawnforitem_force(e, e.origin); -} - -void waypoint_spawnforteleporter_boxes(entity e, vector org1, vector org2, vector destination1, vector destination2, float timetaken) -{ - entity w; - entity dw; - w = waypoint_spawn(org1, org2, WAYPOINTFLAG_GENERATED | WAYPOINTFLAG_TELEPORT | WAYPOINTFLAG_NORELINK); - dw = waypoint_spawn(destination1, destination2, WAYPOINTFLAG_GENERATED); - // one way link to the destination - w.wp00 = dw; - w.wp00mincost = timetaken; // this is just for jump pads - // the teleporter's nearest spawnfunc_waypoint is this one - // (teleporters are not goals, so this is probably useless) - e.nearestwaypoint = w; - e.nearestwaypointtimeout = time + 1000000000; -} - -void waypoint_spawnforteleporter_v(entity e, vector org, vector destination, float timetaken) -{ - org = waypoint_fixorigin(org); - destination = waypoint_fixorigin(destination); - waypoint_spawnforteleporter_boxes(e, org, org, destination, destination, timetaken); -} - -void waypoint_spawnforteleporter(entity e, vector destination, float timetaken) -{ - destination = waypoint_fixorigin(destination); - waypoint_spawnforteleporter_boxes(e, e.absmin, e.absmax, destination, destination, timetaken); -} - -entity waypoint_spawnpersonal(entity this, vector position) -{ - entity w; - - // drop the waypoint to a proper location: - // first move it up by a player height - // then move it down to hit the floor with player bbox size - position = waypoint_fixorigin(position); - - w = waypoint_spawn(position, position, WAYPOINTFLAG_GENERATED | WAYPOINTFLAG_PERSONAL); - w.nearestwaypoint = NULL; - w.nearestwaypointtimeout = 0; - w.owner = this; - - waypoint_schedulerelink(w); - - return w; -} - -void botframe_showwaypointlinks() -{ - if (time < botframe_waypointeditorlightningtime) - return; - botframe_waypointeditorlightningtime = time + 0.5; - FOREACH_CLIENT(IS_PLAYER(it) && !it.isbot, - { - if(IS_ONGROUND(it) || it.waterlevel > WATERLEVEL_NONE) - { - //navigation_testtracewalk = true; - entity head = navigation_findnearestwaypoint(it, false); - // print("currently selected WP is ", etos(head), "\n"); - //navigation_testtracewalk = false; - if (head) - { - entity w; - w = head ;if (w) te_lightning2(NULL, w.origin, it.origin); - w = head.wp00;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp01;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp02;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp03;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp04;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp05;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp06;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp07;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp08;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp09;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp10;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp11;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp12;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp13;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp14;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp15;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp16;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp17;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp18;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp19;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp20;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp21;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp22;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp23;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp24;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp25;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp26;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp27;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp28;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp29;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp30;if (w) te_lightning2(NULL, w.origin, head.origin); - w = head.wp31;if (w) te_lightning2(NULL, w.origin, head.origin); - } - } - }); -} - -float botframe_autowaypoints_fixdown(vector v) -{ - tracebox(v, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), v + '0 0 -64', MOVE_NOMONSTERS, NULL); - if(trace_fraction >= 1) - return 0; - return 1; -} - -float botframe_autowaypoints_createwp(vector v, entity p, .entity fld, float f) -{ - IL_EACH(g_waypoints, boxesoverlap(v - '32 32 32', v + '32 32 32', it.absmin, it.absmax), - { - // if a matching spawnfunc_waypoint already exists, don't add a duplicate - return 0; - }); - - waypoint_schedulerelink(p.(fld) = waypoint_spawn(v, v, f)); - return 1; -} - -// return value: -// 1 = WP created -// 0 = no action needed -// -1 = temp fail, try from world too -// -2 = permanent fail, do not retry -float botframe_autowaypoints_fix_from(entity p, float walkfromwp, entity wp, .entity fld) -{ - // make it possible to go from p to wp, if we can - // if wp is NULL, nearest is chosen - - entity w; - vector porg; - float t, tmin, tmax; - vector o; - vector save; - - if(!botframe_autowaypoints_fixdown(p.origin)) - return -2; - porg = trace_endpos; - - if(wp) - { - // if any WP w fulfills wp -> w -> porg and w is closer than wp, then switch from wp to w - - // if wp -> porg, then OK - float maxdist; - if(navigation_waypoint_will_link(wp.origin, porg, p, walkfromwp, 1050)) - { - // we may find a better one - maxdist = vlen(wp.origin - porg); - } - else - { - // accept any "good" - maxdist = 2100; - } - - float bestdist = maxdist; - IL_EACH(g_waypoints, it != wp && !(it.wpflags & WAYPOINTFLAG_NORELINK), - { - float d = vlen(wp.origin - it.origin) + vlen(it.origin - porg); - if(d < bestdist) - if(navigation_waypoint_will_link(wp.origin, it.origin, p, walkfromwp, 1050)) - if(navigation_waypoint_will_link(it.origin, porg, p, walkfromwp, 1050)) - { - bestdist = d; - p.(fld) = it; - } - }); - if(bestdist < maxdist) - { - LOG_INFO("update chain to new nearest WP ", etos(p.(fld)), "\n"); - return 0; - } - - if(bestdist < 2100) - { - // we know maxdist < 2100 - // so wp -> porg is still valid - // all is good - p.(fld) = wp; - return 0; - } - - // otherwise, no existing WP can fix our issues - } - else - { - save = p.origin; - setorigin(p, porg); - w = navigation_findnearestwaypoint(p, walkfromwp); - setorigin(p, save); - if(w) - { - p.(fld) = w; - return 0; - } - } - - tmin = 0; - tmax = 1; - for (;;) - { - if(tmax - tmin < 0.001) - { - // did not get a good candidate - return -1; - } - - t = (tmin + tmax) * 0.5; - o = antilag_takebackorigin(p, CS(p), time - t); - if(!botframe_autowaypoints_fixdown(o)) - return -2; - o = trace_endpos; - - if(wp) - { - if(!navigation_waypoint_will_link(wp.origin, o, p, walkfromwp, 1050)) - { - // we cannot walk from wp.origin to o - // get closer to tmax - tmin = t; - continue; - } - } - else - { - save = p.origin; - setorigin(p, o); - w = navigation_findnearestwaypoint(p, walkfromwp); - setorigin(p, save); - if(!w) - { - // we cannot walk from any WP to o - // get closer to tmax - tmin = t; - continue; - } - } - - // if we get here, o is valid regarding waypoints - // check if o is connected right to the player - // we break if it succeeds, as that means o is a good waypoint location - if(navigation_waypoint_will_link(o, porg, p, walkfromwp, 1050)) - break; - - // o is no good, we need to get closer to the player - tmax = t; - } - - LOG_INFO("spawning a waypoint for connecting to ", etos(wp), "\n"); - botframe_autowaypoints_createwp(o, p, fld, 0); - return 1; -} - -// 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); - if(r != -1) - return; - r = botframe_autowaypoints_fix_from(p, walkfromwp, NULL, fld); - if(r != -1) - return; - - LOG_INFO("emergency: got no good nearby WP to build a link from, starting a new chain\n"); - if(!botframe_autowaypoints_fixdown(p.origin)) - return; // shouldn't happen, caught above - botframe_autowaypoints_createwp(trace_endpos, p, fld, WAYPOINTFLAG_PROTECTED); -} - -void botframe_deleteuselesswaypoints() -{ - FOREACH_ENTITY_FLOAT(bot_pickup, true, - { - // NOTE: this protects waypoints if they're the ONLY nearest - // waypoint. That's the intention. - navigation_findnearestwaypoint(it, false); // Walk TO item. - navigation_findnearestwaypoint(it, true); // Walk FROM item. - }); - IL_EACH(g_waypoints, true, - { - it.wpflags |= WAYPOINTFLAG_DEAD_END; - it.wpflags &= ~WAYPOINTFLAG_USEFUL; - // WP is useful if: - if (it.wpflags & WAYPOINTFLAG_ITEM) - it.wpflags |= WAYPOINTFLAG_USEFUL; - if (it.wpflags & WAYPOINTFLAG_TELEPORT) - it.wpflags |= WAYPOINTFLAG_USEFUL; - if (it.wpflags & WAYPOINTFLAG_PROTECTED) - it.wpflags |= WAYPOINTFLAG_USEFUL; - // b) WP is closest WP for an item/spawnpoint/other entity - // This has been done above by protecting these WPs. - }); - // c) There are w1, w, w2 so that w1 -> w, w -> w2 and not w1 -> w2. - IL_EACH(g_waypoints, !(it.wpflags & WAYPOINTFLAG_PERSONAL), - { - for (int m = 0; m < 32; ++m) - { - entity w = waypoint_get_link(it, m); - if (!w) - break; - if (w.wpflags & WAYPOINTFLAG_PERSONAL) - continue; - if (w.wpflags & WAYPOINTFLAG_USEFUL) - continue; - for (int j = 0; j < 32; ++j) - { - entity w2 = waypoint_get_link(w, j); - if (!w2) - break; - if (it == w2) - continue; - if (w2.wpflags & WAYPOINTFLAG_PERSONAL) - continue; - // If we got here, it != w2 exist with it -> w - // and w -> w2. That means the waypoint is not - // a dead end. - w.wpflags &= ~WAYPOINTFLAG_DEAD_END; - for (int k = 0; k < 32; ++k) - { - if (waypoint_get_link(it, k) == w2) - continue; - // IF WE GET HERE, w is proven useful - // to get from it to w2! - w.wpflags |= WAYPOINTFLAG_USEFUL; - goto next; - } - } -LABEL(next) - } - }); - // d) The waypoint is a dead end. Dead end waypoints must be kept as - // they are needed to complete routes while autowaypointing. - - IL_EACH(g_waypoints, !(it.wpflags & (WAYPOINTFLAG_USEFUL | WAYPOINTFLAG_DEAD_END)), - { - LOG_INFOF("Removed a waypoint at %v. Try again for more!\n", it.origin); - te_explosion(it.origin); - waypoint_remove(it); - break; - }); - - IL_EACH(g_waypoints, true, - { - it.wpflags &= ~(WAYPOINTFLAG_USEFUL | WAYPOINTFLAG_DEAD_END); // temp flag - }); -} - -void botframe_autowaypoints() -{ - FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it) && !IS_DEAD(it), LAMBDA( - // going back is broken, so only fix waypoints to walk TO the player - //botframe_autowaypoints_fix(p, false, botframe_autowaypoints_lastwp0); - botframe_autowaypoints_fix(it, true, botframe_autowaypoints_lastwp1); - //te_explosion(p.botframe_autowaypoints_lastwp0.origin); - )); - - if (autocvar_g_waypointeditor_auto >= 2) { - botframe_deleteuselesswaypoints(); - } -} - diff --git a/qcsrc/server/bot/waypoints.qh b/qcsrc/server/bot/waypoints.qh deleted file mode 100644 index 23c0fa63c..000000000 --- a/qcsrc/server/bot/waypoints.qh +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once -/* - * Globals and Fields - */ - -const int WAYPOINTFLAG_GENERATED = BIT(23); -const int WAYPOINTFLAG_ITEM = BIT(22); -const int WAYPOINTFLAG_TELEPORT = BIT(21); -const int WAYPOINTFLAG_NORELINK = BIT(20); -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. - -// fields you can query using prvm_global server to get some statistics about waypoint linking culling -float relink_total, relink_walkculled, relink_pvsculled, relink_lengthculled; - -float botframe_waypointeditorlightningtime; -float botframe_loadedforcedlinks; -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; - -// itemscore = (howmuchmoreIwant / howmuchIcanwant) / itemdistance -.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; -.int wpflags; - -.vector wpnearestpoint; - -/* - * Functions - */ - -spawnfunc(waypoint); -void waypoint_addlink(entity from, entity to); -void waypoint_think(entity this); -void waypoint_clearlinks(entity wp); -void waypoint_schedulerelink(entity wp); - -void waypoint_remove(entity e); -void waypoint_removeall(); -void waypoint_schedulerelinkall(); -void waypoint_load_links_hardwired(); -void waypoint_save_links(); -void waypoint_saveall(); - -void waypoint_spawnforitem_force(entity e, vector org); -void waypoint_spawnforitem(entity e); -void waypoint_spawnforteleporter(entity e, vector destination, float timetaken); -void waypoint_spawnforteleporter_v(entity e, vector org, vector destination, float timetaken); -void botframe_showwaypointlinks(); - -float waypoint_loadall(); -float waypoint_load_links(); - -entity waypoint_spawn(vector m1, vector m2, float f); -entity waypoint_spawnpersonal(entity this, vector position); - -vector waypoint_fixorigin(vector position); - -void botframe_autowaypoints(); diff --git a/qcsrc/server/cl_client.qc b/qcsrc/server/cl_client.qc index 8de814507..a3082d2b7 100644 --- a/qcsrc/server/cl_client.qc +++ b/qcsrc/server/cl_client.qc @@ -19,8 +19,7 @@ #include "campaign.qh" #include "command/common.qh" -#include "bot/bot.qh" -#include "bot/navigation.qh" +#include "bot/api.qh" #include "../common/ent_cs.qh" #include diff --git a/qcsrc/server/cl_impulse.qc b/qcsrc/server/cl_impulse.qc index a2aeb88a9..51cb60a72 100644 --- a/qcsrc/server/cl_impulse.qc +++ b/qcsrc/server/cl_impulse.qc @@ -1,12 +1,11 @@ #include "cl_impulse.qh" #include "round_handler.qh" -#include "bot/waypoints.qh" +#include "bot/api.qh" #include "weapons/throwing.qh" #include "command/common.qh" #include "cheats.qh" -#include "bot/navigation.qh" #include "weapons/selection.qh" #include "weapons/tracing.qh" #include "weapons/weaponsystem.qh" diff --git a/qcsrc/server/cl_player.qc b/qcsrc/server/cl_player.qc index 29a55cbd8..a542e6431 100644 --- a/qcsrc/server/cl_player.qc +++ b/qcsrc/server/cl_player.qc @@ -1,6 +1,6 @@ #include "cl_player.qh" -#include "bot/bot.qh" +#include "bot/api.qh" #include "cheats.qh" #include "g_damage.qh" #include "g_subs.qh" diff --git a/qcsrc/server/command/sv_cmd.qc b/qcsrc/server/command/sv_cmd.qc index f263f5e61..f983eeb4e 100644 --- a/qcsrc/server/command/sv_cmd.qc +++ b/qcsrc/server/command/sv_cmd.qc @@ -16,9 +16,7 @@ #include "../playerdemo.qh" #include "../teamplay.qh" -#include "../bot/bot.qh" -#include "../bot/navigation.qh" -#include "../bot/scripting.qh" +#include "../bot/api.qh" #include "../mutators/all.qh" diff --git a/qcsrc/server/g_damage.qc b/qcsrc/server/g_damage.qc index debc54743..d7e9b07c0 100644 --- a/qcsrc/server/g_damage.qc +++ b/qcsrc/server/g_damage.qc @@ -1,6 +1,6 @@ #include "g_damage.qh" -#include "bot/bot.qh" +#include "bot/api.qh" #include "g_hook.qh" #include "mutators/all.qh" #include "scores.qh" diff --git a/qcsrc/server/g_world.qc b/qcsrc/server/g_world.qc index ed35a5b0d..39d8acdfc 100644 --- a/qcsrc/server/g_world.qc +++ b/qcsrc/server/g_world.qc @@ -2,7 +2,7 @@ #include "anticheat.qh" #include "antilag.qh" -#include "bot/bot.qh" +#include "bot/api.qh" #include "campaign.qh" #include "cheats.qh" #include "cl_client.qh" diff --git a/qcsrc/server/mutators/gamemode.qh b/qcsrc/server/mutators/gamemode.qh index ee80b44b0..1e60b203e 100644 --- a/qcsrc/server/mutators/gamemode.qh +++ b/qcsrc/server/mutators/gamemode.qh @@ -11,12 +11,7 @@ #include #include -#include -#include -#include -#include - -#include +#include #include diff --git a/qcsrc/server/mutators/mutator.qh b/qcsrc/server/mutators/mutator.qh index 40fad29b3..a63321a36 100644 --- a/qcsrc/server/mutators/mutator.qh +++ b/qcsrc/server/mutators/mutator.qh @@ -11,12 +11,7 @@ #include #include -#include -#include -#include - -#include -#include +#include #include #include diff --git a/qcsrc/server/mutators/mutator/gamemode_keyhunt.qc b/qcsrc/server/mutators/mutator/gamemode_keyhunt.qc index 236db8aa8..26b381199 100644 --- a/qcsrc/server/mutators/mutator/gamemode_keyhunt.qc +++ b/qcsrc/server/mutators/mutator/gamemode_keyhunt.qc @@ -50,8 +50,6 @@ USING(kh_Think_t, void()); void kh_StartRound(); void kh_Controller_SetThink(float t, kh_Think_t func); -entity kh_worldkeylist; -.entity kh_worldkeynext; #endif #ifdef IMPLEMENTATION diff --git a/qcsrc/server/pathlib/path_waypoint.qc b/qcsrc/server/pathlib/path_waypoint.qc index a2aaf5563..f60932c66 100644 --- a/qcsrc/server/pathlib/path_waypoint.qc +++ b/qcsrc/server/pathlib/path_waypoint.qc @@ -1,5 +1,5 @@ #include "path_waypoint.qh" -#include "../bot/waypoints.qh" +#include "../bot/api.qh" #include "pathlib.qh" #include "main.qh" diff --git a/qcsrc/server/progs.inc b/qcsrc/server/progs.inc index 163b16674..c49b604e6 100644 --- a/qcsrc/server/progs.inc +++ b/qcsrc/server/progs.inc @@ -10,7 +10,6 @@ #include "../server/_mod.inc" #include "bot/_mod.inc" -#include "bot/havocbot/_mod.inc" #include "command/_mod.inc" #include "mutators/_mod.inc" #include "pathlib/_all.inc" diff --git a/qcsrc/server/race.qc b/qcsrc/server/race.qc index a46c8b3d9..b315d6cad 100644 --- a/qcsrc/server/race.qc +++ b/qcsrc/server/race.qc @@ -4,8 +4,7 @@ #include "portals.qh" #include "scores.qh" #include "spawnpoints.qh" -#include "bot/waypoints.qh" -#include "bot/navigation.qh" +#include "bot/api.qh" #include "command/getreplies.qh" #include "../common/deathtypes/all.qh" #include "../common/notifications/all.qh" diff --git a/qcsrc/server/sv_main.qc b/qcsrc/server/sv_main.qc index 52887cc15..4a72bb1ae 100644 --- a/qcsrc/server/sv_main.qc +++ b/qcsrc/server/sv_main.qc @@ -4,8 +4,7 @@ #include "g_hook.qh" #include "g_world.qh" -#include "bot/bot.qh" -#include "bot/waypoints.qh" +#include "bot/api.qh" #include "command/common.qh" diff --git a/qcsrc/server/teamplay.qc b/qcsrc/server/teamplay.qc index e2156705a..56f85bd46 100644 --- a/qcsrc/server/teamplay.qc +++ b/qcsrc/server/teamplay.qc @@ -5,7 +5,7 @@ #include "scores.qh" #include "scores_rules.qh" -#include "bot/bot.qh" +#include "bot/api.qh" #include "command/vote.qh"