Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
b8d94b8
Update README
ed0ard Mar 12, 2026
9e667ef
Update CounterStrikeSharp.API package version
ed0ard Mar 12, 2026
a2ae61c
Updated the signatures for HasVisitedEnemySpawn and GameState_Reset
ed0ard Mar 12, 2026
d0f8599
Updated the signatures for HasVisitedEnemySpawn and GameState_Reset f…
ed0ard Mar 14, 2026
e5d5c95
Update README.md
ed0ard Mar 14, 2026
a4470d5
Strips knives from bots
ed0ard Mar 14, 2026
9379d60
Update README.md
ed0ard Mar 14, 2026
25d4098
Updated CounterStrikeSharp version
ed0ard Mar 14, 2026
7e1fe8e
Update README.md
ed0ard Mar 14, 2026
2d48e3e
Bring the knives back to bots; Keeps them from switching to their kni…
ed0ard Mar 20, 2026
7b51c39
Update README.md
ed0ard Mar 20, 2026
b4c392c
Bots can now buy on the first rounds and after side switches. Same in…
ed0ard Mar 21, 2026
fef5896
Update README.md
ed0ard Mar 21, 2026
5a277c9
Fixed a bug that would cause bots can't move in Deathmatch and Armsrace
ed0ard Mar 21, 2026
febaa1f
Fixed a bug that caused bots to keep attempting to buy and making noise
ed0ard Mar 22, 2026
67d43ac
Update README.md
ed0ard Mar 22, 2026
968b1c5
Make bots more sentitive with noise
ed0ard Mar 23, 2026
bfb0bf5
version 1.5.2
ed0ard Mar 23, 2026
bfe8c9c
Allowed taking damage to interrupt defusing
ed0ard Mar 24, 2026
0888ae5
Allowed taking damage to interrupt defusing
ed0ard Mar 24, 2026
fbd88b6
Allowed bots to spray at all ranges like human players; Enabled bots …
ed0ard Mar 25, 2026
041e58a
Allowed bots to spray at all ranges like human players; Enabled bots …
ed0ard Mar 25, 2026
f1c03a6
Update README.md
ed0ard Mar 25, 2026
5810430
Improved bots' movement comprehensively, especially in dodging and pe…
ed0ard Mar 28, 2026
286508b
Update README.md
ed0ard Mar 28, 2026
b1f0e7f
Refined bots' sensitivity to noise
ed0ard Mar 28, 2026
1f85e13
Update README.md
ed0ard Mar 28, 2026
1a98492
Updated License
ed0ard Mar 28, 2026
1f724bb
Enabled snpier bots to flick shot
ed0ard Apr 7, 2026
f9b21e5
Enabled snpier bots to flick shot
ed0ard Apr 7, 2026
b212ef8
Update README.md
ed0ard Apr 7, 2026
79880cd
Refactored the plugin structure and updated the signatures
ed0ard Apr 9, 2026
fc7b3eb
Updated CounterStrikeSharp.API package version
ed0ard Apr 9, 2026
d902234
Update README.md
ed0ard Apr 9, 2026
aebc603
Enhanced bots' awareness of their surroundings
ed0ard Apr 10, 2026
ad02359
Update README.md
ed0ard Apr 10, 2026
61d5cf6
Update README.md
ed0ard Apr 10, 2026
812e898
Reduced the effect of noise on bots
ed0ard Apr 11, 2026
b33078d
Reduced the effect of noise on bots
ed0ard Apr 11, 2026
aabc0e1
Update README.md
ed0ard Apr 11, 2026
a424f48
Made bots' spray more natural; Made sniper bots more decisive
ed0ard Apr 14, 2026
5814441
Made bots' spray more natural; Made sniper bots more decisive
ed0ard Apr 14, 2026
eee34a7
Update README.md
ed0ard Apr 14, 2026
3ff1aa4
Updated CounterStrikeSharp.API package version to 1.0.367
ed0ard Apr 23, 2026
63765bc
Fixed all the patches broken by the AG2 update
ed0ard Apr 23, 2026
971bb7d
Fixed all the patches broken by the AG2 update
ed0ard Apr 23, 2026
7db3bfa
Updated README.md
ed0ard Apr 23, 2026
dc6d109
Improved bots' hearing; Refined bomb-related logic; Fixed an anti-fla…
ed0ard May 14, 2026
2739426
Updated README.md
ed0ard May 14, 2026
45efdae
Updated all the broken patches
ed0ard May 16, 2026
20e3fc9
Updated README.md
ed0ard May 18, 2026
0cff808
Updated all the broken patches
ed0ard May 19, 2026
f0a2c9c
Improved bots' vision and attention
ed0ard May 22, 2026
ef1d1d2
Updated README.md
ed0ard May 22, 2026
9ab69ac
Allowed bots to spray everywhere; Recoil control follows
ed0ard May 22, 2026
4ac9dd0
Updated version to v1.8.0
ed0ard May 22, 2026
a5bd55d
Improved bot movement, especially peeking; increased bot aggression a…
ed0ard Jun 2, 2026
a21944f
Updated README.md
ed0ard Jun 2, 2026
a293527
Refined bots' defusing logic
ed0ard Jun 9, 2026
f8ce30e
Updated README.md
ed0ard Jun 9, 2026
094261b
Misaka17032 updated Linux Support
ed0ard Jun 11, 2026
08f8113
Updated README.md
ed0ard Jun 11, 2026
b39c9a3
Updated LICENSE
ed0ard Jun 28, 2026
07c5d8d
fix: Fixed 2 patches for the 2026-07-09 game update
XBribo Jul 10, 2026
01780db
refactor: Separated Windows and Linux patches
XBribo Jul 11, 2026
3fe34d6
fix: Fixed 4 patches
XBribo Jul 11, 2026
1aa0509
refactor: Removed Common Reference
XBribo Jul 11, 2026
1a1a2aa
Merge pull request #2 from XBribo/main
ed0ard Jul 11, 2026
4b99348
fix: Fixed 3 patches
XBribo Jul 11, 2026
888ec3a
fix: Fixed 3 patches
XBribo Jul 11, 2026
c7bfb55
Merge pull request #3 from XBribo/main
ed0ard Jul 11, 2026
d54e6fa
Added linux definitions (27 out of 40 patches is working)
AmagiReina Jul 11, 2026
b9e327a
Updated linux definitions (37 out of 40 patches is working)
AmagiReina Jul 11, 2026
bb762b8
Updated all remaining linux patch definitions
AmagiReina Jul 12, 2026
a06e572
Removed unnecessary comments
AmagiReina Jul 12, 2026
1ec46e3
Merge branch 'main' of https://github.com/AmagiReina/CS2-BotAI
AmagiReina Jul 12, 2026
12588dc
Updated BotAI version and author information
ed0ard Jul 12, 2026
d40c270
Updated all the patches for Windows
ed0ard Jul 12, 2026
99449e8
Updated README.md
ed0ard Jul 12, 2026
8090993
Merge branch 'ed0ard:main' into main
AmagiReina Jul 13, 2026
58d9fb5
Added fixes to patches that caused server crash
AmagiReina Jul 19, 2026
904e8ff
Merge pull request #4 from AmagiReina/main
ed0ard Jul 19, 2026
c60ba85
Removed native bot look drift
ed0ard Jul 26, 2026
41c5051
Updated version to 1.8.7
ed0ard Jul 26, 2026
71ed55b
Updated README.md
ed0ard Jul 26, 2026
0d482ef
fix(linux): revalidate patches + m_gameState 0x5100 for build 14172
mrc4tt Jul 27, 2026
69f72bf
Merge pull request #5 from mrc4tt/main
ed0ard Jul 27, 2026
323aa88
Differed m_gameState by platform
ed0ard Jul 30, 2026
1757475
Updated README.md
ed0ard Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
276 changes: 96 additions & 180 deletions BotAI.cs
Original file line number Diff line number Diff line change
@@ -1,268 +1,184 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
using Common;
using Microsoft.Extensions.Logging;
using System.Runtime.InteropServices;
using Common;
using CounterStrikeSharp.API.Modules.Utils;
using CounterStrikeSharp.API;

