adding some states again for following or mimicking movement, also with coordinates to know if coming too far off course

This commit is contained in:
jenz 2026-07-14 11:36:55 +02:00
parent e3cef31246
commit 07d344e730

View File

@ -3,32 +3,78 @@
#define DEBUG
#define PLUGIN_AUTHOR "jenz"
#define PLUGIN_VERSION "1.9"
#define PLUGIN_VERSION "2.2"
#define generic_length 256
#include <sourcemod>
#include <sdktools>
#include <AFKManager>
#include <sdkhooks>
#include <PlayerManager>
#include <cstrike>
#include <socket>
#include <UNLOZE.secret>
#include <mapchooser_extended>
#include <smlib>
int target_client[5]; //4 autism bots.
#define BUFFER_SIZE 198 // ~2 seconds at 66 tick
// ~5.3s of history at 66 tick. Plenty of headroom for approach + backlog +
// resync cycles. At ~48 bytes/frame this is only a few hundred KB total.
#define BUFFER_SIZE 350
enum struct FrameData {
int buttons;
float vel[3];
float angles[3];
float origin[3]; // where the real client actually was when this frame was recorded
int impulse;
int weaponSlot; // 0=none/untracked, 1=primary, 2=secondary, 3=knife, 4=grenade
}
FrameData g_Buffer[MAXPLAYERS+1][BUFFER_SIZE];
int g_WriteHead[MAXPLAYERS+1]; // where were writing next
int g_FrameCount[MAXPLAYERS+1]; // total frames captured
int g_DelayTicks = 128; // delay
int g_WriteHead[MAXPLAYERS+1]; // where were writing next, indexed by REAL client
int g_FrameCount[MAXPLAYERS+1]; // total frames captured, indexed by REAL client
// --- movement state machine (bots only) ---
#define BOT_STATE_IDLE 0
#define BOT_STATE_APPROACH 1
#define BOT_STATE_MIMIC 2
#define ARRIVE_RADIUS 72.0 // horizontal distance to anchor that counts as "arrived"
#define ARRIVE_Z_SLACK 128.0 // max height difference to anchor that still counts as "arrived"
#define APPROACH_SPEED 250.0 // forward move value while walking to the anchor
// Must stay comfortably bigger than ARRIVE_RADIUS or the bot can flap
// between MIMIC and APPROACH right at the boundary.
#define DESYNC_RADIUS 250.0 // if actual pos strays this far from the frame its about to play, resync
// Anti-loop / anti-stuck tuning
#define RESYNC_COOLDOWN 3.0 // min seconds between two resyncs; drift during cooldown just keeps mimicking
#define MAX_RESYNC_STRIKES 3 // this many resyncs on the same target -> give up, pick a new target
#define STUCK_CHECK_INTERVAL 4.0 // seconds between stuck checks while approaching
#define STUCK_MOVE_EPSILON 50.0 // if the bot moved less than this between checks, it counts as stuck
#define MAX_STUCK_STRIKES 2 // first strike re-anchors + jumps, second strike drops the target
int g_BotState[MAXPLAYERS+1]; // indexed by BOT client
float g_AnchorPos[MAXPLAYERS+1][3]; // indexed by BOT client - the single fixed point it walks to
int g_ReadHead[MAXPLAYERS+1]; // indexed by BOT client - read index into g_Buffer[target][]
int g_LastWeaponSlot[MAXPLAYERS+1]; // indexed by BOT client - last weaponSlot category we told it to switch to
// Anti-loop / anti-stuck state, all indexed by BOT client
float g_LastResyncTime[MAXPLAYERS+1]; // GetGameTime() of the last drift-triggered resync
int g_ResyncStrikes[MAXPLAYERS+1]; // consecutive resyncs against the current target
float g_StuckCheckTime[MAXPLAYERS+1]; // GetGameTime() of the last stuck check
float g_StuckCheckPos[MAXPLAYERS+1][3]; // where the bot was at the last stuck check
int g_StuckStrikes[MAXPLAYERS+1]; // consecutive stuck detections while approaching
bool g_WantJump[MAXPLAYERS+1]; // one-shot: press jump on the next approach tick (unstick aid)
// Weapon category is cheap to get slightly stale, so we only actually
// re-check it every WEAPON_CHECK_INTERVAL seconds per real client instead of
// every tick, and just keep writing the cached value into each frame.
#define WEAPON_CHECK_INTERVAL 5.0
int g_CachedWeaponSlot[MAXPLAYERS+1]; // indexed by REAL client - last computed category
float g_NextWeaponCheck[MAXPLAYERS+1]; // indexed by REAL client - GetGameTime() of next allowed check
int ports[7] = {48470, 48471, 48472, 48473, 48474, 48479, 48480}; //last three indexes are ports its sending udp from in plugin
int server_ports[2] = {27015, 27035}; //server ports: ze, ze2
@ -51,7 +97,7 @@ public Plugin myinfo =
{
name = "autism bot simulate player movement",
author = PLUGIN_AUTHOR,
description = "just copies player movement to the autism bots",
description = "just copies player movement to the autism bots. now also AI slopped.",
version = PLUGIN_VERSION,
url = ""
};
@ -61,6 +107,7 @@ public void OnPluginStart()
//talking
RegConsoleCmd("sm_autism", cmd_talk, "talking to the bot through java application");
RegConsoleCmd("sm_botrtv", cmd_botrtv, "making bots rtv");
RegAdminCmd("sm_botstate", cmd_botstate, ADMFLAG_GENERIC, "debug: show state of each autism bot");
HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy);
@ -131,6 +178,29 @@ public void OnPluginEnd()
}
}
// Classifies the clients currently held weapon into a slot category. This
// servers loadout is fixed: raw inventory slot 0=primary, 1=secondary,
// 2=knife, 3=HE grenade (no smoke/flash, no slot 4 ever exists), so we can
// just compare the active weapon against each fixed slot directly.
stock int GetActiveWeaponSlotCategory(int client)
{
int activeWeapon = GetEntPropEnt(client, Prop_Data, "m_hActiveWeapon", 0);
if (activeWeapon <= -1 || !IsValidEntity(activeWeapon))
{
return 0;
}
for (int rawSlot = 0; rawSlot <= 3; rawSlot++)
{
if (activeWeapon == GetPlayerWeaponSlot(client, rawSlot))
{
return rawSlot + 1; // 1=primary, 2=secondary, 3=knife, 4=grenade
}
}
return 0;
}
public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float vel[3], const float angles[3], int weapon, int subtype, int cmdnum, int tickcount, int seed, const int mouse[2])
{
if (!IsValidClient(client) || IsFakeClient(client))
@ -142,9 +212,129 @@ public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float
g_Buffer[client][head].impulse = impulse;
g_Buffer[client][head].vel = vel;
g_Buffer[client][head].angles = angles;
GetClientAbsOrigin(client, g_Buffer[client][head].origin);
float now = GetGameTime();
if (now >= g_NextWeaponCheck[client])
{
g_CachedWeaponSlot[client] = GetActiveWeaponSlotCategory(client);
g_NextWeaponCheck[client] = now + WEAPON_CHECK_INTERVAL;
}
g_Buffer[client][head].weaponSlot = g_CachedWeaponSlot[client];
g_WriteHead[client] = (head + 1) % BUFFER_SIZE;
g_FrameCount[client]++;
// If any bot currently mimicking/approaching this client is about to have
// its unread backlog overwritten by this write, nudge its read head
// forward so it never reads stale/overwritten frames. With 4 bots max
// this loop is cheap.
for (int slot = 1; slot <= 4; slot++)
{
if (target_client[slot] != client)
{
continue;
}
int bot = GetBotClientForSlot(slot);
if (bot == -1 || g_BotState[bot] == BOT_STATE_IDLE)
{
continue;
}
if (g_ReadHead[bot] == g_WriteHead[client])
{
g_ReadHead[bot] = (g_ReadHead[bot] + 1) % BUFFER_SIZE;
}
}
}
// Horizontal (XY) distance between two points. The old 3D check made bots
// standing directly under/over an anchor on another floor "never arrive",
// which also produced the crazy spinning (near-vertical direction vectors
// have unstable yaw).
stock float GetHorizontalDistance(const float a[3], const float b[3])
{
float dx = a[0] - b[0];
float dy = a[1] - b[1];
return SquareRoot(dx * dx + dy * dy);
}
// Faces the bot toward `anchor` (yaw from horizontal components only, so a
// mostly-vertical offset cant destabilize it) and pushes it forward. Shared
// by the normal APPROACH state and by the mid-mimic resync path below.
void ApplyApproachMovement(int bot, const float anchor[3], int &buttons, int &impulse, float vel[3], float angles[3])
{
float botPos[3];
GetClientAbsOrigin(bot, botPos);
float dir[3];
SubtractVectors(anchor, botPos, dir);
dir[2] = 0.0; // yaw only - never derive facing from the vertical component
impulse = 0;
vel[1] = 0.0;
vel[2] = 0.0;
angles[0] = 0.0;
angles[2] = 0.0;
if (GetVectorLength(dir) < 1.0)
{
// Horizontally on top of the anchor but not "arrived" (must be a
// height mismatch). Running forward would just grind into geometry
// and the yaw would be garbage - hold position, keep current yaw,
// and let stuck detection deal with it.
buttons = 0;
vel[0] = 0.0;
return;
}
float faceAngles[3];
GetVectorAngles(dir, faceAngles);
angles[1] = faceAngles[1];
buttons = IN_FORWARD;
vel[0] = APPROACH_SPEED;
if (g_WantJump[bot])
{
buttons |= IN_JUMP;
g_WantJump[bot] = false;
}
}
// Directly sets the bots active weapon to match the category the target is
// holding. Fixed slot layout: category 1-4 maps straight to raw inventory
// slot 0-3 (primary, secondary, knife, HE grenade). Uses smlibs
// Client_SetActiveWeapon, which manipulates the weapon state via SDK calls
// rather than a queued client command - this is why it can be called
// synchronously from inside OnPlayerRunCmd without the reentrancy problems
// a FakeClientCommandEx("slotN") had.
void SyncWeaponSlot(int bot, int wantSlot)
{
if (wantSlot == 0 || wantSlot == g_LastWeaponSlot[bot])
{
return;
}
int weapon = GetPlayerWeaponSlot(bot, wantSlot - 1);
if (weapon != -1 && Weapon_IsValid(weapon))
{
Client_SetActiveWeapon(bot, weapon);
}
g_LastWeaponSlot[bot] = wantSlot;
}
// Drops the bots current target entirely and goes idle. check_team runs
// every 2 seconds and will hand it a fresh target (possibly the same player,
// but with a brand-new anchor and clean strike counters).
void GiveUpOnTarget(int bot)
{
int slot = specific_bot_player[bot];
if (slot != -1)
{
target_client[slot] = -1;
}
g_BotState[bot] = BOT_STATE_IDLE;
}
public Action OnPlayerRunCmd(int client, int& buttons, int& impulse, float vel[3], float angles[3], int& weapon, int& subtype, int& cmdnum, int& tickcount, int& seed, int mouse[2])
@ -154,44 +344,228 @@ public Action OnPlayerRunCmd(int client, int& buttons, int& impulse, float vel[3
return Plugin_Continue;
}
int target = target_client[specific_bot_player[client]];
// Read from the slot that is DelayTicks behind current write head
int readSlot = (g_WriteHead[target] - g_DelayTicks + BUFFER_SIZE) % BUFFER_SIZE;
float now = GetGameTime();
buttons = g_Buffer[target][readSlot].buttons;
impulse = g_Buffer[target][readSlot].impulse;
vel = g_Buffer[target][readSlot].vel;
angles = g_Buffer[target][readSlot].angles;
if (g_BotState[client] == BOT_STATE_APPROACH)
{
float botPos[3];
GetClientAbsOrigin(client, botPos);
// Anti-stuck: if the bot has barely moved since the last check, its
// wedged on geometry. First strike: re-anchor to the targets CURRENT
// position (theyve moved on; the fresh anchor may have a clear path)
// and hop to clear small ledges. Second strike: give up on this
// target and let check_team assign a new one.
if (now - g_StuckCheckTime[client] >= STUCK_CHECK_INTERVAL)
{
if (GetVectorDistance(botPos, g_StuckCheckPos[client]) < STUCK_MOVE_EPSILON)
{
g_StuckStrikes[client]++;
if (g_StuckStrikes[client] >= MAX_STUCK_STRIKES)
{
GiveUpOnTarget(client);
return Plugin_Continue;
}
GetClientAbsOrigin(target, g_AnchorPos[client]);
g_ReadHead[client] = g_WriteHead[target];
g_WantJump[client] = true;
}
else
{
g_StuckStrikes[client] = 0;
}
g_StuckCheckPos[client] = botPos;
g_StuckCheckTime[client] = now;
}
// Arrival is judged horizontally with generous height slack. A pure
// 3D radius made anchors on other floors unreachable forever.
if (GetHorizontalDistance(botPos, g_AnchorPos[client]) <= ARRIVE_RADIUS
&& FloatAbs(botPos[2] - g_AnchorPos[client][2]) <= ARRIVE_Z_SLACK)
{
// arrived - start draining whatever the target did while we were walking here
g_BotState[client] = BOT_STATE_MIMIC;
g_StuckStrikes[client] = 0;
// fall through into mimic logic below on this same tick
}
else
{
ApplyApproachMovement(client, g_AnchorPos[client], buttons, impulse, vel, angles);
return Plugin_Changed;
}
}
if (g_BotState[client] != BOT_STATE_MIMIC)
{
return Plugin_Continue;
}
// Drift check: compare where the bot actually is against where the
// target actually was for the frame were about to play. If its too
// far off (missed a knockback, got stuck on geometry mid-mimic, etc.),
// drop back into APPROACH toward the targets current live position
// instead of continuing to mimic from a wrong spot.
//
// Two guards against the approach<->mimic ping-pong loop:
// - a cooldown, so drift during the cooldown window just keeps
// mimicking instead of instantly re-approaching
// - a strike counter, so a target whose replay keeps desyncing on the
// same geometry gets dropped entirely instead of looping forever
if (g_ReadHead[client] != g_WriteHead[target])
{
int nextSlot = g_ReadHead[client];
float botPos[3];
GetClientAbsOrigin(client, botPos);
if (GetVectorDistance(botPos, g_Buffer[target][nextSlot].origin) > DESYNC_RADIUS
&& now - g_LastResyncTime[client] >= RESYNC_COOLDOWN)
{
g_ResyncStrikes[client]++;
if (g_ResyncStrikes[client] >= MAX_RESYNC_STRIKES)
{
GiveUpOnTarget(client);
return Plugin_Continue;
}
g_LastResyncTime[client] = now;
GetClientAbsOrigin(target, g_AnchorPos[client]);
g_BotState[client] = BOT_STATE_APPROACH;
g_ReadHead[client] = g_WriteHead[target]; // drop the stale backlog, start fresh once we arrive
// reset the stuck tracker for the fresh approach leg
g_StuckStrikes[client] = 0;
g_StuckCheckTime[client] = now;
GetClientAbsOrigin(client, g_StuckCheckPos[client]);
ApplyApproachMovement(client, g_AnchorPos[client], buttons, impulse, vel, angles);
return Plugin_Changed;
}
}
if (g_ReadHead[client] == g_WriteHead[target])
{
// fully drained - nothing new recorded yet, hold still until more frames arrive
return Plugin_Continue;
}
int slot = g_ReadHead[client];
buttons = g_Buffer[target][slot].buttons;
impulse = g_Buffer[target][slot].impulse;
vel = g_Buffer[target][slot].vel;
angles = g_Buffer[target][slot].angles;
SyncWeaponSlot(client, g_Buffer[target][slot].weaponSlot);
// "cleans them out after processing" - advancing the read head past this
// slot means it will never be handed out again, which is the queue
// equivalent of clearing it.
g_ReadHead[client] = (slot + 1) % BUFFER_SIZE;
return Plugin_Changed;
}
// Looks up which bot client (if any) currently occupies a given bot slot (1-4).
int GetBotClientForSlot(int slot)
{
for (int i = 1; i <= MaxClients; i++)
{
if (IsValidClient(i) && specific_bot_player[i] == slot)
{
return i;
}
}
return -1;
}
// Assigns `target` as the client that `bot` should mimic. Captures a single,
// fixed anchor point (the targets position right now) for the bot to walk
// to, and marks the point in the targets buffer where recording of the
// "walk-over" backlog begins. No teleport happens here.
void SetBotTarget(int bot, int target)
{
int slot = specific_bot_player[bot];
if (slot == -1)
{
return;
}
target_client[slot] = target;
g_LastWeaponSlot[bot] = 0; // force weapon re-evaluation once mimicking starts
// clean slate for the anti-loop/anti-stuck trackers
g_ResyncStrikes[bot] = 0;
g_StuckStrikes[bot] = 0;
g_LastResyncTime[bot] = 0.0;
g_StuckCheckTime[bot] = GetGameTime();
g_WantJump[bot] = false;
if (IsValidClient(bot))
{
GetClientAbsOrigin(bot, g_StuckCheckPos[bot]);
}
if (!IsValidClient(target))
{
g_BotState[bot] = BOT_STATE_IDLE;
return;
}
GetClientAbsOrigin(target, g_AnchorPos[bot]);
g_BotState[bot] = BOT_STATE_APPROACH;
g_ReadHead[bot] = g_WriteHead[target]; // everything the target does from now on gets replayed once we arrive
}
// Debug command: prints each bots state machine status so loops/stuck
// situations can be diagnosed live instead of guessed at.
public Action cmd_botstate(int client, int args)
{
static const char stateNames[][] = { "IDLE", "APPROACH", "MIMIC" };
bool found = false;
for (int i = 1; i <= MaxClients; i++)
{
if (!IsValidClient(i) || specific_bot_player[i] == -1)
{
continue;
}
found = true;
int slot = specific_bot_player[i];
int target = target_client[slot];
int backlog = 0;
char targetName[64];
strcopy(targetName, sizeof(targetName), "<none>");
if (IsValidClient(target))
{
GetClientName(target, targetName, sizeof(targetName));
backlog = (g_WriteHead[target] - g_ReadHead[i] + BUFFER_SIZE) % BUFFER_SIZE;
}
ReplyToCommand(client,
"[bot %d] %N | state=%s | target=%s | backlog=%d frames | resync_strikes=%d | stuck_strikes=%d",
slot, i, stateNames[g_BotState[i]], targetName, backlog,
g_ResyncStrikes[i], g_StuckStrikes[i]);
}
if (!found)
{
ReplyToCommand(client, "no autism bots currently connected");
}
return Plugin_Handled;
}
public void Event_RoundStart(Handle event, const char[] name, bool dontBroadcast)
{
target_client[1] = -1;
target_client[2] = -1;
target_client[3] = -1;
target_client[4] = -1;
chat_cooldown = false;
for (int i = 1; i <= MaxClients; i++)
{
if (IsValidClient(i) && IsPlayerAlive(i) && specific_bot_player[i] != -1)
{
for (int j = 1; j <= MaxClients; j++)
{
if (IsValidClient(j) && GetClientTeam(j) == GetClientTeam(i) && IsPlayerAlive(j) && GetClientIdleTime(j) < 20)
{
if (target_client[1] == j || target_client[2] == j || target_client[3] == j || target_client[4])
{
continue; //one of the autism bots already got this dude as their target.
}
float vec[3];
GetClientAbsOrigin(j, vec);
TeleportEntity(i, vec, NULL_VECTOR, NULL_VECTOR);
target_client[specific_bot_player[i]] = j;
break;
}
}
g_BotState[i] = BOT_STATE_IDLE;
g_LastWeaponSlot[i] = 0;
g_ResyncStrikes[i] = 0;
g_StuckStrikes[i] = 0;
g_WantJump[i] = false;
}
}
}
@ -545,7 +919,15 @@ public Action check_team(Handle timer, any data)
{
if (IsPlayerAlive(client))
{
target_client[specific_bot_player[client]] = GetClosestClient_option1(client);
int newTarget = GetClosestClient_option1(client);
if (newTarget != target_client[specific_bot_player[client]])
{
// only reset the walk-over/mimic state when the target
// actually changes - otherwise a bot re-targeting the
// same player every 2 seconds would keep getting yanked
// back into APPROACH state
SetBotTarget(client, newTarget);
}
}
}
}
@ -603,7 +985,7 @@ public int GetClosestClient_option1(int autism_client)
{
if (IsValidClient(i) && !IsFakeClient(i) && IsPlayerAlive(i) && i != autism_client)
{
if (GetClientIdleTime(i) > 20 || GetClientTeam(autism_client) != GetClientTeam(i))
if (GetClientIdleTime(i) > 3 || GetClientTeam(autism_client) != GetClientTeam(i))
{
continue;
}
@ -636,11 +1018,19 @@ public float get_power_distance(int target_player, float pos[3])
public void OnClientPutInServer(int client)
{
specific_bot_player[client] = -1;
g_BotState[client] = BOT_STATE_IDLE;
g_LastWeaponSlot[client] = 0;
g_NextWeaponCheck[client] = 0.0; // force an immediate recheck rather than waiting out a stale timer
g_ResyncStrikes[client] = 0;
g_StuckStrikes[client] = 0;
g_WantJump[client] = false;
}
public void OnClientPostAdminCheck(int client)
{
specific_bot_player[client] = -1;
g_BotState[client] = BOT_STATE_IDLE;
g_LastWeaponSlot[client] = 0;
is_autism_bot1(client);
is_autism_bot2(client);
is_autism_bot3(client);
@ -693,6 +1083,24 @@ stock void connect_socket()
public void OnClientDisconnect(int client)
{
specific_bot_player[client] = -1;
g_BotState[client] = BOT_STATE_IDLE;
g_LastWeaponSlot[client] = 0;
// if this client was somebodys target, put that bot back to idle so it
// doesnt keep trying to read/approach a client thats gone
for (int slot = 1; slot <= 4; slot++)
{
if (target_client[slot] == client)
{
target_client[slot] = -1;
int bot = GetBotClientForSlot(slot);
if (bot != -1)
{
g_BotState[bot] = BOT_STATE_IDLE;
g_LastWeaponSlot[bot] = 0;
}
}
}
}
//Socket callback
@ -717,3 +1125,4 @@ public Action TimerConnect(Handle timer, any arg)
connect_socket();
return Plugin_Handled;
}