From 07d344e730d8a15770e55c543017926f1c28267d Mon Sep 17 00:00:00 2001 From: jenz Date: Tue, 14 Jul 2026 11:36:55 +0200 Subject: [PATCH] adding some states again for following or mimicking movement, also with coordinates to know if coming too far off course --- .../scripting/autism_bot_simulate_movement.sp | 469 ++++++++++++++++-- 1 file changed, 439 insertions(+), 30 deletions(-) diff --git a/AutismBotIngame/scripting/autism_bot_simulate_movement.sp b/AutismBotIngame/scripting/autism_bot_simulate_movement.sp index f777d69..6bc9d21 100755 --- a/AutismBotIngame/scripting/autism_bot_simulate_movement.sp +++ b/AutismBotIngame/scripting/autism_bot_simulate_movement.sp @@ -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 #include #include #include +#include #include #include #include #include +#include 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), ""); + 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; } +