namespace BotAI;

public record PatchInfo(string Name, nint Address, List<byte> OriginalBytes);

public static class BotOffsets
{
public const int m_gameState = 0x6038; // CSGameState* in CCSBot (24632)
public const int m_isRoundOver = 0x08; // bool in CSGameState (8)
public const int m_bombState = 0x0C; // BombState in CSGameState (12)
// Differs by platform: Windows = 0x5128, Linux = 0x5100.
public static readonly int m_gameState =
RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? 0x5100 : 0x5128;
// Offsets inside CSGameState
public const int m_isRoundOver = 0x08;
public const int m_bombState = 0x0C;
public const int m_plantedBombsite = 0x68;

}

[MinimumApiVersion(304)]
public class BotAI : BasePlugin
{
public override string ModuleName => "Patches - Bot AI";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "K4ryuu";
public override string ModuleDescription => "Prevents bots from visiting enemy spawn at round start";
public override string ModuleVersion => "1.8.8";
public override string ModuleAuthor => "K4ryuu & Austin (updated by ed0ard & Misaka17032 & XBribo & AmagiReina)";
public override string ModuleDescription =>
"Improve and fix bots' behavior comprehensively";

private readonly List<PatchInfo> _appliedPatches = [];
private readonly bool _isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);


private readonly Dictionary<string, (string signature, string patch, string expectedOriginal)> _patchDefinitions = new()
{
["HasVisitedEnemySpawn"] = (
signature: "40 88 B7 05 05 00 00", // mov BYTE PTR [rdi+0x505], sil
patch: "C6 87 05 05 00 00 01", // mov BYTE PTR [rdi+0x505], 0x1
expectedOriginal: "40 88 B7 05 05 00 00" // Expected original bytes
),
["GameState_Reset"] = (
signature: "44 89 77 ? F3", // mov [rdi+?], r14d
patch: "0F 1F 40 00", // 4-byte NOP
expectedOriginal: "44 89 77 0C" // Expected original bytes (without wildcard)
),
["Idle_IsSafeAlwaysFalse"] = (
signature: "74 28 33 D2 48 8B CE E8 ? ? ? ? 84 C0 75 1A",
patch: "EB 28", // JZ short +28 -> JMP short +28
expectedOriginal: "74 28" // Check that there is actually a JZ at the address
),
["EscapeFromFlames_OnEnter_NoEquipKnife"] = (
signature: "E8 ? ? ? ? F3 0F 10 46 ? 0F 2E C7 48 89 7E ? 7A ? 74 ? BA ? ? ? ? 48 8D 4E ? 41 B8 ? ? ? ? E8 ? ? ? ? F3 0F 11 7E ? F3 0F 10 46 ? 0F 2E C6 7A ? 74 ? BA ? ? ? ? 48 8D 4E ? 41 B8 ? ? ? ? E8 ? ? ? ? C7 46 ? ? ? ? ? 48 8B 5C 24 ? 48 8B 74 24 ? 0F 28 74 24 ? 0F 28 7C 24 ? 44 0F 28 44 24 ? 48 83 C4 ? 5F C3 CC CC CC CC CC CC CC CC 40 53",
patch: "90 90 90 90 90",
expectedOriginal: "E8 ? ? ? ?"
)
};

