diff --git a/codxe.vcxproj b/codxe.vcxproj
index c99d244..617d38a 100644
--- a/codxe.vcxproj
+++ b/codxe.vcxproj
@@ -97,6 +97,7 @@
+
diff --git a/resources/iw3/_codxe/mods/codjumper/maps/mp/gametypes/cj.gsc b/resources/iw3/_codxe/mods/codjumper/maps/mp/gametypes/cj.gsc
index 741ce92..b11c2c7 100644
--- a/resources/iw3/_codxe/mods/codjumper/maps/mp/gametypes/cj.gsc
+++ b/resources/iw3/_codxe/mods/codjumper/maps/mp/gametypes/cj.gsc
@@ -2753,6 +2753,7 @@ generateMenuOptions()
self addMenuOption("bot_action_menu", "Auto Mantle ON/OFF", ::toggleAutoMantle);
self addMenuOption("bot_action_menu", "Trigger Distance UP", ::modifyTriggerDistance, 10);
self addMenuOption("bot_action_menu", "Trigger Distance DOWN", ::modifyTriggerDistance, -10);
+ self addMenuOption("bot_action_menu", "Toggle stance", ::CycleBotStance);
}
// Player model menu
@@ -2777,6 +2778,31 @@ generateMenuOptions()
self addMenuOption("graphics_menu", "Toggle Greenscreen", ::toggleGreenscreen);
}
+CycleBotStance()
+{
+ bot = self.cj["bots"][self.cj["botnumber"]];
+ if(!isdefined(bot))
+ {
+ self iprintln("^1bot not found.");
+ return;
+ }
+
+ stance = bot GetStance();
+ if (stance == "stand")
+ {
+ bot botaction("+gocrouch");
+ }
+ else if (stance == "crouch")
+ {
+ bot botaction("-gocrouch");
+ bot botaction("+goprone");
+ }
+ else if (stance == "prone")
+ {
+ bot botaction("-goprone"); // returns to stand
+ }
+}
+
toggleRpgLookdown()
{
if (!isdefined(self.cj["settings"]["tas_rpg_lookdown"]))
@@ -3242,12 +3268,14 @@ startAutoMantle()
{
if (distance(botEye, self getorigin()) < self.triggerDistance)
{
- bot botjump();
+ bot botaction("+gostand");
self waittill("position_loaded");
// wait for bot to finish mantling before loading position
if (bot ismantling())
wait 0.5;
+ bot botstop();
+
bot loadPos(0);
}
wait 0.05;
diff --git a/src/game/iw3/mp/components/sv_bots.cpp b/src/game/iw3/mp/components/sv_bots.cpp
new file mode 100644
index 0000000..9194c5d
--- /dev/null
+++ b/src/game/iw3/mp/components/sv_bots.cpp
@@ -0,0 +1,134 @@
+#include "pch.h"
+#include "g_scr_main.h"
+#include "sv_bots.h"
+
+#define KEY_MASK_FIRE 1
+#define KEY_MASK_SPRINT 2
+#define KEY_MASK_MELEE 4
+#define KEY_MASK_RELOAD 16
+#define KEY_MASK_LEANLEFT 64
+#define KEY_MASK_LEANRIGHT 128
+#define KEY_MASK_PRONE 256
+#define KEY_MASK_CROUCH 512
+#define KEY_MASK_JUMP 1024
+#define KEY_MASK_ADS_MODE 2048
+#define KEY_MASK_TEMP_ACTION 4096
+#define KEY_MASK_HOLDBREATH 8192
+#define KEY_MASK_FRAG 16384
+#define KEY_MASK_SMOKE 32768
+#define KEY_MASK_NIGHTVISION 262144
+#define KEY_MASK_ADS 524288
+#define KEY_MASK_USE 8
+#define KEY_MASK_USERELOAD 32
+#define BUTTON_ATTACK KEY_MASK_FIRE
+#define MAX_CLIENTS 18
+
+namespace iw3
+{
+namespace mp
+{
+
+struct BotMovementInfo_t
+{
+ int buttons;
+};
+
+BotMovementInfo_t g_botai[MAX_CLIENTS];
+
+Detour SV_BotUserMove_Detour;
+void SV_BotUserMove_Stub(client_t *cl)
+{
+ if (!cl->gentity)
+ return;
+
+ const int clientNum = cl - svsHeader->clients;
+
+ usercmd_s cmd;
+ memset(&cmd, 0, sizeof(cmd));
+
+ cmd.serverTime = svsHeader->time;
+
+ const playerState_s *ps = SV_GameClientNum(clientNum);
+ cmd.weapon = (unsigned char)ps->weapon;
+
+ if (g_clients[clientNum].sess.archiveTime == 0)
+ {
+ cmd.buttons = g_botai[clientNum].buttons;
+ }
+
+ cl->header.deltaMessage = cl->header.netchan.outgoingSequence - 1;
+ SV_ClientThink(cl, &cmd);
+}
+
+struct BotAction_t
+{
+ const char *action;
+ int key;
+};
+
+const BotAction_t BotActions[] = {{"gostand", KEY_MASK_JUMP}, {"gocrouch", KEY_MASK_CROUCH},
+ {"goprone", KEY_MASK_PRONE}, {"fire", KEY_MASK_FIRE},
+ {"melee", KEY_MASK_MELEE}, {"frag", KEY_MASK_FRAG},
+ {"smoke", KEY_MASK_SMOKE}, {"reload", KEY_MASK_RELOAD},
+ {"sprint", KEY_MASK_SPRINT}, {"leanleft", KEY_MASK_LEANLEFT},
+ {"leanright", KEY_MASK_LEANRIGHT}, {"ads", KEY_MASK_ADS_MODE | KEY_MASK_ADS},
+ {"holdbreath", KEY_MASK_HOLDBREATH}, {"activate", KEY_MASK_USE}};
+
+static void Scr_BotAction(scr_entref_t entref)
+{
+ GetPlayerEntity(entref);
+
+ if (Scr_GetNumParam() != 1)
+ Scr_Error("Usage: botAction();");
+
+ const char *action = Scr_GetString(0);
+
+ if (action[0] != '+' && action[0] != '-')
+ Scr_ParamError(0, "Sign for bot action must be '+' or '-'.");
+
+ bool key_found = false;
+ for (size_t i = 0; i < ARRAYSIZE(g_botai); ++i)
+ {
+ if (!stricmp(&action[1], BotActions[i].action))
+ {
+ key_found = true;
+ if (action[0] == '+')
+ g_botai[entref.entnum].buttons |= BotActions[i].key;
+ else
+ g_botai[entref.entnum].buttons &= ~(BotActions[i].key);
+
+ return;
+ }
+ }
+
+ if (!key_found)
+ {
+ Scr_ParamError(0, va("Unknown bot action."));
+ }
+}
+
+static void Scr_BotStop(scr_entref_t entref)
+{
+ GetPlayerEntity(entref);
+
+ if (Scr_GetNumParam() != 0)
+ Scr_Error("Usage: botStop();");
+
+ g_botai[entref.entnum].buttons = 0;
+}
+
+sv_bots::sv_bots()
+{
+ SV_BotUserMove_Detour = Detour(SV_BotUserMove, SV_BotUserMove_Stub);
+ SV_BotUserMove_Detour.Install();
+
+ Scr_AddMethod("botaction", Scr_BotAction, 0);
+ Scr_AddMethod("botstop", Scr_BotStop, 0);
+}
+
+sv_bots::~sv_bots()
+{
+ SV_BotUserMove_Detour.Remove();
+}
+} // namespace mp
+} // namespace iw3
diff --git a/src/game/iw3/mp/components/sv_bots.h b/src/game/iw3/mp/components/sv_bots.h
new file mode 100644
index 0000000..c1a554e
--- /dev/null
+++ b/src/game/iw3/mp/components/sv_bots.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "pch.h"
+
+namespace iw3
+{
+namespace mp
+{
+class sv_bots : public Module
+{
+ public:
+ sv_bots();
+ ~sv_bots();
+
+ const char *get_name() override
+ {
+ return "sv_bots";
+ };
+};
+} // namespace mp
+} // namespace iw3
diff --git a/src/game/iw3/mp/main.cpp b/src/game/iw3/mp/main.cpp
index f5685d6..4874b94 100644
--- a/src/game/iw3/mp/main.cpp
+++ b/src/game/iw3/mp/main.cpp
@@ -11,6 +11,7 @@
#include "components/pm.h"
#include "components/scr_parser.h"
#include "components/scr_vm_functions.h"
+#include "components/sv_bots.h"
#include "common/config.h"
// Structure to hold data for the active keyboard request
@@ -1373,51 +1374,6 @@ void UI_Refresh_Hook(int localClientNum)
CheckKeyboardCompletion();
}
-struct BotAction
-{
- bool jump;
-};
-
-// map of client index to bot action
-std::map botActions;
-
-void GScr_BotJump(scr_entref_t entref)
-{
- client_t *cl = &svsHeader->clients[entref.entnum];
-
- if (cl->header.state && cl->header.netchan.remoteAddress.type == NA_BOT)
- {
- botActions[entref.entnum].jump = true;
- }
-}
-
-Detour SV_ClientThinkDetour;
-
-// TODO: maybe recreate the original and call it in the hook
-void SV_ClientThinkHook(client_t *cl, usercmd_s *cmd)
-{
- // Check if the client is a bot
- if (cl->header.state && cl->header.netchan.remoteAddress.type == NA_BOT)
- {
- // Reset bot's movement and actions set in SV_BotUserMove
- cmd->forwardmove = 0;
- cmd->rightmove = 0;
- cmd->buttons = 0;
-
- int clientIndex = cl - svsHeader->clients;
- if (botActions.find(clientIndex) != botActions.end())
- {
- if (botActions[clientIndex].jump)
- {
- cmd->buttons = 1024; // BUTTON_JUMP
- botActions[clientIndex].jump = false;
- }
- }
- }
-
- SV_ClientThinkDetour.GetOriginal()(cl, cmd);
-}
-
Detour Pmove_Detour;
// https://github.com/kejjjjj/iw3sptool/blob/17b669233a1ad086deed867469dc9530b84c20e6/iw3sptool/bg/bg_pmove.cpp#L11
@@ -1492,6 +1448,7 @@ IW3_MP_Plugin::IW3_MP_Plugin()
RegisterModule(new mpsp());
RegisterModule(new scr_parser());
RegisterModule(new scr_vm_functions());
+ RegisterModule(new sv_bots());
UI_Refresh_Detour = Detour(UI_Refresh, UI_Refresh_Hook);
UI_Refresh_Detour.Install();
@@ -1517,11 +1474,6 @@ IW3_MP_Plugin::IW3_MP_Plugin()
cmd_function_s *cmdinput_VAR = new cmd_function_s;
Cmd_AddCommandInternal("cmdinput", Cmd_cmdinput_f, cmdinput_VAR);
- Scr_AddMethod("botjump", GScr_BotJump, 0);
-
- SV_ClientThinkDetour = Detour(SV_ClientThink, SV_ClientThinkHook);
- SV_ClientThinkDetour.Install();
-
Dvar_RegisterBool("pm_fixed_fps_enable", false, 0, "Enable fixed FPS mode");
Dvar_RegisterInt("pm_fixed_fps", 250, 0, 1000, 0, "Fixed FPS value");
Pmove_Detour = Detour(Pmove, Pmove_Hook);
@@ -1540,7 +1492,6 @@ IW3_MP_Plugin::~IW3_MP_Plugin()
Load_MapEntsPtr_Detour.Remove();
R_StreamLoadFileSynchronously_Detour.Remove();
- SV_ClientThinkDetour.Remove();
Pmove_Detour.Remove();
}