summaryrefslogtreecommitdiffstats
path: root/src/esp.cpp
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-27 15:22:19 +0100
committersalmonik <salmonik@tuta.io>2026-10-27 15:22:19 +0100
commit27c9ad766e1b1333922b4c14cbde9d9b77c8dab0 (patch)
tree7c758c0d431a389cc3826495b4c6d20ca28c6530 /src/esp.cpp
parentMelee killbot: 360-degree crowbar silent aim (diff)
downloadhl2h-27c9ad766e1b1333922b4c14cbde9d9b77c8dab0.tar.gz
hl2h-27c9ad766e1b1333922b4c14cbde9d9b77c8dab0.zip
Spread control: client-seed no-spread
HL2's CBaseEntity::FireBullets seeds its RNG from the usercmd random_seed (m_bUseServerRandomSeed is false for HL2 weapons), so the client can select the 8-bit seed that puts CShotManipulator::ApplySpread closest to the center. - New feature Spread control -> Active / No spread (both default on). - Precompute the best seed for a single shot and for the shotgun's 8-shot primary (first pellet accurate) and 12-shot secondary windows using the engine's own RandomSeed/RandomFloat from libvstdlib. - Set cmd->random_seed on attack commands; note_modified repairs the CRC. - The random cone offset is neutralized, but the cone size, the pistol accuracy penalty, server view punch and any damage falloff remain server-side; stock HL2 has no distance damage falloff.
Diffstat (limited to '')
-rw-r--r--src/esp.cpp136
1 files changed, 136 insertions, 0 deletions
diff --git a/src/esp.cpp b/src/esp.cpp
index 47df57d..0117d27 100644
--- a/src/esp.cpp
+++ b/src/esp.cpp
@@ -1042,6 +1042,142 @@ bool melee_aimbot_apply(CUserCmd *cmd) {
1042 return false; 1042 return false;
1043} 1043}
1044 1044
1045// ---------------------------------------------------------------------------
1046// weapon spread control
1047// ---------------------------------------------------------------------------
1048//
1049// HL2's CBaseEntity::FireBullets uses CBaseEntity::GetPredictionRandomSeed()
1050// (the client usercmd's random_seed) unless a weapon opts into
1051// m_bUseServerRandomSeed. HL2 weapons do not, so the client can choose the
1052// 8-bit seed and with it the random cone offset. CShotManipulator::ApplySpread
1053// draws four uniform floats per attempt; we precompute the best seed for one
1054// shot and for the shotgun's pellet windows with the engine's own RNG.
1055
1056static bool g_spread_ready = false;
1057static int g_spread_seed1 = 0;
1058static int g_spread_seed8 = 0; // first pellet exact, 7 spread
1059static int g_spread_seed12 = 0; // all 12 spread
1060
1061static void spread_compute() {
1062 using SeedFn = void (*)(int);
1063 using FloatFn = float (*)(float, float);
1064 SeedFn seed = (SeedFn)dlsym(RTLD_DEFAULT, "RandomSeed");
1065 FloatFn rf = (FloatFn)dlsym(RTLD_DEFAULT, "RandomFloat");
1066 if (!seed || !rf) {
1067 size_t slash = g_engine.path.find_last_of('/');
1068 if (slash != std::string::npos) {
1069 std::string path = g_engine.path.substr(0, slash + 1) + "libvstdlib.so";
1070 void *h = dlopen(path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL);
1071 if (!h)
1072 h = dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL);
1073 if (h) {
1074 seed = (SeedFn)dlsym(h, "RandomSeed");
1075 rf = (FloatFn)dlsym(h, "RandomFloat");
1076 }
1077 }
1078 }
1079 if (!seed || !rf) {
1080 logf("spread: RandomSeed/RandomFloat unavailable");
1081 return;
1082 }
1083
1084 auto draw = [&](int s, float &x, float &y) {
1085 seed(s);
1086 int it = 0;
1087 do {
1088 // CShotManipulator::ApplySpread with the default bias of 1.0:
1089 // shotBias = 1.0, flatness = 0.5.
1090 x = rf(-1.0f, 1.0f) * 0.5f + rf(-1.0f, 1.0f) * 0.5f;
1091 y = rf(-1.0f, 1.0f) * 0.5f + rf(-1.0f, 1.0f) * 0.5f;
1092 it++;
1093 } while (x * x + y * y > 1.0f && it < 64);
1094 };
1095
1096 float best1 = 1e9f;
1097 for (int s = 0; s < 256; s++) {
1098 float x, y;
1099 draw(s, x, y);
1100 float r2 = x * x + y * y;
1101 if (r2 < best1) {
1102 best1 = r2;
1103 g_spread_seed1 = s;
1104 }
1105 }
1106
1107 auto window = [&](int n, int first) {
1108 int best_s = 0;
1109 float best = 1e9f;
1110 for (int s = 0; s < 256; s++) {
1111 float score = 0.0f;
1112 for (int i = first; i < n; i++) {
1113 float x, y;
1114 draw(s + i, x, y);
1115 score += x * x + y * y;
1116 }
1117 if (score < best) {
1118 best = score;
1119 best_s = s;
1120 }
1121 }
1122 return best_s;
1123 };
1124 g_spread_seed8 = window(8, 1);
1125 g_spread_seed12 = window(12, 0);
1126 seed(0);
1127
1128 logf("spread: seeds single=%d shotgun8=%d shotgun12=%d", g_spread_seed1,
1129 g_spread_seed8, g_spread_seed12);
1130 g_spread_ready = true;
1131}
1132
1133static bool active_weapon_is_shotgun(void *local_ent) {
1134 if (!local_ent || g_net.activeweapon < 0)
1135 return false;
1136 uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon);
1137 if (h == 0xFFFFFFFFu)
1138 return false;
1139 int idx = (int)(h & 0xFFFF);
1140 if (idx <= 0 || idx > 4096)
1141 return false;
1142 void *wep = entitylist_get(idx);
1143 void *net = entity_networkable(idx);
1144 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
1145 const char *model = wep ? entity_model_name(wep) : nullptr;
1146 return (cls && strstr(cls, "Shotgun")) ||
1147 (model && strstr(model, "shotgun"));
1148}
1149
1150bool spread_control_apply(CUserCmd *cmd, void *local_ent) {
1151 if (!feature_active(FEAT_SPREAD) || !spread_no_spread_enabled())
1152 return false;
1153 if (!(cmd->buttons & (IN_ATTACK | IN_ATTACK2)))
1154 return false;
1155 if (!g_spread_ready) {
1156 static bool tried = false;
1157 if (!tried) {
1158 tried = true;
1159 spread_compute();
1160 }
1161 }
1162 if (!g_spread_ready)
1163 return false;
1164
1165 bool shotgun = active_weapon_is_shotgun(local_ent);
1166 int seed = g_spread_seed1;
1167 if (shotgun)
1168 seed = (cmd->buttons & IN_ATTACK2) ? g_spread_seed12 : g_spread_seed8;
1169 if (cmd->random_seed == seed)
1170 return false;
1171 cmd->random_seed = seed;
1172
1173 static int logs = 0;
1174 if (logs < 10) {
1175 logs++;
1176 logf("spread: seed=%d (%s)", seed, shotgun ? "shotgun" : "single");
1177 }
1178 return true;
1179}
1180
1045bool triggerbot_apply(CUserCmd *cmd) { 1181bool triggerbot_apply(CUserCmd *cmd) {
1046 if (!g_iface.enginetrace || !g_iface.entitylist) 1182 if (!g_iface.enginetrace || !g_iface.entitylist)
1047 return false; 1183 return false;