public override void Load(bool hotReload)
{
Logger.LogInformation("Bot AI Patches loading...");
var patchDefinitions = _isLinux ? LinuxPatchDefinitions.All : WindowsPatchDefinitions.All;

foreach (var patchName in _patchDefinitions.Keys)
foreach (var name in patchDefinitions.Keys)
{
if (ApplyPatch(patchName))
{
Logger.LogInformation($"{patchName} patch applied successfully!");
}
else
{
Logger.LogError($"Failed to apply {patchName} patch!");
}
if (ApplyPatch(name, _isLinux)) Logger.LogInformation($"{name}: applied.");
else Logger.LogError($"{name}: FAILED.");
}

RegisterEventHandler<EventPlayerSpawn>((@event, info) =>
{
var player = @event.Userid;
if (player?.IsValid != true || !player.IsBot)
return HookResult.Continue;
if (player?.IsValid != true || !player.IsBot) return HookResult.Continue;

var pawn = player.PlayerPawn.Value;
if (pawn?.IsValid != true || player.Team <= CsTeam.Spectator || !pawn.BotAllowActive)
if (pawn?.IsValid != true
|| player.Team <= CsTeam.Spectator
|| !pawn.BotAllowActive)
return HookResult.Continue;

var gameRules = Utilities.FindAllEntitiesByDesignerName<CCSGameRulesProxy>("cs_gamerules").FirstOrDefault()?.GameRules;
if (gameRules == null || gameRules.BombPlanted)
return HookResult.Continue;
var gameRules = Utilities
.FindAllEntitiesByDesignerName<CCSGameRulesProxy>("cs_gamerules")
.FirstOrDefault()?.GameRules;

if (gameRules == null || gameRules.BombPlanted) return HookResult.Continue;

UpdateBotBombState(pawn, player.PlayerName);
return HookResult.Continue;
});

Logger.LogInformation($"Applied {_appliedPatches.Count}/{_patchDefinitions.Count} patches.");
}

