From: TimePath Date: Wed, 7 Oct 2015 23:15:40 +0000 (+1100) Subject: Bots: define the API boundaries X-Git-Tag: xonotic-v0.8.2~1849^2~1 X-Git-Url: https://git.rm.cloudns.org/?a=commitdiff_plain;h=13719cec41a5a1b20d0fff3fe1b6df449bc2a884;p=xonotic%2Fxonotic-data.pk3dir.git Bots: define the API boundaries --- diff --git a/qcsrc/common/physics.qc b/qcsrc/common/physics.qc index 1a799c8de..6d6322611 100644 --- a/qcsrc/common/physics.qc +++ b/qcsrc/common/physics.qc @@ -1109,6 +1109,8 @@ void PM_check_frozen(void) } } +.float ladder_time; + void PM_check_hitground() {SELFPARAM(); #ifdef SVQC @@ -1300,6 +1302,8 @@ void PM_swim(float maxspd_mod) PM_ClientMovement_Move(); } +.entity ladder_entity; + void PM_ladder(float maxspd_mod) {SELFPARAM(); // on a spawnfunc_func_ladder or swimming in spawnfunc_func_water diff --git a/qcsrc/common/triggers/func/breakable.qc b/qcsrc/common/triggers/func/breakable.qc index af9a6b423..e6253b14d 100644 --- a/qcsrc/common/triggers/func/breakable.qc +++ b/qcsrc/common/triggers/func/breakable.qc @@ -4,7 +4,6 @@ #include "../../../server/g_subs.qh" #include "../../../server/g_damage.qh" -#include "../../../server/bot/bot.qh" #include "../../../common/csqcmodel_settings.qh" #include "../../../csqcmodellib/sv_model.qh" #include "../../../server/weapons/common.qh" diff --git a/qcsrc/common/weapons/all.qh b/qcsrc/common/weapons/all.qh index c96061fe6..faea45b5f 100644 --- a/qcsrc/common/weapons/all.qh +++ b/qcsrc/common/weapons/all.qh @@ -26,10 +26,6 @@ WepSet ReadWepSet(); #include "../util.qh" -#ifdef SVQC -#include "../../server/bot/aim.qh" -#endif - REGISTRY(Weapons, 72) // Increase as needed. Can be up to 72. REGISTER_REGISTRY(RegisterWeapons) entity get_weaponinfo(int id); diff --git a/qcsrc/server/autocvars.qh b/qcsrc/server/autocvars.qh index affd9437e..6a2ff9d4c 100644 --- a/qcsrc/server/autocvars.qh +++ b/qcsrc/server/autocvars.qh @@ -7,63 +7,6 @@ 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") -bool autocvar_bot_sound_monopoly; -#define autocvar_bot_suffix cvar_string("bot_suffix") -bool autocvar_bot_usemodelnames; -int autocvar_bot_vs_human; -int autocvar_bot_debug; -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") #define autocvar_developer cvar("developer") @@ -293,7 +236,6 @@ float autocvar_g_ctf_dropped_capture_delay; float autocvar_g_ctf_dropped_capture_radius; float autocvar_g_cts_finish_kill_delay; bool autocvar_g_cts_selfdamage; -bool autocvar_g_debug_bot_commands; int autocvar_g_domination_default_teams; bool autocvar_g_domination_disable_frags; int autocvar_g_domination_point_amt; @@ -502,7 +444,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; float autocvar_g_weapon_charge_colormod_blue_full; float autocvar_g_weapon_charge_colormod_blue_half; @@ -534,7 +475,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; @@ -584,7 +524,6 @@ float autocvar_sv_itemstime; string autocvar_sv_jumpspeedcap_max; float autocvar_sv_jumpspeedcap_max_disable_on_ramps; string autocvar_sv_jumpspeedcap_min; -float autocvar_sv_jumpvelocity; bool autocvar_sv_logscores_bots; bool autocvar_sv_logscores_console; bool autocvar_sv_logscores_file; @@ -652,7 +591,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/aim.qc b/qcsrc/server/bot/aim.qc deleted file mode 100644 index 4f750cdb4..000000000 --- a/qcsrc/server/bot/aim.qc +++ /dev/null @@ -1,389 +0,0 @@ -#include "aim.qh" -#include "../_all.qh" - -#include "bot.qh" - -#include "../mutators/mutators_include.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 = spawn(); - 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 = spawn(); - tracetossfaketarget.solid = savesolid; - tracetossfaketarget.movetype = targ.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; - tracetossfaketarget.movetype = 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; - tracetossfaketarget.movetype = 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() -{SELFPARAM(); - if (self.lag1_time) if (time > self.lag1_time) {self.lag_func(self.lag1_time, self.lag1_float1, self.lag1_float2, self.lag1_entity1, self.lag1_vec1, self.lag1_vec2, self.lag1_vec3, self.lag1_vec4);self.lag1_time = 0;} - if (self.lag2_time) if (time > self.lag2_time) {self.lag_func(self.lag2_time, self.lag2_float1, self.lag2_float2, self.lag2_entity1, self.lag2_vec1, self.lag2_vec2, self.lag2_vec3, self.lag2_vec4);self.lag2_time = 0;} - if (self.lag3_time) if (time > self.lag3_time) {self.lag_func(self.lag3_time, self.lag3_float1, self.lag3_float2, self.lag3_entity1, self.lag3_vec1, self.lag3_vec2, self.lag3_vec3, self.lag3_vec4);self.lag3_time = 0;} - if (self.lag4_time) if (time > self.lag4_time) {self.lag_func(self.lag4_time, self.lag4_float1, self.lag4_float2, self.lag4_entity1, self.lag4_vec1, self.lag4_vec2, self.lag4_vec3, self.lag4_vec4);self.lag4_time = 0;} - if (self.lag5_time) if (time > self.lag5_time) {self.lag_func(self.lag5_time, self.lag5_float1, self.lag5_float2, self.lag5_entity1, self.lag5_vec1, self.lag5_vec2, self.lag5_vec3, self.lag5_vec4);self.lag5_time = 0;} -} - -float lag_additem(float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4) -{SELFPARAM(); - if (self.lag1_time == 0) {self.lag1_time = t;self.lag1_float1 = f1;self.lag1_float2 = f2;self.lag1_entity1 = e1;self.lag1_vec1 = v1;self.lag1_vec2 = v2;self.lag1_vec3 = v3;self.lag1_vec4 = v4;return true;} - if (self.lag2_time == 0) {self.lag2_time = t;self.lag2_float1 = f1;self.lag2_float2 = f2;self.lag2_entity1 = e1;self.lag2_vec1 = v1;self.lag2_vec2 = v2;self.lag2_vec3 = v3;self.lag2_vec4 = v4;return true;} - if (self.lag3_time == 0) {self.lag3_time = t;self.lag3_float1 = f1;self.lag3_float2 = f2;self.lag3_entity1 = e1;self.lag3_vec1 = v1;self.lag3_vec2 = v2;self.lag3_vec3 = v3;self.lag3_vec4 = v4;return true;} - if (self.lag4_time == 0) {self.lag4_time = t;self.lag4_float1 = f1;self.lag4_float2 = f2;self.lag4_entity1 = e1;self.lag4_vec1 = v1;self.lag4_vec2 = v2;self.lag4_vec3 = v3;self.lag4_vec4 = v4;return true;} - if (self.lag5_time == 0) {self.lag5_time = t;self.lag5_float1 = f1;self.lag5_float2 = f2;self.lag5_entity1 = e1;self.lag5_vec1 = v1;self.lag5_vec2 = v2;self.lag5_vec3 = v3;self.lag5_vec4 = v4;return true;} - // no room for it (what is the best thing to do here??) - return false; -} - -float bot_shouldattack(entity e) -{SELFPARAM(); - if (e.team == self.team) - { - if (e == self) - return false; - if (teamplay) - if (e.team != 0) - return false; - } - - if(e.frozen) - return false; - - if(teamplay) - { - if(e.team==0) - return false; - } - else if(bot_ignore_bots) - if(IS_BOT_CLIENT(e)) - return false; - - if (!e.takedamage) - return false; - if (e.deadflag) - return false; - if (e.BUTTON_CHAT) - return false; - if(e.flags & FL_NOTARGET) - return false; - - if(MUTATOR_CALLHOOK(BotShouldAttack, e)) - return false; - - return true; -} - -void bot_lagfunc(float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4) -{SELFPARAM(); - if(self.flags & FL_INWATER) - { - self.bot_aimtarg = world; - return; - } - self.bot_aimtarg = e1; - self.bot_aimlatency = self.ping; // FIXME? Shouldn't this be in the lag item? - self.bot_aimselforigin = v1; - self.bot_aimselfvelocity = v2; - self.bot_aimtargorigin = v3; - self.bot_aimtargvelocity = v4; - if(skill <= 0) - self.bot_canfire = (random() < 0.8); - else if(skill <= 1) - self.bot_canfire = (random() < 0.9); - else if(skill <= 2) - self.bot_canfire = (random() < 0.95); - else - self.bot_canfire = 1; -} - -float bot_aimdir(vector v, float maxfiredeviation) -{SELFPARAM(); - float dist, delta_t, blend; - vector desiredang, diffang; - - //dprint("aim ", self.netname, ": old:", vtos(self.v_angle)); - // make sure v_angle is sane first - self.v_angle_y = self.v_angle.y - floor(self.v_angle.y / 360) * 360; - self.v_angle_z = 0; - - // get the desired angles to aim at - //dprint(" at:", vtos(v)); - v = normalize(v); - //te_lightning2(world, self.origin + self.view_ofs, self.origin + self.view_ofs + v * 200); - if (time >= self.bot_badaimtime) - { - self.bot_badaimtime = max(self.bot_badaimtime + 0.3, time); - self.bot_badaimoffset = randomvec() * bound(0, 5 - 0.5 * (skill+self.bot_offsetskill), 5) * autocvar_bot_ai_aimskill_offset; - } - desiredang = vectoangles(v) + self.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 = self.v_angle.z; - //dprint(" / ", vtos(desiredang)); - - //// pain throws off aim - //if (self.bot_painintensity) - //{ - // // shake from pain - // desiredang = desiredang + randomvec() * self.bot_painintensity * 0.2; - //} - - // calculate turn angles - diffang = (desiredang - self.bot_olddesiredang); - // wrap yaw turn - diffang.y = diffang.y - floor(diffang.y / 360) * 360; - if (diffang.y >= 180) - diffang.y = diffang.y - 360; - self.bot_olddesiredang = desiredang; - //dprint(" diff:", vtos(diffang)); - - delta_t = time-self.bot_prevaimtime; - self.bot_prevaimtime = time; - // Here we will try to anticipate the comming aiming direction - self.bot_1st_order_aimfilter= self.bot_1st_order_aimfilter - + (diffang * (1 / delta_t) - self.bot_1st_order_aimfilter) * bound(0, autocvar_bot_ai_aimskill_order_filter_1st,1); - self.bot_2nd_order_aimfilter= self.bot_2nd_order_aimfilter - + (self.bot_1st_order_aimfilter - self.bot_2nd_order_aimfilter) * bound(0, autocvar_bot_ai_aimskill_order_filter_2nd,1); - self.bot_3th_order_aimfilter= self.bot_3th_order_aimfilter - + (self.bot_2nd_order_aimfilter - self.bot_3th_order_aimfilter) * bound(0, autocvar_bot_ai_aimskill_order_filter_3th,1); - self.bot_4th_order_aimfilter= self.bot_4th_order_aimfilter - + (self.bot_3th_order_aimfilter - self.bot_4th_order_aimfilter) * bound(0, autocvar_bot_ai_aimskill_order_filter_4th,1); - self.bot_5th_order_aimfilter= self.bot_5th_order_aimfilter - + (self.bot_4th_order_aimfilter - self.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+self.bot_aimskill,10)*0.1; - desiredang = desiredang + blend * - ( - self.bot_1st_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_1st - + self.bot_2nd_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_2nd - + self.bot_3th_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_3th - + self.bot_4th_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_4th - + self.bot_5th_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_5th - ); - - // calculate turn angles - diffang = desiredang - self.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 >= self.bot_aimthinktime) - { - self.bot_aimthinktime = max(self.bot_aimthinktime + 0.5 - 0.05*(skill+self.bot_thinkskill), time); - self.bot_mouseaim = self.bot_mouseaim + diffang * (1-random()*0.1*bound(1,10-(skill+self.bot_thinkskill),10)); - } - - //self.v_angle = self.v_angle + diffang * bound(0, r * frametime * (skill * 0.5 + 2), 1); - - diffang = self.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 - self.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); - //self.v_angle = self.v_angle + diffang * bound(frametime, r * frametime * (2+skill*skill*0.05-random()*0.05*(10-skill)), 1); - self.v_angle = self.v_angle + diffang * bound(delta_t, r * delta_t * (2+pow(skill+self.bot_mouseskill,3)*0.005-random()), 1); - self.v_angle = self.v_angle * bound(0,autocvar_bot_ai_aimskill_mouse,1) + desiredang * bound(0,(1-autocvar_bot_ai_aimskill_mouse),1); - //self.v_angle = self.v_angle + diffang * bound(0, r * frametime * (skill * 0.5 + 2), 1); - //self.v_angle = self.v_angle + diffang * (1/ blendrate); - self.v_angle_z = 0; - self.v_angle_y = self.v_angle.y - floor(self.v_angle.y / 360) * 360; - //dprint(" turn:", vtos(self.v_angle)); - - makevectors(self.v_angle); - shotorg = self.origin + self.view_ofs; - shotdir = v_forward; - - //dprint(" dir:", vtos(v_forward)); - //te_lightning2(world, shotorg, shotorg + shotdir * 100); - - // calculate turn angles again - //diffang = desiredang - self.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 (vlen(trace_endpos-shotorg) < 500+500*bound(0, skill+self.bot_aggresskill, 10) || random()*random()>bound(0,(skill+self.bot_aggresskill)*0.05,1)) - self.bot_firetimer = time + bound(0.1, 0.5-(skill+self.bot_aggresskill)*0.05, 0.5); - //traceline(shotorg,shotorg+shotdir*1000,false,world); - //dprint(ftos(maxfiredeviation),"\n"); - //dprint(" diff:", vtos(diffang), "\n"); - - return self.bot_canfire && (time < self.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); -} - -float bot_aim(float shotspeed, float shotspeedupward, float maxshottime, float applygravity) -{SELFPARAM(); - float f, r, hf, distanceratio; - vector v; - /* - eprint(self); - dprint("bot_aim(", ftos(shotspeed)); - dprint(", ", ftos(shotspeedupward)); - dprint(", ", ftos(maxshottime)); - dprint(", ", ftos(applygravity)); - dprint(");\n"); - */ - - hf = self.dphitcontentsmask; - self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_CORPSE; - - shotspeed *= W_WeaponSpeedFactor(); - shotspeedupward *= W_WeaponSpeedFactor(); - if (!shotspeed) - { - LOG_TRACE("bot_aim: WARNING: weapon ", WEP_NAME(self.weapon), " shotspeed is zero!\n"); - shotspeed = 1000000; - } - if (!maxshottime) - { - LOG_TRACE("bot_aim: WARNING: weapon ", WEP_NAME(self.weapon), " maxshottime is zero!\n"); - maxshottime = 1; - } - makevectors(self.v_angle); - shotorg = self.origin + self.view_ofs; - shotdir = v_forward; - v = bot_shotlead(self.bot_aimtargorigin, self.bot_aimtargvelocity, shotspeed, self.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 && self.bot_aimtarg) - { - if (!findtrajectorywithleading(shotorg, '0 0 0', '0 0 0', self.bot_aimtarg, shotspeed, shotspeedupward, maxshottime, 0, self)) - { - self.dphitcontentsmask = hf; - return false; - } - - f = bot_aimdir(findtrajectory_velocity - shotspeedupward * '0 0 1', r); - } - else - { - f = bot_aimdir(v - shotorg, r); - //dprint("AIM: ");dprint(vtos(self.bot_aimtargorigin));dprint(" + ");dprint(vtos(self.bot_aimtargvelocity));dprint(" * ");dprint(ftos(self.bot_aimlatency + vlen(self.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, self); - //if (trace_ent.takedamage) - //if (trace_fraction < 1) - //if (!bot_shouldattack(trace_ent)) - // return false; - traceline(shotorg, self.bot_aimtargorigin, false, self); - if (trace_fraction < 1) - if (trace_ent != self.enemy) - if (!bot_shouldattack(trace_ent)) - { - self.dphitcontentsmask = hf; - return false; - } - } - - //if (r > maxshottime * shotspeed) - // return false; - self.dphitcontentsmask = hf; - return true; -} diff --git a/qcsrc/server/bot/aim.qh b/qcsrc/server/bot/aim.qh deleted file mode 100644 index d1cbd0d62..000000000 --- a/qcsrc/server/bot/aim.qh +++ /dev/null @@ -1,101 +0,0 @@ -#ifndef AIM_H -#define AIM_H -/* - * 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_aimselforigin; -.vector bot_aimselfvelocity; -.vector bot_aimtargorigin; -.vector bot_aimtargvelocity; - -.entity bot_aimtarg; - -/* - * Functions - */ - -float lag_additem(float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4); -void lag_update(); -void bot_lagfunc(float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4); - -float bot_shouldattack(entity e); -float bot_aimdir(vector v, float maxfiredeviation); -float bot_aim(float shotspeed, float shotspeedupward, float maxshottime, float 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(float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4) lag_func; -#endif diff --git a/qcsrc/server/bot/api.qh b/qcsrc/server/bot/api.qh new file mode 100644 index 000000000..9dc20ebb3 --- /dev/null +++ b/qcsrc/server/bot/api.qh @@ -0,0 +1,96 @@ +#ifndef BOT_API_H +#define BOT_API_H + +#include "../defs.qh" +#include "../autocvars.qh" +#include "../../common/weapons/all.qh" +#include "../../common/teams.qh" +#include "../teamplay.qh" +#include "../miscfunctions.qh" +#include "../campaign.qh" +#include "../cl_client.qh" +#include "../mutators/mutators_include.qh" +#include "../constants.qh" +#include "../weapons/weaponsystem.qh" + +const int WAYPOINTFLAG_GENERATED = 0x800000; + +int autocvar_bot_vs_human; +int autocvar_bot_number; +bool autocvar_bot_sound_monopoly; +float autocvar_bot_ai_strategyinterval; + +float bot_custom_weapon; +float bot_weapons_close[Weapons_MAX]; +float bot_weapons_far[Weapons_MAX]; +float bot_weapons_mid[Weapons_MAX]; +float currentbots; +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(float shotspeed, float shotspeedupward, float maxshottime, float applygravity); +void bot_clientconnect(); +void bot_clientdisconnect(); +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 e); +void bot_think(); + +entity find_bot_by_name(string name); +entity find_bot_by_number(float number); + +void havocbot_goalrating_controlpoints(float ratingscale, vector org, float sradius); +void havocbot_goalrating_enemyplayers(float ratingscale, vector org, float sradius); +void havocbot_goalrating_items(float ratingscale, vector org, float sradius); + +entity navigation_findnearestwaypoint(entity ent, float walkfromwp); +void navigation_goalrating_end(); +void navigation_goalrating_start(); +void navigation_markroutes(entity fixed_source_waypoint); +void navigation_markroutes_inverted(entity fixed_source_waypoint); +void navigation_routerating(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); + +#endif diff --git a/qcsrc/server/bot/bot.qc b/qcsrc/server/bot/bot.qc deleted file mode 100644 index 00051cadb..000000000 --- a/qcsrc/server/bot/bot.qc +++ /dev/null @@ -1,725 +0,0 @@ -#include "bot.qh" -#include "../_all.qh" - -#include "aim.qh" -#include "navigation.qh" -#include "scripting.qh" -#include "waypoints.qh" - -#include "havocbot/havocbot.qh" -#include "havocbot/scripting.qh" - -#include "../antilag.qh" -#include "../autocvars.qh" -#include "../campaign.qh" -#include "../cl_client.qh" -#include "../constants.qh" -#include "../defs.qh" -#include "../race.qh" -#include "../t_items.qh" - -#include "../mutators/mutators_include.qh" - -#include "../weapons/accuracy.qh" - -#include "../../common/constants.qh" -#include "../../common/mapinfo.qh" -#include "../../common/teams.qh" -#include "../../common/util.qh" - -#include "../../common/weapons/all.qh" - -#include "../../csqcmodellib/sv_model.qh" - -#include "../../dpdefs/dpextensions.qh" -#include "../../dpdefs/progsdefs.qh" - -#include "../../warpzonelib/common.qh" -#include "../../warpzonelib/util_server.qh" - -entity bot_spawn() -{SELFPARAM(); - entity bot = spawnclient(); - if (bot) - { - currentbots = currentbots + 1; - setself(bot); - bot_setnameandstuff(); - ClientConnect(); - PutClientInServer(); - setself(this); - } - return bot; -} - -void bot_think() -{SELFPARAM(); - if (self.bot_nextthink > time) - return; - - self.flags &= ~FL_GODMODE; - if(autocvar_bot_god) - self.flags |= FL_GODMODE; - - self.bot_nextthink = self.bot_nextthink + autocvar_bot_ai_thinkinterval * pow(0.5, self.bot_aiskill); - //if (self.bot_painintensity > 0) - // self.bot_painintensity = self.bot_painintensity - (skill + 1) * 40 * frametime; - - //self.bot_painintensity = self.bot_painintensity + self.bot_oldhealth - self.health; - //self.bot_painintensity = bound(0, self.bot_painintensity, 100); - - if (!IS_PLAYER(self) || (autocvar_g_campaign && !campaign_bots_may_start)) - { - self.bot_nextthink = time + 0.5; - return; - } - - if (self.fixangle) - { - self.v_angle = self.angles; - self.v_angle_z = 0; - self.fixangle = false; - } - - self.dmg_take = 0; - self.dmg_save = 0; - self.dmg_inflictor = world; - - // calculate an aiming latency based on the skill setting - // (simulated network latency + naturally delayed reflexes) - //self.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 - self.ping = bound(0,0.07 - bound(0, (skill + self.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 - self.BUTTON_ATCK = 0; - self.button1 = 0; - self.BUTTON_JUMP = 0; - self.BUTTON_ATCK2 = 0; - self.BUTTON_ZOOM = 0; - self.BUTTON_CROUCH = 0; - self.BUTTON_HOOK = 0; - self.BUTTON_INFO = 0; - self.button8 = 0; - self.BUTTON_CHAT = 0; - self.BUTTON_USE = 0; - - if (time < game_starttime) - { - // block the bot during the countdown to game start - self.movement = '0 0 0'; - self.bot_nextthink = game_starttime; - return; - } - - // if dead, just wait until we can respawn - if (self.deadflag) - { - if (self.deadflag == DEAD_DEAD) - { - self.BUTTON_JUMP = 1; // press jump to respawn - self.bot_strategytime = 0; - } - } - else if(self.aistatus & AI_STATUS_STUCK) - navigation_unstuck(); - - // now call the current bot AI (havocbot for example) - self.bot_ai(); -} - -void bot_setnameandstuff() -{SELFPARAM(); - string readfile, s; - float file, tokens, prio; - entity p; - - 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; - FOR_EACH_CLIENT(p) - { - if(IS_BOT_CLIENT(p)) - if(s == p.cleanname) - { - prio = 0; - break; - } - } - RandomSelection_Add(world, 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)); - - self.bot_forced_team = stof(argv(5)); - - prio = 6; - - #define READSKILL(f,w,r) if(argv(prio) != "") self.f = stof(argv(prio)) * (w); else self.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 - - self.bot_config_loaded = true; - - // this is really only a default, JoinBestTeam is called later - setcolor(self, stof(bot_shirt) * 16 + stof(bot_pants)); - self.bot_preferredcolors = self.clientcolors; - - // pick the name - if (autocvar_bot_usemodelnames) - name = bot_model; - else - name = bot_name; - - // number bots with identical names - float i; - i = 0; - FOR_EACH_CLIENT(p) - { - if(IS_BOT_CLIENT(p)) - if(p.cleanname == name) - ++i; - } - if (i) - self.netname = self.netname_freeme = strzone(strcat(prefix, name, "(", ftos(i), ")", suffix)); - else - self.netname = self.netname_freeme = strzone(strcat(prefix, name, suffix)); - - self.cleanname = strzone(name); - - // pick the model and skin - if(substring(bot_model, -4, 1) != ".") - bot_model = strcat(bot_model, ".iqm"); - self.playermodel = self.playermodel_freeme = strzone(strcat("models/player/", bot_model)); - self.playerskin = self.playerskin_freeme = strzone(bot_skin); - - self.cvar_cl_accuracy_data_share = 1; // share the bots weapon accuracy data with the world - self.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() -{ - entity e; - entity prevbot; - player_count = 0; - currentbots = 0; - player_list = e = findchainflags(flags, FL_CLIENT); - bot_list = world; - prevbot = world; - while (e) - { - player_count = player_count + 1; - e.nextplayer = e.chain; - if (IS_BOT_CLIENT(e)) - { - if (prevbot) - prevbot.nextbot = e; - else - { - bot_list = e; - bot_list.nextbot = world; - } - prevbot = e; - currentbots = currentbots + 1; - } - e = e.chain; - } - LOG_TRACE(strcat("relink: ", ftos(currentbots), " bots seen.\n")); - bot_strategytoken = bot_list; - bot_strategytoken_taken = true; -} - -void bot_clientdisconnect() -{SELFPARAM(); - if (!IS_BOT_CLIENT(self)) - return; - bot_clearqueue(self); - if(self.cleanname) - strunzone(self.cleanname); - if(self.netname_freeme) - strunzone(self.netname_freeme); - if(self.playermodel_freeme) - strunzone(self.playermodel_freeme); - if(self.playerskin_freeme) - strunzone(self.playerskin_freeme); - self.cleanname = string_null; - self.netname_freeme = string_null; - self.playermodel_freeme = string_null; - self.playerskin_freeme = string_null; - if(self.bot_cmd_current) - remove(self.bot_cmd_current); - if(bot_waypoint_queue_owner==self) - bot_waypoint_queue_owner = world; -} - -void bot_clientconnect() -{SELFPARAM(); - if (!IS_BOT_CLIENT(self)) - return; - self.bot_preferredcolors = self.clientcolors; - self.bot_nextthink = time - random(); - self.lag_func = bot_lagfunc; - self.isbot = true; - self.createdtime = self.bot_nextthink; - - if(!self.bot_config_loaded) // This is needed so team overrider doesn't break between matches - bot_setnameandstuff(); - - if(self.bot_forced_team==1) - self.team = NUM_TEAM_1; - else if(self.bot_forced_team==2) - self.team = NUM_TEAM_2; - else if(self.bot_forced_team==3) - self.team = NUM_TEAM_3; - else if(self.bot_forced_team==4) - self.team = NUM_TEAM_4; - else - JoinBestTeam(self, false, true); - - havocbot_setupbot(); -} - -void bot_removefromlargestteam() -{ - float besttime, bestcount, thiscount; - entity best, head; - CheckAllowedTeams(world); - GetTeamCounts(world); - head = findchainfloat(isbot, true); - if (!head) - return; - best = head; - besttime = head.createdtime; - bestcount = 0; - while (head) - { - if(head.team == NUM_TEAM_1) - thiscount = c1; - else if(head.team == NUM_TEAM_2) - thiscount = c2; - else if(head.team == NUM_TEAM_3) - thiscount = c3; - else if(head.team == NUM_TEAM_4) - thiscount = c4; - else - thiscount = 0; - if (thiscount > bestcount) - { - bestcount = thiscount; - besttime = head.createdtime; - best = head; - } - else if (thiscount == bestcount && besttime < head.createdtime) - { - besttime = head.createdtime; - best = head; - } - head = head.chain; - } - currentbots = currentbots - 1; - dropclient(best); -} - -void bot_removenewest() -{ - float besttime; - entity best, head; - - if(teamplay) - { - bot_removefromlargestteam(); - return; - } - - head = findchainfloat(isbot, true); - if (!head) - return; - best = head; - besttime = head.createdtime; - while (head) - { - if (besttime < head.createdtime) - { - besttime = head.createdtime; - best = head; - } - head = head.chain; - } - currentbots = currentbots - 1; - dropclient(best); -} - -void autoskill(float factor) -{ - float bestbot; - float bestplayer; - entity head; - - bestbot = -1; - bestplayer = -1; - FOR_EACH_PLAYER(head) - { - if(IS_REAL_CLIENT(head)) - bestplayer = max(bestplayer, head.totalfrags - head.totalfrags_lastcheck); - else - bestbot = max(bestbot, head.totalfrags - head.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 - } - - FOR_EACH_PLAYER(head) - head.totalfrags_lastcheck = head.totalfrags; -} - -void bot_calculate_stepheightvec(void) -{ - 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() -{ - entity head; - float realplayers, bots, activerealplayers; - - activerealplayers = 0; - realplayers = 0; - - FOR_EACH_REALCLIENT(head) - { - if(IS_PLAYER(head) || g_lms || head.caplayer == 1) - ++activerealplayers; - ++realplayers; - } - - // 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 && (c3==-1 && c4==-1)) - 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() == world) - { - 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 - entity wp = findchain(classname, "waypoint"); - if(time - wp.nextthink > 10) - waypoint_save_links(); - } - } - 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 d5a279400..000000000 --- a/qcsrc/server/bot/bot.qh +++ /dev/null @@ -1,118 +0,0 @@ -#ifndef BOT_H -#define BOT_H -/* - * Globals and Fields - */ - -const int AI_STATUS_ROAMING = 1; // Bot is just crawling the map. No enemies at sight -const int AI_STATUS_ATTACKING = 2; // There are enemies at sight -const int AI_STATUS_RUNNING = 4; // Bot is bunny hopping -const int AI_STATUS_DANGER_AHEAD = 8; // There is lava/slime/trigger_hurt ahead -const int AI_STATUS_OUT_JUMPPAD = 16; // Trying to get out of a "vertical" jump pad -const int AI_STATUS_OUT_WATER = 32; // Trying to get out of water -const int AI_STATUS_WAYPOINT_PERSONAL_LINKING = 64; // Waiting for the personal waypoint to be linked -const int AI_STATUS_WAYPOINT_PERSONAL_GOING = 128; // Going to a personal waypoint -const int AI_STATUS_WAYPOINT_PERSONAL_REACHED = 256; // Personal waypoint reached -const int AI_STATUS_JETPACK_FLYING = 512; -const int AI_STATUS_JETPACK_LANDING = 1024; -const int AI_STATUS_STUCK = 2048; // 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 player_list; -.entity nextbot; -.entity nextplayer; -.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(); -void bot_setnameandstuff(); -void bot_custom_weapon_priority_setup(); -void bot_endgame(); -void bot_relinkplayerlist(); -void bot_clientdisconnect(); -void bot_clientconnect(); -void bot_removefromlargestteam(); -void bot_removenewest(); -void autoskill(float factor); -void bot_serverframe(); - -.void() bot_ai; -.float(entity player, entity item) bot_pickupevalfunc; - -/* - * Imports - */ - -void() havocbot_setupbot; - -void bot_calculate_stepheightvec(void); -#endif diff --git a/qcsrc/server/bot/default/aim.qc b/qcsrc/server/bot/default/aim.qc new file mode 100644 index 000000000..6353d1ee3 --- /dev/null +++ b/qcsrc/server/bot/default/aim.qc @@ -0,0 +1,384 @@ +#include "aim.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 = spawn(); + 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 = spawn(); + tracetossfaketarget.solid = savesolid; + tracetossfaketarget.movetype = targ.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; + tracetossfaketarget.movetype = 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; + tracetossfaketarget.movetype = 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() +{SELFPARAM(); + if (self.lag1_time) if (time > self.lag1_time) {self.lag_func(self.lag1_time, self.lag1_float1, self.lag1_float2, self.lag1_entity1, self.lag1_vec1, self.lag1_vec2, self.lag1_vec3, self.lag1_vec4);self.lag1_time = 0;} + if (self.lag2_time) if (time > self.lag2_time) {self.lag_func(self.lag2_time, self.lag2_float1, self.lag2_float2, self.lag2_entity1, self.lag2_vec1, self.lag2_vec2, self.lag2_vec3, self.lag2_vec4);self.lag2_time = 0;} + if (self.lag3_time) if (time > self.lag3_time) {self.lag_func(self.lag3_time, self.lag3_float1, self.lag3_float2, self.lag3_entity1, self.lag3_vec1, self.lag3_vec2, self.lag3_vec3, self.lag3_vec4);self.lag3_time = 0;} + if (self.lag4_time) if (time > self.lag4_time) {self.lag_func(self.lag4_time, self.lag4_float1, self.lag4_float2, self.lag4_entity1, self.lag4_vec1, self.lag4_vec2, self.lag4_vec3, self.lag4_vec4);self.lag4_time = 0;} + if (self.lag5_time) if (time > self.lag5_time) {self.lag_func(self.lag5_time, self.lag5_float1, self.lag5_float2, self.lag5_entity1, self.lag5_vec1, self.lag5_vec2, self.lag5_vec3, self.lag5_vec4);self.lag5_time = 0;} +} + +float lag_additem(float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4) +{SELFPARAM(); + if (self.lag1_time == 0) {self.lag1_time = t;self.lag1_float1 = f1;self.lag1_float2 = f2;self.lag1_entity1 = e1;self.lag1_vec1 = v1;self.lag1_vec2 = v2;self.lag1_vec3 = v3;self.lag1_vec4 = v4;return true;} + if (self.lag2_time == 0) {self.lag2_time = t;self.lag2_float1 = f1;self.lag2_float2 = f2;self.lag2_entity1 = e1;self.lag2_vec1 = v1;self.lag2_vec2 = v2;self.lag2_vec3 = v3;self.lag2_vec4 = v4;return true;} + if (self.lag3_time == 0) {self.lag3_time = t;self.lag3_float1 = f1;self.lag3_float2 = f2;self.lag3_entity1 = e1;self.lag3_vec1 = v1;self.lag3_vec2 = v2;self.lag3_vec3 = v3;self.lag3_vec4 = v4;return true;} + if (self.lag4_time == 0) {self.lag4_time = t;self.lag4_float1 = f1;self.lag4_float2 = f2;self.lag4_entity1 = e1;self.lag4_vec1 = v1;self.lag4_vec2 = v2;self.lag4_vec3 = v3;self.lag4_vec4 = v4;return true;} + if (self.lag5_time == 0) {self.lag5_time = t;self.lag5_float1 = f1;self.lag5_float2 = f2;self.lag5_entity1 = e1;self.lag5_vec1 = v1;self.lag5_vec2 = v2;self.lag5_vec3 = v3;self.lag5_vec4 = v4;return true;} + // no room for it (what is the best thing to do here??) + return false; +} + +float bot_shouldattack(entity e) +{SELFPARAM(); + if (e.team == self.team) + { + if (e == self) + return false; + if (teamplay) + if (e.team != 0) + return false; + } + + if(e.frozen) + return false; + + if(teamplay) + { + if(e.team==0) + return false; + } + else if(bot_ignore_bots) + if(IS_BOT_CLIENT(e)) + return false; + + if (!e.takedamage) + return false; + if (e.deadflag) + return false; + if (e.BUTTON_CHAT) + return false; + if(e.flags & FL_NOTARGET) + return false; + + if(MUTATOR_CALLHOOK(BotShouldAttack, e)) + return false; + + return true; +} + +void bot_lagfunc(float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4) +{SELFPARAM(); + if(self.flags & FL_INWATER) + { + self.bot_aimtarg = world; + return; + } + self.bot_aimtarg = e1; + self.bot_aimlatency = self.ping; // FIXME? Shouldn't this be in the lag item? + self.bot_aimselforigin = v1; + self.bot_aimselfvelocity = v2; + self.bot_aimtargorigin = v3; + self.bot_aimtargvelocity = v4; + if(skill <= 0) + self.bot_canfire = (random() < 0.8); + else if(skill <= 1) + self.bot_canfire = (random() < 0.9); + else if(skill <= 2) + self.bot_canfire = (random() < 0.95); + else + self.bot_canfire = 1; +} + +float bot_aimdir(vector v, float maxfiredeviation) +{SELFPARAM(); + float dist, delta_t, blend; + vector desiredang, diffang; + + //dprint("aim ", self.netname, ": old:", vtos(self.v_angle)); + // make sure v_angle is sane first + self.v_angle_y = self.v_angle.y - floor(self.v_angle.y / 360) * 360; + self.v_angle_z = 0; + + // get the desired angles to aim at + //dprint(" at:", vtos(v)); + v = normalize(v); + //te_lightning2(world, self.origin + self.view_ofs, self.origin + self.view_ofs + v * 200); + if (time >= self.bot_badaimtime) + { + self.bot_badaimtime = max(self.bot_badaimtime + 0.3, time); + self.bot_badaimoffset = randomvec() * bound(0, 5 - 0.5 * (skill+self.bot_offsetskill), 5) * autocvar_bot_ai_aimskill_offset; + } + desiredang = vectoangles(v) + self.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 = self.v_angle.z; + //dprint(" / ", vtos(desiredang)); + + //// pain throws off aim + //if (self.bot_painintensity) + //{ + // // shake from pain + // desiredang = desiredang + randomvec() * self.bot_painintensity * 0.2; + //} + + // calculate turn angles + diffang = (desiredang - self.bot_olddesiredang); + // wrap yaw turn + diffang.y = diffang.y - floor(diffang.y / 360) * 360; + if (diffang.y >= 180) + diffang.y = diffang.y - 360; + self.bot_olddesiredang = desiredang; + //dprint(" diff:", vtos(diffang)); + + delta_t = time-self.bot_prevaimtime; + self.bot_prevaimtime = time; + // Here we will try to anticipate the comming aiming direction + self.bot_1st_order_aimfilter= self.bot_1st_order_aimfilter + + (diffang * (1 / delta_t) - self.bot_1st_order_aimfilter) * bound(0, autocvar_bot_ai_aimskill_order_filter_1st,1); + self.bot_2nd_order_aimfilter= self.bot_2nd_order_aimfilter + + (self.bot_1st_order_aimfilter - self.bot_2nd_order_aimfilter) * bound(0, autocvar_bot_ai_aimskill_order_filter_2nd,1); + self.bot_3th_order_aimfilter= self.bot_3th_order_aimfilter + + (self.bot_2nd_order_aimfilter - self.bot_3th_order_aimfilter) * bound(0, autocvar_bot_ai_aimskill_order_filter_3th,1); + self.bot_4th_order_aimfilter= self.bot_4th_order_aimfilter + + (self.bot_3th_order_aimfilter - self.bot_4th_order_aimfilter) * bound(0, autocvar_bot_ai_aimskill_order_filter_4th,1); + self.bot_5th_order_aimfilter= self.bot_5th_order_aimfilter + + (self.bot_4th_order_aimfilter - self.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+self.bot_aimskill,10)*0.1; + desiredang = desiredang + blend * + ( + self.bot_1st_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_1st + + self.bot_2nd_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_2nd + + self.bot_3th_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_3th + + self.bot_4th_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_4th + + self.bot_5th_order_aimfilter * autocvar_bot_ai_aimskill_order_mix_5th + ); + + // calculate turn angles + diffang = desiredang - self.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 >= self.bot_aimthinktime) + { + self.bot_aimthinktime = max(self.bot_aimthinktime + 0.5 - 0.05*(skill+self.bot_thinkskill), time); + self.bot_mouseaim = self.bot_mouseaim + diffang * (1-random()*0.1*bound(1,10-(skill+self.bot_thinkskill),10)); + } + + //self.v_angle = self.v_angle + diffang * bound(0, r * frametime * (skill * 0.5 + 2), 1); + + diffang = self.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 - self.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); + //self.v_angle = self.v_angle + diffang * bound(frametime, r * frametime * (2+skill*skill*0.05-random()*0.05*(10-skill)), 1); + self.v_angle = self.v_angle + diffang * bound(delta_t, r * delta_t * (2+pow(skill+self.bot_mouseskill,3)*0.005-random()), 1); + self.v_angle = self.v_angle * bound(0,autocvar_bot_ai_aimskill_mouse,1) + desiredang * bound(0,(1-autocvar_bot_ai_aimskill_mouse),1); + //self.v_angle = self.v_angle + diffang * bound(0, r * frametime * (skill * 0.5 + 2), 1); + //self.v_angle = self.v_angle + diffang * (1/ blendrate); + self.v_angle_z = 0; + self.v_angle_y = self.v_angle.y - floor(self.v_angle.y / 360) * 360; + //dprint(" turn:", vtos(self.v_angle)); + + makevectors(self.v_angle); + shotorg = self.origin + self.view_ofs; + shotdir = v_forward; + + //dprint(" dir:", vtos(v_forward)); + //te_lightning2(world, shotorg, shotorg + shotdir * 100); + + // calculate turn angles again + //diffang = desiredang - self.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 (vlen(trace_endpos-shotorg) < 500+500*bound(0, skill+self.bot_aggresskill, 10) || random()*random()>bound(0,(skill+self.bot_aggresskill)*0.05,1)) + self.bot_firetimer = time + bound(0.1, 0.5-(skill+self.bot_aggresskill)*0.05, 0.5); + //traceline(shotorg,shotorg+shotdir*1000,false,world); + //dprint(ftos(maxfiredeviation),"\n"); + //dprint(" diff:", vtos(diffang), "\n"); + + return self.bot_canfire && (time < self.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); +} + +float bot_aim(float shotspeed, float shotspeedupward, float maxshottime, float applygravity) +{SELFPARAM(); + float f, r, hf, distanceratio; + vector v; + /* + eprint(self); + dprint("bot_aim(", ftos(shotspeed)); + dprint(", ", ftos(shotspeedupward)); + dprint(", ", ftos(maxshottime)); + dprint(", ", ftos(applygravity)); + dprint(");\n"); + */ + + hf = self.dphitcontentsmask; + self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_CORPSE; + + shotspeed *= W_WeaponSpeedFactor(); + shotspeedupward *= W_WeaponSpeedFactor(); + if (!shotspeed) + { + LOG_TRACE("bot_aim: WARNING: weapon ", WEP_NAME(self.weapon), " shotspeed is zero!\n"); + shotspeed = 1000000; + } + if (!maxshottime) + { + LOG_TRACE("bot_aim: WARNING: weapon ", WEP_NAME(self.weapon), " maxshottime is zero!\n"); + maxshottime = 1; + } + makevectors(self.v_angle); + shotorg = self.origin + self.view_ofs; + shotdir = v_forward; + v = bot_shotlead(self.bot_aimtargorigin, self.bot_aimtargvelocity, shotspeed, self.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 && self.bot_aimtarg) + { + if (!findtrajectorywithleading(shotorg, '0 0 0', '0 0 0', self.bot_aimtarg, shotspeed, shotspeedupward, maxshottime, 0, self)) + { + self.dphitcontentsmask = hf; + return false; + } + + f = bot_aimdir(findtrajectory_velocity - shotspeedupward * '0 0 1', r); + } + else + { + f = bot_aimdir(v - shotorg, r); + //dprint("AIM: ");dprint(vtos(self.bot_aimtargorigin));dprint(" + ");dprint(vtos(self.bot_aimtargvelocity));dprint(" * ");dprint(ftos(self.bot_aimlatency + vlen(self.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, self); + //if (trace_ent.takedamage) + //if (trace_fraction < 1) + //if (!bot_shouldattack(trace_ent)) + // return false; + traceline(shotorg, self.bot_aimtargorigin, false, self); + if (trace_fraction < 1) + if (trace_ent != self.enemy) + if (!bot_shouldattack(trace_ent)) + { + self.dphitcontentsmask = hf; + return false; + } + } + + //if (r > maxshottime * shotspeed) + // return false; + self.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..d1cbd0d62 --- /dev/null +++ b/qcsrc/server/bot/default/aim.qh @@ -0,0 +1,101 @@ +#ifndef AIM_H +#define AIM_H +/* + * 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_aimselforigin; +.vector bot_aimselfvelocity; +.vector bot_aimtargorigin; +.vector bot_aimtargvelocity; + +.entity bot_aimtarg; + +/* + * Functions + */ + +float lag_additem(float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4); +void lag_update(); +void bot_lagfunc(float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4); + +float bot_shouldattack(entity e); +float bot_aimdir(vector v, float maxfiredeviation); +float bot_aim(float shotspeed, float shotspeedupward, float maxshottime, float 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(float t, float f1, float f2, entity e1, vector v1, vector v2, vector v3, vector v4) lag_func; +#endif diff --git a/qcsrc/server/bot/default/all.inc b/qcsrc/server/bot/default/all.inc new file mode 100644 index 000000000..a613a3918 --- /dev/null +++ b/qcsrc/server/bot/default/all.inc @@ -0,0 +1,11 @@ +#include "defs.qh" + +#include "bot.qc" +#include "aim.qc" +#include "navigation.qc" +#include "scripting.qc" +#include "waypoints.qc" + +#include "havocbot/havocbot.qc" +#include "havocbot/role_keyhunt.qc" +#include "havocbot/roles.qc" diff --git a/qcsrc/server/bot/default/bot.qc b/qcsrc/server/bot/default/bot.qc new file mode 100644 index 000000000..7eb438ec8 --- /dev/null +++ b/qcsrc/server/bot/default/bot.qc @@ -0,0 +1,697 @@ +#include "bot.qh" + +#include "aim.qh" +#include "navigation.qh" +#include "scripting.qh" +#include "waypoints.qh" + +#include "havocbot/havocbot.qh" + +#include "../../weapons/accuracy.qh" + +entity bot_spawn() +{SELFPARAM(); + entity bot = spawnclient(); + if (bot) + { + currentbots = currentbots + 1; + setself(bot); + bot_setnameandstuff(); + ClientConnect(); + PutClientInServer(); + setself(this); + } + return bot; +} + +void bot_think() +{SELFPARAM(); + if (self.bot_nextthink > time) + return; + + self.flags &= ~FL_GODMODE; + if(autocvar_bot_god) + self.flags |= FL_GODMODE; + + self.bot_nextthink = self.bot_nextthink + autocvar_bot_ai_thinkinterval * pow(0.5, self.bot_aiskill); + //if (self.bot_painintensity > 0) + // self.bot_painintensity = self.bot_painintensity - (skill + 1) * 40 * frametime; + + //self.bot_painintensity = self.bot_painintensity + self.bot_oldhealth - self.health; + //self.bot_painintensity = bound(0, self.bot_painintensity, 100); + + if (!IS_PLAYER(self) || (autocvar_g_campaign && !campaign_bots_may_start)) + { + self.bot_nextthink = time + 0.5; + return; + } + + if (self.fixangle) + { + self.v_angle = self.angles; + self.v_angle_z = 0; + self.fixangle = false; + } + + self.dmg_take = 0; + self.dmg_save = 0; + self.dmg_inflictor = world; + + // calculate an aiming latency based on the skill setting + // (simulated network latency + naturally delayed reflexes) + //self.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 + self.ping = bound(0,0.07 - bound(0, (skill + self.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 + self.BUTTON_ATCK = 0; + self.button1 = 0; + self.BUTTON_JUMP = 0; + self.BUTTON_ATCK2 = 0; + self.BUTTON_ZOOM = 0; + self.BUTTON_CROUCH = 0; + self.BUTTON_HOOK = 0; + self.BUTTON_INFO = 0; + self.button8 = 0; + self.BUTTON_CHAT = 0; + self.BUTTON_USE = 0; + + if (time < game_starttime) + { + // block the bot during the countdown to game start + self.movement = '0 0 0'; + self.bot_nextthink = game_starttime; + return; + } + + // if dead, just wait until we can respawn + if (self.deadflag) + { + if (self.deadflag == DEAD_DEAD) + { + self.BUTTON_JUMP = 1; // press jump to respawn + self.bot_strategytime = 0; + } + } + else if(self.aistatus & AI_STATUS_STUCK) + navigation_unstuck(); + + // now call the current bot AI (havocbot for example) + self.bot_ai(); +} + +void bot_setnameandstuff() +{SELFPARAM(); + string readfile, s; + float file, tokens, prio; + entity p; + + 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; + FOR_EACH_CLIENT(p) + { + if(IS_BOT_CLIENT(p)) + if(s == p.cleanname) + { + prio = 0; + break; + } + } + RandomSelection_Add(world, 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)); + + self.bot_forced_team = stof(argv(5)); + + prio = 6; + + #define READSKILL(f,w,r) if(argv(prio) != "") self.f = stof(argv(prio)) * (w); else self.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 + + self.bot_config_loaded = true; + + // this is really only a default, JoinBestTeam is called later + setcolor(self, stof(bot_shirt) * 16 + stof(bot_pants)); + self.bot_preferredcolors = self.clientcolors; + + // pick the name + if (autocvar_bot_usemodelnames) + name = bot_model; + else + name = bot_name; + + // number bots with identical names + float i; + i = 0; + FOR_EACH_CLIENT(p) + { + if(IS_BOT_CLIENT(p)) + if(p.cleanname == name) + ++i; + } + if (i) + self.netname = self.netname_freeme = strzone(strcat(prefix, name, "(", ftos(i), ")", suffix)); + else + self.netname = self.netname_freeme = strzone(strcat(prefix, name, suffix)); + + self.cleanname = strzone(name); + + // pick the model and skin + if(substring(bot_model, -4, 1) != ".") + bot_model = strcat(bot_model, ".iqm"); + self.playermodel = self.playermodel_freeme = strzone(strcat("models/player/", bot_model)); + self.playerskin = self.playerskin_freeme = strzone(bot_skin); + + self.cvar_cl_accuracy_data_share = 1; // share the bots weapon accuracy data with the world + self.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() +{ + entity e; + entity prevbot; + player_count = 0; + currentbots = 0; + player_list = e = findchainflags(flags, FL_CLIENT); + bot_list = world; + prevbot = world; + while (e) + { + player_count = player_count + 1; + e.nextplayer = e.chain; + if (IS_BOT_CLIENT(e)) + { + if (prevbot) + prevbot.nextbot = e; + else + { + bot_list = e; + bot_list.nextbot = world; + } + prevbot = e; + currentbots = currentbots + 1; + } + e = e.chain; + } + LOG_TRACE(strcat("relink: ", ftos(currentbots), " bots seen.\n")); + bot_strategytoken = bot_list; + bot_strategytoken_taken = true; +} + +void bot_clientdisconnect() +{SELFPARAM(); + if (!IS_BOT_CLIENT(self)) + return; + bot_clearqueue(self); + if(self.cleanname) + strunzone(self.cleanname); + if(self.netname_freeme) + strunzone(self.netname_freeme); + if(self.playermodel_freeme) + strunzone(self.playermodel_freeme); + if(self.playerskin_freeme) + strunzone(self.playerskin_freeme); + self.cleanname = string_null; + self.netname_freeme = string_null; + self.playermodel_freeme = string_null; + self.playerskin_freeme = string_null; + if(self.bot_cmd_current) + remove(self.bot_cmd_current); + if(bot_waypoint_queue_owner==self) + bot_waypoint_queue_owner = world; +} + +void bot_clientconnect() +{SELFPARAM(); + if (!IS_BOT_CLIENT(self)) + return; + self.bot_preferredcolors = self.clientcolors; + self.bot_nextthink = time - random(); + self.lag_func = bot_lagfunc; + self.isbot = true; + self.createdtime = self.bot_nextthink; + + if(!self.bot_config_loaded) // This is needed so team overrider doesn't break between matches + bot_setnameandstuff(); + + if(self.bot_forced_team==1) + self.team = NUM_TEAM_1; + else if(self.bot_forced_team==2) + self.team = NUM_TEAM_2; + else if(self.bot_forced_team==3) + self.team = NUM_TEAM_3; + else if(self.bot_forced_team==4) + self.team = NUM_TEAM_4; + else + JoinBestTeam(self, false, true); + + havocbot_setupbot(); +} + +void bot_removefromlargestteam() +{ + float besttime, bestcount, thiscount; + entity best, head; + CheckAllowedTeams(world); + GetTeamCounts(world); + head = findchainfloat(isbot, true); + if (!head) + return; + best = head; + besttime = head.createdtime; + bestcount = 0; + while (head) + { + if(head.team == NUM_TEAM_1) + thiscount = c1; + else if(head.team == NUM_TEAM_2) + thiscount = c2; + else if(head.team == NUM_TEAM_3) + thiscount = c3; + else if(head.team == NUM_TEAM_4) + thiscount = c4; + else + thiscount = 0; + if (thiscount > bestcount) + { + bestcount = thiscount; + besttime = head.createdtime; + best = head; + } + else if (thiscount == bestcount && besttime < head.createdtime) + { + besttime = head.createdtime; + best = head; + } + head = head.chain; + } + currentbots = currentbots - 1; + dropclient(best); +} + +void bot_removenewest() +{ + float besttime; + entity best, head; + + if(teamplay) + { + bot_removefromlargestteam(); + return; + } + + head = findchainfloat(isbot, true); + if (!head) + return; + best = head; + besttime = head.createdtime; + while (head) + { + if (besttime < head.createdtime) + { + besttime = head.createdtime; + best = head; + } + head = head.chain; + } + currentbots = currentbots - 1; + dropclient(best); +} + +void autoskill(float factor) +{ + float bestbot; + float bestplayer; + entity head; + + bestbot = -1; + bestplayer = -1; + FOR_EACH_PLAYER(head) + { + if(IS_REAL_CLIENT(head)) + bestplayer = max(bestplayer, head.totalfrags - head.totalfrags_lastcheck); + else + bestbot = max(bestbot, head.totalfrags - head.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 + } + + FOR_EACH_PLAYER(head) + head.totalfrags_lastcheck = head.totalfrags; +} + +void bot_calculate_stepheightvec(void) +{ + 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() +{ + entity head; + float realplayers, bots, activerealplayers; + + activerealplayers = 0; + realplayers = 0; + + FOR_EACH_REALCLIENT(head) + { + if(IS_PLAYER(head) || g_lms || head.caplayer == 1) + ++activerealplayers; + ++realplayers; + } + + // 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 && (c3==-1 && c4==-1)) + 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() == world) + { + 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 + entity wp = findchain(classname, "waypoint"); + if(time - wp.nextthink > 10) + waypoint_save_links(); + } + } + 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..b3c628de1 --- /dev/null +++ b/qcsrc/server/bot/default/bot.qh @@ -0,0 +1,109 @@ +#ifndef BOT_H +#define BOT_H +/* + * Globals and Fields + */ + +const int AI_STATUS_ROAMING = 1; // Bot is just crawling the map. No enemies at sight +const int AI_STATUS_ATTACKING = 2; // There are enemies at sight +const int AI_STATUS_RUNNING = 4; // Bot is bunny hopping +const int AI_STATUS_DANGER_AHEAD = 8; // There is lava/slime/trigger_hurt ahead +const int AI_STATUS_OUT_JUMPPAD = 16; // Trying to get out of a "vertical" jump pad +const int AI_STATUS_OUT_WATER = 32; // Trying to get out of water +const int AI_STATUS_WAYPOINT_PERSONAL_LINKING = 64; // Waiting for the personal waypoint to be linked +const int AI_STATUS_WAYPOINT_PERSONAL_GOING = 128; // Going to a personal waypoint +const int AI_STATUS_WAYPOINT_PERSONAL_REACHED = 256; // Personal waypoint reached +const int AI_STATUS_JETPACK_FLYING = 512; +const int AI_STATUS_JETPACK_LANDING = 1024; +const int AI_STATUS_STUCK = 2048; // 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; + + + +entity bot_list; +entity player_list; +.entity nextbot; +.entity nextplayer; +.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; + +/* + * Functions + */ + +entity bot_spawn(); +float bot_fixcount(); + +void bot_think(); +void bot_setnameandstuff(); +void bot_custom_weapon_priority_setup(); +void bot_endgame(); +void bot_relinkplayerlist(); +void bot_clientdisconnect(); +void bot_clientconnect(); +void bot_removefromlargestteam(); +void bot_removenewest(); +void autoskill(float factor); +void bot_serverframe(); + +.void() bot_ai; +.float(entity player, entity item) bot_pickupevalfunc; + +/* + * Imports + */ + +void() havocbot_setupbot; + +void bot_calculate_stepheightvec(void); +#endif diff --git a/qcsrc/server/bot/default/defs.qh b/qcsrc/server/bot/default/defs.qh new file mode 100644 index 000000000..2d7c49975 --- /dev/null +++ b/qcsrc/server/bot/default/defs.qh @@ -0,0 +1,61 @@ +float bot_distance_close; +float bot_distance_far; +float bot_ignore_bots; // let bots not attack other bots (only works in non-teamplay) + +int autocvar_g_waypointeditor_auto; +float autocvar_skill_auto; +float autocvar_sv_jumpvelocity; +bool autocvar_waypoint_benchmark; +bool autocvar_bot_god; +float autocvar_bot_ai_thinkinterval; +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; +bool autocvar_bot_ai_weapon_combo; +float autocvar_bot_ai_weapon_combo_threshold; +string autocvar_bot_config_file; +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; +int autocvar_bot_debug; +bool autocvar_bot_debug_tracewalk; +bool autocvar_bot_debug_goalstack; +bool autocvar_bot_wander_enable; diff --git a/qcsrc/server/bot/default/havocbot/havocbot.qc b/qcsrc/server/bot/default/havocbot/havocbot.qc new file mode 100644 index 000000000..ae5314a8c --- /dev/null +++ b/qcsrc/server/bot/default/havocbot/havocbot.qc @@ -0,0 +1,1311 @@ +#include "havocbot.qh" + +void havocbot_ai() +{SELFPARAM(); + if(self.draggedby) + return; + + if(bot_execute_commands()) + return; + + if (bot_strategytoken == self) + if (!bot_strategytoken_taken) + { + if(self.havocbot_blockhead) + { + self.havocbot_blockhead = false; + } + else + { + if (!self.jumppadcount) + self.havocbot_role(); + } + + // 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(self.deadflag != DEAD_NO) + if(self.goalcurrent==world) + if(self.waterlevel==WATERLEVEL_SWIMMING || (self.aistatus & AI_STATUS_OUT_WATER)) + { + // Look for the closest waypoint out of water + entity newgoal, head; + float bestdistance, distance; + + newgoal = world; + bestdistance = 10000; + for (head = findchain(classname, "waypoint"); head; head = head.chain) + { + distance = vlen(head.origin - self.origin); + if(distance>10000) + continue; + + if(head.origin.z < self.origin.z) + continue; + + if(head.origin.z - self.origin.z - self.view_ofs.z > 100) + continue; + + if (pointcontents(head.origin + head.maxs + '0 0 1') != CONTENT_EMPTY) + continue; + + traceline(self.origin + self.view_ofs , head.origin, true, head); + + if(trace_fraction<1) + continue; + + if(distance= 2) // bots can only reload the held weapon on purpose past this skill + if(self.clip_load < self.clip_size) + self.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(self.clip_load >= 0) // only if we're not reloading a weapon already + { + for (int i = WEP_FIRST; i <= WEP_LAST; ++i) + { + entity e = get_weaponinfo(i); + if ((self.weapons & WepSet_FromWeapon(i)) && (e.spawnflags & WEP_FLAG_RELOADABLE) && (self.weapon_load[i] < e.reloading_ammo)) + self.switchweapon = i; + } + } + } +} + +void havocbot_keyboard_movement(vector destorg) +{SELFPARAM(); + vector keyboard; + float blend, maxspeed; + float sk; + + sk = skill + self.bot_moveskill; + + maxspeed = autocvar_sv_maxspeed; + + if (time < self.havocbot_keyboardtime) + return; + + self.havocbot_keyboardtime = + max( + self.havocbot_keyboardtime + + 0.05/max(1, sk+self.havocbot_keyboardskill) + + random()*0.025/max(0.00025, skill+self.havocbot_keyboardskill) + , time); + keyboard = self.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; + + self.havocbot_keyboard = keyboard * maxspeed; + if (self.havocbot_ducktime>time) self.BUTTON_CROUCH=true; + + keyboard = self.havocbot_keyboard; + blend = bound(0,vlen(destorg-self.origin)/autocvar_bot_ai_keyboard_distance,1); // When getting close move with 360 degree + //dprint("movement ", vtos(self.movement), " keyboard ", vtos(keyboard), " blend ", ftos(blend), "\n"); + self.movement = self.movement + (keyboard - self.movement) * blend; +} + +void havocbot_bunnyhop(vector dir) +{SELFPARAM(); + float bunnyhopdistance; + vector deviation; + float maxspeed; + vector gco, gno; + + // Don't jump when attacking + if(self.aistatus & AI_STATUS_ATTACKING) + return; + + if(IS_PLAYER(self.goalcurrent)) + return; + + maxspeed = autocvar_sv_maxspeed; + + if(self.aistatus & AI_STATUS_DANGER_AHEAD) + { + self.aistatus &= ~AI_STATUS_RUNNING; + self.BUTTON_JUMP = false; + self.bot_canruntogoal = 0; + self.bot_timelastseengoal = 0; + return; + } + + if(self.waterlevel > WATERLEVEL_WETFEET) + { + self.aistatus &= ~AI_STATUS_RUNNING; + return; + } + + if(self.bot_lastseengoal != self.goalcurrent && !(self.aistatus & AI_STATUS_RUNNING)) + { + self.bot_canruntogoal = 0; + self.bot_timelastseengoal = 0; + } + + gco = (self.goalcurrent.absmin + self.goalcurrent.absmax) * 0.5; + bunnyhopdistance = vlen(self.origin - gco); + + // Run only to visible goals + if(self.flags & FL_ONGROUND) + if(self.speed==maxspeed) + if(checkpvs(self.origin + self.view_ofs, self.goalcurrent)) + { + self.bot_lastseengoal = self.goalcurrent; + + // seen it before + if(self.bot_timelastseengoal) + { + // for a period of time + if(time - self.bot_timelastseengoal > autocvar_bot_ai_bunnyhop_firstjumpdelay) + { + float checkdistance; + checkdistance = true; + + // don't run if it is too close + if(self.bot_canruntogoal==0) + { + if(bunnyhopdistance > autocvar_bot_ai_bunnyhop_startdistance) + self.bot_canruntogoal = 1; + else + self.bot_canruntogoal = -1; + } + + if(self.bot_canruntogoal != 1) + return; + + if(self.aistatus & AI_STATUS_ROAMING) + if(self.goalcurrent.classname=="waypoint") + if (!(self.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL)) + if(fabs(gco.z - self.origin.z) < self.maxs.z - self.mins.z) + if(self.goalstack01!=world) + { + gno = (self.goalstack01.absmin + self.goalstack01.absmax) * 0.5; + deviation = vectoangles(gno - self.origin) - vectoangles(gco - self.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(self.origin - gno)) + if(fabs(gno.z - gco.z) < self.maxs.z - self.mins.z) + { + if(vlen(gco - gno) > autocvar_bot_ai_bunnyhop_startdistance) + if(checkpvs(self.origin + self.view_ofs, self.goalstack01)) + { + checkdistance = false; + } + } + } + + if(checkdistance) + { + self.aistatus &= ~AI_STATUS_RUNNING; + if(bunnyhopdistance > autocvar_bot_ai_bunnyhop_stopdistance) + self.BUTTON_JUMP = true; + } + else + { + self.aistatus |= AI_STATUS_RUNNING; + self.BUTTON_JUMP = true; + } + } + } + else + { + self.bot_timelastseengoal = time; + } + } + else + { + self.bot_timelastseengoal = 0; + } + +#if 0 + // Release jump button + if(!cvar("sv_pogostick")) + if((self.flags & FL_ONGROUND) == 0) + { + if(self.velocity.z < 0 || vlen(self.velocity)maxspeed) + { + deviation = vectoangles(dir) - vectoangles(self.velocity); + while (deviation.y < -180) deviation.y = deviation.y + 360; + while (deviation.y > 180) deviation.y = deviation.y - 360; + + if(fabs(deviation.y)>10) + self.movement_x = 0; + + if(deviation.y>10) + self.movement_y = maxspeed * -1; + else if(deviation.y<10) + self.movement_y = maxspeed; + + } + } +#endif +} + +void havocbot_movetogoal() +{SELFPARAM(); + 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 (self.goalentity) + // te_lightning2(self, self.origin, (self.goalentity.absmin + self.goalentity.absmax) * 0.5); + self.movement = '0 0 0'; + maxspeed = autocvar_sv_maxspeed; + + // Jetpack navigation + if(self.goalcurrent) + if(self.navigation_jetpack_goal) + if(self.goalcurrent==self.navigation_jetpack_goal) + if(self.ammo_fuel) + { + if(autocvar_bot_debug_goalstack) + { + debuggoalstack(); + te_wizspike(self.navigation_jetpack_point); + } + + // Take off + if (!(self.aistatus & AI_STATUS_JETPACK_FLYING)) + { + // Brake almost completely so it can get a good direction + if(vlen(self.velocity)>10) + return; + self.aistatus |= AI_STATUS_JETPACK_FLYING; + } + + makevectors(self.v_angle.y * '0 1 0'); + dir = normalize(self.navigation_jetpack_point - self.origin); + + // Landing + if(self.aistatus & AI_STATUS_JETPACK_LANDING) + { + // Calculate brake distance in xy + float db, v, d; + vector dxy; + + dxy = self.origin - ( ( self.goalcurrent.absmin + self.goalcurrent.absmax ) * 0.5 ); dxy.z = 0; + d = vlen(dxy); + v = vlen(self.velocity - self.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(self.velocity.x)>maxspeed*0.3) + { + self.movement_x = dir * v_forward * -maxspeed; + return; + } + // Switch to normal mode + self.navigation_jetpack_goal = world; + self.aistatus &= ~AI_STATUS_JETPACK_LANDING; + self.aistatus &= ~AI_STATUS_JETPACK_FLYING; + return; + } + } + else if(checkpvs(self.origin,self.goalcurrent)) + { + // If I can see the goal switch to landing code + self.aistatus &= ~AI_STATUS_JETPACK_FLYING; + self.aistatus |= AI_STATUS_JETPACK_LANDING; + return; + } + + // Flying + self.BUTTON_HOOK = true; + if(self.navigation_jetpack_point.z - PL_MAX.z + PL_MIN.z < self.origin.z) + { + self.movement_x = dir * v_forward * maxspeed; + self.movement_y = dir * v_right * maxspeed; + } + return; + } + + // Handling of jump pads + if(self.jumppadcount) + { + // If got stuck on the jump pad try to reach the farthest visible waypoint + if(self.aistatus & AI_STATUS_OUT_JUMPPAD) + { + if(fabs(self.velocity.z)<50) + { + entity head, newgoal = world; + float distance, bestdistance = 0; + + for (head = findchain(classname, "waypoint"); head; head = head.chain) + { + + distance = vlen(head.origin - self.origin); + if(distance>1000) + continue; + + traceline(self.origin + self.view_ofs , ( ( head.absmin + head.absmax ) * 0.5 ), true, world); + + if(trace_fraction<1) + continue; + + if(distance>bestdistance) + { + newgoal = head; + bestdistance = distance; + } + } + + if(newgoal) + { + self.ignoregoal = self.goalcurrent; + self.ignoregoaltime = time + autocvar_bot_ai_ignoregoal_timeout; + navigation_clearroute(); + navigation_routetogoal(newgoal, self.origin); + self.aistatus &= ~AI_STATUS_OUT_JUMPPAD; + } + } + else + return; + } + else + { + if(self.velocity.z>0) + { + float threshold, sxy; + vector velxy = self.velocity; velxy_z = 0; + sxy = vlen(velxy); + threshold = maxspeed * 0.2; + if(sxy < threshold) + { + LOG_TRACE("Warning: ", self.netname, " got stuck on a jumppad (velocity in xy is ", ftos(sxy), "), trying to get out of it now\n"); + self.aistatus |= AI_STATUS_OUT_JUMPPAD; + } + return; + } + + // Don't chase players while using a jump pad + if(IS_PLAYER(self.goalcurrent) || IS_PLAYER(self.goalstack01)) + return; + } + } + else if(self.aistatus & AI_STATUS_OUT_JUMPPAD) + self.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 (!(self.flags & FL_ONGROUND)) + { + tracebox(self.origin, self.mins, self.maxs, self.origin + '0 0 -65536', MOVE_NOMONSTERS, self); + if(tracebox_hits_trigger_hurt(self.origin, self.mins, self.maxs, trace_endpos )) + if(self.items & IT_JETPACK) + { + tracebox(self.origin, self.mins, self.maxs, self.origin + '0 0 65536', MOVE_NOMONSTERS, self); + if(tracebox_hits_trigger_hurt(self.origin, self.mins, self.maxs, trace_endpos + '0 0 1' )) + { + if(self.velocity.z<0) + { + self.BUTTON_HOOK = true; + } + } + else + self.BUTTON_HOOK = true; + + // If there is no goal try to move forward + + if(self.goalcurrent==world) + dir = v_forward; + else + dir = normalize(( ( self.goalcurrent.absmin + self.goalcurrent.absmax ) * 0.5 ) - self.origin); + + vector xyvelocity = self.velocity; xyvelocity_z = 0; + float xyspeed = xyvelocity * dir; + + if(xyspeed < (maxspeed / 2)) + { + makevectors(self.v_angle.y * '0 1 0'); + tracebox(self.origin, self.mins, self.maxs, self.origin + (dir * maxspeed * 3), MOVE_NOMONSTERS, self); + if(trace_fraction==1) + { + self.movement_x = dir * v_forward * maxspeed; + self.movement_y = dir * v_right * maxspeed; + if (skill < 10) + havocbot_keyboard_movement(self.origin + dir * 100); + } + } + + self.havocbot_blockhead = true; + + return; + } + else if(self.health>WEP_CVAR(devastator, damage)*0.5) + { + if(self.velocity.z < 0) + if(client_hasweapon(self, WEP_DEVASTATOR.m_id, true, false)) + { + self.movement_x = maxspeed; + + if(self.rocketjumptime) + { + if(time > self.rocketjumptime) + { + self.BUTTON_ATCK2 = true; + self.rocketjumptime = 0; + } + return; + } + + self.switchweapon = WEP_DEVASTATOR.m_id; + self.v_angle_x = 90; + self.BUTTON_ATCK = true; + self.rocketjumptime = time + WEP_CVAR(devastator, detonatedelay); + return; + } + } + else + { + // If there is no goal try to move forward + if(self.goalcurrent==world) + self.movement_x = maxspeed; + } + } + + // If we are under water with no goals, swim up + if(self.waterlevel) + if(self.goalcurrent==world) + { + dir = '0 0 0'; + if(self.waterlevel>WATERLEVEL_SWIMMING) + dir.z = 1; + else if(self.velocity.z >= 0 && !(self.waterlevel == WATERLEVEL_WETFEET && self.watertype == CONTENT_WATER)) + self.BUTTON_JUMP = true; + else + self.BUTTON_JUMP = false; + makevectors(self.v_angle.y * '0 1 0'); + self.movement_x = dir * v_forward * maxspeed; + self.movement_y = dir * v_right * maxspeed; + self.movement_z = dir * v_up * maxspeed; + } + + // if there is nowhere to go, exit + if (self.goalcurrent == world) + return; + + if (self.goalcurrent) + navigation_poptouchedgoals(); + + // if ran out of goals try to use an alternative goal or get a new strategy asap + if(self.goalcurrent == world) + { + self.bot_strategytime = 0; + return; + } + + + if(autocvar_bot_debug_goalstack) + debuggoalstack(); + + m1 = self.goalcurrent.origin + self.goalcurrent.mins; + m2 = self.goalcurrent.origin + self.goalcurrent.maxs; + destorg = self.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 - self.origin; + //dist = vlen(diff); + dir = normalize(diff); + flatdir = diff;flatdir.z = 0; + flatdir = normalize(flatdir); + gco = (self.goalcurrent.absmin + self.goalcurrent.absmax) * 0.5; + + //if (self.bot_dodgevector_time < time) + { + // self.bot_dodgevector_time = time + cvar("bot_ai_dodgeupdateinterval"); + // self.bot_dodgevector_jumpbutton = 1; + evadeobstacle = '0 0 0'; + evadelava = '0 0 0'; + + if (self.waterlevel) + { + if(self.waterlevel>WATERLEVEL_SWIMMING) + { + // flatdir_z = 1; + self.aistatus |= AI_STATUS_OUT_WATER; + } + else + { + if(self.velocity.z >= 0 && !(self.watertype == CONTENT_WATER && gco.z < self.origin.z) && + ( !(self.waterlevel == WATERLEVEL_WETFEET && self.watertype == CONTENT_WATER) || self.aistatus & AI_STATUS_OUT_WATER)) + self.BUTTON_JUMP = true; + else + self.BUTTON_JUMP = false; + } + dir = normalize(flatdir); + makevectors(self.v_angle.y * '0 1 0'); + } + else + { + if(self.aistatus & AI_STATUS_OUT_WATER) + self.aistatus &= ~AI_STATUS_OUT_WATER; + + // jump if going toward an obstacle that doesn't look like stairs we + // can walk up directly + tracebox(self.origin, self.mins, self.maxs, self.origin + self.velocity * 0.2, false, self); + if (trace_fraction < 1) + if (trace_plane_normal.z < 0.7) + { + s = trace_fraction; + tracebox(self.origin + stepheightvec, self.mins, self.maxs, self.origin + self.velocity * 0.2 + stepheightvec, false, self); + if (trace_fraction < s + 0.01) + if (trace_plane_normal.z < 0.7) + { + s = trace_fraction; + tracebox(self.origin + jumpstepheightvec, self.mins, self.maxs, self.origin + self.velocity * 0.2 + jumpstepheightvec, false, self); + if (trace_fraction > s) + self.BUTTON_JUMP = 1; + } + } + + // avoiding dangers and obstacles + vector dst_ahead, dst_down; + makevectors(self.v_angle.y * '0 1 0'); + dst_ahead = self.origin + self.view_ofs + (self.velocity * 0.4) + (v_forward * 32 * 3); + dst_down = dst_ahead - '0 0 1500'; + + // Look ahead + traceline(self.origin + self.view_ofs, dst_ahead, true, world); + + // Check head-banging against walls + if(vlen(self.origin + self.view_ofs - trace_endpos) < 25 && !(self.aistatus & AI_STATUS_OUT_WATER)) + { + self.BUTTON_JUMP = true; + if(self.facingwalltime && time > self.facingwalltime) + { + self.ignoregoal = self.goalcurrent; + self.ignoregoaltime = time + autocvar_bot_ai_ignoregoal_timeout; + self.bot_strategytime = 0; + return; + } + else + { + self.facingwalltime = time + 0.05; + } + } + else + { + self.facingwalltime = 0; + + if(self.ignoregoal != world && time > self.ignoregoaltime) + { + self.ignoregoal = world; + self.ignoregoaltime = 0; + } + } + + // Check for water/slime/lava and dangerous edges + // (only when the bot is on the ground or jumping intentionally) + self.aistatus &= ~AI_STATUS_DANGER_AHEAD; + + if(trace_fraction == 1 && self.jumppadcount == 0 && !self.goalcurrent.wphardwired ) + if((self.flags & FL_ONGROUND) || (self.aistatus & AI_STATUS_RUNNING) || self.BUTTON_JUMP == true) + { + // Look downwards + traceline(dst_ahead , dst_down, true, world); + // te_lightning2(world, self.origin, dst_ahead); // Draw "ahead" look + // te_lightning2(world, dst_ahead, dst_down); // Draw "downwards" look + if(trace_endpos.z < self.origin.z + self.mins.z) + { + s = pointcontents(trace_endpos + '0 0 1'); + if (s != CONTENT_SOLID) + if (s == CONTENT_LAVA || s == CONTENT_SLIME) + evadelava = normalize(self.velocity) * -1; + else if (s == CONTENT_SKY) + evadeobstacle = normalize(self.velocity) * -1; + else if (!boxesoverlap(dst_ahead - self.view_ofs + self.mins, dst_ahead - self.view_ofs + self.maxs, + self.goalcurrent.absmin, self.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, self.mins, self.maxs, trace_endpos)) + { + // Remove dangerous dynamic goals from stack + LOG_TRACE("bot ", self.netname, " avoided the goal ", self.goalcurrent.classname, " ", etos(self.goalcurrent), " because it led to a dangerous path; goal stack cleared\n"); + navigation_clearroute(); + return; + } + } + } + } + + dir = flatdir; + evadeobstacle.z = 0; + evadelava.z = 0; + makevectors(self.v_angle.y * '0 1 0'); + + if(evadeobstacle!='0 0 0'||evadelava!='0 0 0') + self.aistatus |= AI_STATUS_DANGER_AHEAD; + } + + dodge = havocbot_dodge(); + dodge = dodge * bound(0,0.5+(skill+self.bot_dodgeskill)*0.1,1); + evadelava = evadelava * bound(1,3-(skill+self.bot_dodgeskill),3); //Noobs fear lava a lot and take more distance from it + traceline(self.origin, ( ( self.enemy.absmin + self.enemy.absmax ) * 0.5 ), true, world); + if(IS_PLAYER(trace_ent)) + dir = dir * bound(0,(skill+self.bot_dodgeskill)/7,1); + + dir = normalize(dir + dodge + evadeobstacle + evadelava); + // self.bot_dodgevector = dir; + // self.bot_dodgevector_jumpbutton = self.BUTTON_JUMP; + } + + if(time < self.ladder_time) + { + if(self.goalcurrent.origin.z + self.goalcurrent.mins.z > self.origin.z + self.mins.z) + { + if(self.origin.z + self.mins.z < self.ladder_entity.origin.z + self.ladder_entity.maxs.z) + dir.z = 1; + } + else + { + if(self.origin.z + self.mins.z > self.ladder_entity.origin.z + self.ladder_entity.mins.z) + dir.z = -1; + } + } + + //dir = self.bot_dodgevector; + //if (self.bot_dodgevector_jumpbutton) + // self.BUTTON_JUMP = 1; + self.movement_x = dir * v_forward * maxspeed; + self.movement_y = dir * v_right * maxspeed; + self.movement_z = dir * v_up * maxspeed; + + // Emulate keyboard interface + if (skill < 10) + havocbot_keyboard_movement(destorg); + + // Bunnyhop! +// if(self.aistatus & AI_STATUS_ROAMING) + if(self.goalcurrent) + if(skill+self.bot_moveskill >= autocvar_bot_ai_bunnyhop_skilloffset) + havocbot_bunnyhop(dir); + + if ((dir * v_up) >= autocvar_sv_jumpvelocity*0.5 && (self.flags & FL_ONGROUND)) self.BUTTON_JUMP=1; + if (((dodge * v_up) > 0) && random()*frametime >= 0.2*bound(0,(10-skill-self.bot_dodgeskill)*0.1,1)) self.BUTTON_JUMP=true; + if (((dodge * v_up) < 0) && random()*frametime >= 0.5*bound(0,(10-skill-self.bot_dodgeskill)*0.1,1)) self.havocbot_ducktime=time+0.3/bound(0.1,skill+self.bot_dodgeskill,10); +} + +void havocbot_chooseenemy() +{SELFPARAM(); + entity head, best, head2; + float rating, bestrating, hf; + vector eye, v; + if (autocvar_bot_nofire || IS_INDEPENDENT_PLAYER(self)) + { + self.enemy = world; + return; + } + if (self.enemy) + { + if (!bot_shouldattack(self.enemy)) + { + // enemy died or something, find a new target + self.enemy = world; + self.havocbot_chooseenemy_finished = time; + } + else if (self.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(self.origin+self.view_ofs, ( self.enemy.absmin + self.enemy.absmax ) * 0.5,false,world); + if (trace_ent == self.enemy || trace_fraction == 1) + if (vlen((( self.enemy.absmin + self.enemy.absmax ) * 0.5) - self.origin) < 1000) + if (self.health > 30) + { + // remain tracking him for a shot while (case he went after a small corner or pilar + self.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) + self.havocbot_stickenemy = 0; + } + } + if (time < self.havocbot_chooseenemy_finished) + return; + self.havocbot_chooseenemy_finished = time + autocvar_bot_ai_enemydetectioninterval; + eye = self.origin + self.view_ofs; + best = world; + bestrating = 100000000; + head = head2 = findchainfloat(bot_attack, true); + + // Backup hit flags + hf = self.dphitcontentsmask; + + // Search for enemies, if no enemy can be seen directly try to look through transparent objects + + self.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; + :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(head)) + { + traceline(eye, v, true, self); + 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 || self.weapons) // || self.weapon == WEP_RIFLE.m_id + break; + if(scan_transparent) + break; + + // Set flags to see through transparent objects + self.dphitcontentsmask |= DPCONTENTS_OPAQUE; + + head = head2; + scan_transparent = true; + } + + // Restore hit flags + self.dphitcontentsmask = hf; + + self.enemy = best; + self.havocbot_stickenemy = true; + if(best && best.classname == "misc_breakablemodel") + self.havocbot_stickenemy = false; +} + +float havocbot_chooseweapon_checkreload(int new_weapon) +{SELFPARAM(); + // 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 (self.weapon_load[new_weapon] < 0) + { + float i, other_weapon_available = false; + for(i = WEP_FIRST; i <= WEP_LAST; ++i) + { + Weapon w = get_weaponinfo(i); + // if we are out of ammo for all other weapons, it's an emergency to switch to anything else + if (w.wr_checkammo1(w) + w.wr_checkammo2(w)) + other_weapon_available = true; + } + if(other_weapon_available) + return true; + } + + return false; +} + +void havocbot_chooseweapon() +{SELFPARAM(); + int i; + + // ;) + if(g_weaponarena_weapons == WEPSET(TUBA)) + { + self.switchweapon = WEP_TUBA.m_id; + return; + } + + // TODO: clean this up by moving it to weapon code + if(self.enemy==world) + { + // If no weapon was chosen get the first available weapon + if(self.weapon==0) + for(i = WEP_FIRST; i <= WEP_LAST; ++i) if(i != WEP_BLASTER.m_id) + { + if(client_hasweapon(self, i, true, false)) + { + self.switchweapon = i; + return; + } + } + return; + } + + // Do not change weapon during the next second after a combo + float f = time - self.lastcombotime; + if(f < 1) + return; + + float w; + float distance; distance=bound(10,vlen(self.origin-self.enemy.origin)-200,10000); + + // Should it do a weapon combo? + float af, ct, combo_time, combo; + + af = ATTACK_FINISHED(self); + 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+self.bot_weaponskill))+3)); + + combo = false; + + if(autocvar_bot_ai_weapon_combo) + if(self.weapon == self.lastfiredweapon) + if(af > combo_time) + { + combo = true; + self.lastcombotime = time; + } + + distance *= pow(2, self.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(self, w, true, false) ) + { + if ((self.weapon == w && combo) || havocbot_chooseweapon_checkreload(w)) + continue; + self.switchweapon = 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(self, w, true, false) ) + { + if ((self.weapon == w && combo) || havocbot_chooseweapon_checkreload(w)) + continue; + self.switchweapon = 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(self, w, true, false) ) + { + if ((self.weapon == w && combo) || havocbot_chooseweapon_checkreload(w)) + continue; + self.switchweapon = w; + return; + } + } + } +} + +void havocbot_aim() +{SELFPARAM(); + vector selfvel, enemyvel; +// if(self.flags & FL_INWATER) +// return; + if (time < self.nextaim) + return; + self.nextaim = time + 0.1; + selfvel = self.velocity; + if (!self.waterlevel) + selfvel.z = 0; + if (self.enemy) + { + enemyvel = self.enemy.velocity; + if (!self.enemy.waterlevel) + enemyvel.z = 0; + lag_additem(time + self.ping, 0, 0, self.enemy, self.origin, selfvel, (self.enemy.absmin + self.enemy.absmax) * 0.5, enemyvel); + } + else + lag_additem(time + self.ping, 0, 0, world, self.origin, selfvel, ( self.goalcurrent.absmin + self.goalcurrent.absmax ) * 0.5, '0 0 0'); +} + +float havocbot_moveto_refresh_route() +{SELFPARAM(); + // Refresh path to goal if necessary + entity wp; + wp = self.havocbot_personal_waypoint; + navigation_goalrating_start(); + navigation_routerating(wp, 10000, 10000); + navigation_goalrating_end(); + return self.navigation_hasgoals; +} + +float havocbot_moveto(vector pos) +{SELFPARAM(); + entity wp; + + if(self.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) + { + // Step 4: Move to waypoint + if(self.havocbot_personal_waypoint==world) + { + LOG_TRACE("Error: ", self.netname, " trying to walk to a non existent personal waypoint\n"); + self.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING; + return CMD_STATUS_ERROR; + } + + if (!bot_strategytoken_taken) + if(self.havocbot_personal_waypoint_searchtime= 30) + { + LOG_TRACE("Warning: can't walk to the personal waypoint located at ", vtos(self.havocbot_personal_waypoint.origin),"\n"); + self.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_LINKING; + remove(self.havocbot_personal_waypoint); + return CMD_STATUS_ERROR; + } + else + LOG_TRACE(self.netname, " can't walk to its personal waypoint (after ", ftos(self.havocbot_personal_waypoint_failcounter), " failed attempts), trying later\n"); + } + } + + if(autocvar_bot_debug_goalstack) + debuggoalstack(); + + // Heading + vector dir = ( ( self.goalcurrent.absmin + self.goalcurrent.absmax ) * 0.5 ) - (self.origin + self.view_ofs); + dir.z = 0; + bot_aimdir(dir, -1); + + // Go! + havocbot_movetogoal(); + + if(self.aistatus & AI_STATUS_WAYPOINT_PERSONAL_REACHED) + { + // Step 5: Waypoint reached + LOG_TRACE(self.netname, "'s personal waypoint reached\n"); + remove(self.havocbot_personal_waypoint); + self.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_REACHED; + return CMD_STATUS_FINISHED; + } + + return CMD_STATUS_EXECUTING; + } + + // Step 2: Linking waypoint + if(self.aistatus & AI_STATUS_WAYPOINT_PERSONAL_LINKING) + { + // Wait until it is linked + if(!self.havocbot_personal_waypoint.wplinked) + { + LOG_TRACE(self.netname, " waiting for personal waypoint to be linked\n"); + return CMD_STATUS_EXECUTING; + } + + self.havocbot_personal_waypoint_searchtime = time; // so we set the route next frame + self.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_LINKING; + self.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_GOING; + + // Step 3: Route to waypoint + LOG_TRACE(self.netname, " walking to its personal waypoint\n"); + + return CMD_STATUS_EXECUTING; + } + + // Step 1: Spawning waypoint + wp = waypoint_spawnpersonal(pos); + if(wp==world) + { + LOG_TRACE("Error: Can't spawn personal waypoint at ",vtos(pos),"\n"); + return CMD_STATUS_ERROR; + } + + self.havocbot_personal_waypoint = wp; + self.havocbot_personal_waypoint_failcounter = 0; + self.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_LINKING; + + // if pos is inside a teleport, then let's mark it as teleport waypoint + entity head; + for(head = world; (head = find(head, classname, "trigger_teleport")); ) + { + if(WarpZoneLib_BoxTouchesBrush(pos, pos, head, world)) + { + wp.wpflags |= WAYPOINTFLAG_TELEPORT; + self.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() +{ + navigation_clearroute(); + return CMD_STATUS_FINISHED; +} + +void havocbot_setupbot() +{SELFPARAM(); + self.bot_ai = havocbot_ai; + self.cmd_moveto = havocbot_moveto; + self.cmd_resetgoal = havocbot_resetgoal; + + havocbot_chooserole(); +} + +vector havocbot_dodge() +{SELFPARAM(); + // 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 != self) + { + vl = vlen(head.velocity); + if (vl > autocvar_sv_maxspeed * 0.3) + { + n = normalize(head.velocity); + v = self.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 - self.origin); + if (bestdanger < danger) + { + bestdanger = danger; + dodge = normalize(self.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..2d3d32913 --- /dev/null +++ b/qcsrc/server/bot/default/havocbot/havocbot.qh @@ -0,0 +1,66 @@ +#ifndef HAVOCBOT_H +#define HAVOCBOT_H + +/* + * 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(); +void havocbot_aim(); +void havocbot_setupbot(); +void havocbot_movetogoal(); +void havocbot_chooserole(); +void havocbot_chooseenemy(); +void havocbot_chooseweapon(); +void havocbot_bunnyhop(vector dir); +void havocbot_keyboard_movement(vector destorg); + +float havocbot_resetgoal(); +float havocbot_moveto(vector pos); +float havocbot_moveto_refresh_route(); + +vector havocbot_dodge(); + +.void() havocbot_role; +.void() havocbot_previous_role; + +void(float ratingscale, vector org, float sradius) havocbot_goalrating_items; +void(float ratingscale, vector org, float sradius) havocbot_goalrating_enemyplayers; + +/* + * Imports + */ + +.entity draggedby; +.float ladder_time; +.entity ladder_entity; +#endif diff --git a/qcsrc/server/bot/default/havocbot/role_keyhunt.qc b/qcsrc/server/bot/default/havocbot/role_keyhunt.qc new file mode 100644 index 000000000..a26068581 --- /dev/null +++ b/qcsrc/server/bot/default/havocbot/role_keyhunt.qc @@ -0,0 +1,212 @@ +#include "role_keyhunt.qh" + +void() havocbot_role_kh_carrier; +void() havocbot_role_kh_defense; +void() havocbot_role_kh_offense; +void() havocbot_role_kh_freelancer; + + +void havocbot_goalrating_kh(float ratingscale_team, float ratingscale_dropped, float ratingscale_enemy) +{SELFPARAM(); + entity head; + for (head = kh_worldkeylist; head; head = head.kh_worldkeynext) + { + if(head.owner == self) + continue; + if(!kh_tracking_enabled) + { + // if it's carried by our team we know about it + // otherwise we have to see it to know about it + if(!head.owner || head.team != self.team) + { + traceline(self.origin + self.view_ofs, head.origin, MOVE_NOMONSTERS, self); + if (trace_fraction < 1 && trace_ent != head) + continue; // skip what I can't see + } + } + if(!head.owner) + navigation_routerating(head, ratingscale_dropped * BOT_PICKUP_RATING_HIGH, 100000); + else if(head.team == self.team) + navigation_routerating(head.owner, ratingscale_team * BOT_PICKUP_RATING_HIGH, 100000); + else + navigation_routerating(head.owner, ratingscale_enemy * BOT_PICKUP_RATING_HIGH, 100000); + } + + havocbot_goalrating_items(1, self.origin, 10000); +} + +void havocbot_role_kh_carrier() +{SELFPARAM(); + if(self.deadflag != DEAD_NO) + return; + + if (!(self.kh_next)) + { + LOG_TRACE("changing role to freelancer\n"); + self.havocbot_role = havocbot_role_kh_freelancer; + self.havocbot_role_timeout = 0; + return; + } + + if (self.bot_strategytime < time) + { + self.bot_strategytime = time + autocvar_bot_ai_strategyinterval; + navigation_goalrating_start(); + + if(kh_Key_AllOwnedByWhichTeam() == self.team) + havocbot_goalrating_kh(10, 0.1, 0.1); // bring home + else + havocbot_goalrating_kh(4, 4, 1); // play defensively + + navigation_goalrating_end(); + } +} + +void havocbot_role_kh_defense() +{SELFPARAM(); + if(self.deadflag != DEAD_NO) + return; + + if (self.kh_next) + { + LOG_TRACE("changing role to carrier\n"); + self.havocbot_role = havocbot_role_kh_carrier; + self.havocbot_role_timeout = 0; + return; + } + + if (!self.havocbot_role_timeout) + self.havocbot_role_timeout = time + random() * 10 + 20; + if (time > self.havocbot_role_timeout) + { + LOG_TRACE("changing role to freelancer\n"); + self.havocbot_role = havocbot_role_kh_freelancer; + self.havocbot_role_timeout = 0; + return; + } + + if (self.bot_strategytime < time) + { + float key_owner_team; + self.bot_strategytime = time + autocvar_bot_ai_strategyinterval; + navigation_goalrating_start(); + + key_owner_team = kh_Key_AllOwnedByWhichTeam(); + if(key_owner_team == self.team) + havocbot_goalrating_kh(10, 0.1, 0.1); // defend key carriers + else if(key_owner_team == -1) + havocbot_goalrating_kh(4, 1, 0.1); // play defensively + else + havocbot_goalrating_kh(0.1, 0.1, 10); // ATTACK ANYWAY + + navigation_goalrating_end(); + } +} + +void havocbot_role_kh_offense() +{SELFPARAM(); + if(self.deadflag != DEAD_NO) + return; + + if (self.kh_next) + { + LOG_TRACE("changing role to carrier\n"); + self.havocbot_role = havocbot_role_kh_carrier; + self.havocbot_role_timeout = 0; + return; + } + + if (!self.havocbot_role_timeout) + self.havocbot_role_timeout = time + random() * 10 + 20; + if (time > self.havocbot_role_timeout) + { + LOG_TRACE("changing role to freelancer\n"); + self.havocbot_role = havocbot_role_kh_freelancer; + self.havocbot_role_timeout = 0; + return; + } + + if (self.bot_strategytime < time) + { + float key_owner_team; + + self.bot_strategytime = time + autocvar_bot_ai_strategyinterval; + navigation_goalrating_start(); + + key_owner_team = kh_Key_AllOwnedByWhichTeam(); + if(key_owner_team == self.team) + havocbot_goalrating_kh(10, 0.1, 0.1); // defend anyway + else if(key_owner_team == -1) + havocbot_goalrating_kh(0.1, 1, 4); // play offensively + else + havocbot_goalrating_kh(0.1, 0.1, 10); // ATTACK! EMERGENCY! + + navigation_goalrating_end(); + } +} + +void havocbot_role_kh_freelancer() +{SELFPARAM(); + if(self.deadflag != DEAD_NO) + return; + + if (self.kh_next) + { + LOG_TRACE("changing role to carrier\n"); + self.havocbot_role = havocbot_role_kh_carrier; + self.havocbot_role_timeout = 0; + return; + } + + if (!self.havocbot_role_timeout) + self.havocbot_role_timeout = time + random() * 10 + 10; + if (time > self.havocbot_role_timeout) + { + if (random() < 0.5) + { + LOG_TRACE("changing role to offense\n"); + self.havocbot_role = havocbot_role_kh_offense; + } + else + { + LOG_TRACE("changing role to defense\n"); + self.havocbot_role = havocbot_role_kh_defense; + } + self.havocbot_role_timeout = 0; + return; + } + + if (self.bot_strategytime < time) + { + float key_owner_team; + + self.bot_strategytime = time + autocvar_bot_ai_strategyinterval; + navigation_goalrating_start(); + + key_owner_team = kh_Key_AllOwnedByWhichTeam(); + if(key_owner_team == self.team) + havocbot_goalrating_kh(10, 0.1, 0.1); // defend anyway + else if(key_owner_team == -1) + havocbot_goalrating_kh(1, 10, 4); // prefer dropped keys + else + havocbot_goalrating_kh(0.1, 0.1, 10); // ATTACK ANYWAY + + navigation_goalrating_end(); + } +} + +void havocbot_chooserole_kh() +{SELFPARAM(); + float r; + + if(self.deadflag != DEAD_NO) + return; + + r = random() * 3; + if (r < 1) + self.havocbot_role = havocbot_role_kh_offense; + else if (r < 2) + self.havocbot_role = havocbot_role_kh_defense; + else + self.havocbot_role = havocbot_role_kh_freelancer; +} diff --git a/qcsrc/server/bot/default/havocbot/role_keyhunt.qh b/qcsrc/server/bot/default/havocbot/role_keyhunt.qh new file mode 100644 index 000000000..ac3c77e52 --- /dev/null +++ b/qcsrc/server/bot/default/havocbot/role_keyhunt.qh @@ -0,0 +1,5 @@ +#ifndef ROLE_KEYHUNT_H +#define ROLE_KEYHUNT_H + +void havocbot_chooserole_kh(); +#endif diff --git a/qcsrc/server/bot/default/havocbot/roles.qc b/qcsrc/server/bot/default/havocbot/roles.qc new file mode 100644 index 000000000..c520ab3cd --- /dev/null +++ b/qcsrc/server/bot/default/havocbot/roles.qc @@ -0,0 +1,241 @@ +#include "roles.qh" + +.float max_armorvalue; +.float havocbot_role_timeout; + +.void() havocbot_previous_role; +.void() havocbot_role; + +void havocbot_goalrating_items(float ratingscale, vector org, float sradius) +{SELFPARAM(); + entity head; + entity player; + float rating, d, discard, distance, friend_distance, enemy_distance; + vector o; + ratingscale = ratingscale * 0.0001; // items are rated around 10000 already + head = findchainfloat(bot_pickup, true); + + while (head) + { + o = (head.absmin + head.absmax) * 0.5; + distance = vlen(o - org); + friend_distance = 10000; enemy_distance = 10000; + rating = 0; + + if(!head.solid || distance > sradius || (head == self.ignoregoal && time < self.ignoregoaltime) ) + { + head = head.chain; + continue; + } + + // Check if the item can be picked up safely + if(head.classname == "droppedweapon") + { + traceline(o, o + '0 0 -1500', true, world); + + d = pointcontents(trace_endpos + '0 0 1'); + if(d & CONTENT_WATER || d & CONTENT_SLIME || d & CONTENT_LAVA) + { + head = head.chain; + continue; + } + if(tracebox_hits_trigger_hurt(head.origin, head.mins, head.maxs, trace_endpos)) + { + head = head.chain; + continue; + } + } + else + { + // Ignore items under water + traceline(head.origin + head.maxs, head.origin + head.maxs, MOVE_NORMAL, head); + if(trace_dpstartcontents & DPCONTENTS_LIQUIDSMASK) + { + head = head.chain; + continue; + } + } + + if(teamplay) + { + discard = false; + + FOR_EACH_PLAYER(player) + { + + if ( self == player || player.deadflag ) + continue; + + d = vlen(player.origin - o); // distance between player and item + + if ( player.team == self.team ) + { + if ( !IS_REAL_CLIENT(player) || discard ) + continue; + + if( d > friend_distance) + continue; + + friend_distance = d; + + discard = true; + + if( head.health && player.health > self.health ) + continue; + + if( head.armorvalue && player.armorvalue > self.armorvalue) + continue; + + if( head.weapons ) + if( head.weapons & ~player.weapons ) + continue; + + if (head.ammo_shells && player.ammo_shells > self.ammo_shells) + continue; + + if (head.ammo_nails && player.ammo_nails > self.ammo_nails) + continue; + + if (head.ammo_rockets && player.ammo_rockets > self.ammo_rockets) + continue; + + if (head.ammo_cells && player.ammo_cells > self.ammo_cells) + continue; + + if (head.ammo_plasma && player.ammo_plasma > self.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 && distance < enemy_distance) || + (friend_distance > autocvar_bot_ai_friends_aware_pickup_radius ) || !discard ) + rating = head.bot_pickupevalfunc(self, head); + + } + else + rating = head.bot_pickupevalfunc(self, head); + + if(rating > 0) + navigation_routerating(head, rating * ratingscale, 2000); + head = head.chain; + } +} + +void havocbot_goalrating_controlpoints(float ratingscale, vector org, float sradius) +{SELFPARAM(); + entity head; + head = findchain(classname, "dom_controlpoint"); + while (head) + { + if (vlen(( ( head.absmin + head.absmax ) * 0.5 ) - org) < sradius) + { + if(head.cnt > -1) // this is just being fought for + navigation_routerating(head, ratingscale, 5000); + else if(head.goalentity.cnt == 0) // unclaimed point + navigation_routerating(head, ratingscale * 0.5, 5000); + else if(head.goalentity.team != self.team) // other team's point + navigation_routerating(head, ratingscale * 0.2, 5000); + } + head = head.chain; + } +} + +void havocbot_goalrating_enemyplayers(float ratingscale, vector org, float sradius) +{SELFPARAM(); + entity head; + int t; + float distance; + noref bool noteam = ((self.team == 0) || !teamplay); + + if (autocvar_bot_nofire) + return; + + // don't chase players if we're under water + if(self.waterlevel>WATERLEVEL_WETFEET) + return; + + FOR_EACH_PLAYER(head) + { + // TODO: Merge this logic with the bot_shouldattack function + if(bot_shouldattack(head)) + { + distance = vlen(head.origin - org); + if (distance < 100 || distance > sradius) + continue; + + // rate only visible enemies + /* + traceline(self.origin + self.view_ofs, head.origin, MOVE_NOMONSTERS, self); + if (trace_fraction < 1 || trace_ent != head) + continue; + */ + + if((head.flags & FL_INWATER) || (head.flags & FL_PARTIALGROUND)) + continue; + + // not falling + if((head.flags & FL_ONGROUND) == 0) + { + traceline(head.origin, head.origin + '0 0 -1500', true, world); + 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(head.origin, head.mins, head.maxs, trace_endpos)) + continue; + } + + // TODO: rate waypoints near the targetted player at that moment, instead of the player itself + // 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 = (self.health + self.armorvalue ) / (head.health + head.armorvalue ); + navigation_routerating(head, t * ratingscale, 2000); + } + } +} + +// legacy bot role for standard gamemodes +// go to best items +void havocbot_role_generic() +{SELFPARAM(); + if(self.deadflag != DEAD_NO) + return; + + if (self.bot_strategytime < time) + { + self.bot_strategytime = time + autocvar_bot_ai_strategyinterval; + navigation_goalrating_start(); + havocbot_goalrating_items(10000, self.origin, 10000); + havocbot_goalrating_enemyplayers(20000, self.origin, 10000); + //havocbot_goalrating_waypoints(1, self.origin, 1000); + navigation_goalrating_end(); + } +} + +void havocbot_chooserole_generic() +{SELFPARAM(); + self.havocbot_role = havocbot_role_generic; +} + +void havocbot_chooserole() +{SELFPARAM(); + LOG_TRACE("choosing a role...\n"); + self.bot_strategytime = 0; + if (MUTATOR_CALLHOOK(HavocBot_ChooseRole, self)) + return; + else if (g_keyhunt) + havocbot_chooserole_kh(); + else + havocbot_chooserole_generic(); +} diff --git a/qcsrc/server/bot/default/havocbot/roles.qh b/qcsrc/server/bot/default/havocbot/roles.qh new file mode 100644 index 000000000..cfabf0534 --- /dev/null +++ b/qcsrc/server/bot/default/havocbot/roles.qh @@ -0,0 +1,4 @@ +#ifndef ROLES_H +#define ROLES_H +void havocbot_goalrating_controlpoints(float ratingscale, vector org, float sradius); +#endif diff --git a/qcsrc/server/bot/default/navigation.qc b/qcsrc/server/bot/default/navigation.qc new file mode 100644 index 000000000..fb196fcc3 --- /dev/null +++ b/qcsrc/server/bot/default/navigation.qc @@ -0,0 +1,1259 @@ +#include "navigation.qh" + +#include "../common/triggers/trigger/jumppads.qh" + +void bot_debug(string input) +{ + switch(autocvar_bot_debug) + { + case 1: LOG_TRACE(input); break; + case 2: LOG_INFO(input); break; + } +} + +// rough simulation of walking from one point to another to test if a path +// can be traveled, used for waypoint linking and havocbot + +float tracewalk(entity e, vector start, vector m1, vector m2, vector end, float movemode) +{SELFPARAM(); + 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(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(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(trace_endpos); + + if (trace_fraction < 1) + { + swimming = true; + org = trace_endpos - normalize(org - trace_endpos) * stepdist; + for (; org.z < end.z + self.maxs.z; org.z += stepdist) + { + if(autocvar_bot_debug_tracewalk) + debugnode(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(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() +{SELFPARAM(); + //print("bot ", etos(self), " clear\n"); + self.navigation_hasgoals = false; + self.goalcurrent = world; + self.goalstack01 = world; + self.goalstack02 = world; + self.goalstack03 = world; + self.goalstack04 = world; + self.goalstack05 = world; + self.goalstack06 = world; + self.goalstack07 = world; + self.goalstack08 = world; + self.goalstack09 = world; + self.goalstack10 = world; + self.goalstack11 = world; + self.goalstack12 = world; + self.goalstack13 = world; + self.goalstack14 = world; + self.goalstack15 = world; + self.goalstack16 = world; + self.goalstack17 = world; + self.goalstack18 = world; + self.goalstack19 = world; + self.goalstack20 = world; + self.goalstack21 = world; + self.goalstack22 = world; + self.goalstack23 = world; + self.goalstack24 = world; + self.goalstack25 = world; + self.goalstack26 = world; + self.goalstack27 = world; + self.goalstack28 = world; + self.goalstack29 = world; + self.goalstack30 = world; + self.goalstack31 = world; +} + +// 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 e) +{SELFPARAM(); + //print("bot ", etos(self), " push ", etos(e), "\n"); + self.goalstack31 = self.goalstack30; + self.goalstack30 = self.goalstack29; + self.goalstack29 = self.goalstack28; + self.goalstack28 = self.goalstack27; + self.goalstack27 = self.goalstack26; + self.goalstack26 = self.goalstack25; + self.goalstack25 = self.goalstack24; + self.goalstack24 = self.goalstack23; + self.goalstack23 = self.goalstack22; + self.goalstack22 = self.goalstack21; + self.goalstack21 = self.goalstack20; + self.goalstack20 = self.goalstack19; + self.goalstack19 = self.goalstack18; + self.goalstack18 = self.goalstack17; + self.goalstack17 = self.goalstack16; + self.goalstack16 = self.goalstack15; + self.goalstack15 = self.goalstack14; + self.goalstack14 = self.goalstack13; + self.goalstack13 = self.goalstack12; + self.goalstack12 = self.goalstack11; + self.goalstack11 = self.goalstack10; + self.goalstack10 = self.goalstack09; + self.goalstack09 = self.goalstack08; + self.goalstack08 = self.goalstack07; + self.goalstack07 = self.goalstack06; + self.goalstack06 = self.goalstack05; + self.goalstack05 = self.goalstack04; + self.goalstack04 = self.goalstack03; + self.goalstack03 = self.goalstack02; + self.goalstack02 = self.goalstack01; + self.goalstack01 = self.goalcurrent; + self.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() +{SELFPARAM(); + //print("bot ", etos(self), " pop\n"); + self.goalcurrent = self.goalstack01; + self.goalstack01 = self.goalstack02; + self.goalstack02 = self.goalstack03; + self.goalstack03 = self.goalstack04; + self.goalstack04 = self.goalstack05; + self.goalstack05 = self.goalstack06; + self.goalstack06 = self.goalstack07; + self.goalstack07 = self.goalstack08; + self.goalstack08 = self.goalstack09; + self.goalstack09 = self.goalstack10; + self.goalstack10 = self.goalstack11; + self.goalstack11 = self.goalstack12; + self.goalstack12 = self.goalstack13; + self.goalstack13 = self.goalstack14; + self.goalstack14 = self.goalstack15; + self.goalstack15 = self.goalstack16; + self.goalstack16 = self.goalstack17; + self.goalstack17 = self.goalstack18; + self.goalstack18 = self.goalstack19; + self.goalstack19 = self.goalstack20; + self.goalstack20 = self.goalstack21; + self.goalstack21 = self.goalstack22; + self.goalstack22 = self.goalstack23; + self.goalstack23 = self.goalstack24; + self.goalstack24 = self.goalstack25; + self.goalstack25 = self.goalstack26; + self.goalstack26 = self.goalstack27; + self.goalstack27 = self.goalstack28; + self.goalstack28 = self.goalstack29; + self.goalstack29 = self.goalstack30; + self.goalstack30 = self.goalstack31; + self.goalstack31 = world; +} + +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, PL_MIN, PL_MAX, org, bot_navigation_movemode)) + return true; + } + else + { + if (tracewalk(ent, org, PL_MIN, PL_MAX, 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) +{ + entity waylist, w, best; + vector v, org, pm1, pm2; + + pm1 = ent.origin + ent.mins; + pm2 = ent.origin + ent.maxs; + waylist = findchain(classname, "waypoint"); + + // do two scans, because box test is cheaper + w = waylist; + while (w) + { + // if object is touching spawnfunc_waypoint + if(w != ent && w != except) + if (boxesoverlap(pm1, pm2, w.absmin, w.absmax)) + return w; + w = w.chain; + } + + org = ent.origin + 0.5 * (ent.mins + ent.maxs); + org.z = ent.origin.z + ent.mins.z - PL_MIN.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); + + best = world; + + // box check failed, try walk + w = waylist; + while (w) + { + // if object can walk from spawnfunc_waypoint + if(w != ent) + { + if (w.wpisbox) + { + vector wm1, wm2; + wm1 = w.origin + w.mins; + wm2 = w.origin + w.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 = w.origin; + if(navigation_waypoint_will_link(v, org, ent, walkfromwp, bestdist)) + { + bestdist = vlen(v - org); + best = w; + } + } + w = w.chain; + } + return best; +} +entity navigation_findnearestwaypoint(entity ent, float walkfromwp) +{ + entity wp = navigation_findnearestwaypoint_withdist_except(ent, walkfromwp, 1050, world); + 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 waylist, float maxdist) +{SELFPARAM(); + entity head; + vector v, m1, m2, diff; + float c; +// navigation_testtracewalk = true; + c = 0; + head = waylist; + while (head) + { + if (!head.wpconsidered) + { + if (head.wpisbox) + { + m1 = head.origin + head.mins; + m2 = head.origin + head.maxs; + v = self.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 = head.origin; + diff = v - self.origin; + diff.z = max(0, diff.z); + if (vlen(diff) < maxdist) + { + head.wpconsidered = true; + if (tracewalk(self, self.origin, self.mins, self.maxs, v, bot_navigation_movemode)) + { + head.wpnearestpoint = v; + head.wpcost = vlen(v - self.origin) + head.dmg; + head.wpfire = 1; + head.enemy = world; + c = c + 1; + } + } + } + head = head.chain; + } + //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 fixed_source_waypoint) +{SELFPARAM(); + entity w, wp, waylist; + float searching, cost, cost2; + vector p; + w = waylist = findchain(classname, "waypoint"); + while (w) + { + w.wpconsidered = false; + w.wpnearestpoint = '0 0 0'; + w.wpcost = 10000000; + w.wpfire = 0; + w.enemy = world; + w = w.chain; + } + + 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 = world; + } + 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 i, increment, maxdistance; + if(self.flags & FL_ONGROUND) + { + increment = 750; + maxdistance = 50000; + } + else + { + increment = 500; + maxdistance = 1500; + } + + for(i=increment;!navigation_markroutes_nearestwaypoints(waylist, i)&&i cost2 + w.wp00mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp01;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp01mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp02;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp02mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp03;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp03mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp04;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp04mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp05;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp05mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp06;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp06mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp07;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp07mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp08;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp08mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp09;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp09mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp10;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp10mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp11;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp11mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp12;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp12mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp13;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp13mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp14;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp14mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp15;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp15mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp16;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp16mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp17;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp17mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp18;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp18mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp19;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp19mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp20;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp20mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp21;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp21mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp22;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp22mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp23;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp23mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp24;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp24mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp25;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp25mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp26;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp26mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp27;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp27mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp28;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp28mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp29;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp29mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp30;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp30mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + wp = w.wp31;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp31mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); + }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} + } + w = w.chain; + } + } +} + +// queries the entire spawnfunc_waypoint network for pathes leading to the bot +void navigation_markroutes_inverted(entity fixed_source_waypoint) +{ + entity w, wp, waylist; + float searching, cost, cost2; + vector p; + w = waylist = findchain(classname, "waypoint"); + while (w) + { + w.wpconsidered = false; + w.wpnearestpoint = '0 0 0'; + w.wpcost = 10000000; + w.wpfire = 0; + w.enemy = world; + w = w.chain; + } + + 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 = world; + } + else + { + error("need to start with a waypoint\n"); + } + + searching = true; + while (searching) + { + searching = false; + w = waylist; + while (w) + { + if (w.wpfire) + { + searching = true; + w.wpfire = 0; + cost = w.wpcost; // cost to walk from w to home + p = w.wpnearestpoint; + for(wp = waylist; wp; wp = wp.chain) + { + if(w != wp.wp00) if(w != wp.wp01) if(w != wp.wp02) if(w != wp.wp03) + if(w != wp.wp04) if(w != wp.wp05) if(w != wp.wp06) if(w != wp.wp07) + if(w != wp.wp08) if(w != wp.wp09) if(w != wp.wp10) if(w != wp.wp11) + if(w != wp.wp12) if(w != wp.wp13) if(w != wp.wp14) if(w != wp.wp15) + if(w != wp.wp16) if(w != wp.wp17) if(w != wp.wp18) if(w != wp.wp19) + if(w != wp.wp20) if(w != wp.wp21) if(w != wp.wp22) if(w != wp.wp23) + if(w != wp.wp24) if(w != wp.wp25) if(w != wp.wp26) if(w != wp.wp27) + if(w != wp.wp28) if(w != wp.wp29) if(w != wp.wp30) if(w != wp.wp31) + continue; + cost2 = cost + wp.dmg; + navigation_markroutes_checkwaypoint(w, wp, cost2, p); + } + } + w = w.chain; + } + } +} + +// updates the best goal according to a weighted calculation of travel cost and item value of a new proposed item +void navigation_routerating(entity e, float f, float rangebias) +{SELFPARAM(); + 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(self.items & IT_JETPACK) + if(autocvar_bot_ai_navigation_jetpack) + if(vlen(self.origin - o) > autocvar_bot_ai_navigation_jetpack_mindistance) + { + vector pointa, pointb; + + bot_debug(strcat("jetpack ai: evaluating path for ", e.classname, "\n")); + + // Point A + traceline(self.origin, self.origin + '0 0 65535', MOVE_NORMAL, self); + 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, self); + + if(trace_fraction==1) + { + bot_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' * (PL_MAX.z - PL_MIN.z) * 10; + + do{ + npa = pointa + down; + npb = pointb + down; + + if(npa.z<=self.absmax.z) + break; + + if(npb.z<=e.absmax.z) + break; + + traceline(npa, npb, MOVE_NORMAL, self); + 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 - self.origin.z); + + t = zdistance / autocvar_g_jetpack_maxspeed_up; + t += xydistance / autocvar_g_jetpack_maxspeed_side; + fuel = t * autocvar_g_jetpack_fuel * 0.8; + + bot_debug(strcat("jetpack ai: required fuel ", ftos(fuel), " self.ammo_fuel ", ftos(self.ammo_fuel), "\n")); + + // enough fuel ? + if(self.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) + { + bot_debug(strcat("jetpack path: added goal ", e.classname, " (with rating ", ftos(f), ")\n")); + navigation_bestrating = f; + navigation_bestgoal = e; + self.navigation_jetpack_goal = e; + self.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 + { + bot_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) + { + bot_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; + } + + bot_debug(strcat("-- checking ", e.classname, " (with cost ", ftos(nwp.wpcost), ")\n")); + if (nwp) + if (nwp.wpcost < 10000000) + { + //te_wizspike(nwp.wpnearestpoint); + bot_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))); + bot_debug(strcat("considering ", e.classname, " (with rating ", ftos(f), ")\n")); + if (navigation_bestrating < f) + { + bot_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 +float navigation_routetogoal(entity e, vector startposition) +{SELFPARAM(); + self.goalentity = e; + + // if there is no goal, just exit + if (!e) + return false; + + self.navigation_hasgoals = true; + + // put the entity on the goal stack + //print("routetogoal ", etos(e), "\n"); + navigation_pushroute(e); + + if(g_jetpack) + if(e==self.navigation_jetpack_goal) + return true; + + // if it can reach the goal there is nothing more to do + if (tracewalk(self, startposition, PL_MIN, PL_MAX, (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 == world) + return false; + + for (;;) + { + // add the spawnfunc_waypoint to the path + navigation_pushroute(e); + e = e.enemy; + + if(e==world) + 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() +{SELFPARAM(); + vector org, m1, m2; + org = self.origin; + m1 = org + self.mins; + m2 = org + self.maxs; + + if(self.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT) + { + if(self.lastteleporttime>0) + if(time-self.lastteleporttime<(self.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL)?2:0.15) + { + if(self.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) + if(self.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL && self.goalcurrent.owner==self) + { + self.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING; + self.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED; + } + navigation_poproute(); + return; + } + } + + // If for some reason the bot is closer to the next goal, pop the current one + if(self.goalstack01) + if(vlen(self.goalcurrent.origin - self.origin) > vlen(self.goalstack01.origin - self.origin)) + if(checkpvs(self.origin + self.view_ofs, self.goalstack01)) + if(tracewalk(self, self.origin, self.mins, self.maxs, (self.goalstack01.absmin + self.goalstack01.absmax) * 0.5, bot_navigation_movemode)) + { + bot_debug(strcat("path optimized for ", self.netname, ", removed a goal from the queue\n")); + navigation_poproute(); + // 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 itself + if(IS_PLAYER(self.goalcurrent)) + navigation_poproute(); + + // aid for detecting jump pads better (distance based check fails sometimes) + if(self.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT && self.jumppadcount > 0 ) + navigation_poproute(); + + // Loose goal touching check when running + if(self.aistatus & AI_STATUS_RUNNING) + if(self.speed >= autocvar_sv_maxspeed) // if -really- running + if(self.goalcurrent.classname=="waypoint") + { + if(vlen(self.origin - self.goalcurrent.origin)<150) + { + traceline(self.origin + self.view_ofs , self.goalcurrent.origin, true, world); + if(trace_fraction==1) + { + // Detect personal waypoints + if(self.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) + if(self.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL && self.goalcurrent.owner==self) + { + self.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING; + self.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED; + } + + navigation_poproute(); + } + } + } + + while (self.goalcurrent && boxesoverlap(m1, m2, self.goalcurrent.absmin, self.goalcurrent.absmax)) + { + // Detect personal waypoints + if(self.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) + if(self.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL && self.goalcurrent.owner==self) + { + self.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING; + self.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED; + } + + navigation_poproute(); + } +} + +// begin a goal selection session (queries spawnfunc_waypoint network) +void navigation_goalrating_start() +{SELFPARAM(); + if(self.aistatus & AI_STATUS_STUCK) + return; + + self.navigation_jetpack_goal = world; + navigation_bestrating = -1; + self.navigation_hasgoals = false; + navigation_clearroute(); + navigation_bestgoal = world; + navigation_markroutes(world); +} + +// ends a goal selection session (updates goal stack to the best goal) +void navigation_goalrating_end() +{SELFPARAM(); + if(self.aistatus & AI_STATUS_STUCK) + return; + + navigation_routetogoal(navigation_bestgoal, self.origin); + bot_debug(strcat("best goal ", self.goalcurrent.classname , "\n")); + + // If the bot got stuck then try to reach the farthest waypoint + if (!self.navigation_hasgoals) + if (autocvar_bot_wander_enable) + { + if (!(self.aistatus & AI_STATUS_STUCK)) + { + bot_debug(strcat(self.netname, " cannot walk to any goal\n")); + self.aistatus |= AI_STATUS_STUCK; + } + + self.navigation_hasgoals = false; // Reset this value + } +} + +void botframe_updatedangerousobjects(float maxupdate) +{ + entity head, bot_dodgelist; + vector m1, m2, v, o; + float c, d, danger; + c = 0; + bot_dodgelist = findchainfloat(bot_dodge, true); + botframe_dangerwaypoint = find(botframe_dangerwaypoint, classname, "waypoint"); + while (botframe_dangerwaypoint != world) + { + danger = 0; + m1 = botframe_dangerwaypoint.mins; + m2 = botframe_dangerwaypoint.maxs; + head = bot_dodgelist; + while (head) + { + v = head.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 = (head.absmin + head.absmax) * 0.5; + d = head.bot_dodgerating - vlen(o - v); + if (d > 0) + { + traceline(o, v, true, world); + if (trace_fraction == 1) + danger = danger + d; + } + head = head.chain; + } + botframe_dangerwaypoint.dmg = danger; + c = c + 1; + if (c >= maxupdate) + break; + botframe_dangerwaypoint = find(botframe_dangerwaypoint, classname, "waypoint"); + } +} + +void navigation_unstuck() +{SELFPARAM(); + float search_radius = 1000; + + if (!autocvar_bot_wander_enable) + return; + + if (!bot_waypoint_queue_owner) + { + bot_debug(strcat(self.netname, " sutck, taking over the waypoints queue\n")); + bot_waypoint_queue_owner = self; + bot_waypoint_queue_bestgoal = world; + bot_waypoint_queue_bestgoalrating = 0; + } + + if(bot_waypoint_queue_owner!=self) + return; + + if (bot_waypoint_queue_goal) + { + // evaluate the next goal on the queue + float d = vlen(self.origin - bot_waypoint_queue_goal.origin); + bot_debug(strcat(self.netname, " evaluating ", bot_waypoint_queue_goal.classname, " with distance ", ftos(d), "\n")); + if(tracewalk(bot_waypoint_queue_goal, self.origin, PL_MIN, PL_MAX, 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) + { + bot_debug(strcat(self.netname, " stuck, reachable waypoint found, heading to it\n")); + navigation_routetogoal(bot_waypoint_queue_bestgoal, self.origin); + self.bot_strategytime = time + autocvar_bot_ai_strategyinterval; + self.aistatus &= ~AI_STATUS_STUCK; + } + else + { + bot_debug(strcat(self.netname, " stuck, cannot walk to any waypoint at all\n")); + } + + bot_waypoint_queue_owner = world; + } + } + else + { + if(bot_strategytoken!=self) + return; + + // build a new queue + bot_debug(strcat(self.netname, " stuck, scanning reachable waypoints within ", ftos(search_radius)," qu\n")); + + entity head, first; + + first = world; + head = findradius(self.origin, search_radius); + + while(head) + { + if(head.classname=="waypoint") + // if(!(head.wpflags & WAYPOINTFLAG_GENERATED)) + { + if(bot_waypoint_queue_goal) + bot_waypoint_queue_goal.bot_waypoint_queue_nextgoal = head; + else + first = head; + + bot_waypoint_queue_goal = head; + bot_waypoint_queue_goal.bot_waypoint_queue_nextgoal = world; + } + + head = head.chain; + } + + if (first) + bot_waypoint_queue_goal = first; + else + { + bot_debug(strcat(self.netname, " stuck, cannot walk to any waypoint at all\n")); + bot_waypoint_queue_owner = world; + } + } +} + +// Support for debugging tracewalk visually + +void debugresetnodes() +{ + debuglastnode = '0 0 0'; +} + +void debugnode(vector node) +{SELFPARAM(); + if (!IS_PLAYER(self)) + return; + + if(debuglastnode=='0 0 0') + { + debuglastnode = node; + return; + } + + te_lightning2(world, 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() +{SELFPARAM(); + entity goal; + vector org, go; + + if(self.goalcounter==0)goal=self.goalcurrent; + else if(self.goalcounter==1)goal=self.goalstack01; + else if(self.goalcounter==2)goal=self.goalstack02; + else if(self.goalcounter==3)goal=self.goalstack03; + else if(self.goalcounter==4)goal=self.goalstack04; + else if(self.goalcounter==5)goal=self.goalstack05; + else if(self.goalcounter==6)goal=self.goalstack06; + else if(self.goalcounter==7)goal=self.goalstack07; + else if(self.goalcounter==8)goal=self.goalstack08; + else if(self.goalcounter==9)goal=self.goalstack09; + else if(self.goalcounter==10)goal=self.goalstack10; + else if(self.goalcounter==11)goal=self.goalstack11; + else if(self.goalcounter==12)goal=self.goalstack12; + else if(self.goalcounter==13)goal=self.goalstack13; + else if(self.goalcounter==14)goal=self.goalstack14; + else if(self.goalcounter==15)goal=self.goalstack15; + else if(self.goalcounter==16)goal=self.goalstack16; + else if(self.goalcounter==17)goal=self.goalstack17; + else if(self.goalcounter==18)goal=self.goalstack18; + else if(self.goalcounter==19)goal=self.goalstack19; + else if(self.goalcounter==20)goal=self.goalstack20; + else if(self.goalcounter==21)goal=self.goalstack21; + else if(self.goalcounter==22)goal=self.goalstack22; + else if(self.goalcounter==23)goal=self.goalstack23; + else if(self.goalcounter==24)goal=self.goalstack24; + else if(self.goalcounter==25)goal=self.goalstack25; + else if(self.goalcounter==26)goal=self.goalstack26; + else if(self.goalcounter==27)goal=self.goalstack27; + else if(self.goalcounter==28)goal=self.goalstack28; + else if(self.goalcounter==29)goal=self.goalstack29; + else if(self.goalcounter==30)goal=self.goalstack30; + else if(self.goalcounter==31)goal=self.goalstack31; + else goal=world; + + if(goal==world) + { + self.goalcounter = 0; + self.lastposition='0 0 0'; + return; + } + + if(self.lastposition=='0 0 0') + org = self.origin; + else + org = self.lastposition; + + + go = ( goal.absmin + goal.absmax ) * 0.5; + te_lightning2(world, org, go); + self.lastposition = go; + + self.goalcounter++; +} diff --git a/qcsrc/server/bot/default/navigation.qh b/qcsrc/server/bot/default/navigation.qh new file mode 100644 index 000000000..cf4a5ce5b --- /dev/null +++ b/qcsrc/server/bot/default/navigation.qh @@ -0,0 +1,80 @@ +#ifndef NAVIGATION_H +#define NAVIGATION_H +/* + * Globals and Fields + */ + +float navigation_bestrating; +float bot_navigation_movemode; +float navigation_testtracewalk; + +vector jumpstepheightvec; +vector stepheightvec; + +entity botframe_dangerwaypoint; +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(vector node); +void debugnodestatus(vector position, float status); + +void debuggoalstack(); + +float tracewalk(entity e, vector start, vector m1, vector m2, vector end, float movemode); + +float navigation_markroutes_nearestwaypoints(entity waylist, float maxdist); +float navigation_routetogoal(entity e, vector startposition); + +void navigation_clearroute(); +void navigation_pushroute(entity e); +void navigation_poproute(); +void navigation_markroutes_checkwaypoint(entity w, entity wp, float cost2, vector p); +void navigation_markroutes(entity fixed_source_waypoint); +void navigation_markroutes_inverted(entity fixed_source_waypoint); +void navigation_routerating(entity e, float f, float rangebias); +void navigation_poptouchedgoals(); +void navigation_goalrating_start(); +void navigation_goalrating_end(); +void navigation_unstuck(); + +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); +#endif diff --git a/qcsrc/server/bot/default/scripting.qc b/qcsrc/server/bot/default/scripting.qc new file mode 100644 index 000000000..3f53d2b9f --- /dev/null +++ b/qcsrc/server/bot/default/scripting.qc @@ -0,0 +1,1346 @@ +#include "scripting.qh" + +.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); +} + +float bot_havecommand(entity bot, float idx) +{ + if(!bot.bot_cmdqueuebuf_allocated) + return 0; + if(idx < bot.bot_cmdqueuebuf_start) + return 0; + if(idx >= bot.bot_cmdqueuebuf_end) + return 0; + return 1; +} + +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(string placename) +{SELFPARAM(); + entity e; + if(substring(placename, 0, 1) == "@") + { + int i, p; + placename = substring(placename, 1, -1); + string s, s2; + for(i = 0; i < self.bot_places_count; ++i) + if(self.(bot_placenames[i]) == placename) + return self.(bot_places[i]); + // now: i == self.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(world, targetname, s); + if(!e) + LOG_INFO("invalid place ", s, "\n"); + if(i < MAX_BOT_PLACES) + { + self.(bot_placenames[i]) = strzone(placename); + self.(bot_places[i]) = e; + self.bot_places_count += 1; + } + return e; + } + else + { + e = find(world, 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 world; +} + +// Returns a bot by number on list +entity find_bot_by_number(float number) +{ + entity bot; + float c = 0; + + if(!number) + return world; + + bot = findchainflags(flags, FL_CLIENT); + while (bot) + { + if(IS_BOT_CLIENT(bot)) + { + if(++c==number) + return bot; + } + bot = bot.chain; + } + + return world; +} + +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; + +void SV_ParseClientCommand(string s); +float bot_cmd_cc() +{ + SV_ParseClientCommand(bot_cmd.bot_cmd_parm_string); + return CMD_STATUS_FINISHED; +} + +float bot_cmd_impulse() +{SELFPARAM(); + self.impulse = bot_cmd.bot_cmd_parm_float; + return CMD_STATUS_FINISHED; +} + +float bot_cmd_continue() +{SELFPARAM(); + self.bot_exec_status &= ~BOT_EXEC_STATUS_PAUSED; + return CMD_STATUS_FINISHED; +} + +.float bot_cmd_wait_time; +float bot_cmd_wait() +{SELFPARAM(); + if(self.bot_exec_status & BOT_EXEC_STATUS_WAITING) + { + if(time>=self.bot_cmd_wait_time) + { + self.bot_exec_status &= ~BOT_EXEC_STATUS_WAITING; + return CMD_STATUS_FINISHED; + } + else + return CMD_STATUS_EXECUTING; + } + + self.bot_cmd_wait_time = time + bot_cmd.bot_cmd_parm_float; + self.bot_exec_status |= BOT_EXEC_STATUS_WAITING; + return CMD_STATUS_EXECUTING; +} + +float bot_cmd_wait_until() +{SELFPARAM(); + if(time < bot_cmd.bot_cmd_parm_float + bot_barriertime) + { + self.bot_exec_status |= BOT_EXEC_STATUS_WAITING; + return CMD_STATUS_EXECUTING; + } + self.bot_exec_status &= ~BOT_EXEC_STATUS_WAITING; + return CMD_STATUS_FINISHED; +} + +float bot_cmd_barrier() +{SELFPARAM(); + entity cl; + + // 0 = no barrier, 1 = waiting, 2 = waiting finished + + if(self.bot_barrier == 0) // initialization + { + self.bot_barrier = 1; + + //self.colormod = '4 4 0'; + } + + if(self.bot_barrier == 1) // find other bots + { + FOR_EACH_CLIENT(cl) if(cl.isbot) + { + if(cl.bot_cmdqueuebuf_allocated) + if(cl.bot_barrier != 1) + return CMD_STATUS_EXECUTING; // not all are at the barrier yet + } + + // all bots hit the barrier! + FOR_EACH_CLIENT(cl) if(cl.isbot) + { + cl.bot_barrier = 2; // acknowledge barrier + } + + bot_barriertime = time; + } + + // if we get here, the barrier is finished + // so end it... + self.bot_barrier = 0; + //self.colormod = '0 0 0'; + + return CMD_STATUS_FINISHED; +} + +float bot_cmd_turn() +{SELFPARAM(); + self.v_angle_y = self.v_angle.y + bot_cmd.bot_cmd_parm_float; + self.v_angle_y = self.v_angle.y - floor(self.v_angle.y / 360) * 360; + return CMD_STATUS_FINISHED; +} + +float bot_cmd_select_weapon() +{SELFPARAM(); + float id; + + id = bot_cmd.bot_cmd_parm_float; + + if(id < WEP_FIRST || id > WEP_LAST) + return CMD_STATUS_ERROR; + + if(client_hasweapon(self, id, true, false)) + self.switchweapon = 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(string expr) +{SELFPARAM(); + // 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 self.health; + case "speed": + return vlen(self.velocity); + case "flagcarrier": + return ((self.flagcarried!=world)); + } + + LOG_INFO(strcat("ERROR: Unable to convert the expression '",expr,"' into a numeric value\n")); + return 0; +} + +float bot_cmd_if() +{SELFPARAM(); + string expr, val_a, val_b; + float cmpofs; + + if(self.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(self); + return CMD_STATUS_ERROR; + } + + self.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(val_a)==bot_cmd_eval(val_b)) + self.bot_cmd_condition_status |= CMD_CONDITION_true; + else + self.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(val_a)>bot_cmd_eval(val_b)) + self.bot_cmd_condition_status |= CMD_CONDITION_true; + else + self.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(val_a)=self.bot_cmd_aim_endtime) + { + self.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) + { + self.v_angle_x -= stof(argv(1)); + self.v_angle_y += stof(argv(0)); + return CMD_STATUS_FINISHED; + } + + self.bot_cmd_aim_begin = self.v_angle; + + self.bot_cmd_aim_end_x = self.v_angle.x - stof(argv(1)); + self.bot_cmd_aim_end_y = self.v_angle.y + stof(argv(0)); + self.bot_cmd_aim_end_z = 0; + + self.bot_cmd_aim_begintime = time; + self.bot_cmd_aim_endtime = time + step; + + return CMD_STATUS_EXECUTING; +} + +float bot_cmd_aimtarget() +{SELFPARAM(); + if(self.bot_cmd_aim_endtime) + { + return bot_cmd_aim(); + } + + entity e; + string parms; + vector v; + float tokens, step; + + parms = bot_cmd.bot_cmd_parm_string; + + tokens = tokenizebyseparator(parms, " "); + + e = bot_getplace(argv(0)); + if(!e) + return CMD_STATUS_ERROR; + + v = e.origin + (e.mins + e.maxs) * 0.5; + + if(tokens==1) + { + self.v_angle = vectoangles(v - (self.origin + self.view_ofs)); + self.v_angle_x = -self.v_angle.x; + return CMD_STATUS_FINISHED; + } + + if(tokens<1||tokens>2) + return CMD_STATUS_ERROR; + + step = stof(argv(1)); + + self.bot_cmd_aim_begin = self.v_angle; + self.bot_cmd_aim_end = vectoangles(v - (self.origin + self.view_ofs)); + self.bot_cmd_aim_end_x = -self.bot_cmd_aim_end.x; + + self.bot_cmd_aim_begintime = time; + self.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 = 1; +const int BOT_CMD_KEY_BACKWARD = 2; +const int BOT_CMD_KEY_RIGHT = 4; +const int BOT_CMD_KEY_LEFT = 8; +const int BOT_CMD_KEY_JUMP = 16; +const int BOT_CMD_KEY_ATTACK1 = 32; +const int BOT_CMD_KEY_ATTACK2 = 64; +const int BOT_CMD_KEY_USE = 128; +const int BOT_CMD_KEY_HOOK = 256; +const int BOT_CMD_KEY_CROUCH = 512; +const int BOT_CMD_KEY_CHAT = 1024; + +float bot_presskeys() +{SELFPARAM(); + self.movement = '0 0 0'; + self.BUTTON_JUMP = false; + self.BUTTON_CROUCH = false; + self.BUTTON_ATCK = false; + self.BUTTON_ATCK2 = false; + self.BUTTON_USE = false; + self.BUTTON_HOOK = false; + self.BUTTON_CHAT = false; + + if(self.bot_cmd_keys == BOT_CMD_KEY_NONE) + return false; + + if(self.bot_cmd_keys & BOT_CMD_KEY_FORWARD) + self.movement_x = autocvar_sv_maxspeed; + else if(self.bot_cmd_keys & BOT_CMD_KEY_BACKWARD) + self.movement_x = -autocvar_sv_maxspeed; + + if(self.bot_cmd_keys & BOT_CMD_KEY_RIGHT) + self.movement_y = autocvar_sv_maxspeed; + else if(self.bot_cmd_keys & BOT_CMD_KEY_LEFT) + self.movement_y = -autocvar_sv_maxspeed; + + if(self.bot_cmd_keys & BOT_CMD_KEY_JUMP) + self.BUTTON_JUMP = true; + + if(self.bot_cmd_keys & BOT_CMD_KEY_CROUCH) + self.BUTTON_CROUCH = true; + + if(self.bot_cmd_keys & BOT_CMD_KEY_ATTACK1) + self.BUTTON_ATCK = true; + + if(self.bot_cmd_keys & BOT_CMD_KEY_ATTACK2) + self.BUTTON_ATCK2 = true; + + if(self.bot_cmd_keys & BOT_CMD_KEY_USE) + self.BUTTON_USE = true; + + if(self.bot_cmd_keys & BOT_CMD_KEY_HOOK) + self.BUTTON_HOOK = true; + + if(self.bot_cmd_keys & BOT_CMD_KEY_CHAT) + self.BUTTON_CHAT = true; + + return true; +} + + +float bot_cmd_keypress_handler(string key, float enabled) +{SELFPARAM(); + switch(key) + { + case "all": + if(enabled) + self.bot_cmd_keys = power2of(20) - 1; // >:) + else + self.bot_cmd_keys = BOT_CMD_KEY_NONE; + case "forward": + if(enabled) + { + self.bot_cmd_keys |= BOT_CMD_KEY_FORWARD; + self.bot_cmd_keys &= ~BOT_CMD_KEY_BACKWARD; + } + else + self.bot_cmd_keys &= ~BOT_CMD_KEY_FORWARD; + break; + case "backward": + if(enabled) + { + self.bot_cmd_keys |= BOT_CMD_KEY_BACKWARD; + self.bot_cmd_keys &= ~BOT_CMD_KEY_FORWARD; + } + else + self.bot_cmd_keys &= ~BOT_CMD_KEY_BACKWARD; + break; + case "left": + if(enabled) + { + self.bot_cmd_keys |= BOT_CMD_KEY_LEFT; + self.bot_cmd_keys &= ~BOT_CMD_KEY_RIGHT; + } + else + self.bot_cmd_keys &= ~BOT_CMD_KEY_LEFT; + break; + case "right": + if(enabled) + { + self.bot_cmd_keys |= BOT_CMD_KEY_RIGHT; + self.bot_cmd_keys &= ~BOT_CMD_KEY_LEFT; + } + else + self.bot_cmd_keys &= ~BOT_CMD_KEY_RIGHT; + break; + case "jump": + if(enabled) + self.bot_cmd_keys |= BOT_CMD_KEY_JUMP; + else + self.bot_cmd_keys &= ~BOT_CMD_KEY_JUMP; + break; + case "crouch": + if(enabled) + self.bot_cmd_keys |= BOT_CMD_KEY_CROUCH; + else + self.bot_cmd_keys &= ~BOT_CMD_KEY_CROUCH; + break; + case "attack1": + if(enabled) + self.bot_cmd_keys |= BOT_CMD_KEY_ATTACK1; + else + self.bot_cmd_keys &= ~BOT_CMD_KEY_ATTACK1; + break; + case "attack2": + if(enabled) + self.bot_cmd_keys |= BOT_CMD_KEY_ATTACK2; + else + self.bot_cmd_keys &= ~BOT_CMD_KEY_ATTACK2; + break; + case "use": + if(enabled) + self.bot_cmd_keys |= BOT_CMD_KEY_USE; + else + self.bot_cmd_keys &= ~BOT_CMD_KEY_USE; + break; + case "hook": + if(enabled) + self.bot_cmd_keys |= BOT_CMD_KEY_HOOK; + else + self.bot_cmd_keys &= ~BOT_CMD_KEY_HOOK; + break; + case "chat": + if(enabled) + self.bot_cmd_keys |= BOT_CMD_KEY_CHAT; + else + self.bot_cmd_keys &= ~BOT_CMD_KEY_CHAT; + break; + default: + break; + } + + return CMD_STATUS_FINISHED; +} + +float bot_cmd_presskey() +{ + string key; + + key = bot_cmd.bot_cmd_parm_string; + + bot_cmd_keypress_handler(key,true); + + return CMD_STATUS_FINISHED; +} + +float bot_cmd_releasekey() +{ + string key; + + key = bot_cmd.bot_cmd_parm_string; + + return bot_cmd_keypress_handler(key,false); +} + +float bot_cmd_pause() +{SELFPARAM(); + self.button1 = 0; + self.button8 = 0; + self.BUTTON_USE = 0; + self.BUTTON_ATCK = 0; + self.BUTTON_JUMP = 0; + self.BUTTON_HOOK = 0; + self.BUTTON_CHAT = 0; + self.BUTTON_ATCK2 = 0; + self.BUTTON_CROUCH = 0; + + self.movement = '0 0 0'; + self.bot_cmd_keys = BOT_CMD_KEY_NONE; + + self.bot_exec_status |= BOT_EXEC_STATUS_PAUSED; + return CMD_STATUS_FINISHED; +} + +float bot_cmd_moveto() +{SELFPARAM(); + return self.cmd_moveto(bot_cmd.bot_cmd_parm_vector); +} + +float bot_cmd_movetotarget() +{SELFPARAM(); + entity e; + e = bot_getplace(bot_cmd.bot_cmd_parm_string); + if(!e) + return CMD_STATUS_ERROR; + return self.cmd_moveto(e.origin + (e.mins + e.maxs) * 0.5); +} + +float bot_cmd_resetgoal() +{SELFPARAM(); + return self.cmd_resetgoal(); +} + + +float bot_cmd_sound() +{SELFPARAM(); + 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(self, chan, sample, vol, atten); + + return CMD_STATUS_FINISHED; +} + +.entity tuba_note; +float bot_cmd_debug_assert_canfire() +{SELFPARAM(); + float f; + f = bot_cmd.bot_cmd_parm_float; + + if(self.weaponentity.state != WS_READY) + { + if(f) + { + self.colormod = '0 8 8'; + LOG_INFO("Bot ", self.netname, " using ", self.weaponname, " wants to fire, inhibited by weaponentity state\n"); + } + } + else if(ATTACK_FINISHED(self) > time) + { + if(f) + { + self.colormod = '8 0 8'; + LOG_INFO("Bot ", self.netname, " using ", self.weaponname, " wants to fire, inhibited by ATTACK_FINISHED (", ftos(ATTACK_FINISHED(self) - time), " seconds left)\n"); + } + } + else if(self.tuba_note) + { + if(f) + { + self.colormod = '8 0 0'; + LOG_INFO("Bot ", self.netname, " using ", self.weaponname, " wants to fire, bot still has an active tuba note\n"); + } + } + else + { + if(!f) + { + self.colormod = '8 8 0'; + LOG_INFO("Bot ", self.netname, " using ", self.weaponname, " thinks it has fired, but apparently did not; ATTACK_FINISHED says ", ftos(ATTACK_FINISHED(self) - time), " seconds left\n"); + } + } + + return CMD_STATUS_FINISHED; +} + +// + +void bot_command_executed(float rm) +{SELFPARAM(); + entity cmd; + + cmd = bot_cmd; + + if(rm) + bot_dequeuecommand(self, self.bot_cmd_execution_index); + + self.bot_cmd_execution_index++; +} + +void bot_setcurrentcommand() +{SELFPARAM(); + bot_cmd = world; + + if(!self.bot_cmd_current) + { + self.bot_cmd_current = spawn(); + self.bot_cmd_current.classname = "bot_cmd"; + self.bot_cmd_current.is_bot_cmd = 1; + } + + bot_cmd = self.bot_cmd_current; + if(bot_cmd.bot_cmd_index != self.bot_cmd_execution_index || self.bot_cmd_execution_index == 0) + { + if(bot_havecommand(self, self.bot_cmd_execution_index)) + { + string cmdstring; + cmdstring = bot_readcommand(self, self.bot_cmd_execution_index); + if(bot_decodecommand(cmdstring)) + { + bot_cmd.owner = self; + bot_cmd.bot_cmd_index = self.bot_cmd_execution_index; + } + else + { + // Invalid command, remove from queue + bot_cmd = world; + bot_dequeuecommand(self, self.bot_cmd_execution_index); + self.bot_cmd_execution_index++; + } + } + else + bot_cmd = world; + } +} + +void bot_resetqueues() +{ + entity cl; + + FOR_EACH_CLIENT(cl) if(cl.isbot) + { + cl.bot_cmd_execution_index = 0; + bot_clearqueue(cl); + // also, cancel all barriers + cl.bot_barrier = 0; + for(int i = 0; i < cl.bot_places_count; ++i) + { + strunzone(cl.(bot_placenames[i])); + cl.(bot_placenames[i]) = string_null; + } + cl.bot_places_count = 0; + } + + bot_barriertime = time; +} + +bool autocvar_g_debug_bot_commands; + +// 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() +{SELFPARAM(); + float status, ispressingkey; + + // Find command + bot_setcurrentcommand(); + + // 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 == world) + return false; + + // Ignore all commands except continue when the bot is paused + if(self.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(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 = !!bot_presskeys(); + + // Handle conditions + if (!(bot_cmd.bot_cmd_type==BOT_CMD_FI||bot_cmd.bot_cmd_type==BOT_CMD_ELSE)) + if(self.bot_cmd_condition_status & CMD_CONDITION_true && self.bot_cmd_condition_status & CMD_CONDITION_false_BLOCK) + { + bot_command_executed(true); + return -1; + } + else if(self.bot_cmd_condition_status & CMD_CONDITION_false && self.bot_cmd_condition_status & CMD_CONDITION_true_BLOCK) + { + bot_command_executed(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(); + break; + case BOT_CMD_CONTINUE: + status = bot_cmd_continue(); + break; + case BOT_CMD_WAIT: + status = bot_cmd_wait(); + break; + case BOT_CMD_WAIT_UNTIL: + status = bot_cmd_wait_until(); + break; + case BOT_CMD_TURN: + status = bot_cmd_turn(); + break; + case BOT_CMD_MOVETO: + status = bot_cmd_moveto(); + break; + case BOT_CMD_MOVETOTARGET: + status = bot_cmd_movetotarget(); + break; + case BOT_CMD_RESETGOAL: + status = bot_cmd_resetgoal(); + break; + case BOT_CMD_CC: + status = bot_cmd_cc(); + break; + case BOT_CMD_IF: + status = bot_cmd_if(); + break; + case BOT_CMD_ELSE: + status = bot_cmd_else(); + break; + case BOT_CMD_FI: + status = bot_cmd_fi(); + break; + case BOT_CMD_RESETAIM: + status = bot_cmd_resetaim(); + break; + case BOT_CMD_AIM: + status = bot_cmd_aim(); + break; + case BOT_CMD_AIMTARGET: + status = bot_cmd_aimtarget(); + break; + case BOT_CMD_PRESSKEY: + status = bot_cmd_presskey(); + break; + case BOT_CMD_RELEASEKEY: + status = bot_cmd_releasekey(); + break; + case BOT_CMD_SELECTWEAPON: + status = bot_cmd_select_weapon(); + break; + case BOT_CMD_IMPULSE: + status = bot_cmd_impulse(); + break; + case BOT_CMD_BARRIER: + status = bot_cmd_barrier(); + 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(); + break; + case BOT_CMD_DEBUG_ASSERT_CANFIRE: + status = bot_cmd_debug_assert_canfire(); + 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(self,strcat("say ^7", bot_cmd_string[bot_cmd.bot_cmd_type]," ",parms,"\n")); + } + + bot_command_executed(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 +float bot_execute_commands() +{ + float f; + do + { + f = bot_execute_commands_once(); + } + 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..1f2d4da31 --- /dev/null +++ b/qcsrc/server/bot/default/scripting.qh @@ -0,0 +1,84 @@ +#ifndef BOT_SCRIPTING_H +#define BOT_SCRIPTING_H + +#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 + +void bot_clearqueue(entity bot); + +// 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(vector) cmd_moveto; +.float() 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 find_bot_by_name(string name); +entity find_bot_by_number(float number); +#endif diff --git a/qcsrc/server/bot/default/waypoints.qc b/qcsrc/server/bot/default/waypoints.qc new file mode 100644 index 000000000..25031ef34 --- /dev/null +++ b/qcsrc/server/bot/default/waypoints.qc @@ -0,0 +1,1160 @@ +#include "waypoints.qh" + +// 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) +{ + entity w; + w = find(world, classname, "waypoint"); + + if (!(f & WAYPOINTFLAG_PERSONAL)) + while (w) + { + // if a matching spawnfunc_waypoint already exists, don't add a duplicate + if (boxesoverlap(m1, m2, w.absmin, w.absmax)) + return w; + w = find(w, classname, "waypoint"); + } + + w = spawn(); + w.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP; + w.classname = "waypoint"; + w.wpflags = f; + 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, PL_MIN - '1 1 0', PL_MAX + '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"); + remove(w); + return world; + } + 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() +{SELFPARAM(); + entity e; + 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(self.wpisbox), "\n"); + sm1 = self.origin + self.mins; + sm2 = self.origin + self.maxs; + for(e = world; (e = find(e, classname, "waypoint")); ) + { + if (boxesoverlap(self.absmin, self.absmax, e.absmin, e.absmax)) + { + waypoint_addlink(self, e); + waypoint_addlink(e, self); + } + else + { + ++relink_total; + if(!checkpvs(self.origin, e)) + { + ++relink_pvsculled; + continue; + } + sv = e.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 = self.origin; + em1 = e.origin + e.mins; + em2 = e.origin + e.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 (vlen(dv) >= 1050) // max search distance in XY + { + ++relink_lengthculled; + continue; + } + navigation_testtracewalk = 0; + if (!self.wpisbox) + { + tracebox(sv - PL_MIN.z * '0 0 1', PL_MIN, PL_MAX, sv, false, self); + if (!trace_startsolid) + { + //dprint("sv deviation", vtos(trace_endpos - sv), "\n"); + sv = trace_endpos + '0 0 1'; + } + } + if (!e.wpisbox) + { + tracebox(ev - PL_MIN.z * '0 0 1', PL_MIN, PL_MAX, ev, false, e); + if (!trace_startsolid) + { + //dprint("ev deviation", vtos(trace_endpos - ev), "\n"); + ev = trace_endpos + '0 0 1'; + } + } + //traceline(self.origin, e.origin, false, world); + //if (trace_fraction == 1) + if (!self.wpisbox && tracewalk(self, sv, PL_MIN, PL_MAX, ev, MOVE_NOMONSTERS)) + waypoint_addlink(self, e); + else + relink_walkculled += 0.5; + if (!e.wpisbox && tracewalk(e, ev, PL_MIN, PL_MAX, sv, MOVE_NOMONSTERS)) + waypoint_addlink(e, self); + else + relink_walkculled += 0.5; + } + } + navigation_testtracewalk = 0; + self.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 = world; + wp.wp08 = wp.wp09 = wp.wp10 = wp.wp11 = wp.wp12 = wp.wp13 = wp.wp14 = wp.wp15 = world; + wp.wp16 = wp.wp17 = wp.wp18 = wp.wp19 = wp.wp20 = wp.wp21 = wp.wp22 = wp.wp23 = world; + wp.wp24 = wp.wp25 = wp.wp26 = wp.wp27 = wp.wp28 = wp.wp29 = wp.wp30 = wp.wp31 = world; + + 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 == world) + 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 = world; + if (!(wp.wpflags & WAYPOINTFLAG_PERSONAL)) + wp.owner = world; + if (!(wp.wpflags & WAYPOINTFLAG_NORELINK)) + waypoint_clearlinks(wp); + // schedule an actual relink on next frame + wp.think = waypoint_think; + wp.nextthink = time; + wp.effects = EF_LOWPRECISION; +} + +// spawnfunc_waypoint map entity +spawnfunc(waypoint) +{ + setorigin(self, self.origin); + // schedule a relink after other waypoints have had a chance to spawn + waypoint_clearlinks(self); + //waypoint_schedulerelink(self); +} + +// 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 + remove(e); +} + +// empties the map of waypoints +void waypoint_removeall() +{ + entity head, next; + head = findchain(classname, "waypoint"); + while (head) + { + next = head.chain; + remove(head); + head = next; + } +} + +// tell all waypoints to relink +// (is this useful at all?) +void waypoint_schedulerelinkall() +{ + entity head; + relink_total = relink_walkculled = relink_pvsculled = relink_lengthculled = 0; + head = findchain(classname, "waypoint"); + while (head) + { + waypoint_schedulerelink(head); + head = head.chain; + } +} + +// Load waypoint links from file +float waypoint_load_links() +{ + string filename, s; + float file, tokens, c = 0, found; + entity wp_from = world, 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(vlen(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(vlen(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 "); + LOG_TRACE(ftos(c)); + LOG_TRACE(" waypoint links from maps/"); + LOG_TRACE(mapname); + LOG_TRACE(".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 = world, 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 "); + LOG_TRACE(filename); + LOG_TRACE(" 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(vlen(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(vlen(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 "); + LOG_TRACE(ftos(c)); + LOG_TRACE(" waypoint links from maps/"); + LOG_TRACE(mapname); + LOG_TRACE(".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 world; + } +} + +// Save all waypoint links to a file +void waypoint_save_links() +{ + string filename, s; + float file, c, i; + entity w, link; + filename = strcat("maps/", mapname); + filename = strcat(filename, ".waypoints.cache"); + file = fopen(filename, FILE_WRITE); + if (file < 0) + { + LOG_INFO("waypoint links save to "); + LOG_INFO(filename); + LOG_INFO(" failed\n"); + } + c = 0; + w = findchain(classname, "waypoint"); + while (w) + { + for(i=0;i<32;++i) + { + // :S + link = waypoint_get_link(w, i); + if(link==world) + continue; + + s = strcat(vtos(w.origin), "*", vtos(link.origin), "\n"); + fputs(file, s); + ++c; + } + w = w.chain; + } + fclose(file); + botframe_cachedwaypointlinks = true; + + LOG_INFO("saved "); + LOG_INFO(ftos(c)); + LOG_INFO(" waypoints links to maps/"); + LOG_INFO(mapname); + LOG_INFO(".waypoints.cache\n"); +} + +// save waypoints to gamedir/data/maps/mapname.waypoints +void waypoint_saveall() +{ + string filename, s; + float file, c; + entity w; + filename = strcat("maps/", mapname); + filename = strcat(filename, ".waypoints"); + file = fopen(filename, FILE_WRITE); + if (file >= 0) + { + c = 0; + w = findchain(classname, "waypoint"); + while (w) + { + if (!(w.wpflags & WAYPOINTFLAG_GENERATED)) + { + s = strcat(vtos(w.origin + w.mins), "\n"); + s = strcat(s, vtos(w.origin + w.maxs)); + s = strcat(s, "\n"); + s = strcat(s, ftos(w.wpflags)); + s = strcat(s, "\n"); + fputs(file, s); + c = c + 1; + } + w = w.chain; + } + fclose(file); + bprint("saved "); + bprint(ftos(c)); + bprint(" waypoints to maps/"); + bprint(mapname); + bprint(".waypoints\n"); + } + else + { + bprint("waypoint save to "); + bprint(filename); + bprint(" failed\n"); + } + waypoint_save_links(); + botframe_loadedforcedlinks = false; +} + +// 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 "); + LOG_TRACE(ftos(cwp)); + LOG_TRACE(" waypoints and "); + LOG_TRACE(ftos(cwb)); + LOG_TRACE(" wayboxes from maps/"); + LOG_TRACE(mapname); + LOG_TRACE(".waypoints\n"); + } + else + { + LOG_TRACE("waypoint load from "); + LOG_TRACE(filename); + LOG_TRACE(" failed\n"); + } + return cwp + cwb; +} + +vector waypoint_fixorigin(vector position) +{ + tracebox(position + '0 0 1' * (1 - PL_MIN.z), PL_MIN, PL_MAX, position + '0 0 -512', MOVE_NOMONSTERS, world); + if(trace_fraction < 1) + position = trace_endpos; + //traceline(position, position + '0 0 -512', MOVE_NOMONSTERS, world); + //print("position is ", ftos(trace_endpos_z - position_z), " above solid\n"); + return position; +} + +void waypoint_spawnforitem_force(entity e, vector org) +{ + entity w; + + // Fix the waypoint altitude if necessary + org = waypoint_fixorigin(org); + + // don't spawn an item spawnfunc_waypoint if it already exists + w = findchain(classname, "waypoint"); + while (w) + { + if (w.wpisbox) + { + if (boxesoverlap(org, org, w.absmin, w.absmax)) + { + e.nearestwaypoint = w; + return; + } + } + else + { + if (vlen(w.origin - org) < 16) + { + e.nearestwaypoint = w; + return; + } + } + w = w.chain; + } + 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(vector position) +{SELFPARAM(); + 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 = world; + w.nearestwaypointtimeout = 0; + w.owner = self; + + waypoint_schedulerelink(w); + + return w; +} + +void botframe_showwaypointlinks() +{ + entity player, head, w; + if (time < botframe_waypointeditorlightningtime) + return; + botframe_waypointeditorlightningtime = time + 0.5; + player = find(world, classname, "player"); + while (player) + { + if (!player.isbot) + if (player.flags & FL_ONGROUND || player.waterlevel > WATERLEVEL_NONE) + { + //navigation_testtracewalk = true; + head = navigation_findnearestwaypoint(player, false); + // print("currently selected WP is ", etos(head), "\n"); + //navigation_testtracewalk = false; + if (head) + { + w = head ;if (w) te_lightning2(world, w.origin, player.origin); + w = head.wp00;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp01;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp02;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp03;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp04;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp05;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp06;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp07;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp08;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp09;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp10;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp11;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp12;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp13;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp14;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp15;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp16;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp17;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp18;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp19;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp20;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp21;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp22;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp23;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp24;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp25;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp26;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp27;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp28;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp29;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp30;if (w) te_lightning2(world, w.origin, head.origin); + w = head.wp31;if (w) te_lightning2(world, w.origin, head.origin); + } + } + player = find(player, classname, "player"); + } +} + +float botframe_autowaypoints_fixdown(vector v) +{ + tracebox(v, PL_MIN, PL_MAX, v + '0 0 -64', MOVE_NOMONSTERS, world); + if(trace_fraction >= 1) + return 0; + return 1; +} + +float botframe_autowaypoints_createwp(vector v, entity p, .entity fld, float f) +{ + entity w; + + w = find(world, classname, "waypoint"); + while (w) + { + // if a matching spawnfunc_waypoint already exists, don't add a duplicate + if (boxesoverlap(v - '32 32 32', v + '32 32 32', w.absmin, w.absmax)) + //if (boxesoverlap(v - '4 4 4', v + '4 4 4', w.absmin, w.absmax)) + return 0; + w = find(w, classname, "waypoint"); + } + + 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 world, 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; + bestdist = maxdist; + w = find(world, classname, "waypoint"); + while (w) + { + if(w != wp && !(w.wpflags & WAYPOINTFLAG_NORELINK)) + { + float d; + d = vlen(wp.origin - w.origin) + vlen(w.origin - porg); + if(d < bestdist) + if(navigation_waypoint_will_link(wp.origin, w.origin, p, walkfromwp, 1050)) + if(navigation_waypoint_will_link(w.origin, porg, p, walkfromwp, 1050)) + { + bestdist = d; + p.(fld) = w; + } + } + w = find(w, classname, "waypoint"); + } + 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, 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, world, 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() +{ + entity w, w1, w2; + float i, j, k; + for (w = world; (w = findfloat(w, bot_pickup, true)); ) + { + // NOTE: this protects waypoints if they're the ONLY nearest + // waypoint. That's the intention. + navigation_findnearestwaypoint(w, false); // Walk TO item. + navigation_findnearestwaypoint(w, true); // Walk FROM item. + } + for (w = world; (w = find(w, classname, "waypoint")); ) + { + w.wpflags |= WAYPOINTFLAG_DEAD_END; + w.wpflags &= ~WAYPOINTFLAG_USEFUL; + // WP is useful if: + if (w.wpflags & WAYPOINTFLAG_ITEM) + w.wpflags |= WAYPOINTFLAG_USEFUL; + if (w.wpflags & WAYPOINTFLAG_TELEPORT) + w.wpflags |= WAYPOINTFLAG_USEFUL; + if (w.wpflags & WAYPOINTFLAG_PROTECTED) + w.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. + for (w1 = world; (w1 = find(w1, classname, "waypoint")); ) + { + if (w1.wpflags & WAYPOINTFLAG_PERSONAL) + continue; + for (i = 0; i < 32; ++i) + { + w = waypoint_get_link(w1, i); + if (!w) + break; + if (w.wpflags & WAYPOINTFLAG_PERSONAL) + continue; + if (w.wpflags & WAYPOINTFLAG_USEFUL) + continue; + for (j = 0; j < 32; ++j) + { + w2 = waypoint_get_link(w, j); + if (!w2) + break; + if (w1 == w2) + continue; + if (w2.wpflags & WAYPOINTFLAG_PERSONAL) + continue; + // If we got here, w1 != w2 exist with w1 -> w + // and w -> w2. That means the waypoint is not + // a dead end. + w.wpflags &= ~WAYPOINTFLAG_DEAD_END; + for (k = 0; k < 32; ++k) + { + if (waypoint_get_link(w1, k) == w2) + continue; + // IF WE GET HERE, w is proven useful + // to get from w1 to w2! + w.wpflags |= WAYPOINTFLAG_USEFUL; + goto next; + } + } +:next + } + } + // d) The waypoint is a dead end. Dead end waypoints must be kept as + // they are needed to complete routes while autowaypointing. + + for (w = world; (w = find(w, classname, "waypoint")); ) + { + if (!(w.wpflags & (WAYPOINTFLAG_USEFUL | WAYPOINTFLAG_DEAD_END))) + { + LOG_INFOF("Removed a waypoint at %v. Try again for more!\n", w.origin); + te_explosion(w.origin); + waypoint_remove(w); + break; + } + } + for (w = world; (w = find(w, classname, "waypoint")); ) + w.wpflags &= ~(WAYPOINTFLAG_USEFUL | WAYPOINTFLAG_DEAD_END); // temp flag +} + +void botframe_autowaypoints() +{ + entity p; + FOR_EACH_REALPLAYER(p) + { + if(p.deadflag) + continue; + // going back is broken, so only fix waypoints to walk TO the player + //botframe_autowaypoints_fix(p, false, botframe_autowaypoints_lastwp0); + botframe_autowaypoints_fix(p, 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..42082c6ab --- /dev/null +++ b/qcsrc/server/bot/default/waypoints.qh @@ -0,0 +1,68 @@ +#ifndef WAYPOINTS_H +#define WAYPOINTS_H +/* + * Globals and Fields + */ +// const int WAYPOINTFLAG_GENERATED = 0x800000; +const int WAYPOINTFLAG_ITEM = 0x400000; +const int WAYPOINTFLAG_TELEPORT = 0x200000; +const int WAYPOINTFLAG_NORELINK = 0x100000; +const int WAYPOINTFLAG_PERSONAL = 0x80000; +const int WAYPOINTFLAG_PROTECTED = 0x40000; // Useless WP detection never kills these. +const int WAYPOINTFLAG_USEFUL = 0x20000; // Useless WP detection temporary flag. +const int WAYPOINTFLAG_DEAD_END = 0x10000; // 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, wpconsidered, wpisbox, wplinked, wphardwired; +.int wpflags; + +.vector wpnearestpoint; + +/* + * Functions + */ + +spawnfunc(waypoint); +void waypoint_addlink(entity from, entity to); +void waypoint_think(); +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(vector position); + +vector waypoint_fixorigin(vector position); + +void botframe_autowaypoints(); +#endif diff --git a/qcsrc/server/bot/havocbot/havocbot.qc b/qcsrc/server/bot/havocbot/havocbot.qc deleted file mode 100644 index f69527916..000000000 --- a/qcsrc/server/bot/havocbot/havocbot.qc +++ /dev/null @@ -1,1324 +0,0 @@ -#include "havocbot.qh" -#include "../../_all.qh" - -#include "../aim.qh" -#include "../bot.qh" -#include "../navigation.qh" -#include "../scripting.qh" -#include "../waypoints.qh" - -#include "../../../common/constants.qh" - -#include "../../../common/triggers/trigger/jumppads.qh" - -#include "../../../warpzonelib/common.qh" - -void havocbot_ai() -{SELFPARAM(); - if(self.draggedby) - return; - - if(bot_execute_commands()) - return; - - if (bot_strategytoken == self) - if (!bot_strategytoken_taken) - { - if(self.havocbot_blockhead) - { - self.havocbot_blockhead = false; - } - else - { - if (!self.jumppadcount) - self.havocbot_role(); - } - - // 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(self.deadflag != DEAD_NO) - if(self.goalcurrent==world) - if(self.waterlevel==WATERLEVEL_SWIMMING || (self.aistatus & AI_STATUS_OUT_WATER)) - { - // Look for the closest waypoint out of water - entity newgoal, head; - float bestdistance, distance; - - newgoal = world; - bestdistance = 10000; - for (head = findchain(classname, "waypoint"); head; head = head.chain) - { - distance = vlen(head.origin - self.origin); - if(distance>10000) - continue; - - if(head.origin.z < self.origin.z) - continue; - - if(head.origin.z - self.origin.z - self.view_ofs.z > 100) - continue; - - if (pointcontents(head.origin + head.maxs + '0 0 1') != CONTENT_EMPTY) - continue; - - traceline(self.origin + self.view_ofs , head.origin, true, head); - - if(trace_fraction<1) - continue; - - if(distance= 2) // bots can only reload the held weapon on purpose past this skill - if(self.clip_load < self.clip_size) - self.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(self.clip_load >= 0) // only if we're not reloading a weapon already - { - for (int i = WEP_FIRST; i <= WEP_LAST; ++i) - { - entity e = get_weaponinfo(i); - if ((self.weapons & WepSet_FromWeapon(i)) && (e.spawnflags & WEP_FLAG_RELOADABLE) && (self.weapon_load[i] < e.reloading_ammo)) - self.switchweapon = i; - } - } - } -} - -void havocbot_keyboard_movement(vector destorg) -{SELFPARAM(); - vector keyboard; - float blend, maxspeed; - float sk; - - sk = skill + self.bot_moveskill; - - maxspeed = autocvar_sv_maxspeed; - - if (time < self.havocbot_keyboardtime) - return; - - self.havocbot_keyboardtime = - max( - self.havocbot_keyboardtime - + 0.05/max(1, sk+self.havocbot_keyboardskill) - + random()*0.025/max(0.00025, skill+self.havocbot_keyboardskill) - , time); - keyboard = self.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; - - self.havocbot_keyboard = keyboard * maxspeed; - if (self.havocbot_ducktime>time) self.BUTTON_CROUCH=true; - - keyboard = self.havocbot_keyboard; - blend = bound(0,vlen(destorg-self.origin)/autocvar_bot_ai_keyboard_distance,1); // When getting close move with 360 degree - //dprint("movement ", vtos(self.movement), " keyboard ", vtos(keyboard), " blend ", ftos(blend), "\n"); - self.movement = self.movement + (keyboard - self.movement) * blend; -} - -void havocbot_bunnyhop(vector dir) -{SELFPARAM(); - float bunnyhopdistance; - vector deviation; - float maxspeed; - vector gco, gno; - - // Don't jump when attacking - if(self.aistatus & AI_STATUS_ATTACKING) - return; - - if(IS_PLAYER(self.goalcurrent)) - return; - - maxspeed = autocvar_sv_maxspeed; - - if(self.aistatus & AI_STATUS_DANGER_AHEAD) - { - self.aistatus &= ~AI_STATUS_RUNNING; - self.BUTTON_JUMP = false; - self.bot_canruntogoal = 0; - self.bot_timelastseengoal = 0; - return; - } - - if(self.waterlevel > WATERLEVEL_WETFEET) - { - self.aistatus &= ~AI_STATUS_RUNNING; - return; - } - - if(self.bot_lastseengoal != self.goalcurrent && !(self.aistatus & AI_STATUS_RUNNING)) - { - self.bot_canruntogoal = 0; - self.bot_timelastseengoal = 0; - } - - gco = (self.goalcurrent.absmin + self.goalcurrent.absmax) * 0.5; - bunnyhopdistance = vlen(self.origin - gco); - - // Run only to visible goals - if(self.flags & FL_ONGROUND) - if(self.speed==maxspeed) - if(checkpvs(self.origin + self.view_ofs, self.goalcurrent)) - { - self.bot_lastseengoal = self.goalcurrent; - - // seen it before - if(self.bot_timelastseengoal) - { - // for a period of time - if(time - self.bot_timelastseengoal > autocvar_bot_ai_bunnyhop_firstjumpdelay) - { - float checkdistance; - checkdistance = true; - - // don't run if it is too close - if(self.bot_canruntogoal==0) - { - if(bunnyhopdistance > autocvar_bot_ai_bunnyhop_startdistance) - self.bot_canruntogoal = 1; - else - self.bot_canruntogoal = -1; - } - - if(self.bot_canruntogoal != 1) - return; - - if(self.aistatus & AI_STATUS_ROAMING) - if(self.goalcurrent.classname=="waypoint") - if (!(self.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL)) - if(fabs(gco.z - self.origin.z) < self.maxs.z - self.mins.z) - if(self.goalstack01!=world) - { - gno = (self.goalstack01.absmin + self.goalstack01.absmax) * 0.5; - deviation = vectoangles(gno - self.origin) - vectoangles(gco - self.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(self.origin - gno)) - if(fabs(gno.z - gco.z) < self.maxs.z - self.mins.z) - { - if(vlen(gco - gno) > autocvar_bot_ai_bunnyhop_startdistance) - if(checkpvs(self.origin + self.view_ofs, self.goalstack01)) - { - checkdistance = false; - } - } - } - - if(checkdistance) - { - self.aistatus &= ~AI_STATUS_RUNNING; - if(bunnyhopdistance > autocvar_bot_ai_bunnyhop_stopdistance) - self.BUTTON_JUMP = true; - } - else - { - self.aistatus |= AI_STATUS_RUNNING; - self.BUTTON_JUMP = true; - } - } - } - else - { - self.bot_timelastseengoal = time; - } - } - else - { - self.bot_timelastseengoal = 0; - } - -#if 0 - // Release jump button - if(!cvar("sv_pogostick")) - if((self.flags & FL_ONGROUND) == 0) - { - if(self.velocity.z < 0 || vlen(self.velocity)maxspeed) - { - deviation = vectoangles(dir) - vectoangles(self.velocity); - while (deviation.y < -180) deviation.y = deviation.y + 360; - while (deviation.y > 180) deviation.y = deviation.y - 360; - - if(fabs(deviation.y)>10) - self.movement_x = 0; - - if(deviation.y>10) - self.movement_y = maxspeed * -1; - else if(deviation.y<10) - self.movement_y = maxspeed; - - } - } -#endif -} - -void havocbot_movetogoal() -{SELFPARAM(); - 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 (self.goalentity) - // te_lightning2(self, self.origin, (self.goalentity.absmin + self.goalentity.absmax) * 0.5); - self.movement = '0 0 0'; - maxspeed = autocvar_sv_maxspeed; - - // Jetpack navigation - if(self.goalcurrent) - if(self.navigation_jetpack_goal) - if(self.goalcurrent==self.navigation_jetpack_goal) - if(self.ammo_fuel) - { - if(autocvar_bot_debug_goalstack) - { - debuggoalstack(); - te_wizspike(self.navigation_jetpack_point); - } - - // Take off - if (!(self.aistatus & AI_STATUS_JETPACK_FLYING)) - { - // Brake almost completely so it can get a good direction - if(vlen(self.velocity)>10) - return; - self.aistatus |= AI_STATUS_JETPACK_FLYING; - } - - makevectors(self.v_angle.y * '0 1 0'); - dir = normalize(self.navigation_jetpack_point - self.origin); - - // Landing - if(self.aistatus & AI_STATUS_JETPACK_LANDING) - { - // Calculate brake distance in xy - float db, v, d; - vector dxy; - - dxy = self.origin - ( ( self.goalcurrent.absmin + self.goalcurrent.absmax ) * 0.5 ); dxy.z = 0; - d = vlen(dxy); - v = vlen(self.velocity - self.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(self.velocity.x)>maxspeed*0.3) - { - self.movement_x = dir * v_forward * -maxspeed; - return; - } - // Switch to normal mode - self.navigation_jetpack_goal = world; - self.aistatus &= ~AI_STATUS_JETPACK_LANDING; - self.aistatus &= ~AI_STATUS_JETPACK_FLYING; - return; - } - } - else if(checkpvs(self.origin,self.goalcurrent)) - { - // If I can see the goal switch to landing code - self.aistatus &= ~AI_STATUS_JETPACK_FLYING; - self.aistatus |= AI_STATUS_JETPACK_LANDING; - return; - } - - // Flying - self.BUTTON_HOOK = true; - if(self.navigation_jetpack_point.z - PL_MAX.z + PL_MIN.z < self.origin.z) - { - self.movement_x = dir * v_forward * maxspeed; - self.movement_y = dir * v_right * maxspeed; - } - return; - } - - // Handling of jump pads - if(self.jumppadcount) - { - // If got stuck on the jump pad try to reach the farthest visible waypoint - if(self.aistatus & AI_STATUS_OUT_JUMPPAD) - { - if(fabs(self.velocity.z)<50) - { - entity head, newgoal = world; - float distance, bestdistance = 0; - - for (head = findchain(classname, "waypoint"); head; head = head.chain) - { - - distance = vlen(head.origin - self.origin); - if(distance>1000) - continue; - - traceline(self.origin + self.view_ofs , ( ( head.absmin + head.absmax ) * 0.5 ), true, world); - - if(trace_fraction<1) - continue; - - if(distance>bestdistance) - { - newgoal = head; - bestdistance = distance; - } - } - - if(newgoal) - { - self.ignoregoal = self.goalcurrent; - self.ignoregoaltime = time + autocvar_bot_ai_ignoregoal_timeout; - navigation_clearroute(); - navigation_routetogoal(newgoal, self.origin); - self.aistatus &= ~AI_STATUS_OUT_JUMPPAD; - } - } - else - return; - } - else - { - if(self.velocity.z>0) - { - float threshold, sxy; - vector velxy = self.velocity; velxy_z = 0; - sxy = vlen(velxy); - threshold = maxspeed * 0.2; - if(sxy < threshold) - { - LOG_TRACE("Warning: ", self.netname, " got stuck on a jumppad (velocity in xy is ", ftos(sxy), "), trying to get out of it now\n"); - self.aistatus |= AI_STATUS_OUT_JUMPPAD; - } - return; - } - - // Don't chase players while using a jump pad - if(IS_PLAYER(self.goalcurrent) || IS_PLAYER(self.goalstack01)) - return; - } - } - else if(self.aistatus & AI_STATUS_OUT_JUMPPAD) - self.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 (!(self.flags & FL_ONGROUND)) - { - tracebox(self.origin, self.mins, self.maxs, self.origin + '0 0 -65536', MOVE_NOMONSTERS, self); - if(tracebox_hits_trigger_hurt(self.origin, self.mins, self.maxs, trace_endpos )) - if(self.items & IT_JETPACK) - { - tracebox(self.origin, self.mins, self.maxs, self.origin + '0 0 65536', MOVE_NOMONSTERS, self); - if(tracebox_hits_trigger_hurt(self.origin, self.mins, self.maxs, trace_endpos + '0 0 1' )) - { - if(self.velocity.z<0) - { - self.BUTTON_HOOK = true; - } - } - else - self.BUTTON_HOOK = true; - - // If there is no goal try to move forward - - if(self.goalcurrent==world) - dir = v_forward; - else - dir = normalize(( ( self.goalcurrent.absmin + self.goalcurrent.absmax ) * 0.5 ) - self.origin); - - vector xyvelocity = self.velocity; xyvelocity_z = 0; - float xyspeed = xyvelocity * dir; - - if(xyspeed < (maxspeed / 2)) - { - makevectors(self.v_angle.y * '0 1 0'); - tracebox(self.origin, self.mins, self.maxs, self.origin + (dir * maxspeed * 3), MOVE_NOMONSTERS, self); - if(trace_fraction==1) - { - self.movement_x = dir * v_forward * maxspeed; - self.movement_y = dir * v_right * maxspeed; - if (skill < 10) - havocbot_keyboard_movement(self.origin + dir * 100); - } - } - - self.havocbot_blockhead = true; - - return; - } - else if(self.health>WEP_CVAR(devastator, damage)*0.5) - { - if(self.velocity.z < 0) - if(client_hasweapon(self, WEP_DEVASTATOR.m_id, true, false)) - { - self.movement_x = maxspeed; - - if(self.rocketjumptime) - { - if(time > self.rocketjumptime) - { - self.BUTTON_ATCK2 = true; - self.rocketjumptime = 0; - } - return; - } - - self.switchweapon = WEP_DEVASTATOR.m_id; - self.v_angle_x = 90; - self.BUTTON_ATCK = true; - self.rocketjumptime = time + WEP_CVAR(devastator, detonatedelay); - return; - } - } - else - { - // If there is no goal try to move forward - if(self.goalcurrent==world) - self.movement_x = maxspeed; - } - } - - // If we are under water with no goals, swim up - if(self.waterlevel) - if(self.goalcurrent==world) - { - dir = '0 0 0'; - if(self.waterlevel>WATERLEVEL_SWIMMING) - dir.z = 1; - else if(self.velocity.z >= 0 && !(self.waterlevel == WATERLEVEL_WETFEET && self.watertype == CONTENT_WATER)) - self.BUTTON_JUMP = true; - else - self.BUTTON_JUMP = false; - makevectors(self.v_angle.y * '0 1 0'); - self.movement_x = dir * v_forward * maxspeed; - self.movement_y = dir * v_right * maxspeed; - self.movement_z = dir * v_up * maxspeed; - } - - // if there is nowhere to go, exit - if (self.goalcurrent == world) - return; - - if (self.goalcurrent) - navigation_poptouchedgoals(); - - // if ran out of goals try to use an alternative goal or get a new strategy asap - if(self.goalcurrent == world) - { - self.bot_strategytime = 0; - return; - } - - - if(autocvar_bot_debug_goalstack) - debuggoalstack(); - - m1 = self.goalcurrent.origin + self.goalcurrent.mins; - m2 = self.goalcurrent.origin + self.goalcurrent.maxs; - destorg = self.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 - self.origin; - //dist = vlen(diff); - dir = normalize(diff); - flatdir = diff;flatdir.z = 0; - flatdir = normalize(flatdir); - gco = (self.goalcurrent.absmin + self.goalcurrent.absmax) * 0.5; - - //if (self.bot_dodgevector_time < time) - { - // self.bot_dodgevector_time = time + cvar("bot_ai_dodgeupdateinterval"); - // self.bot_dodgevector_jumpbutton = 1; - evadeobstacle = '0 0 0'; - evadelava = '0 0 0'; - - if (self.waterlevel) - { - if(self.waterlevel>WATERLEVEL_SWIMMING) - { - // flatdir_z = 1; - self.aistatus |= AI_STATUS_OUT_WATER; - } - else - { - if(self.velocity.z >= 0 && !(self.watertype == CONTENT_WATER && gco.z < self.origin.z) && - ( !(self.waterlevel == WATERLEVEL_WETFEET && self.watertype == CONTENT_WATER) || self.aistatus & AI_STATUS_OUT_WATER)) - self.BUTTON_JUMP = true; - else - self.BUTTON_JUMP = false; - } - dir = normalize(flatdir); - makevectors(self.v_angle.y * '0 1 0'); - } - else - { - if(self.aistatus & AI_STATUS_OUT_WATER) - self.aistatus &= ~AI_STATUS_OUT_WATER; - - // jump if going toward an obstacle that doesn't look like stairs we - // can walk up directly - tracebox(self.origin, self.mins, self.maxs, self.origin + self.velocity * 0.2, false, self); - if (trace_fraction < 1) - if (trace_plane_normal.z < 0.7) - { - s = trace_fraction; - tracebox(self.origin + stepheightvec, self.mins, self.maxs, self.origin + self.velocity * 0.2 + stepheightvec, false, self); - if (trace_fraction < s + 0.01) - if (trace_plane_normal.z < 0.7) - { - s = trace_fraction; - tracebox(self.origin + jumpstepheightvec, self.mins, self.maxs, self.origin + self.velocity * 0.2 + jumpstepheightvec, false, self); - if (trace_fraction > s) - self.BUTTON_JUMP = 1; - } - } - - // avoiding dangers and obstacles - vector dst_ahead, dst_down; - makevectors(self.v_angle.y * '0 1 0'); - dst_ahead = self.origin + self.view_ofs + (self.velocity * 0.4) + (v_forward * 32 * 3); - dst_down = dst_ahead - '0 0 1500'; - - // Look ahead - traceline(self.origin + self.view_ofs, dst_ahead, true, world); - - // Check head-banging against walls - if(vlen(self.origin + self.view_ofs - trace_endpos) < 25 && !(self.aistatus & AI_STATUS_OUT_WATER)) - { - self.BUTTON_JUMP = true; - if(self.facingwalltime && time > self.facingwalltime) - { - self.ignoregoal = self.goalcurrent; - self.ignoregoaltime = time + autocvar_bot_ai_ignoregoal_timeout; - self.bot_strategytime = 0; - return; - } - else - { - self.facingwalltime = time + 0.05; - } - } - else - { - self.facingwalltime = 0; - - if(self.ignoregoal != world && time > self.ignoregoaltime) - { - self.ignoregoal = world; - self.ignoregoaltime = 0; - } - } - - // Check for water/slime/lava and dangerous edges - // (only when the bot is on the ground or jumping intentionally) - self.aistatus &= ~AI_STATUS_DANGER_AHEAD; - - if(trace_fraction == 1 && self.jumppadcount == 0 && !self.goalcurrent.wphardwired ) - if((self.flags & FL_ONGROUND) || (self.aistatus & AI_STATUS_RUNNING) || self.BUTTON_JUMP == true) - { - // Look downwards - traceline(dst_ahead , dst_down, true, world); - // te_lightning2(world, self.origin, dst_ahead); // Draw "ahead" look - // te_lightning2(world, dst_ahead, dst_down); // Draw "downwards" look - if(trace_endpos.z < self.origin.z + self.mins.z) - { - s = pointcontents(trace_endpos + '0 0 1'); - if (s != CONTENT_SOLID) - if (s == CONTENT_LAVA || s == CONTENT_SLIME) - evadelava = normalize(self.velocity) * -1; - else if (s == CONTENT_SKY) - evadeobstacle = normalize(self.velocity) * -1; - else if (!boxesoverlap(dst_ahead - self.view_ofs + self.mins, dst_ahead - self.view_ofs + self.maxs, - self.goalcurrent.absmin, self.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, self.mins, self.maxs, trace_endpos)) - { - // Remove dangerous dynamic goals from stack - LOG_TRACE("bot ", self.netname, " avoided the goal ", self.goalcurrent.classname, " ", etos(self.goalcurrent), " because it led to a dangerous path; goal stack cleared\n"); - navigation_clearroute(); - return; - } - } - } - } - - dir = flatdir; - evadeobstacle.z = 0; - evadelava.z = 0; - makevectors(self.v_angle.y * '0 1 0'); - - if(evadeobstacle!='0 0 0'||evadelava!='0 0 0') - self.aistatus |= AI_STATUS_DANGER_AHEAD; - } - - dodge = havocbot_dodge(); - dodge = dodge * bound(0,0.5+(skill+self.bot_dodgeskill)*0.1,1); - evadelava = evadelava * bound(1,3-(skill+self.bot_dodgeskill),3); //Noobs fear lava a lot and take more distance from it - traceline(self.origin, ( ( self.enemy.absmin + self.enemy.absmax ) * 0.5 ), true, world); - if(IS_PLAYER(trace_ent)) - dir = dir * bound(0,(skill+self.bot_dodgeskill)/7,1); - - dir = normalize(dir + dodge + evadeobstacle + evadelava); - // self.bot_dodgevector = dir; - // self.bot_dodgevector_jumpbutton = self.BUTTON_JUMP; - } - - if(time < self.ladder_time) - { - if(self.goalcurrent.origin.z + self.goalcurrent.mins.z > self.origin.z + self.mins.z) - { - if(self.origin.z + self.mins.z < self.ladder_entity.origin.z + self.ladder_entity.maxs.z) - dir.z = 1; - } - else - { - if(self.origin.z + self.mins.z > self.ladder_entity.origin.z + self.ladder_entity.mins.z) - dir.z = -1; - } - } - - //dir = self.bot_dodgevector; - //if (self.bot_dodgevector_jumpbutton) - // self.BUTTON_JUMP = 1; - self.movement_x = dir * v_forward * maxspeed; - self.movement_y = dir * v_right * maxspeed; - self.movement_z = dir * v_up * maxspeed; - - // Emulate keyboard interface - if (skill < 10) - havocbot_keyboard_movement(destorg); - - // Bunnyhop! -// if(self.aistatus & AI_STATUS_ROAMING) - if(self.goalcurrent) - if(skill+self.bot_moveskill >= autocvar_bot_ai_bunnyhop_skilloffset) - havocbot_bunnyhop(dir); - - if ((dir * v_up) >= autocvar_sv_jumpvelocity*0.5 && (self.flags & FL_ONGROUND)) self.BUTTON_JUMP=1; - if (((dodge * v_up) > 0) && random()*frametime >= 0.2*bound(0,(10-skill-self.bot_dodgeskill)*0.1,1)) self.BUTTON_JUMP=true; - if (((dodge * v_up) < 0) && random()*frametime >= 0.5*bound(0,(10-skill-self.bot_dodgeskill)*0.1,1)) self.havocbot_ducktime=time+0.3/bound(0.1,skill+self.bot_dodgeskill,10); -} - -void havocbot_chooseenemy() -{SELFPARAM(); - entity head, best, head2; - float rating, bestrating, hf; - vector eye, v; - if (autocvar_bot_nofire || IS_INDEPENDENT_PLAYER(self)) - { - self.enemy = world; - return; - } - if (self.enemy) - { - if (!bot_shouldattack(self.enemy)) - { - // enemy died or something, find a new target - self.enemy = world; - self.havocbot_chooseenemy_finished = time; - } - else if (self.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(self.origin+self.view_ofs, ( self.enemy.absmin + self.enemy.absmax ) * 0.5,false,world); - if (trace_ent == self.enemy || trace_fraction == 1) - if (vlen((( self.enemy.absmin + self.enemy.absmax ) * 0.5) - self.origin) < 1000) - if (self.health > 30) - { - // remain tracking him for a shot while (case he went after a small corner or pilar - self.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) - self.havocbot_stickenemy = 0; - } - } - if (time < self.havocbot_chooseenemy_finished) - return; - self.havocbot_chooseenemy_finished = time + autocvar_bot_ai_enemydetectioninterval; - eye = self.origin + self.view_ofs; - best = world; - bestrating = 100000000; - head = head2 = findchainfloat(bot_attack, true); - - // Backup hit flags - hf = self.dphitcontentsmask; - - // Search for enemies, if no enemy can be seen directly try to look through transparent objects - - self.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; - :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(head)) - { - traceline(eye, v, true, self); - 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 || self.weapons) // || self.weapon == WEP_RIFLE.m_id - break; - if(scan_transparent) - break; - - // Set flags to see through transparent objects - self.dphitcontentsmask |= DPCONTENTS_OPAQUE; - - head = head2; - scan_transparent = true; - } - - // Restore hit flags - self.dphitcontentsmask = hf; - - self.enemy = best; - self.havocbot_stickenemy = true; - if(best && best.classname == "misc_breakablemodel") - self.havocbot_stickenemy = false; -} - -float havocbot_chooseweapon_checkreload(int new_weapon) -{SELFPARAM(); - // 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 (self.weapon_load[new_weapon] < 0) - { - float i, other_weapon_available = false; - for(i = WEP_FIRST; i <= WEP_LAST; ++i) - { - Weapon w = get_weaponinfo(i); - // if we are out of ammo for all other weapons, it's an emergency to switch to anything else - if (w.wr_checkammo1(w) + w.wr_checkammo2(w)) - other_weapon_available = true; - } - if(other_weapon_available) - return true; - } - - return false; -} - -void havocbot_chooseweapon() -{SELFPARAM(); - int i; - - // ;) - if(g_weaponarena_weapons == WEPSET(TUBA)) - { - self.switchweapon = WEP_TUBA.m_id; - return; - } - - // TODO: clean this up by moving it to weapon code - if(self.enemy==world) - { - // If no weapon was chosen get the first available weapon - if(self.weapon==0) - for(i = WEP_FIRST; i <= WEP_LAST; ++i) if(i != WEP_BLASTER.m_id) - { - if(client_hasweapon(self, i, true, false)) - { - self.switchweapon = i; - return; - } - } - return; - } - - // Do not change weapon during the next second after a combo - float f = time - self.lastcombotime; - if(f < 1) - return; - - float w; - float distance; distance=bound(10,vlen(self.origin-self.enemy.origin)-200,10000); - - // Should it do a weapon combo? - float af, ct, combo_time, combo; - - af = ATTACK_FINISHED(self); - 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+self.bot_weaponskill))+3)); - - combo = false; - - if(autocvar_bot_ai_weapon_combo) - if(self.weapon == self.lastfiredweapon) - if(af > combo_time) - { - combo = true; - self.lastcombotime = time; - } - - distance *= pow(2, self.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(self, w, true, false) ) - { - if ((self.weapon == w && combo) || havocbot_chooseweapon_checkreload(w)) - continue; - self.switchweapon = 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(self, w, true, false) ) - { - if ((self.weapon == w && combo) || havocbot_chooseweapon_checkreload(w)) - continue; - self.switchweapon = 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(self, w, true, false) ) - { - if ((self.weapon == w && combo) || havocbot_chooseweapon_checkreload(w)) - continue; - self.switchweapon = w; - return; - } - } - } -} - -void havocbot_aim() -{SELFPARAM(); - vector selfvel, enemyvel; -// if(self.flags & FL_INWATER) -// return; - if (time < self.nextaim) - return; - self.nextaim = time + 0.1; - selfvel = self.velocity; - if (!self.waterlevel) - selfvel.z = 0; - if (self.enemy) - { - enemyvel = self.enemy.velocity; - if (!self.enemy.waterlevel) - enemyvel.z = 0; - lag_additem(time + self.ping, 0, 0, self.enemy, self.origin, selfvel, (self.enemy.absmin + self.enemy.absmax) * 0.5, enemyvel); - } - else - lag_additem(time + self.ping, 0, 0, world, self.origin, selfvel, ( self.goalcurrent.absmin + self.goalcurrent.absmax ) * 0.5, '0 0 0'); -} - -float havocbot_moveto_refresh_route() -{SELFPARAM(); - // Refresh path to goal if necessary - entity wp; - wp = self.havocbot_personal_waypoint; - navigation_goalrating_start(); - navigation_routerating(wp, 10000, 10000); - navigation_goalrating_end(); - return self.navigation_hasgoals; -} - -float havocbot_moveto(vector pos) -{SELFPARAM(); - entity wp; - - if(self.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) - { - // Step 4: Move to waypoint - if(self.havocbot_personal_waypoint==world) - { - LOG_TRACE("Error: ", self.netname, " trying to walk to a non existent personal waypoint\n"); - self.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING; - return CMD_STATUS_ERROR; - } - - if (!bot_strategytoken_taken) - if(self.havocbot_personal_waypoint_searchtime= 30) - { - LOG_TRACE("Warning: can't walk to the personal waypoint located at ", vtos(self.havocbot_personal_waypoint.origin),"\n"); - self.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_LINKING; - remove(self.havocbot_personal_waypoint); - return CMD_STATUS_ERROR; - } - else - LOG_TRACE(self.netname, " can't walk to its personal waypoint (after ", ftos(self.havocbot_personal_waypoint_failcounter), " failed attempts), trying later\n"); - } - } - - if(autocvar_bot_debug_goalstack) - debuggoalstack(); - - // Heading - vector dir = ( ( self.goalcurrent.absmin + self.goalcurrent.absmax ) * 0.5 ) - (self.origin + self.view_ofs); - dir.z = 0; - bot_aimdir(dir, -1); - - // Go! - havocbot_movetogoal(); - - if(self.aistatus & AI_STATUS_WAYPOINT_PERSONAL_REACHED) - { - // Step 5: Waypoint reached - LOG_TRACE(self.netname, "'s personal waypoint reached\n"); - remove(self.havocbot_personal_waypoint); - self.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_REACHED; - return CMD_STATUS_FINISHED; - } - - return CMD_STATUS_EXECUTING; - } - - // Step 2: Linking waypoint - if(self.aistatus & AI_STATUS_WAYPOINT_PERSONAL_LINKING) - { - // Wait until it is linked - if(!self.havocbot_personal_waypoint.wplinked) - { - LOG_TRACE(self.netname, " waiting for personal waypoint to be linked\n"); - return CMD_STATUS_EXECUTING; - } - - self.havocbot_personal_waypoint_searchtime = time; // so we set the route next frame - self.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_LINKING; - self.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_GOING; - - // Step 3: Route to waypoint - LOG_TRACE(self.netname, " walking to its personal waypoint\n"); - - return CMD_STATUS_EXECUTING; - } - - // Step 1: Spawning waypoint - wp = waypoint_spawnpersonal(pos); - if(wp==world) - { - LOG_TRACE("Error: Can't spawn personal waypoint at ",vtos(pos),"\n"); - return CMD_STATUS_ERROR; - } - - self.havocbot_personal_waypoint = wp; - self.havocbot_personal_waypoint_failcounter = 0; - self.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_LINKING; - - // if pos is inside a teleport, then let's mark it as teleport waypoint - entity head; - for(head = world; (head = find(head, classname, "trigger_teleport")); ) - { - if(WarpZoneLib_BoxTouchesBrush(pos, pos, head, world)) - { - wp.wpflags |= WAYPOINTFLAG_TELEPORT; - self.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() -{ - navigation_clearroute(); - return CMD_STATUS_FINISHED; -} - -void havocbot_setupbot() -{SELFPARAM(); - self.bot_ai = havocbot_ai; - self.cmd_moveto = havocbot_moveto; - self.cmd_resetgoal = havocbot_resetgoal; - - havocbot_chooserole(); -} - -vector havocbot_dodge() -{SELFPARAM(); - // 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 != self) - { - vl = vlen(head.velocity); - if (vl > autocvar_sv_maxspeed * 0.3) - { - n = normalize(head.velocity); - v = self.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 - self.origin); - if (bestdanger < danger) - { - bestdanger = danger; - dodge = normalize(self.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 2d3d32913..000000000 --- a/qcsrc/server/bot/havocbot/havocbot.qh +++ /dev/null @@ -1,66 +0,0 @@ -#ifndef HAVOCBOT_H -#define HAVOCBOT_H - -/* - * 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(); -void havocbot_aim(); -void havocbot_setupbot(); -void havocbot_movetogoal(); -void havocbot_chooserole(); -void havocbot_chooseenemy(); -void havocbot_chooseweapon(); -void havocbot_bunnyhop(vector dir); -void havocbot_keyboard_movement(vector destorg); - -float havocbot_resetgoal(); -float havocbot_moveto(vector pos); -float havocbot_moveto_refresh_route(); - -vector havocbot_dodge(); - -.void() havocbot_role; -.void() havocbot_previous_role; - -void(float ratingscale, vector org, float sradius) havocbot_goalrating_items; -void(float ratingscale, vector org, float sradius) havocbot_goalrating_enemyplayers; - -/* - * Imports - */ - -.entity draggedby; -.float ladder_time; -.entity ladder_entity; -#endif diff --git a/qcsrc/server/bot/havocbot/role_keyhunt.qc b/qcsrc/server/bot/havocbot/role_keyhunt.qc deleted file mode 100644 index b1603c792..000000000 --- a/qcsrc/server/bot/havocbot/role_keyhunt.qc +++ /dev/null @@ -1,220 +0,0 @@ -#include "role_keyhunt.qh" -#include "../../_all.qh" - -#include "havocbot.qh" - -#include "../bot.qh" -#include "../navigation.qh" - -#include "../../mutators/mutators_include.qh" - -void() havocbot_role_kh_carrier; -void() havocbot_role_kh_defense; -void() havocbot_role_kh_offense; -void() havocbot_role_kh_freelancer; - - -void havocbot_goalrating_kh(float ratingscale_team, float ratingscale_dropped, float ratingscale_enemy) -{SELFPARAM(); - entity head; - for (head = kh_worldkeylist; head; head = head.kh_worldkeynext) - { - if(head.owner == self) - continue; - if(!kh_tracking_enabled) - { - // if it's carried by our team we know about it - // otherwise we have to see it to know about it - if(!head.owner || head.team != self.team) - { - traceline(self.origin + self.view_ofs, head.origin, MOVE_NOMONSTERS, self); - if (trace_fraction < 1 && trace_ent != head) - continue; // skip what I can't see - } - } - if(!head.owner) - navigation_routerating(head, ratingscale_dropped * BOT_PICKUP_RATING_HIGH, 100000); - else if(head.team == self.team) - navigation_routerating(head.owner, ratingscale_team * BOT_PICKUP_RATING_HIGH, 100000); - else - navigation_routerating(head.owner, ratingscale_enemy * BOT_PICKUP_RATING_HIGH, 100000); - } - - havocbot_goalrating_items(1, self.origin, 10000); -} - -void havocbot_role_kh_carrier() -{SELFPARAM(); - if(self.deadflag != DEAD_NO) - return; - - if (!(self.kh_next)) - { - LOG_TRACE("changing role to freelancer\n"); - self.havocbot_role = havocbot_role_kh_freelancer; - self.havocbot_role_timeout = 0; - return; - } - - if (self.bot_strategytime < time) - { - self.bot_strategytime = time + autocvar_bot_ai_strategyinterval; - navigation_goalrating_start(); - - if(kh_Key_AllOwnedByWhichTeam() == self.team) - havocbot_goalrating_kh(10, 0.1, 0.1); // bring home - else - havocbot_goalrating_kh(4, 4, 1); // play defensively - - navigation_goalrating_end(); - } -} - -void havocbot_role_kh_defense() -{SELFPARAM(); - if(self.deadflag != DEAD_NO) - return; - - if (self.kh_next) - { - LOG_TRACE("changing role to carrier\n"); - self.havocbot_role = havocbot_role_kh_carrier; - self.havocbot_role_timeout = 0; - return; - } - - if (!self.havocbot_role_timeout) - self.havocbot_role_timeout = time + random() * 10 + 20; - if (time > self.havocbot_role_timeout) - { - LOG_TRACE("changing role to freelancer\n"); - self.havocbot_role = havocbot_role_kh_freelancer; - self.havocbot_role_timeout = 0; - return; - } - - if (self.bot_strategytime < time) - { - float key_owner_team; - self.bot_strategytime = time + autocvar_bot_ai_strategyinterval; - navigation_goalrating_start(); - - key_owner_team = kh_Key_AllOwnedByWhichTeam(); - if(key_owner_team == self.team) - havocbot_goalrating_kh(10, 0.1, 0.1); // defend key carriers - else if(key_owner_team == -1) - havocbot_goalrating_kh(4, 1, 0.1); // play defensively - else - havocbot_goalrating_kh(0.1, 0.1, 10); // ATTACK ANYWAY - - navigation_goalrating_end(); - } -} - -void havocbot_role_kh_offense() -{SELFPARAM(); - if(self.deadflag != DEAD_NO) - return; - - if (self.kh_next) - { - LOG_TRACE("changing role to carrier\n"); - self.havocbot_role = havocbot_role_kh_carrier; - self.havocbot_role_timeout = 0; - return; - } - - if (!self.havocbot_role_timeout) - self.havocbot_role_timeout = time + random() * 10 + 20; - if (time > self.havocbot_role_timeout) - { - LOG_TRACE("changing role to freelancer\n"); - self.havocbot_role = havocbot_role_kh_freelancer; - self.havocbot_role_timeout = 0; - return; - } - - if (self.bot_strategytime < time) - { - float key_owner_team; - - self.bot_strategytime = time + autocvar_bot_ai_strategyinterval; - navigation_goalrating_start(); - - key_owner_team = kh_Key_AllOwnedByWhichTeam(); - if(key_owner_team == self.team) - havocbot_goalrating_kh(10, 0.1, 0.1); // defend anyway - else if(key_owner_team == -1) - havocbot_goalrating_kh(0.1, 1, 4); // play offensively - else - havocbot_goalrating_kh(0.1, 0.1, 10); // ATTACK! EMERGENCY! - - navigation_goalrating_end(); - } -} - -void havocbot_role_kh_freelancer() -{SELFPARAM(); - if(self.deadflag != DEAD_NO) - return; - - if (self.kh_next) - { - LOG_TRACE("changing role to carrier\n"); - self.havocbot_role = havocbot_role_kh_carrier; - self.havocbot_role_timeout = 0; - return; - } - - if (!self.havocbot_role_timeout) - self.havocbot_role_timeout = time + random() * 10 + 10; - if (time > self.havocbot_role_timeout) - { - if (random() < 0.5) - { - LOG_TRACE("changing role to offense\n"); - self.havocbot_role = havocbot_role_kh_offense; - } - else - { - LOG_TRACE("changing role to defense\n"); - self.havocbot_role = havocbot_role_kh_defense; - } - self.havocbot_role_timeout = 0; - return; - } - - if (self.bot_strategytime < time) - { - float key_owner_team; - - self.bot_strategytime = time + autocvar_bot_ai_strategyinterval; - navigation_goalrating_start(); - - key_owner_team = kh_Key_AllOwnedByWhichTeam(); - if(key_owner_team == self.team) - havocbot_goalrating_kh(10, 0.1, 0.1); // defend anyway - else if(key_owner_team == -1) - havocbot_goalrating_kh(1, 10, 4); // prefer dropped keys - else - havocbot_goalrating_kh(0.1, 0.1, 10); // ATTACK ANYWAY - - navigation_goalrating_end(); - } -} - -void havocbot_chooserole_kh() -{SELFPARAM(); - float r; - - if(self.deadflag != DEAD_NO) - return; - - r = random() * 3; - if (r < 1) - self.havocbot_role = havocbot_role_kh_offense; - else if (r < 2) - self.havocbot_role = havocbot_role_kh_defense; - else - self.havocbot_role = havocbot_role_kh_freelancer; -} diff --git a/qcsrc/server/bot/havocbot/role_keyhunt.qh b/qcsrc/server/bot/havocbot/role_keyhunt.qh deleted file mode 100644 index f3136c630..000000000 --- a/qcsrc/server/bot/havocbot/role_keyhunt.qh +++ /dev/null @@ -1,8 +0,0 @@ -#ifndef ROLE_KEYHUNT_H -#define ROLE_KEYHUNT_H - -entity kh_worldkeylist; -.entity kh_worldkeynext; - -void havocbot_chooserole_kh(); -#endif diff --git a/qcsrc/server/bot/havocbot/roles.qc b/qcsrc/server/bot/havocbot/roles.qc deleted file mode 100644 index 037968f13..000000000 --- a/qcsrc/server/bot/havocbot/roles.qc +++ /dev/null @@ -1,247 +0,0 @@ -#include "../../_all.qh" - -#include "havocbot.qh" -#include "role_keyhunt.qh" - -#include "../bot.qh" -#include "../navigation.qh" - -.float max_armorvalue; -.float havocbot_role_timeout; - -.void() havocbot_previous_role; -.void() havocbot_role; - -void havocbot_goalrating_items(float ratingscale, vector org, float sradius) -{SELFPARAM(); - entity head; - entity player; - float rating, d, discard, distance, friend_distance, enemy_distance; - vector o; - ratingscale = ratingscale * 0.0001; // items are rated around 10000 already - head = findchainfloat(bot_pickup, true); - - while (head) - { - o = (head.absmin + head.absmax) * 0.5; - distance = vlen(o - org); - friend_distance = 10000; enemy_distance = 10000; - rating = 0; - - if(!head.solid || distance > sradius || (head == self.ignoregoal && time < self.ignoregoaltime) ) - { - head = head.chain; - continue; - } - - // Check if the item can be picked up safely - if(head.classname == "droppedweapon") - { - traceline(o, o + '0 0 -1500', true, world); - - d = pointcontents(trace_endpos + '0 0 1'); - if(d & CONTENT_WATER || d & CONTENT_SLIME || d & CONTENT_LAVA) - { - head = head.chain; - continue; - } - if(tracebox_hits_trigger_hurt(head.origin, head.mins, head.maxs, trace_endpos)) - { - head = head.chain; - continue; - } - } - else - { - // Ignore items under water - traceline(head.origin + head.maxs, head.origin + head.maxs, MOVE_NORMAL, head); - if(trace_dpstartcontents & DPCONTENTS_LIQUIDSMASK) - { - head = head.chain; - continue; - } - } - - if(teamplay) - { - discard = false; - - FOR_EACH_PLAYER(player) - { - - if ( self == player || player.deadflag ) - continue; - - d = vlen(player.origin - o); // distance between player and item - - if ( player.team == self.team ) - { - if ( !IS_REAL_CLIENT(player) || discard ) - continue; - - if( d > friend_distance) - continue; - - friend_distance = d; - - discard = true; - - if( head.health && player.health > self.health ) - continue; - - if( head.armorvalue && player.armorvalue > self.armorvalue) - continue; - - if( head.weapons ) - if( head.weapons & ~player.weapons ) - continue; - - if (head.ammo_shells && player.ammo_shells > self.ammo_shells) - continue; - - if (head.ammo_nails && player.ammo_nails > self.ammo_nails) - continue; - - if (head.ammo_rockets && player.ammo_rockets > self.ammo_rockets) - continue; - - if (head.ammo_cells && player.ammo_cells > self.ammo_cells) - continue; - - if (head.ammo_plasma && player.ammo_plasma > self.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 && distance < enemy_distance) || - (friend_distance > autocvar_bot_ai_friends_aware_pickup_radius ) || !discard ) - rating = head.bot_pickupevalfunc(self, head); - - } - else - rating = head.bot_pickupevalfunc(self, head); - - if(rating > 0) - navigation_routerating(head, rating * ratingscale, 2000); - head = head.chain; - } -} - -void havocbot_goalrating_controlpoints(float ratingscale, vector org, float sradius) -{SELFPARAM(); - entity head; - head = findchain(classname, "dom_controlpoint"); - while (head) - { - if (vlen(( ( head.absmin + head.absmax ) * 0.5 ) - org) < sradius) - { - if(head.cnt > -1) // this is just being fought for - navigation_routerating(head, ratingscale, 5000); - else if(head.goalentity.cnt == 0) // unclaimed point - navigation_routerating(head, ratingscale * 0.5, 5000); - else if(head.goalentity.team != self.team) // other team's point - navigation_routerating(head, ratingscale * 0.2, 5000); - } - head = head.chain; - } -} - -void havocbot_goalrating_enemyplayers(float ratingscale, vector org, float sradius) -{SELFPARAM(); - entity head; - int t; - float distance; - noref bool noteam = ((self.team == 0) || !teamplay); - - if (autocvar_bot_nofire) - return; - - // don't chase players if we're under water - if(self.waterlevel>WATERLEVEL_WETFEET) - return; - - FOR_EACH_PLAYER(head) - { - // TODO: Merge this logic with the bot_shouldattack function - if(bot_shouldattack(head)) - { - distance = vlen(head.origin - org); - if (distance < 100 || distance > sradius) - continue; - - // rate only visible enemies - /* - traceline(self.origin + self.view_ofs, head.origin, MOVE_NOMONSTERS, self); - if (trace_fraction < 1 || trace_ent != head) - continue; - */ - - if((head.flags & FL_INWATER) || (head.flags & FL_PARTIALGROUND)) - continue; - - // not falling - if((head.flags & FL_ONGROUND) == 0) - { - traceline(head.origin, head.origin + '0 0 -1500', true, world); - 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(head.origin, head.mins, head.maxs, trace_endpos)) - continue; - } - - // TODO: rate waypoints near the targetted player at that moment, instead of the player itself - // 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 = (self.health + self.armorvalue ) / (head.health + head.armorvalue ); - navigation_routerating(head, t * ratingscale, 2000); - } - } -} - -// legacy bot role for standard gamemodes -// go to best items -void havocbot_role_generic() -{SELFPARAM(); - if(self.deadflag != DEAD_NO) - return; - - if (self.bot_strategytime < time) - { - self.bot_strategytime = time + autocvar_bot_ai_strategyinterval; - navigation_goalrating_start(); - havocbot_goalrating_items(10000, self.origin, 10000); - havocbot_goalrating_enemyplayers(20000, self.origin, 10000); - //havocbot_goalrating_waypoints(1, self.origin, 1000); - navigation_goalrating_end(); - } -} - -void havocbot_chooserole_generic() -{SELFPARAM(); - self.havocbot_role = havocbot_role_generic; -} - -void havocbot_chooserole() -{SELFPARAM(); - LOG_TRACE("choosing a role...\n"); - self.bot_strategytime = 0; - if (MUTATOR_CALLHOOK(HavocBot_ChooseRole, self)) - return; - else if (g_keyhunt) - havocbot_chooserole_kh(); - else - havocbot_chooserole_generic(); -} diff --git a/qcsrc/server/bot/havocbot/roles.qh b/qcsrc/server/bot/havocbot/roles.qh deleted file mode 100644 index cfabf0534..000000000 --- a/qcsrc/server/bot/havocbot/roles.qh +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef ROLES_H -#define ROLES_H -void havocbot_goalrating_controlpoints(float ratingscale, vector org, float sradius); -#endif diff --git a/qcsrc/server/bot/havocbot/scripting.qh b/qcsrc/server/bot/havocbot/scripting.qh deleted file mode 100644 index c09dbdeac..000000000 --- a/qcsrc/server/bot/havocbot/scripting.qh +++ /dev/null @@ -1,5 +0,0 @@ -#ifndef HAVOCBOT_SCRIPTING_H -#define HAVOCBOT_SCRIPTING_H - -void bot_clearqueue(entity bot); -#endif diff --git a/qcsrc/server/bot/impl.qc b/qcsrc/server/bot/impl.qc new file mode 100644 index 000000000..9f36c4f67 --- /dev/null +++ b/qcsrc/server/bot/impl.qc @@ -0,0 +1,3 @@ +#include "api.qh" + +#include "default/all.inc" diff --git a/qcsrc/server/bot/navigation.qc b/qcsrc/server/bot/navigation.qc deleted file mode 100644 index 99e3901b2..000000000 --- a/qcsrc/server/bot/navigation.qc +++ /dev/null @@ -1,1266 +0,0 @@ -#include "navigation.qh" -#include "../_all.qh" - -#include "bot.qh" -#include "waypoints.qh" - -#include "../t_items.qh" - -#include "../../common/constants.qh" -#include "../../common/triggers/trigger/jumppads.qh" - -void bot_debug(string input) -{ - switch(autocvar_bot_debug) - { - case 1: LOG_TRACE(input); break; - case 2: LOG_INFO(input); break; - } -} - -// rough simulation of walking from one point to another to test if a path -// can be traveled, used for waypoint linking and havocbot - -float tracewalk(entity e, vector start, vector m1, vector m2, vector end, float movemode) -{SELFPARAM(); - 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(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(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(trace_endpos); - - if (trace_fraction < 1) - { - swimming = true; - org = trace_endpos - normalize(org - trace_endpos) * stepdist; - for (; org.z < end.z + self.maxs.z; org.z += stepdist) - { - if(autocvar_bot_debug_tracewalk) - debugnode(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(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() -{SELFPARAM(); - //print("bot ", etos(self), " clear\n"); - self.navigation_hasgoals = false; - self.goalcurrent = world; - self.goalstack01 = world; - self.goalstack02 = world; - self.goalstack03 = world; - self.goalstack04 = world; - self.goalstack05 = world; - self.goalstack06 = world; - self.goalstack07 = world; - self.goalstack08 = world; - self.goalstack09 = world; - self.goalstack10 = world; - self.goalstack11 = world; - self.goalstack12 = world; - self.goalstack13 = world; - self.goalstack14 = world; - self.goalstack15 = world; - self.goalstack16 = world; - self.goalstack17 = world; - self.goalstack18 = world; - self.goalstack19 = world; - self.goalstack20 = world; - self.goalstack21 = world; - self.goalstack22 = world; - self.goalstack23 = world; - self.goalstack24 = world; - self.goalstack25 = world; - self.goalstack26 = world; - self.goalstack27 = world; - self.goalstack28 = world; - self.goalstack29 = world; - self.goalstack30 = world; - self.goalstack31 = world; -} - -// 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 e) -{SELFPARAM(); - //print("bot ", etos(self), " push ", etos(e), "\n"); - self.goalstack31 = self.goalstack30; - self.goalstack30 = self.goalstack29; - self.goalstack29 = self.goalstack28; - self.goalstack28 = self.goalstack27; - self.goalstack27 = self.goalstack26; - self.goalstack26 = self.goalstack25; - self.goalstack25 = self.goalstack24; - self.goalstack24 = self.goalstack23; - self.goalstack23 = self.goalstack22; - self.goalstack22 = self.goalstack21; - self.goalstack21 = self.goalstack20; - self.goalstack20 = self.goalstack19; - self.goalstack19 = self.goalstack18; - self.goalstack18 = self.goalstack17; - self.goalstack17 = self.goalstack16; - self.goalstack16 = self.goalstack15; - self.goalstack15 = self.goalstack14; - self.goalstack14 = self.goalstack13; - self.goalstack13 = self.goalstack12; - self.goalstack12 = self.goalstack11; - self.goalstack11 = self.goalstack10; - self.goalstack10 = self.goalstack09; - self.goalstack09 = self.goalstack08; - self.goalstack08 = self.goalstack07; - self.goalstack07 = self.goalstack06; - self.goalstack06 = self.goalstack05; - self.goalstack05 = self.goalstack04; - self.goalstack04 = self.goalstack03; - self.goalstack03 = self.goalstack02; - self.goalstack02 = self.goalstack01; - self.goalstack01 = self.goalcurrent; - self.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() -{SELFPARAM(); - //print("bot ", etos(self), " pop\n"); - self.goalcurrent = self.goalstack01; - self.goalstack01 = self.goalstack02; - self.goalstack02 = self.goalstack03; - self.goalstack03 = self.goalstack04; - self.goalstack04 = self.goalstack05; - self.goalstack05 = self.goalstack06; - self.goalstack06 = self.goalstack07; - self.goalstack07 = self.goalstack08; - self.goalstack08 = self.goalstack09; - self.goalstack09 = self.goalstack10; - self.goalstack10 = self.goalstack11; - self.goalstack11 = self.goalstack12; - self.goalstack12 = self.goalstack13; - self.goalstack13 = self.goalstack14; - self.goalstack14 = self.goalstack15; - self.goalstack15 = self.goalstack16; - self.goalstack16 = self.goalstack17; - self.goalstack17 = self.goalstack18; - self.goalstack18 = self.goalstack19; - self.goalstack19 = self.goalstack20; - self.goalstack20 = self.goalstack21; - self.goalstack21 = self.goalstack22; - self.goalstack22 = self.goalstack23; - self.goalstack23 = self.goalstack24; - self.goalstack24 = self.goalstack25; - self.goalstack25 = self.goalstack26; - self.goalstack26 = self.goalstack27; - self.goalstack27 = self.goalstack28; - self.goalstack28 = self.goalstack29; - self.goalstack29 = self.goalstack30; - self.goalstack30 = self.goalstack31; - self.goalstack31 = world; -} - -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, PL_MIN, PL_MAX, org, bot_navigation_movemode)) - return true; - } - else - { - if (tracewalk(ent, org, PL_MIN, PL_MAX, 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) -{ - entity waylist, w, best; - vector v, org, pm1, pm2; - - pm1 = ent.origin + ent.mins; - pm2 = ent.origin + ent.maxs; - waylist = findchain(classname, "waypoint"); - - // do two scans, because box test is cheaper - w = waylist; - while (w) - { - // if object is touching spawnfunc_waypoint - if(w != ent && w != except) - if (boxesoverlap(pm1, pm2, w.absmin, w.absmax)) - return w; - w = w.chain; - } - - org = ent.origin + 0.5 * (ent.mins + ent.maxs); - org.z = ent.origin.z + ent.mins.z - PL_MIN.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); - - best = world; - - // box check failed, try walk - w = waylist; - while (w) - { - // if object can walk from spawnfunc_waypoint - if(w != ent) - { - if (w.wpisbox) - { - vector wm1, wm2; - wm1 = w.origin + w.mins; - wm2 = w.origin + w.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 = w.origin; - if(navigation_waypoint_will_link(v, org, ent, walkfromwp, bestdist)) - { - bestdist = vlen(v - org); - best = w; - } - } - w = w.chain; - } - return best; -} -entity navigation_findnearestwaypoint(entity ent, float walkfromwp) -{ - entity wp = navigation_findnearestwaypoint_withdist_except(ent, walkfromwp, 1050, world); - 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 waylist, float maxdist) -{SELFPARAM(); - entity head; - vector v, m1, m2, diff; - float c; -// navigation_testtracewalk = true; - c = 0; - head = waylist; - while (head) - { - if (!head.wpconsidered) - { - if (head.wpisbox) - { - m1 = head.origin + head.mins; - m2 = head.origin + head.maxs; - v = self.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 = head.origin; - diff = v - self.origin; - diff.z = max(0, diff.z); - if (vlen(diff) < maxdist) - { - head.wpconsidered = true; - if (tracewalk(self, self.origin, self.mins, self.maxs, v, bot_navigation_movemode)) - { - head.wpnearestpoint = v; - head.wpcost = vlen(v - self.origin) + head.dmg; - head.wpfire = 1; - head.enemy = world; - c = c + 1; - } - } - } - head = head.chain; - } - //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 fixed_source_waypoint) -{SELFPARAM(); - entity w, wp, waylist; - float searching, cost, cost2; - vector p; - w = waylist = findchain(classname, "waypoint"); - while (w) - { - w.wpconsidered = false; - w.wpnearestpoint = '0 0 0'; - w.wpcost = 10000000; - w.wpfire = 0; - w.enemy = world; - w = w.chain; - } - - 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 = world; - } - 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 i, increment, maxdistance; - if(self.flags & FL_ONGROUND) - { - increment = 750; - maxdistance = 50000; - } - else - { - increment = 500; - maxdistance = 1500; - } - - for(i=increment;!navigation_markroutes_nearestwaypoints(waylist, i)&&i cost2 + w.wp00mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp01;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp01mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp02;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp02mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp03;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp03mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp04;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp04mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp05;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp05mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp06;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp06mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp07;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp07mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp08;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp08mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp09;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp09mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp10;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp10mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp11;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp11mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp12;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp12mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp13;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp13mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp14;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp14mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp15;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp15mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp16;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp16mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp17;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp17mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp18;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp18mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp19;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp19mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp20;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp20mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp21;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp21mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp22;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp22mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp23;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp23mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp24;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp24mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp25;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp25mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp26;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp26mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp27;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp27mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp28;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp28mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp29;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp29mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp30;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp30mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - wp = w.wp31;if (wp){cost2 = cost + wp.dmg;if (wp.wpcost > cost2 + w.wp31mincost) navigation_markroutes_checkwaypoint(w, wp, cost2, p); - }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} - } - w = w.chain; - } - } -} - -// queries the entire spawnfunc_waypoint network for pathes leading to the bot -void navigation_markroutes_inverted(entity fixed_source_waypoint) -{ - entity w, wp, waylist; - float searching, cost, cost2; - vector p; - w = waylist = findchain(classname, "waypoint"); - while (w) - { - w.wpconsidered = false; - w.wpnearestpoint = '0 0 0'; - w.wpcost = 10000000; - w.wpfire = 0; - w.enemy = world; - w = w.chain; - } - - 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 = world; - } - else - { - error("need to start with a waypoint\n"); - } - - searching = true; - while (searching) - { - searching = false; - w = waylist; - while (w) - { - if (w.wpfire) - { - searching = true; - w.wpfire = 0; - cost = w.wpcost; // cost to walk from w to home - p = w.wpnearestpoint; - for(wp = waylist; wp; wp = wp.chain) - { - if(w != wp.wp00) if(w != wp.wp01) if(w != wp.wp02) if(w != wp.wp03) - if(w != wp.wp04) if(w != wp.wp05) if(w != wp.wp06) if(w != wp.wp07) - if(w != wp.wp08) if(w != wp.wp09) if(w != wp.wp10) if(w != wp.wp11) - if(w != wp.wp12) if(w != wp.wp13) if(w != wp.wp14) if(w != wp.wp15) - if(w != wp.wp16) if(w != wp.wp17) if(w != wp.wp18) if(w != wp.wp19) - if(w != wp.wp20) if(w != wp.wp21) if(w != wp.wp22) if(w != wp.wp23) - if(w != wp.wp24) if(w != wp.wp25) if(w != wp.wp26) if(w != wp.wp27) - if(w != wp.wp28) if(w != wp.wp29) if(w != wp.wp30) if(w != wp.wp31) - continue; - cost2 = cost + wp.dmg; - navigation_markroutes_checkwaypoint(w, wp, cost2, p); - } - } - w = w.chain; - } - } -} - -// updates the best goal according to a weighted calculation of travel cost and item value of a new proposed item -void navigation_routerating(entity e, float f, float rangebias) -{SELFPARAM(); - 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(self.items & IT_JETPACK) - if(autocvar_bot_ai_navigation_jetpack) - if(vlen(self.origin - o) > autocvar_bot_ai_navigation_jetpack_mindistance) - { - vector pointa, pointb; - - bot_debug(strcat("jetpack ai: evaluating path for ", e.classname, "\n")); - - // Point A - traceline(self.origin, self.origin + '0 0 65535', MOVE_NORMAL, self); - 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, self); - - if(trace_fraction==1) - { - bot_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' * (PL_MAX.z - PL_MIN.z) * 10; - - do{ - npa = pointa + down; - npb = pointb + down; - - if(npa.z<=self.absmax.z) - break; - - if(npb.z<=e.absmax.z) - break; - - traceline(npa, npb, MOVE_NORMAL, self); - 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 - self.origin.z); - - t = zdistance / autocvar_g_jetpack_maxspeed_up; - t += xydistance / autocvar_g_jetpack_maxspeed_side; - fuel = t * autocvar_g_jetpack_fuel * 0.8; - - bot_debug(strcat("jetpack ai: required fuel ", ftos(fuel), " self.ammo_fuel ", ftos(self.ammo_fuel), "\n")); - - // enough fuel ? - if(self.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) - { - bot_debug(strcat("jetpack path: added goal ", e.classname, " (with rating ", ftos(f), ")\n")); - navigation_bestrating = f; - navigation_bestgoal = e; - self.navigation_jetpack_goal = e; - self.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 - { - bot_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) - { - bot_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; - } - - bot_debug(strcat("-- checking ", e.classname, " (with cost ", ftos(nwp.wpcost), ")\n")); - if (nwp) - if (nwp.wpcost < 10000000) - { - //te_wizspike(nwp.wpnearestpoint); - bot_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))); - bot_debug(strcat("considering ", e.classname, " (with rating ", ftos(f), ")\n")); - if (navigation_bestrating < f) - { - bot_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 -float navigation_routetogoal(entity e, vector startposition) -{SELFPARAM(); - self.goalentity = e; - - // if there is no goal, just exit - if (!e) - return false; - - self.navigation_hasgoals = true; - - // put the entity on the goal stack - //print("routetogoal ", etos(e), "\n"); - navigation_pushroute(e); - - if(g_jetpack) - if(e==self.navigation_jetpack_goal) - return true; - - // if it can reach the goal there is nothing more to do - if (tracewalk(self, startposition, PL_MIN, PL_MAX, (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 == world) - return false; - - for (;;) - { - // add the spawnfunc_waypoint to the path - navigation_pushroute(e); - e = e.enemy; - - if(e==world) - 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() -{SELFPARAM(); - vector org, m1, m2; - org = self.origin; - m1 = org + self.mins; - m2 = org + self.maxs; - - if(self.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT) - { - if(self.lastteleporttime>0) - if(time-self.lastteleporttime<(self.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL)?2:0.15) - { - if(self.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) - if(self.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL && self.goalcurrent.owner==self) - { - self.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING; - self.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED; - } - navigation_poproute(); - return; - } - } - - // If for some reason the bot is closer to the next goal, pop the current one - if(self.goalstack01) - if(vlen(self.goalcurrent.origin - self.origin) > vlen(self.goalstack01.origin - self.origin)) - if(checkpvs(self.origin + self.view_ofs, self.goalstack01)) - if(tracewalk(self, self.origin, self.mins, self.maxs, (self.goalstack01.absmin + self.goalstack01.absmax) * 0.5, bot_navigation_movemode)) - { - bot_debug(strcat("path optimized for ", self.netname, ", removed a goal from the queue\n")); - navigation_poproute(); - // 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 itself - if(IS_PLAYER(self.goalcurrent)) - navigation_poproute(); - - // aid for detecting jump pads better (distance based check fails sometimes) - if(self.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT && self.jumppadcount > 0 ) - navigation_poproute(); - - // Loose goal touching check when running - if(self.aistatus & AI_STATUS_RUNNING) - if(self.speed >= autocvar_sv_maxspeed) // if -really- running - if(self.goalcurrent.classname=="waypoint") - { - if(vlen(self.origin - self.goalcurrent.origin)<150) - { - traceline(self.origin + self.view_ofs , self.goalcurrent.origin, true, world); - if(trace_fraction==1) - { - // Detect personal waypoints - if(self.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) - if(self.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL && self.goalcurrent.owner==self) - { - self.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING; - self.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED; - } - - navigation_poproute(); - } - } - } - - while (self.goalcurrent && boxesoverlap(m1, m2, self.goalcurrent.absmin, self.goalcurrent.absmax)) - { - // Detect personal waypoints - if(self.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING) - if(self.goalcurrent.wpflags & WAYPOINTFLAG_PERSONAL && self.goalcurrent.owner==self) - { - self.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING; - self.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_REACHED; - } - - navigation_poproute(); - } -} - -// begin a goal selection session (queries spawnfunc_waypoint network) -void navigation_goalrating_start() -{SELFPARAM(); - if(self.aistatus & AI_STATUS_STUCK) - return; - - self.navigation_jetpack_goal = world; - navigation_bestrating = -1; - self.navigation_hasgoals = false; - navigation_clearroute(); - navigation_bestgoal = world; - navigation_markroutes(world); -} - -// ends a goal selection session (updates goal stack to the best goal) -void navigation_goalrating_end() -{SELFPARAM(); - if(self.aistatus & AI_STATUS_STUCK) - return; - - navigation_routetogoal(navigation_bestgoal, self.origin); - bot_debug(strcat("best goal ", self.goalcurrent.classname , "\n")); - - // If the bot got stuck then try to reach the farthest waypoint - if (!self.navigation_hasgoals) - if (autocvar_bot_wander_enable) - { - if (!(self.aistatus & AI_STATUS_STUCK)) - { - bot_debug(strcat(self.netname, " cannot walk to any goal\n")); - self.aistatus |= AI_STATUS_STUCK; - } - - self.navigation_hasgoals = false; // Reset this value - } -} - -void botframe_updatedangerousobjects(float maxupdate) -{ - entity head, bot_dodgelist; - vector m1, m2, v, o; - float c, d, danger; - c = 0; - bot_dodgelist = findchainfloat(bot_dodge, true); - botframe_dangerwaypoint = find(botframe_dangerwaypoint, classname, "waypoint"); - while (botframe_dangerwaypoint != world) - { - danger = 0; - m1 = botframe_dangerwaypoint.mins; - m2 = botframe_dangerwaypoint.maxs; - head = bot_dodgelist; - while (head) - { - v = head.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 = (head.absmin + head.absmax) * 0.5; - d = head.bot_dodgerating - vlen(o - v); - if (d > 0) - { - traceline(o, v, true, world); - if (trace_fraction == 1) - danger = danger + d; - } - head = head.chain; - } - botframe_dangerwaypoint.dmg = danger; - c = c + 1; - if (c >= maxupdate) - break; - botframe_dangerwaypoint = find(botframe_dangerwaypoint, classname, "waypoint"); - } -} - -void navigation_unstuck() -{SELFPARAM(); - float search_radius = 1000; - - if (!autocvar_bot_wander_enable) - return; - - if (!bot_waypoint_queue_owner) - { - bot_debug(strcat(self.netname, " sutck, taking over the waypoints queue\n")); - bot_waypoint_queue_owner = self; - bot_waypoint_queue_bestgoal = world; - bot_waypoint_queue_bestgoalrating = 0; - } - - if(bot_waypoint_queue_owner!=self) - return; - - if (bot_waypoint_queue_goal) - { - // evaluate the next goal on the queue - float d = vlen(self.origin - bot_waypoint_queue_goal.origin); - bot_debug(strcat(self.netname, " evaluating ", bot_waypoint_queue_goal.classname, " with distance ", ftos(d), "\n")); - if(tracewalk(bot_waypoint_queue_goal, self.origin, PL_MIN, PL_MAX, 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) - { - bot_debug(strcat(self.netname, " stuck, reachable waypoint found, heading to it\n")); - navigation_routetogoal(bot_waypoint_queue_bestgoal, self.origin); - self.bot_strategytime = time + autocvar_bot_ai_strategyinterval; - self.aistatus &= ~AI_STATUS_STUCK; - } - else - { - bot_debug(strcat(self.netname, " stuck, cannot walk to any waypoint at all\n")); - } - - bot_waypoint_queue_owner = world; - } - } - else - { - if(bot_strategytoken!=self) - return; - - // build a new queue - bot_debug(strcat(self.netname, " stuck, scanning reachable waypoints within ", ftos(search_radius)," qu\n")); - - entity head, first; - - first = world; - head = findradius(self.origin, search_radius); - - while(head) - { - if(head.classname=="waypoint") - // if(!(head.wpflags & WAYPOINTFLAG_GENERATED)) - { - if(bot_waypoint_queue_goal) - bot_waypoint_queue_goal.bot_waypoint_queue_nextgoal = head; - else - first = head; - - bot_waypoint_queue_goal = head; - bot_waypoint_queue_goal.bot_waypoint_queue_nextgoal = world; - } - - head = head.chain; - } - - if (first) - bot_waypoint_queue_goal = first; - else - { - bot_debug(strcat(self.netname, " stuck, cannot walk to any waypoint at all\n")); - bot_waypoint_queue_owner = world; - } - } -} - -// Support for debugging tracewalk visually - -void debugresetnodes() -{ - debuglastnode = '0 0 0'; -} - -void debugnode(vector node) -{SELFPARAM(); - if (!IS_PLAYER(self)) - return; - - if(debuglastnode=='0 0 0') - { - debuglastnode = node; - return; - } - - te_lightning2(world, 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() -{SELFPARAM(); - entity goal; - vector org, go; - - if(self.goalcounter==0)goal=self.goalcurrent; - else if(self.goalcounter==1)goal=self.goalstack01; - else if(self.goalcounter==2)goal=self.goalstack02; - else if(self.goalcounter==3)goal=self.goalstack03; - else if(self.goalcounter==4)goal=self.goalstack04; - else if(self.goalcounter==5)goal=self.goalstack05; - else if(self.goalcounter==6)goal=self.goalstack06; - else if(self.goalcounter==7)goal=self.goalstack07; - else if(self.goalcounter==8)goal=self.goalstack08; - else if(self.goalcounter==9)goal=self.goalstack09; - else if(self.goalcounter==10)goal=self.goalstack10; - else if(self.goalcounter==11)goal=self.goalstack11; - else if(self.goalcounter==12)goal=self.goalstack12; - else if(self.goalcounter==13)goal=self.goalstack13; - else if(self.goalcounter==14)goal=self.goalstack14; - else if(self.goalcounter==15)goal=self.goalstack15; - else if(self.goalcounter==16)goal=self.goalstack16; - else if(self.goalcounter==17)goal=self.goalstack17; - else if(self.goalcounter==18)goal=self.goalstack18; - else if(self.goalcounter==19)goal=self.goalstack19; - else if(self.goalcounter==20)goal=self.goalstack20; - else if(self.goalcounter==21)goal=self.goalstack21; - else if(self.goalcounter==22)goal=self.goalstack22; - else if(self.goalcounter==23)goal=self.goalstack23; - else if(self.goalcounter==24)goal=self.goalstack24; - else if(self.goalcounter==25)goal=self.goalstack25; - else if(self.goalcounter==26)goal=self.goalstack26; - else if(self.goalcounter==27)goal=self.goalstack27; - else if(self.goalcounter==28)goal=self.goalstack28; - else if(self.goalcounter==29)goal=self.goalstack29; - else if(self.goalcounter==30)goal=self.goalstack30; - else if(self.goalcounter==31)goal=self.goalstack31; - else goal=world; - - if(goal==world) - { - self.goalcounter = 0; - self.lastposition='0 0 0'; - return; - } - - if(self.lastposition=='0 0 0') - org = self.origin; - else - org = self.lastposition; - - - go = ( goal.absmin + goal.absmax ) * 0.5; - te_lightning2(world, org, go); - self.lastposition = go; - - self.goalcounter++; -} diff --git a/qcsrc/server/bot/navigation.qh b/qcsrc/server/bot/navigation.qh deleted file mode 100644 index cf4a5ce5b..000000000 --- a/qcsrc/server/bot/navigation.qh +++ /dev/null @@ -1,80 +0,0 @@ -#ifndef NAVIGATION_H -#define NAVIGATION_H -/* - * Globals and Fields - */ - -float navigation_bestrating; -float bot_navigation_movemode; -float navigation_testtracewalk; - -vector jumpstepheightvec; -vector stepheightvec; - -entity botframe_dangerwaypoint; -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(vector node); -void debugnodestatus(vector position, float status); - -void debuggoalstack(); - -float tracewalk(entity e, vector start, vector m1, vector m2, vector end, float movemode); - -float navigation_markroutes_nearestwaypoints(entity waylist, float maxdist); -float navigation_routetogoal(entity e, vector startposition); - -void navigation_clearroute(); -void navigation_pushroute(entity e); -void navigation_poproute(); -void navigation_markroutes_checkwaypoint(entity w, entity wp, float cost2, vector p); -void navigation_markroutes(entity fixed_source_waypoint); -void navigation_markroutes_inverted(entity fixed_source_waypoint); -void navigation_routerating(entity e, float f, float rangebias); -void navigation_poptouchedgoals(); -void navigation_goalrating_start(); -void navigation_goalrating_end(); -void navigation_unstuck(); - -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); -#endif diff --git a/qcsrc/server/bot/null/impl.qc b/qcsrc/server/bot/null/impl.qc new file mode 100644 index 000000000..ea35b7fbc --- /dev/null +++ b/qcsrc/server/bot/null/impl.qc @@ -0,0 +1,39 @@ +bool bot_aim(float shotspeed, float shotspeedupward, float maxshottime, float applygravity) { return false; } +void bot_clientconnect() { } +void bot_clientdisconnect() { } +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 e) { return false; } +void bot_think() { } + +entity find_bot_by_name(string name) { return NULL; } +entity find_bot_by_number(float number) { return NULL; } + +void havocbot_goalrating_controlpoints(float ratingscale, vector org, float sradius) { } +void havocbot_goalrating_enemyplayers(float ratingscale, vector org, float sradius) { } +void havocbot_goalrating_items(float ratingscale, vector org, float sradius) { } + +entity navigation_findnearestwaypoint(entity ent, float walkfromwp) { return NULL; } +void navigation_goalrating_end() { } +void navigation_goalrating_start() { } +void navigation_markroutes(entity fixed_source_waypoint) { } +void navigation_markroutes_inverted(entity fixed_source_waypoint) { } +void navigation_routerating(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; } diff --git a/qcsrc/server/bot/scripting.qc b/qcsrc/server/bot/scripting.qc deleted file mode 100644 index 72951cd66..000000000 --- a/qcsrc/server/bot/scripting.qc +++ /dev/null @@ -1,1347 +0,0 @@ -#include "scripting.qh" -#include "../_all.qh" - -#include "bot.qh" - -.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); -} - -float bot_havecommand(entity bot, float idx) -{ - if(!bot.bot_cmdqueuebuf_allocated) - return 0; - if(idx < bot.bot_cmdqueuebuf_start) - return 0; - if(idx >= bot.bot_cmdqueuebuf_end) - return 0; - return 1; -} - -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(string placename) -{SELFPARAM(); - entity e; - if(substring(placename, 0, 1) == "@") - { - int i, p; - placename = substring(placename, 1, -1); - string s, s2; - for(i = 0; i < self.bot_places_count; ++i) - if(self.(bot_placenames[i]) == placename) - return self.(bot_places[i]); - // now: i == self.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(world, targetname, s); - if(!e) - LOG_INFO("invalid place ", s, "\n"); - if(i < MAX_BOT_PLACES) - { - self.(bot_placenames[i]) = strzone(placename); - self.(bot_places[i]) = e; - self.bot_places_count += 1; - } - return e; - } - else - { - e = find(world, 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 world; -} - -// Returns a bot by number on list -entity find_bot_by_number(float number) -{ - entity bot; - float c = 0; - - if(!number) - return world; - - bot = findchainflags(flags, FL_CLIENT); - while (bot) - { - if(IS_BOT_CLIENT(bot)) - { - if(++c==number) - return bot; - } - bot = bot.chain; - } - - return world; -} - -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; - -void SV_ParseClientCommand(string s); -float bot_cmd_cc() -{ - SV_ParseClientCommand(bot_cmd.bot_cmd_parm_string); - return CMD_STATUS_FINISHED; -} - -float bot_cmd_impulse() -{SELFPARAM(); - self.impulse = bot_cmd.bot_cmd_parm_float; - return CMD_STATUS_FINISHED; -} - -float bot_cmd_continue() -{SELFPARAM(); - self.bot_exec_status &= ~BOT_EXEC_STATUS_PAUSED; - return CMD_STATUS_FINISHED; -} - -.float bot_cmd_wait_time; -float bot_cmd_wait() -{SELFPARAM(); - if(self.bot_exec_status & BOT_EXEC_STATUS_WAITING) - { - if(time>=self.bot_cmd_wait_time) - { - self.bot_exec_status &= ~BOT_EXEC_STATUS_WAITING; - return CMD_STATUS_FINISHED; - } - else - return CMD_STATUS_EXECUTING; - } - - self.bot_cmd_wait_time = time + bot_cmd.bot_cmd_parm_float; - self.bot_exec_status |= BOT_EXEC_STATUS_WAITING; - return CMD_STATUS_EXECUTING; -} - -float bot_cmd_wait_until() -{SELFPARAM(); - if(time < bot_cmd.bot_cmd_parm_float + bot_barriertime) - { - self.bot_exec_status |= BOT_EXEC_STATUS_WAITING; - return CMD_STATUS_EXECUTING; - } - self.bot_exec_status &= ~BOT_EXEC_STATUS_WAITING; - return CMD_STATUS_FINISHED; -} - -float bot_cmd_barrier() -{SELFPARAM(); - entity cl; - - // 0 = no barrier, 1 = waiting, 2 = waiting finished - - if(self.bot_barrier == 0) // initialization - { - self.bot_barrier = 1; - - //self.colormod = '4 4 0'; - } - - if(self.bot_barrier == 1) // find other bots - { - FOR_EACH_CLIENT(cl) if(cl.isbot) - { - if(cl.bot_cmdqueuebuf_allocated) - if(cl.bot_barrier != 1) - return CMD_STATUS_EXECUTING; // not all are at the barrier yet - } - - // all bots hit the barrier! - FOR_EACH_CLIENT(cl) if(cl.isbot) - { - cl.bot_barrier = 2; // acknowledge barrier - } - - bot_barriertime = time; - } - - // if we get here, the barrier is finished - // so end it... - self.bot_barrier = 0; - //self.colormod = '0 0 0'; - - return CMD_STATUS_FINISHED; -} - -float bot_cmd_turn() -{SELFPARAM(); - self.v_angle_y = self.v_angle.y + bot_cmd.bot_cmd_parm_float; - self.v_angle_y = self.v_angle.y - floor(self.v_angle.y / 360) * 360; - return CMD_STATUS_FINISHED; -} - -float bot_cmd_select_weapon() -{SELFPARAM(); - float id; - - id = bot_cmd.bot_cmd_parm_float; - - if(id < WEP_FIRST || id > WEP_LAST) - return CMD_STATUS_ERROR; - - if(client_hasweapon(self, id, true, false)) - self.switchweapon = 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(string expr) -{SELFPARAM(); - // 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 self.health; - case "speed": - return vlen(self.velocity); - case "flagcarrier": - return ((self.flagcarried!=world)); - } - - LOG_INFO(strcat("ERROR: Unable to convert the expression '",expr,"' into a numeric value\n")); - return 0; -} - -float bot_cmd_if() -{SELFPARAM(); - string expr, val_a, val_b; - float cmpofs; - - if(self.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(self); - return CMD_STATUS_ERROR; - } - - self.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(val_a)==bot_cmd_eval(val_b)) - self.bot_cmd_condition_status |= CMD_CONDITION_true; - else - self.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(val_a)>bot_cmd_eval(val_b)) - self.bot_cmd_condition_status |= CMD_CONDITION_true; - else - self.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(val_a)=self.bot_cmd_aim_endtime) - { - self.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) - { - self.v_angle_x -= stof(argv(1)); - self.v_angle_y += stof(argv(0)); - return CMD_STATUS_FINISHED; - } - - self.bot_cmd_aim_begin = self.v_angle; - - self.bot_cmd_aim_end_x = self.v_angle.x - stof(argv(1)); - self.bot_cmd_aim_end_y = self.v_angle.y + stof(argv(0)); - self.bot_cmd_aim_end_z = 0; - - self.bot_cmd_aim_begintime = time; - self.bot_cmd_aim_endtime = time + step; - - return CMD_STATUS_EXECUTING; -} - -float bot_cmd_aimtarget() -{SELFPARAM(); - if(self.bot_cmd_aim_endtime) - { - return bot_cmd_aim(); - } - - entity e; - string parms; - vector v; - float tokens, step; - - parms = bot_cmd.bot_cmd_parm_string; - - tokens = tokenizebyseparator(parms, " "); - - e = bot_getplace(argv(0)); - if(!e) - return CMD_STATUS_ERROR; - - v = e.origin + (e.mins + e.maxs) * 0.5; - - if(tokens==1) - { - self.v_angle = vectoangles(v - (self.origin + self.view_ofs)); - self.v_angle_x = -self.v_angle.x; - return CMD_STATUS_FINISHED; - } - - if(tokens<1||tokens>2) - return CMD_STATUS_ERROR; - - step = stof(argv(1)); - - self.bot_cmd_aim_begin = self.v_angle; - self.bot_cmd_aim_end = vectoangles(v - (self.origin + self.view_ofs)); - self.bot_cmd_aim_end_x = -self.bot_cmd_aim_end.x; - - self.bot_cmd_aim_begintime = time; - self.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 = 1; -const int BOT_CMD_KEY_BACKWARD = 2; -const int BOT_CMD_KEY_RIGHT = 4; -const int BOT_CMD_KEY_LEFT = 8; -const int BOT_CMD_KEY_JUMP = 16; -const int BOT_CMD_KEY_ATTACK1 = 32; -const int BOT_CMD_KEY_ATTACK2 = 64; -const int BOT_CMD_KEY_USE = 128; -const int BOT_CMD_KEY_HOOK = 256; -const int BOT_CMD_KEY_CROUCH = 512; -const int BOT_CMD_KEY_CHAT = 1024; - -float bot_presskeys() -{SELFPARAM(); - self.movement = '0 0 0'; - self.BUTTON_JUMP = false; - self.BUTTON_CROUCH = false; - self.BUTTON_ATCK = false; - self.BUTTON_ATCK2 = false; - self.BUTTON_USE = false; - self.BUTTON_HOOK = false; - self.BUTTON_CHAT = false; - - if(self.bot_cmd_keys == BOT_CMD_KEY_NONE) - return false; - - if(self.bot_cmd_keys & BOT_CMD_KEY_FORWARD) - self.movement_x = autocvar_sv_maxspeed; - else if(self.bot_cmd_keys & BOT_CMD_KEY_BACKWARD) - self.movement_x = -autocvar_sv_maxspeed; - - if(self.bot_cmd_keys & BOT_CMD_KEY_RIGHT) - self.movement_y = autocvar_sv_maxspeed; - else if(self.bot_cmd_keys & BOT_CMD_KEY_LEFT) - self.movement_y = -autocvar_sv_maxspeed; - - if(self.bot_cmd_keys & BOT_CMD_KEY_JUMP) - self.BUTTON_JUMP = true; - - if(self.bot_cmd_keys & BOT_CMD_KEY_CROUCH) - self.BUTTON_CROUCH = true; - - if(self.bot_cmd_keys & BOT_CMD_KEY_ATTACK1) - self.BUTTON_ATCK = true; - - if(self.bot_cmd_keys & BOT_CMD_KEY_ATTACK2) - self.BUTTON_ATCK2 = true; - - if(self.bot_cmd_keys & BOT_CMD_KEY_USE) - self.BUTTON_USE = true; - - if(self.bot_cmd_keys & BOT_CMD_KEY_HOOK) - self.BUTTON_HOOK = true; - - if(self.bot_cmd_keys & BOT_CMD_KEY_CHAT) - self.BUTTON_CHAT = true; - - return true; -} - - -float bot_cmd_keypress_handler(string key, float enabled) -{SELFPARAM(); - switch(key) - { - case "all": - if(enabled) - self.bot_cmd_keys = power2of(20) - 1; // >:) - else - self.bot_cmd_keys = BOT_CMD_KEY_NONE; - case "forward": - if(enabled) - { - self.bot_cmd_keys |= BOT_CMD_KEY_FORWARD; - self.bot_cmd_keys &= ~BOT_CMD_KEY_BACKWARD; - } - else - self.bot_cmd_keys &= ~BOT_CMD_KEY_FORWARD; - break; - case "backward": - if(enabled) - { - self.bot_cmd_keys |= BOT_CMD_KEY_BACKWARD; - self.bot_cmd_keys &= ~BOT_CMD_KEY_FORWARD; - } - else - self.bot_cmd_keys &= ~BOT_CMD_KEY_BACKWARD; - break; - case "left": - if(enabled) - { - self.bot_cmd_keys |= BOT_CMD_KEY_LEFT; - self.bot_cmd_keys &= ~BOT_CMD_KEY_RIGHT; - } - else - self.bot_cmd_keys &= ~BOT_CMD_KEY_LEFT; - break; - case "right": - if(enabled) - { - self.bot_cmd_keys |= BOT_CMD_KEY_RIGHT; - self.bot_cmd_keys &= ~BOT_CMD_KEY_LEFT; - } - else - self.bot_cmd_keys &= ~BOT_CMD_KEY_RIGHT; - break; - case "jump": - if(enabled) - self.bot_cmd_keys |= BOT_CMD_KEY_JUMP; - else - self.bot_cmd_keys &= ~BOT_CMD_KEY_JUMP; - break; - case "crouch": - if(enabled) - self.bot_cmd_keys |= BOT_CMD_KEY_CROUCH; - else - self.bot_cmd_keys &= ~BOT_CMD_KEY_CROUCH; - break; - case "attack1": - if(enabled) - self.bot_cmd_keys |= BOT_CMD_KEY_ATTACK1; - else - self.bot_cmd_keys &= ~BOT_CMD_KEY_ATTACK1; - break; - case "attack2": - if(enabled) - self.bot_cmd_keys |= BOT_CMD_KEY_ATTACK2; - else - self.bot_cmd_keys &= ~BOT_CMD_KEY_ATTACK2; - break; - case "use": - if(enabled) - self.bot_cmd_keys |= BOT_CMD_KEY_USE; - else - self.bot_cmd_keys &= ~BOT_CMD_KEY_USE; - break; - case "hook": - if(enabled) - self.bot_cmd_keys |= BOT_CMD_KEY_HOOK; - else - self.bot_cmd_keys &= ~BOT_CMD_KEY_HOOK; - break; - case "chat": - if(enabled) - self.bot_cmd_keys |= BOT_CMD_KEY_CHAT; - else - self.bot_cmd_keys &= ~BOT_CMD_KEY_CHAT; - break; - default: - break; - } - - return CMD_STATUS_FINISHED; -} - -float bot_cmd_presskey() -{ - string key; - - key = bot_cmd.bot_cmd_parm_string; - - bot_cmd_keypress_handler(key,true); - - return CMD_STATUS_FINISHED; -} - -float bot_cmd_releasekey() -{ - string key; - - key = bot_cmd.bot_cmd_parm_string; - - return bot_cmd_keypress_handler(key,false); -} - -float bot_cmd_pause() -{SELFPARAM(); - self.button1 = 0; - self.button8 = 0; - self.BUTTON_USE = 0; - self.BUTTON_ATCK = 0; - self.BUTTON_JUMP = 0; - self.BUTTON_HOOK = 0; - self.BUTTON_CHAT = 0; - self.BUTTON_ATCK2 = 0; - self.BUTTON_CROUCH = 0; - - self.movement = '0 0 0'; - self.bot_cmd_keys = BOT_CMD_KEY_NONE; - - self.bot_exec_status |= BOT_EXEC_STATUS_PAUSED; - return CMD_STATUS_FINISHED; -} - -float bot_cmd_moveto() -{SELFPARAM(); - return self.cmd_moveto(bot_cmd.bot_cmd_parm_vector); -} - -float bot_cmd_movetotarget() -{SELFPARAM(); - entity e; - e = bot_getplace(bot_cmd.bot_cmd_parm_string); - if(!e) - return CMD_STATUS_ERROR; - return self.cmd_moveto(e.origin + (e.mins + e.maxs) * 0.5); -} - -float bot_cmd_resetgoal() -{SELFPARAM(); - return self.cmd_resetgoal(); -} - - -float bot_cmd_sound() -{SELFPARAM(); - 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(self, chan, sample, vol, atten); - - return CMD_STATUS_FINISHED; -} - -.entity tuba_note; -float bot_cmd_debug_assert_canfire() -{SELFPARAM(); - float f; - f = bot_cmd.bot_cmd_parm_float; - - if(self.weaponentity.state != WS_READY) - { - if(f) - { - self.colormod = '0 8 8'; - LOG_INFO("Bot ", self.netname, " using ", self.weaponname, " wants to fire, inhibited by weaponentity state\n"); - } - } - else if(ATTACK_FINISHED(self) > time) - { - if(f) - { - self.colormod = '8 0 8'; - LOG_INFO("Bot ", self.netname, " using ", self.weaponname, " wants to fire, inhibited by ATTACK_FINISHED (", ftos(ATTACK_FINISHED(self) - time), " seconds left)\n"); - } - } - else if(self.tuba_note) - { - if(f) - { - self.colormod = '8 0 0'; - LOG_INFO("Bot ", self.netname, " using ", self.weaponname, " wants to fire, bot still has an active tuba note\n"); - } - } - else - { - if(!f) - { - self.colormod = '8 8 0'; - LOG_INFO("Bot ", self.netname, " using ", self.weaponname, " thinks it has fired, but apparently did not; ATTACK_FINISHED says ", ftos(ATTACK_FINISHED(self) - time), " seconds left\n"); - } - } - - return CMD_STATUS_FINISHED; -} - -// - -void bot_command_executed(float rm) -{SELFPARAM(); - entity cmd; - - cmd = bot_cmd; - - if(rm) - bot_dequeuecommand(self, self.bot_cmd_execution_index); - - self.bot_cmd_execution_index++; -} - -void bot_setcurrentcommand() -{SELFPARAM(); - bot_cmd = world; - - if(!self.bot_cmd_current) - { - self.bot_cmd_current = spawn(); - self.bot_cmd_current.classname = "bot_cmd"; - self.bot_cmd_current.is_bot_cmd = 1; - } - - bot_cmd = self.bot_cmd_current; - if(bot_cmd.bot_cmd_index != self.bot_cmd_execution_index || self.bot_cmd_execution_index == 0) - { - if(bot_havecommand(self, self.bot_cmd_execution_index)) - { - string cmdstring; - cmdstring = bot_readcommand(self, self.bot_cmd_execution_index); - if(bot_decodecommand(cmdstring)) - { - bot_cmd.owner = self; - bot_cmd.bot_cmd_index = self.bot_cmd_execution_index; - } - else - { - // Invalid command, remove from queue - bot_cmd = world; - bot_dequeuecommand(self, self.bot_cmd_execution_index); - self.bot_cmd_execution_index++; - } - } - else - bot_cmd = world; - } -} - -void bot_resetqueues() -{ - entity cl; - - FOR_EACH_CLIENT(cl) if(cl.isbot) - { - cl.bot_cmd_execution_index = 0; - bot_clearqueue(cl); - // also, cancel all barriers - cl.bot_barrier = 0; - for(int i = 0; i < cl.bot_places_count; ++i) - { - strunzone(cl.(bot_placenames[i])); - cl.(bot_placenames[i]) = string_null; - } - cl.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() -{SELFPARAM(); - float status, ispressingkey; - - // Find command - bot_setcurrentcommand(); - - // 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 == world) - return false; - - // Ignore all commands except continue when the bot is paused - if(self.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(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 = !!bot_presskeys(); - - // Handle conditions - if (!(bot_cmd.bot_cmd_type==BOT_CMD_FI||bot_cmd.bot_cmd_type==BOT_CMD_ELSE)) - if(self.bot_cmd_condition_status & CMD_CONDITION_true && self.bot_cmd_condition_status & CMD_CONDITION_false_BLOCK) - { - bot_command_executed(true); - return -1; - } - else if(self.bot_cmd_condition_status & CMD_CONDITION_false && self.bot_cmd_condition_status & CMD_CONDITION_true_BLOCK) - { - bot_command_executed(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(); - break; - case BOT_CMD_CONTINUE: - status = bot_cmd_continue(); - break; - case BOT_CMD_WAIT: - status = bot_cmd_wait(); - break; - case BOT_CMD_WAIT_UNTIL: - status = bot_cmd_wait_until(); - break; - case BOT_CMD_TURN: - status = bot_cmd_turn(); - break; - case BOT_CMD_MOVETO: - status = bot_cmd_moveto(); - break; - case BOT_CMD_MOVETOTARGET: - status = bot_cmd_movetotarget(); - break; - case BOT_CMD_RESETGOAL: - status = bot_cmd_resetgoal(); - break; - case BOT_CMD_CC: - status = bot_cmd_cc(); - break; - case BOT_CMD_IF: - status = bot_cmd_if(); - break; - case BOT_CMD_ELSE: - status = bot_cmd_else(); - break; - case BOT_CMD_FI: - status = bot_cmd_fi(); - break; - case BOT_CMD_RESETAIM: - status = bot_cmd_resetaim(); - break; - case BOT_CMD_AIM: - status = bot_cmd_aim(); - break; - case BOT_CMD_AIMTARGET: - status = bot_cmd_aimtarget(); - break; - case BOT_CMD_PRESSKEY: - status = bot_cmd_presskey(); - break; - case BOT_CMD_RELEASEKEY: - status = bot_cmd_releasekey(); - break; - case BOT_CMD_SELECTWEAPON: - status = bot_cmd_select_weapon(); - break; - case BOT_CMD_IMPULSE: - status = bot_cmd_impulse(); - break; - case BOT_CMD_BARRIER: - status = bot_cmd_barrier(); - 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(); - break; - case BOT_CMD_DEBUG_ASSERT_CANFIRE: - status = bot_cmd_debug_assert_canfire(); - 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(self,strcat("say ^7", bot_cmd_string[bot_cmd.bot_cmd_type]," ",parms,"\n")); - } - - bot_command_executed(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 -float bot_execute_commands() -{ - float f; - do - { - f = bot_execute_commands_once(); - } - while(f < 0); - return f; -} diff --git a/qcsrc/server/bot/scripting.qh b/qcsrc/server/bot/scripting.qh deleted file mode 100644 index d5cdda9d6..000000000 --- a/qcsrc/server/bot/scripting.qh +++ /dev/null @@ -1,83 +0,0 @@ -#ifndef BOT_SCRIPTING_H -#define BOT_SCRIPTING_H - -#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(vector) cmd_moveto; -.float() 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 find_bot_by_name(string name); -entity find_bot_by_number(float number); -#endif diff --git a/qcsrc/server/bot/waypoints.qc b/qcsrc/server/bot/waypoints.qc deleted file mode 100644 index 8f50d0276..000000000 --- a/qcsrc/server/bot/waypoints.qc +++ /dev/null @@ -1,1170 +0,0 @@ -#include "waypoints.qh" -#include "../_all.qh" - -#include "bot.qh" -#include "navigation.qh" - -#include "../antilag.qh" - -#include "../../common/constants.qh" - -#include "../../warpzonelib/util_server.qh" - -// 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) -{ - entity w; - w = find(world, classname, "waypoint"); - - if (!(f & WAYPOINTFLAG_PERSONAL)) - while (w) - { - // if a matching spawnfunc_waypoint already exists, don't add a duplicate - if (boxesoverlap(m1, m2, w.absmin, w.absmax)) - return w; - w = find(w, classname, "waypoint"); - } - - w = spawn(); - w.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP; - w.classname = "waypoint"; - w.wpflags = f; - 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, PL_MIN - '1 1 0', PL_MAX + '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"); - remove(w); - return world; - } - 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() -{SELFPARAM(); - entity e; - 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(self.wpisbox), "\n"); - sm1 = self.origin + self.mins; - sm2 = self.origin + self.maxs; - for(e = world; (e = find(e, classname, "waypoint")); ) - { - if (boxesoverlap(self.absmin, self.absmax, e.absmin, e.absmax)) - { - waypoint_addlink(self, e); - waypoint_addlink(e, self); - } - else - { - ++relink_total; - if(!checkpvs(self.origin, e)) - { - ++relink_pvsculled; - continue; - } - sv = e.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 = self.origin; - em1 = e.origin + e.mins; - em2 = e.origin + e.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 (vlen(dv) >= 1050) // max search distance in XY - { - ++relink_lengthculled; - continue; - } - navigation_testtracewalk = 0; - if (!self.wpisbox) - { - tracebox(sv - PL_MIN.z * '0 0 1', PL_MIN, PL_MAX, sv, false, self); - if (!trace_startsolid) - { - //dprint("sv deviation", vtos(trace_endpos - sv), "\n"); - sv = trace_endpos + '0 0 1'; - } - } - if (!e.wpisbox) - { - tracebox(ev - PL_MIN.z * '0 0 1', PL_MIN, PL_MAX, ev, false, e); - if (!trace_startsolid) - { - //dprint("ev deviation", vtos(trace_endpos - ev), "\n"); - ev = trace_endpos + '0 0 1'; - } - } - //traceline(self.origin, e.origin, false, world); - //if (trace_fraction == 1) - if (!self.wpisbox && tracewalk(self, sv, PL_MIN, PL_MAX, ev, MOVE_NOMONSTERS)) - waypoint_addlink(self, e); - else - relink_walkculled += 0.5; - if (!e.wpisbox && tracewalk(e, ev, PL_MIN, PL_MAX, sv, MOVE_NOMONSTERS)) - waypoint_addlink(e, self); - else - relink_walkculled += 0.5; - } - } - navigation_testtracewalk = 0; - self.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 = world; - wp.wp08 = wp.wp09 = wp.wp10 = wp.wp11 = wp.wp12 = wp.wp13 = wp.wp14 = wp.wp15 = world; - wp.wp16 = wp.wp17 = wp.wp18 = wp.wp19 = wp.wp20 = wp.wp21 = wp.wp22 = wp.wp23 = world; - wp.wp24 = wp.wp25 = wp.wp26 = wp.wp27 = wp.wp28 = wp.wp29 = wp.wp30 = wp.wp31 = world; - - 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 == world) - 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 = world; - if (!(wp.wpflags & WAYPOINTFLAG_PERSONAL)) - wp.owner = world; - if (!(wp.wpflags & WAYPOINTFLAG_NORELINK)) - waypoint_clearlinks(wp); - // schedule an actual relink on next frame - wp.think = waypoint_think; - wp.nextthink = time; - wp.effects = EF_LOWPRECISION; -} - -// spawnfunc_waypoint map entity -spawnfunc(waypoint) -{ - setorigin(self, self.origin); - // schedule a relink after other waypoints have had a chance to spawn - waypoint_clearlinks(self); - //waypoint_schedulerelink(self); -} - -// 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 - remove(e); -} - -// empties the map of waypoints -void waypoint_removeall() -{ - entity head, next; - head = findchain(classname, "waypoint"); - while (head) - { - next = head.chain; - remove(head); - head = next; - } -} - -// tell all waypoints to relink -// (is this useful at all?) -void waypoint_schedulerelinkall() -{ - entity head; - relink_total = relink_walkculled = relink_pvsculled = relink_lengthculled = 0; - head = findchain(classname, "waypoint"); - while (head) - { - waypoint_schedulerelink(head); - head = head.chain; - } -} - -// Load waypoint links from file -float waypoint_load_links() -{ - string filename, s; - float file, tokens, c = 0, found; - entity wp_from = world, 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(vlen(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(vlen(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 "); - LOG_TRACE(ftos(c)); - LOG_TRACE(" waypoint links from maps/"); - LOG_TRACE(mapname); - LOG_TRACE(".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 = world, 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 "); - LOG_TRACE(filename); - LOG_TRACE(" 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(vlen(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(vlen(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 "); - LOG_TRACE(ftos(c)); - LOG_TRACE(" waypoint links from maps/"); - LOG_TRACE(mapname); - LOG_TRACE(".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 world; - } -} - -// Save all waypoint links to a file -void waypoint_save_links() -{ - string filename, s; - float file, c, i; - entity w, link; - filename = strcat("maps/", mapname); - filename = strcat(filename, ".waypoints.cache"); - file = fopen(filename, FILE_WRITE); - if (file < 0) - { - LOG_INFO("waypoint links save to "); - LOG_INFO(filename); - LOG_INFO(" failed\n"); - } - c = 0; - w = findchain(classname, "waypoint"); - while (w) - { - for(i=0;i<32;++i) - { - // :S - link = waypoint_get_link(w, i); - if(link==world) - continue; - - s = strcat(vtos(w.origin), "*", vtos(link.origin), "\n"); - fputs(file, s); - ++c; - } - w = w.chain; - } - fclose(file); - botframe_cachedwaypointlinks = true; - - LOG_INFO("saved "); - LOG_INFO(ftos(c)); - LOG_INFO(" waypoints links to maps/"); - LOG_INFO(mapname); - LOG_INFO(".waypoints.cache\n"); -} - -// save waypoints to gamedir/data/maps/mapname.waypoints -void waypoint_saveall() -{ - string filename, s; - float file, c; - entity w; - filename = strcat("maps/", mapname); - filename = strcat(filename, ".waypoints"); - file = fopen(filename, FILE_WRITE); - if (file >= 0) - { - c = 0; - w = findchain(classname, "waypoint"); - while (w) - { - if (!(w.wpflags & WAYPOINTFLAG_GENERATED)) - { - s = strcat(vtos(w.origin + w.mins), "\n"); - s = strcat(s, vtos(w.origin + w.maxs)); - s = strcat(s, "\n"); - s = strcat(s, ftos(w.wpflags)); - s = strcat(s, "\n"); - fputs(file, s); - c = c + 1; - } - w = w.chain; - } - fclose(file); - bprint("saved "); - bprint(ftos(c)); - bprint(" waypoints to maps/"); - bprint(mapname); - bprint(".waypoints\n"); - } - else - { - bprint("waypoint save to "); - bprint(filename); - bprint(" failed\n"); - } - waypoint_save_links(); - botframe_loadedforcedlinks = false; -} - -// 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 "); - LOG_TRACE(ftos(cwp)); - LOG_TRACE(" waypoints and "); - LOG_TRACE(ftos(cwb)); - LOG_TRACE(" wayboxes from maps/"); - LOG_TRACE(mapname); - LOG_TRACE(".waypoints\n"); - } - else - { - LOG_TRACE("waypoint load from "); - LOG_TRACE(filename); - LOG_TRACE(" failed\n"); - } - return cwp + cwb; -} - -vector waypoint_fixorigin(vector position) -{ - tracebox(position + '0 0 1' * (1 - PL_MIN.z), PL_MIN, PL_MAX, position + '0 0 -512', MOVE_NOMONSTERS, world); - if(trace_fraction < 1) - position = trace_endpos; - //traceline(position, position + '0 0 -512', MOVE_NOMONSTERS, world); - //print("position is ", ftos(trace_endpos_z - position_z), " above solid\n"); - return position; -} - -void waypoint_spawnforitem_force(entity e, vector org) -{ - entity w; - - // Fix the waypoint altitude if necessary - org = waypoint_fixorigin(org); - - // don't spawn an item spawnfunc_waypoint if it already exists - w = findchain(classname, "waypoint"); - while (w) - { - if (w.wpisbox) - { - if (boxesoverlap(org, org, w.absmin, w.absmax)) - { - e.nearestwaypoint = w; - return; - } - } - else - { - if (vlen(w.origin - org) < 16) - { - e.nearestwaypoint = w; - return; - } - } - w = w.chain; - } - 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(vector position) -{SELFPARAM(); - 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 = world; - w.nearestwaypointtimeout = 0; - w.owner = self; - - waypoint_schedulerelink(w); - - return w; -} - -void botframe_showwaypointlinks() -{ - entity player, head, w; - if (time < botframe_waypointeditorlightningtime) - return; - botframe_waypointeditorlightningtime = time + 0.5; - player = find(world, classname, "player"); - while (player) - { - if (!player.isbot) - if (player.flags & FL_ONGROUND || player.waterlevel > WATERLEVEL_NONE) - { - //navigation_testtracewalk = true; - head = navigation_findnearestwaypoint(player, false); - // print("currently selected WP is ", etos(head), "\n"); - //navigation_testtracewalk = false; - if (head) - { - w = head ;if (w) te_lightning2(world, w.origin, player.origin); - w = head.wp00;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp01;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp02;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp03;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp04;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp05;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp06;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp07;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp08;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp09;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp10;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp11;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp12;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp13;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp14;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp15;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp16;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp17;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp18;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp19;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp20;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp21;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp22;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp23;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp24;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp25;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp26;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp27;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp28;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp29;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp30;if (w) te_lightning2(world, w.origin, head.origin); - w = head.wp31;if (w) te_lightning2(world, w.origin, head.origin); - } - } - player = find(player, classname, "player"); - } -} - -float botframe_autowaypoints_fixdown(vector v) -{ - tracebox(v, PL_MIN, PL_MAX, v + '0 0 -64', MOVE_NOMONSTERS, world); - if(trace_fraction >= 1) - return 0; - return 1; -} - -float botframe_autowaypoints_createwp(vector v, entity p, .entity fld, float f) -{ - entity w; - - w = find(world, classname, "waypoint"); - while (w) - { - // if a matching spawnfunc_waypoint already exists, don't add a duplicate - if (boxesoverlap(v - '32 32 32', v + '32 32 32', w.absmin, w.absmax)) - //if (boxesoverlap(v - '4 4 4', v + '4 4 4', w.absmin, w.absmax)) - return 0; - w = find(w, classname, "waypoint"); - } - - 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 world, 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; - bestdist = maxdist; - w = find(world, classname, "waypoint"); - while (w) - { - if(w != wp && !(w.wpflags & WAYPOINTFLAG_NORELINK)) - { - float d; - d = vlen(wp.origin - w.origin) + vlen(w.origin - porg); - if(d < bestdist) - if(navigation_waypoint_will_link(wp.origin, w.origin, p, walkfromwp, 1050)) - if(navigation_waypoint_will_link(w.origin, porg, p, walkfromwp, 1050)) - { - bestdist = d; - p.(fld) = w; - } - } - w = find(w, classname, "waypoint"); - } - 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, 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, world, 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() -{ - entity w, w1, w2; - float i, j, k; - for (w = world; (w = findfloat(w, bot_pickup, true)); ) - { - // NOTE: this protects waypoints if they're the ONLY nearest - // waypoint. That's the intention. - navigation_findnearestwaypoint(w, false); // Walk TO item. - navigation_findnearestwaypoint(w, true); // Walk FROM item. - } - for (w = world; (w = find(w, classname, "waypoint")); ) - { - w.wpflags |= WAYPOINTFLAG_DEAD_END; - w.wpflags &= ~WAYPOINTFLAG_USEFUL; - // WP is useful if: - if (w.wpflags & WAYPOINTFLAG_ITEM) - w.wpflags |= WAYPOINTFLAG_USEFUL; - if (w.wpflags & WAYPOINTFLAG_TELEPORT) - w.wpflags |= WAYPOINTFLAG_USEFUL; - if (w.wpflags & WAYPOINTFLAG_PROTECTED) - w.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. - for (w1 = world; (w1 = find(w1, classname, "waypoint")); ) - { - if (w1.wpflags & WAYPOINTFLAG_PERSONAL) - continue; - for (i = 0; i < 32; ++i) - { - w = waypoint_get_link(w1, i); - if (!w) - break; - if (w.wpflags & WAYPOINTFLAG_PERSONAL) - continue; - if (w.wpflags & WAYPOINTFLAG_USEFUL) - continue; - for (j = 0; j < 32; ++j) - { - w2 = waypoint_get_link(w, j); - if (!w2) - break; - if (w1 == w2) - continue; - if (w2.wpflags & WAYPOINTFLAG_PERSONAL) - continue; - // If we got here, w1 != w2 exist with w1 -> w - // and w -> w2. That means the waypoint is not - // a dead end. - w.wpflags &= ~WAYPOINTFLAG_DEAD_END; - for (k = 0; k < 32; ++k) - { - if (waypoint_get_link(w1, k) == w2) - continue; - // IF WE GET HERE, w is proven useful - // to get from w1 to w2! - w.wpflags |= WAYPOINTFLAG_USEFUL; - goto next; - } - } -:next - } - } - // d) The waypoint is a dead end. Dead end waypoints must be kept as - // they are needed to complete routes while autowaypointing. - - for (w = world; (w = find(w, classname, "waypoint")); ) - { - if (!(w.wpflags & (WAYPOINTFLAG_USEFUL | WAYPOINTFLAG_DEAD_END))) - { - LOG_INFOF("Removed a waypoint at %v. Try again for more!\n", w.origin); - te_explosion(w.origin); - waypoint_remove(w); - break; - } - } - for (w = world; (w = find(w, classname, "waypoint")); ) - w.wpflags &= ~(WAYPOINTFLAG_USEFUL | WAYPOINTFLAG_DEAD_END); // temp flag -} - -void botframe_autowaypoints() -{ - entity p; - FOR_EACH_REALPLAYER(p) - { - if(p.deadflag) - continue; - // going back is broken, so only fix waypoints to walk TO the player - //botframe_autowaypoints_fix(p, false, botframe_autowaypoints_lastwp0); - botframe_autowaypoints_fix(p, 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 fde524bb9..000000000 --- a/qcsrc/server/bot/waypoints.qh +++ /dev/null @@ -1,69 +0,0 @@ -#ifndef WAYPOINTS_H -#define WAYPOINTS_H -/* - * Globals and Fields - */ - -const int WAYPOINTFLAG_GENERATED = 8388608; -const int WAYPOINTFLAG_ITEM = 4194304; -const int WAYPOINTFLAG_TELEPORT = 2097152; -const int WAYPOINTFLAG_NORELINK = 1048576; -const int WAYPOINTFLAG_PERSONAL = 524288; -const int WAYPOINTFLAG_PROTECTED = 262144; // Useless WP detection never kills these. -const int WAYPOINTFLAG_USEFUL = 131072; // Useless WP detection temporary flag. -const int WAYPOINTFLAG_DEAD_END = 65536; // 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(); -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(vector position); - -vector waypoint_fixorigin(vector position); - -void botframe_autowaypoints(); -#endif diff --git a/qcsrc/server/cl_client.qc b/qcsrc/server/cl_client.qc index 0d0d356a9..127080323 100644 --- a/qcsrc/server/cl_client.qc +++ b/qcsrc/server/cl_client.qc @@ -20,8 +20,7 @@ #include "campaign.qh" #include "command/common.qh" -#include "bot/bot.qh" -#include "bot/navigation.qh" +#include "bot/api.qh" #include "../common/vehicles/all.qh" diff --git a/qcsrc/server/cl_impulse.qc b/qcsrc/server/cl_impulse.qc index a594c83c7..178902375 100644 --- a/qcsrc/server/cl_impulse.qc +++ b/qcsrc/server/cl_impulse.qc @@ -1,12 +1,9 @@ #include "_all.qh" #include "round_handler.qh" -#include "bot/waypoints.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 e94ba6283..82c456f0a 100644 --- a/qcsrc/server/cl_player.qc +++ b/qcsrc/server/cl_player.qc @@ -1,7 +1,6 @@ #include "cl_player.qh" #include "_all.qh" -#include "bot/bot.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 d08588ff9..03d47c747 100644 --- a/qcsrc/server/command/sv_cmd.qc +++ b/qcsrc/server/command/sv_cmd.qc @@ -17,10 +17,6 @@ #include "../playerdemo.qh" #include "../teamplay.qh" -#include "../bot/bot.qh" -#include "../bot/navigation.qh" -#include "../bot/scripting.qh" - #include "../mutators/mutators_include.qh" #include "../../common/constants.qh" diff --git a/qcsrc/server/defs.qh b/qcsrc/server/defs.qh index 4a7edc2b9..102309a08 100644 --- a/qcsrc/server/defs.qh +++ b/qcsrc/server/defs.qh @@ -45,7 +45,6 @@ float sv_clones; float sv_foginterval; float player_count; -float currentbots; float bots_would_leave; void UpdateFrags(entity player, float f); diff --git a/qcsrc/server/g_world.qc b/qcsrc/server/g_world.qc index d6ffeeb5d..efc3b9504 100644 --- a/qcsrc/server/g_world.qc +++ b/qcsrc/server/g_world.qc @@ -3,7 +3,6 @@ #include "anticheat.qh" #include "antilag.qh" -#include "bot/bot.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 d2066b9ab..4a52ffabf 100644 --- a/qcsrc/server/mutators/gamemode.qh +++ b/qcsrc/server/mutators/gamemode.qh @@ -12,14 +12,6 @@ #include "../scores.qh" #include "../scores_rules.qh" -#include "../bot/bot.qh" -#include "../bot/navigation.qh" -#include "../bot/waypoints.qh" -#include "../bot/havocbot/roles.qh" -#include "../bot/havocbot/role_keyhunt.qh" - -#include "../bot/havocbot/havocbot.qh" - #include "../command/vote.qh" #include "../../common/monsters/all.qh" diff --git a/qcsrc/server/mutators/gamemode_assault.qh b/qcsrc/server/mutators/gamemode_assault.qh index 1266492ca..7635a9395 100644 --- a/qcsrc/server/mutators/gamemode_assault.qh +++ b/qcsrc/server/mutators/gamemode_assault.qh @@ -21,9 +21,6 @@ void() havocbot_role_ast_offense; void(entity bot) havocbot_ast_reset_role; -void(float ratingscale, vector org, float sradius) havocbot_goalrating_items; -void(float ratingscale, vector org, float sradius) havocbot_goalrating_enemyplayers; - // scoreboard stuff const float ST_ASSAULT_OBJECTIVES = 1; const float SP_ASSAULT_OBJECTIVES = 4; diff --git a/qcsrc/server/mutators/gamemode_keyhunt.qc b/qcsrc/server/mutators/gamemode_keyhunt.qc index be45c3db0..17f2da70b 100644 --- a/qcsrc/server/mutators/gamemode_keyhunt.qc +++ b/qcsrc/server/mutators/gamemode_keyhunt.qc @@ -50,7 +50,7 @@ float kh_Team_ByID(float t) return 0; } -//entity kh_worldkeylist; +entity kh_worldkeylist; .entity kh_worldkeynext; entity kh_controller; //float kh_tracking_enabled; diff --git a/qcsrc/server/mutators/gamemode_nexball.qc b/qcsrc/server/mutators/gamemode_nexball.qc index f65e05843..e95231ea4 100644 --- a/qcsrc/server/mutators/gamemode_nexball.qc +++ b/qcsrc/server/mutators/gamemode_nexball.qc @@ -3,6 +3,8 @@ #include "gamemode.qh" +#include "../../common/triggers/trigger/jumppads.qh" + float autocvar_g_nexball_basketball_bouncefactor; float autocvar_g_nexball_basketball_bouncestop; float autocvar_g_nexball_basketball_carrier_highspeed; diff --git a/qcsrc/server/mutators/mutator.qh b/qcsrc/server/mutators/mutator.qh index 39adb175c..70c912562 100644 --- a/qcsrc/server/mutators/mutator.qh +++ b/qcsrc/server/mutators/mutator.qh @@ -13,14 +13,6 @@ #include "../scores.qh" #include "../scores_rules.qh" -#include "../bot/bot.qh" -#include "../bot/navigation.qh" -#include "../bot/waypoints.qh" - -#include "../bot/havocbot/havocbot.qh" -#include "../bot/havocbot/roles.qh" -#include "../bot/havocbot/role_keyhunt.qh" - #include "../command/vote.qh" #include "../command/common.qh" diff --git a/qcsrc/server/pathlib/path_waypoint.qc b/qcsrc/server/pathlib/path_waypoint.qc index 4c3a7a1aa..615840d51 100644 --- a/qcsrc/server/pathlib/path_waypoint.qc +++ b/qcsrc/server/pathlib/path_waypoint.qc @@ -1,5 +1,3 @@ -#include "../bot/waypoints.qh" - #include "pathlib.qh" #include "main.qh" diff --git a/qcsrc/server/progs.inc b/qcsrc/server/progs.inc index adb8cf632..75c00833f 100644 --- a/qcsrc/server/progs.inc +++ b/qcsrc/server/progs.inc @@ -45,16 +45,6 @@ #include "t_quake3.qc" #include "t_quake.qc" -#include "bot/aim.qc" -#include "bot/bot.qc" -#include "bot/navigation.qc" -#include "bot/scripting.qc" -#include "bot/waypoints.qc" - -#include "bot/havocbot/havocbot.qc" -#include "bot/havocbot/role_keyhunt.qc" -#include "bot/havocbot/roles.qc" - #include "command/all.qc" #include "mutators/mutators_include.qc" @@ -120,6 +110,8 @@ #include "../warpzonelib/server.qc" #include "../warpzonelib/util_server.qc" +#include "bot/impl.qc" + #if BUILD_MOD #include "../../mod/server/progs.inc" #endif diff --git a/qcsrc/server/race.qc b/qcsrc/server/race.qc index fbf1eaa18..4c451caa7 100644 --- a/qcsrc/server/race.qc +++ b/qcsrc/server/race.qc @@ -5,8 +5,6 @@ #include "portals.qh" #include "scores.qh" #include "spawnpoints.qh" -#include "bot/waypoints.qh" -#include "bot/navigation.qh" #include "command/getreplies.qh" #include "../common/deathtypes.qh" #include "../common/notifications.qh" diff --git a/qcsrc/server/sv_main.qc b/qcsrc/server/sv_main.qc index 138051827..9dfd6b807 100644 --- a/qcsrc/server/sv_main.qc +++ b/qcsrc/server/sv_main.qc @@ -4,9 +4,6 @@ #include "g_hook.qh" #include "g_world.qh" -#include "bot/bot.qh" -#include "bot/waypoints.qh" - #include "command/common.qh" #include "mutators/mutators_include.qh" diff --git a/qcsrc/server/t_items.qc b/qcsrc/server/t_items.qc index e93ee2d7e..e19b5d48f 100644 --- a/qcsrc/server/t_items.qc +++ b/qcsrc/server/t_items.qc @@ -5,9 +5,6 @@ #if defined(SVQC) #include "_all.qh" - #include "bot/bot.qh" - #include "bot/waypoints.qh" - #include "mutators/mutators_include.qh" #include "weapons/common.qh" diff --git a/qcsrc/server/teamplay.qc b/qcsrc/server/teamplay.qc index 6defa736f..8f0120cd8 100644 --- a/qcsrc/server/teamplay.qc +++ b/qcsrc/server/teamplay.qc @@ -6,8 +6,6 @@ #include "scores.qh" #include "scores_rules.qh" -#include "bot/bot.qh" - #include "command/vote.qh" #include "mutators/mutators_include.qh"