private bool UpdateBotBombState(CCSPlayerPawn pawn, string playerName)
{
try
{
if (pawn?.Bot?.Handle == null || pawn.Bot.Handle == nint.Zero)
return false;

nint botPtr = pawn.Bot.Handle;
if (!IsValidMemoryAddress(botPtr))
return false;

nint gameStatePtr = botPtr + BotOffsets.m_gameState;
if (!IsValidMemoryAddress(gameStatePtr))
return false;

bool isRoundOver = Marshal.ReadByte(gameStatePtr + BotOffsets.m_isRoundOver) != 0;
if (isRoundOver)
return true;

nint bombStateAddr = gameStatePtr + BotOffsets.m_bombState;
if (!IsValidMemoryAddress(bombStateAddr))
return false;

if (!MemoryPatch.SetMemAccess(bombStateAddr, sizeof(int)))
return false;

int currentBombState = Marshal.ReadInt32(bombStateAddr);
if (currentBombState != 0)
{
Marshal.WriteInt32(bombStateAddr, 0);
}

return true;
}
catch (Exception ex)
{
Logger.LogError($"Failed to update bot bomb state for {playerName}: {ex.Message}");
return false;
}
}

private static bool IsValidMemoryAddress(nint address)
{
if (address == nint.Zero)
return false;

try
{
Marshal.ReadByte(address);
return true;
}
catch
{
return false;
}
Logger.LogInformation($"Applied {_appliedPatches.Count}/{patchDefinitions.Count} patches.");
}

public override void Unload(bool hotReload)
{
Logger.LogInformation("Bot AI Patches unloading...");

foreach (var patch in _appliedPatches)
{
RestorePatch(patch);
}

foreach (var patch in _appliedPatches) RestorePatch(patch);
_appliedPatches.Clear();
Logger.LogInformation("All patches restored.");
}

private bool ApplyPatch(string name)
// ── Patch machinery ───────────────────────────────────────────────────────

private bool ApplyPatch(string name, bool linux = false)
{
try
{
if (!_patchDefinitions.TryGetValue(name, out var patchDef))
return false;
var patchDefinitions = linux ? LinuxPatchDefinitions.All : WindowsPatchDefinitions.All;
if (!patchDefinitions.TryGetValue(name, out var def)) return false;

string modulePath = GameUtils.GetModulePath("server");
nint address = NativeAPI.FindSignature(modulePath, patchDef.signature);
if (address == 0)
return false;
nint sigAddr = NativeAPI.FindSignature(GameUtils.GetModulePath("server"), def.signature);
if (sigAddr == 0) { Logger.LogError($"'{name}': signature not found."); return false; }

var patchBytes = ParseHexString(patchDef.patch);
if (patchBytes.Count == 0 || !IsValidMemoryAddress(address))
return false;
nint addr = sigAddr + def.patchOffset;
var patchBytes = ParseHex(def.patch);
if (patchBytes.Count == 0 || !IsValid(addr)) return false;

var originalBytes = new List<byte>();
var origBytes = new List<byte>();
for (int i = 0; i < patchBytes.Count; i++)
{
originalBytes.Add(Marshal.ReadByte(address, i));
}
origBytes.Add(Marshal.ReadByte(addr, i));

// Validate original bytes match expected pattern
if (!ValidateOriginalBytes(name, originalBytes, patchDef.expectedOriginal))
if (!ValidateOrig(name, origBytes, def.expectedOriginal))
{
Logger.LogError($"Original bytes validation failed for patch '{name}' - refusing to patch");
Logger.LogError($"'{name}': byte mismatch. Expected [{def.expectedOriginal}] " +
$"got [{string.Join(" ", origBytes.Select(b => $"{b:X2}"))}].");
return false;
}

if (!MemoryPatch.SetMemAccess(address, patchBytes.Count))
return false;

for (int i = 0; i < patchBytes.Count; i++)
{
Marshal.WriteByte(address, i, patchBytes[i]);
}
if (!MemoryPatch.SetMemAccess(addr, patchBytes.Count)) return false;
for (int i = 0; i < patchBytes.Count; i++) Marshal.WriteByte(addr, i, patchBytes[i]);

_appliedPatches.Add(new PatchInfo(name, address, originalBytes));
Logger.LogInformation($"Patch '{name}' applied at 0x{address:X} ({patchBytes.Count} bytes)");
_appliedPatches.Add(new PatchInfo(name, addr, origBytes));
Logger.LogInformation($"'{name}' patched at 0x{addr:X} ({patchBytes.Count} bytes).");
return true;
}
catch (Exception ex)
catch (Exception ex) { Logger.LogError($"'{name}': {ex.Message}"); return false; }
}

private void RestorePatch(PatchInfo p)
{
try
{
Logger.LogError($"Failed to apply patch '{name}': {ex.Message}");
return false;
if (!IsValid(p.Address)) return;
if (!MemoryPatch.SetMemAccess(p.Address, p.OriginalBytes.Count)) return;
for (int i = 0; i < p.OriginalBytes.Count; i++)
Marshal.WriteByte(p.Address, i, p.OriginalBytes[i]);
}
catch (Exception ex) { Logger.LogError($"Restore '{p.Name}': {ex.Message}"); }
}

private void RestorePatch(PatchInfo patch)
private bool ValidateOrig(string name, List<byte> actual, string expectedHex)
{
try
{
if (!IsValidMemoryAddress(patch.Address))
return;

if (!MemoryPatch.SetMemAccess(patch.Address, patch.OriginalBytes.Count))
return;

for (int i = 0; i < patch.OriginalBytes.Count; i++)
var tokens = expectedHex.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (actual.Count != tokens.Length) return false;
for (int i = 0; i < tokens.Length; i++)
{
Marshal.WriteByte(patch.Address, i, patch.OriginalBytes[i]);
if (tokens[i] == "?") continue;
if (actual[i] != Convert.ToByte(tokens[i], 16)) return false;
}
return true;
}
catch (Exception ex)
{
Logger.LogError($"Failed to restore patch '{patch.Name}': {ex.Message}");
}
catch { return false; }
}

private bool ValidateOriginalBytes(string patchName, List<byte> actualBytes, string expectedHex)
private static bool IsValid(nint addr)
{
if (addr == nint.Zero) return false;
try { Marshal.ReadByte(addr); return true; }
catch { return false; }
}

private static List<byte> ParseHex(string hex) =>
[.. hex.Split(' ', StringSplitOptions.RemoveEmptyEntries)
.Where(t => t != "?")
.Select(t => Convert.ToByte(t, 16))];

private bool UpdateBotBombState(CCSPlayerPawn pawn, string playerName)
{
try
{
var expectedTokens = expectedHex.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (actualBytes.Count != expectedTokens.Length)
{
Logger.LogWarning($"Patch '{patchName}': byte count mismatch - expected {expectedTokens.Length}, got {actualBytes.Count}");
return false;
}
if (pawn?.Bot?.Handle is not { } handle || handle == nint.Zero) return false;
if (!IsValid(handle)) return false;

for (int i = 0; i < expectedTokens.Length; i++)
{
if (expectedTokens[i] == "?")
continue; // wildcard → skip check

byte expectedByte = Convert.ToByte(expectedTokens[i], 16);
if (actualBytes[i] != expectedByte)
{
Logger.LogWarning($"Patch '{patchName}': byte mismatch at offset {i} - expected {expectedByte:X2}, got {actualBytes[i]:X2}");
return false;
}
}
nint gsPtr = handle + BotOffsets.m_gameState;
if (!IsValid(gsPtr)) return false;
if (Marshal.ReadByte(gsPtr + BotOffsets.m_isRoundOver) != 0) return true;

Logger.LogInformation($"Patch '{patchName}': original bytes validated successfully");
nint bombAddr = gsPtr + BotOffsets.m_bombState;
if (!IsValid(bombAddr)) return false;
if (!MemoryPatch.SetMemAccess(bombAddr, sizeof(int))) return false;
if (Marshal.ReadInt32(bombAddr) != 0) Marshal.WriteInt32(bombAddr, 0);
return true;
}
catch (Exception ex)
{
Logger.LogError($"Patch '{patchName}': validation error - {ex.Message}");
Logger.LogError($"UpdateBotBombState({playerName}): {ex.Message}");
return false;
}
}

private static List<byte> ParseHexString(string hexString)
{
return [.. hexString.Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(hex => Convert.ToByte(hex, 16))];
}
}
}
Loading